opencode-skills-doctor 0.3.12 → 0.3.14

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
@@ -58,7 +58,7 @@ skills-doctor check
58
58
  What a run looks like:
59
59
 
60
60
  ```text
61
- skills-doctor 0.3.11
61
+ skills-doctor 0.3.13
62
62
  sources: 5 (2 exist) definitions: 29 unique IDs: 29 advertised: 29 description budget: ~3074 tokens
63
63
 
64
64
  [E] token-budget Advertised skill descriptions cost about 3074 tokens per model step, over 2x the 1200 budget
@@ -201,7 +201,7 @@ export { SkillsDoctorPlugin } from "opencode-skills-doctor/v1"
201
201
  }
202
202
  ```
203
203
 
204
- It registers the same `skills_doctor` tool (1.x has no `/skills-doctor` command hook).
204
+ It registers the same `skills_doctor` tool. 1.x plugins cannot register commands, so `skills-doctor install` writes a `/skills-doctor` markdown command.
205
205
 
206
206
  > The package root entry is the V2 plugin and requires OpenCode 2.x. The CLI works on both lines.
207
207
 
@@ -225,6 +225,19 @@ Once installed you get:
225
225
  - the `/skills-doctor` command (1.x: generated markdown command; 2.x: plugin command);
226
226
  - a one-time system-prompt warning when the skill description budget is exceeded.
227
227
 
228
+ ### Uninstall
229
+
230
+ ```powershell
231
+ npx opencode-skills-doctor uninstall
232
+ ```
233
+
234
+ - **2.x** — removes `opencode-skills-doctor` from `plugins`;
235
+ - **1.x** — removes the config entry, deletes the generated plugin entry and `/skills-doctor` command, and uninstalls the package (`--no-npm` skips that);
236
+ - only files generated by the installer are deleted — anything you edited is kept and reported;
237
+ - config files are backed up before editing; `--dry-run` previews, `--config-dir` targets a custom location.
238
+
239
+ Your data is never touched: `.skills-doctor.json`, `.skills-doctor.lock.json` and `.skills-doctor-archive/` stay until you delete them. MCP entries in other clients (Claude Code, Codex, DSH) are per-client configs and must be removed there.
240
+
228
241
  ## MCP server (Claude Code, Codex, Kimi Code, any MCP client)
229
242
 
230
243
  `skills-doctor mcp` speaks the Model Context Protocol over stdio and exposes one tool: `skills_doctor` with arguments `directory?`, `budget?`, `lang?` and `global?`. It does not need OpenCode installed.
@@ -351,7 +364,7 @@ See [SECURITY.md](SECURITY.md) for the full threat model, hardening checklist an
351
364
 
352
365
  - OpenCode **2.x**: add `"plugins": ["opencode-skills-doctor"]` to `opencode.jsonc` — done.
353
366
  - OpenCode **1.x**: needs `npm i --prefix ~/.config/opencode opencode-skills-doctor` plus a one-line local plugin file (`export { SkillsDoctorPlugin } from "opencode-skills-doctor/v1"`), because 1.x resolves plugins differently and `@opencode-ai/plugin` is an optional peer.
354
- - 1.x has **no `/skills-doctor` command** — only the `skills_doctor` tool.
367
+ - 1.x has no plugin-registered `/skills-doctor` command; `skills-doctor install` writes a markdown command file instead.
355
368
 
356
369
  **Safety / behavior**
357
370
 
@@ -58,7 +58,7 @@ skills-doctor check
58
58
  跑出来大概是这样:
59
59
 
60
60
  ```text
61
- skills-doctor 0.3.11
61
+ skills-doctor 0.3.13
62
62
  来源: 5 个(存在 2 个) 定义: 29 唯一 ID: 29 会展示: 29 描述预算: ~3074 tokens
63
63
 
64
64
  [E] token-budget 模型每步要看到的 skill 描述约 3074 tokens,超过预算 1200 的 2 倍
@@ -201,7 +201,7 @@ export { SkillsDoctorPlugin } from "opencode-skills-doctor/v1"
201
201
  }
202
202
  ```
