xhvdhczd/dsh-plugin-skill-manager

dsh-plugin-skill-manager

DSH Web UI plugin: Skills tab with per-skill enable/disable toggles (DeepSeek Harness)

未分類

dsh-plugin-skill-manager

English | 简体中文

A DeepSeek Harness (DSH) plugin that adds a Skills tab to the web UI conversation view ring (next to Chat and Trajectory). It lists every available skill with a live search box and a per-skill enable / disable switch, plus a bulk enable-all / disable-all toggle.

Disabling a skill writes disable-model-invocation: true into the skill's SKILL.md frontmatter, so the model-facing skill catalog drops it while the user-facing surface keeps it — the skill stays visible in the list and can be re-enabled at any time.

Features

  • 🗂️ Skills tab registered into the conversation.view slot, alongside Chat and Trajectory
  • 🔍 Search box filtering by skill name or description
  • 🔘 Bulk switch: enable or disable every visible skill at once
  • 🔄 Live list: the tab drops the host's cached catalog on open, polls a cheap structure revision every 3 s while visible, and offers a Reload button
  • 🔒 Read-only rows: skills with no writable SKILL.md (application-bundled skills such as office-docx / office-pptx / office-xlsx, and runtime-registered skills) render with a Built-in badge and no toggle, instead of failing with skill "…" not found

How it works

The plugin ships two halves in one npm package:

HalfEntryWhat it does
Hostlib/index.jsRegisters POST /api/skill-manager/toggle, …/packages, …/refresh, and …/status on the DSH web server, resolves the skill's SKILL.md through the registry and the disk (project roots first, then user roots), rewrites its frontmatter, and watches the user skill roots for new packages
Clientlib/client.jsRegisters the Skills conversation-view tab, renders the list from the skills.list RPC, and calls the host routes on toggle, reload, and revision poll

Disabling one skill (enabled: false) adds:

disable-model-invocation: true

to the skill frontmatter. The filesystem skill provider watches the skill roots, so the catalog invalidates automatically after the write — no restart needed for the catalog itself (a restart is required after installing or updating the plugin).

Why the refresh routes exist

The core filesystem provider discovers one level deep, and this plugin's package provider only watches containers it already knows, so a package installed as <root>/<pkg>/skills/<name>/SKILL.md while DSH is running is observed by neither watcher and would stay invisible until a restart. The host half therefore:

  1. watches $DSH_HOME/skills and $DSH_AGENTS_HOME/skills recursively and invalidates the registry (plus bumps a revision) after each burst of events (400 ms debounce);
  2. exposes POST /api/skill-manager/refresh to invalidate on demand and POST /api/skill-manager/status to read the revision; and
  3. refuses a toggle it cannot perform with 409 read-only-skill and an explanatory message, which the client turns into a permanent read-only row.

Installation

Requires a DSH profile (the example uses the web profile):

# from the plugin checkout, install into the profile
dsh plugin --profile web add file:./dsh-plugin-skill-manager

# or directly with pnpm
pnpm --dir "$HOME/.dsh/profiles/web" add file:./dsh-plugin-skill-manager

Then add the package to the profile's bundle layers. dsh.profile.bundles in ~/.dsh/profiles/web/package.json should include:

{
  "dsh": {
    "profile": {
      "bundles": [
        "@deepseek-ai/dsh-base",
        "@deepseek-ai/dsh-web-app",
        "dsh-plugin-marketplace",
        "dsh-plugin-skill-manager"
      ]
    }
  }
}

Finally restart DSH and refresh the web UI. A Skills tab appears next to Chat and Trajectory.

Note: file: dependencies are copied into the pnpm store. After editing the plugin sources, re-run pnpm --dir "$HOME/.dsh/profiles/web" remove dsh-plugin-skill-manager && pnpm --dir "$HOME/.dsh/profiles/web" add --force file:./dsh-plugin-skill-manager and restart DSH.

Uninstall

pnpm --dir "$HOME/.dsh/profiles/web" remove dsh-plugin-skill-manager

Remove dsh-plugin-skill-manager from dsh.profile.bundles in ~/.dsh/profiles/web/package.json, then restart DSH. Skills disabled by the plugin keep their frontmatter; re-enable them with the plugin, or delete the disable-model-invocation line manually.

Development

# syntax-check both halves
node --check lib/index.js
node --check lib/client.js

# verify the composed profile tree includes the plugin
dsh --profile web --dump-config | grep -A2 skill-manager

Tests

The three suites import the plugin through the profile's node_modules, so copy them next to the profile and run them from there:

