opencode-skills-doctor 0.9.6 → 0.9.7

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
@@ -5,8 +5,8 @@
5
5
  <h1 align="center">skills-doctor</h1>
6
6
 
7
7
  <p align="center">
8
- <b>Audit the token cost your coding agent pays for skills on every step.</b><br>
9
- A word-piece estimate, boilerplate-aware overlap, and a plan that keeps the routing sentence and hides the redundant skill first.
8
+ <b>Your skills are quietly taxing every single step.</b><br>
9
+ 29 skills = ~3,074 tokens burned before the model even sees your code.
10
10
  </p>
11
11
 
12
12
  <p align="center">
@@ -32,25 +32,27 @@
32
32
  > **Runs 100% locally.** No telemetry, no account, no uploads — your skills,
33
33
  > prompts and files never leave your machine. See [Privacy](#privacy-local-only-no-data-collection).
34
34
 
35
- ## Contents
35
+ ## The hidden tax
36
36
 
37
- - [The 30-second version](#the-30-second-version) · [Privacy](#privacy-local-only-no-data-collection)
38
- - [See it](#see-it) · [How it works](#how-it-works) · [Quick start](#quick-start)
39
- - [Install](#install) · [Use it in CI](#use-it-in-ci-github-action) · [Commands](#commands) · [Works where you work](#works-where-you-work)
40
- - [Why it is safe](#why-it-is-safe) · [Development](#development)
37
+ Every agent skill with a `description` is injected into the system prompt on **every single step**. On a real machine with 29 skills that was **~3,074 tokens** — before the model read a single line of code. Duplicates silently shadow each other. Long descriptions never get trimmed. Broken references go unnoticed.
41
38
 
42
- ## The 30-second version
39
+ `skills-doctor` measures the exact cost, shows which definition actually wins, and gives you a concrete plan to cut it:
43
40
 
44
- Every agent skill with a `description` is advertised to the model on **every single step**. On a machine with 29 skills that is **~3,074 tokens per step** — before the model reads a line of your code. Duplicates silently shadow each other, descriptions drift, and references to files that no longer exist go unnoticed.
41
+ ```text
42
+ current: ~3,124 tokens/step (30 advertised) budget: 1,200
43
+
44
+ Recommended: trim 26 + hide 2 → 1,184 tokens/step (−1,940)
45
+ Over 100 steps that saves ~194,000 tokens.
46
+ ```
45
47
 
46
- `skills-doctor` measures that cost, shows which definition actually wins, and fixes it:
48
+ One command. Runs 100% locally. No account, no telemetry, nothing leaves your machine.
47
49
 
48
50
  | | |
49
51
  |---|---|
50
52
  | **Find** | token budget, shadowed duplicates, flat/dir collisions, long descriptions, oversized bodies, broken references, invalid frontmatter, stale skills |
51
53
  | **Fix** | archive shadowed definitions (reversible), hide redundant skills before unique ones, keep the routing sentence in the plan |
52
54
  | **Update** | sync skills from HTTP catalogs with a lockfile, drift detection and `--prune` |
53
- | **Report** | terminal, JSON, or a self-contained HTML report |
55
+ | **Report** | a short note in the OpenCode chat, plus the terminal and JSON |
54
56
  | **Run it** | CLI · OpenCode plugin (1.x/2.x/Desktop) · MCP server · DeepSeek Harness skill |
55
57
 
56
58
  ## Privacy: local-only, no data collection
@@ -63,17 +65,13 @@ Every agent skill with a `description` is advertised to the model on **every sin
63
65
  | **Never reads** | conversation history, session transcripts, source code, documents, credentials, API keys or environment secrets |
64
66
  | **Uploads** | nothing: there is no server component and no remote endpoint besides the two optional calls below |
65
67
  | **Network** | only when you ask for it. `update` fetches the HTTP catalogs **you** configure; `upgrade` / the daily update check reads the npm registry for the latest version number (cached 24 h — disable with `"checkUpdates": "off"`, `SKILLS_DOCTOR_UPDATE_CHECK=0` or `--no-update-check`) |
66
- | **Writes** | only what you asked for: the HTML/JSON report, `.skills-doctor-archive/`, `.skills-doctor.json`, the lockfile, and the plugin/MCP/skill entries created by `install` |
68
+ | **Writes** | only what you asked for: `.skills-doctor-archive/`, `.skills-doctor.json`, the lockfile, and the plugin/MCP/skill entries created by `install` |
67
69
 
68
70
  `opencode.json(c)` and MCP configs can contain credentials; they are parsed locally only to find the relevant entries, and those values are never sent anywhere nor included in a report. Reports contain file paths and skill descriptions (which are part of your skill files), never file bodies. `check`, `plan`, `scan`, `fix --dry-run` and `mcp` all work with the network fully offline.
69
71
 
70
- ## See it
71
-
72
- <p align="center">
73
- <img src="https://raw.githubusercontent.com/wangyuchen5110/opencodeplugginprogram/main/docs/assets/report.png" alt="skills-doctor HTML report" width="82%">
74
- </p>
72
+ ## What you see
75
73
 
76
- A real `check --html` run: summary cards, the context-budget bar, filterable findings, and the exact potential savings.
74
+ In OpenCode 2, the first message of a session gets one notice in the chat when a skill is over budget, shadowed, or broken. It names at most three problems and does not change any file. `/skills-doctor` posts that notice again. The terminal `check` and `--json` output stay for CI.
77
75
 
78
76
  ## How it works
79
77
 
@@ -84,7 +82,7 @@ flowchart LR
84
82
  C --> D["lint<br/>16 rules + token budget"]
85
83
  D --> E{"findings?"}
86
84
  E -->|"yes"| F["plan / fix<br/>trim · hide · archive"]
87
- E -->|"always"| G["report<br/>terminal · JSON · HTML"]
85
+ E -->|"always"| G["tell you<br/>chat · terminal · JSON"]
88
86
  F --> G
89
87
  G --> H["you · CI · the agent"]
90
88
  ```
@@ -100,26 +98,12 @@ npx -y opencode-skills-doctor check
100
98
 
101
99
  # how many tokens can I actually cut?
102
100
  npx -y opencode-skills-doctor plan
103
-
104
- # shareable HTML report
105
- npx -y opencode-skills-doctor check --html report.html --open
106
- ```
107
-
108
- Example `plan` output on a real setup:
109
-
110
- ```text
111
- current: ~3,124 tokens/step (30 advertised) budget: 1,200
112
-
113
- Option A — trim descriptions 26 skills → 1,606 (−1,518)
114
- Option B — hide skills 11 skills → 1,200 (−1,924)
115
- Recommended trim 26 + hide 2 → 1,184 (−1,940)
116
- over 100 steps that saves ~194,000 tokens
117
101
  ```
118
102
 
119
103
  ## Install
120
104
 
121
105
  ```powershell
122
- # OpenCode — plugin + /skills-doctor, /skills-report, /skills-upgrade commands
106
+ # OpenCode — plugin + /skills-doctor and /skills-upgrade
123
107
  npx -y opencode-skills-doctor install # auto-detects 1.x / 2.x / Desktop
124
108
  npx -y opencode-skills-doctor install --client desktop
125
109
 
@@ -158,16 +142,15 @@ jobs:
158
142
  runs-on: ubuntu-latest
159
143
  steps:
160
144
  - uses: actions/checkout@v4
161
- - uses: wangyuchen5110/opencodeplugginprogram@v0.9.6
145
+ - uses: wangyuchen5110/opencodeplugginprogram@v0.9.7
162
146
  with:
163
147
  budget: "1200"
164
148
  fail-on: warn # error | warn | never
165
- html: skills-report.html # written and uploaded as an artifact
166
149
  comment: "true" # post the summary on the PR
167
150
  ```
168
151
 
169
- It writes a summary to the job page, uploads the HTML report, optionally comments
170
- on the pull request, and fails the job according to `fail-on`.
152
+ It writes a summary to the job page, optionally comments on the pull request,
153
+ and fails the job according to `fail-on`.
171
154
 
172
155
  | Input | Default | Purpose |
173
156
  |---|---|---|
@@ -175,7 +158,6 @@ on the pull request, and fails the job according to `fail-on`.
175
158
  | `budget` | `1200` | Description tokens paid per model step |
176
159
  | `fail-on` | `error` | Fail on `error`, `warn`, or `never` |
177
160
  | `extra-args` | — | Extra `check` flags, e.g. `--no-global` |
178
- | `html` | — | HTML report path; uploaded as an artifact when set |
179
161
  | `comment` | `false` | Post/append the summary as a pull request comment |
180
162
 
181
163
  Outputs: `exit-code`, `error-count`, `warning-count`, `advertised-tokens`.
@@ -206,7 +188,7 @@ All commands accept `--json`, `--lang en|zh`, `--budget`, `--root` and `--no-glo
206
188
  | **Claude Code / Codex** | MCP server (`npx -y opencode-skills-doctor mcp`) |
207
189
  | **Grok CLI / Cursor** | their skill roots are scanned natively |
208
190
  | **Hermes Agent** | MCP entry in `~/.hermes/config.yaml`; `~/.hermes/skills` is scanned |
209
- | **Kimi Code** | `install --client kimi` writes `~/.kimi-code/mcp.json` + the audit skill |
191
+ | **Kimi Code** | `install --client kimi` writes `~/.kimi-code/mcp.json` + the audit skill; or use the native plugin: [kimi-skills-doctor](https://github.com/wangyuchen5110/kimi-skills-doctor) |
210
192
  | **OpenClaw** | separate wrapper with its own native plugin: [openclaw-skills-doctor](https://github.com/wangyuchen5110/openclaw-skills-doctor) |
211
193
 
212
194
  ## Why it is safe
package/SECURITY.md CHANGED
@@ -33,7 +33,7 @@ Include the version, a reproduction, the impact, and a suggested fix if you have
33
33
  - **No network by default.** Two opt-in/opt-out exceptions: `update` fetches the catalog URLs you configure, and the daily update check reads only the latest version number from the npm registry (24 h cache; disable with `"checkUpdates": "off"`, `SKILLS_DOCTOR_UPDATE_CHECK=0` or `--no-update-check`).
34
34
  - **No user data is read.** The tool reads skill files, their referenced resources, agent configuration and its own lockfile. It does not read chat history, session transcripts, source code, documents, model credentials or API keys — and it does not upload anything it reads.
35
35
  - **Config files may hold secrets** (`opencode.json(c)`, MCP configs). They are parsed locally only to locate the entries the tool needs; values are never transmitted, printed, or copied into reports.
36
- - **Reports are local files.** HTML/JSON reports contain file paths and skill descriptions, never file bodies. They can reveal your directory layout, so review them before sharing.
36
+ - **Reports stay on the machine.** Terminal output, JSON, the pull-request comment and the chat notice contain file paths and skill descriptions, never file bodies. They can reveal your directory layout, so review them before sharing.
37
37
  - `check`, `plan`, `scan`, `fix --dry-run` and `mcp` are fully offline; you can run them with networking disabled.
38
38
 
39
39
  ## Risk areas and mitigations
package/SECURITY.zh-CN.md CHANGED
@@ -33,7 +33,7 @@
33
33
  - **默认不联网。** 只有两个例外:`update` 抓取你配置的 catalog URL;每日更新检查只从 npm registry 读取最新版本号(缓存 24h,可用 `"checkUpdates": "off"`、`SKILLS_DOCTOR_UPDATE_CHECK=0` 或 `--no-update-check` 关闭)。
34
34
  - **不读取用户数据。** 只读 skill 文件及其引用资源、agent 配置和自身 lockfile;不读对话历史、会话记录、源代码、文档、模型凭据或 API key,也不上传任何读到的内容。
35
35
  - **配置文件可能含密钥**(`opencode.json(c)`、MCP 配置):只在本地解析以定位需要的条目,值不会被传输、打印或写进报告。
36
- - **报告是本地文件。** HTML/JSON 报告含文件路径和 skill 描述,不含文件正文;可能暴露目录结构,分享前请自行检查。
36
+ - **报告留在本机。** 终端输出、JSON、PR 评论和对话通知含文件路径和 skill 描述,不含文件正文;可能暴露目录结构,分享前请自行检查。
37
37
  - `check`、`plan`、`scan`、`fix --dry-run`、`mcp` 完全离线,可在断网环境运行。
38
38
 
39
39
  ## 风险点与缓解措施
@@ -49,9 +49,9 @@ OpenCode skill 的**本地检测器 + 更新器 + 修复器**,并附带 **Open
49
49
  | **来源** | `.opencode/skills`、`.claude/skills`、`.agents/skills`、`.grok/skills`、`.cursor/skills`、`.hermes/skills`、`.kimi-code/skills`、`.dsh/skills`(含 `$DSH_HOME`)、`opencode.json(c)` 配置、HTTP catalog |
50
50
  | **能查** | 同 ID 遮蔽、重复、超长描述、超大正文、每步 token 预算、断链引用、非法 frontmatter、空正文、陈旧 skill |
51
51
  | **能修** | 归档遮蔽定义(可逆)、先藏重复的 skill、方案只留路由用的第一句 |
52
- | **报告** | 终端、JSON,或自包含 HTML(`--html [file] --open`) |
52
+ | **报告** | OpenCode 对话里的一条通知,加上终端和 JSON |
53
53
  | **规划** | 精确 token 节省:精简描述 / 隐藏 skill,按 N 步折算 |
54
- | **成本** | CLI 0 token;插件工具定义约 191/步,另有每会话一次预警 |
54
+ | **成本** | CLI 0 token;有问题时每个会话在对话里发一条通知 |
55
55
  | **要求** | Node ≥ 20,零运行时依赖 |
56
56
  | **质量** | 63 个测试,ubuntu/macos/windows × Node 20/22/24 CI |
57
57
 
@@ -65,7 +65,7 @@ OpenCode skill 的**本地检测器 + 更新器 + 修复器**,并附带 **Open
65
65
  | **不会读** | 对话历史、会话记录、源代码、文档、凭据、API key、环境变量里的密钥 |
66
66
  | **不会上传** | 任何东西:没有服务端组件,除下面两个可选联网外没有任何远端地址 |
67
67
  | **联网** | 只在你主动触发时发生:`update` 抓取**你自己配置**的 HTTP catalog;`upgrade` / 每日更新检查只读 npm registry 的最新版本号(缓存 24h,可用 `"checkUpdates": "off"`、`SKILLS_DOCTOR_UPDATE_CHECK=0` 或 `--no-update-check` 关闭) |
68
- | **会写** | 只写你要求的内容:HTML/JSON 报告、`.skills-doctor-archive/`、`.skills-doctor.json`、lockfile,以及 `install` 生成的插件/MCP/skill 条目 |
68
+ | **会写** | 只写你要求的内容:`.skills-doctor-archive/`、`.skills-doctor.json`、lockfile,以及 `install` 生成的插件/MCP/skill 条目 |
69
69
 
70
70
  `opencode.json(c)` 和 MCP 配置里可能有凭据;它们只在本地被解析用来定位相关条目,值不会被发送,也不会出现在报告里。报告只含文件路径和 skill 描述(本来就是你的 skill 文件内容),不包含正文。`check`、`plan`、`scan`、`fix --dry-run`、`mcp` 都可以在断网环境下运行。
71
71
 
@@ -132,20 +132,13 @@ skills-doctor check
132
132
  skills-doctor check --json
133
133
  skills-doctor check --budget 800 --strict # CI 用
134
134
  skills-doctor check --root .\some-project --no-global
135
- skills-doctor check --html report.html # 自包含 HTML 报告
136
- skills-doctor check --html report.html --open
137
135
  ```
138
136
 
139
- ### HTML 报告
137
+ ### 对话通知
140
138
 
141
- `--html [file]` 生成单个自包含 HTML 文件(无外部资源):
139
+ OpenCode 2 在你发出第一条消息时,如果有 error 或 warning,就在对话里发一条通知。它是带描述 `skills-doctor` 的 synthetic 消息,会单独占一行,不会再开一轮模型。`/skills-doctor` 会再发一次,没有问题也会说。工具和 MCP 返回同一段短文,不写文件。最多列三条。归档和隐藏仍然要你确认。
142
140
 
143
- - 摘要卡片 + 上下文预算条;
144
- - 可按严重级别和文本过滤的 findings;
145
- - skill 表格:来源、token 成本、描述预览、文件路径;
146
- - 只写 `--html` 不带路径时默认 `./skills-report.html`;`--open` 自动打开浏览器。
147
-
148
- 插件和 MCP 工具同样接受 `html` 参数,所以可以让 agent 先问你要存哪里——例如:*"把报告存到 D:\reports\skills.html"*。
141
+ OpenCode 1.x 没有这条通知接口,同一段话交给模型,让它在回复里说出来。
149
142
 
150
143
  ### `plan`(到底能省多少 token?)
151
144
 
@@ -162,7 +155,7 @@ skills-doctor plan --json
162
155
  - **方案 B**:用最少的隐藏数量把总量压进预算(仍可 `@id` 加载;按单项节省从大到小取,对「最少隐藏数」是最优的);
163
156
  - **推荐方案**:先精简再隐藏,不重复计算,并按 `--steps`(默认 100)折算总节省。
164
157
 
165
- 超预算时 `check` 会打印一行提示,HTML 报告里有同样的「potential savings」卡片。
158
+ 超预算时 `check` 会打印一行提示。具体方案看 `plan`。
166
159
 
167
160
  ### `scan`
168
161
 
@@ -273,9 +266,8 @@ skills-doctor upgrade --apply # npm install -g opencode-skills-doctor@latest
273
266
 
274
267
  它会注册:
275
268
 
276
- - `skills_doctor` 工具(命名空间 `skills`)—— agent 传 `{ directory?, budget?, html? }` 就能拿到 markdown 审计报告;
269
+ - `skills_doctor` 工具(命名空间 `skills`)—— `{ directory?, budget? }` 返回对话简报,不写文件;
277
270
  - `/skills-doctor` 命令 —— 让 agent 跑一次审计并总结修复建议;
278
- - `/skills-report` —— 先问你要把 HTML 报告存到哪里,再写文件;
279
271
  - `/skills-upgrade` —— 检查 skills-doctor 自身有没有新版本。
280
272
 
281
273
  `@opencode/plugin` 是可选 peer 依赖,运行时由 OpenCode 提供。
@@ -317,7 +309,7 @@ npx opencode-skills-doctor install --client desktop
317
309
  ```
318
310
 
319
311
  - 在标准安装位置探测桌面版,并从 `resources/app.asar` 读出内置 core 版本(例如 1.17.11),据此选择对应插件 API;
320
- - 写入与 CLI 相同的插件条目和 `/skills-doctor`、`/skills-report`、`/skills-upgrade` 命令 —— 装完重启桌面应用;
312
+ - 写入与 CLI 相同的插件条目和 `/skills-doctor`、`/skills-upgrade` 命令 —— 装完重启桌面应用;
321
313
  - `--client auto`(默认)在同时装了 CLI 时优先 CLI 版本;`--client opencode` 跳过桌面探测;
322
314
  - `uninstall --client desktop` 反向撤销。
323
315
 
@@ -332,16 +324,15 @@ npx opencode-skills-doctor install
332
324
  安装器会自动识别 OpenCode 版本并接好:
333
325
 
334
326
  - **2.x** —— 在 `opencode.json(c)` 里加 `"plugins": ["opencode-skills-doctor"]`;
335
- - **1.x** —— 把包装进 `~/.config/opencode`、写本地插件入口、生成 `/skills-doctor`、`/skills-report`、`/skills-upgrade` 三个 markdown 命令、补 `"type": "module"`;
327
+ - **1.x** —— 把包装进 `~/.config/opencode`、写本地插件入口、生成 `/skills-doctor`、`/skills-upgrade` 两个 markdown 命令、补 `"type": "module"`;
336
328
  - 改配置前会备份为 `<config>.bak-<timestamp>`。
337
329
 
338
330
  支持 `--dry-run` 预览、`--config-dir <dir>` 指定配置目录、`--no-npm` 跳过装包。
339
331
 
340
332
  装好后你会得到:
341
333
 
342
- - `skills_doctor` 工具(可选 `html` 参数,把报告写到指定路径);
334
+ - `skills_doctor` 工具,返回对话简报,不写文件;
343
335
  - `/skills-doctor` 命令(1.x 是生成的 markdown 命令,2.x 是插件命令);
344
- - `/skills-report` 命令(2.x)—— 会先问你要把 HTML 报告存到哪里;
345
336
  - `/skills-upgrade` 命令(2.x)—— 检查 skills-doctor 自身是否有新版本;
346
337
  - 当 skill 描述预算超标时,系统提示里会注入一次提醒(每个会话最多一次)。
347
338
 
@@ -396,7 +387,7 @@ npx -y opencode-skills-doctor install --client dsh
396
387
 
397
388
  它会找到 `~/.dsh/profiles/<profile>/cordis.patch.yml`(默认 `desktop`,否则用 `--profile <name>`),把 `mcp-skills-doctor` 这一行放在 `- insert:` 块里幂等写入,并把 `skills-doctor-audit` 技能复制到 `~/.dsh/skills/`。重启 DSH(启用 HMR 时会自动重载 patch)加载 MCP 工具;skill 目录有监听,无需重启。
398
389
 
399
- - 工具名 `mcp__skills_doctor__skills_doctor`,参数 `directory`、`budget`、`lang`、`global`、`html`;
390
+ - 工具名 `mcp__skills_doctor__skills_doctor`,参数 `directory`、`budget`、`lang`、`global`;
400
391
  - `uninstall --client dsh` 同时移除配置条目和 skill;
401
392
  - `--dsh-home <dir>` 用于非默认的 DSH 主目录。
402
393
 
package/docs/REFERENCE.md CHANGED
@@ -13,7 +13,7 @@ Local scanner, linter, fixer, updater and **OpenCode plugin** for agent skills
13
13
  | **Skill sources** | `.opencode/skills`, `.claude/skills`, `.agents/skills`, `.grok/skills`, `.cursor/skills`, `.hermes/skills`, `.kimi-code/skills`, `opencode.json(c)` entries, HTTP catalogs |
14
14
  | **What it finds** | shadowed IDs, duplicates, long descriptions, oversized bodies, per-step token budget, broken file references, invalid frontmatter, empty bodies, stale skills |
15
15
  | **What it fixes** | archives shadowed definitions (reversible), hides low-frequency skills, syncs catalogs with drift detection |
16
- | **Reports** | terminal, JSON, or a self-contained HTML report (`--html [file] --open`) |
16
+ | **Reports** | a chat notice in OpenCode, plus the terminal and JSON |
17
17
  | **Plan** | exact token savings: trim descriptions and/or hide skills, projected over N steps |
18
18
  | **Cost** | CLI: 0 tokens. Plugin: ~191 tokens/step for the tool definition, plus a one-time warning per session |
19
19
  | **Requirements** | Node ≥ 20, zero runtime dependencies |
@@ -82,20 +82,13 @@ skills-doctor check
82
82
  skills-doctor check --json
83
83
  skills-doctor check --budget 800 --strict # for CI
84
84
  skills-doctor check --root .\some-project --no-global
85
- skills-doctor check --html report.html # self-contained HTML report
86
- skills-doctor check --html report.html --open
87
85
  ```
88
86
 
89
- ### HTML report
87
+ ### Chat notice
90
88
 
91
- `--html [file]` writes a single self-contained HTML file (no external assets):
89
+ OpenCode 2 posts one notice in the session the first time you send a message, when there is an error or a warning. The notice is a synthetic message with the description `skills-doctor`, so it shows as its own row. It does not start another model turn. `/skills-doctor` posts it again, including when nothing is wrong. The tool and the MCP server return the same short text and do not write a file. At most three findings are listed. Archive and hide still wait for you to confirm.
92
90
 
93
- - summary cards and a context-budget bar;
94
- - findings filterable by severity and text;
95
- - a skill table with scope, token cost, description preview and file path;
96
- - default path `./skills-report.html` when `--html` is given without a value; `--open` launches the browser.
97
-
98
- 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"*.
91
+ On OpenCode 1.x the same text is handed to the model with an instruction to say it in the chat.
99
92
 
100
93
  ### `plan` (how many tokens can I actually save?)
101
94
 
@@ -112,7 +105,7 @@ It answers "how much can I cut, exactly?":
112
105
  - **Option B** hides the fewest skills needed to reach the budget (still loadable with `@id`; picking the largest savings first is optimal for "minimal hides");
113
106
  - **Recommended** trims first, then hides, with no double counting, and projects the saving over `--steps` model steps (default 100).
114
107
 
115
- `check` prints a one-line pointer when you are over budget, and the HTML report shows the same potential-savings card.
108
+ `check` prints a one-line pointer when you are over budget. `plan` is the breakdown.
116
109
 
117
110
  ### `scan`
118
111
 
@@ -223,9 +216,8 @@ The package also exports an OpenCode 2.x plugin:
223
216
 
224
217
  It registers:
225
218
 
226
- - the `skills_doctor` tool (namespace `skills`) — the agent can audit with `{ directory?, budget?, html? }` and gets a markdown report;
227
- - the `/skills-doctor` command — asks the agent to run the audit and summarize fixes;
228
- - `/skills-report` — asks where to save an HTML report, then writes it;
219
+ - the `skills_doctor` tool (namespace `skills`) — `{ directory?, budget? }` returns the short chat briefing and writes nothing;
220
+ - `/skills-doctor` — posts that briefing in the chat;
229
221
  - `/skills-upgrade` — checks whether skills-doctor itself has a newer version.
230
222
 
231
223
  `@opencode/plugin` is an optional peer dependency; OpenCode provides it at runtime.
@@ -267,7 +259,7 @@ npx opencode-skills-doctor install --client desktop
267
259
  ```
268
260
 
269
261
  - detects the app in the standard install locations and reads its bundled core version from `resources/app.asar` (e.g. 1.17.11), then uses the matching plugin API;
270
- - writes the same plugin entry and `/skills-doctor`, `/skills-report`, `/skills-upgrade` commands as the CLI install — restart the desktop app afterwards;
262
+ - writes the same plugin entry and `/skills-doctor` and `/skills-upgrade` commands as the CLI install — restart the desktop app afterwards;
271
263
  - `--client auto` (default) prefers the CLI version when both are installed; `--client opencode` skips desktop detection;
272
264
  - `uninstall --client desktop` reverses it.
273
265
 
@@ -282,18 +274,17 @@ npx opencode-skills-doctor install
282
274
  The installer detects your OpenCode version and wires everything up:
283
275
 
284
276
  - **2.x** — adds `"plugins": ["opencode-skills-doctor"]` to `opencode.json(c)`;
285
- - **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"`;
277
+ - **1.x** — installs the package into `~/.config/opencode`, writes the local plugin entry, generates `/skills-doctor` and `/skills-upgrade` markdown commands, sets `"type": "module"`;
286
278
  - backs up the config file as `<config>.bak-<timestamp>` before editing.
287
279
 
288
280
  Use `--dry-run` to preview, `--config-dir <dir>` for a custom config location, and `--no-npm` to skip the package install.
289
281
 
290
282
  Once installed you get:
291
283
 
292
- - the `skills_doctor` tool (with an optional `html` argument to write a report to a path you choose);
293
- - the `/skills-doctor` command (1.x: generated markdown command; 2.x: plugin command);
294
- - the `/skills-report` command (2.x) that asks where to save the HTML report;
295
- - the `/skills-upgrade` command (2.x) that checks for a newer skills-doctor;
296
- - a one-time system-prompt warning when the skill description budget is exceeded.
284
+ - the `skills_doctor` tool, which returns the chat briefing and writes nothing;
285
+ - `/skills-doctor` (1.x: generated markdown command; 2.x: a notice posted in the chat);
286
+ - `/skills-upgrade` (2.x) that checks for a newer skills-doctor;
287
+ - one chat notice per session when a skill has an error or a warning. On 1.x the same text is given to the model to say out loud. `uninstall` deletes a leftover `/skills-report` command from older installs.
297
288
 
298
289
  ### Uninstall
299
290
 
@@ -346,7 +337,7 @@ npx -y opencode-skills-doctor install --client dsh
346
337
 
347
338
  It locates `~/.dsh/profiles/<profile>/cordis.patch.yml` (default `desktop`; otherwise `--profile <name>`), inserts the `mcp-skills-doctor` row under an `- insert:` block idempotently, and copies the `skills-doctor-audit` skill into `~/.dsh/skills/`. Restart DSH (or let HMR reload the patch) to load the MCP tool; the skill is watched and appears without a restart.
348
339
 
349
- - tool name `mcp__skills_doctor__skills_doctor`, arguments `directory`, `budget`, `lang`, `global`, `html`;
340
+ - tool name `mcp__skills_doctor__skills_doctor`, arguments `directory`, `budget`, `lang`, `global`;
350
341
  - `uninstall --client dsh` removes both the patch entry and the skill;
351
342
  - `--dsh-home <dir>` for a non-standard DSH home.
352
343
 
@@ -41,7 +41,7 @@
41
41
  <text x="22" y="96" fill="#3fb950">✓ 29 skills · ~3,074 tokens/step</text>
42
42
  <text x="22" y="128" fill="#d29922">! over budget ×2</text>
43
43
  <text x="22" y="160" fill="#58a6ff">plan → save ~1,940 tokens/step</text>
44
- <text x="22" y="192" fill="#8b9bb4">HTML report ready →</text>
44
+ <text x="22" y="192" fill="#8b9bb4">told you in the chat →</text>
45
45
  </g>
46
46
  </g>
47
47
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "opencode-skills-doctor",
3
- "version": "0.9.6",
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, Cursor, Hermes Agent, Kimi Code and DeepSeek Harness.",
3
+ "version": "0.9.7",
4
+ "description": "Audit, fix and update agent skills: token cost, broken references, shadowed duplicates, catalog sync. Tells you in the chat. CLI, OpenCode plugin, MCP server; works with OpenCode, Claude Code, Codex, Grok CLI, Cursor, Hermes Agent, Kimi Code and DeepSeek Harness.",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": "./src/plugin.mjs",
@@ -21,9 +21,9 @@ npx -y opencode-skills-doctor check --json
21
21
  Read `summary.advertisedTokens` (cost per model step), `summary.advertised` and `findings[]`.
22
22
  If the command fails, fall back to `opencode-skills-doctor` when installed globally, or ask the user to run it from the repo.
23
23
 
24
- ## 2. Explain the top findings
24
+ ## 2. Tell the user in this chat
25
25
 
26
- Report at most three findings, in plain language, each with its token impact:
26
+ Say at most three findings, in plain language. Do not write a report file.
27
27
 
28
28
  - `token-budget` — descriptions cost roughly N tokens on **every** model step;
29
29
  - `shadowed` — the same skill ID loads from several places, only one wins;
@@ -47,8 +47,3 @@ Report `recommended.savings` (per step), `recommended.final` and `recommended.st
47
47
  - Trim descriptions manually, keeping them discoverable
48
48
  - Ask first, then re-run `check` to confirm the new cost
49
49
 
50
- ## 5. Optional shareable report
51
-
52
- ```powershell
53
- npx -y opencode-skills-doctor check --html skills-report.html --open
54
- ```
package/src/audit.mjs CHANGED
@@ -67,6 +67,35 @@ export function auditSkills({
67
67
  }
68
68
  }
69
69
 
70
+ /**
71
+ * 给对话用的短结论。agent 应原样告诉用户,不要再写成文件。
72
+ */
73
+ export function formatChat(audit, { limit = 3 } = {}) {
74
+ const zh = audit.lang === "zh"
75
+ const problems = audit.findings.filter((finding) => finding.severity === "error" || finding.severity === "warn")
76
+ if (!problems.length) {
77
+ return zh
78
+ ? `skills-doctor:技能没有需要处理的问题。${audit.summary.advertised} 个会在每一步注入,大约 ${audit.summary.advertisedTokens} token。`
79
+ : `skills-doctor: no skill problems to act on. ${audit.summary.advertised} skills are injected on every step, about ${audit.summary.advertisedTokens} tokens.`
80
+ }
81
+ const lines = [
82
+ zh ? "skills-doctor:" : "skills-doctor:",
83
+ zh
84
+ ? `技能有 ${problems.length} 个问题:`
85
+ : `${problems.length} skill problem${problems.length === 1 ? "" : "s"}:`,
86
+ ]
87
+ for (const finding of problems.slice(0, limit)) lines.push(`- ${finding.message}`)
88
+ if (problems.length > limit) {
89
+ lines.push(zh ? `- 还有 ${problems.length - limit} 条,需要的话我再展开。` : `- ${problems.length - limit} more. Ask if you want them.`)
90
+ }
91
+ lines.push(
92
+ zh
93
+ ? "我不会改文件。你确认之后,我可以归档被盖住的副本,或把不常用的藏起来。"
94
+ : "I will not change any files. After you confirm, I can archive shadowed copies or hide skills you rarely use.",
95
+ )
96
+ return lines.join("\n")
97
+ }
98
+
70
99
  /** 把审计结果渲染成适合给 agent / PR 评论用的 markdown。 */
71
100
  export function formatMarkdown(audit, { limit = 25 } = {}) {
72
101
  const lines = []
package/src/cli.mjs CHANGED
@@ -11,7 +11,7 @@ import { startMcpServer } from "./mcp.mjs"
11
11
  import { installOpenCode, uninstallOpenCode } from "./install.mjs"
12
12
  import { installDsh, uninstallDsh } from "./dsh.mjs"
13
13
  import { installKimi, uninstallKimi } from "./kimi.mjs"
14
- import { writeHtmlReport, openReport } from "./html.mjs"
14
+
15
15
  import { planBudget } from "./plan.mjs"
16
16
  import {
17
17
  checkForUpdate,
@@ -80,7 +80,7 @@ function parseArgs(argv) {
80
80
  extraDirs: [],
81
81
  configDir: null,
82
82
  noNpm: false,
83
- html: null,
83
+
84
84
  open: false,
85
85
  apply: false,
86
86
  updateCheck: true,
@@ -174,19 +174,6 @@ function parseArgs(argv) {
174
174
  case "--no-npm":
175
175
  options.noNpm = true
176
176
  break
177
- case "--html": {
178
- const next = args[i + 1]
179
- if (next && !next.startsWith("-")) {
180
- options.html = path.resolve(next)
181
- i++
182
- } else {
183
- options.html = path.resolve("skills-report.html")
184
- }
185
- break
186
- }
187
- case "--open":
188
- options.open = true
189
- break
190
177
  case "--apply":
191
178
  options.apply = true
192
179
  break
@@ -838,23 +825,16 @@ export async function main(argv) {
838
825
  })
839
826
  const result = buildCheckResult(audit, options)
840
827
 
841
- let htmlPath = null
842
- if (options.html) {
843
- htmlPath = writeHtmlReport(audit, options.html)
844
- if (options.open) openReport(htmlPath)
845
- }
846
-
847
828
  if (options.json) {
848
829
  console.log(
849
830
  JSON.stringify(
850
- { ...result, html: htmlPath, skills: serializeSkills(audit.skills, audit.managed) },
831
+ { ...result, skills: serializeSkills(audit.skills, audit.managed) },
851
832
  null,
852
833
  2,
853
834
  ),
854
835
  )
855
836
  } else {
856
837
  printCheck(audit, result, options, t)
857
- if (htmlPath) console.log(dim(t("check.htmlSaved", { path: htmlPath })))
858
838
  await maybeNotifyUpdate({ options, config: loaded.config, t })
859
839
  }
860
840
  process.exitCode = result.exitCode
package/src/i18n.mjs CHANGED
@@ -33,8 +33,6 @@ Options:
33
33
  --into <dir> update: output directory (default: .opencode/skills)
34
34
  --check-only update: only report available updates
35
35
  --prune update: delete skills that disappeared from the catalog
36
- --html [file] check: write a self-contained HTML report (default: skills-report.html)
37
- --open check: open the HTML report after writing it
38
36
  --config-dir <dir> install/uninstall: OpenCode config directory (default: ~/.config/opencode)
39
37
  --client <name> install/uninstall: auto | opencode | desktop | dsh | kimi (default auto)
40
38
  --profile <name> dsh: profile to install into (default: desktop, or the only one)
@@ -60,7 +58,6 @@ Exit codes: 0 ok; 1 errors (and warnings with --strict); 2 bad arguments or runt
60
58
  "check.configError": " ! cannot parse config: {path} ({error})",
61
59
  "check.missingPath": " ! path in config does not exist: {path}",
62
60
  "check.none": "No problems found.",
63
- "check.htmlSaved": " HTML report: {path}",
64
61
  "check.summary": "{errors} error, {warnings} warn, {infos} info",
65
62
  "check.strict": " (--strict: warnings also fail)",
66
63
  "check.hints": "Hints:",
@@ -265,8 +262,6 @@ Exit codes: 0 ok; 1 errors (and warnings with --strict); 2 bad arguments or runt
265
262
  --into <dir> update 的落盘目录(默认 .opencode/skills)
266
263
  --check-only update 只报告可用更新,不下载
267
264
  --prune update: 删除 catalog 中已移除的 skill
268
- --html [file] check: 生成自包含 HTML 报告(默认 skills-report.html)
269
- --open check: 写完报告后用浏览器打开
270
265
  --config-dir <dir> install/uninstall: OpenCode 配置目录(默认 ~/.config/opencode)
271
266
  --client <name> install/uninstall: auto | opencode | desktop | dsh | kimi(默认 auto)
272
267
  --profile <name> dsh: 安装到哪个 profile(默认 desktop,或唯一的一个)
@@ -292,7 +287,6 @@ Exit codes: 0 ok; 1 errors (and warnings with --strict); 2 bad arguments or runt
292
287
  "check.configError": " ! 配置文件无法解析: {path}({error})",
293
288
  "check.missingPath": " ! 配置里引用的路径不存在: {path}",
294
289
  "check.none": "没有发现问题。",
295
- "check.htmlSaved": " HTML 报告: {path}",
296
290
  "check.summary": "{errors} error, {warnings} warn, {infos} info",
297
291
  "check.strict": "(--strict:warning 也会非 0 退出)",
298
292
  "check.hints": "建议:",
package/src/install.mjs CHANGED
@@ -11,15 +11,9 @@ const PLUGIN_FILE = "skills-doctor.js"
11
11
  const V1_PLUGIN_SOURCE = `// Generated by skills-doctor install\n// OpenCode 1.x plugin entry\nexport { SkillsDoctorPlugin } from "opencode-skills-doctor/v1"\n`
12
12
 
13
13
  const V1_COMMAND = `---
14
- description: Audit local skills with skills-doctor
14
+ description: Tell the user about skill problems in this chat
15
15
  ---
16
- Use the skills_doctor tool to audit the skills in this project. Summarize the top findings and propose concrete fixes (archive shadowed duplicates, hide low-frequency skills). $ARGUMENTS
17
- `
18
-
19
- const V1_REPORT_COMMAND = `---
20
- description: Write an HTML skills report to a path you choose
21
- ---
22
- 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
16
+ Call the skills_doctor tool, then tell the user its briefing in this chat. Do not write a report file. Do not change skill files unless the user confirms. $ARGUMENTS
23
17
  `
24
18
 
25
19
  const V1_UPGRADE_COMMAND = `---
@@ -30,9 +24,9 @@ Run \`npx -y opencode-skills-doctor upgrade\` in the terminal (add --apply only
30
24
 
31
25
  const V1_COMMANDS = [
32
26
  { file: "skills-doctor.md", body: V1_COMMAND },
33
- { file: "skills-report.md", body: V1_REPORT_COMMAND },
34
27
  { file: "skills-upgrade.md", body: V1_UPGRADE_COMMAND },
35
28
  ]
29
+ const V1_LEGACY_COMMANDS = ["skills-report.md"]
36
30
 
37
31
  export function defaultConfigDir() {
38
32
  const base = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config")
@@ -353,11 +347,14 @@ export function uninstallOpenCode({
353
347
  }
354
348
  }
355
349
 
356
- for (const command of V1_COMMANDS) {
350
+ for (const command of [...V1_COMMANDS, ...V1_LEGACY_COMMANDS.map((file) => ({ file, legacy: true }))]) {
357
351
  const commandFile = path.join(dir, "command", command.file)
358
352
  if (!fs.existsSync(commandFile)) continue
359
353
  const content = readText(commandFile) ?? ""
360
- if (content.includes("skills_doctor tool")) {
354
+ const owned = command.legacy
355
+ ? content.includes("skills_doctor") || content.includes("skills-doctor")
356
+ : content.includes("skills_doctor tool")
357
+ if (owned) {
361
358
  if (!dryRun) {
362
359
  fs.rmSync(commandFile, { force: true })
363
360
  act(`deleted ${commandFile}`)
package/src/mcp.mjs CHANGED
@@ -1,5 +1,4 @@
1
- import { auditSkills, formatMarkdown } from "./audit.mjs"
2
- import { writeHtmlReport } from "./html.mjs"
1
+ import { auditSkills, formatChat } from "./audit.mjs"
3
2
  import { normalizeLang, detectLang } from "./i18n.mjs"
4
3
 
5
4
  export const PROTOCOL_VERSION = "2025-06-18"
@@ -10,7 +9,7 @@ export function toolDefinition() {
10
9
  return {
11
10
  name: TOOL_NAME,
12
11
  description:
13
- "Audit installed agent skills (OpenCode, Claude Code, Grok, Cursor, Hermes, Kimi, DSH): shadowed duplicates, per-step description token budget, oversized bodies and fix hints. Returns a markdown report.",
12
+ "Audit installed agent skills and return a short briefing to say to the user in the chat. Does not write a file.",
14
13
  inputSchema: {
15
14
  type: "object",
16
15
  properties: {
@@ -31,10 +30,6 @@ export function toolDefinition() {
31
30
  type: "boolean",
32
31
  description: "Include global skill directories such as ~/.config/opencode and ~/.claude (default true)",
33
32
  },
34
- html: {
35
- type: "string",
36
- description: "Optional path; when set, a self-contained HTML report is written there and the path is returned",
37
- },
38
33
  },
39
34
  additionalProperties: false,
40
35
  },
@@ -91,12 +86,7 @@ export async function handleMessage(message, { version = "0.0.0" } = {}) {
91
86
  const lang = normalizeLang(args.lang) ?? detectLang()
92
87
 
93
88
  const audit = auditSkills({ cwd: directory, budget, lang, includeGlobal: args.global !== false })
94
- let text = formatMarkdown(audit)
95
- if (typeof args.html === "string" && args.html.trim()) {
96
- const file = writeHtmlReport(audit, args.html.trim(), { title: "skills-doctor report" })
97
- text += `\n\nHTML report written to \`${file}\``
98
- }
99
- return ok(id, { content: [{ type: "text", text }] })
89
+ return ok(id, { content: [{ type: "text", text: formatChat(audit) }] })
100
90
  }
101
91
 
102
92
  default:
package/src/notify.mjs ADDED
@@ -0,0 +1,62 @@
1
+ import { auditSkills, formatChat } from "./audit.mjs"
2
+
3
+ const settled = new Set()
4
+ const visible = new Set()
5
+
6
+ function keyOf(cwd, sessionID) {
7
+ return `${cwd}::${sessionID}`
8
+ }
9
+
10
+ /** 这条会话里,用户已经能在时间线上看到通知。 */
11
+ export function chatNoticeVisible(cwd, sessionID) {
12
+ return visible.has(keyOf(cwd, sessionID))
13
+ }
14
+
15
+ /**
16
+ * 往 OpenCode 会话里发一条用户能看见的通知。
17
+ * 带 description 的 synthetic 会单独出现在对话里;resume 为 false 时不另开一轮模型。
18
+ * 每个会话最多自动发一次。force 用于用户明确执行了 /skills-doctor。
19
+ */
20
+ export async function postChatNotice({
21
+ sessionID,
22
+ cwd = process.cwd(),
23
+ includeGlobal = true,
24
+ synthetic,
25
+ resume = false,
26
+ force = false,
27
+ when = "problems",
28
+ } = {}) {
29
+ if (!sessionID || typeof synthetic !== "function") return null
30
+ const key = keyOf(cwd, sessionID)
31
+ if (!force && settled.has(key)) return null
32
+
33
+ const audit = auditSkills({ cwd, includeGlobal })
34
+ const notable = audit.findings.some((finding) => finding.severity === "error" || finding.severity === "warn")
35
+ if (when === "problems" && !notable) {
36
+ settled.add(key)
37
+ return null
38
+ }
39
+
40
+ const text = formatChat(audit)
41
+ try {
42
+ await synthetic({
43
+ sessionID,
44
+ text,
45
+ description: "skills-doctor",
46
+ resume,
47
+ })
48
+ } catch {
49
+ if (!force) settled.add(key)
50
+ return null
51
+ }
52
+
53
+ settled.add(key)
54
+ visible.add(key)
55
+ return text
56
+ }
57
+
58
+ /** 测试用。 */
59
+ export function resetChatNotices() {
60
+ settled.clear()
61
+ visible.clear()
62
+ }
package/src/plugin-v1.mjs CHANGED
@@ -1,6 +1,5 @@
1
1
  import { tool } from "@opencode-ai/plugin"
2
- import { auditSkills, formatMarkdown } from "./audit.mjs"
3
- import { writeHtmlReport } from "./html.mjs"
2
+ import { auditSkills, formatChat } from "./audit.mjs"
4
3
  import { budgetWarning } from "./warning.mjs"
5
4
  import { loadConfig } from "./config.mjs"
6
5
  import { checkForUpdate, resolveMode, toolVersion } from "./self-update.mjs"
@@ -33,27 +32,21 @@ export const SkillsDoctorPlugin = async ({ directory }) => ({
33
32
  tool: {
34
33
  skills_doctor: tool({
35
34
  description:
36
- "Audit installed agent skills (OpenCode, Claude Code, Grok, Cursor, Hermes, Kimi, DSH): shadowing, per-step token budget and fix hints. Returns a markdown report.",
35
+ "Audit installed agent skills and return a short briefing to say to the user in the chat. Does not write a file.",
37
36
  args: z.object({
38
37
  directory: z.string().optional().describe("Directory to audit (defaults to the current project)"),
39
38
  budget: z.number().optional().describe("Description token budget (default 1200)"),
40
- html: z.string().optional().describe("Optional path; when set, a self-contained HTML report is written there"),
41
39
  }),
42
40
  execute: async (args) => {
43
41
  const cwd = args.directory || directory || process.cwd()
44
- const audit = auditSkills({ cwd, budget: typeof args.budget === "number" ? args.budget : null, lang: "en" })
45
- let markdown = formatMarkdown(audit)
46
- if (typeof args.html === "string" && args.html.trim()) {
47
- const file = writeHtmlReport(audit, args.html.trim(), { title: "skills-doctor report" })
48
- markdown += `\n\nHTML report written to \`${file}\``
49
- }
50
- return markdown
42
+ const audit = auditSkills({ cwd, budget: typeof args.budget === "number" ? args.budget : null })
43
+ return formatChat(audit)
51
44
  },
52
45
  }),
53
46
  },
54
47
  "experimental.chat.system.transform": async (input, output) => {
55
48
  const warning = budgetWarning({ cwd: directory, sessionID: input?.sessionID })
56
- if (warning) output.system.push(warning)
49
+ if (warning) output.system.push(`Say this to the user in the chat. Do not write a file.\n\n${warning}`)
57
50
  const note = await updateNote({ directory, sessionID: input?.sessionID })
58
51
  if (note) output.system.push(note)
59
52
  },
package/src/plugin.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Plugin } from "@opencode/plugin"
2
- import { auditSkills, formatMarkdown } from "./audit.mjs"
3
- import { writeHtmlReport } from "./html.mjs"
2
+ import { auditSkills, formatChat } from "./audit.mjs"
3
+ import { postChatNotice, chatNoticeVisible } from "./notify.mjs"
4
4
  import { budgetWarning } from "./warning.mjs"
5
5
  import { loadConfig } from "./config.mjs"
6
6
  import { checkForUpdate, resolveMode, toolVersion } from "./self-update.mjs"
@@ -35,16 +35,12 @@ export default Plugin.define({
35
35
  editor.namespace({ name: "skills", description: "OpenCode skill diagnostics" })
36
36
  editor.add({
37
37
  name: "doctor",
38
- description: "Audit installed agent skills (OpenCode, Claude Code, Grok, Cursor, Hermes, Kimi, DSH): shadowing, per-step token budget and fix hints",
38
+ description: "Audit installed agent skills and return a short briefing to say to the user in the chat. Does not write a file.",
39
39
  input: {
40
40
  type: "object",
41
41
  properties: {
42
42
  directory: { type: "string", description: "Directory to audit (defaults to the current project)" },
43
43
  budget: { type: "number", description: "Description token budget (default 1200)" },
44
- html: {
45
- type: "string",
46
- description: "Optional path; when set, a self-contained HTML report is written there",
47
- },
48
44
  },
49
45
  additionalProperties: false,
50
46
  },
@@ -52,13 +48,8 @@ export default Plugin.define({
52
48
  execute: async (input) => {
53
49
  const directory = (input && typeof input.directory === "string" && input.directory) || ctx.location.directory
54
50
  const budget = input && typeof input.budget === "number" ? input.budget : null
55
- const audit = auditSkills({ cwd: directory, budget, lang: "en" })
56
- let markdown = formatMarkdown(audit)
57
- if (input && typeof input.html === "string" && input.html.trim()) {
58
- const file = writeHtmlReport(audit, input.html.trim(), { title: "skills-doctor report" })
59
- markdown += `\n\nHTML report written to \`${file}\``
60
- }
61
- return { content: markdown }
51
+ const audit = auditSkills({ cwd: directory, budget })
52
+ return { content: formatChat(audit) }
62
53
  },
63
54
  })
64
55
  })
@@ -66,25 +57,15 @@ export default Plugin.define({
66
57
  await ctx.command.transform((editor) => {
67
58
  editor.add({
68
59
  name: "skills-doctor",
69
- description: "Audit local OpenCode skills",
70
- execute: async ({ sessionID, prompt, delivery }) => {
71
- await ctx.session.prompt({
72
- ...prompt,
73
- sessionID,
74
- text: "Run a skills audit with the skills_doctor tool, then summarize the top issues and propose concrete fixes.",
75
- delivery,
76
- })
77
- },
78
- })
79
- editor.add({
80
- name: "skills-report",
81
- description: "Write a self-contained HTML skills report to a path you choose",
82
- execute: async ({ sessionID, prompt, delivery }) => {
83
- await ctx.session.prompt({
84
- ...prompt,
60
+ description: "Tell the user about skill problems in this chat",
61
+ execute: async ({ sessionID }) => {
62
+ await postChatNotice({
85
63
  sessionID,
86
- text: "Ask me where to save the HTML report (a file path, default ./skills-report.html), then call the skills_doctor tool with the html argument set to that path and tell me where it was written.",
87
- delivery,
64
+ cwd: ctx.location.directory,
65
+ synthetic: (input) => ctx.session.synthetic(input),
66
+ resume: true,
67
+ force: true,
68
+ when: "always",
88
69
  })
89
70
  },
90
71
  })
@@ -102,10 +83,32 @@ export default Plugin.define({
102
83
  })
103
84
  })
104
85
 
105
- // 每个会话最多注入一次预算预警,避免刷屏
86
+ // 用户发消息时,把问题直接写进对话。resume 为 false,不额外叫醒模型。
87
+ // 没有 prompt hook 的旧宿主仍靠下面的 context 退路。
88
+ try {
89
+ await ctx.session.hook("prompt", async (event) => {
90
+ await postChatNotice({
91
+ sessionID: event.sessionID,
92
+ cwd: ctx.location.directory,
93
+ synthetic: (input) => ctx.session.synthetic(input),
94
+ })
95
+ })
96
+ } catch {
97
+ // 旧版本不认识这个 hook
98
+ }
99
+
100
+ // 通知已经出现在对话里时,不再把同一段话塞进系统提示。
101
+ // 老版本没有 synthetic 时,退回系统提示,让模型在回复里说出来。
106
102
  await ctx.session.hook("context", async (event) => {
107
- const warning = budgetWarning({ cwd: ctx.location.directory, sessionID: event.sessionID })
108
- if (warning) event.system.push({ type: "text", text: warning })
103
+ if (!chatNoticeVisible(ctx.location.directory, event.sessionID)) {
104
+ const warning = budgetWarning({ cwd: ctx.location.directory, sessionID: event.sessionID })
105
+ if (warning) {
106
+ event.system.push({
107
+ type: "text",
108
+ text: `Say this to the user in the chat. Do not write a file.\n\n${warning}`,
109
+ })
110
+ }
111
+ }
109
112
  const note = await updateNote({ directory: ctx.location.directory, sessionID: event.sessionID })
110
113
  if (note) event.system.push({ type: "text", text: note })
111
114
  })
package/src/warning.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { auditSkills } from "./audit.mjs"
1
+ import { auditSkills, formatChat } from "./audit.mjs"
2
2
 
3
3
  const cache = new Map()
4
4
  const warned = new Set()
@@ -15,12 +15,10 @@ export function budgetWarning({ cwd = process.cwd(), sessionID = "global", ttlMs
15
15
  let entry = cache.get(cwd)
16
16
  if (!entry || now - entry.at > ttlMs) {
17
17
  const audit = auditSkills({ cwd, includeGlobal })
18
- const over = audit.counts.errors > 0
18
+ const notable = audit.findings.some((finding) => finding.severity === "error" || finding.severity === "warn")
19
19
  entry = {
20
20
  at: now,
21
- text: over
22
- ? `[skills-doctor] ${audit.summary.unique} skills use ~${audit.summary.advertisedTokens} tokens of descriptions per step (budget ${audit.budget}); ${audit.counts.errors} error(s), ${audit.counts.warnings} warning(s). Use the skills_doctor tool for details, then hide low-frequency skills with \`skills-doctor fix --hide <ids>\`.`
23
- : null,
21
+ text: notable ? formatChat(audit) : null,
24
22
  }
25
23
  cache.set(cwd, entry)
26
24
  }
package/src/html.mjs DELETED
@@ -1,171 +0,0 @@
1
- import fs from "node:fs"
2
- import path from "node:path"
3
- import { spawn } from "node:child_process"
4
- import { planBudget } from "./plan.mjs"
5
-
6
- function esc(value) {
7
- return String(value ?? "").replace(/[&<>"']/g, (char) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[char])
8
- }
9
-
10
- const SEVERITY_LABEL = { error: "error", warn: "warn", info: "info" }
11
-
12
- /** 生成自包含 HTML 报告(无外部资源,可直接分享/作 CI artifact)。 */
13
- export function renderHtml(audit, { title = "skills-doctor report", now = new Date() } = {}) {
14
- const summary = audit.summary
15
- const counts = audit.counts
16
- const budget = audit.budget
17
- const plan = planBudget(audit)
18
- const ratio = budget > 0 ? Math.min(1.5, summary.advertisedTokens / (budget * 2)) : 0
19
- const barWidth = Math.min(100, Math.round(ratio * 100))
20
- const barClass = summary.advertisedTokens > budget * 2 ? "over" : summary.advertisedTokens > budget ? "warn" : "ok"
21
-
22
- const findings = audit.findings
23
- .map(
24
- (finding) => `<tr class="finding ${finding.severity}" data-severity="${finding.severity}">
25
- <td><span class="badge ${finding.severity}">${SEVERITY_LABEL[finding.severity] ?? finding.severity}</span></td>
26
- <td class="rule">${esc(finding.rule)}</td>
27
- <td>${esc(finding.message)}</td>
28
- </tr>`,
29
- )
30
- .join("\n")
31
-
32
- const skills = audit.skills
33
- .map(
34
- (skill) => `<tr class="skill">
35
- <td class="mono">${esc(skill.id)}</td>
36
- <td>${esc(skill.source.scope)}</td>
37
- <td>${skill.advertised ? "yes" : "no"}</td>
38
- <td class="num">${skill.descriptionTokens}</td>
39
- <td class="num">${skill.bodyTokens}</td>
40
- <td class="desc">${esc(String(skill.description ?? "").slice(0, 80))}</td>
41
- <td class="mono path">${esc(skill.file)}</td>
42
- </tr>`,
43
- )
44
- .join("\n")
45
-
46
- return `<!doctype html>
47
- <html lang="en">
48
- <head>
49
- <meta charset="utf-8">
50
- <meta name="viewport" content="width=device-width, initial-scale=1">
51
- <title>${esc(title)}</title>
52
- <style>
53
- :root { color-scheme: light dark; --fg:#1f2328; --muted:#656d76; --bg:#fff; --card:#f6f8fa; --border:#d0d7de; --red:#cf222e; --yellow:#9a6700; --green:#1a7f37; --blue:#0969da; }
54
- @media (prefers-color-scheme: dark) { :root { --fg:#e6edf3; --muted:#8d96a0; --bg:#0d1117; --card:#161b22; --border:#30363d; --red:#ff7b72; --yellow:#d29922; --green:#3fb950; --blue:#58a6ff; } }
55
- * { box-sizing: border-box; }
56
- body { margin:0; padding:32px; font:14px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; color:var(--fg); background:var(--bg); }
57
- h1 { margin:0 0 4px; font-size:24px; }
58
- h2 { margin:32px 0 12px; font-size:18px; }
59
- .sub { color:var(--muted); font-size:12px; }
60
- .cards { display:grid; grid-template-columns:repeat(auto-fit,minmax(150px,1fr)); gap:12px; margin:20px 0; }
61
- .card { background:var(--card); border:1px solid var(--border); border-radius:10px; padding:12px 14px; }
62
- .card .k { color:var(--muted); font-size:12px; }
63
- .card .v { font-size:22px; font-weight:600; margin-top:2px; }
64
- .card.error .v { color:var(--red); } .card.warn .v { color:var(--yellow); } .card.info .v { color:var(--blue); }
65
- .bar { height:10px; border-radius:6px; background:var(--card); border:1px solid var(--border); overflow:hidden; margin:8px 0 4px; }
66
- .bar > span { display:block; height:100%; width:${barWidth}%; }
67
- .bar > span.ok { background:var(--green); } .bar > span.warn { background:var(--yellow); } .bar > span.over { background:var(--red); }
68
- table { width:100%; border-collapse:collapse; background:var(--bg); border:1px solid var(--border); border-radius:10px; overflow:hidden; }
69
- th, td { text-align:left; padding:8px 10px; border-bottom:1px solid var(--border); vertical-align:top; }
70
- th { background:var(--card); font-size:12px; color:var(--muted); text-transform:uppercase; letter-spacing:.03em; }
71
- tr:last-child td { border-bottom:none; }
72
- .badge { display:inline-block; padding:1px 8px; border-radius:999px; font-size:11px; font-weight:600; border:1px solid currentColor; }
73
- .badge.error { color:var(--red); } .badge.warn { color:var(--yellow); } .badge.info { color:var(--blue); }
74
- .rule { font-family:ui-monospace,SFMono-Regular,Menlo,monospace; font-size:12px; white-space:nowrap; }
75
- .mono { font-family:ui-monospace,SFMono-Regular,Menlo,monospace; font-size:12px; }
76
- .path { color:var(--muted); word-break:break-all; }
77
- .num { text-align:right; font-variant-numeric:tabular-nums; }
78
- .toolbar { display:flex; gap:8px; flex-wrap:wrap; margin:12px 0; }
79
- .toolbar button, .toolbar input { font:inherit; padding:6px 10px; border-radius:8px; border:1px solid var(--border); background:var(--card); color:var(--fg); }
80
- .toolbar button.active { border-color:var(--blue); color:var(--blue); }
81
- .empty { color:var(--muted); padding:16px; }
82
- footer { margin-top:40px; color:var(--muted); font-size:12px; }
83
- </style>
84
- </head>
85
- <body>
86
- <h1>${esc(title)}</h1>
87
- <div class="sub">generated ${esc(now.toISOString())} · ${esc(audit.cwd)}</div>
88
-
89
- <div class="cards">
90
- <div class="card"><div class="k">definitions</div><div class="v">${summary.total}</div></div>
91
- <div class="card"><div class="k">unique IDs</div><div class="v">${summary.unique}</div></div>
92
- <div class="card"><div class="k">advertised</div><div class="v">${summary.advertised}</div></div>
93
- <div class="card"><div class="k">description tokens / step</div><div class="v">${summary.advertisedTokens}</div></div>
94
- <div class="card"><div class="k">potential savings / step</div><div class="v">${plan.recommended.savings}</div></div>
95
- <div class="card error"><div class="k">errors</div><div class="v">${counts.errors}</div></div>
96
- <div class="card warn"><div class="k">warnings</div><div class="v">${counts.warnings}</div></div>
97
- <div class="card info"><div class="k">info</div><div class="v">${counts.infos}</div></div>
98
- </div>
99
-
100
- <h2>Context budget</h2>
101
- <div class="bar"><span class="${barClass}"></span></div>
102
- <div class="sub">${summary.advertisedTokens} tokens vs budget ${budget}${summary.advertisedTokens > budget * 2 ? " (over 2x)" : summary.advertisedTokens > budget ? " (over)" : " (ok)"}</div>
103
- <div class="sub">plan: trim ${plan.trim.count} + hide ${plan.recommended.hides.length} → ~${plan.recommended.final} tokens/step (saves ~${plan.recommended.savings}; ~${plan.recommended.stepsSavings} over ${plan.steps} steps)</div>
104
-
105
- <h2>Findings (${audit.findings.length})</h2>
106
- <div class="toolbar">
107
- <button data-filter="all" class="active">all</button>
108
- <button data-filter="error">error</button>
109
- <button data-filter="warn">warn</button>
110
- <button data-filter="info">info</button>
111
- <input id="q" placeholder="filter text…" />
112
- </div>
113
- <table id="findings">
114
- <thead><tr><th>severity</th><th>rule</th><th>message</th></tr></thead>
115
- <tbody>
116
- ${findings || '<tr><td colspan="3" class="empty">No problems found.</td></tr>'}
117
- </tbody>
118
- </table>
119
-
120
- <h2>Skills (${audit.skills.length})</h2>
121
- <table>
122
- <thead><tr><th>ID</th><th>scope</th><th>advertised</th><th>desc tokens</th><th>body tokens</th><th>description</th><th>file</th></tr></thead>
123
- <tbody>
124
- ${skills}
125
- </tbody>
126
- </table>
127
-
128
- <footer>skills-doctor · ${esc(audit.version ?? "")} · updates: re-run <code>skills-doctor check --html</code></footer>
129
-
130
- <script>
131
- const buttons = [...document.querySelectorAll("[data-filter]")]
132
- const q = document.getElementById("q")
133
- let active = "all"
134
- function apply() {
135
- const text = (q.value || "").toLowerCase()
136
- for (const row of document.querySelectorAll("tr.finding")) {
137
- const okSeverity = active === "all" || row.dataset.severity === active
138
- const okText = !text || row.textContent.toLowerCase().includes(text)
139
- row.style.display = okSeverity && okText ? "" : "none"
140
- }
141
- }
142
- for (const button of buttons) {
143
- button.addEventListener("click", () => {
144
- active = button.dataset.filter
145
- for (const other of buttons) other.classList.toggle("active", other === button)
146
- apply()
147
- })
148
- }
149
- q.addEventListener("input", apply)
150
- </script>
151
- </body>
152
- </html>
153
- `
154
- }
155
-
156
- export function writeHtmlReport(audit, file, options = {}) {
157
- const target = path.resolve(file)
158
- fs.mkdirSync(path.dirname(target), { recursive: true })
159
- fs.writeFileSync(target, renderHtml(audit, options), "utf8")
160
- return target
161
- }
162
-
163
- /** 用系统默认程序打开报告;失败时静默(调用方已经知道路径)。 */
164
- export function openReport(file) {
165
- const target = path.resolve(file)
166
- try {
167
- if (process.platform === "win32") spawn("cmd", ["/c", "start", "", target], { detached: true, stdio: "ignore" }).unref()
168
- else if (process.platform === "darwin") spawn("open", [target], { detached: true, stdio: "ignore" }).unref()
169
- else spawn("xdg-open", [target], { detached: true, stdio: "ignore" }).unref()
170
- } catch {}
171
- }