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 +36 -8
- package/docs/README.zh-CN.md +36 -8
- package/package.json +1 -1
- package/src/cli.mjs +78 -1
- package/src/config.mjs +4 -0
- package/src/i18n.mjs +25 -0
- package/src/install.mjs +7 -0
- package/src/plugin-v1.mjs +20 -0
- package/src/plugin.mjs +33 -1
- package/src/self-update.mjs +142 -0
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** |
|
|
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.
|
|
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
|
|
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,
|
|
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.
|
|
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 #
|
|
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
|
|
package/docs/README.zh-CN.md
CHANGED
|
@@ -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
|
-
| **质量** |
|
|
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.
|
|
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.
|
|
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 #
|
|
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.
|
|
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
|
+
}
|