ai-optimize-master 0.0.0-stage → 0.3.2
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/.codex-plugin/plugin.json +35 -0
- package/.mcp.json +11 -0
- package/LICENSE +21 -0
- package/README.md +97 -2
- package/README.zh-CN.md +104 -0
- package/SECURITY.md +89 -0
- package/bin/dsh-optimizer.mjs +205 -0
- package/cordis.patch.yml +3 -0
- package/index.js +90 -0
- package/lib/apply.mjs +264 -0
- package/lib/archive.mjs +130 -0
- package/lib/evolve.mjs +128 -0
- package/lib/manifest.mjs +63 -0
- package/lib/scan.mjs +436 -0
- package/lib/util.mjs +130 -0
- package/mcp-server.mjs +65 -0
- package/package.json +82 -4
- package/skill/SKILL.md +45 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ai-optimize-master",
|
|
3
|
+
"version": "0.3.2",
|
|
4
|
+
"description": "AI 优化大师 — audit duplicate, broken and stale AI-tool skills, propose improvements, archive reversibly.",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "2Origin",
|
|
7
|
+
"url": "https://github.com/dongsheng123132"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/dongsheng123132/ai-optimize-master",
|
|
10
|
+
"repository": "https://github.com/dongsheng123132/ai-optimize-master",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"keywords": [
|
|
13
|
+
"ai-optimize-master",
|
|
14
|
+
"deepseek-harness",
|
|
15
|
+
"skill-management",
|
|
16
|
+
"mcp",
|
|
17
|
+
"cleanup"
|
|
18
|
+
],
|
|
19
|
+
"mcpServers": "./.mcp.json",
|
|
20
|
+
"interface": {
|
|
21
|
+
"displayName": "AI 优化大师",
|
|
22
|
+
"shortDescription": "Audit duplicate, broken and stale AI-tool skills; propose improvements; clean up reversibly.",
|
|
23
|
+
"longDescription": "Scans the skill roots of every AI tool on this machine, reports byte-identical duplicates, diverged copies, dangling local references and unreadable roots, surfaces MCP declarations without executing any server, and proposes improvements as diffs. Cleanup is opt-in per skill: a hash-bound plan, a journal, and byte-exact undo.",
|
|
24
|
+
"developerName": "2Origin",
|
|
25
|
+
"category": "Developer Tools",
|
|
26
|
+
"capabilities": [
|
|
27
|
+
"Read",
|
|
28
|
+
"Audit",
|
|
29
|
+
"Reversible cleanup"
|
|
30
|
+
],
|
|
31
|
+
"defaultPrompt": [
|
|
32
|
+
"扫描我的 AI 工具技能目录,告诉我哪些重复、断链、该升级。"
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
}
|
package/.mcp.json
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 火火的(深圳)科技有限公司 / 2Origin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,98 @@
|
|
|
1
|
-
#
|
|
1
|
+
# AI Optimize Master · know where every skill came from, review the change, restore the config
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> For people who run Claude Code, Codex, Cursor, DSH, OpenClaw and Hermes side by side:
|
|
4
|
+
> how many copies of that skill do you have? Which ones point at files that no longer
|
|
5
|
+
> exist? How many places declare MCP servers?
|
|
6
|
+
>
|
|
7
|
+
> This version does one thing: **establish the facts, then clean up reversibly.**
|
|
8
|
+
|
|
9
|
+
MIT licensed · no account · no network calls.
|
|
10
|
+
|
|
11
|
+
## What it is
|
|
12
|
+
|
|
13
|
+
One governance core with three surfaces:
|
|
14
|
+
|
|
15
|
+
| Step | What it does | Writes to your disk? |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| `scan` | Inventories each AI tool's skill roots, duplicate definitions, dangling references, MCP declarations, tool versions | **Never** |
|
|
18
|
+
| `plan` | Turns your explicit selections into a reviewable plan with a `planHash` | No |
|
|
19
|
+
| `apply` | Executes low-risk archive steps only; requires the exact hash; journals each step | Move only, reversible |
|
|
20
|
+
| `undo` | Restores from the journal in reverse order | Refuses to clobber later edits |
|
|
21
|
+
|
|
22
|
+
## Three real scenarios
|
|
23
|
+
|
|
24
|
+
1. **A Windows workstation with 14 AI tools × 28 skills each** (≈392 copies). `scan`
|
|
25
|
+
separates "same name, same content" from "same name, different content" and never
|
|
26
|
+
merges those into one conclusion.
|
|
27
|
+
2. **A skill tells the agent to open `scripts/run.mjs`, but the file is gone.** `scan`
|
|
28
|
+
reports `skills.broken-references` with the exact missing paths.
|
|
29
|
+
3. **Cleanup broke a tool.** `undo` restores from the journal; if you edited the file
|
|
30
|
+
after apply, it reports a conflict instead of overwriting.
|
|
31
|
+
|
|
32
|
+
## First run (three commands)
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npx ai-optimize-master scan # read-only inventory
|
|
36
|
+
npx ai-optimize-master evolve # improvement proposals (diffs only)
|
|
37
|
+
npx ai-optimize-master plan --select <skill> --out plan.json # reviewable plan
|
|
38
|
+
npx ai-optimize-master apply --plan plan.json --expect <planHash> # requires the hash
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
As a DSH plugin, the tools are `dsh_optimizer_scan|plan|apply|undo`. The MCP surface
|
|
42
|
+
(`node mcp-server.mjs`, stdio) exposes **only** `optimizer_scan` and `optimizer_plan`.
|
|
43
|
+
|
|
44
|
+
## What it proves
|
|
45
|
+
|
|
46
|
+
Verification is `npm test`, not prose. 23 contract tests:
|
|
47
|
+
|
|
48
|
+
| Id | Defends against |
|
|
49
|
+
|---|---|
|
|
50
|
+
| O1 | "Just checking" mutating the machine |
|
|
51
|
+
| O2 | An unreadable root being reported as clean |
|
|
52
|
+
| O3 | A binary on PATH that crashes being reported as installed |
|
|
53
|
+
| O4 | Apply without reading the plan (exit 4) |
|
|
54
|
+
| O5 | A plan changed after review (exit 3) |
|
|
55
|
+
| O6 | Journal completeness and byte-exact undo |
|
|
56
|
+
| O7 | Admin steps silently skipped yet journaled |
|
|
57
|
+
| O8 | Tests polluting the real machine |
|
|
58
|
+
| O9 | `bytes/4` presented as a measured token count |
|
|
59
|
+
| O10 | Plan hashes drifting with object key order |
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
npm test # 16 tests
|
|
63
|
+
npm run check # structure, read-only scan, MCP write surface absent
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Before you start
|
|
67
|
+
|
|
68
|
+
- **Does it delete?** No. `apply` moves directories into `$DSH_OPTIMIZER_HOME/archive/`.
|
|
69
|
+
- **Does it edit MCP config?** No. MCP is read-only in v0.2.
|
|
70
|
+
- **Does it upgrade Node or PowerShell?** No. Versions are advisory.
|
|
71
|
+
- **Can I delete duplicates immediately?** Identical content only proves duplicate
|
|
72
|
+
definition, not that a copy is unused. Single-copy skills are `medium` risk and are
|
|
73
|
+
not executed by default.
|
|
74
|
+
- **Cross-platform?** Yes. Extra Windows roots are declared through the
|
|
75
|
+
`DSH_OPTIMIZER_ROOTS` environment variable; no drive letters are guessed.
|
|
76
|
+
- **Cost?** MIT, offline, no credentials.
|
|
77
|
+
|
|
78
|
+
## Evidence
|
|
79
|
+
|
|
80
|
+
- Source: this repository, `main`
|
|
81
|
+
- Verification: `npm test` (16) and `npm run check`
|
|
82
|
+
- Boundaries: [`SECURITY.md`](SECURITY.md)
|
|
83
|
+
- Last reviewed: 2026-11-06
|
|
84
|
+
|
|
85
|
+
## Recognised skill roots
|
|
86
|
+
|
|
87
|
+
22 known locations are probed by default (DSH / agents / Claude / Codex / Cursor / OpenClaw / U-King / Hermes / OpenCode / Pi / Gemini / Qwen / Trae / Windsurf / Qoder / CodeBuddy / CommandCode / Grok / iFlow / Lingma / ZCode / the agents canonical source). Missing directories are skipped, never created and never reported as errors. Extra shared trees (e.g. a Windows workstation) are added with `DSH_OPTIMIZER_ROOTS`.
|
|
88
|
+
|
|
89
|
+
## Support matrix
|
|
90
|
+
|
|
91
|
+
| Capability | Windows | macOS | Linux |
|
|
92
|
+
|---|---|---|---|
|
|
93
|
+
| Skill inventory / duplicates / broken refs | ✅ | ✅ | ✅ |
|
|
94
|
+
| Read-only MCP declaration inventory | ✅ | ✅ | ✅ |
|
|
95
|
+
| Tool version probes (node/git/pwsh/claude/codex) | ✅ | ✅ | ✅ |
|
|
96
|
+
| Reversible archive | ✅ | ✅ | ✅ |
|
|
97
|
+
| Version upgrades / MCP uninstall | ❌ advisory only | ❌ advisory only | ❌ advisory only |
|
|
98
|
+
| Starting MCP servers / executing skill scripts | ❌ out of scope | ❌ out of scope | ❌ out of scope |
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# AI 优化大师 · AI 工具越装越多,也能查清来源、审阅变更、恢复配置
|
|
2
|
+
|
|
3
|
+
> 给同时装了 Claude Code、Codex、Cursor、DSH、OpenClaw、Hermes 的人:
|
|
4
|
+
> 你的 skill 被复制了几份?哪些指向了不存在的脚本?MCP 配了几处?
|
|
5
|
+
> 这一版只做一件事:**先把事实查清,再用可回滚的方式收干净。**
|
|
6
|
+
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
|
|
9
|
+
## 它是什么
|
|
10
|
+
|
|
11
|
+
一个 **DSH 插件 + CLI + 只读 MCP**,共用一套治理核心:
|
|
12
|
+
|
|
13
|
+
| 动作 | 做什么 | 会不会改你的盘 |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| `scan` | 盘点每个 AI 工具的 skill 根、重复定义、断链引用、MCP 声明、工具版本 | **绝不写** |
|
|
16
|
+
| `plan` | 把选中的 skill 变成一份带风险的方案,算出 `planHash` | 不改 |
|
|
17
|
+
| `apply` | 只执行低风险归档,必须出示 `planHash`,每步写 journal | 只移动,可撤销 |
|
|
18
|
+
| `undo` | 按 journal 逆序还原 | 有改动就报冲突,不覆盖 |
|
|
19
|
+
|
|
20
|
+
## 三个真实场景
|
|
21
|
+
|
|
22
|
+
1. **一台 Windows 工作站装了 14 个 AI 工具,每个都有 28 个 skill**——约 392 份副本。
|
|
23
|
+
`scan` 会把「同名同内容」和「同名不同内容」分开报,不合并结论。
|
|
24
|
+
2. **某个 skill 让 AI 去读 `scripts/run.mjs`,但那个文件早就没了**——`scan` 报
|
|
25
|
+
`skills.broken-references`,并列出缺哪个文件。
|
|
26
|
+
3. **清理完发现某个工具不能用了**——`undo` 从 journal 还原,遇到你后来改过的文件
|
|
27
|
+
会报冲突而不是覆盖。
|
|
28
|
+
|
|
29
|
+
## 第一次使用(三步)
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# 1. 只读体检(不会改动任何东西)
|
|
33
|
+
npx ai-optimize-master scan
|
|
34
|
+
|
|
35
|
+
# 2. 出一份方案(先看再决定)
|
|
36
|
+
npx ai-optimize-master plan --select <skill名> --out plan.json
|
|
37
|
+
|
|
38
|
+
# 3. 拿着方案哈希执行;没有哈希会被拒绝(退出码 4)
|
|
39
|
+
npx ai-optimize-master apply --plan plan.json --expect <planHash>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
安装到 DSH 作为插件:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# 插件清单在 .codex-plugin/plugin.json,MCP 面只读(scan/plan)
|
|
46
|
+
node mcp-server.mjs # stdio MCP,只有两个只读工具
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## 它证明过什么
|
|
50
|
+
|
|
51
|
+
本仓库的验收不靠自述,靠 `npm test` 的 23 条判据:
|
|
52
|
+
|
|
53
|
+
| 判据 | 防的是什么 |
|
|
54
|
+
|---|---|
|
|
55
|
+
| O1 scan 全程零写盘 | 「检查一下」把机器改了 |
|
|
56
|
+
| O2 没跑 ≠ 通过 | 读不到的目录被当成干净 |
|
|
57
|
+
| O3 存在 ≠ 可用 | PATH 里有、一跑就崩,却被报成已安装 |
|
|
58
|
+
| O4 无 `--expect` 必拒 | 没读方案就执行 |
|
|
59
|
+
| O5 哈希过期必拒 | 方案被改过仍照跑 |
|
|
60
|
+
| O6 journal 完备 + 逐字节还原 | 删了回不去 |
|
|
61
|
+
| O7 管理员步骤不偷跑 | 没做却留下「做过」的痕迹 |
|
|
62
|
+
| O8 沙箱隔离 | 测试污染真机 |
|
|
63
|
+
| O9 估算必须带标签 | 把 `bytes/4` 当精确 token 数 |
|
|
64
|
+
| O10 planHash 键序无关、值敏感 | 同一方案哈希漂移 |
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
npm test # 16 项
|
|
68
|
+
npm run check # 结构 + 只读边界 + MCP 不暴露 apply
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## 开始前你可能想知道
|
|
72
|
+
|
|
73
|
+
- **它会删东西吗?** 不删。`apply` 只把目录移动到 `$DSH_OPTIMIZER_HOME/archive/`。
|
|
74
|
+
- **它会改我的 MCP 配置吗?** 不会。v0.2 的 MCP 面只读,只报声明数量与条目名。
|
|
75
|
+
- **它会帮我升级 Node / PowerShell 吗?** 不会。版本只做建议,不做自动升级。
|
|
76
|
+
- **重复的 skill 能直接删吗?** 不能直接下结论。同名同内容只说明是重复定义,
|
|
77
|
+
不说明哪份没人用。方案里单副本 skill 是 `medium` 风险,默认不执行。
|
|
78
|
+
- **Mac 和 Windows 都能跑吗?** 核心是跨平台的;Windows 上多出的工具根目录用
|
|
79
|
+
`DSH_OPTIMIZER_ROOTS` 显式声明,不猜盘符。
|
|
80
|
+
- **免费吗?** MIT。没有账号、没有网络请求。
|
|
81
|
+
|
|
82
|
+
## 本页依据
|
|
83
|
+
|
|
84
|
+
- 源码:本仓库 `main`
|
|
85
|
+
- 验收:`npm test`(16 项)与 `npm run check`
|
|
86
|
+
- 安全边界:`SECURITY.md`
|
|
87
|
+
- 最后核对:2026-11-06
|
|
88
|
+
|
|
89
|
+
## 支持矩阵
|
|
90
|
+
|
|
91
|
+
| 能力 | Windows | macOS | Linux |
|
|
92
|
+
|---|---|---|---|
|
|
93
|
+
| skill 根盘点 / 重复 / 断链 | ✅ | ✅ | ✅ |
|
|
94
|
+
| MCP 声明只读盘点 | ✅ | ✅ | ✅ |
|
|
95
|
+
| 工具版本探测(node/git/pwsh/claude/codex) | ✅ | ✅ | ✅ |
|
|
96
|
+
| 可回滚归档 | ✅ | ✅ | ✅ |
|
|
97
|
+
| 版本升级 / MCP 自动卸载 | ❌ 仅建议 | ❌ 仅建议 | ❌ 仅建议 |
|
|
98
|
+
| 动态启动 MCP / 执行 skill 脚本 | ❌ 不做 | ❌ 不做 | ❌ 不做 |
|
|
99
|
+
|
|
100
|
+
## 和 U-King「AI 优化大师」的关系
|
|
101
|
+
|
|
102
|
+
同一个方向,两个入口,**共享同一套治理语义**(资产模型、方案、哈希门禁、journal、
|
|
103
|
+
退出码)。U-King 侧已有环境体检与一键优化(可回滚);本仓库负责跨工具资产盘点与
|
|
104
|
+
清理。两边不互相重写执行器。
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Security model
|
|
2
|
+
|
|
3
|
+
## Scope
|
|
4
|
+
|
|
5
|
+
`ai-optimize-master` inspects AI-tool asset directories and MCP configuration files on
|
|
6
|
+
the machine where it runs. It does not phone home, does not upload inventory, and
|
|
7
|
+
does not require credentials.
|
|
8
|
+
|
|
9
|
+
## Surface boundaries
|
|
10
|
+
|
|
11
|
+
| Surface | Read | Plan | Apply / Undo |
|
|
12
|
+
|---|---|---|---|
|
|
13
|
+
| CLI (`bin/ai-optimize-master.mjs`) | yes | yes | yes (local, with `--expect`) |
|
|
14
|
+
| DSH plugin tools | yes | yes | yes (with `expect`) |
|
|
15
|
+
| MCP server (`mcp-server.mjs`) | yes | yes | **no — deliberately absent** |
|
|
16
|
+
|
|
17
|
+
The MCP surface exposes only `optimizer_scan` and `optimizer_plan`. A remote MCP
|
|
18
|
+
caller must not be able to modify this machine; **apply and undo are intentionally
|
|
19
|
+
not importable from `mcp-server.mjs`**. `scripts/check.mjs` fails the build if that
|
|
20
|
+
boundary is broken.
|
|
21
|
+
|
|
22
|
+
## What apply can do
|
|
23
|
+
|
|
24
|
+
`apply` only performs `archive-skill` steps: it copies a skill directory into the
|
|
25
|
+
optimizer's own archive root, verifies the copy, then removes the original from the
|
|
26
|
+
tool's skill root.
|
|
27
|
+
|
|
28
|
+
Hard constraints:
|
|
29
|
+
|
|
30
|
+
- Every step is validated before any write: unknown `risk`/`allow` values, unknown
|
|
31
|
+
step kinds, and malformed previews are rejected with exit **1** instead of being
|
|
32
|
+
silently downgraded.
|
|
33
|
+
- The source must live inside one of the plan's `authorizedRoots`, must not be a
|
|
34
|
+
symlink, and its real path must also stay inside a root (prefix checks alone do
|
|
35
|
+
not catch symlinked ancestors).
|
|
36
|
+
- The archive target must not already exist. A previous archive is never merged
|
|
37
|
+
into; the step is blocked as `archive-target-exists`.
|
|
38
|
+
- The caller must pass the exact `planHash` (`--expect`); otherwise the run exits
|
|
39
|
+
**4 (denied)**.
|
|
40
|
+
- If the plan body no longer matches its hash, the run exits **3 (diverged)** and
|
|
41
|
+
doesn't touch anything.
|
|
42
|
+
- Default `--allow low` executes only low-risk steps. A single-copy skill is
|
|
43
|
+
`medium` risk and is skipped unless explicitly allowed.
|
|
44
|
+
- A step marked `requiresAdmin` is blocked when not elevated, and **no journal
|
|
45
|
+
entry is written** for a step that never ran.
|
|
46
|
+
- The journal is written **before** the first destructive operation (intent) and
|
|
47
|
+
rewritten after every step, so a crash mid-run still leaves an accurate record.
|
|
48
|
+
- The journal stores a full directory manifest (relative path, entry type, file
|
|
49
|
+
SHA-256, symlink target) for both the source and the archived copy. A single
|
|
50
|
+
`SKILL.md` hash is not treated as proof that the archive is complete.
|
|
51
|
+
|
|
52
|
+
## What apply cannot do (v0.2)
|
|
53
|
+
|
|
54
|
+
- It does not delete anything permanently: archive is a move into
|
|
55
|
+
`$DSH_OPTIMIZER_HOME/archive/`.
|
|
56
|
+
- It does not modify MCP configuration files. MCP surfaces are read-only.
|
|
57
|
+
- It does not install, upgrade, or downgrade Node, Git, or PowerShell. Version
|
|
58
|
+
findings are advisory only.
|
|
59
|
+
- It does not start MCP servers or execute skill scripts. Dynamic probing of
|
|
60
|
+
unknown code is out of scope.
|
|
61
|
+
|
|
62
|
+
## Undo semantics
|
|
63
|
+
|
|
64
|
+
`undo` processes journal steps in reverse order and checks two things before
|
|
65
|
+
restoring: the archived tree still matches its recorded manifest, and the original
|
|
66
|
+
path is still free. If the user re-created the original path after apply, the step
|
|
67
|
+
is reported as `conflict`/`target-occupied` and **is not overwritten**. An
|
|
68
|
+
already-restored step is detected by manifest and reported as `already-restored`
|
|
69
|
+
instead of failing.
|
|
70
|
+
|
|
71
|
+
## Concurrent writes
|
|
72
|
+
|
|
73
|
+
`archive` builds a manifest, copies to a staging directory, verifies the copy,
|
|
74
|
+
renames it into place, re-checks the source manifest, and only then removes the
|
|
75
|
+
source. This narrows the window in which a concurrently edited file could be lost,
|
|
76
|
+
but it cannot eliminate it. When the source changes during the copy, the step is
|
|
77
|
+
blocked as `source-changed-during-copy` and nothing is deleted.
|
|
78
|
+
|
|
79
|
+
## Known limits, stated honestly
|
|
80
|
+
|
|
81
|
+
- Duplicate detection compares `SKILL.md` content hashes. Identical content does
|
|
82
|
+
**not** prove a copy is unused, and divergence does not prove either copy is
|
|
83
|
+
stale.
|
|
84
|
+
- An unreadable root is reported as `applicable: false` with a reason. It is never
|
|
85
|
+
reported as clean.
|
|
86
|
+
- A binary that exists on PATH but exits non-zero is reported as **unusable**, not
|
|
87
|
+
as installed.
|
|
88
|
+
- Archives are recoverable but not encrypted; treat `$DSH_OPTIMIZER_HOME` as
|
|
89
|
+
sensitive as the skills it contains.
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// CLI entry. Exit codes are part of the contract:
|
|
3
|
+
// 0 success | 1 usage/runtime failure | 3 diverged | 4 denied
|
|
4
|
+
import { readFile, writeFile, mkdir } from 'node:fs/promises'
|
|
5
|
+
import { join, resolve, dirname } from 'node:path'
|
|
6
|
+
import { scan } from '../lib/scan.mjs'
|
|
7
|
+
import { propose, compareScans } from '../lib/evolve.mjs'
|
|
8
|
+
import { buildPlan, applyPlan, undoJournal } from '../lib/apply.mjs'
|
|
9
|
+
import { home, expandHome } from '../lib/util.mjs'
|
|
10
|
+
|
|
11
|
+
function usage() {
|
|
12
|
+
return `AI 优化大师 (ai-optimize-master) v0.3 — audit and reversibly clean AI tool assets
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
ai-optimize-master scan [--json] [--save-baseline FILE] [--no-probes]
|
|
16
|
+
ai-optimize-master evolve [--in FILE] [--baseline FILE] [--out FILE] [--json]
|
|
17
|
+
ai-optimize-master plan [--select name]... [--root id]... [--in FILE] [--out FILE] [--json]
|
|
18
|
+
ai-optimize-master apply --plan FILE --expect HASH [--allow low|medium|high] [--json]
|
|
19
|
+
ai-optimize-master undo --journal FILE [--json]
|
|
20
|
+
|
|
21
|
+
Exit codes: 0 ok · 1 usage/runtime error · 3 diverged · 4 denied
|
|
22
|
+
|
|
23
|
+
Safety:
|
|
24
|
+
- scan is strictly read-only (the only file it writes is an explicit --save-baseline).
|
|
25
|
+
- evolve proposes improvements and diffs; it never edits a skill.
|
|
26
|
+
- plan never mutates anything.
|
|
27
|
+
- apply requires --expect with the exact plan hash and journals every change.
|
|
28
|
+
- Only skills that live inside a declared skill root can be archived.
|
|
29
|
+
`
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parseArgs(argv) {
|
|
33
|
+
const args = { _: [] }
|
|
34
|
+
for (let i = 0; i < argv.length; i++) {
|
|
35
|
+
const token = argv[i]
|
|
36
|
+
if (!token.startsWith('--')) { args._.push(token); continue }
|
|
37
|
+
const key = token.slice(2)
|
|
38
|
+
const next = argv[i + 1]
|
|
39
|
+
if (next === undefined || next.startsWith('--')) { args[key] = true; continue }
|
|
40
|
+
if (args[key] === undefined) args[key] = next
|
|
41
|
+
else (Array.isArray(args[key]) ? args[key] : (args[key] = [args[key]])).push(next)
|
|
42
|
+
i++
|
|
43
|
+
}
|
|
44
|
+
return args
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function toList(value) {
|
|
48
|
+
if (value === undefined) return []
|
|
49
|
+
return Array.isArray(value) ? value : [value]
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function readJson(file) {
|
|
53
|
+
return JSON.parse(await readFile(expandHome(file), 'utf8'))
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function ensureDir(dir) {
|
|
57
|
+
await mkdir(dir, { recursive: true })
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function journalWriter(root) {
|
|
61
|
+
return {
|
|
62
|
+
async write(doc) {
|
|
63
|
+
await ensureDir(root)
|
|
64
|
+
const file = join(root, `${doc.planHash}.json`)
|
|
65
|
+
await writeFile(file, JSON.stringify(doc, null, 2), 'utf8')
|
|
66
|
+
return file
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function cmdScan(args) {
|
|
72
|
+
const roots = toList(args.roots).flatMap(v => String(v).split(process.platform === 'win32' ? ';' : ':'))
|
|
73
|
+
.filter(Boolean)
|
|
74
|
+
.map(dir => ({ id: `extra:${dir.split(/[\\/]/).pop()}`, label: `Extra root ${dir}`, dir }))
|
|
75
|
+
const result = await scan({
|
|
76
|
+
roots: roots.length ? [...(await import('../lib/scan.mjs')).defaultRoots(), ...roots] : undefined,
|
|
77
|
+
probes: !args['no-probes']
|
|
78
|
+
})
|
|
79
|
+
if (args['save-baseline']) {
|
|
80
|
+
// The inspected trees are still untouched; this writes only the named file.
|
|
81
|
+
const target = expandHome(String(args['save-baseline']))
|
|
82
|
+
await mkdir(dirname(resolve(target)), { recursive: true })
|
|
83
|
+
await writeFile(target, JSON.stringify(result, null, 2), 'utf8')
|
|
84
|
+
}
|
|
85
|
+
if (args.json) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`)
|
|
86
|
+
else process.stdout.write(renderScan(result))
|
|
87
|
+
return 0
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function cmdEvolve(args) {
|
|
91
|
+
const current = args.in ? await readJson(args.in) : await scan({ probes: false })
|
|
92
|
+
const report = await propose(current)
|
|
93
|
+
if (args.baseline) {
|
|
94
|
+
const baseline = await readJson(args.baseline)
|
|
95
|
+
report.changes = compareScans(baseline, current)
|
|
96
|
+
}
|
|
97
|
+
if (args.out) {
|
|
98
|
+
const target = expandHome(String(args.out))
|
|
99
|
+
await mkdir(dirname(resolve(target)), { recursive: true })
|
|
100
|
+
await writeFile(target, JSON.stringify(report, null, 2), 'utf8')
|
|
101
|
+
}
|
|
102
|
+
if (args.json) {
|
|
103
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`)
|
|
104
|
+
} else {
|
|
105
|
+
const lines = [`AI 优化大师 · evolve (proposals only, nothing was changed)`]
|
|
106
|
+
lines.push(`Proposals: ${report.proposals.length} ${JSON.stringify(report.counts)}`)
|
|
107
|
+
for (const p of report.proposals.slice(0, 20)) {
|
|
108
|
+
lines.push(` [${p.risk}] ${p.kind} · ${p.skill}: ${p.suggestion.slice(0, 90)}${p.suggestion.length > 90 ? '…' : ''}`)
|
|
109
|
+
}
|
|
110
|
+
if (report.proposals.length > 20) lines.push(` … ${report.proposals.length - 20} more (use --json)`)
|
|
111
|
+
if (report.changes) {
|
|
112
|
+
lines.push('', `Changes since baseline: +${report.changes.added.length} / -${report.changes.resolved.length}`)
|
|
113
|
+
}
|
|
114
|
+
lines.push('', 'Nothing is applied by evolve. Review a diff, then edit the skill yourself or archive it.')
|
|
115
|
+
process.stdout.write(`${lines.join('\n')}\n`)
|
|
116
|
+
}
|
|
117
|
+
return 0
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function renderScan(result) {
|
|
121
|
+
const lines = []
|
|
122
|
+
lines.push(`AI tool inventory (${result.platform}) — read-only`)
|
|
123
|
+
lines.push('')
|
|
124
|
+
for (const root of result.roots) {
|
|
125
|
+
if (!root.applicable) { lines.push(` ${root.label}: ${root.notApplicableReason}`); continue }
|
|
126
|
+
lines.push(` ${root.label.padEnd(28)} ${String(root.skillCount).padStart(3)} skills ${root.dir}`)
|
|
127
|
+
}
|
|
128
|
+
const counts = result.findings.reduce((acc, f) => { acc[f.severity] = (acc[f.severity] ?? 0) + 1; return acc }, {})
|
|
129
|
+
lines.push('')
|
|
130
|
+
lines.push(`Findings: ${Object.entries(counts).map(([k, v]) => `${v} ${k}`).join(' · ') || 'none'}`)
|
|
131
|
+
for (const f of result.findings.filter(f => f.severity !== 'info')) lines.push(` [${f.severity}] ${f.title}`)
|
|
132
|
+
lines.push('')
|
|
133
|
+
lines.push(`Duplicates: ${result.duplicates.length} (identical ${result.duplicates.filter(d => d.classification === 'identical-copies').length})`)
|
|
134
|
+
lines.push(`MCP declarations: ${result.mcp.map(m => `${m.id}=${m.servers.length}`).join(' ') || 'none'}`)
|
|
135
|
+
return `${lines.join('\n')}\n`
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function cmdPlan(args) {
|
|
139
|
+
const scanResult = args.in ? await readJson(args.in) : await scan({ probes: false })
|
|
140
|
+
const names = toList(args.select)
|
|
141
|
+
const roots = toList(args.root)
|
|
142
|
+
const selections = names.map((name, index) => roots[index] ? { name, root: roots[index] } : name)
|
|
143
|
+
const plan = buildPlan(scanResult, { selections, home: home() })
|
|
144
|
+
const archiveRoot = plan.archiveRoot
|
|
145
|
+
if (args.out) {
|
|
146
|
+
await ensureDir(dirname(resolve(expandHome(args.out))))
|
|
147
|
+
await writeFile(expandHome(args.out), JSON.stringify(plan, null, 2), 'utf8')
|
|
148
|
+
}
|
|
149
|
+
if (args.json) process.stdout.write(`${JSON.stringify(plan, null, 2)}\n`)
|
|
150
|
+
else {
|
|
151
|
+
const lines = [`Plan ${plan.planHash}`, `Archive root: ${archiveRoot}`, `Steps: ${plan.steps.length}`]
|
|
152
|
+
for (const step of plan.steps) lines.push(` [${step.risk}] ${step.id} ${step.preview.from} -> ${step.preview.to}`)
|
|
153
|
+
for (const skip of plan.skipped) lines.push(` skipped ${skip.name}: ${skip.reason}`)
|
|
154
|
+
lines.push('', 'Apply with:', ` dsh-optimizer apply --plan <file> --expect ${plan.planHash}`)
|
|
155
|
+
process.stdout.write(`${lines.join('\n')}\n`)
|
|
156
|
+
}
|
|
157
|
+
return 0
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function cmdApply(args) {
|
|
161
|
+
if (!args.plan) { process.stderr.write('apply requires --plan\n'); return 1 }
|
|
162
|
+
const plan = await readJson(args.plan)
|
|
163
|
+
const journalRoot = join(home(), 'journal')
|
|
164
|
+
const result = await applyPlan(plan, {
|
|
165
|
+
expect: typeof args.expect === 'string' ? args.expect : null,
|
|
166
|
+
allow: typeof args.allow === 'string' ? args.allow : 'low',
|
|
167
|
+
journal: journalWriter(journalRoot),
|
|
168
|
+
isAdmin: typeof process.getuid === 'function' ? process.getuid() === 0 : false
|
|
169
|
+
})
|
|
170
|
+
if (args.json) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`)
|
|
171
|
+
else {
|
|
172
|
+
for (const step of result.results ?? []) process.stdout.write(`${step.status.padEnd(8)} ${step.id}${step.blocked ? ` (${step.blocked})` : ''}\n`)
|
|
173
|
+
if (result.reason) process.stderr.write(`${result.reason}\n`)
|
|
174
|
+
}
|
|
175
|
+
return result.code
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function cmdUndo(args) {
|
|
179
|
+
if (!args.journal) { process.stderr.write('undo requires --journal\n'); return 1 }
|
|
180
|
+
const doc = await readJson(args.journal)
|
|
181
|
+
const result = await undoJournal(doc, { journal: journalWriter(join(home(), 'journal')) })
|
|
182
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`)
|
|
183
|
+
return result.code
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const commands = { scan: cmdScan, evolve: cmdEvolve, plan: cmdPlan, apply: cmdApply, undo: cmdUndo }
|
|
187
|
+
|
|
188
|
+
async function main() {
|
|
189
|
+
const argv = process.argv.slice(2)
|
|
190
|
+
const command = argv[0]
|
|
191
|
+
if (!command || command === '--help' || command === '-h' || command === 'help') {
|
|
192
|
+
process.stdout.write(usage())
|
|
193
|
+
return 0
|
|
194
|
+
}
|
|
195
|
+
const handler = commands[command]
|
|
196
|
+
if (!handler) { process.stderr.write(`unknown command: ${command}\n\n${usage()}`); return 1 }
|
|
197
|
+
try {
|
|
198
|
+
return await handler(parseArgs(argv.slice(1)))
|
|
199
|
+
} catch (error) {
|
|
200
|
+
process.stderr.write(`dsh-optimizer ${command} failed: ${error?.message ?? error}\n`)
|
|
201
|
+
return 1
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
process.exitCode = await main()
|
package/cordis.patch.yml
ADDED
package/index.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// DSH plugin surface: three tools, all sharing one core.
|
|
2
|
+
// scan — strictly read-only inventory.
|
|
3
|
+
// plan — turns findings + explicit selections into a reviewable, hash-bound plan.
|
|
4
|
+
// apply — executes a low-risk, reversible archive; requires the exact plan hash.
|
|
5
|
+
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
6
|
+
import { scan } from './lib/scan.mjs'
|
|
7
|
+
import { buildPlan, applyPlan, undoJournal } from './lib/apply.mjs'
|
|
8
|
+
import { home } from './lib/util.mjs'
|
|
9
|
+
import { writeFile, mkdir } from 'node:fs/promises'
|
|
10
|
+
import { join } from 'node:path'
|
|
11
|
+
|
|
12
|
+
export const name = 'ai-optimize-master'
|
|
13
|
+
export const inject = ['tools']
|
|
14
|
+
|
|
15
|
+
const renderJson = (_args, value) => [{ type: 'text', text: JSON.stringify(value, null, 2) }]
|
|
16
|
+
|
|
17
|
+
const journal = {
|
|
18
|
+
async write(doc) {
|
|
19
|
+
const root = join(home(), 'journal')
|
|
20
|
+
await mkdir(root, { recursive: true })
|
|
21
|
+
const file = join(root, `${doc.planHash}.json`)
|
|
22
|
+
await writeFile(file, JSON.stringify(doc, null, 2), 'utf8')
|
|
23
|
+
return file
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function createDefinitions() {
|
|
28
|
+
return [
|
|
29
|
+
defineTool({
|
|
30
|
+
name: 'dsh_optimizer_scan',
|
|
31
|
+
description: 'Read-only inventory of AI tool skill roots, duplicate/broken skill definitions, MCP declarations, and tool versions. Never writes to any inspected directory. A tool that exists on PATH but fails to run is reported as unusable, not as installed.',
|
|
32
|
+
parameters: {
|
|
33
|
+
includeProbes: { type: 'boolean', description: 'Run the allowlisted version probes (node/git/pwsh/claude/codex). Default true.' }
|
|
34
|
+
},
|
|
35
|
+
output: { schema: { type: 'json' }, render: renderJson },
|
|
36
|
+
async execute(args = {}) {
|
|
37
|
+
const result = await scan({ probes: args.includeProbes ?? true })
|
|
38
|
+
return { ...result, readOnly: true }
|
|
39
|
+
}
|
|
40
|
+
}),
|
|
41
|
+
defineTool({
|
|
42
|
+
name: 'dsh_optimizer_plan',
|
|
43
|
+
description: 'Build a reviewable plan from a scan. Selective: only explicitly selected skills become steps. Returns a planHash that apply requires. Does not modify anything.',
|
|
44
|
+
parameters: {
|
|
45
|
+
select: { type: 'array', description: 'Skill names to consider archiving.', items: { type: 'string' } },
|
|
46
|
+
root: { type: 'array', description: 'Optional root id per selection, in the same order.', items: { type: 'string' } }
|
|
47
|
+
},
|
|
48
|
+
output: { schema: { type: 'json' }, render: renderJson },
|
|
49
|
+
async execute(args = {}) {
|
|
50
|
+
const scanResult = await scan({ probes: false })
|
|
51
|
+
const selected = (args.select ?? []).map((skillName, index) => (args.root?.[index]
|
|
52
|
+
? { name: skillName, root: args.root[index] }
|
|
53
|
+
: skillName))
|
|
54
|
+
return buildPlan(scanResult, { selections: selected })
|
|
55
|
+
}
|
|
56
|
+
}),
|
|
57
|
+
defineTool({
|
|
58
|
+
name: 'dsh_optimizer_apply',
|
|
59
|
+
description: 'Apply a previously planned archive step set. Requires the exact planHash via expect; without it the call is denied. Journals every change so undo can restore byte-identical content. Only low-risk steps by default.',
|
|
60
|
+
parameters: {
|
|
61
|
+
plan: { type: 'json', required: true, description: 'The plan object returned by dsh_optimizer_plan.' },
|
|
62
|
+
expect: { type: 'string', required: true, description: 'The planHash being approved.' },
|
|
63
|
+
allow: { type: 'string', description: 'Maximum risk to execute: low|medium|high. Default low.' }
|
|
64
|
+
},
|
|
65
|
+
output: { schema: { type: 'json' }, render: renderJson },
|
|
66
|
+
async execute(args) {
|
|
67
|
+
const result = await applyPlan(args.plan, {
|
|
68
|
+
expect: args.expect,
|
|
69
|
+
allow: args.allow ?? 'low',
|
|
70
|
+
journal,
|
|
71
|
+
isAdmin: false
|
|
72
|
+
})
|
|
73
|
+
if (result.code === 4) throw Object.assign(new Error('denied: apply requires the exact plan hash'), { code: 'DENIED' })
|
|
74
|
+
if (result.code === 3) throw Object.assign(new Error('diverged: plan hash does not match plan content'), { code: 'DIVERGED' })
|
|
75
|
+
return result
|
|
76
|
+
}
|
|
77
|
+
}),
|
|
78
|
+
defineTool({
|
|
79
|
+
name: 'dsh_optimizer_undo',
|
|
80
|
+
description: 'Undo an applied journal in reverse order. A step whose content changed after apply is reported as a conflict instead of being overwritten.',
|
|
81
|
+
parameters: {
|
|
82
|
+
journal: { type: 'json', required: true, description: 'The journal document returned by apply.' }
|
|
83
|
+
},
|
|
84
|
+
output: { schema: { type: 'json' }, render: renderJson },
|
|
85
|
+
async execute(args) {
|
|
86
|
+
return undoJournal(args.journal, { journal })
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
]
|
|
90
|
+
}
|