203
203
 
204
- 它会注册同样的 `skills_doctor` 工具(1.x 没有 `/skills-doctor` 命令钩子)。
204
+ 它会注册同样的 `skills_doctor` 工具。1.x 插件无法注册命令,`skills-doctor install` 会生成 `/skills-doctor` 的 markdown 命令。
205
205
 
206
206
  > 包根入口是 V2 插件,需要 OpenCode 2.x;CLI 两条线都能用。
207
207
 
@@ -225,6 +225,19 @@ npx opencode-skills-doctor install
225
225
  - `/skills-doctor` 命令(1.x 是生成的 markdown 命令,2.x 是插件命令);
226
226
  - 当 skill 描述预算超标时,系统提示里会注入一次提醒(每个会话最多一次)。
227
227
 
228
+ ### 卸载
229
+
230
+ ```powershell
231
+ npx opencode-skills-doctor uninstall
232
+ ```
233
+
234
+ - **2.x** —— 从 `plugins` 移除 `opencode-skills-doctor`;
235
+ - **1.x** —— 移除配置条目、删除生成的插件入口和 `/skills-doctor` 命令、卸载 npm 包(`--no-npm` 可跳过);
236
+ - 只删除安装器生成的文件;你手改过的文件会保留并提示;
237
+ - 改配置前会备份;`--dry-run` 预览,`--config-dir` 指定目录。
238
+
239
+ 你的数据不会被自动删除:`.skills-doctor.json`、`.skills-doctor.lock.json`、`.skills-doctor-archive/` 需要手动清理。其他客户端(Claude Code、Codex、DSH)的 MCP 配置也需在各自配置里删除。
240
+
228
241
  ## MCP server(Claude Code、Codex、Kimi Code、任何 MCP 客户端)
229
242
 
230
243
  `skills-doctor mcp` 用 stdio 讲 Model Context Protocol,只暴露一个工具:`skills_doctor`,参数 `directory?`、`budget?`、`lang?`、`global?`。不需要装 OpenCode。
