kun2-5code / dsh-plugin-template

Listed

A starter template for DeepSeek Harness (dsh) plugins: six plugin shapes, fourteen browser UI surfaces, and demo slash commands in one installable bundle.

mainTool View source

Installation

npx -y @deepseek-ai/dsh plugin --profile web add github:kun2-5code/dsh-plugin-template

This installation command is an unverified starting point generated from the GitHub repository address.

README

Maintainer-authored documentation snapshot.

View on GitHub ↗
Commit 0b4fbdaSynced Aug 18, 2026

dsh-plugin-template

English | 简体中文

A ready-to-run, ready-to-install starter template for DeepSeek Harness (dsh) plugins. It demonstrates the six most common plugin shapes in one minimal installable bundle:

  • ConfigConfig interface + Schemastery schema; validation and defaults apply at load time (docs)
  • Toolctx.tools.register(defineTool(...)) registers a model-callable tool (docs)
  • Eventsctx.on / ctx.emit with declaration merging for typed events (docs)
  • Service — a class-form plugin that provides a service to other plugins (docs)
  • Hook — a tools/pre-execute permission gate that denies tool calls by config (docs)
  • Browser half (client)src/client/ registers browser UI on fourteen surfaces (index: docs/ui-surfaces.md): a clickable config card under Settings → Plugins → Configurable (writes greeting / maxRetries / verbose into the settings document, taking effect live; on a stock harness the card renders a read-only "not exposed" explainer instead of vanishing), a sidebar footer action button, an input dock strip above the composer, a shell overlay pill, a header utility badge, input tool-row buttons (left/right), a custom command row for /dsh-demo, a General settings row, a Plugins tab, a settings header action, a session header action, a composer dock strip, and per-message actions on AI replies — plus a presentResult render intent on the greet tool. Only the config card's data path is gated by the harness allowlist; the other thirteen are pure slot registrations that work on any harness.

The template follows the official bundle distribution model: the package declares dsh.bundle plus cordis.patch.yml, and dsh plugin add activates it as a config layer.

Directory structure

dsh-plugin-template/
├── package.json        # npm manifest + dsh.bundle / dsh.client declarations + prepare build script
├── tsconfig.json       # strict type-check configuration (tsc --noEmit)
├── tsdown.config.ts    # build config: Node library (lib/) + client bundle (lib/client.js), self-contained for git-install prepare
├── cordis.patch.yml    # bundle config layer: inserts the plugin rows
├── dev/cordis.yml      # local dev overlay (points at source; use with dsh web --patch; host half only)
├── docs/
│   └── ui-surfaces.md  # where the plugin registers UI + index of every slot (bilingual: ui-surfaces.zh.md)
├── src/
│   ├── index.ts        # main plugin: Config + tool + events + effect, config wired through the settings namespace
│   ├── commands.ts     # host half: demo slash commands /hello (replies world) and /dsh-demo (custom row)
│   ├── service.ts      # optional example: Service provider (disabled by default)
│   ├── hook.ts         # optional example: hook permission gate (disabled by default)
│   └── client/         # browser half: one module per UI surface (see docs/ui-surfaces.md)
│       ├── index.ts        # client entry: inject + apply, assembles the registrations
│       ├── constants.ts    # shared NAMESPACE + DEMO_COMMAND_NAME (keep in sync with package.json name / cordis.patch.yml)
│       ├── types.ts        # minimal structural types for ctx services (no @deepseek-ai client imports)
│       ├── styles.ts       # one injected <style> with all dtpl-* classes (theme tokens only)
│       ├── config-card.ts  # settings.plugin.item: the clickable config card (staged form + status states)
│       ├── sidebar-action.ts # sidebar.footer.action: sidebar-footer button
│       ├── input-dock.ts   # conversation.input.dock: strip above the composer (session-scoped)
│       ├── shell-overlay.ts # shell.overlay: frame-wide floating pill
│       ├── header-utilities.ts # conversation.session.header.utilities: right-aligned header badge
│       ├── input-left.ts   # conversation.input.left: tool-row control at the left end
│       ├── input-right.ts  # conversation.input.right: tool-row control next to send
│       ├── commandview.ts  # conversation.chat.commandview: custom row for /dsh-demo
│       ├── general-item.ts # settings.general.item: one preference row in Settings → General
│       ├── plugins-tab.ts  # settings.plugins.tab: a new tab in the Plugins page
│       ├── settings-action.ts # settings.action: button in the settings header
│       ├── header-actions.ts # conversation.session.header.actions: session-title action button
│       ├── composer-dock.ts  # conversation.composer.dock: strip under the composer card
│       └── assistant-actions.ts # conversation.chat.assistant-actions: per-message button
└── test/smoke.mjs      # smoke test on the build output (incl. settings wiring unit test)

