opencode-skills-doctor 0.3.5 → 0.3.6
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 +26 -0
- package/README.zh-CN.md +26 -0
- package/package.json +5 -3
- package/src/cli.mjs +10 -2
- package/src/i18n.mjs +2 -0
- package/src/mcp.mjs +150 -0
package/README.md
CHANGED
|
@@ -155,6 +155,32 @@ It registers the same `skills_doctor` tool (1.x has no `/skills-doctor` command
|
|
|
155
155
|
|
|
156
156
|
> The package root entry is the V2 plugin and requires OpenCode 2.x. The CLI works on both lines.
|
|
157
157
|
|
|
158
|
+
## MCP server (Claude Code, Codex, Kimi Code, any MCP client)
|
|
159
|
+
|
|
160
|
+
`skills-doctor mcp` speaks the Model Context Protocol over stdio and exposes one tool: `skills_doctor` with arguments `directory?`, `budget?`, `lang?` and `global?`. It does not need OpenCode installed.
|
|
161
|
+
|
|
162
|
+
Claude Code (`.mcp.json` in the project, or your MCP config):
|
|
163
|
+
|
|
164
|
+
```json
|
|
165
|
+
{
|
|
166
|
+
"mcpServers": {
|
|
167
|
+
"skills-doctor": { "command": "npx", "args": ["-y", "opencode-skills-doctor", "mcp"] }
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Codex (`~/.codex/config.toml`):
|
|
173
|
+
|
|
174
|
+
```toml
|
|
175
|
+
[mcp_servers.skills-doctor]
|
|
176
|
+
command = "npx"
|
|
177
|
+
args = ["-y", "opencode-skills-doctor", "mcp"]
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Any other MCP client: run `npx -y opencode-skills-doctor mcp` and allow the `skills_doctor` tool.
|
|
181
|
+
|
|
182
|
+
It scans `.opencode/skills`, `.claude/skills` and `.agents/skills` (project and global), so it can audit skills used by other agents too.
|
|
183
|
+
|
|
158
184
|
## Rules
|
|
159
185
|
|
|
160
186
|
| Rule | Level | Meaning |
|
package/README.zh-CN.md
CHANGED
|
@@ -155,6 +155,32 @@ export { SkillsDoctorPlugin } from "opencode-skills-doctor/v1"
|
|
|
155
155
|
|
|
156
156
|
> 包根入口是 V2 插件,需要 OpenCode 2.x;CLI 两条线都能用。
|
|
157
157
|
|
|
158
|
+
## MCP server(Claude Code、Codex、Kimi Code、任何 MCP 客户端)
|
|
159
|
+
|
|
160
|
+
`skills-doctor mcp` 用 stdio 讲 Model Context Protocol,只暴露一个工具:`skills_doctor`,参数 `directory?`、`budget?`、`lang?`、`global?`。不需要装 OpenCode。
|
|
161
|
+
|
|
162
|
+
Claude Code(项目里的 `.mcp.json`,或你的 MCP 配置):
|
|
163
|
+
|
|
164
|
+
```json
|
|
165
|
+
{
|
|
166
|
+
"mcpServers": {
|
|
167
|
+
"skills-doctor": { "command": "npx", "args": ["-y", "opencode-skills-doctor", "mcp"] }
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Codex(`~/.codex/config.toml`):
|
|
173
|
+
|
|
174
|
+
```toml
|
|
175
|
+
[mcp_servers.skills-doctor]
|
|
176
|
+
command = "npx"
|
|
177
|
+
args = ["-y", "opencode-skills-doctor", "mcp"]
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
其他 MCP 客户端:启动 `npx -y opencode-skills-doctor mcp` 并允许 `skills_doctor` 工具即可。
|
|
181
|
+
|
|
182
|
+
它扫描 `.opencode/skills`、`.claude/skills`、`.agents/skills`(项目和全局),所以其他 agent 用的 skill 也能一起体检。
|
|
183
|
+
|
|
158
184
|
## 检测规则
|
|
159
185
|
|
|
160
186
|
| 规则 | 级别 | 含义 |
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
{
|
|
2
2
|
"name": "opencode-skills-doctor",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"description": "Audit, fix and update
|
|
3
|
+
"version": "0.3.6",
|
|
4
|
+
"description": "Audit, fix and update agent skills: per-step context cost, shadowed duplicates, safe archive, catalog sync. CLI, OpenCode plugin and MCP server (Claude Code / Codex / any MCP client).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": "./src/plugin.mjs",
|
|
@@ -40,6 +40,8 @@
|
|
|
40
40
|
"keywords": [
|
|
41
41
|
"opencode",
|
|
42
42
|
"skills",
|
|
43
|
+
"agent-skills",
|
|
44
|
+
"mcp",
|
|
43
45
|
"lint",
|
|
44
46
|
"fix",
|
|
45
47
|
"cli",
|
package/src/cli.mjs
CHANGED
|
@@ -7,10 +7,13 @@ import { inspectManaged } from "./managed.mjs"
|
|
|
7
7
|
import { planFixes, applyFixes, hideSkills } from "./fix.mjs"
|
|
8
8
|
import { auditSkills } from "./audit.mjs"
|
|
9
9
|
import { loadConfig } from "./config.mjs"
|
|
10
|
+
import { startMcpServer } from "./mcp.mjs"
|
|
10
11
|
import { padDisplay } from "./parse.mjs"
|
|
11
12
|
import { detectLang, normalizeLang, translator } from "./i18n.mjs"
|
|
12
13
|
|
|
13
|
-
const pkg = JSON.parse(
|
|
14
|
+
const pkg = JSON.parse(
|
|
15
|
+
fs.readFileSync(new URL("../package.json", import.meta.url), "utf8").replace(/^\uFEFF/, ""),
|
|
16
|
+
)
|
|
14
17
|
|
|
15
18
|
const color = process.stdout.isTTY && !process.env.NO_COLOR
|
|
16
19
|
const paint = (code, text) => (color ? `\x1b[${code}m${text}\x1b[0m` : text)
|
|
@@ -57,7 +60,7 @@ function parseArgs(argv) {
|
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
const args = [...argv]
|
|
60
|
-
const commands = new Set(["check", "scan", "update", "fix", "help"])
|
|
63
|
+
const commands = new Set(["check", "scan", "update", "fix", "mcp", "help"])
|
|
61
64
|
if (args[0] && commands.has(args[0])) {
|
|
62
65
|
options.command = args.shift()
|
|
63
66
|
}
|
|
@@ -425,6 +428,11 @@ export async function main(argv) {
|
|
|
425
428
|
return
|
|
426
429
|
}
|
|
427
430
|
|
|
431
|
+
if (options.command === "mcp") {
|
|
432
|
+
startMcpServer({ version: pkg.version })
|
|
433
|
+
return
|
|
434
|
+
}
|
|
435
|
+
|
|
428
436
|
if (options.command === "check") {
|
|
429
437
|
const audit = auditSkills({
|
|
430
438
|
cwd,
|
package/src/i18n.mjs
CHANGED
|
@@ -10,6 +10,7 @@ Usage:
|
|
|
10
10
|
skills-doctor scan [options] list every skill with its source and precedence
|
|
11
11
|
skills-doctor fix [options] archive shadowed definitions (safe structural fixes)
|
|
12
12
|
skills-doctor update [options] compare and download skills from HTTP catalogs
|
|
13
|
+
skills-doctor mcp run as an MCP server (Claude Code, Codex, any MCP client)
|
|
13
14
|
|
|
14
15
|
Options:
|
|
15
16
|
--root <dir> working directory (default: current directory)
|
|
@@ -151,6 +152,7 @@ Exit codes: 0 ok; 1 errors (and warnings with --strict); 2 bad arguments or runt
|
|
|
151
152
|
skills-doctor scan [选项] 列出所有 skill 及其来源/优先级
|
|
152
153
|
skills-doctor fix [选项] 归档被遮蔽的定义(安全的结构性修复)
|
|
153
154
|
skills-doctor update [选项] 对比并拉取 HTTP catalog 里的 skill
|
|
155
|
+
skills-doctor mcp 以 MCP server 方式运行(Claude Code、Codex 等任何 MCP 客户端)
|
|
154
156
|
|
|
155
157
|
选项:
|
|
156
158
|
--root <dir> 以该目录为工作目录(默认当前目录)
|
package/src/mcp.mjs
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { auditSkills, formatMarkdown } from "./audit.mjs"
|
|
2
|
+
import { normalizeLang, detectLang } from "./i18n.mjs"
|
|
3
|
+
|
|
4
|
+
export const PROTOCOL_VERSION = "2025-06-18"
|
|
5
|
+
export const SERVER_NAME = "skills-doctor"
|
|
6
|
+
export const TOOL_NAME = "skills_doctor"
|
|
7
|
+
|
|
8
|
+
export function toolDefinition() {
|
|
9
|
+
return {
|
|
10
|
+
name: TOOL_NAME,
|
|
11
|
+
description:
|
|
12
|
+
"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
|
+
inputSchema: {
|
|
14
|
+
type: "object",
|
|
15
|
+
properties: {
|
|
16
|
+
directory: {
|
|
17
|
+
type: "string",
|
|
18
|
+
description: "Directory to audit (default: the server process working directory)",
|
|
19
|
+
},
|
|
20
|
+
budget: {
|
|
21
|
+
type: "number",
|
|
22
|
+
description: "Description token budget per model step (default 1200)",
|
|
23
|
+
},
|
|
24
|
+
lang: {
|
|
25
|
+
type: "string",
|
|
26
|
+
enum: ["en", "zh"],
|
|
27
|
+
description: "Report language (default: auto from system locale)",
|
|
28
|
+
},
|
|
29
|
+
global: {
|
|
30
|
+
type: "boolean",
|
|
31
|
+
description: "Include global skill directories such as ~/.config/opencode and ~/.claude (default true)",
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
additionalProperties: false,
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function ok(id, result) {
|
|
40
|
+
return { jsonrpc: "2.0", id, result }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function fail(id, code, message) {
|
|
44
|
+
return { jsonrpc: "2.0", id, error: { code, message } }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 处理一条 JSON-RPC 消息;通知返回 null。 */
|
|
48
|
+
export async function handleMessage(message, { version = "0.0.0" } = {}) {
|
|
49
|
+
if (!message || typeof message !== "object" || typeof message.method !== "string") return null
|
|
50
|
+
const { id, method, params } = message
|
|
51
|
+
const isNotification = id === undefined || id === null
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
switch (method) {
|
|
55
|
+
case "initialize":
|
|
56
|
+
return ok(id, {
|
|
57
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
58
|
+
capabilities: { tools: { listChanged: false } },
|
|
59
|
+
serverInfo: { name: SERVER_NAME, version },
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
case "notifications/initialized":
|
|
63
|
+
case "notifications/cancelled":
|
|
64
|
+
return null
|
|
65
|
+
|
|
66
|
+
case "ping":
|
|
67
|
+
return ok(id, {})
|
|
68
|
+
|
|
69
|
+
case "tools/list":
|
|
70
|
+
return ok(id, { tools: [toolDefinition()] })
|
|
71
|
+
|
|
72
|
+
case "resources/list":
|
|
73
|
+
return ok(id, { resources: [] })
|
|
74
|
+
|
|
75
|
+
case "prompts/list":
|
|
76
|
+
return ok(id, { prompts: [] })
|
|
77
|
+
|
|
78
|
+
case "tools/call": {
|
|
79
|
+
const name = params?.name
|
|
80
|
+
if (name !== TOOL_NAME) return fail(id, -32602, `unknown tool: ${name}`)
|
|
81
|
+
|
|
82
|
+
const args = params?.arguments && typeof params.arguments === "object" ? params.arguments : {}
|
|
83
|
+
const directory =
|
|
84
|
+
typeof args.directory === "string" && args.directory.trim() ? args.directory.trim() : process.cwd()
|
|
85
|
+
const budget = typeof args.budget === "number" && args.budget > 0 ? args.budget : null
|
|
86
|
+
const lang = normalizeLang(args.lang) ?? detectLang()
|
|
87
|
+
|
|
88
|
+
const audit = auditSkills({ cwd: directory, budget, lang, includeGlobal: args.global !== false })
|
|
89
|
+
return ok(id, { content: [{ type: "text", text: formatMarkdown(audit) }] })
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
default:
|
|
93
|
+
if (isNotification) return null
|
|
94
|
+
return fail(id, -32601, `method not found: ${method}`)
|
|
95
|
+
}
|
|
96
|
+
} catch (err) {
|
|
97
|
+
if (isNotification) return null
|
|
98
|
+
return fail(id, -32603, String(err?.message ?? err))
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** stdio transport:newline-delimited JSON-RPC(MCP 标准)。 */
|
|
103
|
+
export function startMcpServer({ version, input = process.stdin, output = process.stdout, onExit = () => process.exit(0) } = {}) {
|
|
104
|
+
let buffer = ""
|
|
105
|
+
let queue = Promise.resolve()
|
|
106
|
+
|
|
107
|
+
const write = (payload) => {
|
|
108
|
+
output.write(JSON.stringify(payload) + "\n")
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const handleLine = (line) => {
|
|
112
|
+
let message
|
|
113
|
+
try {
|
|
114
|
+
message = JSON.parse(line)
|
|
115
|
+
} catch {
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
queue = queue
|
|
119
|
+
.then(() => handleMessage(message, { version }))
|
|
120
|
+
.then((response) => {
|
|
121
|
+
if (response) write(response)
|
|
122
|
+
})
|
|
123
|
+
.catch((err) => {
|
|
124
|
+
write(fail(message?.id ?? null, -32603, String(err?.message ?? err)))
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
input.setEncoding?.("utf8")
|
|
129
|
+
input.on("data", (chunk) => {
|
|
130
|
+
buffer += chunk
|
|
131
|
+
let index
|
|
132
|
+
while ((index = buffer.indexOf("\n")) >= 0) {
|
|
133
|
+
const line = buffer.slice(0, index).trim()
|
|
134
|
+
buffer = buffer.slice(index + 1)
|
|
135
|
+
if (line) handleLine(line)
|
|
136
|
+
}
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
let finished = false
|
|
140
|
+
const finish = () => {
|
|
141
|
+
if (finished) return
|
|
142
|
+
finished = true
|
|
143
|
+
// 等队列里的异步响应写完再退出,避免管道结束时丢包
|
|
144
|
+
queue.finally(onExit)
|
|
145
|
+
}
|
|
146
|
+
input.on("end", finish)
|
|
147
|
+
input.on("close", finish)
|
|
148
|
+
|
|
149
|
+
return { write, close: finish }
|
|
150
|
+
}
|