--- name: dsh-plugin-development description: Use when developing, modifying, debugging, installing, or verifying a dsh plugin (DeepSeek Harness plugin / dsh插件 / DSH插件) — a Cordis plugin package that runs inside DeepSeek Harness profiles (web / headless / custom). Triggers include "开发dsh插件", "写个dsh插件", "给DSH写插件", "deepseek-harness插件", extending the DeepSeek Harness WebUI (theme, sidebar, settings pages, tool cards, chat UI), hooking host behavior (session events such as user/message or turn/end, LLM requests, dynamic tools), or wiring a plugin into cordis.patch.yml and a profile via pnpm / dsh plugin. Also use when debugging why a plugin fails to load, activate, or render. --- # Develop DeepSeek Harness (dsh) Plugins Build, install, and debug **dsh plugins**: npm packages whose Cordis plugin entries extend a DeepSeek Harness profile. This skill works from any agent runtime (Claude Code, Codex, Hermes, OpenClaw, …) using only file + shell tools — it does not assume DSH-internal tools. ## Ground truth first (golden rule) Never invent the API. Before writing code, read the real interfaces: - Installed packages: `.d.ts` files under the dsh installation. Find it with `node -p "require.resolve('@deepseek-ai/dsh/package.json')"` — if that throws `MODULE_NOT_FOUND` (global install not on the resolution path), use `readlink -f "$(which dsh)"` and walk up to the `@deepseek-ai/dsh` package dir, or `npm root -g`. The whole `@deepseek-ai/*` surface lives in that package's `node_modules`. Read `lib/types/**/*.d.ts` of the exact package (e.g. `@deepseek-ai/dsh-session`, `@deepseek-ai/dsh-llm`) for the service/event surface of the **installed version**. - Composed profile tree: `dsh web --dump-config` (with the user layer) and `dsh web --dump-default-config` (bundle layers only). Note: dumps rewrite the profile's `cordis.yml`, so on a read-only `$DSH_HOME` they fail with `EROFS` — then read the profile files and installed patch files directly. - CLI surface: `dsh --help`, `dsh --version`, `dsh web --help`. - Source repo: . Every package's `package.json` → `repository.directory` points to its source dir (e.g. `apps/cli`, `packages/bundle/base`, `packages/session/...`). ## Architecture in one screen - DSH is a **Cordis plugin framework**. A **profile** (e.g. `web`) is `$DSH_HOME/profiles/` whose `package.json` declares `dsh.profile.bundles` — an ordered list of plugin-bundle layers (web = `@deepseek-ai/dsh-base` + `@deepseek-ai/dsh-web-app`). - Each bundle contributes a patch (`cordis.patch.yml`). The composed tree is: bundle patches in `bundles` order → profile `cordis.patch.yml` → `$DSH_HOME/cordis.patch.yml` → `--patch` overlays. - **Rows are addressed by `id`; the last write wins per row; a patch replaces the row's whole `config` (no merging).** `- id: ` addresses an existing row; `- insert:` adds new rows. - A **dsh plugin** is any npm package a patch row's `name:` points at. It has a **host half** (Node: services, events, tools, side effects) and optionally a **browser half** (Web UI/theme) declared with `dsh.client` in its `package.json`. ## Standard workflow 1. **Locate install & target profile** — `echo $DSH_HOME`, `dsh --version`, `ls $DSH_HOME/profiles`. Confirm which profile the plugin targets (`web` for the GUI, `headless` for CLI runs). 2. **Inspect the composed tree** — `dsh web --dump-config`. Find the row id to add or override; copy the shape of a similar existing row. 3. **Decide host vs client** (table below). Prefer the side closest to the data owner; a pure-UI plugin keeps its host half a no-op carrier. 4. **Scaffold** from `templates/` in this skill. 5. **Write code against real surfaces** — read the installed `.d.ts` of every service/event you touch (`sessions`, `llm`, `timer`, `theme`, `slots`, …). See `references/services-events.md`. 6. **Register + install** — patch row (`- insert:` for a new row, `- id:` to override) + `dsh plugin --profile web add ` (local path, `file:`, `link:`, `github:user/repo#tag`, or registry name). 7. **Verify (loop)** — `dsh web --dump-config` shows your row; boot `dsh web`; client plugins appear as rows in WebUI 设置 → 插件管理; server behavior via a `dsh --profile headless "..."` smoke run or session logs. 8. **Debug** with the failure table below. ## Host vs Client | Want to … | Side | Key services / entry points | | --- | --- | --- | | Files, processes, network, timers, sessions, LLM, dynamic model tools | Host | `sessions`, `llm`, `agents`, `skills`, `subagents`, `settings`, `credentials`, `fs`, `sandbox`, `approval`, `timer` | | Hook conversation lifecycle (turn / step / message events) | Host | `ctx.on('turn/end' | 'user/message' | 'assistant/message' | 'step/start' | …)` | | Page theme, layout, sidebar, settings pages, tool cards, chat UI | Client | `theme`, `slots`, `modules`, `settings`, `remote` | | Host data shown on the client | Both | Host service/event + client slot; RPC via the typert gateway (`ctx.remote`, see references) | ## Plugin anatomy (minimal) Host-only entry (`lib/index.js`, ESM — `"type": "module"`): ```js import z from '@deepseek-ai/schemastery' export const name = 'example-turn-logger' export const inject = ['timer'] // hard dependencies only export const Config = z.object({ // validated against the patch `config:` greeting: z.string().default('hi'), }) export function apply(ctx, config) { ctx.on('turn/end', (payload) => { // verified payload (dsh 0.1.0-rc.6): { turn: number, reason: TurnEndReason } // re-check the installed @deepseek-ai/dsh-session/lib/types/types.d.ts first console.log(config.greeting, payload.turn, payload.reason.kind) }) ctx.effect(() => ctx.interval(() => console.log('tick'), 60_000)) } ``` The Loader also accepts a default-export **function/class** (a class constructed with `(ctx, config)` and calling `super(ctx, '')` publishes a Service) or a default-export **object** `{ apply, name?, inject?, Config? }`. Client half (`lib/client.js`, plain JS — no `import`/JSX/TS) plus the manifest in `package.json`: ```json "dsh": { "client": { "platform": "web", "inject": ["@deepseek-ai/dsh-client-ui-theme"], "immediately": true } }, "exports": { "./client": "./lib/client.js" } ``` ```js window.__ModuleLoader__.load({ id: 'example-client', factory: (require) => { var module = { exports: {} } var exports = module.exports exports.apply = (ctx) => { const theme = ctx.get('theme') if (theme !== undefined) { ctx.effect(() => theme.overrideTokens('example', { '--dsw-alias-brand-primary': { light: '#c96442', dark: '#d97757' }, })) } } return module.exports }, }) ``` ## Rules (violations fail at load or render) - **ESM only** on the host; **plain JS only** in the browser half (no `import`, JSX, TypeScript, decorators). Browser UI must use `React.createElement` (React arrives via `require('react')`). - Optional services: `ctx.get('x')` with an absence check. Declare `inject: ['x']` only for hard dependencies, or you get `service "x" is not declared` / `cannot get property "timer" without inject`. - Every listener, subscription, timer, and token override must be disposable and owned inside `apply` (`ctx.on(...)`, `ctx.effect(() => disposer)`, retained disposers). No module-scope side effects, no process/page-wide globals. - Host↔client values must be JSON-safe. Never `JSON.stringify`/`structuredClone` live Service / Session / Event / Slot objects — extract the scalar leaves you need. - A patch `config:` **replaces** the whole row config; use `!!js` expressions for dynamic values (`!!js process.env.X`, `!!js ctx.webStartup.port ?? 3080`, `!!js dshHomePath('sessions')`). - Client plugins need a **full page reload** after install. HMR auto-reload only exists while the `pnpm run dev:web` watcher rebuilds client bundles. - Install client-only and dual-half packages with the `dsh.client` manifest **and** a `./client` export, or the browser half is never served. ## Common failures | Symptom | Check first | | --- | --- | | Row missing / plugin not loaded | `dsh web --dump-config`; row `id`/`name` correct; package resolvable (two-anchor resolution: profile `node_modules`, then the flat fallback `$DSH_HOME/profiles/node_modules`) | | `service "x" is not declared` | `ctx.x` used without `inject`; switch to `ctx.get('x')` or declare the hard dependency | | `cannot get property "timer" without inject` | Timers are a Service, not a global: declare `inject: ['timer']` | | Config validation error at boot | schemastery `Config` vs patch `config` mismatch; `--dump-config` shows the composed value | | Client half never appears | `dsh.client` malformed (`platform` string, `inject` string[], `immediately` boolean), missing `./client` export, or no page reload | | Client render error | Browser console; JSX/TS/`import`/Node globals in `lib/client.js` | | Plugin loads but has no effect | Event names / payloads drift per DSH version — re-read the installed `.d.ts` | | Dependency resolution error | The import isn't resolvable from the profile; either it is not an in-box package (declare it in `dependencies`, pnpm installs it) or the package is not installed | ## Installing this skill into an agent runtime Copy this folder into the runtime's skills directory: - **Cross-runtime** (Codex, Copilot CLI, Gemini CLI, and DSH itself scan it): `~/.agents/skills/dsh-plugin-development/` - **Claude Code**: `~/.claude/skills/dsh-plugin-development/` - **DSH home**: `$DSH_HOME/skills/dsh-plugin-development/` - **Hermes / OpenClaw / others**: drop the folder wherever that tool scans for `SKILL.md` (e.g. Hermes plugin dirs, OpenClaw skill dirs). ## References & templates - `references/plugin-package.md` — full package / entry / manifest / patch / CLI contract, verified against dsh 0.1.0-rc.6. - `references/services-events.md` — service & event surface and how to enumerate it at runtime. - `templates/server-plugin/` — host-only starter (Config + events + timer). - `templates/client-plugin/` — browser-only starter (theme token override + CSS, theme-proven pattern). - `templates/dual-half-plugin/` — host service + browser UI starter with RPC notes.