opencode-skills-doctor 0.6.3 → 0.8.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
@@ -116,6 +116,8 @@ All commands accept `--json`, `--lang en|zh`, `--budget`, `--root` and `--no-glo
116
116
  | **DeepSeek Harness** | `install --client dsh` adds the MCP entry and the `skills-doctor-audit` skill |
117
117
  | **Claude Code / Codex** | MCP server (`npx -y opencode-skills-doctor mcp`) |
118
118
  | **Grok CLI / Cursor** | their skill roots are scanned natively |
119
+ | **Hermes Agent** | MCP entry in `~/.hermes/config.yaml`; `~/.hermes/skills` is scanned |
120
+ | **Kimi Code** | `install --client kimi` writes `~/.kimi-code/mcp.json` + the audit skill |
119
121
 
120
122
  ## Why it is safe
121
123
 
@@ -425,6 +425,29 @@ grok mcp doctor skills-doctor # ✓ server started · handshake OK · 1 tool d
425
425
 
426
426
  Grok 的工具命名是 `<server>__<tool>`,所以调用 `skills_doctor__skills_doctor`;配置写入 `~/.grok/config.toml` 的 `[mcp_servers.skills-doctor]`。
427
427
 
428
+ ### Hermes Agent(Nous Research)
429
+
430
+ Hermes 从 `~/.hermes/config.yaml` 读取 MCP server:
431
+
432
+ ```yaml
433
+ mcp_servers:
434
+ skills_doctor:
435
+ command: "npx"
436
+ args: ["-y", "opencode-skills-doctor", "mcp"]
437
+ ```
438
+
439
+ Hermes 的工具命名是 `mcp_<server>_<tool>`,即 `mcp_skills_doctor_skills_doctor`。它的 skill 在 `~/.hermes/skills`(含导入的 `openclaw-imports/`)和项目 `.hermes/skills`,`check` 都会扫描。
440
+
441
+ ### Kimi Code(Moonshot)
442
+
443
+ 一条命令写入 MCP 配置和审计 skill:
444
+
445
+ ```powershell
446
+ npx -y opencode-skills-doctor install --client kimi
447
+ ```
448
+
449
+ 它会把 `skills-doctor` 合并进 `~/.kimi-code/mcp.json`(或 `$KIMI_CODE_HOME/mcp.json`),并把 skill 复制到 `$KIMI_CODE_HOME/skills/`。之后开新的 Kimi Code 会话即可,工具名是 `mcp__skills_doctor__skills_doctor`。`check` 也会扫描 `~/.kimi-code/skills` 和项目 `.kimi-code/skills`;`uninstall --client kimi` 反向撤销。
450
+
428
451
  ## 检测规则
429
452
 
430
453
  | 规则 | 级别 | 含义 |
package/docs/REFERENCE.md CHANGED
@@ -10,7 +10,7 @@ Local scanner, linter, fixer, updater and **OpenCode plugin** for agent skills
10
10
  |---|---|
11
11
  | **What it is** | Audit, fix and update the skills your coding agent loads |
12
12
  | **Interfaces** | CLI · OpenCode plugin (1.x / 2.x) · MCP server (Claude Code, Codex, DSH, …) · Grok CLI compatible |
13
- | **Skill sources** | `.opencode/skills`, `.claude/skills`, `.agents/skills`, `.grok/skills`, `.cursor/skills`, `opencode.json(c)` entries, HTTP catalogs |
13
+ | **Skill sources** | `.opencode/skills`, `.claude/skills`, `.agents/skills`, `.grok/skills`, `.cursor/skills`, `.hermes/skills`, `.kimi-code/skills`, `opencode.json(c)` entries, HTTP catalogs |
14
14
  | **What it finds** | shadowed IDs, duplicates, long descriptions, oversized bodies, per-step token budget, broken file references, invalid frontmatter, empty bodies, stale skills |
15
15
  | **What it fixes** | archives shadowed definitions (reversible), hides low-frequency skills, syncs catalogs with drift detection |
16
16
  | **Reports** | terminal, JSON, or a self-contained HTML report (`--html [file] --open`) |
@@ -408,6 +408,29 @@ grok mcp doctor skills-doctor # ✓ server started · handshake OK · 1 tool d
408
408
 
409
409
  Grok namespaces tools as `<server>__<tool>`, so call `skills_doctor__skills_doctor`. The config lands in `~/.grok/config.toml` as `[mcp_servers.skills-doctor]`.
410
410
 
411
+ ### Hermes Agent (Nous Research)
412
+
413
+ Hermes reads MCP servers from `~/.hermes/config.yaml`:
414
+
415
+ ```yaml
416
+ mcp_servers:
417
+ skills_doctor:
418
+ command: "npx"
419
+ args: ["-y", "opencode-skills-doctor", "mcp"]
420
+ ```
421
+
422
+ Hermes names tools `mcp_<server>_<tool>`, so the tool is `mcp_skills_doctor_skills_doctor`. Its skills live in `~/.hermes/skills` (including imported `openclaw-imports/`) and project `.hermes/skills`; `check` scans both.
423
+
424
+ ### Kimi Code (Moonshot)
425
+
426
+ One command writes the MCP entry and the audit skill:
427
+
428
+ ```powershell
429
+ npx -y opencode-skills-doctor install --client kimi
430
+ ```
431
+
432
+ It merges `skills-doctor` into `~/.kimi-code/mcp.json` (or `$KIMI_CODE_HOME/mcp.json`) and copies the skill to `$KIMI_CODE_HOME/skills/`. Start a new Kimi Code session afterwards; the tool is `mcp__skills_doctor__skills_doctor` (Kimi uses the same `mcp__<server>__<tool>` shape as Claude Code). `check` also scans `~/.kimi-code/skills` and project `.kimi-code/skills`; `uninstall --client kimi` reverses both.
433
+
411
434
  ## Rules
412
435
 
413
436
  | Rule | Level | Meaning |
@@ -451,9 +474,13 @@ Grok namespaces tools as `<server>__<tool>`, so call `skills_doctor__skills_doct
451
474
  6. project `.cursor/skills`
