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 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
- <p align="center">
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` 会下载不受信内容
@@ -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
- # skills-doctor
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
- [English](../README.md) · [完整英文参考](REFERENCE.md)
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
- [![npm](https://img.shields.io/npm/v/opencode-skills-doctor)](https://www.npmjs.com/package/opencode-skills-doctor)
15
- [![downloads](https://img.shields.io/npm/dw/opencode-skills-doctor)](https://www.npmjs.com/package/opencode-skills-doctor)
16
- [![node](https://img.shields.io/node/v/opencode-skills-doctor)](https://www.npmjs.com/package/opencode-skills-doctor)
17
- [![license](https://img.shields.io/npm/l/opencode-skills-doctor)](../LICENSE)
18
- [![test](https://github.com/wangyuchen5110/opencodeplugginprogram/actions/workflows/test.yml/badge.svg)](https://github.com/wangyuchen5110/opencodeplugginprogram/actions/workflows/test.yml)
31
+ > **纯本地运行。** 无遥测、不需要账号、不上传 —— 你的 skill、提示词和文件不会离开你的机器。见[隐私](#隐私纯本地运行不收集数据)。
19
32
 
20
33
  OpenCode skill 的**本地检测器 + 更新器 + 修复器**,并附带 **OpenCode 插件入口** —— 零依赖,Node ≥ 20。
21
34
 
22
- [English](../README.md) | **简体中文** · [变更记录](../CHANGELOG.md) · [安全策略](../SECURITY.zh-CN.md)
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`、`opencode.json(c)` 配置、HTTP catalog |
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
- | **质量** | 58 个测试,ubuntu/macos/windows × Node 20/22/24 CI |
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. `~/.config/opencode/skills`
482
- 14. project `.opencode/skills`
483
- 15. 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
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.8.1",
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
- const orphaned = Object.keys(previousSkills).filter((name) => !validNames.has(name))
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
- if (process.platform === "darwin" || exe.includes(".app")) {
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 version = readAsarVersion(asarPathFor(exe))
75
- return { installed: true, exe, version, asar: asarPathFor(exe) }
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.replace(/,(\s*[}\]])/g, "$1")
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. ~/.config/opencode/skills
128
- * 14. 项目 .opencode/skills(项目根 → cwd)
129
- * 15. opencode.json(c) 里的 skills 数组 / skills.paths
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({ cwd = process.cwd(), home = os.homedir(), includeGlobal = true, extraDirs = [] } = {}) {
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
- text = fs.readFileSync(file, "utf8")
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: Buffer.byteLength(text),
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; parsed in compatibility mode (unknown fields ignored)",
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
- } catch {}
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 集合只算一次;先按集合大小做 0.75 上界预筛)
207
- const withDesc = described.map((skill) => ({ skill, tokens: tokenSet(skill.description) }))
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
- const min = Math.min(a.tokens.size, b.tokens.size)
215
- const max = Math.max(a.tokens.size, b.tokens.size)
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 local agent skills (OpenCode / Claude Code / .agents): duplicate and shadowed definitions, per-step description token budget, oversized bodies and fix hints. Returns a markdown report.",
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
- const empty = { data: {}, body: text, hasFrontmatter: false }
10
- if (!text.startsWith("---")) return empty
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 = text.split(/\r?\n/)
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 local OpenCode skills: duplicates, shadowing, context budget and fix hints. Returns a markdown report.",
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 local OpenCode skills: duplicates, shadowing, context budget and fix hints",
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: {