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 +22 -40
- package/SECURITY.md +1 -1
- package/SECURITY.zh-CN.md +1 -1
- package/docs/README.zh-CN.md +12 -21
- package/docs/REFERENCE.md +14 -23
- package/docs/assets/banner.svg +1 -1
- package/package.json +2 -2
- package/skills/dsh-skills-doctor/SKILL.md +2 -7
- package/src/audit.mjs +29 -0
- package/src/cli.mjs +3 -23
- package/src/i18n.mjs +0 -6
- package/src/install.mjs +8 -11
- package/src/mcp.mjs +3 -13
- package/src/notify.mjs +62 -0
- package/src/plugin-v1.mjs +5 -12
- package/src/plugin.mjs +38 -35
- package/src/warning.mjs +3 -5
- package/src/html.mjs +0 -171
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>
|
|
9
|
-
|
|
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
|
-
##
|
|
35
|
+
## The hidden tax
|
|
36
36
|
|
|
37
|
-
|
|
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
|
-
|
|
39
|
+
`skills-doctor` measures the exact cost, shows which definition actually wins, and gives you a concrete plan to cut it:
|
|
43
40
|
|
|
44
|
-
|
|
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
|
-
|
|
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** |
|
|
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:
|
|
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
|
-
##
|
|
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
|
-
|
|
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["
|
|
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
|
|
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.
|
|
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,
|
|
170
|
-
|
|
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
|
|
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
|
-
-
|
|
36
|
+
- **报告留在本机。** 终端输出、JSON、PR 评论和对话通知含文件路径和 skill 描述,不含文件正文;可能暴露目录结构,分享前请自行检查。
|
|
37
37
|
- `check`、`plan`、`scan`、`fix --dry-run`、`mcp` 完全离线,可在断网环境运行。
|
|
38
38
|
|
|
39
39
|
## 风险点与缓解措施
|
package/docs/README.zh-CN.md
CHANGED
|
@@ -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
|
-
| **报告** |
|
|
52
|
+
| **报告** | OpenCode 对话里的一条通知,加上终端和 JSON |
|
|
53
53
|
| **规划** | 精确 token 节省:精简描述 / 隐藏 skill,按 N 步折算 |
|
|
54
|
-
| **成本** | CLI 0 token
|
|
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
|
-
| **会写** |
|
|
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
|
-
###
|
|
137
|
+
### 对话通知
|
|
140
138
|
|
|
141
|
-
|
|
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`
|
|
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`)——
|
|
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-
|
|
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-
|
|
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`
|
|
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
|
|
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** |
|
|
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
|
-
###
|
|
87
|
+
### Chat notice
|
|
90
88
|
|
|
91
|
-
|
|
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
|
-
|
|
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
|
|
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`) —
|
|
227
|
-
-
|
|
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
|
|
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
|
|
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
|
|
293
|
-
-
|
|
294
|
-
-
|
|
295
|
-
-
|
|
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
|
|
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
|
|
package/docs/assets/banner.svg
CHANGED
|
@@ -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">
|
|
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.
|
|
4
|
-
"description": "Audit, fix and update agent skills: token cost, broken references, shadowed duplicates,
|
|
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.
|
|
24
|
+
## 2. Tell the user in this chat
|
|
25
25
|
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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:
|
|
14
|
+
description: Tell the user about skill problems in this chat
|
|
15
15
|
---
|
|
16
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
|
45
|
-
|
|
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,
|
|
3
|
-
import {
|
|
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
|
|
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
|
|
56
|
-
|
|
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: "
|
|
70
|
-
execute: async ({ sessionID
|
|
71
|
-
await
|
|
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
|
-
|
|
87
|
-
|
|
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
|
-
|
|
108
|
-
|
|
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
|
|
18
|
+
const notable = audit.findings.some((finding) => finding.severity === "error" || finding.severity === "warn")
|
|
19
19
|
entry = {
|
|
20
20
|
at: now,
|
|
21
|
-
text:
|
|
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) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[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
|
-
}
|