Skip to content

CH 18 · Your First Plugin: hello-plugin

Word count~2,990 wordsTime~15 minPrereqCH 08 (Plugin Tree), CH 17 (Plugin Installation)LevelReproducible

Chapter Goal

In CH 17 you've learned to install plugins made by others. This chapter starts writing your own — the goal is the minimum: write a hello-plugin and actually have it loaded by dsh. You don't write tools, you don't touch the interface, you only verify one thing: the plugin you write can be discovered, loaded, and run by dsh. Once this step is connected, the cool stuff in CH 19 to CH 23 (tools, hooks, UI, publishing) all grow on top of it.

First, a mindset: plugins are not mysterious. CH 08 said "everything is a plugin", the reverse is: if you want dsh to have one more capability, write a small module that exports an apply function. That's the entire skeleton of a plugin.

Hands-on: Write a hello-plugin

In the workspace you want, create the directory (first cd to that directory, then run):

powershell
New-Item -ItemType Directory -Path "hello-plugin\src" -Force

Then create hello-plugin\src\hello-plugin.js, write:

js
export const name = 'hello-plugin'

export function apply(ctx) {
  console.log('[hello-plugin] plugin loaded!')
}

Just these two lines of core logic: when the plugin is loaded, print [hello-plugin] plugin loaded!. If it can print, that proves "your code was run by dsh" — that's the first milestone.

Here we use JS rather than the official TS example: the globally installed dsh doesn't have a built-in tsx runtime, and directly loading .ts will error; using .js needs no build, zero dependencies, runs in five minutes. When we write more complex plugins, we'll bring in TypeScript and the build chain (CH 20 expands on that).

Load It into dsh

Just having the file, dsh doesn't know to load it. We need an "overlay" to tell dsh: additionally load this plugin. Create hello-plugin\cordis.yml (the path in name below is an example, replace with your own directory, see notes after):

yaml
- insert:
    - id: hello
      name: 'file:///E:/software-workspace/DeepSeek%20harness%20demo/hello-plugin/src/hello-plugin.js'

Three notes:

  • name must be a full URL starting with file://, you cannot write E:\... or E:/.... On Windows, dsh's module loader only accepts the file:///E:/... form; writing the drive letter path directly will report Only URLs with a scheme in: file, data, and node are supported.
  • Spaces in the path must be encoded as %20. For example, if your directory is my work space, write it as my%20work%20space.
  • This is an absolute path. The patch file only contributes configuration, the module resolution root is still the profile directory, so local plugins must be written as full paths.

Run headless Once for Quick Verification

Don't touch the Web UI, use headless to quickly verify the plugin is actually loaded:

powershell
cd your-workspace-directory
dsh --profile headless --patch "./hello-plugin/cordis.yml" "Just reply: hi"

In the output you'll see these two lines:

text
[hello-plugin] plugin loaded!
hi

The first line is printed by your plugin at startup, the second is the model's reply after completing the task. See [hello-plugin] plugin loaded!, your first plugin is up and running.

Then Load It into the Web UI

Headless can verify, but the plugin is meant to be used in the Web UI. First stop the running dsh web (otherwise the port is occupied), then start with the patch:

powershell
dsh web --patch "./hello-plugin/cordis.yml"

Open http://127.0.0.1:3080, the terminal where dsh was started will also print [hello-plugin] plugin loaded!. hello-plugin has no UI effect for now, its only "output" is that log line — but this proves it has entered the web plugin tree, sitting alongside the members of the tree CH 08 talked about.

Look at the terminal output: the first line is the plugin load log printed at dsh startup, the next two lines are the Web UI's ready information.

Auto Cleanup on Unload

Everything you register with ctx (event listeners, tools, timers) will be automatically cleaned up by the framework when the plugin is unloaded; you don't need to manually removeListener or clearInterval. If you do have resources that need manual release (e.g. a network connection), use ctx.effect() to tell the framework how to clean up:

js
export function apply(ctx) {
  ctx.effect(() => {
    const timer = setInterval(() => {
      console.log('heartbeat')
    }, 5000)

    // The returned function runs when the plugin is unloaded
    return () => clearInterval(timer)
  })
}

The cleanup function returned by effect will be called at the moment the plugin is unloaded — this is the standard way dsh helps you manage resource lifecycle.

Declare Dependencies: inject

If your plugin needs other capabilities (e.g. tools, llm), declare inject, and the framework will ensure the dependencies are ready before loading your plugin:

js
export const name = 'my-tool-plugin'
export const inject = ['tools']

export function apply(ctx) {
  // Here ctx.tools is guaranteed to be available
  ctx.tools.register(/* ... */)
}

inject is the entry point for "service dependencies" in Cordis. We'll get familiar with it for now, and use it officially in CH 20 when writing tool plugins.

Three Plugin Forms

The apply function is the most common form, but plugins support three ways of writing (CH 19 will detail each, here's the overview):

FormWhat it looks likeWhen to use
Functionexport function apply(ctx) {}Default choice, what this tutorial uses
Objectexport default { name, apply(ctx) {} }When you want to carry some static metadata along
Classexport default class extends Service {}When you want to provide services to other plugins (CH 19 expands)

For now, just remember one line: function form solves 90% of needs, leave the service form for when "your plugin needs to be depended on by other plugins".

Let dsh Do It: One Prompt Does It

The above steps you walked through manually, all learned. But dsh itself is an Agent — it can do the job of writing plugins, and "it writes and verifies itself".

Send this prompt directly in the Web UI's input box:

text
In my current workspace, help me write a minimum hello-plugin plugin that dsh can load. First go read the official dsh plugin development docs, figure out how plugins should be written and loaded, then implement per the official spec, verify it's actually loaded, and finally tell me the result and where the files are.

You don't need to tell it any technical details — it will go read the official plugin dev docs itself, decide how to write, how to load, how to verify. You just watch it work, then open the file to check what it wrote. This is exactly the extension of "everything is a plugin": the job of writing plugins can also be done by an Agent assembled from plugins.

I actually ran it once, the top right shows the reference materials it listed and the files produced (package.json, index.js, cordis.patch.yml). After it finished writing, the right-side file panel directly shows the new hello-plugin directory in the workspace — exactly where the sidebar plugin installed in CH 17 shines, no need to switch to the file manager to check what it wrote.

Common Pitfalls

ProblemWhat's happeningHow to handle
Reports Only URLs with a scheme in: file...On Windows the path is written as a drive letter formChange name to a full URL like file:///E:/...
Reports file/module not foundSpaces in the path aren't encodedWrite spaces as %20
No response after patch and restart?Plugin path or yml is misspelledCheck the spellings of id and name, use dsh --profile web --patch ./hello-plugin/cordis.yml --dump-config to see if the plugin is in the config tree
Loading .ts directly errors?Global dsh has no built-in tsx runtimeFirst use .js to run without building, when TS is needed build to .js first then load
Port occupied, can't start?Previous dsh web is still runningStop the old process first, then start

What you learned in this chapter

You pass if you can complete the items below:

  • [ ] State the minimum form of a plugin: a module that exports an apply(ctx) function
  • [ ] Create a hello-plugin and declare it with a file:// URL in cordis.yml
  • [ ] Use dsh --profile headless --patch ... to quickly verify the plugin is loaded
  • [ ] Start web with --patch to bring the plugin into the Web UI's plugin tree
  • [ ] Know that ctx.effect() does resource cleanup, inject declares service dependencies, plugins have three forms: function / object / class

Open Source · MIT · Community Driven