opencode-skills-doctor 0.3.15 → 0.4.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 +72 -11
- package/docs/README.zh-CN.md +72 -11
- package/package.json +2 -2
- package/src/cli.mjs +109 -6
- package/src/config.mjs +4 -0
- package/src/discover.mjs +27 -5
- package/src/html.mjs +167 -0
- package/src/i18n.mjs +51 -0
- package/src/install.mjs +32 -12
- package/src/lint.mjs +46 -0
- package/src/mcp.mjs +11 -1
- package/src/plugin-v1.mjs +28 -1
- package/src/plugin.mjs +56 -2
- package/src/refs.mjs +40 -0
- package/src/self-update.mjs +142 -0
package/README.md
CHANGED
|
@@ -15,13 +15,14 @@ Local scanner, linter, fixer, updater and **OpenCode plugin** for agent skills
|
|
|
15
15
|
| | |
|
|
16
16
|
|---|---|
|
|
17
17
|
| **What it is** | Audit, fix and update the skills your coding agent loads |
|
|
18
|
-
| **Interfaces** | CLI · OpenCode plugin (1.x / 2.x) · MCP server (Claude Code, Codex, DSH, …) |
|
|
19
|
-
| **Skill sources** | `.opencode/skills`, `.claude/skills`, `.agents/skills`, `opencode.json(c)` entries, HTTP catalogs |
|
|
20
|
-
| **What it finds** | shadowed IDs, duplicates, long descriptions, oversized bodies, per-step token budget, stale skills |
|
|
18
|
+
| **Interfaces** | CLI · OpenCode plugin (1.x / 2.x) · MCP server (Claude Code, Codex, DSH, …) · Grok CLI compatible |
|
|
19
|
+
| **Skill sources** | `.opencode/skills`, `.claude/skills`, `.agents/skills`, `.grok/skills`, `.cursor/skills`, `opencode.json(c)` entries, HTTP catalogs |
|
|
20
|
+
| **What it finds** | shadowed IDs, duplicates, long descriptions, oversized bodies, per-step token budget, broken file references, invalid frontmatter, empty bodies, stale skills |
|
|
21
21
|
| **What it fixes** | archives shadowed definitions (reversible), hides low-frequency skills, syncs catalogs with drift detection |
|
|
22
|
+
| **Reports** | terminal, JSON, or a self-contained HTML report (`--html [file] --open`) |
|
|
22
23
|
| **Cost** | CLI: 0 tokens. Plugin: ~191 tokens/step for the tool definition, plus a one-time warning per session |
|
|
23
24
|
| **Requirements** | Node ≥ 20, zero runtime dependencies |
|
|
24
|
-
| **Quality** |
|
|
25
|
+
| **Quality** | 49 tests, CI on ubuntu/macos/windows × Node 20/22/24 |
|
|
25
26
|
|
|
26
27
|
## Why
|
|
27
28
|
|
|
@@ -86,8 +87,21 @@ skills-doctor check
|
|
|
86
87
|
skills-doctor check --json
|
|
87
88
|
skills-doctor check --budget 800 --strict # for CI
|
|
88
89
|
skills-doctor check --root .\some-project --no-global
|
|
90
|
+
skills-doctor check --html report.html # self-contained HTML report
|
|
91
|
+
skills-doctor check --html report.html --open
|
|
89
92
|
```
|
|
90
93
|
|
|
94
|
+
### HTML report
|
|
95
|
+
|
|
96
|
+
`--html [file]` writes a single self-contained HTML file (no external assets):
|
|
97
|
+
|
|
98
|
+
- summary cards and a context-budget bar;
|
|
99
|
+
- findings filterable by severity and text;
|
|
100
|
+
- a skill table with scope, token cost, description preview and file path;
|
|
101
|
+
- default path `./skills-report.html` when `--html` is given without a value; `--open` launches the browser.
|
|
102
|
+
|
|
103
|
+
The plugin and MCP tool accept the same thing as an `html` argument, so the agent can ask you where to save it — for example: *"save the report to D:\reports\skills.html"*.
|
|
104
|
+
|
|
91
105
|
### `scan`
|
|
92
106
|
|
|
93
107
|
Lists every skill with its source, precedence, whether the model can see it, and token cost.
|
|
@@ -143,6 +157,30 @@ Local execution is free, but any tool registered with the model is visible on ev
|
|
|
143
157
|
|
|
144
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.
|
|
145
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
|
+
|
|
146
184
|
## Configuration
|
|
147
185
|
|
|
148
186
|
Optional `.skills-doctor.json` in the working directory (or pass `--config <file>`):
|
|
@@ -152,12 +190,13 @@ Optional `.skills-doctor.json` in the working directory (or pass `--config <file
|
|
|
152
190
|
"budget": 1200,
|
|
153
191
|
"staleDays": 180,
|
|
154
192
|
"lang": "en",
|
|
193
|
+
"checkUpdates": "notify",
|
|
155
194
|
"rules": { "long-description": "off", "hidden-skill": "info" },
|
|
156
195
|
"ignore": ["legacy-skill"]
|
|
157
196
|
}
|
|
158
197
|
```
|
|
159
198
|
|
|
160
|
-
`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.
|
|
161
200
|
|
|
162
201
|
## OpenCode plugin
|
|
163
202
|
|
|
@@ -172,8 +211,10 @@ The package also exports an OpenCode 2.x plugin:
|
|
|
172
211
|
|
|
173
212
|
It registers:
|
|
174
213
|
|
|
175
|
-
- the `skills_doctor` tool (namespace `skills`) — the agent can audit with `{ directory?, budget? }` and gets a markdown report;
|
|
176
|
-
- 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.
|
|
177
218
|
|
|
178
219
|
`@opencode/plugin` is an optional peer dependency; OpenCode provides it at runtime.
|
|
179
220
|
|
|
@@ -221,8 +262,9 @@ Use `--dry-run` to preview, `--config-dir <dir>` for a custom config location, a
|
|
|
221
262
|
|
|
222
263
|
Once installed you get:
|
|
223
264
|
|
|
224
|
-
- the `skills_doctor` tool;
|
|
265
|
+
- the `skills_doctor` tool (with an optional `html` argument to write a report to a path you choose);
|
|
225
266
|
- the `/skills-doctor` command (1.x: generated markdown command; 2.x: plugin command);
|
|
267
|
+
- the `/skills-report` command (2.x) that asks where to save the HTML report;
|
|
226
268
|
- a one-time system-prompt warning when the skill description budget is exceeded.
|
|
227
269
|
|
|
228
270
|
### Uninstall
|
|
@@ -305,6 +347,17 @@ On Windows, if the stdio spawn cannot resolve `npx`, use `command: cmd` and `arg
|
|
|
305
347
|
|
|
306
348
|
`check` reads the lockfile and reports drift (`managed-drift`), missing files (`managed-missing`) and unknown lock versions (`lock-version-unsupported`). Re-running `update` re-downloads drifted files.
|
|
307
349
|
|
|
350
|
+
### Grok CLI
|
|
351
|
+
|
|
352
|
+
Grok discovers skills from `.grok/skills` (project, repo and `~/.grok/skills`), plus `.claude/skills`, `.cursor/skills` and `.agents/skills`. `skills-doctor` scans exactly the same locations, so:
|
|
353
|
+
|
|
354
|
+
```powershell
|
|
355
|
+
skills-doctor check --root . # covers your Grok project skills
|
|
356
|
+
skills-doctor check --root $HOME # covers ~/.grok/skills and other user-level skills
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
No install step is needed — Grok loads `SKILL.md` packages natively. If your Grok build has MCP enabled, you can additionally register the MCP server above to let the model call `skills_doctor` itself.
|
|
360
|
+
|
|
308
361
|
## Rules
|
|
309
362
|
|
|
310
363
|
| Rule | Level | Meaning |
|
|
@@ -321,6 +374,10 @@ On Windows, if the stdio spawn cannot resolve `npx`, use `command: cmd` and `arg
|
|
|
321
374
|
| `invalid-id` | info | ID is not kebab-case |
|
|
322
375
|
| `hidden-skill` | info | Hidden via `opencode/autoinvoke: false` or `disable-model-invocation: true` |
|
|
323
376
|
| `stale-skill` | info | Not updated within `--stale-days` |
|
|
377
|
+
| `broken-reference` | warn | `references/`, `scripts/`, `assets/`, `templates/` or `examples/` path does not exist |
|
|
378
|
+
| `reference-outside-skill` | info | A referenced file lives outside the skill directory (hurts portability) |
|
|
379
|
+
| `invalid-frontmatter` | warn | Unclosed `---` fence, or non-string name/description / non-object metadata |
|
|
380
|
+
| `empty-body` | info | Body is nearly empty, so the skill carries no instructions |
|
|
324
381
|
| `managed-drift` | warn | A file installed by `update` was modified afterwards |
|
|
325
382
|
| `managed-missing` | warn | Recorded in the lock, but the local file is gone |
|
|
326
383
|
| `managed-unsafe` | warn | Path traversal or another unsafe path in the lock |
|
|
@@ -340,9 +397,13 @@ On Windows, if the stdio spawn cannot resolve `npx`, use `command: cmd` and `arg
|
|
|
340
397
|
2. project `.claude/skills` (from the project root up to the current directory)
|
|
341
398
|
3. `~/.agents/skills`
|
|
342
399
|
4. project `.agents/skills`
|
|
343
|
-
5. `~/.
|
|
344
|
-
6. project `.
|
|
345
|
-
7.
|
|
400
|
+
5. `~/.cursor/skills`
|
|
401
|
+
6. project `.cursor/skills`
|
|
402
|
+
7. `~/.grok/skills`
|
|
403
|
+
8. project `.grok/skills`
|
|
404
|
+
9. `~/.config/opencode/skills`
|
|
405
|
+
10. project `.opencode/skills`
|
|
406
|
+
11. the `skills` array in `opencode.json(c)` (2.x) or `skills.paths` (1.x); relative paths resolve from the working directory; supports `~/`, absolute paths and HTTP catalogs
|
|
346
407
|
|
|
347
408
|
## JSON output
|
|
348
409
|
|
package/docs/README.zh-CN.md
CHANGED
|
@@ -15,13 +15,14 @@ OpenCode skill 的**本地检测器 + 更新器 + 修复器**,并附带 **Open
|
|
|
15
15
|
| | |
|
|
16
16
|
|---|---|
|
|
17
17
|
| **是什么** | 审计、修复、更新 coding agent 加载的 skill |
|
|
18
|
-
| **形态** | CLI · OpenCode 插件(1.x / 2.x)· MCP server(Claude Code、Codex、DSH
|
|
19
|
-
| **来源** | `.opencode/skills`、`.claude/skills`、`.agents/skills`、`opencode.json(c)` 配置、HTTP catalog |
|
|
20
|
-
| **能查** | 同 ID 遮蔽、重复、超长描述、超大正文、每步 token
|
|
18
|
+
| **形态** | CLI · OpenCode 插件(1.x / 2.x)· MCP server(Claude Code、Codex、DSH…)· 兼容 Grok CLI |
|
|
19
|
+
| **来源** | `.opencode/skills`、`.claude/skills`、`.agents/skills`、`.grok/skills`、`.cursor/skills`、`opencode.json(c)` 配置、HTTP catalog |
|
|
20
|
+
| **能查** | 同 ID 遮蔽、重复、超长描述、超大正文、每步 token 预算、断链引用、非法 frontmatter、空正文、陈旧 skill |
|
|
21
21
|
| **能修** | 归档遮蔽定义(可逆)、隐藏低频 skill、catalog 同步并检测 drift |
|
|
22
|
+
| **报告** | 终端、JSON,或自包含 HTML(`--html [file] --open`) |
|
|
22
23
|
| **成本** | CLI 0 token;插件工具定义约 191/步,另有每会话一次预警 |
|
|
23
24
|
| **要求** | Node ≥ 20,零运行时依赖 |
|
|
24
|
-
| **质量** |
|
|
25
|
+
| **质量** | 49 个测试,ubuntu/macos/windows × Node 20/22/24 CI |
|
|
25
26
|
|
|
26
27
|
## 为什么需要它
|
|
27
28
|
|
|
@@ -86,8 +87,21 @@ skills-doctor check
|
|
|
86
87
|
skills-doctor check --json
|
|
87
88
|
skills-doctor check --budget 800 --strict # CI 用
|
|
88
89
|
skills-doctor check --root .\some-project --no-global
|
|
90
|
+
skills-doctor check --html report.html # 自包含 HTML 报告
|
|
91
|
+
skills-doctor check --html report.html --open
|
|
89
92
|
```
|
|
90
93
|
|
|
94
|
+
### HTML 报告
|
|
95
|
+
|
|
96
|
+
`--html [file]` 生成单个自包含 HTML 文件(无外部资源):
|
|
97
|
+
|
|
98
|
+
- 摘要卡片 + 上下文预算条;
|
|
99
|
+
- 可按严重级别和文本过滤的 findings;
|
|
100
|
+
- skill 表格:来源、token 成本、描述预览、文件路径;
|
|
101
|
+
- 只写 `--html` 不带路径时默认 `./skills-report.html`;`--open` 自动打开浏览器。
|
|
102
|
+
|
|
103
|
+
插件和 MCP 工具同样接受 `html` 参数,所以可以让 agent 先问你要存哪里——例如:*"把报告存到 D:\reports\skills.html"*。
|
|
104
|
+
|
|
91
105
|
### `scan`
|
|
92
106
|
|
|
93
107
|
列出所有 skill 及其来源、优先级、是否会被模型看到、token 占用。
|
|
@@ -143,6 +157,30 @@ skills-doctor update --into .\team-skills
|
|
|
143
157
|
|
|
144
158
|
所以插件是用固定的少量开销,换「模型能自己体检」。上面的例子里 skill 描述每步 3074 tokens,压到 1200 预算能省下 10 倍于插件开销的 token。想要**零上下文成本**,就别装插件——用 `npx opencode-skills-doctor check` 在终端或 CI 里跑。
|
|
145
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
|
+
|
|
146
184
|
## 配置
|
|
147
185
|
|
|
148
186
|
在工作目录放一个可选的 `.skills-doctor.json`(或用 `--config <file>` 指定):
|
|
@@ -152,12 +190,13 @@ skills-doctor update --into .\team-skills
|
|
|
152
190
|
"budget": 1200,
|
|
153
191
|
"staleDays": 180,
|
|
154
192
|
"lang": "en",
|
|
193
|
+
"checkUpdates": "notify",
|
|
155
194
|
"rules": { "long-description": "off", "hidden-skill": "info" },
|
|
156
195
|
"ignore": ["legacy-skill"]
|
|
157
196
|
}
|
|
158
197
|
```
|
|
159
198
|
|
|
160
|
-
`rules` 接受 `off` / `info` / `warn` / `error`;`ignore` 会丢掉只涉及这些 ID 的 finding
|
|
199
|
+
`rules` 接受 `off` / `info` / `warn` / `error`;`ignore` 会丢掉只涉及这些 ID 的 finding;`checkUpdates` 取 `notify` / `open` / `off`。命令行参数优先级高于配置文件。
|
|
161
200
|
|
|
162
201
|
## OpenCode 插件
|
|
163
202
|
|
|
@@ -172,8 +211,10 @@ skills-doctor update --into .\team-skills
|
|
|
172
211
|
|
|
173
212
|
它会注册:
|
|
174
213
|
|
|
175
|
-
- `skills_doctor` 工具(命名空间 `skills`)—— agent 传 `{ directory?, budget? }` 就能拿到 markdown 审计报告;
|
|
176
|
-
- `/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 自身有没有新版本。
|
|
177
218
|
|
|
178
219
|
`@opencode/plugin` 是可选 peer 依赖,运行时由 OpenCode 提供。
|
|
179
220
|
|
|
@@ -221,8 +262,9 @@ npx opencode-skills-doctor install
|
|
|
221
262
|
|
|
222
263
|
装好后你会得到:
|
|
223
264
|
|
|
224
|
-
- `skills_doctor`
|
|
265
|
+
- `skills_doctor` 工具(可选 `html` 参数,把报告写到指定路径);
|
|
225
266
|
- `/skills-doctor` 命令(1.x 是生成的 markdown 命令,2.x 是插件命令);
|
|
267
|
+
- `/skills-report` 命令(2.x)—— 会先问你要把 HTML 报告存到哪里;
|
|
226
268
|
- 当 skill 描述预算超标时,系统提示里会注入一次提醒(每个会话最多一次)。
|
|
227
269
|
|
|
228
270
|
### 卸载
|
|
@@ -305,6 +347,17 @@ Windows 上如果 stdio 起不来 `npx`,改成 `command: cmd`、`args: ["/c",
|
|
|
305
347
|
|
|
306
348
|
`check` 会读 lockfile,报告 drift(`managed-drift`)、文件缺失(`managed-missing`)和未知 lock 版本(`lock-version-unsupported`);再跑一次 `update` 会重下被改坏的文件。
|
|
307
349
|
|
|
350
|
+
### Grok CLI
|
|
351
|
+
|
|
352
|
+
Grok 从 `.grok/skills`(项目、仓库、`~/.grok/skills`)以及 `.claude/skills`、`.cursor/skills`、`.agents/skills` 发现 skill。skills-doctor 扫描的正是同一批位置:
|
|
353
|
+
|
|
354
|
+
```powershell
|
|
355
|
+
skills-doctor check --root . # 覆盖当前项目的 Grok skill
|
|
356
|
+
skills-doctor check --root $HOME # 覆盖 ~/.grok/skills 等用户级 skill
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
不需要额外安装——Grok 原生加载 `SKILL.md`。如果该 Grok 版本支持 MCP,还可以把上面的 MCP server 接进去,让模型自己调用 `skills_doctor`。
|
|
360
|
+
|
|
308
361
|
## 检测规则
|
|
309
362
|
|
|
310
363
|
| 规则 | 级别 | 含义 |
|
|
@@ -321,6 +374,10 @@ Windows 上如果 stdio 起不来 `npx`,改成 `command: cmd`、`args: ["/c",
|
|
|
321
374
|
| `invalid-id` | info | 不符合 kebab-case 规范 |
|
|
322
375
|
| `hidden-skill` | info | 被 `opencode/autoinvoke: false` 或 `disable-model-invocation: true` 隐藏 |
|
|
323
376
|
| `stale-skill` | info | `--stale-days` 指定的天数内没更新 |
|
|
377
|
+
| `broken-reference` | warn | 引用的 `references/`、`scripts/`、`assets/`、`templates/`、`examples/` 文件不存在 |
|
|
378
|
+
| `reference-outside-skill` | info | 被引用的文件在 skill 目录之外(影响可移植性) |
|
|
379
|
+
| `invalid-frontmatter` | warn | `---` 未闭合,或 name/description 非字符串 / metadata 非对象 |
|
|
380
|
+
| `empty-body` | info | 正文几乎是空的,skill 没有实际指令 |
|
|
324
381
|
| `managed-drift` | warn | 由 update 安装的文件在更新后被改过 |
|
|
325
382
|
| `managed-missing` | warn | lock 里记录了,但本地文件没了 |
|
|
326
383
|
| `managed-unsafe` | warn | lock 里有路径穿越等不安全路径 |
|
|
@@ -340,9 +397,13 @@ Windows 上如果 stdio 起不来 `npx`,改成 `command: cmd`、`args: ["/c",
|
|
|
340
397
|
2. 项目 `.claude/skills`(项目根 → 当前目录逐级)
|
|
341
398
|
3. `~/.agents/skills`
|
|
342
399
|
4. 项目 `.agents/skills`
|
|
343
|
-
5. `~/.
|
|
344
|
-
6. 项目 `.
|
|
345
|
-
7.
|
|
400
|
+
5. `~/.cursor/skills`
|
|
401
|
+
6. 项目 `.cursor/skills`
|
|
402
|
+
7. `~/.grok/skills`
|
|
403
|
+
8. 项目 `.grok/skills`
|
|
404
|
+
9. `~/.config/opencode/skills`
|
|
405
|
+
10. 项目 `.opencode/skills`
|
|
406
|
+
11. `opencode.json(c)` 的 `skills` 数组(2.x)或 `skills.paths`(1.x);相对路径按当前工作目录解析,支持 `~/`、绝对路径、HTTP catalog
|
|
346
407
|
|
|
347
408
|
## JSON 输出
|
|
348
409
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-skills-doctor",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Audit, fix and update agent skills:
|
|
3
|
+
"version": "0.4.1",
|
|
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": {
|
|
7
7
|
".": "./src/plugin.mjs",
|
package/src/cli.mjs
CHANGED
|
@@ -9,6 +9,15 @@ import { auditSkills } from "./audit.mjs"
|
|
|
9
9
|
import { loadConfig } from "./config.mjs"
|
|
10
10
|
import { startMcpServer } from "./mcp.mjs"
|
|
11
11
|
import { installOpenCode, uninstallOpenCode } from "./install.mjs"
|
|
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"
|
|
12
21
|
import { padDisplay } from "./parse.mjs"
|
|
13
22
|
import { detectLang, normalizeLang, translator } from "./i18n.mjs"
|
|
14
23
|
|
|
@@ -38,6 +47,10 @@ const HINT_KEYS = {
|
|
|
38
47
|
"invalid-id": "hint.invalid-id",
|
|
39
48
|
"hidden-skill": "hint.hidden-skill",
|
|
40
49
|
"stale-skill": "hint.stale-skill",
|
|
50
|
+
"invalid-frontmatter": "hint.invalid-frontmatter",
|
|
51
|
+
"empty-body": "hint.empty-body",
|
|
52
|
+
"broken-reference": "hint.broken-reference",
|
|
53
|
+
"reference-outside-skill": "hint.reference-outside-skill",
|
|
41
54
|
}
|
|
42
55
|
|
|
43
56
|
function parseArgs(argv) {
|
|
@@ -59,12 +72,16 @@ function parseArgs(argv) {
|
|
|
59
72
|
prune: false,
|
|
60
73
|
configDir: null,
|
|
61
74
|
noNpm: false,
|
|
75
|
+
html: null,
|
|
76
|
+
open: false,
|
|
77
|
+
apply: false,
|
|
78
|
+
updateCheck: true,
|
|
62
79
|
help: false,
|
|
63
80
|
version: false,
|
|
64
81
|
}
|
|
65
82
|
|
|
66
83
|
const args = [...argv]
|
|
67
|
-
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"])
|
|
68
85
|
if (args[0] && commands.has(args[0])) {
|
|
69
86
|
options.command = args.shift()
|
|
70
87
|
}
|
|
@@ -125,6 +142,25 @@ function parseArgs(argv) {
|
|
|
125
142
|
case "--no-npm":
|
|
126
143
|
options.noNpm = true
|
|
127
144
|
break
|
|
145
|
+
case "--html": {
|
|
146
|
+
const next = args[i + 1]
|
|
147
|
+
if (next && !next.startsWith("-")) {
|
|
148
|
+
options.html = path.resolve(next)
|
|
149
|
+
i++
|
|
150
|
+
} else {
|
|
151
|
+
options.html = path.resolve("skills-report.html")
|
|
152
|
+
}
|
|
153
|
+
break
|
|
154
|
+
}
|
|
155
|
+
case "--open":
|
|
156
|
+
options.open = true
|
|
157
|
+
break
|
|
158
|
+
case "--apply":
|
|
159
|
+
options.apply = true
|
|
160
|
+
break
|
|
161
|
+
case "--no-update-check":
|
|
162
|
+
options.updateCheck = false
|
|
163
|
+
break
|
|
128
164
|
case "-h":
|
|
129
165
|
case "--help":
|
|
130
166
|
options.help = true
|
|
@@ -426,6 +462,27 @@ function runFix(audit, options, t) {
|
|
|
426
462
|
return errors.length ? 1 : 0
|
|
427
463
|
}
|
|
428
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
|
+
|
|
429
486
|
export async function main(argv) {
|
|
430
487
|
const options = parseArgs(argv)
|
|
431
488
|
|
|
@@ -497,6 +554,44 @@ export async function main(argv) {
|
|
|
497
554
|
return
|
|
498
555
|
}
|
|
499
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
|
+
|
|
500
595
|
if (options.command === "check") {
|
|
501
596
|
const audit = auditSkills({
|
|
502
597
|
cwd,
|
|
@@ -506,21 +601,28 @@ export async function main(argv) {
|
|
|
506
601
|
budget: options.budget,
|
|
507
602
|
staleDays: options.staleDays,
|
|
508
603
|
})
|
|
604
|
+
const result = buildCheckResult(audit, options)
|
|
605
|
+
|
|
606
|
+
let htmlPath = null
|
|
607
|
+
if (options.html) {
|
|
608
|
+
htmlPath = writeHtmlReport(audit, options.html)
|
|
609
|
+
if (options.open) openReport(htmlPath)
|
|
610
|
+
}
|
|
611
|
+
|
|
509
612
|
if (options.json) {
|
|
510
613
|
console.log(
|
|
511
614
|
JSON.stringify(
|
|
512
|
-
{ ...
|
|
615
|
+
{ ...result, html: htmlPath, skills: serializeSkills(audit.skills, audit.managed) },
|
|
513
616
|
null,
|
|
514
617
|
2,
|
|
515
618
|
),
|
|
516
619
|
)
|
|
517
620
|
} else {
|
|
518
|
-
const result = buildCheckResult(audit, options)
|
|
519
621
|
printCheck(audit, result, options, t)
|
|
520
|
-
|
|
521
|
-
|
|
622
|
+
if (htmlPath) console.log(dim(t("check.htmlSaved", { path: htmlPath })))
|
|
623
|
+
await maybeNotifyUpdate({ options, config: loaded.config, t })
|
|
522
624
|
}
|
|
523
|
-
process.exitCode =
|
|
625
|
+
process.exitCode = result.exitCode
|
|
524
626
|
return
|
|
525
627
|
}
|
|
526
628
|
|
|
@@ -545,6 +647,7 @@ export async function main(argv) {
|
|
|
545
647
|
return
|
|
546
648
|
}
|
|
547
649
|
printScan(discovered, options, managedInfo.managed, t)
|
|
650
|
+
await maybeNotifyUpdate({ options, config: loaded.config, t })
|
|
548
651
|
return
|
|
549
652
|
}
|
|
550
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/discover.mjs
CHANGED
|
@@ -110,14 +110,19 @@ function isUnder(parent, child) {
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
/**
|
|
113
|
-
*
|
|
113
|
+
* 返回 skill 来源(顺序即优先级,越后越高)。这是多个 agent 的并集,
|
|
114
|
+
* 不同 agent 的真实优先级可能不同(Grok 的 local 最高,OpenCode 的 config 最高):
|
|
114
115
|
* 1. ~/.claude/skills
|
|
115
116
|
* 2. 项目 .claude/skills(远端 → cwd)
|
|
116
117
|
* 3. ~/.agents/skills
|
|
117
118
|
* 4. 项目 .agents/skills(远端 → cwd)
|
|
118
|
-
* 5. ~/.
|
|
119
|
-
* 6. 项目 .
|
|
120
|
-
* 7.
|
|
119
|
+
* 5. ~/.cursor/skills
|
|
120
|
+
* 6. 项目 .cursor/skills
|
|
121
|
+
* 7. ~/.grok/skills
|
|
122
|
+
* 8. 项目 .grok/skills
|
|
123
|
+
* 9. ~/.config/opencode/skills
|
|
124
|
+
* 10. 项目 .opencode/skills(项目根 → cwd)
|
|
125
|
+
* 11. opencode.json(c) 里的 skills 数组 / skills.paths
|
|
121
126
|
*/
|
|
122
127
|
export function discoverSources({ cwd = process.cwd(), home = os.homedir(), includeGlobal = true } = {}) {
|
|
123
128
|
const projectRoot = findProjectRoot(cwd) ?? path.resolve(cwd)
|
|
@@ -140,6 +145,12 @@ export function discoverSources({ cwd = process.cwd(), home = os.homedir(), incl
|
|
|
140
145
|
if (includeGlobal) addSource("agents-global", "global", path.join(home, ".agents", "skills"))
|
|
141
146
|
for (const dir of projectChain) addSource("agents-project", "project", path.join(dir, ".agents", "skills"))
|
|
142
147
|
|
|
148
|
+
if (includeGlobal) addSource("cursor-global", "global", path.join(home, ".cursor", "skills"))
|
|
149
|
+
for (const dir of projectChain) addSource("cursor-project", "project", path.join(dir, ".cursor", "skills"))
|
|
150
|
+
|
|
151
|
+
if (includeGlobal) addSource("grok-global", "global", path.join(home, ".grok", "skills"))
|
|
152
|
+
for (const dir of projectChain) addSource("grok-project", "project", path.join(dir, ".grok", "skills"))
|
|
153
|
+
|
|
143
154
|
if (includeGlobal) addSource("opencode-global", "global", path.join(home, ".config", "opencode", "skills"))
|
|
144
155
|
for (const dir of projectChain) addSource("opencode-project", "project", path.join(dir, ".opencode", "skills"))
|
|
145
156
|
|
|
@@ -246,7 +257,14 @@ function buildSkill({ id, file, source, form }) {
|
|
|
246
257
|
} catch {
|
|
247
258
|
text = ""
|
|
248
259
|
}
|
|
249
|
-
const { data, body } = parseFrontmatter(text)
|
|
260
|
+
const { data, body, hasFrontmatter } = parseFrontmatter(text)
|
|
261
|
+
const startsWithFence = /^---\r?\n/.test(text)
|
|
262
|
+
const frontmatterBroken = startsWithFence && !hasFrontmatter
|
|
263
|
+
const frontmatterInvalid =
|
|
264
|
+
hasFrontmatter &&
|
|
265
|
+
((data.name !== undefined && typeof data.name !== "string") ||
|
|
266
|
+
(data.description !== undefined && typeof data.description !== "string") ||
|
|
267
|
+
(data.metadata !== undefined && (typeof data.metadata !== "object" || data.metadata === null)))
|
|
250
268
|
const autoinvoke = data?.metadata?.["opencode/autoinvoke"]
|
|
251
269
|
const disabled = data?.["disable-model-invocation"]
|
|
252
270
|
const description = typeof data.description === "string" ? data.description.trim() : ""
|
|
@@ -269,7 +287,11 @@ function buildSkill({ id, file, source, form }) {
|
|
|
269
287
|
disabled,
|
|
270
288
|
hidden,
|
|
271
289
|
advertised: Boolean(description) && !hidden,
|
|
290
|
+
hasFrontmatter,
|
|
291
|
+
frontmatterBroken,
|
|
292
|
+
frontmatterInvalid,
|
|
272
293
|
body,
|
|
294
|
+
bodyChars: body.trim().length,
|
|
273
295
|
bodyTokens: estimateTokens(body),
|
|
274
296
|
bytes: Buffer.byteLength(text),
|
|
275
297
|
descriptionTokens: estimateTokens(description),
|