opencode-skills-doctor 0.7.0 → 0.8.1
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 +1 -0
- package/docs/README.zh-CN.md +10 -0
- package/docs/REFERENCE.md +16 -4
- package/package.json +3 -2
- package/src/audit.mjs +2 -1
- package/src/cli.mjs +51 -3
- package/src/discover.mjs +15 -4
- package/src/i18n.mjs +20 -2
- package/src/kimi.mjs +121 -0
package/README.md
CHANGED
|
@@ -117,6 +117,7 @@ All commands accept `--json`, `--lang en|zh`, `--budget`, `--root` and `--no-glo
|
|
|
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
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 |
|
|
120
121
|
|
|
121
122
|
## Why it is safe
|
|
122
123
|
|
package/docs/README.zh-CN.md
CHANGED
|
@@ -438,6 +438,16 @@ mcp_servers:
|
|
|
438
438
|
|
|
439
439
|
Hermes 的工具命名是 `mcp_<server>_<tool>`,即 `mcp_skills_doctor_skills_doctor`。它的 skill 在 `~/.hermes/skills`(含导入的 `openclaw-imports/`)和项目 `.hermes/skills`,`check` 都会扫描。
|
|
440
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
|
+
|
|
441
451
|
## 检测规则
|
|
442
452
|
|
|
443
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`, `.hermes/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`) |
|
|
@@ -421,6 +421,16 @@ mcp_servers:
|
|
|
421
421
|
|
|
422
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
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
|
+
|
|
424
434
|
## Rules
|
|
425
435
|
|
|
426
436
|
| Rule | Level | Meaning |
|
|
@@ -466,9 +476,11 @@ Hermes names tools `mcp_<server>_<tool>`, so the tool is `mcp_skills_doctor_skil
|
|
|
466
476
|
8. project `.grok/skills`
|
|
467
477
|
9. `~/.hermes/skills`
|
|
468
478
|
10. project `.hermes/skills`
|
|
469
|
-
11. `~/.
|
|
470
|
-
12. project `.
|
|
471
|
-
13.
|
|
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
|
|
472
484
|
|
|
473
485
|
## JSON output
|
|
474
486
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-skills-doctor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
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": {
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
"./plugin": "./src/plugin.mjs",
|
|
9
9
|
"./server": "./src/plugin-v1.mjs",
|
|
10
10
|
"./v1": "./src/plugin-v1.mjs",
|
|
11
|
-
"./audit": "./src/audit.mjs"
|
|
11
|
+
"./audit": "./src/audit.mjs",
|
|
12
|
+
"./cli": "./src/cli.mjs"
|
|
12
13
|
},
|
|
13
14
|
"bin": {
|
|
14
15
|
"skills-doctor": "./bin/skills-doctor.mjs"
|
package/src/audit.mjs
CHANGED
|
@@ -16,13 +16,14 @@ export function auditSkills({
|
|
|
16
16
|
configPath = null,
|
|
17
17
|
budget = null,
|
|
18
18
|
staleDays = null,
|
|
19
|
+
extraDirs = [],
|
|
19
20
|
} = {}) {
|
|
20
21
|
const loaded = loadConfig(cwd, configPath)
|
|
21
22
|
const config = loaded.config
|
|
22
23
|
const resolvedLang = normalizeLang(lang) ?? normalizeLang(config.lang) ?? detectLang()
|
|
23
24
|
const t = translator(resolvedLang)
|
|
24
25
|
|
|
25
|
-
const discovered = scanAll(discoverSources({ cwd, includeGlobal }))
|
|
26
|
+
const discovered = scanAll(discoverSources({ cwd, includeGlobal, extraDirs }))
|
|
26
27
|
const managedInfo = inspectManaged(discovered.scans, { t })
|
|
27
28
|
|
|
28
29
|
const effectiveBudget = Number.isFinite(budget) && budget > 0 ? budget : config.budget ?? 1200
|
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,8 @@ function parseArgs(argv) {
|
|
|
75
76
|
client: "auto",
|
|
76
77
|
profile: null,
|
|
77
78
|
dshHome: null,
|
|
79
|
+
kimiHome: null,
|
|
80
|
+
extraDirs: [],
|
|
78
81
|
configDir: null,
|
|
79
82
|
noNpm: false,
|
|
80
83
|
html: null,
|
|
@@ -162,6 +165,12 @@ function parseArgs(argv) {
|
|
|
162
165
|
case "--dsh-home":
|
|
163
166
|
options.dshHome = path.resolve(args[++i] ?? ".")
|
|
164
167
|
break
|
|
168
|
+
case "--kimi-home":
|
|
169
|
+
options.kimiHome = path.resolve(args[++i] ?? ".")
|
|
170
|
+
break
|
|
171
|
+
case "--extra-dir":
|
|
172
|
+
options.extraDirs.push(path.resolve(args[++i] ?? "."))
|
|
173
|
+
break
|
|
165
174
|
case "--no-npm":
|
|
166
175
|
options.noNpm = true
|
|
167
176
|
break
|
|
@@ -600,8 +609,8 @@ export async function main(argv) {
|
|
|
600
609
|
process.exit(2)
|
|
601
610
|
}
|
|
602
611
|
|
|
603
|
-
if (!["auto", "opencode", "desktop", "dsh"].includes(options.client)) {
|
|
604
|
-
console.error("unsupported --client: expected auto, opencode, desktop or
|
|
612
|
+
if (!["auto", "opencode", "desktop", "dsh", "kimi"].includes(options.client)) {
|
|
613
|
+
console.error("unsupported --client: expected auto, opencode, desktop, dsh or kimi")
|
|
605
614
|
process.exitCode = 2
|
|
606
615
|
return
|
|
607
616
|
}
|
|
@@ -648,6 +657,25 @@ export async function main(argv) {
|
|
|
648
657
|
return
|
|
649
658
|
}
|
|
650
659
|
|
|
660
|
+
if (options.command === "install" && options.client === "kimi") {
|
|
661
|
+
const result = installKimi({ home: options.kimiHome ?? undefined, dryRun: options.dryRun })
|
|
662
|
+
if (options.json) {
|
|
663
|
+
console.log(JSON.stringify(result, null, 2))
|
|
664
|
+
} else {
|
|
665
|
+
console.log(bold(t("kimi.title")))
|
|
666
|
+
if (result.home) console.log(dim(t("kimi.home", { dir: result.home })))
|
|
667
|
+
if (result.mcpFile) console.log(dim(t("kimi.mcp", { file: result.mcpFile })))
|
|
668
|
+
if (result.skillDir) console.log(dim(t("kimi.skill", { dir: result.skillDir })))
|
|
669
|
+
if (result.dryRun) console.log(dim(t("install.dryRun")))
|
|
670
|
+
for (const action of result.actions) console.log(` · ${action}`)
|
|
671
|
+
for (const error of result.errors) console.log(red(t("install.error", { error })))
|
|
672
|
+
console.log("")
|
|
673
|
+
console.log(result.errors.length ? red(t("kimi.failed")) : dim(t("kimi.restart")))
|
|
674
|
+
}
|
|
675
|
+
process.exitCode = result.errors.length ? 1 : 0
|
|
676
|
+
return
|
|
677
|
+
}
|
|
678
|
+
|
|
651
679
|
if (options.command === "install") {
|
|
652
680
|
const result = installOpenCode({
|
|
653
681
|
configDir: options.configDir ?? undefined,
|
|
@@ -698,6 +726,23 @@ export async function main(argv) {
|
|
|
698
726
|
return
|
|
699
727
|
}
|
|
700
728
|
|
|
729
|
+
if (options.command === "uninstall" && options.client === "kimi") {
|
|
730
|
+
const result = uninstallKimi({ home: options.kimiHome ?? undefined, dryRun: options.dryRun })
|
|
731
|
+
if (options.json) {
|
|
732
|
+
console.log(JSON.stringify(result, null, 2))
|
|
733
|
+
} else {
|
|
734
|
+
console.log(bold(t("kimi.uninstallTitle")))
|
|
735
|
+
if (result.home) console.log(dim(t("kimi.home", { dir: result.home })))
|
|
736
|
+
if (result.dryRun) console.log(dim(t("install.dryRun")))
|
|
737
|
+
for (const action of result.actions) console.log(` · ${action}`)
|
|
738
|
+
for (const error of result.errors) console.log(red(t("install.error", { error })))
|
|
739
|
+
console.log("")
|
|
740
|
+
console.log(result.errors.length ? red(t("kimi.failed")) : dim(t("kimi.restart")))
|
|
741
|
+
}
|
|
742
|
+
process.exitCode = result.errors.length ? 1 : 0
|
|
743
|
+
return
|
|
744
|
+
}
|
|
745
|
+
|
|
701
746
|
if (options.command === "uninstall") {
|
|
702
747
|
const result = uninstallOpenCode({
|
|
703
748
|
configDir: options.configDir ?? undefined,
|
|
@@ -773,6 +818,7 @@ export async function main(argv) {
|
|
|
773
818
|
configPath: options.config,
|
|
774
819
|
budget: options.budget,
|
|
775
820
|
staleDays: options.staleDays,
|
|
821
|
+
extraDirs: options.extraDirs,
|
|
776
822
|
})
|
|
777
823
|
const plan = planBudget(audit, { descTarget: options.descTarget, steps: options.steps })
|
|
778
824
|
if (options.json) console.log(JSON.stringify(plan, null, 2))
|
|
@@ -788,6 +834,7 @@ export async function main(argv) {
|
|
|
788
834
|
configPath: options.config,
|
|
789
835
|
budget: options.budget,
|
|
790
836
|
staleDays: options.staleDays,
|
|
837
|
+
extraDirs: options.extraDirs,
|
|
791
838
|
})
|
|
792
839
|
const result = buildCheckResult(audit, options)
|
|
793
840
|
|
|
@@ -814,7 +861,7 @@ export async function main(argv) {
|
|
|
814
861
|
return
|
|
815
862
|
}
|
|
816
863
|
|
|
817
|
-
const discovered = scanAll(discoverSources({ cwd, includeGlobal: options.global }))
|
|
864
|
+
const discovered = scanAll(discoverSources({ cwd, includeGlobal: options.global, extraDirs: options.extraDirs }))
|
|
818
865
|
const managedInfo = inspectManaged(discovered.scans, { t })
|
|
819
866
|
|
|
820
867
|
if (options.command === "scan") {
|
|
@@ -852,6 +899,7 @@ export async function main(argv) {
|
|
|
852
899
|
configPath: options.config,
|
|
853
900
|
budget: options.budget,
|
|
854
901
|
staleDays: options.staleDays,
|
|
902
|
+
extraDirs: options.extraDirs,
|
|
855
903
|
})
|
|
856
904
|
process.exitCode = runFix(audit, options, t)
|
|
857
905
|
}
|
package/src/discover.mjs
CHANGED
|
@@ -122,11 +122,13 @@ function isUnder(parent, child) {
|
|
|
122
122
|
* 8. 项目 .grok/skills
|
|
123
123
|
* 9. ~/.hermes/skills(Hermes Agent)
|
|
124
124
|
* 10. 项目 .hermes/skills
|
|
125
|
-
* 11. ~/.
|
|
126
|
-
* 12. 项目 .
|
|
127
|
-
* 13. opencode
|
|
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
|
|
128
130
|
*/
|
|
129
|
-
export function discoverSources({ cwd = process.cwd(), home = os.homedir(), includeGlobal = true } = {}) {
|
|
131
|
+
export function discoverSources({ cwd = process.cwd(), home = os.homedir(), includeGlobal = true, extraDirs = [] } = {}) {
|
|
130
132
|
const projectRoot = findProjectRoot(cwd) ?? path.resolve(cwd)
|
|
131
133
|
const projectChain = pathChain(projectRoot, cwd)
|
|
132
134
|
const sources = []
|
|
@@ -156,9 +158,18 @@ export function discoverSources({ cwd = process.cwd(), home = os.homedir(), incl
|
|
|
156
158
|
if (includeGlobal) addSource("hermes-global", "global", path.join(home, ".hermes", "skills"))
|
|
157
159
|
for (const dir of projectChain) addSource("hermes-project", "project", path.join(dir, ".hermes", "skills"))
|
|
158
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
|
+
|
|
159
164
|
if (includeGlobal) addSource("opencode-global", "global", path.join(home, ".config", "opencode", "skills"))
|
|
160
165
|
for (const dir of projectChain) addSource("opencode-project", "project", path.join(dir, ".opencode", "skills"))
|
|
161
166
|
|
|
167
|
+
for (const dir of extraDirs) {
|
|
168
|
+
if (typeof dir !== "string" || !dir.trim()) continue
|
|
169
|
+
const abs = path.resolve(dir)
|
|
170
|
+
if (fs.existsSync(abs)) addSource("extra", isUnder(projectRoot, abs) ? "project" : "global", abs)
|
|
171
|
+
}
|
|
172
|
+
|
|
162
173
|
const configFiles = []
|
|
163
174
|
if (includeGlobal) {
|
|
164
175
|
for (const file of configCandidates(path.join(home, ".config", "opencode"))) {
|
package/src/i18n.mjs
CHANGED
|
@@ -36,9 +36,11 @@ 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)
|
|
43
|
+
--extra-dir <dir> scan an additional skill directory (repeatable; e.g. ~/.openclaw/skills)
|
|
42
44
|
--no-npm install/uninstall: do not run npm (1.x)
|
|
43
45
|
--apply upgrade: run npm install -g opencode-skills-doctor@latest
|
|
44
46
|
--no-update-check skip the automatic update check for this run
|
|
@@ -129,6 +131,13 @@ Exit codes: 0 ok; 1 errors (and warnings with --strict); 2 bad arguments or runt
|
|
|
129
131
|
"dsh.skill": " skill: {dir}",
|
|
130
132
|
"dsh.restart": "Restart DSH to load the MCP tool (HMR reloads the patch when enabled; the skill updates live).",
|
|
131
133
|
"dsh.failed": "DSH integration failed.",
|
|
134
|
+
"kimi.title": "Kimi Code integration",
|
|
135
|
+
"kimi.uninstallTitle": "Remove Kimi Code integration",
|
|
136
|
+
"kimi.home": " home: {dir}",
|
|
137
|
+
"kimi.mcp": " mcp: {file}",
|
|
138
|
+
"kimi.skill": " skill: {dir}",
|
|
139
|
+
"kimi.restart": "Start a new Kimi Code session to load the MCP tool.",
|
|
140
|
+
"kimi.failed": "Kimi Code integration failed.",
|
|
132
141
|
"update.toolAvailable":
|
|
133
142
|
" ! skills-doctor {latest} is available (you have {current}); update with: {cmd} · {url}",
|
|
134
143
|
"upgrade.title": "skills-doctor upgrade",
|
|
@@ -257,9 +266,11 @@ Exit codes: 0 ok; 1 errors (and warnings with --strict); 2 bad arguments or runt
|
|
|
257
266
|
--html [file] check: 生成自包含 HTML 报告(默认 skills-report.html)
|
|
258
267
|
--open check: 写完报告后用浏览器打开
|
|
259
268
|
--config-dir <dir> install/uninstall: OpenCode 配置目录(默认 ~/.config/opencode)
|
|
260
|
-
--client <name> install/uninstall: auto | opencode | desktop | dsh(默认 auto)
|
|
269
|
+
--client <name> install/uninstall: auto | opencode | desktop | dsh | kimi(默认 auto)
|
|
261
270
|
--profile <name> dsh: 安装到哪个 profile(默认 desktop,或唯一的一个)
|
|
262
271
|
--dsh-home <dir> dsh: DSH 主目录(默认 ~/.dsh)
|
|
272
|
+
--kimi-home <dir> kimi: Kimi Code 主目录(默认 ~/.kimi-code 或 $KIMI_CODE_HOME)
|
|
273
|
+
--extra-dir <dir> 额外扫描一个 skill 目录(可重复;例如 ~/.openclaw/skills)
|
|
263
274
|
--no-npm install/uninstall: 不执行 npm(1.x)
|
|
264
275
|
--apply upgrade: 执行 npm install -g opencode-skills-doctor@latest
|
|
265
276
|
--no-update-check 本次运行跳过自动更新检查
|
|
@@ -350,6 +361,13 @@ Exit codes: 0 ok; 1 errors (and warnings with --strict); 2 bad arguments or runt
|
|
|
350
361
|
"dsh.skill": " skill: {dir}",
|
|
351
362
|
"dsh.restart": "重启 DSH 加载 MCP 工具(启用 HMR 时会自动重载 patch;skill 即时更新)。",
|
|
352
363
|
"dsh.failed": "DSH 集成失败。",
|
|
364
|
+
"kimi.title": "Kimi Code 集成",
|
|
365
|
+
"kimi.uninstallTitle": "撤销 Kimi Code 集成",
|
|
366
|
+
"kimi.home": " 主目录: {dir}",
|
|
367
|
+
"kimi.mcp": " mcp: {file}",
|
|
368
|
+
"kimi.skill": " skill: {dir}",
|
|
369
|
+
"kimi.restart": "开一个新的 Kimi Code 会话加载 MCP 工具。",
|
|
370
|
+
"kimi.failed": "Kimi Code 集成失败。",
|
|
353
371
|
"update.toolAvailable": " ! skills-doctor 有新版本 {latest}(当前 {current});更新:{cmd} · {url}",
|
|
354
372
|
"upgrade.title": "skills-doctor 自更新",
|
|
355
373
|
"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
|
+
}
|