opencode-skills-doctor 0.3.10 → 0.3.11
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 +20 -0
- package/docs/README.zh-CN.md +20 -0
- package/package.json +1 -1
- package/src/cli.mjs +32 -1
- package/src/i18n.mjs +20 -0
- package/src/install.mjs +211 -0
- package/src/plugin-v1.mjs +5 -0
- package/src/plugin.mjs +7 -0
- package/src/warning.mjs +37 -0
package/README.md
CHANGED
|
@@ -157,6 +157,26 @@ It registers the same `skills_doctor` tool (1.x has no `/skills-doctor` command
|
|
|
157
157
|
|
|
158
158
|
> The package root entry is the V2 plugin and requires OpenCode 2.x. The CLI works on both lines.
|
|
159
159
|
|
|
160
|
+
### Deep integration (one command)
|
|
161
|
+
|
|
162
|
+
```powershell
|
|
163
|
+
npx opencode-skills-doctor install
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
The installer detects your OpenCode version and wires everything up:
|
|
167
|
+
|
|
168
|
+
- **2.x** — adds `"plugins": ["opencode-skills-doctor"]` to `opencode.json(c)`;
|
|
169
|
+
- **1.x** — installs the package into `~/.config/opencode`, writes the local plugin entry, adds a `/skills-doctor` command, sets `"type": "module"`;
|
|
170
|
+
- backs up the config file as `<config>.bak-<timestamp>` before editing.
|
|
171
|
+
|
|
172
|
+
Use `--dry-run` to preview, `--config-dir <dir>` for a custom config location, and `--no-npm` to skip the package install.
|
|
173
|
+
|
|
174
|
+
Once installed you get:
|
|
175
|
+
|
|
176
|
+
- the `skills_doctor` tool;
|
|
177
|
+
- the `/skills-doctor` command (1.x: generated markdown command; 2.x: plugin command);
|
|
178
|
+
- a one-time system-prompt warning when the skill description budget is exceeded.
|
|
179
|
+
|
|
160
180
|
## MCP server (Claude Code, Codex, Kimi Code, any MCP client)
|
|
161
181
|
|
|
162
182
|
`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.
|
package/docs/README.zh-CN.md
CHANGED
|
@@ -157,6 +157,26 @@ export { SkillsDoctorPlugin } from "opencode-skills-doctor/v1"
|
|
|
157
157
|
|
|
158
158
|
> 包根入口是 V2 插件,需要 OpenCode 2.x;CLI 两条线都能用。
|
|
159
159
|
|
|
160
|
+
### 一键深度集成
|
|
161
|
+
|
|
162
|
+
```powershell
|
|
163
|
+
npx opencode-skills-doctor install
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
安装器会自动识别 OpenCode 版本并接好:
|
|
167
|
+
|
|
168
|
+
- **2.x** —— 在 `opencode.json(c)` 里加 `"plugins": ["opencode-skills-doctor"]`;
|
|
169
|
+
- **1.x** —— 把包装进 `~/.config/opencode`、写本地插件入口、生成 `/skills-doctor` 命令、补 `"type": "module"`;
|
|
170
|
+
- 改配置前会备份为 `<config>.bak-<timestamp>`。
|
|
171
|
+
|
|
172
|
+
支持 `--dry-run` 预览、`--config-dir <dir>` 指定配置目录、`--no-npm` 跳过装包。
|
|
173
|
+
|
|
174
|
+
装好后你会得到:
|
|
175
|
+
|
|
176
|
+
- `skills_doctor` 工具;
|
|
177
|
+
- `/skills-doctor` 命令(1.x 是生成的 markdown 命令,2.x 是插件命令);
|
|
178
|
+
- 当 skill 描述预算超标时,系统提示里会注入一次提醒(每个会话最多一次)。
|
|
179
|
+
|
|
160
180
|
## MCP server(Claude Code、Codex、Kimi Code、任何 MCP 客户端)
|
|
161
181
|
|
|
162
182
|
`skills-doctor mcp` 用 stdio 讲 Model Context Protocol,只暴露一个工具:`skills_doctor`,参数 `directory?`、`budget?`、`lang?`、`global?`。不需要装 OpenCode。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-skills-doctor",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.11",
|
|
4
4
|
"description": "Audit, fix and update agent skills: per-step context cost, shadowed duplicates, safe archive, catalog sync. CLI, OpenCode plugin and MCP server (Claude Code / Codex / any MCP client).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
package/src/cli.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import { planFixes, applyFixes, hideSkills } from "./fix.mjs"
|
|
|
8
8
|
import { auditSkills } from "./audit.mjs"
|
|
9
9
|
import { loadConfig } from "./config.mjs"
|
|
10
10
|
import { startMcpServer } from "./mcp.mjs"
|
|
11
|
+
import { installOpenCode } from "./install.mjs"
|
|
11
12
|
import { padDisplay } from "./parse.mjs"
|
|
12
13
|
import { detectLang, normalizeLang, translator } from "./i18n.mjs"
|
|
13
14
|
|
|
@@ -56,12 +57,14 @@ function parseArgs(argv) {
|
|
|
56
57
|
into: null,
|
|
57
58
|
checkOnly: false,
|
|
58
59
|
prune: false,
|
|
60
|
+
configDir: null,
|
|
61
|
+
noNpm: false,
|
|
59
62
|
help: false,
|
|
60
63
|
version: false,
|
|
61
64
|
}
|
|
62
65
|
|
|
63
66
|
const args = [...argv]
|
|
64
|
-
const commands = new Set(["check", "scan", "update", "fix", "mcp", "help"])
|
|
67
|
+
const commands = new Set(["check", "scan", "update", "fix", "install", "mcp", "help"])
|
|
65
68
|
if (args[0] && commands.has(args[0])) {
|
|
66
69
|
options.command = args.shift()
|
|
67
70
|
}
|
|
@@ -116,6 +119,12 @@ function parseArgs(argv) {
|
|
|
116
119
|
case "--prune":
|
|
117
120
|
options.prune = true
|
|
118
121
|
break
|
|
122
|
+
case "--config-dir":
|
|
123
|
+
options.configDir = path.resolve(args[++i] ?? ".")
|
|
124
|
+
break
|
|
125
|
+
case "--no-npm":
|
|
126
|
+
options.noNpm = true
|
|
127
|
+
break
|
|
119
128
|
case "-h":
|
|
120
129
|
case "--help":
|
|
121
130
|
options.help = true
|
|
@@ -444,6 +453,28 @@ export async function main(argv) {
|
|
|
444
453
|
return
|
|
445
454
|
}
|
|
446
455
|
|
|
456
|
+
if (options.command === "install") {
|
|
457
|
+
const result = installOpenCode({
|
|
458
|
+
configDir: options.configDir ?? undefined,
|
|
459
|
+
dryRun: options.dryRun,
|
|
460
|
+
installPackage: !options.noNpm,
|
|
461
|
+
})
|
|
462
|
+
if (options.json) {
|
|
463
|
+
console.log(JSON.stringify(result, null, 2))
|
|
464
|
+
} else {
|
|
465
|
+
console.log(bold(t("install.title")))
|
|
466
|
+
console.log(dim(t("install.version", { version: result.version ?? "unknown" })))
|
|
467
|
+
console.log(dim(t("install.configDir", { dir: result.configDir })))
|
|
468
|
+
if (result.dryRun) console.log(dim(t("install.dryRun")))
|
|
469
|
+
for (const action of result.actions) console.log(` · ${action}`)
|
|
470
|
+
for (const error of result.errors) console.log(red(t("install.error", { error })))
|
|
471
|
+
console.log("")
|
|
472
|
+
console.log(result.errors.length ? red(t("install.failed")) : dim(t("install.next")))
|
|
473
|
+
}
|
|
474
|
+
process.exitCode = result.errors.length ? 1 : 0
|
|
475
|
+
return
|
|
476
|
+
}
|
|
477
|
+
|
|
447
478
|
if (options.command === "check") {
|
|
448
479
|
const audit = auditSkills({
|
|
449
480
|
cwd,
|
package/src/i18n.mjs
CHANGED
|
@@ -10,6 +10,7 @@ Usage:
|
|
|
10
10
|
skills-doctor scan [options] list every skill with its source and precedence
|
|
11
11
|
skills-doctor fix [options] archive shadowed definitions (safe structural fixes)
|
|
12
12
|
skills-doctor update [options] compare and download skills from HTTP catalogs
|
|
13
|
+
skills-doctor install [options] wire the plugin into OpenCode (1.x / 2.x)
|
|
13
14
|
skills-doctor mcp run as an MCP server (Claude Code, Codex, any MCP client)
|
|
14
15
|
|
|
15
16
|
Options:
|
|
@@ -27,6 +28,8 @@ Options:
|
|
|
27
28
|
--into <dir> update: output directory (default: .opencode/skills)
|
|
28
29
|
--check-only update: only report available updates
|
|
29
30
|
--prune update: delete skills that disappeared from the catalog
|
|
31
|
+
--config-dir <dir> install: OpenCode config directory (default: ~/.config/opencode)
|
|
32
|
+
--no-npm install: do not run npm install (1.x)
|
|
30
33
|
-h, --help show this help
|
|
31
34
|
-v, --version print version
|
|
32
35
|
|
|
@@ -74,6 +77,13 @@ Exit codes: 0 ok; 1 errors (and warnings with --strict); 2 bad arguments or runt
|
|
|
74
77
|
"update.pruned": " · pruned {count} skills no longer in the catalog: {names}",
|
|
75
78
|
"update.orphaned": " ! {count} skills are no longer in the catalog (use --prune to delete them)",
|
|
76
79
|
"update.lock": " lock: {path} ({name})",
|
|
80
|
+
"install.title": "OpenCode integration",
|
|
81
|
+
"install.version": " OpenCode: {version}",
|
|
82
|
+
"install.configDir": " config dir: {dir}",
|
|
83
|
+
"install.dryRun": " (dry run: nothing is written)",
|
|
84
|
+
"install.error": " ✗ {error}",
|
|
85
|
+
"install.failed": "Integration failed.",
|
|
86
|
+
"install.next": "Restart OpenCode to activate.",
|
|
77
87
|
"update.lockTooNew": " ! lock is newer than this tool; parsed in compatibility mode (unknown fields ignored)",
|
|
78
88
|
"update.detector": " check now sees these skills ({dir} is where opencode reads)",
|
|
79
89
|
"update.noCatalogs": "No HTTP catalog configured. Add https://... to the skills array in opencode.json(c).",
|
|
@@ -156,6 +166,7 @@ Exit codes: 0 ok; 1 errors (and warnings with --strict); 2 bad arguments or runt
|
|
|
156
166
|
skills-doctor scan [选项] 列出所有 skill 及其来源/优先级
|
|
157
167
|
skills-doctor fix [选项] 归档被遮蔽的定义(安全的结构性修复)
|
|
158
168
|
skills-doctor update [选项] 对比并拉取 HTTP catalog 里的 skill
|
|
169
|
+
skills-doctor install [选项] 把插件接入 OpenCode(1.x / 2.x)
|
|
159
170
|
skills-doctor mcp 以 MCP server 方式运行(Claude Code、Codex 等任何 MCP 客户端)
|
|
160
171
|
|
|
161
172
|
选项:
|
|
@@ -173,6 +184,8 @@ Exit codes: 0 ok; 1 errors (and warnings with --strict); 2 bad arguments or runt
|
|
|
173
184
|
--into <dir> update 的落盘目录(默认 .opencode/skills)
|
|
174
185
|
--check-only update 只报告可用更新,不下载
|
|
175
186
|
--prune update: 删除 catalog 中已移除的 skill
|
|
187
|
+
--config-dir <dir> install: OpenCode 配置目录(默认 ~/.config/opencode)
|
|
188
|
+
--no-npm install: 不执行 npm install(1.x)
|
|
176
189
|
-h, --help 帮助
|
|
177
190
|
-v, --version 版本
|
|
178
191
|
|
|
@@ -220,6 +233,13 @@ Exit codes: 0 ok; 1 errors (and warnings with --strict); 2 bad arguments or runt
|
|
|
220
233
|
"update.pruned": " · 清理 catalog 中已移除的 skill {count} 个: {names}",
|
|
221
234
|
"update.orphaned": " ! {count} 个 skill 已不在 catalog 中(加 --prune 可删除本地残留)",
|
|
222
235
|
"update.lock": " lock: {path}({name})",
|
|
236
|
+
"install.title": "OpenCode 集成",
|
|
237
|
+
"install.version": " OpenCode: {version}",
|
|
238
|
+
"install.configDir": " 配置目录: {dir}",
|
|
239
|
+
"install.dryRun": " (dry run:不会写入任何内容)",
|
|
240
|
+
"install.error": " ✗ {error}",
|
|
241
|
+
"install.failed": "集成失败。",
|
|
242
|
+
"install.next": "重启 OpenCode 生效。",
|
|
223
243
|
"update.lockTooNew": " ! lock 版本比本工具新,已按兼容模式解析(未知字段忽略)",
|
|
224
244
|
"update.detector": " check 现在可以直接检测到这些 skill({dir} 是 opencode 会读取的目录)",
|
|
225
245
|
"update.noCatalogs": "没有配置 HTTP catalog。在 opencode.json(c) 的 skills 数组里写 https://... 即可。",
|
package/src/install.mjs
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import fs from "node:fs"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import os from "node:os"
|
|
4
|
+
import { spawnSync, execFileSync } from "node:child_process"
|
|
5
|
+
import { stripJsonc } from "./discover.mjs"
|
|
6
|
+
|
|
7
|
+
export const PACKAGE_NAME = "opencode-skills-doctor"
|
|
8
|
+
const PLUGIN_FILE = "skills-doctor.js"
|
|
9
|
+
const COMMAND_FILE = "skills-doctor.md"
|
|
10
|
+
|
|
11
|
+
const V1_PLUGIN_SOURCE = `// Generated by skills-doctor install\n// OpenCode 1.x plugin entry\nexport { SkillsDoctorPlugin } from "opencode-skills-doctor/v1"\n`
|
|
12
|
+
|
|
13
|
+
const V1_COMMAND = `---
|
|
14
|
+
description: Audit local skills with skills-doctor
|
|
15
|
+
---
|
|
16
|
+
Use the skills_doctor tool to audit the skills in this project. Summarize the top findings and propose concrete fixes (archive shadowed duplicates, hide low-frequency skills). $ARGUMENTS
|
|
17
|
+
`
|
|
18
|
+
|
|
19
|
+
export function defaultConfigDir() {
|
|
20
|
+
const base = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config")
|
|
21
|
+
return path.join(base, "opencode")
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function readJsonc(file) {
|
|
25
|
+
return JSON.parse(stripJsonc(fs.readFileSync(file, "utf8").replace(/^\uFEFF/, "")))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function writeJson(file, data) {
|
|
29
|
+
fs.writeFileSync(file, JSON.stringify(data, null, 2) + "\n", "utf8")
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function readText(file) {
|
|
33
|
+
try {
|
|
34
|
+
return fs.readFileSync(file, "utf8").replace(/^\uFEFF/, "")
|
|
35
|
+
} catch {
|
|
36
|
+
return null
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function detectVersion() {
|
|
41
|
+
try {
|
|
42
|
+
const out = execFileSync("opencode", ["--version"], { encoding: "utf8", timeout: 15000 })
|
|
43
|
+
const match = /(\d+)\.(\d+)\.(\d+)/.exec(String(out))
|
|
44
|
+
if (match) return match[0]
|
|
45
|
+
} catch {}
|
|
46
|
+
return null
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function configFile(dir) {
|
|
50
|
+
const candidates = ["opencode.jsonc", "opencode.json"]
|
|
51
|
+
.map((name) => path.join(dir, name))
|
|
52
|
+
.filter((file) => fs.existsSync(file))
|
|
53
|
+
|
|
54
|
+
// 优先沿用已经在配置 plugin/plugins 的文件,避免把插件写进另一个配置文件后被覆盖
|
|
55
|
+
for (const file of candidates) {
|
|
56
|
+
try {
|
|
57
|
+
const config = readJsonc(file)
|
|
58
|
+
if (config.plugin !== undefined || config.plugins !== undefined) return file
|
|
59
|
+
} catch {}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return candidates[0] ?? path.join(dir, "opencode.json")
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function backup(file, stamp) {
|
|
66
|
+
if (!fs.existsSync(file)) return null
|
|
67
|
+
const target = `${file}.bak-${stamp}`
|
|
68
|
+
fs.copyFileSync(file, target)
|
|
69
|
+
return target
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function ensureUniqueEntry(list, entry) {
|
|
73
|
+
if (!Array.isArray(list)) return [entry]
|
|
74
|
+
return list.includes(entry) ? list : [...list, entry]
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function installPackage(dir) {
|
|
78
|
+
const npm = process.platform === "win32" ? "npm.cmd" : "npm"
|
|
79
|
+
const result = spawnSync(npm, ["install", "--prefix", dir, PACKAGE_NAME + "@latest", "--no-audit", "--no-fund"], {
|
|
80
|
+
stdio: "inherit",
|
|
81
|
+
env: process.env,
|
|
82
|
+
timeout: 180000,
|
|
83
|
+
})
|
|
84
|
+
return result.status === 0
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 把 skills-doctor 装进 OpenCode:
|
|
89
|
+
* - 2.x:写入 plugins 数组(OpenCode 自己从 npm 拉包)
|
|
90
|
+
* - 1.x:在配置目录安装包、写本地插件入口、写 /skills-doctor 命令、补插件数组
|
|
91
|
+
*/
|
|
92
|
+
export function installOpenCode({ configDir = defaultConfigDir(), version = null, dryRun = false, installPackage: withPackage = true } = {}) {
|
|
93
|
+
const dir = path.resolve(configDir)
|
|
94
|
+
const actions = []
|
|
95
|
+
const errors = []
|
|
96
|
+
const dry = (message) => actions.push(`[dry-run] ${message}`)
|
|
97
|
+
const act = (message) => actions.push(message)
|
|
98
|
+
|
|
99
|
+
const cfgFile = configFile(dir)
|
|
100
|
+
const existingText = readText(cfgFile)
|
|
101
|
+
let config = {}
|
|
102
|
+
if (existingText !== null) {
|
|
103
|
+
try {
|
|
104
|
+
config = readJsonc(cfgFile)
|
|
105
|
+
} catch (err) {
|
|
106
|
+
errors.push(`cannot parse ${cfgFile}: ${err.message}`)
|
|
107
|
+
return { version, major: null, configDir: dir, configFile: cfgFile, actions, errors, dryRun }
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const resolvedVersion = version ?? detectVersion()
|
|
112
|
+
const inferredMajor = config.plugin !== undefined ? 1 : 2
|
|
113
|
+
const major = resolvedVersion ? Number(resolvedVersion.split(".")[0]) : inferredMajor
|
|
114
|
+
|
|
115
|
+
if (dryRun) dry(`config dir: ${dir}`)
|
|
116
|
+
if (!fs.existsSync(dir)) {
|
|
117
|
+
if (!dryRun) fs.mkdirSync(dir, { recursive: true })
|
|
118
|
+
act(`created config dir ${dir}`)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-")
|
|
122
|
+
|
|
123
|
+
if (major >= 2) {
|
|
124
|
+
const next = { ...config, plugins: ensureUniqueEntry(config.plugins, PACKAGE_NAME) }
|
|
125
|
+
const changed = !Array.isArray(config.plugins) || !config.plugins.includes(PACKAGE_NAME)
|
|
126
|
+
if (changed) {
|
|
127
|
+
if (!dryRun) {
|
|
128
|
+
const backupFile = backup(cfgFile, stamp)
|
|
129
|
+
if (backupFile) act(`backup ${backupFile}`)
|
|
130
|
+
writeJson(cfgFile, next)
|
|
131
|
+
act(`added "${PACKAGE_NAME}" to plugins in ${cfgFile}`)
|
|
132
|
+
} else dry(`add "${PACKAGE_NAME}" to plugins in ${cfgFile}`)
|
|
133
|
+
} else {
|
|
134
|
+
act(`plugins already contains "${PACKAGE_NAME}"`)
|
|
135
|
+
}
|
|
136
|
+
act("restart OpenCode; the /skills-doctor command and skills_doctor tool will be available")
|
|
137
|
+
return { version: resolvedVersion, major, configDir: dir, configFile: cfgFile, actions, errors, dryRun }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// 1.x
|
|
141
|
+
const pluginFile = path.join(dir, "plugins", PLUGIN_FILE)
|
|
142
|
+
const commandFile = path.join(dir, "command", COMMAND_FILE)
|
|
143
|
+
const pluginPath = pluginFile
|
|
144
|
+
|
|
145
|
+
const hasPackage = fs.existsSync(path.join(dir, "node_modules", PACKAGE_NAME, "package.json"))
|
|
146
|
+
if (!hasPackage) {
|
|
147
|
+
if (withPackage && !dryRun) {
|
|
148
|
+
act(`installing ${PACKAGE_NAME} into ${dir}`)
|
|
149
|
+
if (!installPackage(dir)) errors.push(`npm install failed for ${PACKAGE_NAME}`)
|
|
150
|
+
else act(`installed ${PACKAGE_NAME}`)
|
|
151
|
+
} else if (dryRun) {
|
|
152
|
+
dry(`install ${PACKAGE_NAME} into ${dir}`)
|
|
153
|
+
} else {
|
|
154
|
+
act(`skipped npm install; run: npm i --prefix "${dir}" ${PACKAGE_NAME}`)
|
|
155
|
+
}
|
|
156
|
+
} else {
|
|
157
|
+
act(`${PACKAGE_NAME} already installed in ${dir}`)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (!fs.existsSync(pluginFile)) {
|
|
161
|
+
if (!dryRun) {
|
|
162
|
+
fs.mkdirSync(path.dirname(pluginFile), { recursive: true })
|
|
163
|
+
fs.writeFileSync(pluginFile, V1_PLUGIN_SOURCE, "utf8")
|
|
164
|
+
act(`wrote plugin entry ${pluginFile}`)
|
|
165
|
+
} else dry(`write plugin entry ${pluginFile}`)
|
|
166
|
+
} else {
|
|
167
|
+
act(`plugin entry already exists ${pluginFile}`)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (!fs.existsSync(commandFile)) {
|
|
171
|
+
if (!dryRun) {
|
|
172
|
+
fs.mkdirSync(path.dirname(commandFile), { recursive: true })
|
|
173
|
+
fs.writeFileSync(commandFile, V1_COMMAND, "utf8")
|
|
174
|
+
act(`wrote command ${commandFile}`)
|
|
175
|
+
} else dry(`write command ${commandFile}`)
|
|
176
|
+
} else {
|
|
177
|
+
act(`command already exists ${commandFile}`)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const nextPlugin = ensureUniqueEntry(config.plugin, pluginPath)
|
|
181
|
+
const pluginChanged = !Array.isArray(config.plugin) || !config.plugin.includes(pluginPath)
|
|
182
|
+
if (pluginChanged) {
|
|
183
|
+
if (!dryRun) {
|
|
184
|
+
const backupFile = backup(cfgFile, stamp)
|
|
185
|
+
if (backupFile) act(`backup ${backupFile}`)
|
|
186
|
+
writeJson(cfgFile, { ...config, plugin: nextPlugin })
|
|
187
|
+
act(`added plugin entry to ${cfgFile}`)
|
|
188
|
+
} else dry(`add plugin entry to ${cfgFile}`)
|
|
189
|
+
} else {
|
|
190
|
+
act("plugin array already configured")
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const pkgFile = path.join(dir, "package.json")
|
|
194
|
+
const pkgText = readText(pkgFile)
|
|
195
|
+
let pkg = {}
|
|
196
|
+
if (pkgText !== null) {
|
|
197
|
+
try {
|
|
198
|
+
pkg = JSON.parse(pkgText)
|
|
199
|
+
} catch {}
|
|
200
|
+
}
|
|
201
|
+
if (pkg.type !== "module") {
|
|
202
|
+
if (!dryRun) {
|
|
203
|
+
pkg.type = "module"
|
|
204
|
+
fs.writeFileSync(pkgFile, JSON.stringify(pkg, null, 2) + "\n", "utf8")
|
|
205
|
+
act(`set "type": "module" in ${pkgFile}`)
|
|
206
|
+
} else dry(`set "type": "module" in ${pkgFile}`)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
act("restart OpenCode; the skills_doctor tool and /skills-doctor command will be available")
|
|
210
|
+
return { version: resolvedVersion, major, configDir: dir, configFile: cfgFile, actions, errors, dryRun }
|
|
211
|
+
}
|
package/src/plugin-v1.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { tool } from "@opencode-ai/plugin"
|
|
2
2
|
import { auditSkills, formatMarkdown } from "./audit.mjs"
|
|
3
|
+
import { budgetWarning } from "./warning.mjs"
|
|
3
4
|
|
|
4
5
|
const z = tool.schema
|
|
5
6
|
|
|
@@ -25,6 +26,10 @@ export const SkillsDoctorPlugin = async ({ directory }) => ({
|
|
|
25
26
|
},
|
|
26
27
|
}),
|
|
27
28
|
},
|
|
29
|
+
"experimental.chat.system.transform": async (input, output) => {
|
|
30
|
+
const warning = budgetWarning({ cwd: directory, sessionID: input?.sessionID })
|
|
31
|
+
if (warning) output.system.push(warning)
|
|
32
|
+
},
|
|
28
33
|
})
|
|
29
34
|
|
|
30
35
|
export const server = SkillsDoctorPlugin
|
package/src/plugin.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Plugin } from "@opencode/plugin"
|
|
2
2
|
import { auditSkills, formatMarkdown } from "./audit.mjs"
|
|
3
|
+
import { budgetWarning } from "./warning.mjs"
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* OpenCode 插件入口:把 skills-doctor 暴露成 skills_doctor 工具和 /skills-doctor 命令。
|
|
@@ -48,5 +49,11 @@ export default Plugin.define({
|
|
|
48
49
|
},
|
|
49
50
|
})
|
|
50
51
|
})
|
|
52
|
+
|
|
53
|
+
// 每个会话最多注入一次预算预警,避免刷屏
|
|
54
|
+
await ctx.session.hook("context", (event) => {
|
|
55
|
+
const warning = budgetWarning({ cwd: ctx.location.directory, sessionID: event.sessionID })
|
|
56
|
+
if (warning) event.system.push({ type: "text", text: warning })
|
|
57
|
+
})
|
|
51
58
|
},
|
|
52
59
|
})
|
package/src/warning.mjs
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { auditSkills } from "./audit.mjs"
|
|
2
|
+
|
|
3
|
+
const cache = new Map()
|
|
4
|
+
const warned = new Set()
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 每个会话最多注入一次「skill 预算超标」提醒。
|
|
8
|
+
* 只在存在 error(默认预算超标 2 倍)时提醒,避免刷屏。
|
|
9
|
+
*/
|
|
10
|
+
export function budgetWarning({ cwd = process.cwd(), sessionID = "global", ttlMs = 60000, includeGlobal = true } = {}) {
|
|
11
|
+
const key = `${cwd}::${sessionID}`
|
|
12
|
+
if (warned.has(key)) return null
|
|
13
|
+
|
|
14
|
+
const now = Date.now()
|
|
15
|
+
let entry = cache.get(cwd)
|
|
16
|
+
if (!entry || now - entry.at > ttlMs) {
|
|
17
|
+
const audit = auditSkills({ cwd, includeGlobal })
|
|
18
|
+
const over = audit.counts.errors > 0
|
|
19
|
+
entry = {
|
|
20
|
+
at: now,
|
|
21
|
+
text: over
|
|
22
|
+
? `[skills-doctor] ${audit.summary.unique} skills use ~${audit.summary.advertisedTokens} tokens of descriptions per step (budget ${audit.budget}); ${audit.counts.errors} error(s), ${audit.counts.warnings} warning(s). Use the skills_doctor tool for details, then hide low-frequency skills with \`skills-doctor fix --hide <ids>\`.`
|
|
23
|
+
: null,
|
|
24
|
+
}
|
|
25
|
+
cache.set(cwd, entry)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!entry.text) return null
|
|
29
|
+
warned.add(key)
|
|
30
|
+
return entry.text
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 测试用:清空缓存与已提醒集合。 */
|
|
34
|
+
export function resetWarningState() {
|
|
35
|
+
cache.clear()
|
|
36
|
+
warned.clear()
|
|
37
|
+
}
|