Quick start

Install as a bundle (for users)

From any directory, install this package (or your fork) into a dsh profile:

# local directory
dsh plugin --profile demo add /path/to/dsh-plugin-template

# or directly from GitHub (replace with your own repo after forking)
dsh plugin --profile demo add github:you/dsh-plugin-template

A GitHub install pulls source; pnpm runs prepare (i.e. tsdown) to build lib/. On pnpm ≥10 the first git-dependency prepare is refused; add the package name pnpm prints to the profile's pnpm-workspace.yaml and retry:

allowBuilds:
  dsh-plugin-template: true

This allowlist authorizes executing that package's code at install time — only allow source you trust, and prefer pinning a commit: github:you/dsh-plugin-template#<sha>.

Verify the config layer and boot:

dsh --profile demo --dump-config   # should show a "# == dsh-plugin-template" layer
dsh --profile demo

Note: a custom-named profile (e.g. demo) contains only dsh-base and is headless (no GUI). For the Web GUI and the config card below, use the web profile (= dsh-base + dsh-web-app) — see testing the config card.

Local development (modifying the plugin)

From the root of a deepseek-harness source checkout, load this repo's source directly via an overlay (no install, no build):

pnpm dsh web --patch /absolute/path/to/dsh-plugin-template/dev/cordis.yml

Set name in dev/cordis.yml to this repo's absolute path on your machine, open http://127.0.0.1:3080, and ask the model to call the greet tool.

⚠️ A --patch overlay only loads the plugin's host half (module resolution cannot reach package-level declarations). To test the browser-half config card you must install into a profile (resolved by name: dsh-plugin-template) — see the next section.

Run the checks yourself during development:

pnpm install
pnpm typecheck
pnpm build
node test/smoke.mjs

If this repo sits INSIDE a deepseek-harness checkout (nested, as in the harness repo root), pnpm install is captured by the parent workspace and installs nothing here — the template is not a workspace member. Use pnpm install --ignore-workspace (pnpm ≥9.5) so the template installs its own node_modules from its own lockfile; or clone the template standalone.

Testing the config card (in the GUI)

The config card renders in the browser and depends on dsh's client-modules discovering the dsh.client declaration by package name, so the package must be installed into a profile (a --patch source path won't do):

# 1. Build (produces lib/index.js + lib/client.js)
cd /path/to/dsh-plugin-template && pnpm build

# 2. Install into the web profile (= dsh-base + dsh-web-app, full GUI)
dsh plugin --profile web add /path/to/dsh-plugin-template

# 3. Boot the web GUI (`dsh web` is equivalent to `dsh --profile web`)
dsh web

Open http://127.0.0.1:3080:

  1. Bottom-left SettingsPluginsConfigurable tab: you should see a dsh-plugin-template card. On a stock harness it renders a read-only "not exposed" status card (see below); after the one-line harness change it renders the editable greeting / maxRetries / verbose fields;
  2. Change greeting, click Save — the status line should confirm it takes effect immediately;
  3. Back in a session, ask the model to call the greet tool — you should see the new greeting (the host half reads the resolved namespace value live, no restart);
  4. The change lands in the settings document (settings.yaml under $DSH_HOME) and survives restarts; to restore a default, edit the field back or clear it in the card.

