Bundle
dsh-skill-catalog
DeepSeek Harness plugin: scan every skill your DSH can load into one browsable catalog (collection > category > command), with a sidebar panel, click-to-insert, and savable custom categories & command notes.
- Source
- GoJo22
- License
- MIT
- Updated
- Updated 5 hours ago
Readme
# dsh-skill-catalog
A **DeepSeek Harness (DSH) plugin** (not a skill): scan every skill your DSH can
load, merge them into one browsable catalog, and put it in a sidebar panel —
solving "too many skills, no idea which command comes from which source".
DSH natively supports `/` invocation, so this plugin only does **cataloging and
browsing** — it registers no slash commands of its own.
English | [中文](README.zh-CN.md)
## Features
- **Single entry point**: a button at the **bottom of the DSH left sidebar**
(next to Settings) opens a **narrow panel** on the right that sits beside the
conversation instead of covering it.
- **Scans exactly what DSH can load**: every direct child of `~/.dsh/skills`
and `~/.agents/skills` becomes a collection (single-skill directory,
`skills/` skill pack, or loose `.md`). Inclusion rules mirror DSH's own
source and skill-pack provider loading rules — if DSH won't load it, it is
not listed.
- **Refresh**: the ↻ button re-scans `SKILL.md` from disk.
- **Click to insert**: clicking a command card writes `/command` straight into
the **current conversation input** (via DSH's
`uiSession.current.value.props.inputActions`, preferring `setDraft(text)`);
falls back to copying to the clipboard when the composer is unavailable.
- **Right-click for details**: right-click a command card for an info card
explaining what it does (your custom note takes precedence — see below).
The card flips away from viewport edges; it closes on left-click elsewhere,
Esc, list scroll, the ✕ button, or right-clicking the same card again.
- **Native tool `dsh_catalog`**: ask in chat "what is /estimate" or "which
skills do I have" and get the three-level catalog (collection › category ›
command) plus the panel address.
- **Custom categories (saved, persistent)**: paste "category + commands" in
the panel's ⚙ Categories editor — saved once, effective forever. See
[Custom categories](#custom-categories).
- **Custom command notes (saved, persistent)**: paste a Markdown table in the
panel's ✎ Notes editor; right-clicking a command then shows your note (the
skill's own description is kept as an "original description" footnote), and
noted commands carry a blue marker and a "noted" badge. See
[Custom command notes](#custom-command-notes).
## Scan sources
| Source | Skill location (scan roots) | Parser |
|---|---|---|
| DeepSeek Harness | `~/.dsh/skills`, `~/.agents/skills` (each direct child = one collection) | skill_md |
Three-level structure: **collection › category › command**. Core rules
(mirroring "how DSH reads skills"):
- **Collection = the file name one level below a skills root.** DSH reads
skills from `~/.dsh/skills` and `~/.agents/skills` (project-level
`<project>/.dsh/skills` and `<project>/.agents/skills` exist too; this
plugin does not scan those yet). Each direct child is a collection:
- `<root>/<name>/SKILL.md` — single-skill collection
- `<root>/<name>/skills/<s>/SKILL.md` — skill-pack collection, one command
per skill inside the pack
- `<root>/<name>.md` — loose skill collection (name = file stem)
- pure resource directories (neither layout) are skipped entirely
- **Category** has three sources, highest priority first:
1. **Custom categories** saved in the panel (see below)
2. the skill frontmatter's `category`
3. **Auto-categorization** — keyword extraction from name + description
(weighted, stop-worded), grouped by "same primary topic". Zero config,
zero rule tables.
The category dropdown is **linked to the collection**: pick a collection
first, then its categories appear.
- **Command** = the skill name (`/name`). Inclusion rules mirror DSH's source
(`isPotentialSkillPath`, max 2 path segments below the root) plus the
skill-pack provider's loading rules.
## Custom categories
Auto-categorization is the zero-config fallback, but if you have **your own
taxonomy** for a skill library (e.g. 74 commands split into Onboarding /
Game Design / QA & Testing), paste it once in the panel and it sticks.
### How to
1. Click **⚙ Categories** at the top right of the panel;
2. pick the **collection** the rules belong to (defaults to the current
filter; "All collections" matches by command name across collections);
3. paste lines of the form **category name followed by its commands**:
```
Onboarding & Navigation /start /help /project-stage-detect /adopt /settings
Game Design /brainstorm /map-systems /design-system /quick-design
QA & Testing /qa-plan /smoke-check /soak-test /regression-suite
```
4. Click **Save & apply**. The panel reports how many categories/commands
were parsed and how many matched the current catalog.
Parsing is lenient: tokens starting with `/` are commands, everything else
merges into the category name (so names with spaces and `&` like
`Onboarding & Navigation` work as-is); leading `-`, `1.`, `#` and backticks
are stripped; commands dedupe within a category; **order is preserved** —
what you paste is what the editor shows when you reopen it.
### Why refresh / plugin installs & removals never lose it
| Scenario | Mechanism |
|---|---|
| Click ↻ Refresh | Refresh only re-scans `SKILL.md`; every catalog build re-reads and re-applies your rules |
| Reopen the client / restart DSH | Rules live on disk, independent of any process |
| Uninstall/update this plugin | Rules live in `~/.dsh-catalog/`, **not** in the plugin directory and not in the `assets/` runtime artifact |
| Another plugin is removed | Rules are stored **by command name** and only match, never filter: a missing command keeps its rule and recovers automatically when the plugin returns |
| Install a new plugin | Commands not in any rule go to `(Uncategorized)` in strict mode, otherwise auto-categorization |
### Storage location and format
`~/.dsh-catalog/categories.json` (created on first save):
```json
{
"version": 1,
"updated_at": "2026-01-01T00:00:00",
"rules": [
{
"collection": "my-game-skills",
"category": "Game Design",
"commands": ["brainstorm", "map-systems", "design-system"]
}
],
"strict_collections": ["my-game-skills"]
}
```
- `collection` empty = global rule (matches by command name, any collection).
- `strict_collections`: commands in these collections that no rule lists all
go to `(Uncategorized)`, dropping auto-categorization (the checkbox in the
editor). Uncheck to let new skills keep auto-categorizing.
- If the file is corrupted it is backed up as `*.corrupt-<timestamp>.json`
and scanning continues with empty rules.
### HTTP API
The panel talks to these endpoints (the server listens on `127.0.0.1` only):
| Method | Path | Description |
|---|---|---|
| GET | `/api/custom?collection=X` | Read saved rules; also returns an editable paste-format text |
| POST | `/api/custom` | Save. body: `{ text \| rules, collection, strict, mode }`, `mode` = `replace` (default) or `merge` |
| POST | `/api/custom/reset` | Clear all custom categories |
## Custom command notes
A skill's own description is often one English sentence that doesn't tell you
how to use it. This feature lets you **write your own note**, shown on
right-click.
### How to
1. Click **✎ Notes** at the top right of the panel;
2. pick a collection and paste a table like this (the most common shape of a
skill list — copy it straight in):
```
| Command | Note |
|---|---|
| **ask-matt** | Skill router. Ask it when unsure which skill/flow to use. |
| **code-review** | Dual-axis code review: repo standards + requirement spec, parallel subagents, side-by-side report. |
```
3. Click **Save & apply**; right-clicking a command card now shows your note.
Parsing: split on `|`, the first cell is the command name (`**`, backticks
and a leading `/` are stripped), the rest merges into the note; **header rows
(`| Command | Note |`) and `|---|---|` separator rows are skipped
automatically**; `name: note` and `name␣␣note` (two or more spaces) also
work. Order is preserved and round-trips into the same table format.
### Display rules
| Case | Right-click info card shows |
|---|---|
| Custom note exists | Your note (body); the skill's description as an "original description" footnote |
| No custom note | The skill's own description (as before) |
The info card is `position: fixed`, follows the cursor, and flips back inside
the viewport near edges; long notes scroll inside the card. **Right-click the
same card again to close** (toggle); right-clicking a different card switches
content. Other ways to close: left-click elsewhere, Esc, scroll the list, or
the ✕ on the card.
Noted commands carry a **blue left marker** and a **"noted" badge**, and the
search box matches note text too.
### Persistence
Same mechanism as custom categories: stored in `~/.dsh-catalog/notes.json`
(**not** in the plugin directory, **not** in `assets/`), re-read on every
catalog build — refresh, reopen, reinstall, and other plugins' installs or
removals don't affect it. Notes are stored **by command name** and kept while
a command is missing.
```json
{
"version": 1,
"updated_at": "2026-01-01T00:00:00",
"notes": [
{ "collection": "dsh-mattpocock-skills", "command": "ask-matt", "note": "Skill router…" }
]
}
```
`collection` empty = global (matches by command name, any collection).
### HTTP API
| Method | Path | Description |
|---|---|---|
| GET | `/api/notes?collection=X` | Read saved notes; also returns an editable table text |
| POST | `/api/notes` | Save. body: `{ text \| notes, collection, mode }` |
| POST | `/api/notes/reset` | Clear all custom notes |
## Install (DSH plugin)
```bash
# From npm (once published):
dsh plugin --profile web add dsh-skill-catalog
dsh plugin --profile desktop add dsh-skill-catalog
# Or from a local checkout (any profile; web = dsh web, desktop = DSH desktop client):
dsh plugin --profile web add "link:/absolute/path/to/dsh-skill-catalog"
dsh plugin --profile desktop add "link:/absolute/path/to/dsh-skill-catalog"
# Verify registration
dsh web --dump-config | findstr dsh-skill-catalog
dsh web # restart the harness
```
Uninstall: `dsh plugin --profile web remove dsh-skill-catalog`
The panel UI needs the right-sidebar services
(`@deepseek-ai/dsh-client-ui-sidebar-right`). Profiles without them still get
the tool and the HTTP service — only the panel stays hidden.
## Configuration
| Env var | Default | Description |
|---|---|---|
| `DSH_CATALOG_PORT` | `8765` | Preferred port for the panel server (falls back 8765→8769 on conflict). The sidebar panel auto-discovers ports in the **8765–8769** range only; moving the server outside that range (e.g. 9000) breaks panel discovery, though the API stays reachable at that port directly |
Custom data locations are documented under
[Custom categories](#custom-categories) (`~/.dsh-catalog/categories.json`,
edited in the panel, not by hand).
### Including skill libraries from elsewhere
Scan roots are fixed to DSH's own two skills roots: `~/.dsh/skills` and
`~/.agents/skills`. To bring a library from elsewhere (e.g. `D:\path\to\skills`)
into the catalog, link it in — no code changes needed:
```powershell
# Recommended shape: real parent directory, skills inside points at the target
New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.agents\skills\MyLib"
New-Item -ItemType Junction -Path "$env:USERPROFILE\.agents\skills\MyLib\skills" `
-Target "D:\path\to\skills"
```
All three link shapes are recognized (the scanner detects the layout, so a
link can no longer make a whole entry vanish):
| Shape | Structure | Result |
|---|---|---|
| Skill pack | `<name>/skills/<s>/SKILL.md` | one command per skill in the pack |
| Folded link | `<name>/<s>/SKILL.md` (link points directly at `skills/`) | same, validated by directory name |
| Single skill | `<name>/SKILL.md` | one command |
## Using it in DSH
- Sidebar: click the book icon at the bottom of the DSH left sidebar to open
the visual catalog.
- Chat: call `dsh_catalog` (accepts `query` / `collection` / `refresh`).
- Click-to-insert: clicking a command card writes `/command` into the current
conversation input (or copies it); press Enter to invoke.
## Privacy
- The panel server listens on `127.0.0.1` loopback only, never exposed.
- The scan artifact `assets/command-catalog.json` contains local absolute
paths; it is a runtime artifact and excluded via `.gitignore`. The
repository contains no hardcoded local paths or personal data.
- Personalization lives **outside the repository**: custom categories in
`~/.dsh-catalog/categories.json` (local collection and command names, not
distributed with the repo).
- Auto-categorization is driven entirely by each skill's own
name/description; nothing is uploaded and no external service is contacted.
## Structure
```
dsh-catalog/
package.json Plugin manifest (dsh.bundle.patch / dsh.client)
cordis.patch.yml Plugin registration (bundle insert)
lib/index.js Cordis host entry: registers the tool + starts the panel server
lib/scan.js DSH skill scanner (no third-party deps, Node port):
direct children of skills roots = collections
(single skill / skill pack / loose), auto-categorization
+ custom category application
lib/customCategories.js Custom categories: persistence
(~/.dsh-catalog/categories.json), paste parsing,
override application
lib/customNotes.js Custom command notes: persistence
(~/.dsh-catalog/notes.json), table parsing,
right-click info card display
lib/store.js User-data directory + legacy-location migration
lib/server.js Panel data HTTP service (/api/catalog, /api/refresh,
/api/mode, /api/custom R/W, /api/custom/reset,
/api/notes R/W, /api/notes/reset)
client.js DSH browser entry: registers via window.__ModuleLoader__.load
sidebar icon (sidebar.footer.action) + right panel
(sidebarRight), native React components, data via
/api/catalog
assets/icon.svg Plugin/sidebar icon
assets/command-catalog.json Scan artifact (runtime-generated, gitignored)
```
## Extending: add a scan source
Append one record to the `SOURCES` list in `lib/scan.js`; no parsing logic
changes needed. `collection` is usually **omitted** — collection names are
derived from the directories the roots actually hit:
```js
{
client: "My Source",
kind: "skill_md", // DSH skills-root source
roots: ["~/.mysource/skills"],
// collection: "explicit name", // only for non-directory sources
}
```
## License
[MIT](LICENSE)
Install
dsh plugin --profile web add github:GoJo22/dsh-catalog
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install dsh-skill-catalog from the hub
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.