opencode-skills-doctor 0.4.0 → 0.4.2

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
@@ -22,7 +22,7 @@ Local scanner, linter, fixer, updater and **OpenCode plugin** for agent skills
22
22
  | **Reports** | terminal, JSON, or a self-contained HTML report (`--html [file] --open`) |
23
23
  | **Cost** | CLI: 0 tokens. Plugin: ~191 tokens/step for the tool definition, plus a one-time warning per session |
24
24
  | **Requirements** | Node ≥ 20, zero runtime dependencies |
25
- | **Quality** | 49 tests, CI on ubuntu/macos/windows × Node 20/22/24 |
25
+ | **Quality** | 50 tests, CI on ubuntu/macos/windows × Node 20/22/24 |
26
26
 
27
27
  ## Why
28
28
 
@@ -59,7 +59,7 @@ skills-doctor check
59
59
  What a run looks like:
60
60
 
61
61
  ```text
62
- skills-doctor 0.3.13
62
+ skills-doctor 0.4.1
63
63
  sources: 5 (2 exist) definitions: 29 unique IDs: 29 advertised: 29 description budget: ~3074 tokens
64
64
 
65
65
  [E] token-budget Advertised skill descriptions cost about 3074 tokens per model step, over 2x the 1200 budget
@@ -157,6 +157,30 @@ Local execution is free, but any tool registered with the model is visible on ev
157
157
 
158
158
  So the plugin trades a small fixed overhead for the agent being able to audit by itself. In the example above the skill descriptions cost 3,074 tokens per step; dropping that to the 1,200 budget saves more than 10× the plugin overhead. If you want **zero** context cost, don't install the plugin — run `npx opencode-skills-doctor check` from the terminal or CI instead.
159
159
 
160
+ ## Self-update
161
+
162
+ `skills-doctor` checks npm for a newer version of itself at most once per 24 hours (skipped in CI, non-TTY and `--json` runs):
163
+
164
+ ```powershell
165
+ skills-doctor upgrade # check now
166
+ skills-doctor upgrade --open # check and open the GitHub release page
167
+ skills-doctor upgrade --apply # npm install -g opencode-skills-doctor@latest
168
+ ```
169
+
170
+ Opt out permanently in `.skills-doctor.json`:
171
+
172
+ ```json
173
+ { "checkUpdates": "off" }
174
+ ```
175
+
176
+ | Value | Behavior |
177
+ |---|---|
178
+ | `"notify"` (default) | print a one-line notice when a newer version exists |
179
+ | `"open"` | same, plus opens the release page in the browser |
180
+ | `"off"` | never check |
181
+
182
+ Per run: `--no-update-check`. Per environment: `SKILLS_DOCTOR_UPDATE_CHECK=0`. The plugin injects the same notice into the system prompt once per session when an update exists. Cached state lives in `~/.skills-doctor/self-update.json`.
183
+
160
184
  ## Configuration
161
185
 
162
186
  Optional `.skills-doctor.json` in the working directory (or pass `--config <file>`):
@@ -166,12 +190,13 @@ Optional `.skills-doctor.json` in the working directory (or pass `--config <file
166
190
  "budget": 1200,
167
191
  "staleDays": 180,
168
192
  "lang": "en",
193
+ "checkUpdates": "notify",
169
194
  "rules": { "long-description": "off", "hidden-skill": "info" },
170
195
  "ignore": ["legacy-skill"]
171
196
  }
