EdisonYang1982/dsh-skill-switch ↗★ 0
dsh-skill-switch
Per-session skill switch for DeepSeek Harness: see which skills a session loads, toggle them off, and keep a pure-chat session free of skill overhead. Also mounts whole skill families in and out of the host's skill hub. 用于控制会话加载的技能,适合需要纯净聊天、免除技能开销的用户。
Other repositories with this package name
Install
npx -p @deepseek-ai/dsh dsh plugin --profile web add github:EdisonYang1982/dsh-skill-switchREADME
Read the full README ↗Configuration
Defaults are neutral and live in the bundled cordis.patch.yml. Everything
deployment-specific belongs in your profile patch ( /cordis.patch.yml),
keyed by the row id:
- id: skill-switch
name: dsh-skill-switch
config:
defaultMode: all # posture a new session starts in: all | none | work
workSkills: [] # skills the `work` posture enables
syncScript: '' # absolute path of a sync script; empty hides the control
hubDir: '' # absolute path of the hub the host scans (needed for skillGroups)
skillGroups: [] # all-or-nothing bundles, see below
defaultMode: all keeps the stock behaviour — you turn things off when you want
to. Set none if you would rather opt in per session.
Sync (syncScript)
The host reads skills from a hub directory, not from wherever you author them. If your skill repository lives elsewhere (a network mount, a synced folder), a script bridges the two:
| Stage | Who | Automatic? |
|---|---|---|
| Repository → hub (copy) | syncScript | ❌ a button in the strip |
| Hub → strip (display) | file watcher + SSE | ✅ automatic |
Point syncScript at your script and a Sync skills button appears. Clicking it
runs the script, invalidates the catalog cache, and pushes a refresh to every open
strip.
Manual on purpose. If the repository is on a network mount, every scan costs something; you decide when to pay it, and nothing runs while you are not looking. Both halves are required — without the sync the host does not know the new skill exists, so there is nothing for the strip to refresh.
Feedback follows the macOS idiom rather than a persistent log pane:
| Phase | Button shows | Then |
|---|---|---|
| Running | ◌ Syncing | — |
| Success | ✓ Synced | reverts after 2s — no layout cost |
| Failure | ⚠ Sync failed (red) | stays, and unfolds the script's output |
Success needs no acknowledgement, so it withdraws. Failure stays because it asks something of you, and its text is what tells you what went wrong.
Skill groups (skillGroups)
Some skill families are both huge and interdependent: a member's
SKILL.md links its siblings by relative path (lark-doc → ../lark-shared). A
per-skill switch cannot express them — loading one alone breaks its references,
and listing all of them costs their catalog lines whether or not the session uses
them.
A skill group parks the family outside the hub and moves it in or out as one unit:
| State | Catalog cost | Members live in |
|---|---|---|
| Parked | 0 | parkDir |
| Mounted | one line each | hubDir |
hubDir: '/home/you/.agents/skills'
skillGroups:
- id: lark
label: 'Feishu CLI'
description: '27 Feishu skills, mounted and parked as one unit'
parkDir: '/home/you/.agents-parked/lark'
discover: true # read members from parkDir; no hand-maintained list
The strip shows a dashed chip per group: ⬡ Feishu CLI 27 when parked, solid when
mounted. Clicking moves the whole family.
Why whole groups, and why moving directories rather than hiding lines. Three design decisions, each buying tokens:
- Parked costs 0, hidden costs its lines. The cheapest token is one never sent. Hiding a skill in the catalog still requires the registry to discover it first, and discovery is what renders the line — so per-skill filtering can only ever remove the line it already paid to compute. Moving the directory out of the hub means the registry never sees it at all. On the measured family above that is the difference between 0 and 5,559 characters.
- All-or-nothing, because the family is genuinely atomic. Members reference
each other by relative path; a per-member switch would let you build a broken
state (one member mounted, its
lark-shareddependency parked). A group switch cannot express that state, which is the point. - A roster file, so the count survives a restart. After mounting, the park directory is empty — membership has to be recorded somewhere, and an in-memory list would report 0 members on the next host start, which is exactly the state the user just switched into.
Member roster. Mounting writes the member names to /.members.
After a mount the park directory is empty, so that file is the only record of
membership — an in-memory list would not survive a host restart. Unmounting reads
it to decide what moves back.
Safety boundary. Group membership is read from parkDir only, never by
scanning the hub root — the hub holds unrelated skills, and scanning it would
adopt them into the group and sweep them into the park directory on the next
unmount. test/group.test.mjs locks this down.