opencode-skills-doctor 0.3.10 → 0.3.12
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 +140 -5
- package/docs/README.zh-CN.md +139 -4
- package/package.json +1 -1
- package/src/cli.mjs +32 -1
- package/src/i18n.mjs +20 -0
- package/src/install.mjs +211 -0
- package/src/plugin-v1.mjs +5 -0
- package/src/plugin.mjs +7 -0
- package/src/warning.mjs +37 -0
package/README.md
CHANGED
|
@@ -1,10 +1,27 @@
|
|
|
1
1
|
# skills-doctor
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/opencode-skills-doctor)
|
|
4
|
+
[](https://www.npmjs.com/package/opencode-skills-doctor)
|
|
5
|
+
[](https://www.npmjs.com/package/opencode-skills-doctor)
|
|
6
|
+
[](LICENSE)
|
|
3
7
|
[](https://github.com/wangyuchen5110/opencodeplugginprogram/actions/workflows/test.yml)
|
|
4
8
|
|
|
5
|
-
Local scanner, linter, fixer, updater and **OpenCode plugin** for skills — zero dependencies, Node ≥ 20.
|
|
9
|
+
Local scanner, linter, fixer, updater and **OpenCode plugin** for agent skills — zero dependencies, Node ≥ 20.
|
|
6
10
|
|
|
7
|
-
**English** | [简体中文](docs/README.zh-CN.md)
|
|
11
|
+
**English** | [简体中文](docs/README.zh-CN.md) · [Changelog](CHANGELOG.md) · [Security](SECURITY.md)
|
|
12
|
+
|
|
13
|
+
## At a glance
|
|
14
|
+
|
|
15
|
+
| | |
|
|
16
|
+
|---|---|
|
|
17
|
+
| **What it is** | Audit, fix and update the skills your coding agent loads |
|
|
18
|
+
| **Interfaces** | CLI · OpenCode plugin (1.x / 2.x) · MCP server (Claude Code, Codex, DSH, …) |
|
|
19
|
+
| **Skill sources** | `.opencode/skills`, `.claude/skills`, `.agents/skills`, `opencode.json(c)` entries, HTTP catalogs |
|
|
20
|
+
| **What it finds** | shadowed IDs, duplicates, long descriptions, oversized bodies, per-step token budget, stale skills |
|
|
21
|
+
| **What it fixes** | archives shadowed definitions (reversible), hides low-frequency skills, syncs catalogs with drift detection |
|
|
22
|
+
| **Cost** | CLI: 0 tokens. Plugin: ~191 tokens/step for the tool definition, plus a one-time warning per session |
|
|
23
|
+
| **Requirements** | Node ≥ 20, zero runtime dependencies |
|
|
24
|
+
| **Quality** | 41 tests, CI on ubuntu/macos/windows × Node 20/22/24 |
|
|
8
25
|
|
|
9
26
|
## Why
|
|
10
27
|
|
|
@@ -38,6 +55,24 @@ npm link
|
|
|
38
55
|
skills-doctor check
|
|
39
56
|
```
|
|
40
57
|
|
|
58
|
+
What a run looks like:
|
|
59
|
+
|
|
60
|
+
```text
|
|
61
|
+
skills-doctor 0.3.11
|
|
62
|
+
sources: 5 (2 exist) definitions: 29 unique IDs: 29 advertised: 29 description budget: ~3074 tokens
|
|
63
|
+
|
|
64
|
+
[E] token-budget Advertised skill descriptions cost about 3074 tokens per model step, over 2x the 1200 budget
|
|
65
|
+
[W] long-description "deep-research" description is 917 chars; it is paid on every model step
|
|
66
|
+
[W] large-body "academic-paper" body is about 9980 tokens; invoking it pastes the whole body into context
|
|
67
|
+
...
|
|
68
|
+
|
|
69
|
+
1 error, 18 warn, 0 info
|
|
70
|
+
|
|
71
|
+
Hints:
|
|
72
|
+
- Trim descriptions, or hide low-frequency skills with metadata.opencode/autoinvoke: false.
|
|
73
|
+
- Move details into references/ and keep SKILL.md to the workflow.
|
|
74
|
+
```
|
|
75
|
+
|
|
41
76
|
## Commands
|
|
42
77
|
|
|
43
78
|
All commands accept `--lang en|zh` (default: auto from the system locale) and `--json`.
|
|
@@ -95,6 +130,19 @@ skills-doctor update --into .\team-skills
|
|
|
95
130
|
- Running `update` again re-downloads drifted files (self-healing);
|
|
96
131
|
- Use `--prune` to delete skills that disappeared from the catalog (by default they are reported but kept).
|
|
97
132
|
|
|
133
|
+
## Does the plugin cost tokens?
|
|
134
|
+
|
|
135
|
+
Local execution is free, but any tool registered with the model is visible on every request. Measured on this repo:
|
|
136
|
+
|
|
137
|
+
| Component | Tokens | When |
|
|
138
|
+
|---|---|---|
|
|
139
|
+
| Scan / lint / fix / update (local work) | **0** | always |
|
|
140
|
+
| `skills_doctor` tool definition | **~191** | every request |
|
|
141
|
+
| Budget warning injected into the system prompt | **~60–80** | once per session, only when over budget |
|
|
142
|
+
| Audit report returned by the tool | **~940** | only when the tool is actually called |
|
|
143
|
+
|
|
144
|
+
So the plugin trades a small fixed overhead for the agent being able to audit by itself. In the example above the skill descriptions cost 3,074 tokens per step; dropping that to the 1,200 budget saves more than 10× the plugin overhead. If you want **zero** context cost, don't install the plugin — run `npx opencode-skills-doctor check` from the terminal or CI instead.
|
|
145
|
+
|
|
98
146
|
## Configuration
|
|
99
147
|
|
|
100
148
|
Optional `.skills-doctor.json` in the working directory (or pass `--config <file>`):
|
|
@@ -157,6 +205,26 @@ It registers the same `skills_doctor` tool (1.x has no `/skills-doctor` command
|
|
|
157
205
|
|
|
158
206
|
> The package root entry is the V2 plugin and requires OpenCode 2.x. The CLI works on both lines.
|
|
159
207
|
|
|
208
|
+
### Deep integration (one command)
|
|
209
|
+
|
|
210
|
+
```powershell
|
|
211
|
+
npx opencode-skills-doctor install
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
The installer detects your OpenCode version and wires everything up:
|
|
215
|
+
|
|
216
|
+
- **2.x** — adds `"plugins": ["opencode-skills-doctor"]` to `opencode.json(c)`;
|
|
217
|
+
- **1.x** — installs the package into `~/.config/opencode`, writes the local plugin entry, adds a `/skills-doctor` command, sets `"type": "module"`;
|
|
218
|
+
- backs up the config file as `<config>.bak-<timestamp>` before editing.
|
|
219
|
+
|
|
220
|
+
Use `--dry-run` to preview, `--config-dir <dir>` for a custom config location, and `--no-npm` to skip the package install.
|
|
221
|
+
|
|
222
|
+
Once installed you get:
|
|
223
|
+
|
|
224
|
+
- the `skills_doctor` tool;
|
|
225
|
+
- the `/skills-doctor` command (1.x: generated markdown command; 2.x: plugin command);
|
|
226
|
+
- a one-time system-prompt warning when the skill description budget is exceeded.
|
|
227
|
+
|
|
160
228
|
## MCP server (Claude Code, Codex, Kimi Code, any MCP client)
|
|
161
229
|
|
|
162
230
|
`skills-doctor mcp` speaks the Model Context Protocol over stdio and exposes one tool: `skills_doctor` with arguments `directory?`, `budget?`, `lang?` and `global?`. It does not need OpenCode installed.
|
|
@@ -201,6 +269,29 @@ Restart DSH. The tool shows up as `mcp__skills_doctor__skills_doctor`.
|
|
|
201
269
|
|
|
202
270
|
On Windows, if the stdio spawn cannot resolve `npx`, use `command: cmd` and `args: ["/c", "npx", "-y", "opencode-skills-doctor", "mcp"]`.
|
|
203
271
|
|
|
272
|
+
## How it works
|
|
273
|
+
|
|
274
|
+
**Audit pipeline**
|
|
275
|
+
|
|
276
|
+
1. Discover every skill source (see the precedence list below).
|
|
277
|
+
2. Parse each `SKILL.md` / flat `.md` frontmatter and estimate its description/body cost.
|
|
278
|
+
3. Resolve which definition wins for each ID (later registration wins; `SKILL.md` directory form wins ties).
|
|
279
|
+
4. Run the rules, compute the per-step token budget, and emit findings with fix hints.
|
|
280
|
+
|
|
281
|
+
**Fix pipeline**
|
|
282
|
+
|
|
283
|
+
`fix` only touches definitions that are shadowed (never loaded): it moves them into `.skills-doctor-archive/<timestamp>/` inside the same source. The effective definition is never moved, and files managed by `update` are skipped. Scope defaults to the project; `--all` extends it to global sources.
|
|
284
|
+
|
|
285
|
+
**Update pipeline**
|
|
286
|
+
|
|
287
|
+
1. Fetch `<catalog>/index.json` (3 attempts with backoff on 5xx/network errors).
|
|
288
|
+
2. Compare each skill's `version` with the lockfile.
|
|
289
|
+
3. Download changed files (same-origin only, path traversal blocked) and write them atomically.
|
|
290
|
+
4. Delete files the new version no longer lists, and clean empty directories.
|
|
291
|
+
5. Verify hashes, write the lockfile, and — with `--prune` — delete skills removed from the catalog.
|
|
292
|
+
|
|
293
|
+
`check` reads the lockfile and reports drift (`managed-drift`), missing files (`managed-missing`) and unknown lock versions (`lock-version-unsupported`). Re-running `update` re-downloads drifted files.
|
|
294
|
+
|
|
204
295
|
## Rules
|
|
205
296
|
|
|
206
297
|
| Rule | Level | Meaning |
|
|
@@ -277,17 +368,61 @@ See [SECURITY.md](SECURITY.md) for the full threat model, hardening checklist an
|
|
|
277
368
|
- `check` is offline; ask a catalog for new versions with `update --check-only`.
|
|
278
369
|
- Config precedence is assumed: global config < project config < `skills` array order.
|
|
279
370
|
|
|
371
|
+
## FAQ and troubleshooting
|
|
372
|
+
|
|
373
|
+
**A skill I created does not appear.**
|
|
374
|
+
Check: does the file have a `description`? Is it hidden via `opencode/autoinvoke: false` or `disable-model-invocation`? Is the ID derived from the path (directory name for `SKILL.md`, filename for flat `.md`)? Is an earlier/later source shadowing it? `skills-doctor scan` shows the winning definition for each ID.
|
|
375
|
+
|
|
376
|
+
**`fix` did nothing.**
|
|
377
|
+
`fix` only archives **shadowed** duplicates and defaults to project-only. Run `skills-doctor fix --dry-run` to see what it would do, and `--all` to include global sources. Content issues (long descriptions, large bodies) are reported, not rewritten.
|
|
378
|
+
|
|
379
|
+
**`check` says the budget is exceeded but the agent feels fine.**
|
|
380
|
+
The token numbers are heuristics, and the 1,200-token budget is a recommendation. Tune it with `--budget` or `.skills-doctor.json`; treat the report as a relative comparison, not billing.
|
|
381
|
+
|
|
382
|
+
**Where did `fix` move my files?**
|
|
383
|
+
Into `.skills-doctor-archive/<timestamp>/` inside the same skill source. Move them back to restore.
|
|
384
|
+
|
|
385
|
+
**Does it update skills automatically in the background?**
|
|
386
|
+
No daemon. Run `skills-doctor update` when you want to sync; use `--check-only` to just list available updates.
|
|
387
|
+
|
|
388
|
+
**Does it work on OpenCode 1.x?**
|
|
389
|
+
Yes — the CLI works on both lines, and `skills-doctor install` wires up the 1.x plugin (local plugin entry + generated command). 1.x has no `/skills-doctor` command registration from plugins, so the installer writes a command file instead.
|
|
390
|
+
|
|
391
|
+
**Why does the plugin cost tokens if it runs locally?**
|
|
392
|
+
Executing code is free, but the tool definition is part of the model's context. That is why the CLI mode exists: zero context cost.
|
|
393
|
+
|
|
280
394
|
## Roadmap
|
|
281
395
|
|
|
282
396
|
- v0.4: content-aware suggestions (which skills are never used), catalog signature verification
|
|
283
397
|
- Later: V1/Claude skill migration report
|
|
284
398
|
|
|
285
|
-
## Development
|
|
399
|
+
## Development and contributing
|
|
286
400
|
|
|
287
401
|
```powershell
|
|
288
|
-
npm test
|
|
402
|
+
npm test # 41 tests (node test/doctor.test.mjs)
|
|
403
|
+
npm run bench # lint timing for 100/500/1000 skills
|
|
289
404
|
```
|
|
290
405
|
|
|
291
|
-
|
|
406
|
+
- CI runs on ubuntu/macos/windows × Node 20/22/24; `prepublishOnly` re-runs the tests before every publish.
|
|
407
|
+
- Issues and PRs are welcome: for behavior changes, open an issue first with the scenario and expected output. Link the issue in the PR.
|
|
408
|
+
- Keep the CLI dependency-free and the output bilingual (`--lang en|zh`) — both are project invariants.
|
|
409
|
+
- Security reports: see [SECURITY.md](SECURITY.md); do not open a public issue for vulnerabilities.
|
|
410
|
+
- Release notes live in [CHANGELOG.md](CHANGELOG.md).
|
|
411
|
+
|
|
412
|
+
### Project layout
|
|
413
|
+
|
|
414
|
+
| Path | Purpose |
|
|
415
|
+
|---|---|
|
|
416
|
+
| `src/audit.mjs` | one-call audit used by CLI, plugin and MCP |
|
|
417
|
+
| `src/discover.mjs` | source discovery, precedence, JSONC parsing |
|
|
418
|
+
| `src/lint.mjs` | rules, token budget, near-duplicate detection |
|
|
419
|
+
| `src/fix.mjs` | archive / hide |
|
|
420
|
+
| `src/catalog.mjs` | updater, lockfile, retries, atomic writes |
|
|
421
|
+
| `src/managed.mjs` | lock verification and drift detection |
|
|
422
|
+
| `src/cli.mjs` | command dispatch and output |
|
|
423
|
+
| `src/i18n.mjs` | English/Chinese strings |
|
|
424
|
+
| `src/mcp.mjs` | MCP stdio server |
|
|
425
|
+
| `src/plugin.mjs` / `src/plugin-v1.mjs` | OpenCode 2.x / 1.x entries |
|
|
426
|
+
| `src/install.mjs` | one-command OpenCode integration |
|
|
292
427
|
|
|
293
428
|
MIT
|
package/docs/README.zh-CN.md
CHANGED
|
@@ -1,10 +1,27 @@
|
|
|
1
1
|
# skills-doctor
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/opencode-skills-doctor)
|
|
4
|
+
[](https://www.npmjs.com/package/opencode-skills-doctor)
|
|
5
|
+
[](https://www.npmjs.com/package/opencode-skills-doctor)
|
|
6
|
+
[](../LICENSE)
|
|
3
7
|
[](https://github.com/wangyuchen5110/opencodeplugginprogram/actions/workflows/test.yml)
|
|
4
8
|
|
|
5
9
|
OpenCode skill 的**本地检测器 + 更新器 + 修复器**,并附带 **OpenCode 插件入口** —— 零依赖,Node ≥ 20。
|
|
6
10
|
|
|
7
|
-
[English](../README.md) | **简体中文**
|
|
11
|
+
[English](../README.md) | **简体中文** · [变更记录](../CHANGELOG.md) · [安全策略](../SECURITY.zh-CN.md)
|
|
12
|
+
|
|
13
|
+
## 一张表看懂
|
|
14
|
+
|
|
15
|
+
| | |
|
|
16
|
+
|---|---|
|
|
17
|
+
| **是什么** | 审计、修复、更新 coding agent 加载的 skill |
|
|
18
|
+
| **形态** | CLI · OpenCode 插件(1.x / 2.x)· MCP server(Claude Code、Codex、DSH…) |
|
|
19
|
+
| **来源** | `.opencode/skills`、`.claude/skills`、`.agents/skills`、`opencode.json(c)` 配置、HTTP catalog |
|
|
20
|
+
| **能查** | 同 ID 遮蔽、重复、超长描述、超大正文、每步 token 预算、陈旧 skill |
|
|
21
|
+
| **能修** | 归档遮蔽定义(可逆)、隐藏低频 skill、catalog 同步并检测 drift |
|
|
22
|
+
| **成本** | CLI 0 token;插件工具定义约 191/步,另有每会话一次预警 |
|
|
23
|
+
| **要求** | Node ≥ 20,零运行时依赖 |
|
|
24
|
+
| **质量** | 41 个测试,ubuntu/macos/windows × Node 20/22/24 CI |
|
|
8
25
|
|
|
9
26
|
## 为什么需要它
|
|
10
27
|
|
|
@@ -38,6 +55,24 @@ npm link
|
|
|
38
55
|
skills-doctor check
|
|
39
56
|
```
|
|
40
57
|
|
|
58
|
+
跑出来大概是这样:
|
|
59
|
+
|
|
60
|
+
```text
|
|
61
|
+
skills-doctor 0.3.11
|
|
62
|
+
来源: 5 个(存在 2 个) 定义: 29 唯一 ID: 29 会展示: 29 描述预算: ~3074 tokens
|
|
63
|
+
|
|
64
|
+
[E] token-budget 模型每步要看到的 skill 描述约 3074 tokens,超过预算 1200 的 2 倍
|
|
65
|
+
[W] long-description "deep-research" 的 description 有 917 字符,每个模型步骤都会占上下文
|
|
66
|
+
[W] large-body "academic-paper" 正文约 9980 tokens,被调用时会整段塞进上下文
|
|
67
|
+
...
|
|
68
|
+
|
|
69
|
+
1 error, 18 warn, 0 info
|
|
70
|
+
|
|
71
|
+
建议:
|
|
72
|
+
- 精简 description,或把低频 skill 设为 metadata.opencode/autoinvoke: false 隐藏。
|
|
73
|
+
- 正文拆到 references/,SKILL.md 只保留流程。
|
|
74
|
+
```
|
|
75
|
+
|
|
41
76
|
## 命令
|
|
42
77
|
|
|
43
78
|
所有命令都支持 `--lang en|zh`(默认按系统 locale 自动选择)和 `--json`。
|
|
@@ -95,6 +130,19 @@ skills-doctor update --into .\team-skills
|
|
|
95
130
|
- 再跑一次 `update` 会自动重下被改坏的文件(drift 自愈);
|
|
96
131
|
- 用 `--prune` 删除 catalog 中已下架的 skill(默认只提示、不删)。
|
|
97
132
|
|
|
133
|
+
## 插件到底费不费 token?
|
|
134
|
+
|
|
135
|
+
本地执行不花 token,但注册给模型的工具每次请求都会带上定义。本仓库实测:
|
|
136
|
+
|
|
137
|
+
| 项 | token | 何时产生 |
|
|
138
|
+
|---|---|---|
|
|
139
|
+
| 扫描 / 检测 / 修复 / 更新(本地) | **0** | 始终 |
|
|
140
|
+
| `skills_doctor` 工具定义 | **约 191** | 每次请求 |
|
|
141
|
+
| 注入系统提示的预算预警 | **约 60–80** | 每会话一次,且仅在超标时 |
|
|
142
|
+
| 工具返回的审计报告 | **约 940** | 仅真正调用工具时 |
|
|
143
|
+
|
|
144
|
+
所以插件是用固定的少量开销,换「模型能自己体检」。上面的例子里 skill 描述每步 3074 tokens,压到 1200 预算能省下 10 倍于插件开销的 token。想要**零上下文成本**,就别装插件——用 `npx opencode-skills-doctor check` 在终端或 CI 里跑。
|
|
145
|
+
|
|
98
146
|
## 配置
|
|
99
147
|
|
|
100
148
|
在工作目录放一个可选的 `.skills-doctor.json`(或用 `--config <file>` 指定):
|
|
@@ -157,6 +205,26 @@ export { SkillsDoctorPlugin } from "opencode-skills-doctor/v1"
|
|
|
157
205
|
|
|
158
206
|
> 包根入口是 V2 插件,需要 OpenCode 2.x;CLI 两条线都能用。
|
|
159
207
|
|
|
208
|
+
### 一键深度集成
|
|
209
|
+
|
|
210
|
+
```powershell
|
|
211
|
+
npx opencode-skills-doctor install
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
安装器会自动识别 OpenCode 版本并接好:
|
|
215
|
+
|
|
216
|
+
- **2.x** —— 在 `opencode.json(c)` 里加 `"plugins": ["opencode-skills-doctor"]`;
|
|
217
|
+
- **1.x** —— 把包装进 `~/.config/opencode`、写本地插件入口、生成 `/skills-doctor` 命令、补 `"type": "module"`;
|
|
218
|
+
- 改配置前会备份为 `<config>.bak-<timestamp>`。
|
|
219
|
+
|
|
220
|
+
支持 `--dry-run` 预览、`--config-dir <dir>` 指定配置目录、`--no-npm` 跳过装包。
|
|
221
|
+
|
|
222
|
+
装好后你会得到:
|
|
223
|
+
|
|
224
|
+
- `skills_doctor` 工具;
|
|
225
|
+
- `/skills-doctor` 命令(1.x 是生成的 markdown 命令,2.x 是插件命令);
|
|
226
|
+
- 当 skill 描述预算超标时,系统提示里会注入一次提醒(每个会话最多一次)。
|
|
227
|
+
|
|
160
228
|
## MCP server(Claude Code、Codex、Kimi Code、任何 MCP 客户端)
|
|
161
229
|
|
|
162
230
|
`skills-doctor mcp` 用 stdio 讲 Model Context Protocol,只暴露一个工具:`skills_doctor`,参数 `directory?`、`budget?`、`lang?`、`global?`。不需要装 OpenCode。
|
|
@@ -201,6 +269,29 @@ DSH 自带 MCP 客户端 bundle(`@deepseek-ai/dsh-mcp-client`)。在 profile
|
|
|
201
269
|
|
|
202
270
|
Windows 上如果 stdio 起不来 `npx`,改成 `command: cmd`、`args: ["/c", "npx", "-y", "opencode-skills-doctor", "mcp"]`。
|
|
203
271
|
|
|
272
|
+
## 工作原理
|
|
273
|
+
|
|
274
|
+
**审计流程**
|
|
275
|
+
|
|
276
|
+
1. 发现所有 skill 来源(见下面的优先级列表)。
|
|
277
|
+
2. 解析每个 `SKILL.md` / 扁平 `.md` 的 frontmatter,估算描述与正文成本。
|
|
278
|
+
3. 解析每个 ID 实际生效的定义(注册越后越优先;同序时 `SKILL.md` 目录形式优先)。
|
|
279
|
+
4. 跑规则、计算每步 token 预算,输出 finding 与修复建议。
|
|
280
|
+
|
|
281
|
+
**修复流程**
|
|
282
|
+
|
|
283
|
+
`fix` 只动「被遮蔽、永远不会加载」的定义:把它们移到同一来源下的 `.skills-doctor-archive/<时间戳>/`。生效版本永远不会被移动,`update` 管理的文件会跳过。默认只处理项目内,`--all` 才扩展到全局来源。
|
|
284
|
+
|
|
285
|
+
**更新流程**
|
|
286
|
+
|
|
287
|
+
1. 抓 `<catalog>/index.json`(5xx/网络错误重试 3 次)。
|
|
288
|
+
2. 对比每个 skill 的 `version` 与 lockfile。
|
|
289
|
+
3. 下载变化的文件(仅同源、阻断路径穿越),原子写入。
|
|
290
|
+
4. 删除新版本不再包含的文件,清理空目录。
|
|
291
|
+
5. 校验哈希、写 lockfile;加 `--prune` 时删除已从 catalog 下架的 skill。
|
|
292
|
+
|
|
293
|
+
`check` 会读 lockfile,报告 drift(`managed-drift`)、文件缺失(`managed-missing`)和未知 lock 版本(`lock-version-unsupported`);再跑一次 `update` 会重下被改坏的文件。
|
|
294
|
+
|
|
204
295
|
## 检测规则
|
|
205
296
|
|
|
206
297
|
| 规则 | 级别 | 含义 |
|
|
@@ -277,17 +368,61 @@ Windows 上如果 stdio 起不来 `npx`,改成 `command: cmd`、`args: ["/c",
|
|
|
277
368
|
- `check` 不联网;要看 catalog 新版本用 `update --check-only`。
|
|
278
369
|
- 配置优先级假设:全局配置 < 项目配置 < `skills` 数组顺序。
|
|
279
370
|
|
|
371
|
+
## 常见问题(FAQ)
|
|
372
|
+
|
|
373
|
+
**我建的 skill 没出现。**
|
|
374
|
+
依次检查:有没有 `description`?是不是被 `opencode/autoinvoke: false` 或 `disable-model-invocation` 隐藏了?ID 是否按路径推导(目录形式取目录名,扁平 `.md` 取文件名)?是不是被别的来源遮蔽了?跑 `skills-doctor scan` 能看到每个 ID 实际生效的是哪份。
|
|
375
|
+
|
|
376
|
+
**`fix` 跑了但什么都没变。**
|
|
377
|
+
`fix` 只归档**被遮蔽**的重复定义,而且默认只处理项目内。先 `skills-doctor fix --dry-run` 看会做什么;要动全局加 `--all`。描述过长、正文过大属于内容问题,只报告、不自动改写。
|
|
378
|
+
|
|
379
|
+
**`check` 说预算超标,但用起来没感觉。**
|
|
380
|
+
token 是启发式估算,1200 是建议预算。可以用 `--budget` 或 `.skills-doctor.json` 调整;把它当相对比较,不是账单。
|
|
381
|
+
|
|
382
|
+
**`fix` 把我的文件挪到哪了?**
|
|
383
|
+
同一 skill 来源下的 `.skills-doctor-archive/<时间戳>/`,移回去即可恢复。
|
|
384
|
+
|
|
385
|
+
**会在后台自动更新 skill 吗?**
|
|
386
|
+
没有常驻进程。想同步时跑 `skills-doctor update`;只想看有没有新版本用 `--check-only`。
|
|
387
|
+
|
|
388
|
+
**支持 OpenCode 1.x 吗?**
|
|
389
|
+
支持。CLI 两条线都能用;`skills-doctor install` 会给 1.x 写本地插件入口并生成命令文件(1.x 没有插件注册命令的 API)。
|
|
390
|
+
|
|
391
|
+
**本地插件为什么还费 token?**
|
|
392
|
+
执行代码免费,但工具定义会进模型上下文。这也是保留 CLI 模式的原因:零上下文成本。
|
|
393
|
+
|
|
280
394
|
## Roadmap
|
|
281
395
|
|
|
282
396
|
- v0.4:内容层面的建议(哪些 skill 从未被使用)、catalog 签名校验
|
|
283
397
|
- 后续:V1/Claude skill 兼容迁移报告
|
|
284
398
|
|
|
285
|
-
##
|
|
399
|
+
## 开发与贡献
|
|
286
400
|
|
|
287
401
|
```powershell
|
|
288
|
-
npm test
|
|
402
|
+
npm test # 41 个测试(node test/doctor.test.mjs)
|
|
403
|
+
npm run bench # 100/500/1000 个 skill 的 lint 耗时
|
|
289
404
|
```
|
|
290
405
|
|
|
291
|
-
|
|
406
|
+
- CI 跑 ubuntu/macos/windows × Node 20/22/24;`prepublishOnly` 会在每次发布前重跑测试。
|
|
407
|
+
- 欢迎 issue 和 PR:行为改动请先开 issue 说明场景与期望输出,PR 里关联该 issue。
|
|
408
|
+
- 两条不变量:CLI 保持零依赖;输出保持中英双语(`--lang en|zh`)。
|
|
409
|
+
- 安全问题请看 [SECURITY.zh-CN.md](../SECURITY.zh-CN.md),不要开公开 issue。
|
|
410
|
+
- 版本记录见 [CHANGELOG.md](../CHANGELOG.md)。
|
|
411
|
+
|
|
412
|
+
### 目录结构
|
|
413
|
+
|
|
414
|
+
| 路径 | 作用 |
|
|
415
|
+
|---|---|
|
|
416
|
+
| `src/audit.mjs` | 一次完整审计(CLI / 插件 / MCP 共用) |
|
|
417
|
+
| `src/discover.mjs` | 来源发现、优先级、JSONC 解析 |
|
|
418
|
+
| `src/lint.mjs` | 规则、token 预算、近似描述检测 |
|
|
419
|
+
| `src/fix.mjs` | 归档 / hide |
|
|
420
|
+
| `src/catalog.mjs` | 更新器、lockfile、重试、原子写入 |
|
|
421
|
+
| `src/managed.mjs` | lock 校验与 drift 检测 |
|
|
422
|
+
| `src/cli.mjs` | 命令分发与输出 |
|
|
423
|
+
| `src/i18n.mjs` | 中英文案 |
|
|
424
|
+
| `src/mcp.mjs` | MCP stdio server |
|
|
425
|
+
| `src/plugin.mjs` / `src/plugin-v1.mjs` | OpenCode 2.x / 1.x 入口 |
|
|
426
|
+
| `src/install.mjs` | 一键接入 OpenCode |
|
|
292
427
|
|
|
293
428
|
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-skills-doctor",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.12",
|
|
4
4
|
"description": "Audit, fix and update agent skills: per-step context cost, shadowed duplicates, safe archive, catalog sync. CLI, OpenCode plugin and MCP server (Claude Code / Codex / any MCP client).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
package/src/cli.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import { planFixes, applyFixes, hideSkills } from "./fix.mjs"
|
|
|
8
8
|
import { auditSkills } from "./audit.mjs"
|
|
9
9
|
import { loadConfig } from "./config.mjs"
|
|
10
10
|
import { startMcpServer } from "./mcp.mjs"
|
|
11
|
+
import { installOpenCode } from "./install.mjs"
|
|
11
12
|
import { padDisplay } from "./parse.mjs"
|
|
12
13
|
import { detectLang, normalizeLang, translator } from "./i18n.mjs"
|
|
13
14
|
|
|
@@ -56,12 +57,14 @@ function parseArgs(argv) {
|
|
|
56
57
|
into: null,
|
|
57
58
|
checkOnly: false,
|
|
58
59
|
prune: false,
|
|
60
|
+
configDir: null,
|
|
61
|
+
noNpm: false,
|
|
59
62
|
help: false,
|
|
60
63
|
version: false,
|
|
61
64
|
}
|
|
62
65
|
|
|
63
66
|
const args = [...argv]
|
|
64
|
-
const commands = new Set(["check", "scan", "update", "fix", "mcp", "help"])
|
|
67
|
+
const commands = new Set(["check", "scan", "update", "fix", "install", "mcp", "help"])
|
|
65
68
|
if (args[0] && commands.has(args[0])) {
|
|
66
69
|
options.command = args.shift()
|
|
67
70
|
}
|
|
@@ -116,6 +119,12 @@ function parseArgs(argv) {
|
|
|
116
119
|
case "--prune":
|
|
117
120
|
options.prune = true
|
|
118
121
|
break
|
|
122
|
+
case "--config-dir":
|
|
123
|
+
options.configDir = path.resolve(args[++i] ?? ".")
|
|
124
|
+
break
|
|
125
|
+
case "--no-npm":
|
|
126
|
+
options.noNpm = true
|
|
127
|
+
break
|
|
119
128
|
case "-h":
|
|
120
129
|
case "--help":
|
|
121
130
|
options.help = true
|
|
@@ -444,6 +453,28 @@ export async function main(argv) {
|
|
|
444
453
|
return
|
|
445
454
|
}
|
|
446
455
|
|
|
456
|
+
if (options.command === "install") {
|
|
457
|
+
const result = installOpenCode({
|
|
458
|
+
configDir: options.configDir ?? undefined,
|
|
459
|
+
dryRun: options.dryRun,
|
|
460
|
+
installPackage: !options.noNpm,
|
|
461
|
+
})
|
|
462
|
+
if (options.json) {
|
|
463
|
+
console.log(JSON.stringify(result, null, 2))
|
|
464
|
+
} else {
|
|
465
|
+
console.log(bold(t("install.title")))
|
|
466
|
+
console.log(dim(t("install.version", { version: result.version ?? "unknown" })))
|
|
467
|
+
console.log(dim(t("install.configDir", { dir: result.configDir })))
|
|
468
|
+
if (result.dryRun) console.log(dim(t("install.dryRun")))
|
|
469
|
+
for (const action of result.actions) console.log(` · ${action}`)
|
|
470
|
+
for (const error of result.errors) console.log(red(t("install.error", { error })))
|
|
471
|
+
console.log("")
|
|
472
|
+
console.log(result.errors.length ? red(t("install.failed")) : dim(t("install.next")))
|
|
473
|
+
}
|
|
474
|
+
process.exitCode = result.errors.length ? 1 : 0
|
|
475
|
+
return
|
|
476
|
+
}
|
|
477
|
+
|
|
447
478
|
if (options.command === "check") {
|
|
448
479
|
const audit = auditSkills({
|
|
449
480
|
cwd,
|
package/src/i18n.mjs
CHANGED
|
@@ -10,6 +10,7 @@ Usage:
|
|
|
10
10
|
skills-doctor scan [options] list every skill with its source and precedence
|
|
11
11
|
skills-doctor fix [options] archive shadowed definitions (safe structural fixes)
|
|
12
12
|
skills-doctor update [options] compare and download skills from HTTP catalogs
|
|
13
|
+
skills-doctor install [options] wire the plugin into OpenCode (1.x / 2.x)
|
|
13
14
|
skills-doctor mcp run as an MCP server (Claude Code, Codex, any MCP client)
|
|
14
15
|
|
|
15
16
|
Options:
|
|
@@ -27,6 +28,8 @@ Options:
|
|
|
27
28
|
--into <dir> update: output directory (default: .opencode/skills)
|
|
28
29
|
--check-only update: only report available updates
|
|
29
30
|
--prune update: delete skills that disappeared from the catalog
|
|
31
|
+
--config-dir <dir> install: OpenCode config directory (default: ~/.config/opencode)
|
|
32
|
+
--no-npm install: do not run npm install (1.x)
|
|
30
33
|
-h, --help show this help
|
|
31
34
|
-v, --version print version
|
|
32
35
|
|
|
@@ -74,6 +77,13 @@ Exit codes: 0 ok; 1 errors (and warnings with --strict); 2 bad arguments or runt
|
|
|
74
77
|
"update.pruned": " · pruned {count} skills no longer in the catalog: {names}",
|
|
75
78
|
"update.orphaned": " ! {count} skills are no longer in the catalog (use --prune to delete them)",
|
|
76
79
|
"update.lock": " lock: {path} ({name})",
|
|
80
|
+
"install.title": "OpenCode integration",
|
|
81
|
+
"install.version": " OpenCode: {version}",
|
|
82
|
+
"install.configDir": " config dir: {dir}",
|
|
83
|
+
"install.dryRun": " (dry run: nothing is written)",
|
|
84
|
+
"install.error": " ✗ {error}",
|
|
85
|
+
"install.failed": "Integration failed.",
|
|
86
|
+
"install.next": "Restart OpenCode to activate.",
|
|
77
87
|
"update.lockTooNew": " ! lock is newer than this tool; parsed in compatibility mode (unknown fields ignored)",
|
|
78
88
|
"update.detector": " check now sees these skills ({dir} is where opencode reads)",
|
|
79
89
|
"update.noCatalogs": "No HTTP catalog configured. Add https://... to the skills array in opencode.json(c).",
|
|
@@ -156,6 +166,7 @@ Exit codes: 0 ok; 1 errors (and warnings with --strict); 2 bad arguments or runt
|
|
|
156
166
|
skills-doctor scan [选项] 列出所有 skill 及其来源/优先级
|
|
157
167
|
skills-doctor fix [选项] 归档被遮蔽的定义(安全的结构性修复)
|
|
158
168
|
skills-doctor update [选项] 对比并拉取 HTTP catalog 里的 skill
|
|
169
|
+
skills-doctor install [选项] 把插件接入 OpenCode(1.x / 2.x)
|
|
159
170
|
skills-doctor mcp 以 MCP server 方式运行(Claude Code、Codex 等任何 MCP 客户端)
|
|
160
171
|
|
|
161
172
|
选项:
|
|
@@ -173,6 +184,8 @@ Exit codes: 0 ok; 1 errors (and warnings with --strict); 2 bad arguments or runt
|
|
|
173
184
|
--into <dir> update 的落盘目录(默认 .opencode/skills)
|
|
174
185
|
--check-only update 只报告可用更新,不下载
|
|
175
186
|
--prune update: 删除 catalog 中已移除的 skill
|
|
187
|
+
--config-dir <dir> install: OpenCode 配置目录(默认 ~/.config/opencode)
|
|
188
|
+
--no-npm install: 不执行 npm install(1.x)
|
|
176
189
|
-h, --help 帮助
|
|
177
190
|
-v, --version 版本
|
|
178
191
|
|
|
@@ -220,6 +233,13 @@ Exit codes: 0 ok; 1 errors (and warnings with --strict); 2 bad arguments or runt
|
|
|
220
233
|
"update.pruned": " · 清理 catalog 中已移除的 skill {count} 个: {names}",
|
|
221
234
|
"update.orphaned": " ! {count} 个 skill 已不在 catalog 中(加 --prune 可删除本地残留)",
|
|
222
235
|
"update.lock": " lock: {path}({name})",
|
|
236
|
+
"install.title": "OpenCode 集成",
|
|
237
|
+
"install.version": " OpenCode: {version}",
|
|
238
|
+
"install.configDir": " 配置目录: {dir}",
|
|
239
|
+
"install.dryRun": " (dry run:不会写入任何内容)",
|
|
240
|
+
"install.error": " ✗ {error}",
|
|
241
|
+
"install.failed": "集成失败。",
|
|
242
|
+
"install.next": "重启 OpenCode 生效。",
|
|
223
243
|
"update.lockTooNew": " ! lock 版本比本工具新,已按兼容模式解析(未知字段忽略)",
|
|
224
244
|
"update.detector": " check 现在可以直接检测到这些 skill({dir} 是 opencode 会读取的目录)",
|
|
225
245
|
"update.noCatalogs": "没有配置 HTTP catalog。在 opencode.json(c) 的 skills 数组里写 https://... 即可。",
|
package/src/install.mjs
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import fs from "node:fs"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import os from "node:os"
|
|
4
|
+
import { spawnSync, execFileSync } from "node:child_process"
|
|
5
|
+
import { stripJsonc } from "./discover.mjs"
|
|
6
|
+
|
|
7
|
+
export const PACKAGE_NAME = "opencode-skills-doctor"
|
|
8
|
+
const PLUGIN_FILE = "skills-doctor.js"
|
|
9
|
+
const COMMAND_FILE = "skills-doctor.md"
|
|
10
|
+
|
|
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
|
+
|
|
13
|
+
const V1_COMMAND = `---
|
|
14
|
+
description: Audit local skills with skills-doctor
|
|
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
|
+
export function defaultConfigDir() {
|
|
20
|
+
const base = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config")
|
|
21
|
+
return path.join(base, "opencode")
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function readJsonc(file) {
|
|
25
|
+
return JSON.parse(stripJsonc(fs.readFileSync(file, "utf8").replace(/^\uFEFF/, "")))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function writeJson(file, data) {
|
|
29
|
+
fs.writeFileSync(file, JSON.stringify(data, null, 2) + "\n", "utf8")
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function readText(file) {
|
|
33
|
+
try {
|
|
34
|
+
return fs.readFileSync(file, "utf8").replace(/^\uFEFF/, "")
|
|
35
|
+
} catch {
|
|
36
|
+
return null
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function detectVersion() {
|
|
41
|
+
try {
|
|
42
|
+
const out = execFileSync("opencode", ["--version"], { encoding: "utf8", timeout: 15000 })
|
|
43
|
+
const match = /(\d+)\.(\d+)\.(\d+)/.exec(String(out))
|
|
44
|
+
if (match) return match[0]
|
|
45
|
+
} catch {}
|
|
46
|
+
return null
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function configFile(dir) {
|
|
50
|
+
const candidates = ["opencode.jsonc", "opencode.json"]
|
|
51
|
+
.map((name) => path.join(dir, name))
|
|
52
|
+
.filter((file) => fs.existsSync(file))
|
|
53
|
+
|
|
54
|
+
// 优先沿用已经在配置 plugin/plugins 的文件,避免把插件写进另一个配置文件后被覆盖
|
|
55
|
+
for (const file of candidates) {
|
|
56
|
+
try {
|
|
57
|
+
const config = readJsonc(file)
|
|
58
|
+
if (config.plugin !== undefined || config.plugins !== undefined) return file
|
|
59
|
+
} catch {}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return candidates[0] ?? path.join(dir, "opencode.json")
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function backup(file, stamp) {
|
|
66
|
+
if (!fs.existsSync(file)) return null
|
|
67
|
+
const target = `${file}.bak-${stamp}`
|
|
68
|
+
fs.copyFileSync(file, target)
|
|
69
|
+
return target
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function ensureUniqueEntry(list, entry) {
|
|
73
|
+
if (!Array.isArray(list)) return [entry]
|
|
74
|
+
return list.includes(entry) ? list : [...list, entry]
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function installPackage(dir) {
|
|
78
|
+
const npm = process.platform === "win32" ? "npm.cmd" : "npm"
|
|
79
|
+
const result = spawnSync(npm, ["install", "--prefix", dir, PACKAGE_NAME + "@latest", "--no-audit", "--no-fund"], {
|
|
80
|
+
stdio: "inherit",
|
|
81
|
+
env: process.env,
|
|
82
|
+
timeout: 180000,
|
|
83
|
+
})
|
|
84
|
+
return result.status === 0
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 把 skills-doctor 装进 OpenCode:
|
|
89
|
+
* - 2.x:写入 plugins 数组(OpenCode 自己从 npm 拉包)
|
|
90
|
+
* - 1.x:在配置目录安装包、写本地插件入口、写 /skills-doctor 命令、补插件数组
|
|
91
|
+
*/
|
|
92
|
+
export function installOpenCode({ configDir = defaultConfigDir(), version = null, dryRun = false, installPackage: withPackage = true } = {}) {
|
|
93
|
+
const dir = path.resolve(configDir)
|
|
94
|
+
const actions = []
|
|
95
|
+
const errors = []
|
|
96
|
+
const dry = (message) => actions.push(`[dry-run] ${message}`)
|
|
97
|
+
const act = (message) => actions.push(message)
|
|
98
|
+
|
|
99
|
+
const cfgFile = configFile(dir)
|
|
100
|
+
const existingText = readText(cfgFile)
|
|
101
|
+
let config = {}
|
|
102
|
+
if (existingText !== null) {
|
|
103
|
+
try {
|
|
104
|
+
config = readJsonc(cfgFile)
|
|
105
|
+
} catch (err) {
|
|
106
|
+
errors.push(`cannot parse ${cfgFile}: ${err.message}`)
|
|
107
|
+
return { version, major: null, configDir: dir, configFile: cfgFile, actions, errors, dryRun }
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const resolvedVersion = version ?? detectVersion()
|
|
112
|
+
const inferredMajor = config.plugin !== undefined ? 1 : 2
|
|
113
|
+
const major = resolvedVersion ? Number(resolvedVersion.split(".")[0]) : inferredMajor
|
|
114
|
+
|
|
115
|
+
if (dryRun) dry(`config dir: ${dir}`)
|
|
116
|
+
if (!fs.existsSync(dir)) {
|
|
117
|
+
if (!dryRun) fs.mkdirSync(dir, { recursive: true })
|
|
118
|
+
act(`created config dir ${dir}`)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-")
|
|
122
|
+
|
|
123
|
+
if (major >= 2) {
|
|
124
|
+
const next = { ...config, plugins: ensureUniqueEntry(config.plugins, PACKAGE_NAME) }
|
|
125
|
+
const changed = !Array.isArray(config.plugins) || !config.plugins.includes(PACKAGE_NAME)
|
|
126
|
+
if (changed) {
|
|
127
|
+
if (!dryRun) {
|
|
128
|
+
const backupFile = backup(cfgFile, stamp)
|
|
129
|
+
if (backupFile) act(`backup ${backupFile}`)
|
|
130
|
+
writeJson(cfgFile, next)
|
|
131
|
+
act(`added "${PACKAGE_NAME}" to plugins in ${cfgFile}`)
|
|
132
|
+
} else dry(`add "${PACKAGE_NAME}" to plugins in ${cfgFile}`)
|
|
133
|
+
} else {
|
|
134
|
+
act(`plugins already contains "${PACKAGE_NAME}"`)
|
|
135
|
+
}
|
|
136
|
+
act("restart OpenCode; the /skills-doctor command and skills_doctor tool will be available")
|
|
137
|
+
return { version: resolvedVersion, major, configDir: dir, configFile: cfgFile, actions, errors, dryRun }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// 1.x
|
|
141
|
+
const pluginFile = path.join(dir, "plugins", PLUGIN_FILE)
|
|
142
|
+
const commandFile = path.join(dir, "command", COMMAND_FILE)
|
|
143
|
+
const pluginPath = pluginFile
|
|
144
|
+
|
|
145
|
+
const hasPackage = fs.existsSync(path.join(dir, "node_modules", PACKAGE_NAME, "package.json"))
|
|
146
|
+
if (!hasPackage) {
|
|
147
|
+
if (withPackage && !dryRun) {
|
|
148
|
+
act(`installing ${PACKAGE_NAME} into ${dir}`)
|
|
149
|
+
if (!installPackage(dir)) errors.push(`npm install failed for ${PACKAGE_NAME}`)
|
|
150
|
+
else act(`installed ${PACKAGE_NAME}`)
|
|
151
|
+
} else if (dryRun) {
|
|
152
|
+
dry(`install ${PACKAGE_NAME} into ${dir}`)
|
|
153
|
+
} else {
|
|
154
|
+
act(`skipped npm install; run: npm i --prefix "${dir}" ${PACKAGE_NAME}`)
|
|
155
|
+
}
|
|
156
|
+
} else {
|
|
157
|
+
act(`${PACKAGE_NAME} already installed in ${dir}`)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (!fs.existsSync(pluginFile)) {
|
|
161
|
+
if (!dryRun) {
|
|
162
|
+
fs.mkdirSync(path.dirname(pluginFile), { recursive: true })
|
|
163
|
+
fs.writeFileSync(pluginFile, V1_PLUGIN_SOURCE, "utf8")
|
|
164
|
+
act(`wrote plugin entry ${pluginFile}`)
|
|
165
|
+
} else dry(`write plugin entry ${pluginFile}`)
|
|
166
|
+
} else {
|
|
167
|
+
act(`plugin entry already exists ${pluginFile}`)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (!fs.existsSync(commandFile)) {
|
|
171
|
+
if (!dryRun) {
|
|
172
|
+
fs.mkdirSync(path.dirname(commandFile), { recursive: true })
|
|
173
|
+
fs.writeFileSync(commandFile, V1_COMMAND, "utf8")
|
|
174
|
+
act(`wrote command ${commandFile}`)
|
|
175
|
+
} else dry(`write command ${commandFile}`)
|
|
176
|
+
} else {
|
|
177
|
+
act(`command already exists ${commandFile}`)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const nextPlugin = ensureUniqueEntry(config.plugin, pluginPath)
|
|
181
|
+
const pluginChanged = !Array.isArray(config.plugin) || !config.plugin.includes(pluginPath)
|
|
182
|
+
if (pluginChanged) {
|
|
183
|
+
if (!dryRun) {
|
|
184
|
+
const backupFile = backup(cfgFile, stamp)
|
|
185
|
+
if (backupFile) act(`backup ${backupFile}`)
|
|
186
|
+
writeJson(cfgFile, { ...config, plugin: nextPlugin })
|
|
187
|
+
act(`added plugin entry to ${cfgFile}`)
|
|
188
|
+
} else dry(`add plugin entry to ${cfgFile}`)
|
|
189
|
+
} else {
|
|
190
|
+
act("plugin array already configured")
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const pkgFile = path.join(dir, "package.json")
|
|
194
|
+
const pkgText = readText(pkgFile)
|
|
195
|
+
let pkg = {}
|
|
196
|
+
if (pkgText !== null) {
|
|
197
|
+
try {
|
|
198
|
+
pkg = JSON.parse(pkgText)
|
|
199
|
+
} catch {}
|
|
200
|
+
}
|
|
201
|
+
if (pkg.type !== "module") {
|
|
202
|
+
if (!dryRun) {
|
|
203
|
+
pkg.type = "module"
|
|
204
|
+
fs.writeFileSync(pkgFile, JSON.stringify(pkg, null, 2) + "\n", "utf8")
|
|
205
|
+
act(`set "type": "module" in ${pkgFile}`)
|
|
206
|
+
} else dry(`set "type": "module" in ${pkgFile}`)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
act("restart OpenCode; the skills_doctor tool and /skills-doctor command will be available")
|
|
210
|
+
return { version: resolvedVersion, major, configDir: dir, configFile: cfgFile, actions, errors, dryRun }
|
|
211
|
+
}
|
package/src/plugin-v1.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { tool } from "@opencode-ai/plugin"
|
|
2
2
|
import { auditSkills, formatMarkdown } from "./audit.mjs"
|
|
3
|
+
import { budgetWarning } from "./warning.mjs"
|
|
3
4
|
|
|
4
5
|
const z = tool.schema
|
|
5
6
|
|
|
@@ -25,6 +26,10 @@ export const SkillsDoctorPlugin = async ({ directory }) => ({
|
|
|
25
26
|
},
|
|
26
27
|
}),
|
|
27
28
|
},
|
|
29
|
+
"experimental.chat.system.transform": async (input, output) => {
|
|
30
|
+
const warning = budgetWarning({ cwd: directory, sessionID: input?.sessionID })
|
|
31
|
+
if (warning) output.system.push(warning)
|
|
32
|
+
},
|
|
28
33
|
})
|
|
29
34
|
|
|
30
35
|
export const server = SkillsDoctorPlugin
|
package/src/plugin.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Plugin } from "@opencode/plugin"
|
|
2
2
|
import { auditSkills, formatMarkdown } from "./audit.mjs"
|
|
3
|
+
import { budgetWarning } from "./warning.mjs"
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* OpenCode 插件入口:把 skills-doctor 暴露成 skills_doctor 工具和 /skills-doctor 命令。
|
|
@@ -48,5 +49,11 @@ export default Plugin.define({
|
|
|
48
49
|
},
|
|
49
50
|
})
|
|
50
51
|
})
|
|
52
|
+
|
|
53
|
+
// 每个会话最多注入一次预算预警,避免刷屏
|
|
54
|
+
await ctx.session.hook("context", (event) => {
|
|
55
|
+
const warning = budgetWarning({ cwd: ctx.location.directory, sessionID: event.sessionID })
|
|
56
|
+
if (warning) event.system.push({ type: "text", text: warning })
|
|
57
|
+
})
|
|
51
58
|
},
|
|
52
59
|
})
|
package/src/warning.mjs
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { auditSkills } from "./audit.mjs"
|
|
2
|
+
|
|
3
|
+
const cache = new Map()
|
|
4
|
+
const warned = new Set()
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 每个会话最多注入一次「skill 预算超标」提醒。
|
|
8
|
+
* 只在存在 error(默认预算超标 2 倍)时提醒,避免刷屏。
|
|
9
|
+
*/
|
|
10
|
+
export function budgetWarning({ cwd = process.cwd(), sessionID = "global", ttlMs = 60000, includeGlobal = true } = {}) {
|
|
11
|
+
const key = `${cwd}::${sessionID}`
|
|
12
|
+
if (warned.has(key)) return null
|
|
13
|
+
|
|
14
|
+
const now = Date.now()
|
|
15
|
+
let entry = cache.get(cwd)
|
|
16
|
+
if (!entry || now - entry.at > ttlMs) {
|
|
17
|
+
const audit = auditSkills({ cwd, includeGlobal })
|
|
18
|
+
const over = audit.counts.errors > 0
|
|
19
|
+
entry = {
|
|
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,
|
|
24
|
+
}
|
|
25
|
+
cache.set(cwd, entry)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!entry.text) return null
|
|
29
|
+
warned.add(key)
|
|
30
|
+
return entry.text
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 测试用:清空缓存与已提醒集合。 */
|
|
34
|
+
export function resetWarningState() {
|
|
35
|
+
cache.clear()
|
|
36
|
+
warned.clear()
|
|
37
|
+
}
|