cp test/*.test.mjs "$HOME/.dsh/profiles/web/"
cd "$HOME/.dsh/profiles/web"
node package-scan.test.mjs         # package containers, ranks, provider roots()/list()
node target-resolution.test.mjs    # writable roots, read-only classification, toggle targets
node client-load.test.mjs          # browser bundle loads and registers the Skills view

Package layout

dsh-plugin-skill-manager/
├── package.json        # dsh.bundle.patch + dsh.client declarations
├── cordis.patch.yml    # loader row mounting the host plugin
├── test/               # standalone suites (see Tests)
└── lib/
    ├── index.js        # host half: toggle/packages/refresh/status routes + root watcher
    └── client.js       # client half: Skills tab (__ModuleLoader__ bundle)

Skill packages (two-level layout)

The host half also understands skill packages: a skill root may contain package directories where the level-1 name is the installed skill-package name and the level-2 skills directory holds the skill bundles:

~/.dsh/skills/
├── my-package/                       # level 1: skill-package name
│   ├── .claude-plugin/plugin.json    # optional manifest (Claude Code compatible)
│   └── skills/                       # level 2: skills
│       ├── group/                    # optional grouping layer (bounded depth)
│       │   └── some-skill/SKILL.md
│       └── other-skill/SKILL.md
└── flat-skill/SKILL.md               # flat layout: also registered

Recognition. A package is identified by its manifest first — .claude-plugin/plugin.json or dsh-plugin.json — whose skills array is the authoritative list of skill directories; without a manifest, a package with a skills/ subtree is discovered by a bounded walk (depth ≤ maxNestingDepth).

Both layouts are registered. Besides packaged skills, flat skills — a level-1 directory holding SKILL.md directly, the shape produced by unzipping, copying, or git clone-ing a skill — are registered as containers too, keeping the root's own rank:

  • where the core filesystem provider also covers that root, the core result wins the name collision, so nothing is listed twice;
  • where the core provider does not cover that root (some profile / agent-preset combinations), the plugin still surfaces those skills in the model catalog and the Skills tab, so no directory restructuring is needed just to be visible.

Set flatSkills: false to restore the package-only behaviour.

Effects. The host registers a packages skill provider (a FileSystemSkillProvider subclass) whose scan roots are the package containers and the flat skill directories: containers are ranked root.rank - packageRankOffset, flat skills keep root.rank. The toggle route locates packaged and flat skills alike.

UI. The Skills tab groups rows under their package: a collapsible package header (mattpocock ▸) that expands to the package's skills, backed by POST /api/skill-manager/packages; skills outside any package render ungrouped. Searching auto-expands every group so matches stay visible.

Built-in skills are hidden by default. Rows the host cannot toggle — application-bundled skills such as the office set and the bundled docx/pdf/pptx/xlsx entries — stay out of the list. A Show built-in / Hide built-in switch in the toolbar reveals them and reports how many are currently hidden; once shown they keep their "Built-in (read-only)" badge. The list therefore defaults to what can be acted on.

Config (cordis plugin config, e.g. via the loader patch row):

KeyDefaultMeaning
enablePackagestrueMaster switch for package scanning
manifestFiles['.claude-plugin/plugin.json','dsh-plugin.json']Manifest file names, in priority order
maxNestingDepth4Fallback walk depth under <pkg>/skills
packageRankOffset10Rank delta so packaged skills beat flat skills of the same root
flatSkillstrueAlso register <root>/<skill>/SKILL.md flat skills as containers
includeRoots['project-dsh','project-agents','custom','user-dsh','user-agents']Which base roots participate

A DSH restart is required after updating the plugin; packaged and flat skills then appear without further setup. The package-scan helpers are exported from lib/index.js and covered by a standalone test (run from the profile dir: node skill-pkg-test.mjs).

A skill is installed but missing? Check the layout first

A missing row in the Skills tab is almost always a layout problem. Make sure the skill matches either shape:

~/.dsh/skills/<name>/SKILL.md                    # flat
~/.dsh/skills/<package>/skills/<name>/SKILL.md   # packaged

The next most common causes, in order: a SKILL.md without name / description frontmatter, a name that is not kebab-case (^[a-z0-9]+(?:-[a-z0-9]+)*$), or frontmatter that fails YAML parsing (the host logs skill file … ignored).

Mind line endings when validating that YAML: on Windows files are often CRLF, and locating the closing frontmatter with \n--- truncates the block and reports a false syntax error. Compare whole lines with the trailing \r stripped instead.

License

MIT