172
197
  ```
173
198
 
174
- `rules` accepts `off`, `info`, `warn` or `error`; `ignore` drops findings that only involve those IDs. CLI flags override the file.
199
+ `rules` accepts `off`, `info`, `warn` or `error`; `ignore` drops findings that only involve those IDs; `checkUpdates` is `notify` / `open` / `off`. CLI flags override the file.
175
200
 
176
201
  ## OpenCode plugin
177
202
 
@@ -186,8 +211,10 @@ The package also exports an OpenCode 2.x plugin:
186
211
 
187
212
  It registers:
188
213
 
189
- - the `skills_doctor` tool (namespace `skills`) — the agent can audit with `{ directory?, budget? }` and gets a markdown report;
190
- - the `/skills-doctor` command — asks the agent to run the audit and summarize fixes.
214
+ - the `skills_doctor` tool (namespace `skills`) — the agent can audit with `{ directory?, budget?, html? }` and gets a markdown report;
215
+ - the `/skills-doctor` command — asks the agent to run the audit and summarize fixes;
216
+ - `/skills-report` — asks where to save an HTML report, then writes it;
217
+ - `/skills-upgrade` — checks whether skills-doctor itself has a newer version.
191
218
 
192
219
  `@opencode/plugin` is an optional peer dependency; OpenCode provides it at runtime.
193
220
 
@@ -228,7 +255,7 @@ npx opencode-skills-doctor install
228
255
  The installer detects your OpenCode version and wires everything up:
229
256
 
230
257
  - **2.x** — adds `"plugins": ["opencode-skills-doctor"]` to `opencode.json(c)`;
231
- - **1.x** — installs the package into `~/.config/opencode`, writes the local plugin entry, adds a `/skills-doctor` command, sets `"type": "module"`;
258
+ - **1.x** — installs the package into `~/.config/opencode`, writes the local plugin entry, generates `/skills-doctor`, `/skills-report` and `/skills-upgrade` markdown commands, sets `"type": "module"`;
232
259
  - backs up the config file as `<config>.bak-<timestamp>` before editing.
233
260
 
234
261
  Use `--dry-run` to preview, `--config-dir <dir>` for a custom config location, and `--no-npm` to skip the package install.
@@ -238,6 +265,7 @@ Once installed you get:
238
265
  - the `skills_doctor` tool (with an optional `html` argument to write a report to a path you choose);
239
266
  - the `/skills-doctor` command (1.x: generated markdown command; 2.x: plugin command);
240
267
  - the `/skills-report` command (2.x) that asks where to save the HTML report;
268
+ - the `/skills-upgrade` command (2.x) that checks for a newer skills-doctor;
241
269
  - a one-time system-prompt warning when the skill description budget is exceeded.
242
270
 
243
271
  ### Uninstall
@@ -440,13 +468,13 @@ Executing code is free, but the tool definition is part of the model's context.
440
468
 
441
469
  ## Roadmap
442
470
 
443
- - v0.4: content-aware suggestions (which skills are never used), catalog signature verification
471
+ - v0.5: CI governance (baseline that only reports new issues, official GitHub Action), content-aware suggestions (which skills are never used), catalog signature verification
444
472
  - Later: V1/Claude skill migration report
445
473
 
446
474
  ## Development and contributing
447
475
 
448
476
  ```powershell
449
- npm test # 41 tests (node test/doctor.test.mjs)
477
+ npm test # 50 tests (node test/doctor.test.mjs)
450
478
  npm run bench # lint timing for 100/500/1000 skills
451
479
  ```
452
480
 
@@ -22,7 +22,7 @@ OpenCode skill 的**本地检测器 + 更新器 + 修复器**,并附带 **Open
22
22
  | **报告** | 终端、JSON,或自包含 HTML(`--html [file] --open`) |
23
23
  | **成本** | CLI 0 token;插件工具定义约 191/步,另有每会话一次预警 |
24
24
  | **要求** | Node ≥ 20,零运行时依赖 |
25
- | **质量** | 49 个测试,ubuntu/macos/windows × Node 20/22/24 CI |
25
+ | **质量** | 50 个测试,ubuntu/macos/windows × Node 20/22/24 CI |
26
26
 
27
27
  ## 为什么需要它
28
28
 
@@ -59,7 +59,7 @@ skills-doctor check
59
59
  跑出来大概是这样:
60
60
 
61
61
  ```text
62
- skills-doctor 0.3.13
62
+ skills-doctor 0.4.1
63
63
  来源: 5 个(存在 2 个) 定义: 29 唯一 ID: 29 会展示: 29 描述预算: ~3074 tokens
64
64
 
65
65
  [E] token-budget 模型每步要看到的 skill 描述约 3074 tokens,超过预算 1200 的 2 倍
@@ -157,6 +157,30 @@ skills-doctor update --into .\team-skills
157
157
 
158
158
  所以插件是用固定的少量开销,换「模型能自己体检」。上面的例子里 skill 描述每步 3074 tokens,压到 1200 预算能省下 10 倍于插件开销的 token。想要**零上下文成本**,就别装插件——用 `npx opencode-skills-doctor check` 在终端或 CI 里跑。
159
159
 
160
+ ## 自更新
161
+
162
+ `skills-doctor` 最多每 24 小时查一次 npm 上自己有没有新版本(CI、非 TTY、`--json` 时自动跳过):
163
+
164
+ ```powershell
165
+ skills-doctor upgrade # 立即检查
166
+ skills-doctor upgrade --open # 检查并打开 GitHub Release 页面
167
+ skills-doctor upgrade --apply # npm install -g opencode-skills-doctor@latest
168
+ ```
169
+
170
+ 在 `.skills-doctor.json` 里永久关闭:
171
+
172
+ ```json
173
+ { "checkUpdates": "off" }
174
+ ```
175
+
176
+ | 取值 | 行为 |
177
+ |---|---|
178
+ | `"notify"`(默认) | 有新版本时打印一行提示 |
179
+ | `"open"` | 提示 + 自动打开 Release 页面 |
180
+ | `"off"` | 完全不检查 |
181
+
182
+ 单次关闭:`--no-update-check`;环境变量:`SKILLS_DOCTOR_UPDATE_CHECK=0`。插件在有新版本时会把提示注入系统提示(每个会话一次)。检查缓存位于 `~/.skills-doctor/self-update.json`。
183
+
160
184
  ## 配置
