opencode-skills-doctor 0.4.1 → 0.5.0
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 +24 -5
- package/docs/README.zh-CN.md +24 -5
- package/package.json +1 -1
- package/src/cli.mjs +111 -1
- package/src/html.mjs +4 -0
- package/src/i18n.mjs +48 -0
- package/src/plan.mjs +142 -0
package/README.md
CHANGED
|
@@ -20,9 +20,10 @@ Local scanner, linter, fixer, updater and **OpenCode plugin** for agent skills
|
|
|
20
20
|
| **What it finds** | shadowed IDs, duplicates, long descriptions, oversized bodies, per-step token budget, broken file references, invalid frontmatter, empty bodies, stale skills |
|
|
21
21
|
| **What it fixes** | archives shadowed definitions (reversible), hides low-frequency skills, syncs catalogs with drift detection |
|
|
22
22
|
| **Reports** | terminal, JSON, or a self-contained HTML report (`--html [file] --open`) |
|
|
23
|
+
| **Plan** | exact token savings: trim descriptions and/or hide skills, projected over N steps |
|
|
23
24
|
| **Cost** | CLI: 0 tokens. Plugin: ~191 tokens/step for the tool definition, plus a one-time warning per session |
|
|
24
25
|
| **Requirements** | Node ≥ 20, zero runtime dependencies |
|
|
25
|
-
| **Quality** |
|
|
26
|
+
| **Quality** | 53 tests, CI on ubuntu/macos/windows × Node 20/22/24 |
|
|
26
27
|
|
|
27
28
|
## Why
|
|
28
29
|
|
|
@@ -59,7 +60,7 @@ skills-doctor check
|
|
|
59
60
|
What a run looks like:
|
|
60
61
|
|
|
61
62
|
```text
|
|
62
|
-
skills-doctor 0.
|
|
63
|
+
skills-doctor 0.4.1
|
|
63
64
|
sources: 5 (2 exist) definitions: 29 unique IDs: 29 advertised: 29 description budget: ~3074 tokens
|
|
64
65
|
|
|
65
66
|
[E] token-budget Advertised skill descriptions cost about 3074 tokens per model step, over 2x the 1200 budget
|
|
@@ -102,6 +103,23 @@ skills-doctor check --html report.html --open
|
|
|
102
103
|
|
|
103
104
|
The plugin and MCP tool accept the same thing as an `html` argument, so the agent can ask you where to save it — for example: *"save the report to D:\reports\skills.html"*.
|
|
104
105
|
|
|
106
|
+
### `plan` (how many tokens can I actually save?)
|
|
107
|
+
|
|
108
|
+
```powershell
|
|
109
|
+
skills-doctor plan
|
|
110
|
+
skills-doctor plan --budget 800 --desc-target 160 --steps 200
|
|
111
|
+
skills-doctor plan --json
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
It answers "how much can I cut, exactly?":
|
|
115
|
+
|
|
116
|
+
- only **advertised** definitions cost tokens — archiving shadowed duplicates saves 0, and the plan says so instead of pretending;
|
|
117
|
+
- **Option A** trims every description longer than `--desc-target` (default 160 chars, cut at sentence boundaries) and computes the exact saving;
|
|
118
|
+
- **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");
|
|
119
|
+
- **Recommended** trims first, then hides, with no double counting, and projects the saving over `--steps` model steps (default 100).
|
|
120
|
+
|
|
121
|
+
`check` prints a one-line pointer when you are over budget, and the HTML report shows the same potential-savings card.
|
|
122
|
+
|
|
105
123
|
### `scan`
|
|
106
124
|
|
|
107
125
|
Lists every skill with its source, precedence, whether the model can see it, and token cost.
|
|
@@ -255,7 +273,7 @@ npx opencode-skills-doctor install
|
|
|
255
273
|
The installer detects your OpenCode version and wires everything up:
|
|
256
274
|
|
|
257
275
|
- **2.x** — adds `"plugins": ["opencode-skills-doctor"]` to `opencode.json(c)`;
|
|
258
|
-
- **1.x** — installs the package into `~/.config/opencode`, writes the local plugin entry,
|
|
276
|
+
- **1.x** — installs the package into `~/.config/opencode`, writes the local plugin entry, generates `/skills-doctor`, `/skills-report` and `/skills-upgrade` markdown commands, sets `"type": "module"`;
|
|
259
277
|
- backs up the config file as `<config>.bak-<timestamp>` before editing.
|
|
260
278
|
|
|
261
279
|
Use `--dry-run` to preview, `--config-dir <dir>` for a custom config location, and `--no-npm` to skip the package install.
|
|
@@ -265,6 +283,7 @@ Once installed you get:
|
|
|
265
283
|
- the `skills_doctor` tool (with an optional `html` argument to write a report to a path you choose);
|
|
266
284
|
- the `/skills-doctor` command (1.x: generated markdown command; 2.x: plugin command);
|
|
267
285
|
- the `/skills-report` command (2.x) that asks where to save the HTML report;
|
|
286
|
+
- the `/skills-upgrade` command (2.x) that checks for a newer skills-doctor;
|
|
268
287
|
- a one-time system-prompt warning when the skill description budget is exceeded.
|
|
269
288
|
|
|
270
289
|
### Uninstall
|
|
@@ -467,13 +486,13 @@ Executing code is free, but the tool definition is part of the model's context.
|
|
|
467
486
|
|
|
468
487
|
## Roadmap
|
|
469
488
|
|
|
470
|
-
- v0.
|
|
489
|
+
- v0.6: CI governance (baseline that only reports new issues, official GitHub Action), content-aware suggestions (which skills are never used), catalog signature verification
|
|
471
490
|
- Later: V1/Claude skill migration report
|
|
472
491
|
|
|
473
492
|
## Development and contributing
|
|
474
493
|
|
|
475
494
|
```powershell
|
|
476
|
-
npm test #
|
|
495
|
+
npm test # 53 tests (node test/doctor.test.mjs)
|
|
477
496
|
npm run bench # lint timing for 100/500/1000 skills
|
|
478
497
|
```
|
|
479
498
|
|
package/docs/README.zh-CN.md
CHANGED
|
@@ -20,9 +20,10 @@ OpenCode skill 的**本地检测器 + 更新器 + 修复器**,并附带 **Open
|
|
|
20
20
|
| **能查** | 同 ID 遮蔽、重复、超长描述、超大正文、每步 token 预算、断链引用、非法 frontmatter、空正文、陈旧 skill |
|
|
21
21
|
| **能修** | 归档遮蔽定义(可逆)、隐藏低频 skill、catalog 同步并检测 drift |
|
|
22
22
|
| **报告** | 终端、JSON,或自包含 HTML(`--html [file] --open`) |
|
|
23
|
+
| **规划** | 精确 token 节省:精简描述 / 隐藏 skill,按 N 步折算 |
|
|
23
24
|
| **成本** | CLI 0 token;插件工具定义约 191/步,另有每会话一次预警 |
|
|
24
25
|
| **要求** | Node ≥ 20,零运行时依赖 |
|
|
25
|
-
| **质量** |
|
|
26
|
+
| **质量** | 53 个测试,ubuntu/macos/windows × Node 20/22/24 CI |
|
|
26
27
|
|
|
27
28
|
## 为什么需要它
|
|
28
29
|
|
|
@@ -59,7 +60,7 @@ skills-doctor check
|
|
|
59
60
|
跑出来大概是这样:
|
|
60
61
|
|
|
61
62
|
```text
|
|
62
|
-
skills-doctor 0.
|
|
63
|
+
skills-doctor 0.4.1
|
|
63
64
|
来源: 5 个(存在 2 个) 定义: 29 唯一 ID: 29 会展示: 29 描述预算: ~3074 tokens
|
|
64
65
|
|
|
65
66
|
[E] token-budget 模型每步要看到的 skill 描述约 3074 tokens,超过预算 1200 的 2 倍
|
|
@@ -102,6 +103,23 @@ skills-doctor check --html report.html --open
|
|
|
102
103
|
|
|
103
104
|
插件和 MCP 工具同样接受 `html` 参数,所以可以让 agent 先问你要存哪里——例如:*"把报告存到 D:\reports\skills.html"*。
|
|
104
105
|
|
|
106
|
+
### `plan`(到底能省多少 token?)
|
|
107
|
+
|
|
108
|
+
```powershell
|
|
109
|
+
skills-doctor plan
|
|
110
|
+
skills-doctor plan --budget 800 --desc-target 160 --steps 200
|
|
111
|
+
skills-doctor plan --json
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
回答「能省多少、怎么省」:
|
|
115
|
+
|
|
116
|
+
- 只有**会展示给模型**的定义才花 token —— 归档遮蔽重复定义省 0,方案里会明说,不糊弄;
|
|
117
|
+
- **方案 A**:把所有超过 `--desc-target`(默认 160 字符,按句子边界截断)的 description 精简,算出精确节省;
|
|
118
|
+
- **方案 B**:用最少的隐藏数量把总量压进预算(仍可 `@id` 加载;按单项节省从大到小取,对「最少隐藏数」是最优的);
|
|
119
|
+
- **推荐方案**:先精简再隐藏,不重复计算,并按 `--steps`(默认 100)折算总节省。
|
|
120
|
+
|
|
121
|
+
超预算时 `check` 会打印一行提示,HTML 报告里有同样的「potential savings」卡片。
|
|
122
|
+
|
|
105
123
|
### `scan`
|
|
106
124
|
|
|
107
125
|
列出所有 skill 及其来源、优先级、是否会被模型看到、token 占用。
|
|
@@ -255,7 +273,7 @@ npx opencode-skills-doctor install
|
|
|
255
273
|
安装器会自动识别 OpenCode 版本并接好:
|
|
256
274
|
|
|
257
275
|
- **2.x** —— 在 `opencode.json(c)` 里加 `"plugins": ["opencode-skills-doctor"]`;
|
|
258
|
-
- **1.x** —— 把包装进 `~/.config/opencode`、写本地插件入口、生成 `/skills-doctor` 命令、补 `"type": "module"`;
|
|
276
|
+
- **1.x** —— 把包装进 `~/.config/opencode`、写本地插件入口、生成 `/skills-doctor`、`/skills-report`、`/skills-upgrade` 三个 markdown 命令、补 `"type": "module"`;
|
|
259
277
|
- 改配置前会备份为 `<config>.bak-<timestamp>`。
|
|
260
278
|
|
|
261
279
|
支持 `--dry-run` 预览、`--config-dir <dir>` 指定配置目录、`--no-npm` 跳过装包。
|
|
@@ -265,6 +283,7 @@ npx opencode-skills-doctor install
|
|
|
265
283
|
- `skills_doctor` 工具(可选 `html` 参数,把报告写到指定路径);
|
|
266
284
|
- `/skills-doctor` 命令(1.x 是生成的 markdown 命令,2.x 是插件命令);
|
|
267
285
|
- `/skills-report` 命令(2.x)—— 会先问你要把 HTML 报告存到哪里;
|
|
286
|
+
- `/skills-upgrade` 命令(2.x)—— 检查 skills-doctor 自身是否有新版本;
|
|
268
287
|
- 当 skill 描述预算超标时,系统提示里会注入一次提醒(每个会话最多一次)。
|
|
269
288
|
|
|
270
289
|
### 卸载
|
|
@@ -467,13 +486,13 @@ token 是启发式估算,1200 是建议预算。可以用 `--budget` 或 `.ski
|
|
|
467
486
|
|
|
468
487
|
## Roadmap
|
|
469
488
|
|
|
470
|
-
- v0.
|
|
489
|
+
- v0.6:CI 治理(baseline 只报新增问题、官方 GitHub Action)、内容层建议(哪些 skill 从未被使用)、catalog 签名校验
|
|
471
490
|
- 后续:V1/Claude skill 兼容迁移报告
|
|
472
491
|
|
|
473
492
|
## 开发与贡献
|
|
474
493
|
|
|
475
494
|
```powershell
|
|
476
|
-
npm test #
|
|
495
|
+
npm test # 53 个测试(node test/doctor.test.mjs)
|
|
477
496
|
npm run bench # 100/500/1000 个 skill 的 lint 耗时
|
|
478
497
|
```
|
|
479
498
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-skills-doctor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Audit, fix and update agent skills: token cost, broken references, shadowed duplicates, HTML reports, catalog sync. CLI, OpenCode plugin, MCP server; works with OpenCode, Claude Code, Codex, Grok CLI and Cursor skill layouts.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
package/src/cli.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import { loadConfig } from "./config.mjs"
|
|
|
10
10
|
import { startMcpServer } from "./mcp.mjs"
|
|
11
11
|
import { installOpenCode, uninstallOpenCode } from "./install.mjs"
|
|
12
12
|
import { writeHtmlReport, openReport } from "./html.mjs"
|
|
13
|
+
import { planBudget } from "./plan.mjs"
|
|
13
14
|
import {
|
|
14
15
|
checkForUpdate,
|
|
15
16
|
resolveMode,
|
|
@@ -76,12 +77,14 @@ function parseArgs(argv) {
|
|
|
76
77
|
open: false,
|
|
77
78
|
apply: false,
|
|
78
79
|
updateCheck: true,
|
|
80
|
+
descTarget: 160,
|
|
81
|
+
steps: 100,
|
|
79
82
|
help: false,
|
|
80
83
|
version: false,
|
|
81
84
|
}
|
|
82
85
|
|
|
83
86
|
const args = [...argv]
|
|
84
|
-
const commands = new Set(["check", "scan", "update", "fix", "install", "uninstall", "upgrade", "mcp", "help"])
|
|
87
|
+
const commands = new Set(["check", "scan", "plan", "update", "fix", "install", "uninstall", "upgrade", "mcp", "help"])
|
|
85
88
|
if (args[0] && commands.has(args[0])) {
|
|
86
89
|
options.command = args.shift()
|
|
87
90
|
}
|
|
@@ -101,6 +104,12 @@ function parseArgs(argv) {
|
|
|
101
104
|
case "--stale-days":
|
|
102
105
|
options.staleDays = Number(args[++i])
|
|
103
106
|
break
|
|
107
|
+
case "--desc-target":
|
|
108
|
+
options.descTarget = Number(args[++i])
|
|
109
|
+
break
|
|
110
|
+
case "--steps":
|
|
111
|
+
options.steps = Number(args[++i])
|
|
112
|
+
break
|
|
104
113
|
case "--strict":
|
|
105
114
|
options.strict = true
|
|
106
115
|
break
|
|
@@ -279,6 +288,92 @@ function printCheck(audit, result, options, t) {
|
|
|
279
288
|
console.log(bold(t("check.hints")))
|
|
280
289
|
for (const rule of usedRules) console.log(dim(` - ${t(HINT_KEYS[rule])}`))
|
|
281
290
|
}
|
|
291
|
+
|
|
292
|
+
if (audit.summary.advertisedTokens > audit.budget) {
|
|
293
|
+
const plan = planBudget(audit)
|
|
294
|
+
if (plan.recommended.savings > 0) {
|
|
295
|
+
console.log("")
|
|
296
|
+
console.log(yellow(t("check.planHint", { tokens: plan.recommended.savings })))
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function printPlan(plan, t) {
|
|
302
|
+
console.log(bold(t("plan.title")))
|
|
303
|
+
console.log(dim(t("plan.current", { tokens: plan.current, count: plan.advertised })))
|
|
304
|
+
console.log(dim(t("plan.budget", { budget: plan.budget, gap: plan.gap })))
|
|
305
|
+
console.log(dim(t("plan.steps", { steps: plan.steps })))
|
|
306
|
+
console.log("")
|
|
307
|
+
|
|
308
|
+
const sessionLine = (savings) => {
|
|
309
|
+
if (plan.steps > 0 && savings > 0) console.log(dim(t("plan.session", { tokens: savings * plan.steps, steps: plan.steps })))
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (plan.current <= plan.budget) {
|
|
313
|
+
console.log(green(t("plan.under")))
|
|
314
|
+
if (plan.trim.count) {
|
|
315
|
+
console.log("")
|
|
316
|
+
console.log(bold(t("plan.trimTitle", { chars: plan.descTarget })))
|
|
317
|
+
console.log(
|
|
318
|
+
t("plan.trimLine", {
|
|
319
|
+
count: plan.trim.count,
|
|
320
|
+
savings: plan.trim.savings,
|
|
321
|
+
final: Math.max(0, plan.current - plan.trim.savings),
|
|
322
|
+
}),
|
|
323
|
+
)
|
|
324
|
+
sessionLine(plan.trim.savings)
|
|
325
|
+
}
|
|
326
|
+
return
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
console.log(bold(t("plan.trimTitle", { chars: plan.descTarget })))
|
|
330
|
+
if (plan.trim.count) {
|
|
331
|
+
console.log(
|
|
332
|
+
t("plan.trimLine", {
|
|
333
|
+
count: plan.trim.count,
|
|
334
|
+
savings: plan.trim.savings,
|
|
335
|
+
final: Math.max(0, plan.current - plan.trim.savings),
|
|
336
|
+
}),
|
|
337
|
+
)
|
|
338
|
+
for (const item of plan.trim.items.slice(0, 10)) {
|
|
339
|
+
console.log(dim(t("plan.trimItem", { id: item.id, saving: item.saving })))
|
|
340
|
+
}
|
|
341
|
+
if (plan.trim.items.length > 10) console.log(dim(t("plan.more", { count: plan.trim.items.length - 10 })))
|
|
342
|
+
} else {
|
|
343
|
+
console.log(dim(t("plan.trimNone")))
|
|
344
|
+
}
|
|
345
|
+
console.log("")
|
|
346
|
+
|
|
347
|
+
console.log(bold(t("plan.hideTitle")))
|
|
348
|
+
if (plan.hideOnly.count) {
|
|
349
|
+
console.log(
|
|
350
|
+
t("plan.hideLine", {
|
|
351
|
+
count: plan.hideOnly.count,
|
|
352
|
+
savings: plan.hideOnly.savings,
|
|
353
|
+
final: plan.hideOnly.final,
|
|
354
|
+
}),
|
|
355
|
+
)
|
|
356
|
+
} else {
|
|
357
|
+
console.log(dim(t("plan.hideNone")))
|
|
358
|
+
}
|
|
359
|
+
console.log("")
|
|
360
|
+
|
|
361
|
+
console.log(bold(t("plan.recTitle")))
|
|
362
|
+
console.log(
|
|
363
|
+
t("plan.recLine", {
|
|
364
|
+
trims: plan.trim.count,
|
|
365
|
+
hides: plan.recommended.hides.length,
|
|
366
|
+
final: plan.recommended.final,
|
|
367
|
+
savings: plan.recommended.savings,
|
|
368
|
+
}),
|
|
369
|
+
)
|
|
370
|
+
for (const item of plan.recommended.hides) {
|
|
371
|
+
console.log(dim(t("plan.hideItem", { id: item.id, tokens: item.tokens })))
|
|
372
|
+
}
|
|
373
|
+
if (plan.recommended.hidesEverything) console.log(yellow(t("plan.hidesAll")))
|
|
374
|
+
sessionLine(plan.recommended.savings)
|
|
375
|
+
console.log("")
|
|
376
|
+
console.log(dim(t("plan.next")))
|
|
282
377
|
}
|
|
283
378
|
|
|
284
379
|
function printScan(discovered, options, managed, t) {
|
|
@@ -592,6 +687,21 @@ export async function main(argv) {
|
|
|
592
687
|
return
|
|
593
688
|
}
|
|
594
689
|
|
|
690
|
+
if (options.command === "plan") {
|
|
691
|
+
const audit = auditSkills({
|
|
692
|
+
cwd,
|
|
693
|
+
includeGlobal: options.global,
|
|
694
|
+
lang,
|
|
695
|
+
configPath: options.config,
|
|
696
|
+
budget: options.budget,
|
|
697
|
+
staleDays: options.staleDays,
|
|
698
|
+
})
|
|
699
|
+
const plan = planBudget(audit, { descTarget: options.descTarget, steps: options.steps })
|
|
700
|
+
if (options.json) console.log(JSON.stringify(plan, null, 2))
|
|
701
|
+
else printPlan(plan, t)
|
|
702
|
+
return
|
|
703
|
+
}
|
|
704
|
+
|
|
595
705
|
if (options.command === "check") {
|
|
596
706
|
const audit = auditSkills({
|
|
597
707
|
cwd,
|
package/src/html.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs"
|
|
2
2
|
import path from "node:path"
|
|
3
3
|
import { spawn } from "node:child_process"
|
|
4
|
+
import { planBudget } from "./plan.mjs"
|
|
4
5
|
|
|
5
6
|
function esc(value) {
|
|
6
7
|
return String(value ?? "").replace(/[&<>"']/g, (char) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[char])
|
|
@@ -13,6 +14,7 @@ export function renderHtml(audit, { title = "skills-doctor report", now = new Da
|
|
|
13
14
|
const summary = audit.summary
|
|
14
15
|
const counts = audit.counts
|
|
15
16
|
const budget = audit.budget
|
|
17
|
+
const plan = planBudget(audit)
|
|
16
18
|
const ratio = budget > 0 ? Math.min(1.5, summary.advertisedTokens / (budget * 2)) : 0
|
|
17
19
|
const barWidth = Math.min(100, Math.round(ratio * 100))
|
|
18
20
|
const barClass = summary.advertisedTokens > budget * 2 ? "over" : summary.advertisedTokens > budget ? "warn" : "ok"
|
|
@@ -89,6 +91,7 @@ export function renderHtml(audit, { title = "skills-doctor report", now = new Da
|
|
|
89
91
|
<div class="card"><div class="k">unique IDs</div><div class="v">${summary.unique}</div></div>
|
|
90
92
|
<div class="card"><div class="k">advertised</div><div class="v">${summary.advertised}</div></div>
|
|
91
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>
|
|
92
95
|
<div class="card error"><div class="k">errors</div><div class="v">${counts.errors}</div></div>
|
|
93
96
|
<div class="card warn"><div class="k">warnings</div><div class="v">${counts.warnings}</div></div>
|
|
94
97
|
<div class="card info"><div class="k">info</div><div class="v">${counts.infos}</div></div>
|
|
@@ -97,6 +100,7 @@ export function renderHtml(audit, { title = "skills-doctor report", now = new Da
|
|
|
97
100
|
<h2>Context budget</h2>
|
|
98
101
|
<div class="bar"><span class="${barClass}"></span></div>
|
|
99
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>
|
|
100
104
|
|
|
101
105
|
<h2>Findings (${audit.findings.length})</h2>
|
|
102
106
|
<div class="toolbar">
|
package/src/i18n.mjs
CHANGED
|
@@ -7,6 +7,7 @@ const dict = {
|
|
|
7
7
|
|
|
8
8
|
Usage:
|
|
9
9
|
skills-doctor check [options] check sources, conflicts and context budget (default)
|
|
10
|
+
skills-doctor plan [options] estimate how many tokens you can save, and how
|
|
10
11
|
skills-doctor scan [options] list every skill with its source and precedence
|
|
11
12
|
skills-doctor fix [options] archive shadowed definitions (safe structural fixes)
|
|
12
13
|
skills-doctor update [options] compare and download skills from HTTP catalogs
|
|
@@ -19,6 +20,8 @@ Options:
|
|
|
19
20
|
--root <dir> working directory (default: current directory)
|
|
20
21
|
--no-global skip ~/.claude, ~/.agents and ~/.config/opencode
|
|
21
22
|
--budget <n> tokens allowed for advertised descriptions (default 1200)
|
|
23
|
+
--desc-target <n> plan: target description length in chars (default 160)
|
|
24
|
+
--steps <n> plan: project savings over n model steps (default 100)
|
|
22
25
|
--stale-days <n> report skills not updated in n days (default: off)
|
|
23
26
|
--strict exit non-zero on warnings too
|
|
24
27
|
--json print JSON
|
|
@@ -56,6 +59,27 @@ Exit codes: 0 ok; 1 errors (and warnings with --strict); 2 bad arguments or runt
|
|
|
56
59
|
"check.summary": "{errors} error, {warnings} warn, {infos} info",
|
|
57
60
|
"check.strict": " (--strict: warnings also fail)",
|
|
58
61
|
"check.hints": "Hints:",
|
|
62
|
+
"check.planHint": " ! potential: about {tokens} tokens/step can be saved; run `skills-doctor plan` for the breakdown",
|
|
63
|
+
|
|
64
|
+
"plan.title": "skills-doctor plan",
|
|
65
|
+
"plan.current": " current: ~{tokens} tokens/step ({count} advertised skills)",
|
|
66
|
+
"plan.budget": " budget: {budget} gap: {gap}",
|
|
67
|
+
"plan.steps": " projection: {steps} model steps",
|
|
68
|
+
"plan.under": "Under budget — no changes needed.",
|
|
69
|
+
"plan.trimTitle": "Option A — trim descriptions to {chars} chars (stays discoverable)",
|
|
70
|
+
"plan.trimLine": " {count} skills, saves ~{savings} tokens/step → ~{final} tokens",
|
|
71
|
+
"plan.trimNone": " nothing to trim.",
|
|
72
|
+
"plan.trimItem": " - {id} (−{saving})",
|
|
73
|
+
"plan.hideTitle": "Option B — hide skills (autoinvoke: false, still loadable with @id)",
|
|
74
|
+
"plan.hideLine": " hide {count} skills, saves ~{savings} tokens/step → ~{final} tokens",
|
|
75
|
+
"plan.hideNone": " no hide needed.",
|
|
76
|
+
"plan.recTitle": "Recommended",
|
|
77
|
+
"plan.recLine": " trim {trims} + hide {hides} → ~{final} tokens/step (saves ~{savings})",
|
|
78
|
+
"plan.hideItem": " - hide {id} (−{tokens})",
|
|
79
|
+
"plan.hidesAll": " ! this would hide every skill; consider raising the budget instead.",
|
|
80
|
+
"plan.session": " over {steps} steps that saves ~{tokens} tokens",
|
|
81
|
+
"plan.more": " …and {count} more",
|
|
82
|
+
"plan.next": "Next: hide with `skills-doctor fix --hide <ids>`, or trim descriptions and keep discoverability.",
|
|
59
83
|
|
|
60
84
|
"scan.title": "skills-doctor {version} — scan",
|
|
61
85
|
"scan.workdir": " working dir: {dir}",
|
|
@@ -192,6 +216,7 @@ Exit codes: 0 ok; 1 errors (and warnings with --strict); 2 bad arguments or runt
|
|
|
192
216
|
|
|
193
217
|
用法:
|
|
194
218
|
skills-doctor check [选项] 检查来源、冲突与上下文预算(默认命令)
|
|
219
|
+
skills-doctor plan [选项] 估算能省多少 token、怎么省
|
|
195
220
|
skills-doctor scan [选项] 列出所有 skill 及其来源/优先级
|
|
196
221
|
skills-doctor fix [选项] 归档被遮蔽的定义(安全的结构性修复)
|
|
197
222
|
skills-doctor update [选项] 对比并拉取 HTTP catalog 里的 skill
|
|
@@ -204,6 +229,8 @@ Exit codes: 0 ok; 1 errors (and warnings with --strict); 2 bad arguments or runt
|
|
|
204
229
|
--root <dir> 以该目录为工作目录(默认当前目录)
|
|
205
230
|
--no-global 不扫描 ~/.claude、~/.agents、~/.config/opencode
|
|
206
231
|
--budget <n> 模型每步可看的 description token 预算(默认 1200)
|
|
232
|
+
--desc-target <n> plan: description 目标字符数(默认 160)
|
|
233
|
+
--steps <n> plan: 按 n 个模型步骤折算节省量(默认 100)
|
|
207
234
|
--stale-days <n> 超过 n 天未更新的 skill 给出提示(默认关闭)
|
|
208
235
|
--strict 有 warning 也返回非 0
|
|
209
236
|
--json 输出 JSON
|
|
@@ -241,6 +268,27 @@ Exit codes: 0 ok; 1 errors (and warnings with --strict); 2 bad arguments or runt
|
|
|
241
268
|
"check.summary": "{errors} error, {warnings} warn, {infos} info",
|
|
242
269
|
"check.strict": "(--strict:warning 也会非 0 退出)",
|
|
243
270
|
"check.hints": "建议:",
|
|
271
|
+
"check.planHint": " ! 预计每步还能省约 {tokens} tokens;运行 `skills-doctor plan` 看具体方案",
|
|
272
|
+
|
|
273
|
+
"plan.title": "skills-doctor 省 token 方案",
|
|
274
|
+
"plan.current": " 当前: 约 {tokens} tokens/步({count} 个会展示的 skill)",
|
|
275
|
+
"plan.budget": " 预算: {budget} 差距: {gap}",
|
|
276
|
+
"plan.steps": " 折算: {steps} 个模型步骤",
|
|
277
|
+
"plan.under": "已在预算内,无需改动。",
|
|
278
|
+
"plan.trimTitle": "方案 A —— 精简 description 到 {chars} 字符(仍可被发现)",
|
|
279
|
+
"plan.trimLine": " {count} 个 skill,每步省约 {savings} tokens → 约 {final} tokens",
|
|
280
|
+
"plan.trimNone": " 没有需要精简的。",
|
|
281
|
+
"plan.trimItem": " - {id}(−{saving})",
|
|
282
|
+
"plan.hideTitle": "方案 B —— 隐藏 skill(autoinvoke: false,仍可 @id 加载)",
|
|
283
|
+
"plan.hideLine": " 隐藏 {count} 个,每步省约 {savings} tokens → 约 {final} tokens",
|
|
284
|
+
"plan.hideNone": " 无需隐藏。",
|
|
285
|
+
"plan.recTitle": "推荐方案",
|
|
286
|
+
"plan.recLine": " 精简 {trims} 个 + 隐藏 {hides} 个 → 约 {final} tokens/步(省约 {savings})",
|
|
287
|
+
"plan.hideItem": " - 隐藏 {id}(−{tokens})",
|
|
288
|
+
"plan.hidesAll": " ! 这样会把所有 skill 都隐藏;建议改为提高预算。",
|
|
289
|
+
"plan.session": " 按 {steps} 步计算,共省约 {tokens} tokens",
|
|
290
|
+
"plan.more": " …还有 {count} 个",
|
|
291
|
+
"plan.next": "下一步:用 `skills-doctor fix --hide <ids>` 隐藏,或精简 description 保留可发现性。",
|
|
244
292
|
|
|
245
293
|
"scan.title": "skills-doctor {version} — scan",
|
|
246
294
|
"scan.workdir": " 工作目录: {dir}",
|
package/src/plan.mjs
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { estimateTokens } from "./parse.mjs"
|
|
2
|
+
import { resolveWinners } from "./lint.mjs"
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_DESC_TARGET = 160
|
|
5
|
+
export const DEFAULT_STEPS = 100
|
|
6
|
+
|
|
7
|
+
function costOf(skill, description = skill.description) {
|
|
8
|
+
return estimateTokens(`${skill.id} ${skill.displayName} ${description}`)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 把 description 截到目标长度,优先在句子边界断开(中英文标点都算)。
|
|
13
|
+
* 纯函数:相同输入永远得到相同输出,便于在报告里预览「如果这么改会省多少」。
|
|
14
|
+
*/
|
|
15
|
+
export function trimDescription(text, limit = DEFAULT_DESC_TARGET) {
|
|
16
|
+
const value = String(text ?? "")
|
|
17
|
+
if (value.length <= limit) return value
|
|
18
|
+
const slice = value.slice(0, limit)
|
|
19
|
+
const cut = Math.max(
|
|
20
|
+
slice.lastIndexOf("。"),
|
|
21
|
+
slice.lastIndexOf("."),
|
|
22
|
+
slice.lastIndexOf("!"),
|
|
23
|
+
slice.lastIndexOf("?"),
|
|
24
|
+
slice.lastIndexOf(";"),
|
|
25
|
+
slice.lastIndexOf(";"),
|
|
26
|
+
slice.lastIndexOf("\n"),
|
|
27
|
+
)
|
|
28
|
+
const trimmed = cut >= limit * 0.5 ? value.slice(0, cut + 1).trim() : slice.trim()
|
|
29
|
+
return trimmed || slice.trim()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** 在候选中选「数量最少」的一组,使节省量 ≥ gap;按单项节省降序取前缀即为最优。 */
|
|
33
|
+
function pickMinimal(items, gap) {
|
|
34
|
+
if (gap <= 0) return { count: 0, savings: 0, items: [], enough: true }
|
|
35
|
+
const sorted = [...items].sort((a, b) => b.tokens - a.tokens || a.id.localeCompare(b.id))
|
|
36
|
+
const picked = []
|
|
37
|
+
let savings = 0
|
|
38
|
+
for (const item of sorted) {
|
|
39
|
+
if (savings >= gap) break
|
|
40
|
+
picked.push(item)
|
|
41
|
+
savings += item.tokens
|
|
42
|
+
}
|
|
43
|
+
return { count: picked.length, savings, items: picked, enough: savings >= gap }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 预算规划:当前每步消耗多少、精简到多少、隐藏多少、最终能省多少。
|
|
48
|
+
*
|
|
49
|
+
* 规则:
|
|
50
|
+
* - 只有会被「展示给模型」的定义才有成本;被遮蔽的重复定义省不出 token(诚实很重要)。
|
|
51
|
+
* - 精简 description 不改变可发现性;隐藏(autoinvoke: false)省掉整条成本,但仍可用 @id 加载。
|
|
52
|
+
* - 推荐方案先精简所有超长描述,再按「单项节省最大」贪心隐藏,直到进入预算。
|
|
53
|
+
* 被隐藏的技能若也被精简过,其节省按原始成本计,不会重复计算。
|
|
54
|
+
*/
|
|
55
|
+
export function planBudget(
|
|
56
|
+
audit,
|
|
57
|
+
{ budget = audit.budget, descTarget = DEFAULT_DESC_TARGET, steps = DEFAULT_STEPS } = {},
|
|
58
|
+
) {
|
|
59
|
+
const target = Number.isFinite(budget) && budget > 0 ? budget : audit.budget
|
|
60
|
+
const winners = [...resolveWinners(audit.skills).values()]
|
|
61
|
+
const advertised = winners.filter((skill) => skill.advertised)
|
|
62
|
+
|
|
63
|
+
const current = advertised.reduce((sum, skill) => sum + costOf(skill), 0)
|
|
64
|
+
const gap = Math.max(0, current - target)
|
|
65
|
+
|
|
66
|
+
// 1) 精简:所有 description 超过目标的,按句子边界截断
|
|
67
|
+
const trimMap = new Map()
|
|
68
|
+
const trims = []
|
|
69
|
+
for (const skill of advertised) {
|
|
70
|
+
if (skill.description.length <= descTarget) continue
|
|
71
|
+
const preview = trimDescription(skill.description, descTarget)
|
|
72
|
+
const before = costOf(skill)
|
|
73
|
+
const after = costOf(skill, preview)
|
|
74
|
+
const saving = Math.max(0, before - after)
|
|
75
|
+
if (!saving) continue
|
|
76
|
+
trimMap.set(skill.id, { preview, cost: after })
|
|
77
|
+
trims.push({
|
|
78
|
+
id: skill.id,
|
|
79
|
+
file: skill.file,
|
|
80
|
+
before,
|
|
81
|
+
after,
|
|
82
|
+
saving,
|
|
83
|
+
preview,
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
trims.sort((a, b) => b.saving - a.saving || a.id.localeCompare(b.id))
|
|
87
|
+
const trimSavings = trims.reduce((sum, item) => sum + item.saving, 0)
|
|
88
|
+
|
|
89
|
+
// 2) 只隐藏(不精简)的方案
|
|
90
|
+
const hideCandidates = advertised.map((skill) => ({
|
|
91
|
+
id: skill.id,
|
|
92
|
+
file: skill.file,
|
|
93
|
+
tokens: costOf(skill),
|
|
94
|
+
}))
|
|
95
|
+
const hideOnly = pickMinimal(hideCandidates, gap)
|
|
96
|
+
|
|
97
|
+
// 3) 推荐方案:先精简,再在「精简后的成本」上隐藏
|
|
98
|
+
const afterTrim = advertised.map((skill) => ({
|
|
99
|
+
id: skill.id,
|
|
100
|
+
file: skill.file,
|
|
101
|
+
tokens: trimMap.get(skill.id)?.cost ?? costOf(skill),
|
|
102
|
+
originalTokens: costOf(skill),
|
|
103
|
+
}))
|
|
104
|
+
const postTrimTotal = afterTrim.reduce((sum, item) => sum + item.tokens, 0)
|
|
105
|
+
const recommendedHides = pickMinimal(afterTrim, Math.max(0, postTrimTotal - target))
|
|
106
|
+
const hideSavings = recommendedHides.savings
|
|
107
|
+
const savings = trimSavings + hideSavings
|
|
108
|
+
const final = Math.max(0, current - savings)
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
cwd: audit.cwd,
|
|
112
|
+
budget: target,
|
|
113
|
+
descTarget,
|
|
114
|
+
steps,
|
|
115
|
+
current,
|
|
116
|
+
gap,
|
|
117
|
+
advertised: advertised.length,
|
|
118
|
+
trim: {
|
|
119
|
+
count: trims.length,
|
|
120
|
+
savings: trimSavings,
|
|
121
|
+
items: trims,
|
|
122
|
+
},
|
|
123
|
+
hideOnly: {
|
|
124
|
+
count: hideOnly.count,
|
|
125
|
+
savings: hideOnly.savings,
|
|
126
|
+
final: Math.max(0, current - hideOnly.savings),
|
|
127
|
+
enough: hideOnly.enough,
|
|
128
|
+
items: hideOnly.items,
|
|
129
|
+
},
|
|
130
|
+
recommended: {
|
|
131
|
+
trims: trims.map((item) => item.id),
|
|
132
|
+
hides: recommendedHides.items,
|
|
133
|
+
trimSavings,
|
|
134
|
+
hideSavings,
|
|
135
|
+
savings,
|
|
136
|
+
final,
|
|
137
|
+
reachable: final <= target,
|
|
138
|
+
stepsSavings: savings * steps,
|
|
139
|
+
hidesEverything: advertised.length > 0 && recommendedHides.count === advertised.length,
|
|
140
|
+
},
|
|
141
|
+
}
|
|
142
|
+
}
|