452
475
  7. `~/.grok/skills`
453
476
  8. project `.grok/skills`
454
- 9. `~/.config/opencode/skills`
455
- 10. project `.opencode/skills`
456
- 11. the `skills` array in `opencode.json(c)` (2.x) or `skills.paths` (1.x); relative paths resolve from the working directory; supports `~/`, absolute paths and HTTP catalogs
477
+ 9. `~/.hermes/skills`
478
+ 10. project `.hermes/skills`
479
+ 11. `~/.kimi-code/skills`
480
+ 12. project `.kimi-code/skills`
481
+ 13. `~/.config/opencode/skills`
482
+ 14. project `.opencode/skills`
483
+ 15. the `skills` array in `opencode.json(c)` (2.x) or `skills.paths` (1.x); relative paths resolve from the working directory; supports `~/`, absolute paths and HTTP catalogs
457
484
 
458
485
  ## JSON output
459
486
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-skills-doctor",
3
- "version": "0.6.3",
3
+ "version": "0.8.0",
4
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": {
package/src/cli.mjs CHANGED
@@ -10,6 +10,7 @@ import { loadConfig } from "./config.mjs"
10
10
  import { startMcpServer } from "./mcp.mjs"
11
11
  import { installOpenCode, uninstallOpenCode } from "./install.mjs"
12
12
  import { installDsh, uninstallDsh } from "./dsh.mjs"
13
+ import { installKimi, uninstallKimi } from "./kimi.mjs"
13
14
  import { writeHtmlReport, openReport } from "./html.mjs"
14
15
  import { planBudget } from "./plan.mjs"
