The fastest way to understand “everything is a plugin” is to write one. The contract is small enough to hold in your head — smaller than most HTTP frameworks’ middleware — and everything else is convention on top of it.
The whole contract
A plugin is a TypeScript module that exports a name and an apply function:
import type { Context } from '@deepseek-ai/cordis'
export const name = 'hello-plugin'
export function apply(ctx: Context) {
// Required dependencies are ready before apply runs.
console.log('[hello-plugin] plugin loaded!')
}
That’s it. The framework loads the module, calls apply with a context object, and you register capabilities through that ctx — tools, event listeners, timers, UI contributions. The context is the entire API surface; the architecture’s promise is that this surface has no second class.
One detail worth internalizing early: everything registered through ctx cleans itself up on unload. Event listeners, tools, timers — you never write removeListener or clearInterval for them. For resources that need manual teardown (a network connection, a child process), there’s ctx.effect():
export function apply(ctx: Context) {
ctx.effect(() => {
const timer = setInterval(() => console.log('heartbeat'), 5000)
return () => clearInterval(timer) // runs on unload
})
}
This is why plugins compose: nothing leaks when they leave.
Develop it live: the patch layer
You don’t install your plugin during development — you patch it in. Working from a harness checkout (the tutorial’s assumption), create a scratch project and an overlay file:
mkdir -p scratch-plugin/src
# scratch-plugin/cordis.yml
- insert:
- id: hello
name: '/absolute/path/to/deepseek-harness/scratch-plugin/src/my-plugin.ts'
The path must be absolute — patch files contribute configuration only; they don’t change how the loader resolves modules. Then launch with the overlay:
pnpm dsh web --patch ./scratch-plugin/cordis.yml
Open http://127.0.0.1:3080 and watch the terminal: [hello-plugin] plugin loaded!. You are now developing against the live Web UI with your plugin mounted. This insert/patch mechanism is not a debugging hack — as the next section shows, it is the distribution format.
Package it: the bundle
To distribute, turn the plugin into a bundle — an npm package whose package.json declares a dsh.bundle manifest:
hello-plugin/
├── package.json # declares dsh.bundle
├── cordis.patch.yml # the layer applied when a profile lists this bundle
└── index.js # plugin modules the patch rows reference
{
"name": "dsh-hello-plugin",
"version": "0.1.0",
"type": "module",
"main": "index.js",
"files": ["index.js", "cordis.patch.yml"],
"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }
}
Read the manifest as answering one question — what does this package contribute? — with the answer being: a patch file that inserts or overrides plugin rows.
Two nouns, two manifests
The system runs on a deliberate two-word vocabulary, and conflating them is the classic beginner mistake:
- A bundle is what you write and publish: an npm package declaring
dsh.bundle, contributing one config layer. - A profile is what a user launches: a directory under
$DSH_HOME/profiles/<name>declaringdsh.profile, describing which bundles, in what order, compose the launched configuration.
Layer order decides the final merged configuration — which is why a profile is an ordered composition rather than a set. Users install your bundle into a profile with dsh plugin add and launch with dsh --profile <name>. Nothing is both a bundle and a profile.
If you’ve used the Web UI, you’ve been running a profile all along — installing plugins adds bundles to it.
Ship it
- Publish the npm package. The
filesarray must include the patch file — forgetting it is the classic broken-install bug. - Tag the repo with the
dsh-pluginGitHub topic — it’s the ecosystem’s discovery signal. - Submit a line to the community awesome list: one line under the right category, both README languages.
One warning to carry with you, verbatim in spirit from the community list: installing a plugin runs third-party code with your permissions. Tool approvals don’t sandbox plugin code. Review sources before installing; publish code you’d let a stranger review. The ecosystem’s speed is a direct consequence of this trust model — honor it.
Where to go deeper
The official tutorial continues into configuration and tool registration (docs/user/develop/basic/), and the framework docs cover events and lifecycle. For a working reference, the community plugin template is a cleaned-up starting point — clone it, rename, ship.
Your first plugin is now fifteen lines away from a thing people can install. The architecture did that on purpose.