After editing the client half (src/client/), rerun pnpm build and refresh the page (the client bundle's rev query cache-busts).

The config card on a stock harness (no source edits)

The card is a browser plugin (src/client/config-card.ts) that binds the settings namespace dsh-plugin-template through the settingsScope service. It always renders — but on a stock harness it shows a read-only "not exposed" status card instead of editable fields. Why: dsh's web gateway serves settings namespaces only from an explicit allowlist (WEB_SETTINGS_NAMESPACES in packages/host/apiproxy/src/api-proxy.ts), and a namespace absent from it answers settings-not-exposed even when its owner plugin registered it. This is a harness-side registration decision (the same source comment calls moving the declaration into settings.register() "deferred work"), not a template defect: the built-in cards render because their namespaces (shell, agent-loop, …) are allowlisted, and there is currently no plugin-side channel to add one — the gateway's RPC map is compile-time fixed and no registration flag exists yet.

What works on a stock harness with zero edits:

  • the entire host half — the greet tool, events, the service, the hook gate — including live config reads: writes are only gated at the web RPC, the plugin itself reads the resolved namespace value on every execution;
  • the card slot itself: the card appears under Settings → Plugins → Configurable and explains the exposure state instead of vanishing silently.

To make the card editable, pick one:

  1. add 'dsh-plugin-template' to WEB_SETTINGS_NAMESPACES in packages/host/apiproxy/src/api-proxy.ts (one line; rebuild/restart the harness; lost when you update the checkout):
const WEB_SETTINGS_NAMESPACES = [
  'agent-loop', 'shell', 'locale', 'permission', 'ui-conversation', 'ui-theme', 'web-search-deepseek',
  'dsh-plugin-template',   // ← add this line
] as const
  1. wait for the harness's deferred work — moving the exposure declaration into settings.register() — which this template already targets by registering the namespace the canonical way (installSettingsSection).

Making it your own plugin

  1. Rename the package: keep package.json name (npm name, e.g. dsh-my-plugin), src/index.ts name, and cordis.patch.yml id/name consistent; when renaming the ./service subpath, update exports/files too. Renaming also touches browser-half spots: the client bundle id in tsdown.config.ts (__ModuleLoader__.load({ id })), NAMESPACE in src/client/constants.ts, and dsh.client in package.json (if you need inject).
  2. Change the Config interface and Config schema: anything two deployments should be able to set differently must be a config field (design principles). The config is wired to the settings namespace — does the GUI card auto-render a form from your schema? No: the card in src/client/config-card.ts is hand-written; add a field row there for each new config field.
  3. Register your tool in apply: ctx.tools.register(defineTool({...})); execute returns the canonical value declared by output.schema, and output.render is the pure function for model-visible rendering (tool reference).
  4. To provide capabilities to other plugins, enable src/service.ts and uncomment its row in cordis.patch.yml.
  5. Remember to declare module '@deepseek-ai/cordis' to merge Context / Events types — that is what keeps cross-package boundaries type-safe.
  6. To intercept tool calls, act as a permission gate, or respond to system hooks, enable src/hook.ts (uncomment its cordis.patch.yml row): ctx.on('tools/pre-execute', ...) returns { kind: 'deny', reason } or calls next() to allow (extension cookbook).
  7. Config reads: every read in src/index.ts goes through configSource() (the resolved settings-namespace value, falling back to the composition entry). If you derive registration-level facts from config in apply (e.g. register different tools by config), rebuild them in installSettingsSection's onChange rather than reading only at execution points (see bash-local).

How the browser half works

  • package.json declares dsh.client: { platform: "web" } + exports["./client"] → dsh's client-modules discovers it and loads lib/client.js as a browser plugin;
  • the client entry (src/client/index.ts) assembles one registration per UI surface — the config card (settings.plugin.item), the sidebar footer action (sidebar.footer.action), and the input dock (conversation.input.dock) — see the UI surfaces index;
  • the config card binds the dsh-plugin-template namespace via the settingsScope service: reads snapshots, stages drafts, and writes field-by-field on save (revision-fenced);
  • the host half (src/index.ts) registers the same namespace with installSettingsSection (the cordis.yml config is the base layer) and reads the resolved value lazily in the tool → saving takes effect immediately;
  • at runtime the client half depends only on react (provided by the browser platform module table); everything else goes through ctx services and no @deepseek-ai client package is imported — keep that discipline when editing the template.

Publishing

  • npm: pnpm publish (files already includes the build output and the patch; no extra steps)
  • tarball: pnpm pack, then dsh plugin --profile demo add ./dsh-plugin-template-0.1.0.tgz
  • git: dsh plugin add github:you/dsh-plugin-template (combined with the allowBuilds step above)

Project files and signals

Shown items are public repository signals detected in the directory snapshot.

TestsDetected
DocumentationDetected

Repository information

Language
TypeScript
License
MIT
Last updated
Aug 16, 2026, 4:19 PM

Install deliberately

Review source code, permissions, lifecycle hooks, dependencies and network access. Test untrusted plugins in an isolated environment.