15
16
  import {
@@ -75,6 +76,7 @@ function parseArgs(argv) {
75
76
  client: "auto",
76
77
  profile: null,
77
78
  dshHome: null,
79
+ kimiHome: null,
78
80
  configDir: null,
79
81
  noNpm: false,
80
82
  html: null,
@@ -162,6 +164,9 @@ function parseArgs(argv) {
162
164
  case "--dsh-home":
163
165
  options.dshHome = path.resolve(args[++i] ?? ".")
164
166
  break
167
+ case "--kimi-home":
168
+ options.kimiHome = path.resolve(args[++i] ?? ".")
169
+ break
165
170
  case "--no-npm":
166
171
  options.noNpm = true
167
172
  break
@@ -600,8 +605,8 @@ export async function main(argv) {
600
605
  process.exit(2)
601
606
  }
602
607
 
603
- if (!["auto", "opencode", "desktop", "dsh"].includes(options.client)) {
604
- console.error("unsupported --client: expected auto, opencode, desktop or dsh")
608
+ if (!["auto", "opencode", "desktop", "dsh", "kimi"].includes(options.client)) {
609
+ console.error("unsupported --client: expected auto, opencode, desktop, dsh or kimi")
605
610
  process.exitCode = 2
606
611
  return
607
612
  }
@@ -648,6 +653,25 @@ export async function main(argv) {
648
653
  return
649
654
  }
650
655
 
656
+ if (options.command === "install" && options.client === "kimi") {
657
+ const result = installKimi({ home: options.kimiHome ?? undefined, dryRun: options.dryRun })
658
+ if (options.json) {
659
+ console.log(JSON.stringify(result, null, 2))
660
+ } else {
661
+ console.log(bold(t("kimi.title")))
662
+ if (result.home) console.log(dim(t("kimi.home", { dir: result.home })))
663
+ if (result.mcpFile) console.log(dim(t("kimi.mcp", { file: result.mcpFile })))
664
+ if (result.skillDir) console.log(dim(t("kimi.skill", { dir: result.skillDir })))
665
+ if (result.dryRun) console.log(dim(t("install.dryRun")))
666
+ for (const action of result.actions) console.log(` · ${action}`)
667
+ for (const error of result.errors) console.log(red(t("install.error", { error })))
668
+ console.log("")
669
+ console.log(result.errors.length ? red(t("kimi.failed")) : dim(t("kimi.restart")))
670
+ }
671
+ process.exitCode = result.errors.length ? 1 : 0
672
+ return
673
+ }
674
+
651
675
  if (options.command === "install") {
652
676
  const result = installOpenCode({
653
677
  configDir: options.configDir ?? undefined,
@@ -698,6 +722,23 @@ export async function main(argv) {
698
722
  return
699
723
  }
700
724
 
725
+ if (options.command === "uninstall" && options.client === "kimi") {
726
+ const result = uninstallKimi({ home: options.kimiHome ?? undefined, dryRun: options.dryRun })
727
+ if (options.json) {
728
+ console.log(JSON.stringify(result, null, 2))
729
+ } else {
730
+ console.log(bold(t("kimi.uninstallTitle")))
731
+ if (result.home) console.log(dim(t("kimi.home", { dir: result.home })))
732
+ if (result.dryRun) console.log(dim(t("install.dryRun")))
733
+ for (const action of result.actions) console.log(` · ${action}`)
734
+ for (const error of result.errors) console.log(red(t("install.error", { error })))
735
+ console.log("")
736
+ console.log(result.errors.length ? red(t("kimi.failed")) : dim(t("kimi.restart")))
737
+ }
738
+ process.exitCode = result.errors.length ? 1 : 0
739
+ return
740
+ }
741
+
701
742
  if (options.command === "uninstall") {
702
743
  const result = uninstallOpenCode({
703
744
  configDir: options.configDir ?? undefined,
package/src/discover.mjs CHANGED
@@ -120,9 +120,13 @@ function isUnder(parent, child) {
120
120
  * 6. 项目 .cursor/skills
121
121
  * 7. ~/.grok/skills
122
122
  * 8. 项目 .grok/skills
123
- * 9. ~/.config/opencode/skills
124
- * 10. 项目 .opencode/skills(项目根 → cwd)
125
- * 11. opencode.json(c) 里的 skills 数组 / skills.paths
123
+ * 9. ~/.hermes/skills(Hermes Agent)
124
+ * 10. 项目 .hermes/skills
125
+ * 11. ~/.kimi-code/skills(Kimi Code)
126
+ * 12. 项目 .kimi-code/skills
127
+ * 13. ~/.config/opencode/skills
128
+ * 14. 项目 .opencode/skills(项目根 → cwd)
129
+ * 15. opencode.json(c) 里的 skills 数组 / skills.paths
126
130
  */
127
131
  export function discoverSources({ cwd = process.cwd(), home = os.homedir(), includeGlobal = true } = {}) {
128
132
  const projectRoot = findProjectRoot(cwd) ?? path.resolve(cwd)
@@ -151,6 +155,12 @@ export function discoverSources({ cwd = process.cwd(), home = os.homedir(), incl
151
155
  if (includeGlobal) addSource("grok-global", "global", path.join(home, ".grok", "skills"))
152
156
  for (const dir of projectChain) addSource("grok-project", "project", path.join(dir, ".grok", "skills"))
153
157
 
158
+ if (includeGlobal) addSource("hermes-global", "global", path.join(home, ".hermes", "skills"))
159
+ for (const dir of projectChain) addSource("hermes-project", "project", path.join(dir, ".hermes", "skills"))
160
+
161
+ if (includeGlobal) addSource("kimi-global", "global", path.join(home, ".kimi-code", "skills"))
162
+ for (const dir of projectChain) addSource("kimi-project", "project", path.join(dir, ".kimi-code", "skills"))
163
+
154
164
  if (includeGlobal) addSource("opencode-global", "global", path.join(home, ".config", "opencode", "skills"))
155
165
  for (const dir of projectChain) addSource("opencode-project", "project", path.join(dir, ".opencode", "skills"))
156
166
 
package/src/i18n.mjs CHANGED
@@ -36,9 +36,10 @@ 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 | dsh (default auto)
39
+ --client <name> install/uninstall: auto | opencode | desktop | dsh | kimi (default auto)
40
40
  --profile <name> dsh: profile to install into (default: desktop, or the only one)
41
41
  --dsh-home <dir> dsh: DSH home directory (default: ~/.dsh)
42
+ --kimi-home <dir> kimi: Kimi Code home (default: ~/.kimi-code or $KIMI_CODE_HOME)
42
43
  --no-npm install/uninstall: do not run npm (1.x)
43
44
  --apply upgrade: run npm install -g opencode-skills-doctor@latest
44
45
  --no-update-check skip the automatic update check for this run
@@ -129,6 +130,13 @@ Exit codes: 0 ok; 1 errors (and warnings with --strict); 2 bad arguments or runt
129
130
  "dsh.skill": " skill: {dir}",
130
131
  "dsh.restart": "Restart DSH to load the MCP tool (HMR reloads the patch when enabled; the skill updates live).",
131
132
  "dsh.failed": "DSH integration failed.",
133
+ "kimi.title": "Kimi Code integration",
134
+ "kimi.uninstallTitle": "Remove Kimi Code integration",
135
+ "kimi.home": " home: {dir}",
136
+ "kimi.mcp": " mcp: {file}",
137
+ "kimi.skill": " skill: {dir}",
138
+ "kimi.restart": "Start a new Kimi Code session to load the MCP tool.",
139
+ "kimi.failed": "Kimi Code integration failed.",
132
140
  "update.toolAvailable":
133
141
  " ! skills-doctor {latest} is available (you have {current}); update with: {cmd} · {url}",
134
142
  "upgrade.title": "skills-doctor upgrade",
@@ -257,9 +265,10 @@ Exit codes: 0 ok; 1 errors (and warnings with --strict); 2 bad arguments or runt
257
265
  --html [file] check: 生成自包含 HTML 报告(默认 skills-report.html)
258
266
  --open check: 写完报告后用浏览器打开
259
267
  --config-dir <dir> install/uninstall: OpenCode 配置目录(默认 ~/.config/opencode)
260
- --client <name> install/uninstall: auto | opencode | desktop | dsh(默认 auto)
268
+ --client <name> install/uninstall: auto | opencode | desktop | dsh | kimi(默认 auto)
261
269
  --profile <name> dsh: 安装到哪个 profile(默认 desktop,或唯一的一个)
262
270
  --dsh-home <dir> dsh: DSH 主目录(默认 ~/.dsh)
271
+ --kimi-home <dir> kimi: Kimi Code 主目录(默认 ~/.kimi-code 或 $KIMI_CODE_HOME)
263
272
  --no-npm install/uninstall: 不执行 npm(1.x)
264
273
  --apply upgrade: 执行 npm install -g opencode-skills-doctor@latest
265
274
  --no-update-check 本次运行跳过自动更新检查
@@ -350,6 +359,13 @@ Exit codes: 0 ok; 1 errors (and warnings with --strict); 2 bad arguments or runt
350
359
  "dsh.skill": " skill: {dir}",
351
360
  "dsh.restart": "重启 DSH 加载 MCP 工具(启用 HMR 时会自动重载 patch;skill 即时更新)。",
352
361
  "dsh.failed": "DSH 集成失败。",
362
+ "kimi.title": "Kimi Code 集成",
363
+ "kimi.uninstallTitle": "撤销 Kimi Code 集成",
364
+ "kimi.home": " 主目录: {dir}",
365
+ "kimi.mcp": " mcp: {file}",
366
+ "kimi.skill": " skill: {dir}",
367
+ "kimi.restart": "开一个新的 Kimi Code 会话加载 MCP 工具。",
368
+ "kimi.failed": "Kimi Code 集成失败。",
353
369
  "update.toolAvailable": " ! skills-doctor 有新版本 {latest}(当前 {current});更新:{cmd} · {url}",
354
370
  "upgrade.title": "skills-doctor 自更新",
355
371
  "upgrade.current": " 当前版本: {version}",
package/src/kimi.mjs ADDED
@@ -0,0 +1,121 @@
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 SERVER_NAME = "skills-doctor"
7
+ export const SKILL_NAME = "skills-doctor-audit"
8
+
9
+ const ENTRY = { command: "npx", args: ["-y", "opencode-skills-doctor", "mcp"] }
10
+
11
+ export function kimiHome(env = process.env) {
12
+ return env.KIMI_CODE_HOME || path.join(os.homedir(), ".kimi-code")
13
+ }
14
+
15
+ function skillSourceDir() {
16
+ return fileURLToPath(new URL("../skills/dsh-skills-doctor", import.meta.url))
17
+ }
18
+
19
+ function readJson(file) {
20
+ if (!fs.existsSync(file)) return {}
21
+ try {
22
+ return JSON.parse(fs.readFileSync(file, "utf8").replace(/^\uFEFF/, ""))
23
+ } catch {
24
+ return null
25
+ }
26
+ }
27
+
28
+ function copySkill(source, target, dryRun, act, dry) {
29
+ const file = path.join(source, "SKILL.md")
30
+ if (!fs.existsSync(file)) return false
31
+ const targetFile = path.join(target, "SKILL.md")
32
+ if (fs.existsSync(targetFile) && fs.readFileSync(targetFile, "utf8") === fs.readFileSync(file, "utf8")) {
33
+ act(`skill already up to date ${target}`)
34
+ return true
35
+ }
36
+ if (dryRun) {
37
+ dry(`install skill ${SKILL_NAME} into ${target}`)
38
+ return true
39
+ }
40
+ fs.mkdirSync(target, { recursive: true })
41
+ fs.copyFileSync(file, targetFile)
42
+ act(`wrote skill ${targetFile}`)
43
+ return true
44
+ }
45
+
46
+ function removeSkill(target, dryRun, act, dry) {
47
+ const file = path.join(target, "SKILL.md")
48
+ if (!fs.existsSync(file)) return false
49
+ if (!fs.readFileSync(file, "utf8").includes("Skill audit workflow")) {
50
+ act(`kept ${target} (not the generated skill)`)
51
+ return false
52
+ }
53
+ if (dryRun) {
54
+ dry(`delete skill ${target}`)
55
+ return true
56
+ }
57
+ fs.rmSync(target, { recursive: true, force: true })
58
+ act(`deleted skill ${target}`)
59
+ return true
60
+ }
61
+
62
+ export function installKimi({ home = kimiHome(), dryRun = false } = {}) {
63
+ const actions = []
64
+ const errors = []
65
+ const act = (message) => actions.push(message)
66
+ const dry = (message) => actions.push(`[dry-run] ${message}`)
67
+
68
+ const mcpFile = path.join(home, "mcp.json")
69
+ const skillDir = path.join(home, "skills", SKILL_NAME)
70
+
71
+ const config = readJson(mcpFile)
72
+ if (config === null) {
73
+ errors.push(`cannot parse ${mcpFile}`)
74
+ return { ok: false, home, mcpFile, skillDir, actions, errors, dryRun }
75
+ }
76
+ config.mcpServers = config.mcpServers && typeof config.mcpServers === "object" ? config.mcpServers : {}
77
+
78
+ const existing = config.mcpServers[SERVER_NAME]
79
+ const same = existing && existing.command === ENTRY.command && JSON.stringify(existing.args) === JSON.stringify(ENTRY.args)
80
+ if (same) {
81
+ act(`${SERVER_NAME} already present in ${mcpFile}`)
82
+ } else if (dryRun) {
83
+ dry(`add ${SERVER_NAME} to mcpServers in ${mcpFile}`)
84
+ } else {
85
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-")
86
+ if (fs.existsSync(mcpFile)) fs.copyFileSync(mcpFile, `${mcpFile}.bak-${stamp}`)
87
+ config.mcpServers[SERVER_NAME] = ENTRY
88
+ fs.mkdirSync(home, { recursive: true })
89
+ fs.writeFileSync(mcpFile, JSON.stringify(config, null, 2) + "\n", "utf8")
90
+ act(`wrote ${SERVER_NAME} to ${mcpFile}`)
91
+ }
92
+
93
+ copySkill(skillSourceDir(), skillDir, dryRun, act, dry)
94
+ return { ok: errors.length === 0, home, mcpFile, skillDir, actions, errors, dryRun }
95
+ }
96
+
97
+ export function uninstallKimi({ home = kimiHome(), dryRun = false } = {}) {
98
+ const actions = []
99
+ const errors = []
100
+ const act = (message) => actions.push(message)
101
+ const dry = (message) => actions.push(`[dry-run] ${message}`)
102
+
103
+ const mcpFile = path.join(home, "mcp.json")
104
+ const skillDir = path.join(home, "skills", SKILL_NAME)
105
+
106
+ const config = readJson(mcpFile)
107
+ if (config && config.mcpServers && config.mcpServers[SERVER_NAME]) {
108
+ if (dryRun) {
109
+ dry(`remove ${SERVER_NAME} from ${mcpFile}`)
110
+ } else {
111
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-")
112
+ fs.copyFileSync(mcpFile, `${mcpFile}.bak-${stamp}`)
113
+ delete config.mcpServers[SERVER_NAME]
114
+ fs.writeFileSync(mcpFile, JSON.stringify(config, null, 2) + "\n", "utf8")
115
+ act(`removed ${SERVER_NAME} from ${mcpFile}`)
116
+ }
117
+ }
118
+
119
+ removeSkill(skillDir, dryRun, act, dry)
120
+ return { ok: errors.length === 0, home, mcpFile, skillDir, actions, errors, dryRun }
121
+ }