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 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** | 41 tests, CI on ubuntu/macos/windows × Node 20/22/24 |
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. 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.
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. `~/.config/opencode/skills`
344
- 6. project `.opencode/skills`
345
- 7. 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
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
 
@@ -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 预算、陈旧 skill |
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
- | **质量** | 41 个测试,ubuntu/macos/windows × Node 20/22/24 CI |
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. `~/.config/opencode/skills`
344
- 6. 项目 `.opencode/skills`
345
- 7. `opencode.json(c)` 的 `skills` 数组(2.x)或 `skills.paths`(1.x);相对路径按当前工作目录解析,支持 `~/`、绝对路径、HTTP catalog
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.3.15",
4
- "description": "Audit, fix and update agent skills: per-step context cost, shadowed duplicates, safe archive, catalog sync. CLI, OpenCode plugin and MCP server (Claude Code / Codex / any MCP client).",
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
- { ...buildCheckResult(audit, options), skills: serializeSkills(audit.skills, audit.managed) },
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
- process.exitCode = result.exitCode
521
- return
622
+ if (htmlPath) console.log(dim(t("check.htmlSaved", { path: htmlPath })))
623
+ await maybeNotifyUpdate({ options, config: loaded.config, t })
522
624
  }
523
- process.exitCode = buildCheckResult(audit, options).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
- * 按官方文档的注册顺序返回 skill 来源(顺序即优先级,越后越高):
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. ~/.config/opencode/skills
119
- * 6. 项目 .opencode/skills(项目根 → cwd)
120
- * 7. opencode.json(c) 里的 skills 数组
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),