161
185
 
162
186
  在工作目录放一个可选的 `.skills-doctor.json`(或用 `--config <file>` 指定):
@@ -166,12 +190,13 @@ skills-doctor update --into .\team-skills
166
190
  "budget": 1200,
167
191
  "staleDays": 180,
168
192
  "lang": "en",
193
+ "checkUpdates": "notify",
169
194
  "rules": { "long-description": "off", "hidden-skill": "info" },
170
195
  "ignore": ["legacy-skill"]
171
196
  }
172
197
  ```
173
198
 
174
- `rules` 接受 `off` / `info` / `warn` / `error`;`ignore` 会丢掉只涉及这些 ID 的 finding。命令行参数优先级高于配置文件。
199
+ `rules` 接受 `off` / `info` / `warn` / `error`;`ignore` 会丢掉只涉及这些 ID 的 finding;`checkUpdates` 取 `notify` / `open` / `off`。命令行参数优先级高于配置文件。
175
200
 
176
201
  ## OpenCode 插件
177
202
 
@@ -186,8 +211,10 @@ skills-doctor update --into .\team-skills
186
211
 
187
212
  它会注册:
188
213
 
189
- - `skills_doctor` 工具(命名空间 `skills`)—— agent 传 `{ directory?, budget? }` 就能拿到 markdown 审计报告;
190
- - `/skills-doctor` 命令 —— 让 agent 跑一次审计并总结修复建议。
214
+ - `skills_doctor` 工具(命名空间 `skills`)—— agent 传 `{ directory?, budget?, html? }` 就能拿到 markdown 审计报告;
215
+ - `/skills-doctor` 命令 —— 让 agent 跑一次审计并总结修复建议;
216
+ - `/skills-report` —— 先问你要把 HTML 报告存到哪里,再写文件;
217
+ - `/skills-upgrade` —— 检查 skills-doctor 自身有没有新版本。
191
218
 
192
219
  `@opencode/plugin` 是可选 peer 依赖,运行时由 OpenCode 提供。
193
220
 
@@ -228,7 +255,7 @@ npx opencode-skills-doctor install
228
255
  安装器会自动识别 OpenCode 版本并接好:
229
256
 
230
257
  - **2.x** —— 在 `opencode.json(c)` 里加 `"plugins": ["opencode-skills-doctor"]`;
231
- - **1.x** —— 把包装进 `~/.config/opencode`、写本地插件入口、生成 `/skills-doctor` 命令、补 `"type": "module"`;
258
+ - **1.x** —— 把包装进 `~/.config/opencode`、写本地插件入口、生成 `/skills-doctor`、`/skills-report`、`/skills-upgrade` 三个 markdown 命令、补 `"type": "module"`;
232
259
  - 改配置前会备份为 `<config>.bak-<timestamp>`。
233
260
 
234
261
  支持 `--dry-run` 预览、`--config-dir <dir>` 指定配置目录、`--no-npm` 跳过装包。
@@ -238,6 +265,7 @@ npx opencode-skills-doctor install
238
265
  - `skills_doctor` 工具(可选 `html` 参数,把报告写到指定路径);
239
266
  - `/skills-doctor` 命令(1.x 是生成的 markdown 命令,2.x 是插件命令);
240
267
  - `/skills-report` 命令(2.x)—— 会先问你要把 HTML 报告存到哪里;
268
+ - `/skills-upgrade` 命令(2.x)—— 检查 skills-doctor 自身是否有新版本;
241
269
  - 当 skill 描述预算超标时,系统提示里会注入一次提醒(每个会话最多一次)。
242
270
 
243
271
  ### 卸载
@@ -440,13 +468,13 @@ token 是启发式估算,1200 是建议预算。可以用 `--budget` 或 `.ski
440
468
 
441
469
  ## Roadmap
442
470
 
443
- - v0.4:内容层面的建议(哪些 skill 从未被使用)、catalog 签名校验
471
+ - v0.5:CI 治理(baseline 只报新增问题、官方 GitHub Action)、内容层建议(哪些 skill 从未被使用)、catalog 签名校验
444
472
  - 后续:V1/Claude skill 兼容迁移报告
445
473
 
446
474
  ## 开发与贡献
447
475
 
448
476
  ```powershell
449
- npm test # 41 个测试(node test/doctor.test.mjs)
477
+ npm test # 50 个测试(node test/doctor.test.mjs)
450
478
  npm run bench # 100/500/1000 个 skill 的 lint 耗时
451
479
  ```