@@ -351,7 +364,7 @@ Windows 上如果 stdio 起不来 `npx`,改成 `command: cmd`、`args: ["/c",
351
364
 
352
365
  - OpenCode **2.x**:在 `opencode.jsonc` 加 `"plugins": ["opencode-skills-doctor"]` 即可。
353
366
  - OpenCode **1.x**:需要 `npm i --prefix ~/.config/opencode opencode-skills-doctor`,再加一个一行本地插件文件(`export { SkillsDoctorPlugin } from "opencode-skills-doctor/v1"`)——因为 1.x 解析插件的机制不同,且 `@opencode-ai/plugin` 是可选 peer 依赖。
354
- - 1.x **没有 `/skills-doctor` 命令**,只有 `skills_doctor` 工具。
367
+ - 1.x 没有「插件注册 /skills-doctor 命令」的能力;`skills-doctor install` 会生成 markdown 命令文件来代替。
355
368
 
356
369
  **安全性 / 行为**
357
370
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-skills-doctor",
3
- "version": "0.3.12",
3
+ "version": "0.3.14",
4
4
  "description": "Audit, fix and update agent skills: per-step context cost, shadowed duplicates, safe archive, catalog sync. CLI, OpenCode plugin and MCP server (Claude Code / Codex / any MCP client).",
5
5
  "type": "module",
6
6
  "exports": {
package/src/cli.mjs CHANGED
@@ -8,7 +8,7 @@ import { planFixes, applyFixes, hideSkills } from "./fix.mjs"
8
8
  import { auditSkills } from "./audit.mjs"
9
9
  import { loadConfig } from "./config.mjs"
10
10
  import { startMcpServer } from "./mcp.mjs"
11
- import { installOpenCode } from "./install.mjs"
11
+ import { installOpenCode, uninstallOpenCode } from "./install.mjs"
12
12
  import { padDisplay } from "./parse.mjs"
13
13
  import { detectLang, normalizeLang, translator } from "./i18n.mjs"
14
14
 
@@ -64,7 +64,7 @@ function parseArgs(argv) {
64
64
  }
65
65
 
66
66
  const args = [...argv]
67
- const commands = new Set(["check", "scan", "update", "fix", "install", "mcp", "help"])
67
+ const commands = new Set(["check", "scan", "update", "fix", "install", "uninstall", "mcp", "help"])
68
68
  if (args[0] && commands.has(args[0])) {
69
69
  options.command = args.shift()
70
70
  }
@@ -475,6 +475,28 @@ export async function main(argv) {
475
475
  return
476
476
  }
477
477
 
478
+ if (options.command === "uninstall") {
479
+ const result = uninstallOpenCode({
480
+ configDir: options.configDir ?? undefined,
481
+ dryRun: options.dryRun,
482
+ removePackage: !options.noNpm,
483
+ })
484
+ if (options.json) {
485
+ console.log(JSON.stringify(result, null, 2))
486
+ } else {
487
+ console.log(bold(t("uninstall.title")))
488
+ console.log(dim(t("install.version", { version: result.version ?? "unknown" })))
489
+ console.log(dim(t("install.configDir", { dir: result.configDir })))
490
+ if (result.dryRun) console.log(dim(t("install.dryRun")))
491
+ for (const action of result.actions) console.log(` · ${action}`)
492
+ for (const error of result.errors) console.log(red(t("install.error", { error })))
493
+ console.log("")
494
+ console.log(result.errors.length ? red(t("install.failed")) : dim(t("uninstall.next")))
495
+ }
496
+ process.exitCode = result.errors.length ? 1 : 0
497
+ return
498
+ }
499
+
478
500
  if (options.command === "check") {
479
501
  const audit = auditSkills({
480
502
  cwd,
package/src/i18n.mjs CHANGED
@@ -11,6 +11,7 @@ Usage:
11
11
  skills-doctor fix [options] archive shadowed definitions (safe structural fixes)
12
12
  skills-doctor update [options] compare and download skills from HTTP catalogs
13
13
  skills-doctor install [options] wire the plugin into OpenCode (1.x / 2.x)
14
+ skills-doctor uninstall [options] remove it again (config entry, generated files, package)
14
15
  skills-doctor mcp run as an MCP server (Claude Code, Codex, any MCP client)
15
16
 
16
17
  Options:
@@ -28,8 +29,8 @@ Options:
28
29
  --into <dir> update: output directory (default: .opencode/skills)
29
30
  --check-only update: only report available updates
30
31
  --prune update: delete skills that disappeared from the catalog
31
- --config-dir <dir> install: OpenCode config directory (default: ~/.config/opencode)
32
- --no-npm install: do not run npm install (1.x)
32
+ --config-dir <dir> install/uninstall: OpenCode config directory (default: ~/.config/opencode)
33
+ --no-npm install/uninstall: do not run npm (1.x)
33
34
  -h, --help show this help
34
35
  -v, --version print version
35
36
 
@@ -84,6 +85,8 @@ Exit codes: 0 ok; 1 errors (and warnings with --strict); 2 bad arguments or runt
84
85
  "install.error": " ✗ {error}",
85
86
  "install.failed": "Integration failed.",
86
87
  "install.next": "Restart OpenCode to activate.",
88
+ "uninstall.title": "Remove OpenCode integration",
89
+ "uninstall.next": "Restart OpenCode to finish removal.",
87
90
  "update.lockTooNew": " ! lock is newer than this tool; parsed in compatibility mode (unknown fields ignored)",
88
91
  "update.detector": " check now sees these skills ({dir} is where opencode reads)",
89
92
  "update.noCatalogs": "No HTTP catalog configured. Add https://... to the skills array in opencode.json(c).",
@@ -167,6 +170,7 @@ Exit codes: 0 ok; 1 errors (and warnings with --strict); 2 bad arguments or runt
167
170
  skills-doctor fix [选项] 归档被遮蔽的定义(安全的结构性修复)
168
171
  skills-doctor update [选项] 对比并拉取 HTTP catalog 里的 skill
169
172
  skills-doctor install [选项] 把插件接入 OpenCode(1.x / 2.x)
173
+ skills-doctor uninstall [选项] 撤销安装(配置条目、生成的文件、npm 包)
170
174
  skills-doctor mcp 以 MCP server 方式运行(Claude Code、Codex 等任何 MCP 客户端)
171
175
 
172
176
  选项:
@@ -184,8 +188,8 @@ Exit codes: 0 ok; 1 errors (and warnings with --strict); 2 bad arguments or runt
184
188
  --into <dir> update 的落盘目录(默认 .opencode/skills)
185
189
  --check-only update 只报告可用更新,不下载
186
190
  --prune update: 删除 catalog 中已移除的 skill
187
- --config-dir <dir> install: OpenCode 配置目录(默认 ~/.config/opencode)
188
- --no-npm install: 不执行 npm install(1.x)
191
+ --config-dir <dir> install/uninstall: OpenCode 配置目录(默认 ~/.config/opencode)
192
+ --no-npm install/uninstall: 不执行 npm(1.x)
189
193
  -h, --help 帮助
190
194
  -v, --version 版本
191
195
 
@@ -240,6 +244,8 @@ Exit codes: 0 ok; 1 errors (and warnings with --strict); 2 bad arguments or runt
240
244
  "install.error": " ✗ {error}",
241
245
  "install.failed": "集成失败。",
242
246
  "install.next": "重启 OpenCode 生效。",
247
+ "uninstall.title": "撤销 OpenCode 集成",
248
+ "uninstall.next": "重启 OpenCode 完成卸载。",
243
249
  "update.lockTooNew": " ! lock 版本比本工具新,已按兼容模式解析(未知字段忽略)",
244
250
  "update.detector": " check 现在可以直接检测到这些 skill({dir} 是 opencode 会读取的目录)",
245
251
  "update.noCatalogs": "没有配置 HTTP catalog。在 opencode.json(c) 的 skills 数组里写 https://... 即可。",
package/src/install.mjs CHANGED
@@ -209,3 +209,111 @@ export function installOpenCode({ configDir = defaultConfigDir(), version = null
209
209
  act("restart OpenCode; the skills_doctor tool and /skills-doctor command will be available")
210
210
  return { version: resolvedVersion, major, configDir: dir, configFile: cfgFile, actions, errors, dryRun }
211
211
  }
212
+
213
+ function removePackage(dir) {
214
+ const npm = process.platform === "win32" ? "npm.cmd" : "npm"
215
+ const result = spawnSync(npm, ["uninstall", "--prefix", dir, PACKAGE_NAME, "--no-audit", "--no-fund"], {
216
+ stdio: "inherit",
217
+ env: process.env,
218
+ timeout: 180000,
219
+ })
220
+ return result.status === 0
221
+ }
222
+
223
+ /**
224
+ * 撤销 install:移除配置里的插件条目、删除安装器生成的文件、可选卸载 npm 包。
225
+ * 只删除「由安装器生成」的文件,用户自己改过的一律保留并提示。
226
+ */
227
+ export function uninstallOpenCode({ configDir = defaultConfigDir(), version = null, dryRun = false, removePackage: withPackage = true } = {}) {
228
+ const dir = path.resolve(configDir)
229
+ const actions = []
230
+ const errors = []
231
+ const dry = (message) => actions.push(`[dry-run] ${message}`)
232
+ const act = (message) => actions.push(message)
233
+
234
+ const cfgFile = configFile(dir)
235
+ const existingText = readText(cfgFile)
236
+ let config = {}
237
+ if (existingText !== null) {
238
+ try {
239
+ config = readJsonc(cfgFile)
240
+ } catch (err) {
241
+ errors.push(`cannot parse ${cfgFile}: ${err.message}`)
242
+ return { version, major: null, configDir: dir, configFile: cfgFile, actions, errors, dryRun }
243
+ }
244
+ }
245
+
246
+ const resolvedVersion = version ?? detectVersion()
247
+ const inferredMajor = config.plugin !== undefined ? 1 : 2
248
+ const major = resolvedVersion ? Number(resolvedVersion.split(".")[0]) : inferredMajor
249
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-")
250
+
251
+ if (major >= 2) {
252
+ if (Array.isArray(config.plugins) && config.plugins.includes(PACKAGE_NAME)) {
253
+ const next = config.plugins.filter((entry) => entry !== PACKAGE_NAME)
254
+ if (!dryRun) {
255
+ const backupFile = backup(cfgFile, stamp)
256
+ if (backupFile) act(`backup ${backupFile}`)
257
+ writeJson(cfgFile, { ...config, plugins: next })
258
+ act(`removed "${PACKAGE_NAME}" from plugins in ${cfgFile}`)
259
+ } else dry(`remove "${PACKAGE_NAME}" from plugins in ${cfgFile}`)
260
+ } else {
261
+ act(`"${PACKAGE_NAME}" is not configured in plugins`)
262
+ }
263
+ act("restart OpenCode to finish removal")
264
+ return { version: resolvedVersion, major, configDir: dir, configFile: cfgFile, actions, errors, dryRun }
265
+ }
266
+
267
+ const pluginFile = path.join(dir, "plugins", PLUGIN_FILE)
268
+ const commandFile = path.join(dir, "command", COMMAND_FILE)
269
+
270
+ if (Array.isArray(config.plugin) && config.plugin.includes(pluginFile)) {
271
+ const next = config.plugin.filter((entry) => entry !== pluginFile)
272
+ if (!dryRun) {
273
+ const backupFile = backup(cfgFile, stamp)
274
+ if (backupFile) act(`backup ${backupFile}`)
275
+ writeJson(cfgFile, { ...config, plugin: next })
276
+ act(`removed plugin entry from ${cfgFile}`)
277
+ } else dry(`remove plugin entry from ${cfgFile}`)
278
+ } else {
279
+ act("plugin entry is not configured")
280
+ }
281
+
282
+ if (fs.existsSync(pluginFile)) {
283
+ const content = readText(pluginFile) ?? ""
284
+ if (content.includes("Generated by skills-doctor install")) {
285
+ if (!dryRun) {
286
+ fs.rmSync(pluginFile, { force: true })
287
+ act(`deleted ${pluginFile}`)
288
+ } else dry(`delete ${pluginFile}`)
289
+ } else {
290
+ act(`kept ${pluginFile} (not generated by skills-doctor)`)
291
+ }
292
+ }
293
+
294
+ if (fs.existsSync(commandFile)) {
295
+ const content = readText(commandFile) ?? ""
296
+ if (content.includes("skills_doctor tool")) {
297
+ if (!dryRun) {
298
+ fs.rmSync(commandFile, { force: true })
299
+ act(`deleted ${commandFile}`)
300
+ } else dry(`delete ${commandFile}`)
301
+ } else {
302
+ act(`kept ${commandFile} (not generated by skills-doctor)`)
303
+ }
304
+ }
305
+
306
+ const hasPackage = fs.existsSync(path.join(dir, "node_modules", PACKAGE_NAME, "package.json"))
307
+ if (withPackage && hasPackage) {
308
+ if (dryRun) {
309
+ dry(`uninstall ${PACKAGE_NAME} from ${dir}`)
310
+ } else {
311
+ act(`uninstalling ${PACKAGE_NAME} from ${dir}`)
312
+ if (removePackage(dir)) act(`uninstalled ${PACKAGE_NAME}`)
313
+ else errors.push(`npm uninstall failed for ${PACKAGE_NAME}`)
314
+ }
315
+ }
316
+
317
+ act("restart OpenCode to finish removal")
318
+ return { version: resolvedVersion, major, configDir: dir, configFile: cfgFile, actions, errors, dryRun }
319
+ }