opencode-skills-doctor 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -23,7 +23,7 @@ Local scanner, linter, fixer, updater and **OpenCode plugin** for agent skills
23
23
  | **Plan** | exact token savings: trim descriptions and/or hide skills, projected over N steps |
24
24
  | **Cost** | CLI: 0 tokens. Plugin: ~191 tokens/step for the tool definition, plus a one-time warning per session |
25
25
  | **Requirements** | Node ≥ 20, zero runtime dependencies |
26
- | **Quality** | 55 tests, CI on ubuntu/macos/windows × Node 20/22/24 |
26
+ | **Quality** | 57 tests, CI on ubuntu/macos/windows × Node 20/22/24 |
27
27
 
28
28
  ## Why
29
29
 
@@ -342,7 +342,19 @@ It scans `.opencode/skills`, `.claude/skills` and `.agents/skills` (project and
342
342
 
343
343
  ### DeepSeek Harness (DSH)
344
344
 
345
- DSH ships an MCP client bundle (`@deepseek-ai/dsh-mcp-client`). Add one entry to your profile patch, e.g. `~/.dsh/profiles/<profile>/cordis.patch.yml`:
345
+ One command installs both the MCP tool and a skill that tells the model when to audit:
346
+
347
+ ```powershell
348
+ npx -y opencode-skills-doctor install --client dsh
349
+ ```
350
+
351
+ It locates `~/.dsh/profiles/<profile>/cordis.patch.yml` (default `desktop`; otherwise `--profile <name>`), appends the `mcp-skills-doctor` entry idempotently, and copies the `skills-doctor-audit` skill into `~/.dsh/skills/`. Restart DSH to load the MCP tool; the skill is watched and appears without a restart.
352
+
353
+ - tool name `mcp__skills_doctor__skills_doctor`, arguments `directory`, `budget`, `lang`, `global`, `html`;
354
+ - `uninstall --client dsh` removes both the patch entry and the skill;
355
+ - `--dsh-home <dir>` for a non-standard DSH home.
356
+
357
+ Manual fallback — add this to the profile patch yourself:
346
358
 
347
359
  ```yaml
348
360
  - id: mcp-skills-doctor
@@ -354,8 +366,6 @@ DSH ships an MCP client bundle (`@deepseek-ai/dsh-mcp-client`). Add one entry to
354
366
  args: ["-y", "opencode-skills-doctor", "mcp"]
355
367
  ```
356
368
 
357
- Restart DSH. The tool shows up as `mcp__skills_doctor__skills_doctor`.
358
-
359
369
  On Windows, if the stdio spawn cannot resolve `npx`, use `command: cmd` and `args: ["/c", "npx", "-y", "opencode-skills-doctor", "mcp"]`.
360
370
 
361
371
  ## How it works
@@ -507,7 +517,7 @@ Executing code is free, but the tool definition is part of the model's context.
507
517
  ## Development and contributing
508
518
 
509
519
  ```powershell
510
- npm test # 55 tests (node test/doctor.test.mjs)
520
+ npm test # 57 tests (node test/doctor.test.mjs)
511
521
  npm run bench # lint timing for 100/500/1000 skills
512
522
  ```
513
523
 
@@ -23,7 +23,7 @@ OpenCode skill 的**本地检测器 + 更新器 + 修复器**,并附带 **Open
23
23
  | **规划** | 精确 token 节省:精简描述 / 隐藏 skill,按 N 步折算 |
24
24
  | **成本** | CLI 0 token;插件工具定义约 191/步,另有每会话一次预警 |
25
25
  | **要求** | Node ≥ 20,零运行时依赖 |
26
- | **质量** | 55 个测试,ubuntu/macos/windows × Node 20/22/24 CI |
26
+ | **质量** | 57 个测试,ubuntu/macos/windows × Node 20/22/24 CI |
27
27
 
28
28
  ## 为什么需要它
29
29
 
@@ -342,7 +342,19 @@ args = ["-y", "opencode-skills-doctor", "mcp"]
342
342
 
343
343
  ### DeepSeek Harness(DSH)
344
344
 
345
- DSH 自带 MCP 客户端 bundle(`@deepseek-ai/dsh-mcp-client`)。在 profile 的 patch 里加一条,例如 `~/.dsh/profiles/<profile>/cordis.patch.yml`:
345
+ 一条命令同时装好 MCP 工具和「告诉模型什么时候该体检」的 skill:
346
+
347
+ ```powershell
348
+ npx -y opencode-skills-doctor install --client dsh
349
+ ```
350
+
351
+ 它会找到 `~/.dsh/profiles/<profile>/cordis.patch.yml`(默认 `desktop`,否则用 `--profile <name>`),幂等地追加 `mcp-skills-doctor` 条目,并把 `skills-doctor-audit` 技能复制到 `~/.dsh/skills/`。重启 DSH 加载 MCP 工具;skill 目录有监听,无需重启。
352
+
353
+ - 工具名 `mcp__skills_doctor__skills_doctor`,参数 `directory`、`budget`、`lang`、`global`、`html`;
354
+ - `uninstall --client dsh` 同时移除配置条目和 skill;
355
+ - `--dsh-home <dir>` 用于非默认的 DSH 主目录。
356
+
357
+ 手动兜底 —— 自己往 profile patch 里加:
346
358
 
347
359
  ```yaml
348
360
  - id: mcp-skills-doctor
@@ -354,8 +366,6 @@ DSH 自带 MCP 客户端 bundle(`@deepseek-ai/dsh-mcp-client`)。在 profile
354
366
  args: ["-y", "opencode-skills-doctor", "mcp"]
355
367
  ```
356
368
 
357
- 重启 DSH。工具会以 `mcp__skills_doctor__skills_doctor` 出现。
358
-
359
369
  Windows 上如果 stdio 起不来 `npx`,改成 `command: cmd`、`args: ["/c", "npx", "-y", "opencode-skills-doctor", "mcp"]`。
360
370
 
361
371
  ## 工作原理
@@ -507,7 +517,7 @@ token 是启发式估算,1200 是建议预算。可以用 `--budget` 或 `.ski
507
517
  ## 开发与贡献
508
518
 
509
519
  ```powershell
510
- npm test # 55 个测试(node test/doctor.test.mjs)
520
+ npm test # 57 个测试(node test/doctor.test.mjs)
511
521
  npm run bench # 100/500/1000 个 skill 的 lint 耗时
512
522
  ```
513
523
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "opencode-skills-doctor",
3
- "version": "0.5.1",
4
- "description": "Audit, fix and update agent skills: token cost, broken references, shadowed duplicates, HTML reports, catalog sync. CLI, OpenCode plugin, MCP server; works with OpenCode, Claude Code, Codex, Grok CLI and Cursor skill layouts.",
3
+ "version": "0.6.0",
4
+ "description": "Audit, fix and update agent skills: token cost, broken references, shadowed duplicates, HTML reports, catalog sync. CLI, OpenCode plugin, MCP server; works with OpenCode, Claude Code, Codex, Grok CLI, Cursor and DeepSeek Harness.",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": "./src/plugin.mjs",
@@ -37,6 +37,7 @@
37
37
  "bin",
38
38
  "src",
39
39
  "docs",
40
+ "skills",
40
41
  "README.md",
41
42
  "SECURITY.md",
42
43
  "SECURITY.zh-CN.md"
@@ -0,0 +1,54 @@
1
+ ---
2
+ name: skills-doctor-audit
3
+ description: Audit installed agent skills for per-step token cost, shadowed duplicates, broken references and budget overruns, then propose concrete fixes.
4
+ whenToUse: Use when the user mentions too many skills, context bloat, token cost, duplicate or missing skills, or asks for a skill health check.
5
+ metadata:
6
+ author: wangyuchen5110
7
+ source: opencode-skills-doctor
8
+ ---
9
+
10
+ # Skill audit workflow
11
+
12
+ Audit the user's installed skills and turn the result into a short, actionable answer.
13
+ Never modify skill files without explicit confirmation.
14
+
15
+ ## 1. Run the read-only audit
16
+
17
+ ```powershell
18
+ npx -y opencode-skills-doctor check --json
19
+ ```
20
+
21
+ Read `summary.advertisedTokens` (cost per model step), `summary.advertised` and `findings[]`.
22
+ If the command fails, fall back to `opencode-skills-doctor` when installed globally, or ask the user to run it from the repo.
23
+
24
+ ## 2. Explain the top findings
25
+
26
+ Report at most three findings, in plain language, each with its token impact:
27
+
28
+ - `token-budget` — descriptions cost roughly N tokens on **every** model step;
29
+ - `shadowed` — the same skill ID loads from several places, only one wins;
30
+ - `long-description` / `large-body` — descriptions or bodies that are paid on every step / every invocation;
31
+ - `broken-reference` — a `references/` or `scripts/` file the skill points at is missing.
32
+
33
+ ## 3. Show what can actually be saved
34
+
35
+ ```powershell
36
+ npx -y opencode-skills-doctor plan --json
37
+ ```
38
+
39
+ Report `recommended.savings` (per step), `recommended.final` and `recommended.stepsSavings`
40
+ (per 100 steps). Explain the difference: trimming descriptions keeps discoverability; hiding a skill
41
+ (`autoinvoke: false`) removes it from the advertised list but keeps `@id` loading.
42
+
43
+ ## 4. Propose fixes — do not apply silently
44
+
45
+ - Hide low-frequency skills: `npx -y opencode-skills-doctor fix --hide id1,id2`
46
+ - Archive shadowed duplicates (reversible): `npx -y opencode-skills-doctor fix`
47
+ - Trim descriptions manually, keeping them discoverable
48
+ - Ask first, then re-run `check` to confirm the new cost
49
+
50
+ ## 5. Optional shareable report
51
+
52
+ ```powershell
53
+ npx -y opencode-skills-doctor check --html skills-report.html --open
54
+ ```
package/src/cli.mjs CHANGED
@@ -9,6 +9,7 @@ import { auditSkills } from "./audit.mjs"
9
9
  import { loadConfig } from "./config.mjs"
10
10
  import { startMcpServer } from "./mcp.mjs"
11
11
  import { installOpenCode, uninstallOpenCode } from "./install.mjs"
12
+ import { installDsh, uninstallDsh } from "./dsh.mjs"
12
13
  import { writeHtmlReport, openReport } from "./html.mjs"
13
14
  import { planBudget } from "./plan.mjs"
14
15
  import {
@@ -72,6 +73,8 @@ function parseArgs(argv) {
72
73
  checkOnly: false,
73
74
  prune: false,
74
75
  client: "auto",
76
+ profile: null,
77
+ dshHome: null,
75
78
  configDir: null,
76
79
  noNpm: false,
77
80
  html: null,
@@ -153,6 +156,12 @@ function parseArgs(argv) {
153
156
  options.client = String(args[++i] ?? "").toLowerCase()
154
157
  break
155
158
  }
159
+ case "--profile":
160
+ options.profile = args[++i] ?? null
161
+ break
162
+ case "--dsh-home":
163
+ options.dshHome = path.resolve(args[++i] ?? ".")
164
+ break
156
165
  case "--no-npm":
157
166
  options.noNpm = true
158
167
  break
@@ -591,8 +600,8 @@ export async function main(argv) {
591
600
  process.exit(2)
592
601
  }
593
602
 
594
- if (!["auto", "opencode", "desktop"].includes(options.client)) {
595
- console.error("unsupported --client: expected auto, opencode or desktop")
603
+ if (!["auto", "opencode", "desktop", "dsh"].includes(options.client)) {
604
+ console.error("unsupported --client: expected auto, opencode, desktop or dsh")
596
605
  process.exitCode = 2
597
606
  return
598
607
  }
@@ -616,6 +625,29 @@ export async function main(argv) {
616
625
  return
617
626
  }
618
627
 
628
+ if (options.command === "install" && options.client === "dsh") {
629
+ const result = installDsh({
630
+ home: options.dshHome ?? undefined,
631
+ profile: options.profile ?? undefined,
632
+ dryRun: options.dryRun,
633
+ })
634
+ if (options.json) {
635
+ console.log(JSON.stringify(result, null, 2))
636
+ } else {
637
+ console.log(bold(t("dsh.title")))
638
+ if (result.profile) console.log(dim(t("dsh.profile", { profile: result.profile })))
639
+ if (result.patchFile) console.log(dim(t("dsh.patch", { file: result.patchFile })))
640
+ if (result.skillDir) console.log(dim(t("dsh.skill", { dir: result.skillDir })))
641
+ if (result.dryRun) console.log(dim(t("install.dryRun")))
642
+ for (const action of result.actions) console.log(` · ${action}`)
643
+ for (const error of result.errors) console.log(red(t("install.error", { error })))
644
+ console.log("")
645
+ console.log(result.errors.length ? red(t("dsh.failed")) : dim(t("dsh.restart")))
646
+ }
647
+ process.exitCode = result.errors.length ? 1 : 0
648
+ return
649
+ }
650
+
619
651
  if (options.command === "install") {
620
652
  const result = installOpenCode({
621
653
  configDir: options.configDir ?? undefined,
@@ -645,6 +677,27 @@ export async function main(argv) {
645
677
  return
646
678
  }
647
679
 
680
+ if (options.command === "uninstall" && options.client === "dsh") {
681
+ const result = uninstallDsh({
682
+ home: options.dshHome ?? undefined,
683
+ profile: options.profile ?? undefined,
684
+ dryRun: options.dryRun,
685
+ })
686
+ if (options.json) {
687
+ console.log(JSON.stringify(result, null, 2))
688
+ } else {
689
+ console.log(bold(t("dsh.uninstallTitle")))
690
+ if (result.profile) console.log(dim(t("dsh.profile", { profile: result.profile })))
691
+ if (result.dryRun) console.log(dim(t("install.dryRun")))
692
+ for (const action of result.actions) console.log(` · ${action}`)
693
+ for (const error of result.errors) console.log(red(t("install.error", { error })))
694
+ console.log("")
695
+ console.log(result.errors.length ? red(t("dsh.failed")) : dim(t("dsh.restart")))
696
+ }
697
+ process.exitCode = result.errors.length ? 1 : 0
698
+ return
699
+ }
700
+
648
701
  if (options.command === "uninstall") {
649
702
  const result = uninstallOpenCode({
650
703
  configDir: options.configDir ?? undefined,
package/src/dsh.mjs ADDED
@@ -0,0 +1,185 @@
1
+ import fs from "node:fs"
2
+ import path from "node:path"
3
+ import os from "node:os"
4
+ import { fileURLToPath } from "node:url"
5
+
6
+ export const ENTRY_ID = "mcp-skills-doctor"
7
+ export const SERVER_NAME = "skills_doctor"
8
+ export const SKILL_NAME = "skills-doctor-audit"
9
+
10
+ const ENTRIES = `- id: ${ENTRY_ID}
11
+ name: "@deepseek-ai/dsh-mcp-client"
12
+ config:
13
+ serverName: ${SERVER_NAME}
14
+ transport: stdio
15
+ command: npx
16
+ args: ["-y", "opencode-skills-doctor", "mcp"]
17
+ `
18
+
19
+ export function dshHome(env = process.env) {
20
+ return env.DSH_HOME || path.join(os.homedir(), ".dsh")
21
+ }
22
+
23
+ export function profileDir(home, profile) {
24
+ return path.join(home, "profiles", profile)
25
+ }
26
+
27
+ /** DSH profile 目录(含 package.json 的子目录)。 */
28
+ export function listProfiles(home) {
29
+ const dir = path.join(home, "profiles")
30
+ try {
31
+ return fs
32
+ .readdirSync(dir, { withFileTypes: true })
33
+ .filter((entry) => entry.isDirectory() && fs.existsSync(path.join(dir, entry.name, "package.json")))
34
+ .map((entry) => entry.name)
35
+ .sort()
36
+ } catch {
37
+ return []
38
+ }
39
+ }
40
+
41
+ function pickProfile({ home, profile }) {
42
+ const profiles = listProfiles(home)
43
+ if (profile) {
44
+ if (!profiles.includes(profile)) {
45
+ return { error: `DSH profile "${profile}" not found under ${path.join(home, "profiles")}` }
46
+ }
47
+ return { profile, profiles }
48
+ }
49
+ if (profiles.includes("desktop")) return { profile: "desktop", profiles }
50
+ if (profiles.length === 1) return { profile: profiles[0], profiles }
51
+ if (!profiles.length) return { error: `no DSH profile found under ${path.join(home, "profiles")}` }
52
+ return { error: `multiple DSH profiles found (${profiles.join(", ")}); pass --profile <name>` }
53
+ }
54
+
55
+ function skillSourceDir() {
56
+ return fileURLToPath(new URL("../skills/dsh-skills-doctor", import.meta.url))
57
+ }
58
+
59
+ function copySkill(source, target, dryRun, act, dry) {
60
+ if (!fs.existsSync(source)) return false
61
+ const file = path.join(source, "SKILL.md")
62
+ if (!fs.existsSync(file)) return false
63
+ const targetFile = path.join(target, "SKILL.md")
64
+ if (fs.existsSync(targetFile) && fs.readFileSync(targetFile, "utf8") === fs.readFileSync(file, "utf8")) {
65
+ act(`skill already up to date ${target}`)
66
+ return true
67
+ }
68
+ if (dryRun) {
69
+ dry(`install skill ${SKILL_NAME} into ${target}`)
70
+ return true
71
+ }
72
+ fs.mkdirSync(target, { recursive: true })
73
+ fs.copyFileSync(file, targetFile)
74
+ act(`wrote skill ${targetFile}`)
75
+ return true
76
+ }
77
+
78
+ /** 在 cordis.patch.yml 里追加 MCP 条目(幂等)。 */
79
+ function appendEntry(patchFile, dryRun, act, dry) {
80
+ const existing = fs.existsSync(patchFile) ? fs.readFileSync(patchFile, "utf8") : ""
81
+ if (existing.includes(ENTRY_ID) || existing.includes(`serverName: ${SERVER_NAME}`)) {
82
+ act(`MCP entry already present in ${patchFile}`)
83
+ return false
84
+ }
85
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-")
86
+ if (dryRun) {
87
+ dry(`append ${ENTRY_ID} to ${patchFile}`)
88
+ return true
89
+ }
90
+ if (existing) fs.copyFileSync(patchFile, `${patchFile}.bak-${stamp}`)
91
+ const header = existing
92
+ ? (existing.endsWith("\n") ? "" : "\n")
93
+ : "# DSH profile patch layer\n"
94
+ const next = `${existing}${header}${existing.trim() ? "\n" : ""}${ENTRIES}`
95
+ fs.writeFileSync(patchFile, next, "utf8")
96
+ act(`appended ${ENTRY_ID} to ${patchFile}`)
97
+ return true
98
+ }
99
+
100
+ function removeEntry(patchFile, dryRun, act, dry) {
101
+ if (!fs.existsSync(patchFile)) return false
102
+ const text = fs.readFileSync(patchFile, "utf8")
103
+ const lines = text.split(/\r?\n/)
104
+ const index = lines.findIndex((line) => line.trim() === `- id: ${ENTRY_ID}`)
105
+ if (index < 0) return false
106
+
107
+ let end = index + 1
108
+ while (end < lines.length) {
109
+ const line = lines[end]
110
+ if (line.trim() === "" || /^[ \t]/.test(line)) end++
111
+ else break
112
+ }
113
+ if (dryRun) {
114
+ dry(`remove ${ENTRY_ID} from ${patchFile}`)
115
+ return true
116
+ }
117
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-")
118
+ fs.copyFileSync(patchFile, `${patchFile}.bak-${stamp}`)
119
+ fs.writeFileSync(patchFile, lines.slice(0, index).concat(lines.slice(end)).join("\n"), "utf8")
120
+ act(`removed ${ENTRY_ID} from ${patchFile}`)
121
+ return true
122
+ }
123
+
124
+ function removeSkill(target, dryRun, act, dry) {
125
+ const file = path.join(target, "SKILL.md")
126
+ if (!fs.existsSync(file)) return false
127
+ const text = fs.readFileSync(file, "utf8")
128
+ if (!text.includes("Skill audit workflow")) {
129
+ act(`kept ${target} (not the generated skill)`)
130
+ return false
131
+ }
132
+ if (dryRun) {
133
+ dry(`delete skill ${target}`)
134
+ return true
135
+ }
136
+ fs.rmSync(target, { recursive: true, force: true })
137
+ act(`deleted skill ${target}`)
138
+ return true
139
+ }
140
+
141
+ export function installDsh({ home = dshHome(), profile = null, dryRun = false } = {}) {
142
+ const actions = []
143
+ const errors = []
144
+ const dry = (message) => actions.push(`[dry-run] ${message}`)
145
+ const act = (message) => actions.push(message)
146
+
147
+ const picked = pickProfile({ home, profile })
148
+ if (picked.error) {
149
+ errors.push(picked.error)
150
+ return { ok: false, home, actions, errors, dryRun }
151
+ }
152
+
153
+ const dir = profileDir(home, picked.profile)
154
+ const patchFile = path.join(dir, "cordis.patch.yml")
155
+ const skillDir = path.join(home, "skills", SKILL_NAME)
156
+
157
+ if (dryRun) dry(`profile: ${picked.profile} (${dir})`)
158
+ else act(`profile: ${picked.profile}`)
159
+
160
+ appendEntry(patchFile, dryRun, act, dry)
161
+ copySkill(skillSourceDir(), skillDir, dryRun, act, dry)
162
+
163
+ return { ok: errors.length === 0, home, profile: picked.profile, patchFile, skillDir, actions, errors, dryRun }
164
+ }
165
+
166
+ export function uninstallDsh({ home = dshHome(), profile = null, dryRun = false } = {}) {
167
+ const actions = []
168
+ const errors = []
169
+ const dry = (message) => actions.push(`[dry-run] ${message}`)
170
+ const act = (message) => actions.push(message)
171
+
172
+ const picked = pickProfile({ home, profile })
173
+ if (picked.error) {
174
+ errors.push(picked.error)
175
+ return { ok: false, home, actions, errors, dryRun }
176
+ }
177
+
178
+ const dir = profileDir(home, picked.profile)
179
+ const patchFile = path.join(dir, "cordis.patch.yml")
180
+ const skillDir = path.join(home, "skills", SKILL_NAME)
181
+
182
+ removeEntry(patchFile, dryRun, act, dry)
183
+ removeSkill(skillDir, dryRun, act, dry)
184
+ return { ok: errors.length === 0, home, profile: picked.profile, patchFile, skillDir, actions, errors, dryRun }
185
+ }
package/src/i18n.mjs CHANGED
@@ -36,7 +36,9 @@ Options:
36
36
  --html [file] check: write a self-contained HTML report (default: skills-report.html)
37
37
  --open check: open the HTML report after writing it
38
38
  --config-dir <dir> install/uninstall: OpenCode config directory (default: ~/.config/opencode)
39
- --client <name> install/uninstall: auto | opencode | desktop (default auto)
39
+ --client <name> install/uninstall: auto | opencode | desktop | dsh (default auto)
40
+ --profile <name> dsh: profile to install into (default: desktop, or the only one)
41
+ --dsh-home <dir> dsh: DSH home directory (default: ~/.dsh)
40
42
  --no-npm install/uninstall: do not run npm (1.x)
41
43
  --apply upgrade: run npm install -g opencode-skills-doctor@latest
42
44
  --no-update-check skip the automatic update check for this run
@@ -120,6 +122,13 @@ Exit codes: 0 ok; 1 errors (and warnings with --strict); 2 bad arguments or runt
120
122
  "install.next": "Restart OpenCode to activate.",
121
123
  "uninstall.title": "Remove OpenCode integration",
122
124
  "uninstall.next": "Restart OpenCode to finish removal.",
125
+ "dsh.title": "DSH integration",
126
+ "dsh.uninstallTitle": "Remove DSH integration",
127
+ "dsh.profile": " profile: {profile}",
128
+ "dsh.patch": " patch: {file}",
129
+ "dsh.skill": " skill: {dir}",
130
+ "dsh.restart": "Restart DSH to load the MCP tool (the skill updates live).",
131
+ "dsh.failed": "DSH integration failed.",
123
132
  "update.toolAvailable":
124
133
  " ! skills-doctor {latest} is available (you have {current}); update with: {cmd} · {url}",
125
134
  "upgrade.title": "skills-doctor upgrade",
@@ -248,7 +257,9 @@ Exit codes: 0 ok; 1 errors (and warnings with --strict); 2 bad arguments or runt
248
257
  --html [file] check: 生成自包含 HTML 报告(默认 skills-report.html)
249
258
  --open check: 写完报告后用浏览器打开
250
259
  --config-dir <dir> install/uninstall: OpenCode 配置目录(默认 ~/.config/opencode)
251
- --client <name> install/uninstall: auto | opencode | desktop(默认 auto)
260
+ --client <name> install/uninstall: auto | opencode | desktop | dsh(默认 auto)
261
+ --profile <name> dsh: 安装到哪个 profile(默认 desktop,或唯一的一个)
262
+ --dsh-home <dir> dsh: DSH 主目录(默认 ~/.dsh)
252
263
  --no-npm install/uninstall: 不执行 npm(1.x)
253
264
  --apply upgrade: 执行 npm install -g opencode-skills-doctor@latest
254
265
  --no-update-check 本次运行跳过自动更新检查
@@ -332,6 +343,13 @@ Exit codes: 0 ok; 1 errors (and warnings with --strict); 2 bad arguments or runt
332
343
  "install.next": "重启 OpenCode 生效。",
333
344
  "uninstall.title": "撤销 OpenCode 集成",
334
345
  "uninstall.next": "重启 OpenCode 完成卸载。",
346
+ "dsh.title": "DSH 集成",
347
+ "dsh.uninstallTitle": "撤销 DSH 集成",
348
+ "dsh.profile": " profile: {profile}",
349
+ "dsh.patch": " patch: {file}",
350
+ "dsh.skill": " skill: {dir}",
351
+ "dsh.restart": "重启 DSH 加载 MCP 工具(skill 会即时更新)。",
352
+ "dsh.failed": "DSH 集成失败。",
335
353
  "update.toolAvailable": " ! skills-doctor 有新版本 {latest}(当前 {current});更新:{cmd} · {url}",
336
354
  "upgrade.title": "skills-doctor 自更新",
337
355
  "upgrade.current": " 当前版本: {version}",