452
480
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-skills-doctor",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
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 and Cursor skill layouts.",
5
5
  "type": "module",
6
6
  "exports": {
package/src/cli.mjs CHANGED
@@ -10,6 +10,14 @@ import { loadConfig } from "./config.mjs"
10
10
  import { startMcpServer } from "./mcp.mjs"
11
11
  import { installOpenCode, uninstallOpenCode } from "./install.mjs"
12
12
  import { writeHtmlReport, openReport } from "./html.mjs"
13
+ import {
14
+ checkForUpdate,
15
+ resolveMode,
16
+ upgradeCommand,
17
+ applyUpgrade,
18
+ openReleasePage,
19
+ RELEASES_URL,
20
+ } from "./self-update.mjs"
13
21
  import { padDisplay } from "./parse.mjs"
14
22
  import { detectLang, normalizeLang, translator } from "./i18n.mjs"
15
23
 
@@ -66,12 +74,14 @@ function parseArgs(argv) {
66
74
  noNpm: false,
67
75
  html: null,
68
76
  open: false,
77
+ apply: false,
78
+ updateCheck: true,
69
79
  help: false,
70
80
  version: false,
71
81
  }
72
82
 
73
83
  const args = [...argv]
74
- const commands = new Set(["check", "scan", "update", "fix", "install", "uninstall", "mcp", "help"])
84
+ const commands = new Set(["check", "scan", "update", "fix", "install", "uninstall", "upgrade", "mcp", "help"])
75
85
  if (args[0] && commands.has(args[0])) {
76
86
  options.command = args.shift()
77
87
  }
@@ -145,6 +155,12 @@ function parseArgs(argv) {
145
155
  case "--open":
146
156
  options.open = true
147
157
  break
158
+ case "--apply":
159
+ options.apply = true
160
+ break
161
+ case "--no-update-check":
162
+ options.updateCheck = false
163
+ break
148
164
  case "-h":
149
165
  case "--help":
150
166
  options.help = true
@@ -446,6 +462,27 @@ function runFix(audit, options, t) {
446
462
  return errors.length ? 1 : 0
447
463
  }
448
464
 
465
+ async function maybeNotifyUpdate({ options, config, t }) {
466
+ if (!options.updateCheck) return
467
+ if (options.json || !process.stdout.isTTY || process.env.CI) return
468
+ const mode = resolveMode(config)
469
+ if (mode === "off") return
470
+ const update = await checkForUpdate({ current: pkg.version, mode })
471
+ if (!update) return
472
+ console.log("")
473
+ console.log(
474
+ dim(
475
+ t("update.toolAvailable", {
476
+ latest: update.latest,
477
+ current: pkg.version,
478
+ cmd: "npx opencode-skills-doctor@latest check",
479
+ url: update.url,
480
+ }),
481
+ ),
482
+ )
483
+ if (mode === "open") openReleasePage({ url: update.url })
484
+ }
485
+
449
486
  export async function main(argv) {
450
487
  const options = parseArgs(argv)
451
488
 
@@ -517,6 +554,44 @@ export async function main(argv) {
517
554
  return
518
555
  }
519
556
 
557
+ if (options.command === "upgrade") {
558
+ const update = await checkForUpdate({ current: pkg.version, force: true })
559
+ if (options.json) {
560
+ console.log(
561
+ JSON.stringify(
562
+ {
563
+ current: pkg.version,
564
+ latest: update?.latest ?? pkg.version,
565
+ updateAvailable: Boolean(update),
566
+ url: update?.url ?? RELEASES_URL,
567
+ command: upgradeCommand(),
568
+ },
569
+ null,
570
+ 2,
571
+ ),
572
+ )
573
+ } else {
574
+ console.log(bold(t("upgrade.title")))
575
+ console.log(dim(t("upgrade.current", { version: pkg.version })))
576
+ if (update) {
577
+ console.log(t("upgrade.available", { latest: update.latest, current: pkg.version }))
578
+ console.log(dim(t("upgrade.command", { cmd: upgradeCommand() })))
579
+ console.log(dim(t("upgrade.open", { url: update.url })))
580
+ if (options.open) openReleasePage({ url: update.url })
581
+ if (options.apply) {
582
+ if (applyUpgrade()) console.log(green(t("upgrade.done")))
583
+ else {
584
+ console.log(red(t("upgrade.failed", { cmd: upgradeCommand() })))
585
+ process.exitCode = 1
586
+ }
587
+ }
588
+ } else {
589
+ console.log(green(t("upgrade.upToDate")))
590
+ }
591
+ }
592
+ return
593
+ }
594
+
520
595
  if (options.command === "check") {
521
596
  const audit = auditSkills({
522
597
  cwd,
@@ -545,6 +620,7 @@ export async function main(argv) {
545
620
  } else {
546
621
  printCheck(audit, result, options, t)
547
622
  if (htmlPath) console.log(dim(t("check.htmlSaved", { path: htmlPath })))
623
+ await maybeNotifyUpdate({ options, config: loaded.config, t })
548
624
  }
549
625
  process.exitCode = result.exitCode
550
626
  return
@@ -571,6 +647,7 @@ export async function main(argv) {
571
647
  return
572
648
  }
573
649
  printScan(discovered, options, managedInfo.managed, t)
650
+ await maybeNotifyUpdate({ options, config: loaded.config, t })
574
651
  return
575
652
  }
576
653
 
package/src/config.mjs CHANGED
@@ -15,6 +15,10 @@ function normalize(raw) {
15
15
  }
16
16
  }
17
17
  if (Array.isArray(raw?.ignore)) config.ignore = raw.ignore.filter((id) => typeof id === "string")
18
+ const checkUpdates = raw?.checkUpdates ?? raw?.updateCheck
19
+ if (checkUpdates === false || checkUpdates === "off") config.checkUpdates = "off"
20
+ else if (checkUpdates === true) config.checkUpdates = "notify"
21
+ else if (checkUpdates === "notify" || checkUpdates === "open") config.checkUpdates = checkUpdates
18
22
  return config
19
23
  }
20
24
 
package/src/i18n.mjs CHANGED
@@ -12,6 +12,7 @@ Usage:
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
14
  skills-doctor uninstall [options] remove it again (config entry, generated files, package)
15
+ skills-doctor upgrade [options] check for a newer skills-doctor itself (--apply installs it)
15
16
  skills-doctor mcp run as an MCP server (Claude Code, Codex, any MCP client)
16
17
 
17
18
  Options:
@@ -33,6 +34,8 @@ Options:
33
34
  --open check: open the HTML report after writing it
34
35
  --config-dir <dir> install/uninstall: OpenCode config directory (default: ~/.config/opencode)
35
36
  --no-npm install/uninstall: do not run npm (1.x)
37
+ --apply upgrade: run npm install -g opencode-skills-doctor@latest
38
+ --no-update-check skip the automatic update check for this run
36
39
  -h, --help show this help
37
40
  -v, --version print version
38
41
 
@@ -90,6 +93,16 @@ Exit codes: 0 ok; 1 errors (and warnings with --strict); 2 bad arguments or runt
90
93
  "install.next": "Restart OpenCode to activate.",
91
94
  "uninstall.title": "Remove OpenCode integration",
92
95
  "uninstall.next": "Restart OpenCode to finish removal.",
96
+ "update.toolAvailable":
97
+ " ! skills-doctor {latest} is available (you have {current}); update with: {cmd} · {url}",
98
+ "upgrade.title": "skills-doctor upgrade",
99
+ "upgrade.current": " current: {version}",
100
+ "upgrade.available": "New version available: {latest} (current {current})",
101
+ "upgrade.command": " upgrade with: {cmd}",
102
+ "upgrade.open": " release notes: {url}",
103
+ "upgrade.upToDate": "Already up to date.",
104
+ "upgrade.done": "Upgraded to the latest version.",
105
+ "upgrade.failed": "Upgrade failed; run it manually: {cmd}",
93
106
  "update.lockTooNew": " ! lock is newer than this tool; parsed in compatibility mode (unknown fields ignored)",
94
107
  "update.detector": " check now sees these skills ({dir} is where opencode reads)",
95
108
  "update.noCatalogs": "No HTTP catalog configured. Add https://... to the skills array in opencode.json(c).",
@@ -184,6 +197,7 @@ Exit codes: 0 ok; 1 errors (and warnings with --strict); 2 bad arguments or runt
184
197
  skills-doctor update [选项] 对比并拉取 HTTP catalog 里的 skill
185
198
  skills-doctor install [选项] 把插件接入 OpenCode(1.x / 2.x)
186
199
  skills-doctor uninstall [选项] 撤销安装(配置条目、生成的文件、npm 包)
200
+ skills-doctor upgrade [选项] 检查 skills-doctor 自身是否有新版本(--apply 直接装)
187
201
  skills-doctor mcp 以 MCP server 方式运行(Claude Code、Codex 等任何 MCP 客户端)
188
202
 
189
203
  选项:
@@ -205,6 +219,8 @@ Exit codes: 0 ok; 1 errors (and warnings with --strict); 2 bad arguments or runt
205
219
  --open check: 写完报告后用浏览器打开
206
220
  --config-dir <dir> install/uninstall: OpenCode 配置目录(默认 ~/.config/opencode)
207
221
  --no-npm install/uninstall: 不执行 npm(1.x)
222
+ --apply upgrade: 执行 npm install -g opencode-skills-doctor@latest
223
+ --no-update-check 本次运行跳过自动更新检查
208
224
  -h, --help 帮助
209
225
  -v, --version 版本
210
226
 
@@ -262,6 +278,15 @@ Exit codes: 0 ok; 1 errors (and warnings with --strict); 2 bad arguments or runt
262
278
  "install.next": "重启 OpenCode 生效。",
263
279
  "uninstall.title": "撤销 OpenCode 集成",
264
280
  "uninstall.next": "重启 OpenCode 完成卸载。",
281
+ "update.toolAvailable": " ! skills-doctor 有新版本 {latest}(当前 {current});更新:{cmd} · {url}",
282
+ "upgrade.title": "skills-doctor 自更新",
283
+ "upgrade.current": " 当前版本: {version}",
284
+ "upgrade.available": "发现新版本: {latest}(当前 {current})",
285
+ "upgrade.command": " 更新命令: {cmd}",
286
+ "upgrade.open": " 更新说明: {url}",
287
+ "upgrade.upToDate": "已是最新版本。",
288
+ "upgrade.done": "已升级到最新版本。",
289
+ "upgrade.failed": "升级失败,请手动执行: {cmd}",
265
290
  "update.lockTooNew": " ! lock 版本比本工具新,已按兼容模式解析(未知字段忽略)",
266
291
  "update.detector": " check 现在可以直接检测到这些 skill({dir} 是 opencode 会读取的目录)",
267
292
  "update.noCatalogs": "没有配置 HTTP catalog。在 opencode.json(c) 的 skills 数组里写 https://... 即可。",
package/src/install.mjs CHANGED
@@ -21,9 +21,16 @@ description: Write an HTML skills report to a path you choose
21
21
  Ask the user where to save the HTML report (default ./skills-report.html), then call the skills_doctor tool with the html argument set to that path and tell them where it was written. $ARGUMENTS
22
22
  `
23
23
 
24
+ const V1_UPGRADE_COMMAND = `---
25
+ description: Check whether skills-doctor itself has a newer version
26
+ ---
27
+ Run \`npx -y opencode-skills-doctor upgrade\` in the terminal (add --apply only after the user confirms), then report whether a newer version exists. $ARGUMENTS
28
+ `
29
+
24
30
  const V1_COMMANDS = [
25
31
  { file: "skills-doctor.md", body: V1_COMMAND },
26
32
  { file: "skills-report.md", body: V1_REPORT_COMMAND },
33
+ { file: "skills-upgrade.md", body: V1_UPGRADE_COMMAND },
27
34
  ]
28
35
 
29
36
  export function defaultConfigDir() {
package/src/plugin-v1.mjs CHANGED
@@ -2,6 +2,24 @@ import { tool } from "@opencode-ai/plugin"
2
2
  import { auditSkills, formatMarkdown } from "./audit.mjs"
3
3
  import { writeHtmlReport } from "./html.mjs"
4
4
  import { budgetWarning } from "./warning.mjs"
5
+ import { loadConfig } from "./config.mjs"
6
+ import { checkForUpdate, resolveMode, toolVersion } from "./self-update.mjs"
7
+
8
+ const VERSION = toolVersion()
9
+ const notifiedSessions = new Set()
10
+
11
+ async function updateNote({ directory, sessionID }) {
12
+ if (!sessionID || notifiedSessions.has(sessionID)) return null
13
+ notifiedSessions.add(sessionID)
14
+ try {
15
+ const { config } = loadConfig(directory)
16
+ const update = await checkForUpdate({ current: VERSION, mode: resolveMode(config) })
17
+ if (!update) return null
18
+ return `skills-doctor ${update.latest} is available (current ${VERSION}). Update: npx -y opencode-skills-doctor@latest upgrade --apply`
19
+ } catch {
20
+ return null
21
+ }
22
+ }
5
23
 
6
24
  const z = tool.schema
7
25
 
@@ -36,6 +54,8 @@ export const SkillsDoctorPlugin = async ({ directory }) => ({
36
54
  "experimental.chat.system.transform": async (input, output) => {
37
55
  const warning = budgetWarning({ cwd: directory, sessionID: input?.sessionID })
38
56
  if (warning) output.system.push(warning)
57
+ const note = await updateNote({ directory, sessionID: input?.sessionID })
58
+ if (note) output.system.push(note)
39
59
  },
40
60
  })
41
61
 
package/src/plugin.mjs CHANGED
@@ -2,6 +2,24 @@ import { Plugin } from "@opencode/plugin"
2
2
  import { auditSkills, formatMarkdown } from "./audit.mjs"
3
3
  import { writeHtmlReport } from "./html.mjs"
4
4
  import { budgetWarning } from "./warning.mjs"
5
+ import { loadConfig } from "./config.mjs"
6
+ import { checkForUpdate, resolveMode, toolVersion } from "./self-update.mjs"
7
+
8
+ const VERSION = toolVersion()
9
+ const notifiedSessions = new Set()
10
+
11
+ async function updateNote({ directory, sessionID }) {
12
+ if (!sessionID || notifiedSessions.has(sessionID)) return null
13
+ notifiedSessions.add(sessionID)
14
+ try {
15
+ const { config } = loadConfig(directory)
16
+ const update = await checkForUpdate({ current: VERSION, mode: resolveMode(config) })
17
+ if (!update) return null
18
+ return `skills-doctor ${update.latest} is available (current ${VERSION}). Update: npx -y opencode-skills-doctor@latest upgrade --apply`
19
+ } catch {
20
+ return null
21
+ }
22
+ }
5
23
 
6
24
  /**
7
25
  * OpenCode 插件入口:把 skills-doctor 暴露成 skills_doctor 工具和 /skills-doctor 命令。
@@ -70,12 +88,26 @@ export default Plugin.define({
70
88
  })
71
89
  },
72
90
  })
91
+ editor.add({
92
+ name: "skills-upgrade",
93
+ description: "Check whether skills-doctor itself has a newer version",
94
+ execute: async ({ sessionID, prompt, delivery }) => {
95
+ await ctx.session.prompt({
96
+ ...prompt,
97
+ sessionID,
98
+ text: "Run `npx -y opencode-skills-doctor upgrade` in the terminal (add --apply only after I confirm), then report whether a newer version exists.",
99
+ delivery,
100
+ })
101
+ },
102
+ })
73
103
  })
74
104
 
75
105
  // 每个会话最多注入一次预算预警,避免刷屏
76
- await ctx.session.hook("context", (event) => {
106
+ await ctx.session.hook("context", async (event) => {
77
107
  const warning = budgetWarning({ cwd: ctx.location.directory, sessionID: event.sessionID })
78
108
  if (warning) event.system.push({ type: "text", text: warning })
109
+ const note = await updateNote({ directory: ctx.location.directory, sessionID: event.sessionID })
110
+ if (note) event.system.push({ type: "text", text: note })
79
111
  })
80
112
  },
81
113
  })
@@ -0,0 +1,142 @@
1
+ import fs from "node:fs"
2
+ import path from "node:path"
3
+ import os from "node:os"
4
+ import { spawnSync, spawn } from "node:child_process"
5
+
6
+ export const REGISTRY_URL =
7
+ process.env.SKILLS_DOCTOR_REGISTRY_URL || "https://registry.npmjs.org/opencode-skills-doctor/latest"
8
+ export const RELEASES_URL = "https://github.com/wangyuchen5110/opencodeplugginprogram/releases/latest"
9
+
10
+ export const CACHE_DIR = path.join(os.homedir(), ".skills-doctor")
11
+ export const CACHE_FILE = path.join(CACHE_DIR, "self-update.json")
12
+ const DAY_MS = 24 * 60 * 60 * 1000
13
+
14
+ let cachedVersion = null
15
+ /** 当前安装版本(读取包内 package.json,插件/MCP 里也能用)。 */
16
+ export function toolVersion() {
17
+ if (cachedVersion) return cachedVersion
18
+ try {
19
+ const file = new URL("../package.json", import.meta.url)
20
+ cachedVersion = JSON.parse(fs.readFileSync(file, "utf8").replace(/^\uFEFF/, "")).version ?? "0.0.0"
21
+ } catch {
22
+ cachedVersion = "0.0.0"
23
+ }
24
+ return cachedVersion
25
+ }
26
+
27
+ /** 语义化版本比较:只看 major.minor.patch,预发布版视为更低。 */
28
+ export function compareVersions(a, b) {
29
+ const parse = (value) => {
30
+ const [core, pre = ""] = String(value ?? "").trim().replace(/^v/i, "").split("-")
31
+ const numbers = core.split(".").map((part) => Number.parseInt(part, 10) || 0)
32
+ return { numbers: [numbers[0] ?? 0, numbers[1] ?? 0, numbers[2] ?? 0], pre }
33
+ }
34
+ const left = parse(a)
35
+ const right = parse(b)
36
+ for (let i = 0; i < 3; i++) {
37
+ if (left.numbers[i] !== right.numbers[i]) return left.numbers[i] > right.numbers[i] ? 1 : -1
38
+ }
39
+ if (left.pre === right.pre) return 0
40
+ if (!left.pre) return 1
41
+ if (!right.pre) return -1
42
+ return left.pre > right.pre ? 1 : -1
43
+ }
44
+
45
+ /** off | notify | open;环境变量 SKILLS_DOCTOR_UPDATE_CHECK 可以强制关闭/开启。 */
46
+ export function resolveMode(config = {}, env = process.env) {
47
+ const envValue = String(env.SKILLS_DOCTOR_UPDATE_CHECK ?? "").trim().toLowerCase()
48
+ if (["0", "false", "off", "no"].includes(envValue)) return "off"
49
+ if (["1", "true", "on", "yes"].includes(envValue)) return "notify"
50
+
51
+ const value = config.checkUpdates ?? config.updateCheck
52
+ if (value === false) return "off"
53
+ if (value === true) return "notify"
54
+ if (value === "off" || value === "notify" || value === "open") return value
55
+ return "notify"
56
+ }
57
+
58
+ export async function fetchLatest({ fetchImpl = globalThis.fetch, url = REGISTRY_URL, timeoutMs = 4000 } = {}) {
59
+ if (typeof fetchImpl !== "function") throw new Error("fetch is not available in this Node runtime")
60
+ const controller = new AbortController()
61
+ const timer = setTimeout(() => controller.abort(), timeoutMs)
62
+ try {
63
+ const response = await fetchImpl(url, { signal: controller.signal, headers: { accept: "application/json" } })
64
+ if (!response.ok) throw new Error(`HTTP ${response.status}`)
65
+ const data = await response.json()
66
+ const version = String(data?.version ?? "").trim()
67
+ if (!version) throw new Error("missing version")
68
+ return { version, url: RELEASES_URL }
69
+ } finally {
70
+ clearTimeout(timer)
71
+ }
72
+ }
73
+
74
+ function readCache(file) {
75
+ try {
76
+ return JSON.parse(fs.readFileSync(file, "utf8").replace(/^\uFEFF/, ""))
77
+ } catch {
78
+ return null
79
+ }
80
+ }
81
+
82
+ function writeCache(file, value) {
83
+ try {
84
+ fs.mkdirSync(path.dirname(file), { recursive: true })
85
+ fs.writeFileSync(file, JSON.stringify(value, null, 2) + "\n", "utf8")
86
+ } catch {}
87
+ }
88
+
89
+ /**
90
+ * 检查工具自身是否有新版本。
91
+ * 返回 null(没有/关闭/失败)或 { latest, url, current, checkedAt, cached }。
92
+ */
93
+ export async function checkForUpdate({
94
+ current,
95
+ mode = "notify",
96
+ ttlMs = DAY_MS,
97
+ now = Date.now(),
98
+ fetchImpl,
99
+ cacheFile = CACHE_FILE,
100
+ force = false,
101
+ } = {}) {
102
+ if (mode === "off" || !current) return null
103
+ const cache = readCache(cacheFile)
104
+
105
+ if (!force && cache && cache.current === current && Number.isFinite(cache.checkedAt) && now - cache.checkedAt < ttlMs) {
106
+ if (cache.latest && compareVersions(cache.latest, current) > 0) return { ...cache, cached: true }
107
+ return null
108
+ }
109
+
110
+ try {
111
+ const latest = await fetchLatest({ fetchImpl })
112
+ writeCache(cacheFile, { current, latest: latest.version, url: latest.url, checkedAt: now })
113
+ if (compareVersions(latest.version, current) > 0) {
114
+ return { latest: latest.version, url: latest.url, current, checkedAt: now, cached: false }
115
+ }
116
+ return null
117
+ } catch {
118
+ return null
119
+ }
120
+ }
121
+
122
+ export function upgradeCommand() {
123
+ return "npm install -g opencode-skills-doctor@latest"
124
+ }
125
+
126
+ /** 真正执行全局升级;调用方负责确认(CLI 的 --apply)。 */
127
+ export function applyUpgrade({ spawnImpl = spawnSync, npm = process.platform === "win32" ? "npm.cmd" : "npm" } = {}) {
128
+ const result = spawnImpl(npm, ["install", "-g", "opencode-skills-doctor@latest", "--no-audit", "--no-fund"], {
129
+ stdio: "inherit",
130
+ env: process.env,
131
+ timeout: 300000,
132
+ })
133
+ return result.status === 0
134
+ }
135
+
136
+ export function openReleasePage({ url = RELEASES_URL, spawnImpl = spawn } = {}) {
137
+ try {
138
+ if (process.platform === "win32") spawnImpl("cmd", ["/c", "start", "", url], { detached: true, stdio: "ignore" }).unref()
139
+ else if (process.platform === "darwin") spawnImpl("open", [url], { detached: true, stdio: "ignore" }).unref()
140
+ else spawnImpl("xdg-open", [url], { detached: true, stdio: "ignore" }).unref()
141
+ } catch {}
142
+ }