opencode-skills-doctor 0.8.1 → 0.9.3
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 +90 -11
- package/SECURITY.md +9 -0
- package/SECURITY.zh-CN.md +9 -0
- package/docs/README.zh-CN.md +42 -10
- package/docs/REFERENCE.md +5 -3
- package/package.json +2 -2
- package/src/audit.mjs +3 -1
- package/src/catalog.mjs +37 -1
- package/src/desktop.mjs +7 -4
- package/src/discover.mjs +62 -7
- package/src/i18n.mjs +6 -2
- package/src/install.mjs +7 -2
- package/src/lint.mjs +8 -7
- package/src/mcp.mjs +1 -1
- package/src/parse.mjs +6 -3
- package/src/plugin-v1.mjs +1 -1
- package/src/plugin.mjs +1 -1
package/README.md
CHANGED
|
@@ -2,25 +2,41 @@
|
|
|
2
2
|
<img src="https://raw.githubusercontent.com/wangyuchen5110/opencodeplugginprogram/main/docs/assets/banner.svg" alt="skills-doctor — audit the hidden token cost of agent skills" width="100%">
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
|
-
<
|
|
6
|
-
<a href="https://www.npmjs.com/package/opencode-skills-doctor"><img src="https://img.shields.io/npm/v/opencode-skills-doctor?style=flat-square&color=3fb950" alt="npm version"></a>
|
|
7
|
-
<a href="https://www.npmjs.com/package/opencode-skills-doctor"><img src="https://img.shields.io/npm/dw/opencode-skills-doctor?style=flat-square" alt="npm downloads"></a>
|
|
8
|
-
<a href="https://github.com/wangyuchen5110/opencodeplugginprogram/stargazers"><img src="https://img.shields.io/github/stars/wangyuchen5110/opencodeplugginprogram?style=flat-square&color=58a6ff" alt="GitHub stars"></a>
|
|
9
|
-
<img src="https://img.shields.io/badge/node-%E2%89%A520-3fb950?style=flat-square" alt="Node ≥ 20">
|
|
10
|
-
<a href="LICENSE"><img src="https://img.shields.io/npm/l/opencode-skills-doctor?style=flat-square" alt="MIT license"></a>
|
|
11
|
-
<a href="https://github.com/wangyuchen5110/opencodeplugginprogram/actions/workflows/test.yml"><img src="https://github.com/wangyuchen5110/opencodeplugginprogram/actions/workflows/test.yml/badge.svg?style=flat-square" alt="tests"></a>
|
|
12
|
-
</p>
|
|
5
|
+
<h1 align="center">skills-doctor</h1>
|
|
13
6
|
|
|
14
7
|
<p align="center">
|
|
15
8
|
<b>Audit, fix and update the skills your coding agent loads.</b><br>
|
|
16
9
|
Measure the per-step token tax, find shadowed duplicates and broken references — then fix them safely.
|
|
17
10
|
</p>
|
|
18
11
|
|
|
12
|
+
<p align="center">
|
|
13
|
+
<a href="https://www.npmjs.com/package/opencode-skills-doctor"><img src="https://img.shields.io/npm/v/opencode-skills-doctor?style=for-the-badge&color=3fb950" alt="npm version"></a>
|
|
14
|
+
<a href="https://www.npmjs.com/package/opencode-skills-doctor"><img src="https://img.shields.io/npm/dw/opencode-skills-doctor?style=for-the-badge&color=2ea043" alt="npm downloads"></a>
|
|
15
|
+
<a href="https://github.com/wangyuchen5110/opencodeplugginprogram/stargazers"><img src="https://img.shields.io/github/stars/wangyuchen5110/opencodeplugginprogram?style=for-the-badge&color=58a6ff" alt="GitHub stars"></a>
|
|
16
|
+
<a href="https://github.com/wangyuchen5110/opencodeplugginprogram/actions/workflows/test.yml"><img src="https://github.com/wangyuchen5110/opencodeplugginprogram/actions/workflows/test.yml/badge.svg" alt="tests"></a>
|
|
17
|
+
</p>
|
|
18
|
+
|
|
19
|
+
<p align="center">
|
|
20
|
+
<img src="https://img.shields.io/badge/runs%20locally-100%25-3fb950?style=for-the-badge" alt="runs locally">
|
|
21
|
+
<img src="https://img.shields.io/badge/telemetry-none-3fb950?style=for-the-badge" alt="no telemetry">
|
|
22
|
+
<img src="https://img.shields.io/badge/user%20data-never%20read-3fb950?style=for-the-badge" alt="user data is never read">
|
|
23
|
+
<img src="https://img.shields.io/badge/node-%E2%89%A520-3fb950?style=for-the-badge" alt="Node ≥ 20">
|
|
24
|
+
<a href="LICENSE"><img src="https://img.shields.io/npm/l/opencode-skills-doctor?style=for-the-badge" alt="MIT license"></a>
|
|
25
|
+
</p>
|
|
26
|
+
|
|
19
27
|
<p align="center">
|
|
20
28
|
English · <a href="docs/README.zh-CN.md">简体中文</a> · <a href="CHANGELOG.md">Changelog</a> · <a href="SECURITY.md">Security</a> · <a href="docs/REFERENCE.md">Full reference</a>
|
|
21
29
|
</p>
|
|
22
30
|
|
|
23
|
-
|
|
31
|
+
> **Runs 100% locally.** No telemetry, no account, no uploads — your skills,
|
|
32
|
+
> prompts and files never leave your machine. See [Privacy](#privacy-local-only-no-data-collection).
|
|
33
|
+
|
|
34
|
+
## Contents
|
|
35
|
+
|
|
36
|
+
- [The 30-second version](#the-30-second-version) · [Privacy](#privacy-local-only-no-data-collection)
|
|
37
|
+
- [See it](#see-it) · [How it works](#how-it-works) · [Quick start](#quick-start)
|
|
38
|
+
- [Install](#install) · [Use it in CI](#use-it-in-ci-github-action) · [Commands](#commands) · [Works where you work](#works-where-you-work)
|
|
39
|
+
- [Why it is safe](#why-it-is-safe) · [Development](#development)
|
|
24
40
|
|
|
25
41
|
## The 30-second version
|
|
26
42
|
|
|
@@ -36,6 +52,20 @@ Every agent skill with a `description` is advertised to the model on **every sin
|
|
|
36
52
|
| **Report** | terminal, JSON, or a self-contained HTML report |
|
|
37
53
|
| **Run it** | CLI · OpenCode plugin (1.x/2.x/Desktop) · MCP server · DeepSeek Harness skill |
|
|
38
54
|
|
|
55
|
+
## Privacy: local-only, no data collection
|
|
56
|
+
|
|
57
|
+
`skills-doctor` runs entirely on your machine. There is no telemetry, no analytics, no account, no API key, and nothing is uploaded — not your skills, prompts, conversations or files.
|
|
58
|
+
|
|
59
|
+
| | |
|
|
60
|
+
|---|---|
|
|
61
|
+
| **Reads** | only skill definitions (`SKILL.md` plus the `references/`, `scripts/`, `assets/` files they point at), agent config (`opencode.json(c)`, `.skills-doctor.json`, DSH patch, MCP configs) and the lockfile |
|
|
62
|
+
| **Never reads** | conversation history, session transcripts, source code, documents, credentials, API keys or environment secrets |
|
|
63
|
+
| **Uploads** | nothing: there is no server component and no remote endpoint besides the two optional calls below |
|
|
64
|
+
| **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`) |
|
|
65
|
+
| **Writes** | only what you asked for: the HTML/JSON report, `.skills-doctor-archive/`, `.skills-doctor.json`, the lockfile, and the plugin/MCP/skill entries created by `install` |
|
|
66
|
+
|
|
67
|
+
`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.
|
|
68
|
+
|
|
39
69
|
## See it
|
|
40
70
|
|
|
41
71
|
<p align="center">
|
|
@@ -44,6 +74,23 @@ Every agent skill with a `description` is advertised to the model on **every sin
|
|
|
44
74
|
|
|
45
75
|
A real `check --html` run: summary cards, the context-budget bar, filterable findings, and the exact potential savings.
|
|
46
76
|
|
|
77
|
+
## How it works
|
|
78
|
+
|
|
79
|
+
```mermaid
|
|
80
|
+
flowchart LR
|
|
81
|
+
A["skill roots<br/>OpenCode · Claude · Grok · Cursor · Hermes · Kimi · DSH"] --> B["discover<br/>precedence resolved"]
|
|
82
|
+
B --> C["scan SKILL.md<br/>frontmatter + body + hash"]
|
|
83
|
+
C --> D["lint<br/>16 rules + token budget"]
|
|
84
|
+
D --> E{"findings?"}
|
|
85
|
+
E -->|"yes"| F["plan / fix<br/>trim · hide · archive"]
|
|
86
|
+
E -->|"always"| G["report<br/>terminal · JSON · HTML"]
|
|
87
|
+
F --> G
|
|
88
|
+
G --> H["you · CI · the agent"]
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Everything runs in one pass and is deterministic: the same skill files produce the
|
|
92
|
+
same numbers, so a report is safe to diff in CI.
|
|
93
|
+
|
|
47
94
|
## Quick start
|
|
48
95
|
|
|
49
96
|
```powershell
|
|
@@ -89,7 +136,38 @@ npx -y opencode-skills-doctor mcp
|
|
|
89
136
|
{ "mcpServers": { "skills-doctor": { "command": "npx", "args": ["-y", "opencode-skills-doctor", "mcp"] } } }
|
|
90
137
|
```
|
|
91
138
|
|
|
92
|
-
Nothing to configure for **skills discovery**: it reads `.opencode/skills`, `.claude/skills`, `.agents/skills`, `.grok/skills`, `.cursor/skills` and their user-level equivalents. See the [full reference](docs/REFERENCE.md) for OpenCode 1.x, DSH YAML, Codex TOML and Claude Code details.
|
|
139
|
+
Nothing to configure for **skills discovery**: it reads `.opencode/skills`, `.claude/skills`, `.agents/skills`, `.grok/skills`, `.cursor/skills`, `.hermes/skills`, `.kimi-code/skills`, `.dsh/skills` (plus `$DSH_HOME`) and their user-level equivalents. See the [full reference](docs/REFERENCE.md) for OpenCode 1.x, DSH YAML, Codex TOML and Claude Code details.
|
|
140
|
+
|
|
141
|
+
## Use it in CI (GitHub Action)
|
|
142
|
+
|
|
143
|
+
A composite action ships in this repo: no install step, no runtime dependencies —
|
|
144
|
+
it runs the checked-out code directly.
|
|
145
|
+
|
|
146
|
+
```yaml
|
|
147
|
+
name: skills
|
|
148
|
+
on: [push, pull_request]
|
|
149
|
+
|
|
150
|
+
permissions:
|
|
151
|
+
contents: read
|
|
152
|
+
pull-requests: write # only needed for `comment: true`
|
|
153
|
+
|
|
154
|
+
jobs:
|
|
155
|
+
audit:
|
|
156
|
+
runs-on: ubuntu-latest
|
|
157
|
+
steps:
|
|
158
|
+
- uses: actions/checkout@v4
|
|
159
|
+
- uses: wangyuchen5110/opencodeplugginprogram@v0.9.3
|
|
160
|
+
with:
|
|
161
|
+
budget: "1200"
|
|
162
|
+
fail-on: warn # error | warn | never
|
|
163
|
+
html: skills-report.html # written and uploaded as an artifact
|
|
164
|
+
comment: "true" # post the summary on the PR
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
It writes a summary to the job page, uploads the HTML report, optionally comments
|
|
168
|
+
on the pull request, and fails the job according to `fail-on`. Outputs:
|
|
169
|
+
`exit-code`, `error-count`, `warning-count`, `advertised-tokens`. Anything else
|
|
170
|
+
goes through `extra-args` (for example `--no-global`).
|
|
93
171
|
|
|
94
172
|
## Commands
|
|
95
173
|
|
|
@@ -113,11 +191,12 @@ All commands accept `--json`, `--lang en|zh`, `--budget`, `--root` and `--no-glo
|
|
|
113
191
|
| **OpenCode 2.x** | `plugins: ["opencode-skills-doctor"]` → tool + commands + budget warning |
|
|
114
192
|
| **OpenCode 1.x** | `install` writes the local plugin entry and markdown commands |
|
|
115
193
|
| **OpenCode Desktop** | shares the CLI config; `--client desktop` detects the app and its bundled core |
|
|
116
|
-
| **DeepSeek Harness** | `install --client dsh` adds the MCP entry and the `skills-doctor-audit` skill |
|
|
194
|
+
| **DeepSeek Harness** | `install --client dsh` adds the MCP entry and the `skills-doctor-audit` skill; `$DSH_HOME/skills` is scanned |
|
|
117
195
|
| **Claude Code / Codex** | MCP server (`npx -y opencode-skills-doctor mcp`) |
|
|
118
196
|
| **Grok CLI / Cursor** | their skill roots are scanned natively |
|
|
119
197
|
| **Hermes Agent** | MCP entry in `~/.hermes/config.yaml`; `~/.hermes/skills` is scanned |
|
|
120
198
|
| **Kimi Code** | `install --client kimi` writes `~/.kimi-code/mcp.json` + the audit skill |
|
|
199
|
+
| **OpenClaw** | separate wrapper with its own native plugin: [openclaw-skills-doctor](https://github.com/wangyuchen5110/openclaw-skills-doctor) |
|
|
121
200
|
|
|
122
201
|
## Why it is safe
|
|
123
202
|
|
package/SECURITY.md
CHANGED
|
@@ -27,6 +27,15 @@ Include the version, a reproduction, the impact, and a suggested fix if you have
|
|
|
27
27
|
- `update` — writes downloaded files into a skill directory and updates the lock;
|
|
28
28
|
- everything else (`check`, `scan`, `mcp`) is read-only.
|
|
29
29
|
|
|
30
|
+
## Data handling and privacy
|
|
31
|
+
|
|
32
|
+
- **Nothing leaves your machine.** No telemetry, analytics, accounts, or crash reporting. There is no backend service.
|
|
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
|
+
- **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
|
+
- **Config files may hold secrets** (`opencode.json(c)`, MCP configs). They are parsed locally only to locate the entries the tool needs; values are never transmitted, printed, or copied into reports.
|
|
36
|
+
- **Reports are local files.** HTML/JSON reports contain file paths and skill descriptions, never file bodies. They can reveal your directory layout, so review them before sharing.
|
|
37
|
+
- `check`, `plan`, `scan`, `fix --dry-run` and `mcp` are fully offline; you can run them with networking disabled.
|
|
38
|
+
|
|
30
39
|
## Risk areas and mitigations
|
|
31
40
|
|
|
32
41
|
### `update` downloads untrusted content
|
package/SECURITY.zh-CN.md
CHANGED
|
@@ -27,6 +27,15 @@
|
|
|
27
27
|
- `update` —— 下载文件写入 skill 目录并更新 lock;
|
|
28
28
|
- 其余(`check`、`scan`、`mcp`)都是只读。
|
|
29
29
|
|
|
30
|
+
## 数据处理与隐私
|
|
31
|
+
|
|
32
|
+
- **数据不会离开你的机器。** 没有遥测、统计、账号或崩溃上报,也没有任何后端服务。
|
|
33
|
+
- **默认不联网。** 只有两个例外:`update` 抓取你配置的 catalog URL;每日更新检查只从 npm registry 读取最新版本号(缓存 24h,可用 `"checkUpdates": "off"`、`SKILLS_DOCTOR_UPDATE_CHECK=0` 或 `--no-update-check` 关闭)。
|
|
34
|
+
- **不读取用户数据。** 只读 skill 文件及其引用资源、agent 配置和自身 lockfile;不读对话历史、会话记录、源代码、文档、模型凭据或 API key,也不上传任何读到的内容。
|
|
35
|
+
- **配置文件可能含密钥**(`opencode.json(c)`、MCP 配置):只在本地解析以定位需要的条目,值不会被传输、打印或写进报告。
|
|
36
|
+
- **报告是本地文件。** HTML/JSON 报告含文件路径和 skill 描述,不含文件正文;可能暴露目录结构,分享前请自行检查。
|
|
37
|
+
- `check`、`plan`、`scan`、`fix --dry-run`、`mcp` 完全离线,可在断网环境运行。
|
|
38
|
+
|
|
30
39
|
## 风险点与缓解措施
|
|
31
40
|
|
|
32
41
|
### `update` 会下载不受信内容
|
package/docs/README.zh-CN.md
CHANGED
|
@@ -2,24 +2,42 @@
|
|
|
2
2
|
<img src="https://raw.githubusercontent.com/wangyuchen5110/opencodeplugginprogram/main/docs/assets/banner.svg" alt="skills-doctor" width="100%">
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
|
+
<h1 align="center">skills-doctor</h1>
|
|
6
|
+
|
|
5
7
|
<p align="center">
|
|
6
8
|
<b>审计、修复、更新你的 coding agent 加载的 skill。</b><br>
|
|
7
9
|
量出每步 token 税,找出遮蔽重复和断链,再安全地修掉。
|
|
8
10
|
</p>
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
<p align="center">
|
|
13
|
+
<a href="https://www.npmjs.com/package/opencode-skills-doctor"><img src="https://img.shields.io/npm/v/opencode-skills-doctor?style=for-the-badge&color=3fb950" alt="npm"></a>
|
|
14
|
+
<a href="https://www.npmjs.com/package/opencode-skills-doctor"><img src="https://img.shields.io/npm/dw/opencode-skills-doctor?style=for-the-badge&color=2ea043" alt="downloads"></a>
|
|
15
|
+
<a href="https://github.com/wangyuchen5110/opencodeplugginprogram/stargazers"><img src="https://img.shields.io/github/stars/wangyuchen5110/opencodeplugginprogram?style=for-the-badge&color=58a6ff" alt="stars"></a>
|
|
16
|
+
<a href="https://github.com/wangyuchen5110/opencodeplugginprogram/actions/workflows/test.yml"><img src="https://github.com/wangyuchen5110/opencodeplugginprogram/actions/workflows/test.yml/badge.svg" alt="tests"></a>
|
|
17
|
+
</p>
|
|
18
|
+
|
|
19
|
+
<p align="center">
|
|
20
|
+
<img src="https://img.shields.io/badge/runs%20locally-100%25-3fb950?style=for-the-badge" alt="本地运行">
|
|
21
|
+
<img src="https://img.shields.io/badge/telemetry-none-3fb950?style=for-the-badge" alt="无遥测">
|
|
22
|
+
<img src="https://img.shields.io/badge/user%20data-never%20read-3fb950?style=for-the-badge" alt="不读取用户数据">
|
|
23
|
+
<img src="https://img.shields.io/badge/node-%E2%89%A520-3fb950?style=for-the-badge" alt="Node ≥ 20">
|
|
24
|
+
<a href="../LICENSE"><img src="https://img.shields.io/npm/l/opencode-skills-doctor?style=for-the-badge" alt="MIT"></a>
|
|
25
|
+
</p>
|
|
11
26
|
|
|
12
|
-
|
|
27
|
+
<p align="center">
|
|
28
|
+
<a href="../README.md">English</a> · 简体中文 · <a href="../CHANGELOG.md">变更记录</a> · <a href="../SECURITY.zh-CN.md">安全策略</a> · <a href="REFERENCE.md">完整参考</a>
|
|
29
|
+
</p>
|
|
13
30
|
|
|
14
|
-
[
|
|
15
|
-
[](https://www.npmjs.com/package/opencode-skills-doctor)
|
|
16
|
-
[](https://www.npmjs.com/package/opencode-skills-doctor)
|
|
17
|
-
[](../LICENSE)
|
|
18
|
-
[](https://github.com/wangyuchen5110/opencodeplugginprogram/actions/workflows/test.yml)
|
|
31
|
+
> **纯本地运行。** 无遥测、不需要账号、不上传 —— 你的 skill、提示词和文件不会离开你的机器。见[隐私](#隐私纯本地运行不收集数据)。
|
|
19
32
|
|
|
20
33
|
OpenCode skill 的**本地检测器 + 更新器 + 修复器**,并附带 **OpenCode 插件入口** —— 零依赖,Node ≥ 20。
|
|
21
34
|
|
|
22
|
-
|
|
35
|
+
## 目录
|
|
36
|
+
|
|
37
|
+
- [一张表看懂](#一张表看懂) · [隐私](#隐私纯本地运行不收集数据)
|
|
38
|
+
- [为什么需要它](#为什么需要它) · [快速开始](#快速开始) · [命令](#命令)
|
|
39
|
+
- [工作原理](#工作原理) · [检测规则](#检测规则) · [安全](#安全)
|
|
40
|
+
- [常见问题](#常见问题faq) · [开发与贡献](#开发与贡献)
|
|
23
41
|
|
|
24
42
|
## 一张表看懂
|
|
25
43
|
|
|
@@ -27,14 +45,28 @@ OpenCode skill 的**本地检测器 + 更新器 + 修复器**,并附带 **Open
|
|
|
27
45
|
|---|---|
|
|
28
46
|
| **是什么** | 审计、修复、更新 coding agent 加载的 skill |
|
|
29
47
|
| **形态** | CLI · OpenCode 插件(1.x / 2.x)· MCP server(Claude Code、Codex、DSH…)· 兼容 Grok CLI |
|
|
30
|
-
| **来源** | `.opencode/skills`、`.claude/skills`、`.agents/skills`、`.grok/skills`、`.cursor/skills
|
|
48
|
+
| **来源** | `.opencode/skills`、`.claude/skills`、`.agents/skills`、`.grok/skills`、`.cursor/skills`、`.hermes/skills`、`.kimi-code/skills`、`.dsh/skills`(含 `$DSH_HOME`)、`opencode.json(c)` 配置、HTTP catalog |
|
|
31
49
|
| **能查** | 同 ID 遮蔽、重复、超长描述、超大正文、每步 token 预算、断链引用、非法 frontmatter、空正文、陈旧 skill |
|
|
32
50
|
| **能修** | 归档遮蔽定义(可逆)、隐藏低频 skill、catalog 同步并检测 drift |
|
|
33
51
|
| **报告** | 终端、JSON,或自包含 HTML(`--html [file] --open`) |
|
|
34
52
|
| **规划** | 精确 token 节省:精简描述 / 隐藏 skill,按 N 步折算 |
|
|
35
53
|
| **成本** | CLI 0 token;插件工具定义约 191/步,另有每会话一次预警 |
|
|
36
54
|
| **要求** | Node ≥ 20,零运行时依赖 |
|
|
37
|
-
| **质量** |
|
|
55
|
+
| **质量** | 63 个测试,ubuntu/macos/windows × Node 20/22/24 CI |
|
|
56
|
+
|
|
57
|
+
## 隐私:纯本地运行,不收集数据
|
|
58
|
+
|
|
59
|
+
`skills-doctor` 完全在你自己的机器上运行。没有遥测、没有统计、不需要账号和 API key,也不会把你的 skill、提示词、对话或文件上传到任何地方。
|
|
60
|
+
|
|
61
|
+
| | |
|
|
62
|
+
|---|---|
|
|
63
|
+
| **会读** | 只读 skill 定义(`SKILL.md` 及其引用的 `references/`、`scripts/`、`assets/` 文件)、agent 配置(`opencode.json(c)`、`.skills-doctor.json`、DSH patch、MCP 配置)和它自己的 lockfile |
|
|
64
|
+
| **不会读** | 对话历史、会话记录、源代码、文档、凭据、API key、环境变量里的密钥 |
|
|
65
|
+
| **不会上传** | 任何东西:没有服务端组件,除下面两个可选联网外没有任何远端地址 |
|
|
66
|
+
| **联网** | 只在你主动触发时发生:`update` 抓取**你自己配置**的 HTTP catalog;`upgrade` / 每日更新检查只读 npm registry 的最新版本号(缓存 24h,可用 `"checkUpdates": "off"`、`SKILLS_DOCTOR_UPDATE_CHECK=0` 或 `--no-update-check` 关闭) |
|
|
67
|
+
| **会写** | 只写你要求的内容:HTML/JSON 报告、`.skills-doctor-archive/`、`.skills-doctor.json`、lockfile,以及 `install` 生成的插件/MCP/skill 条目 |
|
|
68
|
+
|
|
69
|
+
`opencode.json(c)` 和 MCP 配置里可能有凭据;它们只在本地被解析用来定位相关条目,值不会被发送,也不会出现在报告里。报告只含文件路径和 skill 描述(本来就是你的 skill 文件内容),不包含正文。`check`、`plan`、`scan`、`fix --dry-run`、`mcp` 都可以在断网环境下运行。
|
|
38
70
|
|
|
39
71
|
## 为什么需要它
|
|
40
72
|
|
package/docs/REFERENCE.md
CHANGED
|
@@ -478,9 +478,11 @@ It merges `skills-doctor` into `~/.kimi-code/mcp.json` (or `$KIMI_CODE_HOME/mcp.
|
|
|
478
478
|
10. project `.hermes/skills`
|
|
479
479
|
11. `~/.kimi-code/skills`
|
|
480
480
|
12. project `.kimi-code/skills`
|
|
481
|
-
13. `~/.
|
|
482
|
-
14. project `.
|
|
483
|
-
15.
|
|
481
|
+
13. `$DSH_HOME/skills` (default `~/.dsh/skills`) — DeepSeek Harness, where `install --client dsh` puts its skill
|
|
482
|
+
14. project `.dsh/skills`
|
|
483
|
+
15. `~/.config/opencode/skills`
|
|
484
|
+
16. project `.opencode/skills`
|
|
485
|
+
17. the `skills` array in `opencode.json(c)` (2.x) or `skills.paths` (1.x); relative paths resolve from the working directory; supports `~/`, absolute paths and HTTP catalogs
|
|
484
486
|
|
|
485
487
|
## JSON output
|
|
486
488
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-skills-doctor",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Audit, fix and update agent skills: token cost, broken references, shadowed duplicates, HTML reports, catalog sync. CLI, OpenCode plugin, MCP server; works with OpenCode, Claude Code, Codex, Grok CLI, Cursor and DeepSeek Harness.",
|
|
3
|
+
"version": "0.9.3",
|
|
4
|
+
"description": "Audit, fix and update agent skills: token cost, broken references, shadowed duplicates, HTML reports, catalog sync. CLI, OpenCode plugin, MCP server; works with OpenCode, Claude Code, Codex, Grok CLI, Cursor, Hermes Agent, Kimi Code and DeepSeek Harness.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": "./src/plugin.mjs",
|
package/src/audit.mjs
CHANGED
|
@@ -17,13 +17,15 @@ export function auditSkills({
|
|
|
17
17
|
budget = null,
|
|
18
18
|
staleDays = null,
|
|
19
19
|
extraDirs = [],
|
|
20
|
+
home = undefined,
|
|
21
|
+
env = undefined,
|
|
20
22
|
} = {}) {
|
|
21
23
|
const loaded = loadConfig(cwd, configPath)
|
|
22
24
|
const config = loaded.config
|
|
23
25
|
const resolvedLang = normalizeLang(lang) ?? normalizeLang(config.lang) ?? detectLang()
|
|
24
26
|
const t = translator(resolvedLang)
|
|
25
27
|
|
|
26
|
-
const discovered = scanAll(discoverSources({ cwd, includeGlobal, extraDirs }))
|
|
28
|
+
const discovered = scanAll(discoverSources({ cwd, includeGlobal, extraDirs, home, env }))
|
|
27
29
|
const managedInfo = inspectManaged(discovered.scans, { t })
|
|
28
30
|
|
|
29
31
|
const effectiveBudget = Number.isFinite(budget) && budget > 0 ? budget : config.budget ?? 1200
|
package/src/catalog.mjs
CHANGED
|
@@ -19,6 +19,20 @@ function safeRelPath(p) {
|
|
|
19
19
|
return true
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* A catalog entry name becomes a directory under the target root, so it must be
|
|
24
|
+
* exactly one plain path segment - otherwise `../x` or `a/b` escapes `into`.
|
|
25
|
+
*/
|
|
26
|
+
function safeSkillName(name) {
|
|
27
|
+
if (typeof name !== "string") return false
|
|
28
|
+
const value = name.trim()
|
|
29
|
+
if (!value || value === "." || value === "..") return false
|
|
30
|
+
if (value.includes("/") || value.includes("\\")) return false
|
|
31
|
+
if (path.isAbsolute(value)) return false
|
|
32
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(value)) return false
|
|
33
|
+
return true
|
|
34
|
+
}
|
|
35
|
+
|
|
22
36
|
function resolveInside(baseDir, rel, t) {
|
|
23
37
|
const base = path.resolve(baseDir)
|
|
24
38
|
const target = path.resolve(base, rel)
|
|
@@ -32,6 +46,7 @@ export function validateCatalogEntry(entry, t = translator("en")) {
|
|
|
32
46
|
const problems = []
|
|
33
47
|
if (!entry || typeof entry !== "object") return [t("catalog.entryNotObject")]
|
|
34
48
|
if (typeof entry.name !== "string" || !entry.name.trim()) problems.push(t("catalog.missingName"))
|
|
49
|
+
else if (!safeSkillName(entry.name)) problems.push(t("catalog.safeName", { name: entry.name }))
|
|
35
50
|
if (typeof entry.version !== "string" && typeof entry.version !== "number") {
|
|
36
51
|
problems.push(t("catalog.missingVersion"))
|
|
37
52
|
}
|
|
@@ -151,6 +166,26 @@ export async function updateCatalogs(
|
|
|
151
166
|
const lockTooNew = Number(lock.version ?? 1) > LOCK_VERSION
|
|
152
167
|
const results = []
|
|
153
168
|
|
|
169
|
+
// Never rewrite a lock written by a newer tool: its semantics are unknown and
|
|
170
|
+
// downgrading it could silently break drift detection for that version.
|
|
171
|
+
if (lockTooNew) {
|
|
172
|
+
return {
|
|
173
|
+
lockPath,
|
|
174
|
+
lockTooNew,
|
|
175
|
+
results: catalogs.map((catalog) => ({
|
|
176
|
+
url: catalog.url,
|
|
177
|
+
error: t("catalog.lockTooNew", { found: lock.version, supported: LOCK_VERSION }),
|
|
178
|
+
updated: [],
|
|
179
|
+
unchanged: [],
|
|
180
|
+
invalid: [],
|
|
181
|
+
orphaned: [],
|
|
182
|
+
pruned: [],
|
|
183
|
+
removedFiles: [],
|
|
184
|
+
files: 0,
|
|
185
|
+
})),
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
154
189
|
for (const catalog of catalogs) {
|
|
155
190
|
const result = {
|
|
156
191
|
url: catalog.url,
|
|
@@ -226,7 +261,8 @@ export async function updateCatalogs(
|
|
|
226
261
|
const validNames = new Set(
|
|
227
262
|
skills.filter((entry) => validateCatalogEntry(entry, t).length === 0).map((entry) => entry.name),
|
|
228
263
|
)
|
|
229
|
-
|
|
264
|
+
// A tampered or older lock can carry unsafe names; never touch those paths.
|
|
265
|
+
const orphaned = Object.keys(previousSkills).filter((name) => safeSkillName(name) && !validNames.has(name))
|
|
230
266
|
result.orphaned = orphaned
|
|
231
267
|
|
|
232
268
|
if (!checkOnly) {
|
package/src/desktop.mjs
CHANGED
|
@@ -21,8 +21,10 @@ export function desktopCandidates(platform = process.platform, env = process.env
|
|
|
21
21
|
return ["/usr/bin/opencode-desktop", "/usr/local/bin/opencode-desktop", "/opt/OpenCode/opencode-desktop", "/opt/opencode-desktop/opencode-desktop"]
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
function asarPathFor(exe) {
|
|
25
|
-
|
|
24
|
+
function asarPathFor(exe, platform = process.platform) {
|
|
25
|
+
// Callers may probe another platform (tests, cross-platform tooling), so use
|
|
26
|
+
// the requested platform instead of the host one.
|
|
27
|
+
if (platform === "darwin" || exe.includes(".app")) {
|
|
26
28
|
return path.join(path.dirname(path.dirname(path.dirname(path.dirname(exe)))), "Resources", "app.asar")
|
|
27
29
|
}
|
|
28
30
|
return path.join(path.dirname(exe), "resources", "app.asar")
|
|
@@ -71,8 +73,9 @@ export function readAsarVersion(asarFile) {
|
|
|
71
73
|
export function detectDesktop({ platform = process.platform, env = process.env, home = os.homedir() } = {}) {
|
|
72
74
|
for (const exe of desktopCandidates(platform, env, home)) {
|
|
73
75
|
if (!fs.existsSync(exe)) continue
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
+
const asar = asarPathFor(exe, platform)
|
|
77
|
+
const version = readAsarVersion(asar)
|
|
78
|
+
return { installed: true, exe, version, asar }
|
|
76
79
|
}
|
|
77
80
|
return { installed: false, exe: null, version: null, asar: null }
|
|
78
81
|
}
|
package/src/discover.mjs
CHANGED
|
@@ -14,6 +14,11 @@ export function expandHome(p, home = os.homedir()) {
|
|
|
14
14
|
|
|
15
15
|
/** 去掉 JSONC 的注释和尾逗号,字符串内的内容不动。 */
|
|
16
16
|
export function stripJsonc(text) {
|
|
17
|
+
return stripTrailingCommas(stripJsonComments(text))
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** First pass: drop `//` and `/* *\/` comments, leaving strings untouched. */
|
|
21
|
+
function stripJsonComments(text) {
|
|
17
22
|
let out = ""
|
|
18
23
|
let inString = false
|
|
19
24
|
let escaped = false
|
|
@@ -63,7 +68,41 @@ export function stripJsonc(text) {
|
|
|
63
68
|
out += ch
|
|
64
69
|
}
|
|
65
70
|
|
|
66
|
-
return out
|
|
71
|
+
return out
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Second pass: drop commas that only precede a closing brace/bracket.
|
|
76
|
+
* Done character by character so a `,}` inside a string survives.
|
|
77
|
+
*/
|
|
78
|
+
function stripTrailingCommas(text) {
|
|
79
|
+
let out = ""
|
|
80
|
+
let inString = false
|
|
81
|
+
let escaped = false
|
|
82
|
+
|
|
83
|
+
for (let i = 0; i < text.length; i++) {
|
|
84
|
+
const ch = text[i]
|
|
85
|
+
if (inString) {
|
|
86
|
+
out += ch
|
|
87
|
+
if (escaped) escaped = false
|
|
88
|
+
else if (ch === "\\") escaped = true
|
|
89
|
+
else if (ch === '"') inString = false
|
|
90
|
+
continue
|
|
91
|
+
}
|
|
92
|
+
if (ch === '"') {
|
|
93
|
+
inString = true
|
|
94
|
+
out += ch
|
|
95
|
+
continue
|
|
96
|
+
}
|
|
97
|
+
if (ch === ",") {
|
|
98
|
+
let j = i + 1
|
|
99
|
+
while (j < text.length && /\s/.test(text[j])) j++
|
|
100
|
+
if (text[j] === "}" || text[j] === "]") continue
|
|
101
|
+
}
|
|
102
|
+
out += ch
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return out
|
|
67
106
|
}
|
|
68
107
|
|
|
69
108
|
export function readJsoncFile(file) {
|
|
@@ -124,17 +163,26 @@ function isUnder(parent, child) {
|
|
|
124
163
|
* 10. 项目 .hermes/skills
|
|
125
164
|
* 11. ~/.kimi-code/skills(Kimi Code)
|
|
126
165
|
* 12. 项目 .kimi-code/skills
|
|
127
|
-
* 13. ~/.
|
|
128
|
-
* 14. 项目 .
|
|
129
|
-
* 15. opencode
|
|
166
|
+
* 13. $DSH_HOME/skills(默认 ~/.dsh/skills,DeepSeek Harness)
|
|
167
|
+
* 14. 项目 .dsh/skills
|
|
168
|
+
* 15. ~/.config/opencode/skills
|
|
169
|
+
* 16. 项目 .opencode/skills(项目根 → cwd)
|
|
170
|
+
* 17. opencode.json(c) 里的 skills 数组 / skills.paths
|
|
130
171
|
*/
|
|
131
|
-
export function discoverSources({
|
|
172
|
+
export function discoverSources({
|
|
173
|
+
cwd = process.cwd(),
|
|
174
|
+
home = os.homedir(),
|
|
175
|
+
includeGlobal = true,
|
|
176
|
+
extraDirs = [],
|
|
177
|
+
env = process.env,
|
|
178
|
+
} = {}) {
|
|
132
179
|
const projectRoot = findProjectRoot(cwd) ?? path.resolve(cwd)
|
|
133
180
|
const projectChain = pathChain(projectRoot, cwd)
|
|
134
181
|
const sources = []
|
|
135
182
|
const catalogs = []
|
|
136
183
|
const missing = []
|
|
137
184
|
const seen = new Set()
|
|
185
|
+
const dshHome = env.DSH_HOME ? path.resolve(expandHome(env.DSH_HOME, home)) : path.join(home, ".dsh")
|
|
138
186
|
|
|
139
187
|
const addSource = (kind, scope, dir) => {
|
|
140
188
|
const abs = path.resolve(dir)
|
|
@@ -161,6 +209,9 @@ export function discoverSources({ cwd = process.cwd(), home = os.homedir(), incl
|
|
|
161
209
|
if (includeGlobal) addSource("kimi-global", "global", path.join(home, ".kimi-code", "skills"))
|
|
162
210
|
for (const dir of projectChain) addSource("kimi-project", "project", path.join(dir, ".kimi-code", "skills"))
|
|
163
211
|
|
|
212
|
+
if (includeGlobal) addSource("dsh-global", "global", path.join(dshHome, "skills"))
|
|
213
|
+
for (const dir of projectChain) addSource("dsh-project", "project", path.join(dir, ".dsh", "skills"))
|
|
214
|
+
|
|
164
215
|
if (includeGlobal) addSource("opencode-global", "global", path.join(home, ".config", "opencode", "skills"))
|
|
165
216
|
for (const dir of projectChain) addSource("opencode-project", "project", path.join(dir, ".opencode", "skills"))
|
|
166
217
|
|
|
@@ -268,8 +319,12 @@ function walkForSkillMd(dir, source, out, depth, seen) {
|
|
|
268
319
|
|
|
269
320
|
function buildSkill({ id, file, source, form }) {
|
|
270
321
|
let text = ""
|
|
322
|
+
let bytes = 0
|
|
271
323
|
try {
|
|
272
|
-
|
|
324
|
+
const buffer = fs.readFileSync(file)
|
|
325
|
+
bytes = buffer.length
|
|
326
|
+
// Strip a UTF-8 BOM so the frontmatter fence is found (Windows editors add one).
|
|
327
|
+
text = buffer.toString("utf8").replace(/^\uFEFF/, "")
|
|
273
328
|
} catch {
|
|
274
329
|
text = ""
|
|
275
330
|
}
|
|
@@ -309,7 +364,7 @@ function buildSkill({ id, file, source, form }) {
|
|
|
309
364
|
body,
|
|
310
365
|
bodyChars: body.trim().length,
|
|
311
366
|
bodyTokens: estimateTokens(body),
|
|
312
|
-
bytes
|
|
367
|
+
bytes,
|
|
313
368
|
descriptionTokens: estimateTokens(description),
|
|
314
369
|
hash: sha1(body.replace(/\s+/g, " ").trim()),
|
|
315
370
|
mtime,
|
package/src/i18n.mjs
CHANGED
|
@@ -148,7 +148,7 @@ Exit codes: 0 ok; 1 errors (and warnings with --strict); 2 bad arguments or runt
|
|
|
148
148
|
"upgrade.upToDate": "Already up to date.",
|
|
149
149
|
"upgrade.done": "Upgraded to the latest version.",
|
|
150
150
|
"upgrade.failed": "Upgrade failed; run it manually: {cmd}",
|
|
151
|
-
"update.lockTooNew": " ! lock is newer than this tool;
|
|
151
|
+
"update.lockTooNew": " ! lock is newer than this tool; refusing to rewrite it (upgrade skills-doctor)",
|
|
152
152
|
"update.detector": " check now sees these skills ({dir} is where opencode reads)",
|
|
153
153
|
"update.noCatalogs": "No HTTP catalog configured. Add https://... to the skills array in opencode.json(c).",
|
|
154
154
|
"update.driftRepair": "{name} {version} (drift repaired)",
|
|
@@ -229,6 +229,8 @@ Exit codes: 0 ok; 1 errors (and warnings with --strict); 2 bad arguments or runt
|
|
|
229
229
|
"catalog.missingVersion": "missing version",
|
|
230
230
|
"catalog.missingFiles": "missing files",
|
|
231
231
|
"catalog.safePath": "unsafe file path: {file}",
|
|
232
|
+
"catalog.safeName": "unsafe skill name (one plain path segment required): {name}",
|
|
233
|
+
"catalog.lockTooNew": "lock version {found} is newer than supported ({supported}); refusing to rewrite it - upgrade skills-doctor",
|
|
232
234
|
"catalog.missingEntryFile": "files has no SKILL.md or <name>.md",
|
|
233
235
|
},
|
|
234
236
|
|
|
@@ -377,7 +379,7 @@ Exit codes: 0 ok; 1 errors (and warnings with --strict); 2 bad arguments or runt
|
|
|
377
379
|
"upgrade.upToDate": "已是最新版本。",
|
|
378
380
|
"upgrade.done": "已升级到最新版本。",
|
|
379
381
|
"upgrade.failed": "升级失败,请手动执行: {cmd}",
|
|
380
|
-
"update.lockTooNew": " ! lock
|
|
382
|
+
"update.lockTooNew": " ! lock 版本比本工具新,拒绝改写(请升级 skills-doctor)",
|
|
381
383
|
"update.detector": " check 现在可以直接检测到这些 skill({dir} 是 opencode 会读取的目录)",
|
|
382
384
|
"update.noCatalogs": "没有配置 HTTP catalog。在 opencode.json(c) 的 skills 数组里写 https://... 即可。",
|
|
383
385
|
"update.driftRepair": "{name} {version}(修复 drift)",
|
|
@@ -452,6 +454,8 @@ Exit codes: 0 ok; 1 errors (and warnings with --strict); 2 bad arguments or runt
|
|
|
452
454
|
"catalog.missingVersion": "缺少 version",
|
|
453
455
|
"catalog.missingFiles": "缺少 files",
|
|
454
456
|
"catalog.safePath": "不安全的文件路径: {file}",
|
|
457
|
+
"catalog.safeName": "不安全的 skill 名(必须是一个普通路径段): {name}",
|
|
458
|
+
"catalog.lockTooNew": "lock 版本 {found} 比本工具支持的 {supported} 新,拒绝改写 —— 请升级 skills-doctor",
|
|
455
459
|
"catalog.missingEntryFile": "files 里没有 SKILL.md 或 <name>.md",
|
|
456
460
|
},
|
|
457
461
|
}
|
package/src/install.mjs
CHANGED
|
@@ -236,12 +236,17 @@ export function installOpenCode({
|
|
|
236
236
|
const pkgFile = path.join(dir, "package.json")
|
|
237
237
|
const pkgText = readText(pkgFile)
|
|
238
238
|
let pkg = {}
|
|
239
|
+
let pkgParsed = pkgText === null
|
|
239
240
|
if (pkgText !== null) {
|
|
240
241
|
try {
|
|
241
242
|
pkg = JSON.parse(pkgText)
|
|
242
|
-
|
|
243
|
+
pkgParsed = true
|
|
244
|
+
} catch (err) {
|
|
245
|
+
// Never overwrite a package.json we could not parse - that would drop it.
|
|
246
|
+
errors.push(`cannot parse ${pkgFile}: ${String(err?.message ?? err)}`)
|
|
247
|
+
}
|
|
243
248
|
}
|
|
244
|
-
if (pkg.type !== "module") {
|
|
249
|
+
if (pkgParsed && pkg.type !== "module") {
|
|
245
250
|
if (!dryRun) {
|
|
246
251
|
pkg.type = "module"
|
|
247
252
|
fs.writeFileSync(pkgFile, JSON.stringify(pkg, null, 2) + "\n", "utf8")
|
package/src/lint.mjs
CHANGED
|
@@ -203,18 +203,19 @@ export function lint(skills, options = {}) {
|
|
|
203
203
|
})
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
-
// 5) 描述近似(token
|
|
207
|
-
const withDesc = described
|
|
206
|
+
// 5) 描述近似(token 集合只算一次;按大小升序后用 Jaccard 上界提前结束)
|
|
207
|
+
const withDesc = described
|
|
208
|
+
.map((skill) => ({ skill, tokens: tokenSet(skill.description) }))
|
|
209
|
+
.filter((entry) => entry.tokens.size > 0)
|
|
210
|
+
.sort((x, y) => x.tokens.size - y.tokens.size)
|
|
208
211
|
const pairs = []
|
|
209
212
|
for (let i = 0; i < withDesc.length; i++) {
|
|
213
|
+
const a = withDesc[i]
|
|
210
214
|
for (let j = i + 1; j < withDesc.length; j++) {
|
|
211
|
-
const a = withDesc[i]
|
|
212
215
|
const b = withDesc[j]
|
|
213
216
|
if (a.skill.id === b.skill.id) continue
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
// Jaccard ≤ min/max,所以比值低于阈值时不可能达到阈值
|
|
217
|
-
if (!min || min / max < NEAR_DUPLICATE_THRESHOLD) continue
|
|
217
|
+
// 大小升序:a 为空集已过滤,|a|/|b| 随 j 单调下降,低于阈值后再无可能命中。
|
|
218
|
+
if (a.tokens.size / b.tokens.size < NEAR_DUPLICATE_THRESHOLD) break
|
|
218
219
|
const score = jaccard(a.tokens, b.tokens)
|
|
219
220
|
if (score >= NEAR_DUPLICATE_THRESHOLD) pairs.push({ a: a.skill.id, b: b.skill.id, score })
|
|
220
221
|
}
|
package/src/mcp.mjs
CHANGED
|
@@ -10,7 +10,7 @@ export function toolDefinition() {
|
|
|
10
10
|
return {
|
|
11
11
|
name: TOOL_NAME,
|
|
12
12
|
description:
|
|
13
|
-
"Audit
|
|
13
|
+
"Audit installed agent skills (OpenCode, Claude Code, Grok, Cursor, Hermes, Kimi, DSH): shadowed duplicates, per-step description token budget, oversized bodies and fix hints. Returns a markdown report.",
|
|
14
14
|
inputSchema: {
|
|
15
15
|
type: "object",
|
|
16
16
|
properties: {
|
package/src/parse.mjs
CHANGED
|
@@ -6,10 +6,13 @@ import { createHash } from "node:crypto"
|
|
|
6
6
|
|
|
7
7
|
/** @param {string} text */
|
|
8
8
|
export function parseFrontmatter(text) {
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
// A UTF-8 BOM (Windows editors add one) would otherwise hide the opening `---`
|
|
10
|
+
// and make the whole skill look like it has no frontmatter.
|
|
11
|
+
const source = String(text ?? "").replace(/^\uFEFF/, "")
|
|
12
|
+
const empty = { data: {}, body: source, hasFrontmatter: false }
|
|
13
|
+
if (!source.startsWith("---")) return empty
|
|
11
14
|
|
|
12
|
-
const lines =
|
|
15
|
+
const lines = source.split(/\r?\n/)
|
|
13
16
|
if (lines[0].trim() !== "---") return empty
|
|
14
17
|
|
|
15
18
|
let end = -1
|
package/src/plugin-v1.mjs
CHANGED
|
@@ -33,7 +33,7 @@ export const SkillsDoctorPlugin = async ({ directory }) => ({
|
|
|
33
33
|
tool: {
|
|
34
34
|
skills_doctor: tool({
|
|
35
35
|
description:
|
|
36
|
-
"Audit
|
|
36
|
+
"Audit installed agent skills (OpenCode, Claude Code, Grok, Cursor, Hermes, Kimi, DSH): shadowing, per-step token budget and fix hints. Returns a markdown report.",
|
|
37
37
|
args: z.object({
|
|
38
38
|
directory: z.string().optional().describe("Directory to audit (defaults to the current project)"),
|
|
39
39
|
budget: z.number().optional().describe("Description token budget (default 1200)"),
|
package/src/plugin.mjs
CHANGED
|
@@ -35,7 +35,7 @@ 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
|
|
38
|
+
description: "Audit installed agent skills (OpenCode, Claude Code, Grok, Cursor, Hermes, Kimi, DSH): shadowing, per-step token budget and fix hints",
|
|
39
39
|
input: {
|
|
40
40
|
type: "object",
|
|
41
41
|
properties: {
|