@yottameta/yotta-dev-mcp 0.2.1 → 0.2.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/CHANGELOG.md +16 -0
- package/README.md +7 -2
- package/README.zh-CN.md +9 -4
- package/SKILL.md +13 -1
- package/bin/install.js +275 -81
- package/install.sh +320 -2
- package/package.json +1 -1
- package/scripts/dev_engine.py +1 -1
- package/scripts/dev_selftest.py +5 -4
- package/scripts/yotta_dev_mcp.py +86 -6
- package/server.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
## v0.2.3 (2026-10-01)
|
|
2
|
+
|
|
3
|
+
- 安装器卫生批次:`bin/install.js` / `install.sh` 统一(未知参数报错 exit 2、`--help` / `--version`、残留清理白名单、嵌套载荷保留);由模板单一真源渲染,接入漂移门禁。
|
|
4
|
+
- `--global` 不再强制 `--yes`(统一安装器口径);`--dry-run` 保留。
|
|
5
|
+
|
|
6
|
+
## v0.2.2 (2026-09-26)
|
|
7
|
+
|
|
8
|
+
新增工具分组 `--tools core|full`(与元忆同口径),供需要把工具表常驻上下文的宿主使用。
|
|
9
|
+
|
|
10
|
+
- `core` = 5 工具:`repo_map` / `find_code` / `review_code` / `review_diff` / `verify_change`;紧凑 JSON schema 3,883 字符。
|
|
11
|
+
- `full` = 全部 18 工具(默认,行为与 0.2.1 一致);同一口径下 schema 13,230 字符。
|
|
12
|
+
- 背景:产品侧实测 18 工具 `schemaChars 8174`,超过其 8,000 硬上限,无法默认常驻;core 档按同一比例约 2.4k,留有充足余量。
|
|
13
|
+
- `core` 模式下调用 full 专属工具会返回明确提示("属于 full 分组,请用 --tools full 重新启动"),不做静默失败。
|
|
14
|
+
- `--help` / `--version` 支持;非法分组值以中文用法错误 + 退出码 2 拒绝。
|
|
15
|
+
- 回归:新增 7 项分档用例(默认 full / core 列表与顺序 / core 拒绝 full 专属工具 / schema 预算 / 非法值拒绝 / stdio 两种端到端);元开全量 189/189(1 skip)。
|
|
16
|
+
|
|
1
17
|
# Changelog
|
|
2
18
|
|
|
3
19
|
## v0.2.1 (2026-09-25)
|
package/README.md
CHANGED
|
@@ -126,8 +126,13 @@ python scripts/dev_engine.py mcp-doctor
|
|
|
126
126
|
|
|
127
127
|
## Current version
|
|
128
128
|
|
|
129
|
-
`0.2.
|
|
130
|
-
|
|
129
|
+
`0.2.2` (2026-09-26) adds tool profiles. Start the server with `--tools core`
|
|
130
|
+
for a five-tool set (`repo_map` / `find_code` / `review_code` / `review_diff` /
|
|
131
|
+
`verify_change`, ~3.9k characters of compact JSON schema) so hosts that keep the
|
|
132
|
+
tool table resident stay inside their schema budget; `--tools full` (default)
|
|
133
|
+
keeps all 18 tools. Calling a full-only tool in `core` mode returns an explicit
|
|
134
|
+
message instead of failing silently. Previous `0.2.1`: `mcp_doctor` covers
|
|
135
|
+
Codex, WorkBuddy, OpenCode, Cursor and other hosts with a coverage report;
|
|
131
136
|
`self_test(mode="installed")` is distribution-aware; `repo_map` resolves
|
|
132
137
|
`from . import X`; `scan_secrets` filters path/hash/filename noise; and
|
|
133
138
|
`review_code` ignores temporary directories.
|
package/README.zh-CN.md
CHANGED
|
@@ -114,10 +114,15 @@ python scripts/dev_engine.py mcp-doctor
|
|
|
114
114
|
|
|
115
115
|
## 当前版本
|
|
116
116
|
|
|
117
|
-
`0.2.
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
117
|
+
`0.2.2`(2026-09-26)新增工具分组:用 `--tools core` 启动只暴露 5 个工具
|
|
118
|
+
(`repo_map` / `find_code` / `review_code` / `review_diff` / `verify_change`,
|
|
119
|
+
紧凑 JSON schema 约 3.9k 字符),供需要把工具表常驻上下文的宿主留在自己的
|
|
120
|
+
schema 预算内;`--tools full`(默认)保持全部 18 个工具。core 模式下调用
|
|
121
|
+
full 专属工具会返回明确提示,不做静默失败。上一版 `0.2.1`:`mcp_doctor`
|
|
122
|
+
覆盖 Codex / WorkBuddy / OpenCode / Cursor 等宿主并返回 coverage;
|
|
123
|
+
`self_test(mode="installed")` 按分发形态判断 banner;`repo_map` 修复
|
|
124
|
+
`from . import X`;`scan_secrets` 过滤路径 / 哈希 / 文件名噪声;
|
|
125
|
+
`review_code` 忽略临时目录。
|
|
121
126
|
|
|
122
127
|
`0.2.0`(2026-09-25)在原有十二个确定性工具之上新增 `system_model`、
|
|
123
128
|
`architecture_review`、`impact_analysis`、`verify_change`、`self_test`、
|
package/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: yotta-dev-mcp
|
|
3
3
|
description: 元开(yotta-dev-mcp)—— 本地、确定性的开发工具 MCP,把只读默认的开发能力暴露为 stdio MCP server:repo_map / system_model / architecture_review / impact_analysis / verify_change / self_test / run_adapter / find_code / compress_output / review_code / review_diff / mcp_doctor / scan_secrets / scan_dependencies / check_publish_readiness / run_checks / scaffold_skill / workflow_state。触发:让 AI 在陌生项目里先做结构盘点、构建系统模型或架构契约(.yotta/architecture.json)、按契约评审架构、分析改动影响锥与回归面、按验证阶梯产出证据账本、对元开自身做完整性 / 反证自测、探测或显式运行 import-linter / dependency-cruiser / Repomix 可选适配器、定位代码、评审改动、扫描密钥/依赖、检查发布就绪、运行白名单检查、生成脚手架或读取 .workflow 状态时;或用户说 元开 / 开发能力 MCP / yotta-dev-mcp / 代码库地图 / 系统模型 / 架构契约 / 架构评审 / 影响分析 / 验证账本 / 反证自测 / 代码评审 MCP / 适配器 等。边界:Python 3.8+ 标准库、离线默认;除 run_checks(显式 allow_execute)、verify_change 的 L2-L4 策略检查(显式 allow_execute)与 self_test 的测试子集(显式 allow_execute)、run_adapter 的 action=run(显式 allow_execute)、scaffold_skill / workflow_state 的显式 apply 外均为只读;不上传源码、不自动修改、不提交、不联网查询包是否存在、不自动安装或下载适配器。
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.3
|
|
5
5
|
license: MIT
|
|
6
6
|
---
|
|
7
7
|
|
|
@@ -37,6 +37,18 @@ npx -y @yottameta/yotta-dev-mcp
|
|
|
37
37
|
python scripts/yotta_dev_mcp.py
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
+
工具分组(`--tools`,与元忆同口径):
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
python scripts/yotta_dev_mcp.py --tools core # 5 工具:repo_map / find_code / review_code / review_diff / verify_change
|
|
44
|
+
python scripts/yotta_dev_mcp.py --tools full # 18 工具(默认)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`core` 给「需要把工具表常驻在上下文里」的宿主用:schema 体积约为 `full` 的 29%
|
|
48
|
+
(5 工具 3,883 字符 vs 18 工具 13,230 字符,紧凑 JSON 口径),适合默认启用;
|
|
49
|
+
`full` 暴露全部工具,适合按需调用、不常驻的场景。`core` 模式下调用 full 专属工具会返回
|
|
50
|
+
明确提示(提示用 `--tools full` 重启)。
|
|
51
|
+
|
|
40
52
|
自动接入说明:AI 首次协助配置时,必须先展示目标配置文件、完整配置片段和影响,
|
|
41
53
|
并获得用户明确同意后再写入 `mcpServers`。用户拒绝时不要写文件,直接提供上面的启动命令
|
|
42
54
|
让用户手动接入。
|
package/bin/install.js
CHANGED
|
@@ -1,46 +1,92 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* yotta-dev-mcp
|
|
3
|
+
* yotta-dev-mcp 跨平台安装器(YottaSkills)
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* --agent <name>
|
|
7
|
-
* --dir <path>
|
|
8
|
-
* --
|
|
9
|
-
*
|
|
10
|
-
* --
|
|
5
|
+
* 用法:
|
|
6
|
+
* npx -y --package @yottameta/yotta-dev-mcp yotta-dev-mcp-install --agent <name> # 按智能体默认用户级目录安装(推荐)
|
|
7
|
+
* npx -y --package @yottameta/yotta-dev-mcp yotta-dev-mcp-install --dir <path> # 装到指定目录(用户改了目录的智能体)
|
|
8
|
+
* npx -y --package @yottameta/yotta-dev-mcp yotta-dev-mcp-install -g # 安装到全部已知智能体用户级目录
|
|
9
|
+
* npx -y --package @yottameta/yotta-dev-mcp yotta-dev-mcp-install # 安装到检测到的项目级目录
|
|
10
|
+
* npx -y --package @yottameta/yotta-dev-mcp yotta-dev-mcp-install --list # 列出智能体 -> 默认目录
|
|
11
|
+
* npx -y --package @yottameta/yotta-dev-mcp yotta-dev-mcp-install --dry-run # 只显示将写入的目标,不写文件
|
|
12
|
+
* npx -y --package @yottameta/yotta-dev-mcp yotta-dev-mcp-install --version # 显示版本
|
|
13
|
+
* npx -y --package @yottameta/yotta-dev-mcp yotta-dev-mcp-install --help # 显示帮助
|
|
14
|
+
*
|
|
15
|
+
* 退出码: 0 成功 / 1 安装失败 / 2 用法错误 / 4 目标错误
|
|
11
16
|
*/
|
|
12
17
|
'use strict';
|
|
13
18
|
const fs = require('fs');
|
|
14
|
-
const os = require('os');
|
|
15
19
|
const path = require('path');
|
|
20
|
+
const os = require('os');
|
|
16
21
|
|
|
17
22
|
const SKILL_NAME = 'yotta-dev-mcp';
|
|
18
23
|
const PKG_ROOT = path.join(__dirname, '..');
|
|
24
|
+
|
|
25
|
+
// 顶层开发文件 / 目录:只在安装包顶层跳过;技能包内嵌套同名载荷必须保留。
|
|
26
|
+
// 与 install.sh 同口径:开发件与运行时不相关目录一律不落目标。
|
|
27
|
+
const TOP_SKIP = [
|
|
28
|
+
'package.json', 'package-lock.json', 'bin', 'lib', 'test',
|
|
29
|
+
'.github', '.git', '.gitignore', '.npmignore', '.gitattributes',
|
|
30
|
+
'.yotta', '.tmp', 'install.sh', 'node_modules',
|
|
31
|
+
];
|
|
32
|
+
const TOP_SKIP_SET = new Set(TOP_SKIP);
|
|
33
|
+
// 缓存 / 编译产物:任意层级跳过,安装时同样清理。
|
|
34
|
+
const CACHE_DIRS = new Set(['__pycache__', '.pytest_cache', '.mypy_cache']);
|
|
35
|
+
|
|
36
|
+
class UsageError extends Error {}
|
|
37
|
+
class TargetError extends Error {}
|
|
38
|
+
class InstallError extends Error {}
|
|
39
|
+
|
|
40
|
+
// 智能体 -> 用户级默认技能目录(dirs 按优先级排列;--agent 装到第一个)
|
|
41
|
+
// 依据官方文档:.agents/skills 并非通用目录,被 OpenCode / Cursor / Cline / Amp /
|
|
42
|
+
// Kimi / Gemini CLI / GitHub Copilot 等读取;Claude Code 与 Codex 默认不读 .agents。
|
|
19
43
|
const AGENT_DIRS = {
|
|
20
|
-
claude:
|
|
21
|
-
cursor:
|
|
22
|
-
codex:
|
|
23
|
-
gemini:
|
|
24
|
-
goose:
|
|
25
|
-
amp:
|
|
26
|
-
opencode:
|
|
27
|
-
windsurf:
|
|
28
|
-
workbuddy: { label: 'WorkBuddy',
|
|
29
|
-
kiro:
|
|
30
|
-
trae:
|
|
31
|
-
'trae-cn': { label: 'Trae IDE',
|
|
32
|
-
qwen:
|
|
33
|
-
comate:
|
|
34
|
-
codebuddy: { label: 'CodeBuddy Code',
|
|
35
|
-
kimi:
|
|
36
|
-
agents:
|
|
44
|
+
claude: { label: 'Claude Code', dirs: ['.claude/skills'] },
|
|
45
|
+
cursor: { label: 'Cursor', dirs: ['.cursor/skills', '.agents/skills'] },
|
|
46
|
+
codex: { label: 'Codex', dirs: ['.codex/skills'] }, // 特判:$CODEX_HOME/skills
|
|
47
|
+
gemini: { label: 'Gemini CLI', dirs: ['.gemini/skills', '.agents/skills'] },
|
|
48
|
+
goose: { label: 'Goose', dirs: ['.config/goose/skills', '.agents/skills'] },
|
|
49
|
+
amp: { label: 'Amp', dirs: ['.config/agents/skills', '.agents/skills'] },
|
|
50
|
+
opencode: { label: 'OpenCode', dirs: ['.config/opencode/skills'] }, // 特判:$XDG_CONFIG_HOME
|
|
51
|
+
windsurf: { label: 'Windsurf', dirs: ['.codeium/windsurf/skills'] },
|
|
52
|
+
workbuddy: { label: 'WorkBuddy', dirs: ['.workbuddy/skills'] },
|
|
53
|
+
kiro: { label: 'Kiro', dirs: ['.kiro/skills'] },
|
|
54
|
+
trae: { label: 'Trae Code CLI', dirs: ['.traecli/skills'] },
|
|
55
|
+
'trae-cn': { label: 'Trae IDE(国内)', dirs: ['.trae-cn/skills'] },
|
|
56
|
+
qwen: { label: 'Qwen Code', dirs: ['.qwen/skills'] },
|
|
57
|
+
comate: { label: 'Comate 文心快码', dirs: ['.comate/skills'] },
|
|
58
|
+
codebuddy: { label: 'CodeBuddy Code', dirs: ['.codebuddy/skills'] },
|
|
59
|
+
kimi: { label: 'Kimi Code CLI', dirs: ['.kimi/skills'] },
|
|
60
|
+
agents: { label: '通用 AGENTS.md', dirs: ['.agents/skills'] },
|
|
37
61
|
};
|
|
38
62
|
|
|
63
|
+
const PROJECT_DIRS = [
|
|
64
|
+
'.claude/skills',
|
|
65
|
+
'.cursor/skills',
|
|
66
|
+
'.codex/skills',
|
|
67
|
+
'.config/goose/skills',
|
|
68
|
+
'.config/agents/skills',
|
|
69
|
+
'.opencode/skills',
|
|
70
|
+
'.codeium/windsurf/skills',
|
|
71
|
+
'.workbuddy/skills',
|
|
72
|
+
'.kiro/skills',
|
|
73
|
+
'.traecli/skills',
|
|
74
|
+
'.gemini/skills',
|
|
75
|
+
'.trae-cn/skills',
|
|
76
|
+
'.qwen/skills',
|
|
77
|
+
'.comate/skills',
|
|
78
|
+
'.codebuddy/skills',
|
|
79
|
+
'.kimi/skills',
|
|
80
|
+
'.agents/skills',
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
// Codex 用户级目录特判:优先 $CODEX_HOME/skills,否则 ~/.codex/skills
|
|
39
84
|
function codexUserDir() {
|
|
40
85
|
const base = process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
|
|
41
86
|
return path.join(base, 'skills');
|
|
42
87
|
}
|
|
43
88
|
|
|
89
|
+
// OpenCode 用户级目录特判:优先 $XDG_CONFIG_HOME/opencode/skills,否则 ~/.config/opencode/skills
|
|
44
90
|
function opencodeUserDir() {
|
|
45
91
|
const base = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
|
|
46
92
|
return path.join(base, 'opencode', 'skills');
|
|
@@ -52,91 +98,239 @@ function resolveUserDir(rel) {
|
|
|
52
98
|
return path.join(os.homedir(), rel);
|
|
53
99
|
}
|
|
54
100
|
|
|
55
|
-
function
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
101
|
+
function displayDir(rel) {
|
|
102
|
+
if (process.platform === 'win32') return '%USERPROFILE%\\' + rel.replace(/\//g, '\\');
|
|
103
|
+
return '~/' + rel;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function skillVersion() {
|
|
107
|
+
try {
|
|
108
|
+
const data = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, 'package.json'), 'utf8'));
|
|
109
|
+
return data.version || null;
|
|
110
|
+
} catch (_) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function usage() {
|
|
116
|
+
console.log(SKILL_NAME + ' 安装器(YottaSkills)');
|
|
117
|
+
console.log('');
|
|
118
|
+
console.log('用法:');
|
|
119
|
+
console.log(' node bin/install.js --agent <name> 按智能体默认用户级目录安装(推荐)');
|
|
120
|
+
console.log(' node bin/install.js --dir <path> 装到指定目录(用户改了目录的智能体)');
|
|
121
|
+
console.log(' node bin/install.js -g 安装到全部已知智能体用户级目录');
|
|
122
|
+
console.log(' node bin/install.js 安装到检测到的项目级目录');
|
|
123
|
+
console.log('');
|
|
124
|
+
console.log('参数:');
|
|
125
|
+
console.log(' --agent <name> 智能体键名,见 --list');
|
|
126
|
+
console.log(' --dir <path> 自定义技能目录');
|
|
127
|
+
console.log(' -g, --global 安装到全部已知用户级目录');
|
|
128
|
+
console.log(' --list, -l 列出支持的智能体目录');
|
|
129
|
+
console.log(' --dry-run 只显示将写入的目标,不写文件');
|
|
130
|
+
console.log(' --version, -v 显示版本');
|
|
131
|
+
console.log(' --help, -h 显示帮助');
|
|
132
|
+
console.log(' --yes, -y 兼容参数(-g 不再强制要求)');
|
|
133
|
+
console.log('');
|
|
134
|
+
console.log('退出码: 0 成功 / 1 安装失败 / 2 用法错误 / 4 目标错误');
|
|
59
135
|
}
|
|
60
136
|
|
|
61
|
-
function
|
|
62
|
-
|
|
63
|
-
for (
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
137
|
+
function parseArgs(argv) {
|
|
138
|
+
const opts = { help: false, version: false, list: false, global: false, dryRun: false, yes: false, dir: null, agent: null };
|
|
139
|
+
for (let i = 0; i < argv.length; i++) {
|
|
140
|
+
const arg = argv[i];
|
|
141
|
+
if (arg === '--help' || arg === '-h') opts.help = true;
|
|
142
|
+
else if (arg === '--version' || arg === '-v') opts.version = true;
|
|
143
|
+
else if (arg === '--list' || arg === '-l') opts.list = true;
|
|
144
|
+
else if (arg === '--global' || arg === '-g') opts.global = true;
|
|
145
|
+
else if (arg === '--dry-run') opts.dryRun = true;
|
|
146
|
+
else if (arg === '--yes' || arg === '-y') opts.yes = true;
|
|
147
|
+
else if (arg === '--dir') {
|
|
148
|
+
const value = argv[++i];
|
|
149
|
+
if (!value) throw new UsageError('--dir 需要一个非空路径');
|
|
150
|
+
opts.dir = value;
|
|
151
|
+
} else if (arg === '--agent') {
|
|
152
|
+
const value = argv[++i];
|
|
153
|
+
if (!value) throw new UsageError('--agent 需要一个非空名称');
|
|
154
|
+
opts.agent = String(value).toLowerCase();
|
|
155
|
+
} else {
|
|
156
|
+
throw new UsageError('未知参数: ' + arg);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (!opts.help && !opts.version) {
|
|
160
|
+
const selected = [opts.dir, opts.agent, opts.global].filter(Boolean).length;
|
|
161
|
+
if (selected > 1) throw new UsageError('--dir / --agent / -g 只能选一个');
|
|
162
|
+
}
|
|
163
|
+
return opts;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function printList() {
|
|
167
|
+
console.log('智能体 -> 默认技能目录(--agent <name> 装到第一个,用户级):');
|
|
168
|
+
for (const [key, value] of Object.entries(AGENT_DIRS)) {
|
|
169
|
+
const resolved = value.dirs.map(displayDir);
|
|
170
|
+
console.log(' ' + key.padEnd(10) + value.label.padEnd(18) + resolved.join('、'));
|
|
171
|
+
}
|
|
172
|
+
console.log('\n说明:Windows 用 %USERPROFILE%,Linux/macOS 用 ~;仅收录有官方默认目录的智能体。');
|
|
173
|
+
console.log('改了目录的请用 --dir <路径>,不要依赖默认位置;若设置了 CODEX_HOME / XDG_CONFIG_HOME,安装自动以该变量为准。');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function assertSafeTarget(target) {
|
|
177
|
+
const rel = path.relative(PKG_ROOT, target);
|
|
178
|
+
if (rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel))) {
|
|
179
|
+
throw new UsageError('目标目录不能在技能源目录内(防止自装自毁)');
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function shouldSkipCache(name, isFile) {
|
|
184
|
+
if (CACHE_DIRS.has(name)) return true;
|
|
185
|
+
if (isFile && (name.endsWith('.pyc') || name.endsWith('.pyo'))) return true;
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function copyDir(src, dst, topLevel) {
|
|
190
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
191
|
+
// 只跳过安装包顶层开发文件;template/ 等嵌套同名载荷必须保留。
|
|
192
|
+
if (topLevel && TOP_SKIP_SET.has(entry.name)) continue;
|
|
193
|
+
if (shouldSkipCache(entry.name, entry.isFile())) continue;
|
|
194
|
+
const s = path.join(src, entry.name);
|
|
195
|
+
const d = path.join(dst, entry.name);
|
|
67
196
|
if (entry.isDirectory()) {
|
|
68
|
-
|
|
197
|
+
fs.mkdirSync(d, { recursive: true });
|
|
198
|
+
copyDir(s, d, false);
|
|
69
199
|
} else if (entry.isFile()) {
|
|
70
|
-
fs.copyFileSync(
|
|
200
|
+
fs.copyFileSync(s, d);
|
|
71
201
|
}
|
|
72
202
|
}
|
|
73
203
|
}
|
|
74
204
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
205
|
+
// 清理旧版安装残留(fail-closed 白名单):
|
|
206
|
+
// 仅在目标目录已存在且含 SKILL.md 时触发;只删顶层开发项 + 任意层级缓存;
|
|
207
|
+
// 不整目录删除、不跟随符号链接。
|
|
208
|
+
function cleanResidue(target) {
|
|
209
|
+
const removed = [];
|
|
210
|
+
let stat = null;
|
|
211
|
+
try { stat = fs.statSync(target); } catch (_) { return removed; }
|
|
212
|
+
if (!stat.isDirectory()) return removed;
|
|
213
|
+
if (!fs.existsSync(path.join(target, 'SKILL.md'))) return removed;
|
|
214
|
+
for (const name of TOP_SKIP) {
|
|
215
|
+
const p = path.join(target, name);
|
|
216
|
+
let entry = null;
|
|
217
|
+
try { entry = fs.lstatSync(p); } catch (_) { continue; }
|
|
218
|
+
if (entry.isSymbolicLink()) continue;
|
|
219
|
+
fs.rmSync(p, { recursive: true, force: true });
|
|
220
|
+
removed.push(name);
|
|
80
221
|
}
|
|
81
|
-
|
|
82
|
-
|
|
222
|
+
const walk = (dir) => {
|
|
223
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
224
|
+
const p = path.join(dir, entry.name);
|
|
225
|
+
if (entry.isSymbolicLink()) continue;
|
|
226
|
+
if (entry.isDirectory()) {
|
|
227
|
+
if (CACHE_DIRS.has(entry.name)) {
|
|
228
|
+
fs.rmSync(p, { recursive: true, force: true });
|
|
229
|
+
removed.push(path.relative(target, p));
|
|
230
|
+
} else {
|
|
231
|
+
walk(p);
|
|
232
|
+
}
|
|
233
|
+
} else if (entry.isFile() && (entry.name.endsWith('.pyc') || entry.name.endsWith('.pyo'))) {
|
|
234
|
+
fs.unlinkSync(p);
|
|
235
|
+
removed.push(path.relative(target, p));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
walk(target);
|
|
240
|
+
return removed;
|
|
83
241
|
}
|
|
84
242
|
|
|
85
|
-
function
|
|
86
|
-
if (
|
|
87
|
-
|
|
243
|
+
function installTo(dest, opts) {
|
|
244
|
+
if (!dest || typeof dest !== 'string') throw new UsageError('目标目录不能为空');
|
|
245
|
+
const target = path.resolve(dest, SKILL_NAME);
|
|
246
|
+
assertSafeTarget(target);
|
|
247
|
+
if (opts.dryRun) {
|
|
248
|
+
console.log('[dry-run] 将安装到 -> ' + target);
|
|
249
|
+
return target;
|
|
250
|
+
}
|
|
251
|
+
try {
|
|
252
|
+
for (const rel of cleanResidue(target)) console.log('已清理残留: ' + rel);
|
|
253
|
+
fs.mkdirSync(target, { recursive: true });
|
|
254
|
+
copyDir(PKG_ROOT, target, true);
|
|
255
|
+
if (!fs.existsSync(path.join(target, 'SKILL.md'))) {
|
|
256
|
+
throw new InstallError('安装结果缺少 SKILL.md');
|
|
257
|
+
}
|
|
258
|
+
} catch (err) {
|
|
259
|
+
if (err instanceof UsageError || err instanceof TargetError || err instanceof InstallError) throw err;
|
|
260
|
+
throw new InstallError('安装到 ' + target + ' 失败: ' + err.message);
|
|
261
|
+
}
|
|
262
|
+
console.log('installed -> ' + target);
|
|
263
|
+
return target;
|
|
88
264
|
}
|
|
89
265
|
|
|
90
|
-
function
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const agent = agentIndex !== -1 ? String(args[agentIndex + 1]).toLowerCase() : null;
|
|
100
|
-
|
|
101
|
-
if (list) {
|
|
102
|
-
for (const [key, value] of Object.entries(AGENT_DIRS)) {
|
|
103
|
-
console.log(' ' + key.padEnd(10) + value.label.padEnd(18) +
|
|
104
|
-
value.dirs.map(displayDir).join(', '));
|
|
266
|
+
function run() {
|
|
267
|
+
const opts = parseArgs(process.argv.slice(2));
|
|
268
|
+
if (opts.help) { usage(); return; }
|
|
269
|
+
if (opts.version) {
|
|
270
|
+
const version = skillVersion();
|
|
271
|
+
if (!version) {
|
|
272
|
+
console.error(SKILL_NAME + ' 版本未知(未找到 package.json)');
|
|
273
|
+
process.exitCode = 1;
|
|
274
|
+
return;
|
|
105
275
|
}
|
|
276
|
+
console.log(SKILL_NAME + ' v' + version);
|
|
106
277
|
return;
|
|
107
278
|
}
|
|
108
|
-
if (
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (agent) {
|
|
113
|
-
const info = AGENT_DIRS[agent];
|
|
279
|
+
if (opts.list) { printList(); return; }
|
|
280
|
+
|
|
281
|
+
if (opts.dir) { installTo(opts.dir, opts); return; }
|
|
282
|
+
|
|
283
|
+
if (opts.agent) {
|
|
284
|
+
const info = AGENT_DIRS[opts.agent];
|
|
114
285
|
if (!info) {
|
|
115
|
-
|
|
116
|
-
process.exitCode = 2;
|
|
117
|
-
return;
|
|
286
|
+
throw new UsageError('未收录智能体: ' + opts.agent + '。可用: ' + Object.keys(AGENT_DIRS).join(', ') + ';自定义目录请用 --dir <路径>。');
|
|
118
287
|
}
|
|
119
|
-
installTo(resolveUserDir(info.dirs[0]),
|
|
288
|
+
installTo(resolveUserDir(info.dirs[0]), opts);
|
|
289
|
+
console.log('完成。');
|
|
120
290
|
return;
|
|
121
291
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
console.error('Refusing --global without --yes. Use --dry-run to preview.');
|
|
125
|
-
process.exitCode = 2;
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
292
|
+
|
|
293
|
+
if (opts.global) {
|
|
128
294
|
const seen = new Set();
|
|
129
295
|
for (const value of Object.values(AGENT_DIRS)) {
|
|
130
296
|
for (const rel of value.dirs) {
|
|
131
297
|
if (seen.has(rel)) continue;
|
|
132
298
|
seen.add(rel);
|
|
133
|
-
installTo(resolveUserDir(rel),
|
|
299
|
+
installTo(resolveUserDir(rel), opts);
|
|
134
300
|
}
|
|
135
301
|
}
|
|
302
|
+
if (!opts.dryRun) console.log('完成。');
|
|
136
303
|
return;
|
|
137
304
|
}
|
|
138
|
-
|
|
139
|
-
|
|
305
|
+
|
|
306
|
+
const dirs = PROJECT_DIRS.filter((d) => fs.existsSync(d));
|
|
307
|
+
if (!dirs.length) {
|
|
308
|
+
throw new TargetError('未检测到项目级智能体目录。可用 --agent <name> 装到用户级,或用 --dir <路径> 指定目录。');
|
|
309
|
+
}
|
|
310
|
+
for (const dir of dirs) installTo(dir, opts);
|
|
311
|
+
if (!opts.dryRun) console.log('完成。');
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function main() {
|
|
315
|
+
try {
|
|
316
|
+
run();
|
|
317
|
+
} catch (err) {
|
|
318
|
+
if (err instanceof UsageError) {
|
|
319
|
+
console.error('用法错误: ' + err.message);
|
|
320
|
+
usage();
|
|
321
|
+
process.exitCode = 2;
|
|
322
|
+
} else if (err instanceof TargetError) {
|
|
323
|
+
console.error('目标错误: ' + err.message);
|
|
324
|
+
process.exitCode = 4;
|
|
325
|
+
} else if (err instanceof InstallError) {
|
|
326
|
+
console.error('安装失败: ' + err.message);
|
|
327
|
+
console.error('修复建议: 检查目录权限与磁盘空间后重试,或用 --dir 换一个目录。');
|
|
328
|
+
process.exitCode = 1;
|
|
329
|
+
} else {
|
|
330
|
+
console.error('未知错误: ' + (err && err.message ? err.message : String(err)));
|
|
331
|
+
process.exitCode = 1;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
140
334
|
}
|
|
141
335
|
|
|
142
336
|
main();
|
package/install.sh
CHANGED
|
@@ -1,9 +1,327 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
+
# yotta-dev-mcp 多智能体安装脚本(YottaSkills)
|
|
3
|
+
# 用法:
|
|
4
|
+
# bash install.sh --agent <name> # 按智能体默认用户级目录安装
|
|
5
|
+
# bash install.sh --dir <path> # 装到指定目录(用户改过目录的智能体)
|
|
6
|
+
# bash install.sh -g # 装到全部已知智能体用户级目录
|
|
7
|
+
# bash install.sh # 检测并安装到已存在的项目级目录
|
|
8
|
+
# bash install.sh --list # 列出智能体 -> 默认目录
|
|
9
|
+
# bash install.sh --dry-run # 只显示将写入的目标,不写文件
|
|
10
|
+
# bash install.sh --version # 显示版本
|
|
11
|
+
# bash install.sh --help # 显示帮助
|
|
12
|
+
# 退出码: 0 成功 / 1 安装失败 / 2 用法错误 / 4 目标错误
|
|
2
13
|
set -euo pipefail
|
|
3
14
|
|
|
15
|
+
SKILL_NAME="yotta-dev-mcp"
|
|
4
16
|
SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
17
|
+
_IS_WINDOWS=0
|
|
5
18
|
case "$(uname -s)" in
|
|
6
|
-
MINGW*|MSYS*)
|
|
19
|
+
MINGW*|MSYS*)
|
|
20
|
+
SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -W)"
|
|
21
|
+
_IS_WINDOWS=1
|
|
22
|
+
;;
|
|
23
|
+
CYGWIN*) _IS_WINDOWS=1 ;;
|
|
7
24
|
esac
|
|
8
25
|
|
|
9
|
-
|
|
26
|
+
# 智能体 -> 用户级默认目录(--agent 装到第一个)
|
|
27
|
+
# .agents/skills 并非通用目录:OpenCode / Cursor / Cline / Amp / Kimi / Gemini CLI / GitHub Copilot 读取。
|
|
28
|
+
dirs_for() {
|
|
29
|
+
case "$1" in
|
|
30
|
+
claude) echo ".claude/skills" ;;
|
|
31
|
+
cursor) echo ".cursor/skills .agents/skills" ;;
|
|
32
|
+
codex) echo "__CODEX__" ;;
|
|
33
|
+
gemini) echo ".gemini/skills .agents/skills" ;;
|
|
34
|
+
goose) echo ".config/goose/skills .agents/skills" ;;
|
|
35
|
+
amp) echo ".config/agents/skills .agents/skills" ;;
|
|
36
|
+
opencode) echo "__OPENCODE__" ;;
|
|
37
|
+
windsurf) echo ".codeium/windsurf/skills" ;;
|
|
38
|
+
workbuddy) echo ".workbuddy/skills" ;;
|
|
39
|
+
kiro) echo ".kiro/skills" ;;
|
|
40
|
+
trae) echo ".traecli/skills" ;;
|
|
41
|
+
trae-cn) echo ".trae-cn/skills" ;;
|
|
42
|
+
qwen) echo ".qwen/skills" ;;
|
|
43
|
+
comate) echo ".comate/skills" ;;
|
|
44
|
+
codebuddy) echo ".codebuddy/skills" ;;
|
|
45
|
+
kimi) echo ".kimi/skills" ;;
|
|
46
|
+
agents) echo ".agents/skills" ;;
|
|
47
|
+
*) return 1 ;;
|
|
48
|
+
esac
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
codex_dir() {
|
|
52
|
+
if [ -n "${CODEX_HOME:-}" ]; then printf '%s' "$CODEX_HOME/skills"; else printf '%s' "$HOME/.codex/skills"; fi
|
|
53
|
+
}
|
|
54
|
+
opencode_dir() {
|
|
55
|
+
if [ -n "${XDG_CONFIG_HOME:-}" ]; then printf '%s' "$XDG_CONFIG_HOME/opencode/skills"; else printf '%s' "$HOME/.config/opencode/skills"; fi
|
|
56
|
+
}
|
|
57
|
+
resolve_user() {
|
|
58
|
+
case "$1" in
|
|
59
|
+
__CODEX__) codex_dir ;;
|
|
60
|
+
__OPENCODE__) opencode_dir ;;
|
|
61
|
+
*) printf '%s' "$HOME/$1" ;;
|
|
62
|
+
esac
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
skill_version() {
|
|
66
|
+
local pkg="$SOURCE_DIR/package.json" v=""
|
|
67
|
+
[ -f "$pkg" ] || return 0
|
|
68
|
+
if command -v awk >/dev/null 2>&1; then
|
|
69
|
+
v="$(awk -F'"' '{for (i=1;i<=NF;i++) if ($i=="version") {print $(i+2); exit}}' "$pkg" 2>/dev/null || true)"
|
|
70
|
+
fi
|
|
71
|
+
if [ -z "$v" ]; then
|
|
72
|
+
v="$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$pkg" 2>/dev/null | head -n 1 || true)"
|
|
73
|
+
fi
|
|
74
|
+
printf '%s' "$v"
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
usage() {
|
|
78
|
+
echo "$SKILL_NAME 安装脚本(YottaSkills)"
|
|
79
|
+
echo ""
|
|
80
|
+
echo "用法:"
|
|
81
|
+
echo " bash install.sh --agent <name> 按智能体默认用户级目录安装(推荐)"
|
|
82
|
+
echo " bash install.sh --dir <path> 装到指定目录(用户改了目录的智能体)"
|
|
83
|
+
echo " bash install.sh -g 安装到全部已知智能体用户级目录"
|
|
84
|
+
echo " bash install.sh 安装到检测到的项目级目录"
|
|
85
|
+
echo ""
|
|
86
|
+
echo "参数:"
|
|
87
|
+
echo " --agent <name> 智能体键名,见 --list"
|
|
88
|
+
echo " --dir <path> 自定义技能目录"
|
|
89
|
+
echo " -g, --global 安装到全部已知用户级目录"
|
|
90
|
+
echo " --list, -l 列出支持的智能体目录"
|
|
91
|
+
echo " --dry-run 只显示将写入的目标,不写文件"
|
|
92
|
+
echo " --version, -v 显示版本"
|
|
93
|
+
echo " --help, -h 显示帮助"
|
|
94
|
+
echo " --yes, -y 兼容参数(-g 不再强制要求)"
|
|
95
|
+
echo ""
|
|
96
|
+
echo "退出码: 0 成功 / 1 安装失败 / 2 用法错误 / 4 目标错误"
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
resolve_path() {
|
|
100
|
+
# 与 SOURCE_DIR 同一风格(Windows Git Bash 用 pwd -W),供路径包含判断使用
|
|
101
|
+
local p="$1"
|
|
102
|
+
if [ "$_IS_WINDOWS" = "1" ]; then
|
|
103
|
+
(cd "$p" 2>/dev/null && pwd -W)
|
|
104
|
+
else
|
|
105
|
+
(cd "$p" 2>/dev/null && pwd -P)
|
|
106
|
+
fi
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
assert_safe_target() {
|
|
110
|
+
local dest="$1" src_real dest_real parent
|
|
111
|
+
src_real="$(resolve_path "$SOURCE_DIR")" || return 0
|
|
112
|
+
if [ -d "$dest" ]; then
|
|
113
|
+
dest_real="$(resolve_path "$dest")" || return 0
|
|
114
|
+
else
|
|
115
|
+
parent="$(dirname "$dest")"
|
|
116
|
+
dest_real="$(resolve_path "$parent")" || return 0
|
|
117
|
+
dest_real="$dest_real/$(basename "$dest")"
|
|
118
|
+
fi
|
|
119
|
+
case "$dest_real" in
|
|
120
|
+
"$src_real"|"$src_real"/*)
|
|
121
|
+
echo "安装失败:目标目录不能在技能源目录内(防止自装自毁)" >&2
|
|
122
|
+
return 1
|
|
123
|
+
;;
|
|
124
|
+
esac
|
|
125
|
+
return 0
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
# 清理旧版安装残留(fail-closed 白名单):仅在目标目录已存在且含 SKILL.md 时触发;
|
|
129
|
+
# 只删顶层开发项 + 任意层级缓存;不整目录删除、不跟随符号链接。
|
|
130
|
+
DEV_SKIP="package.json package-lock.json bin lib test .github .git .gitignore .npmignore .gitattributes .yotta .tmp install.sh node_modules"
|
|
131
|
+
clean_residue() {
|
|
132
|
+
local dest="$1" name p
|
|
133
|
+
[ -d "$dest" ] || return 0
|
|
134
|
+
[ -f "$dest/SKILL.md" ] || return 0
|
|
135
|
+
for name in $DEV_SKIP; do
|
|
136
|
+
p="$dest/$name"
|
|
137
|
+
[ -L "$p" ] && continue
|
|
138
|
+
if [ -e "$p" ]; then
|
|
139
|
+
rm -rf -- "$p"
|
|
140
|
+
echo "已清理残留: $name"
|
|
141
|
+
fi
|
|
142
|
+
done
|
|
143
|
+
find "$dest" -type d \( -name '__pycache__' -o -name '.pytest_cache' -o -name '.mypy_cache' \) -prune -exec rm -rf -- {} + 2>/dev/null || true
|
|
144
|
+
find "$dest" -type f \( -name '*.pyc' -o -name '*.pyo' \) -delete 2>/dev/null || true
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
# 复制技能本体:顶层跳过开发件与运行时不相关目录(嵌套同名保留);
|
|
148
|
+
# 任意层级跳过缓存(__pycache__ / .pytest_cache / .mypy_cache / *.pyc / *.pyo)。
|
|
149
|
+
copy_tree() {
|
|
150
|
+
local src="$1" dst="$2" top="$3" entry base
|
|
151
|
+
mkdir -p "$dst"
|
|
152
|
+
for entry in "$src"/* "$src"/.[!.]* "$src"/..?*; do
|
|
153
|
+
[ -e "$entry" ] || [ -L "$entry" ] || continue
|
|
154
|
+
base="$(basename "$entry")"
|
|
155
|
+
case "$base" in
|
|
156
|
+
__pycache__|.pytest_cache|.mypy_cache) continue ;;
|
|
157
|
+
esac
|
|
158
|
+
if [ "$top" = "1" ]; then
|
|
159
|
+
case " $DEV_SKIP " in
|
|
160
|
+
*" $base "*) continue ;;
|
|
161
|
+
esac
|
|
162
|
+
fi
|
|
163
|
+
if [ -d "$entry" ] && [ ! -L "$entry" ]; then
|
|
164
|
+
copy_tree "$entry" "$dst/$base" 0 || return 1
|
|
165
|
+
else
|
|
166
|
+
case "$base" in
|
|
167
|
+
*.pyc|*.pyo) continue ;;
|
|
168
|
+
esac
|
|
169
|
+
cp -RP "$entry" "$dst/$base" || return 1
|
|
170
|
+
fi
|
|
171
|
+
done
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
install_to() {
|
|
175
|
+
local base="$1" dry="$2" dest
|
|
176
|
+
dest="$base/$SKILL_NAME"
|
|
177
|
+
if [ -L "$base" ]; then
|
|
178
|
+
echo "安装失败:目标目录是符号链接,拒绝跟随:$base" >&2
|
|
179
|
+
return 1
|
|
180
|
+
fi
|
|
181
|
+
if [ -e "$base" ] && [ ! -d "$base" ]; then
|
|
182
|
+
echo "安装失败:目标路径已存在且不是目录:$base" >&2
|
|
183
|
+
return 1
|
|
184
|
+
fi
|
|
185
|
+
if [ -L "$dest" ]; then
|
|
186
|
+
echo "安装失败:技能目录是符号链接,拒绝跟随:$dest" >&2
|
|
187
|
+
return 1
|
|
188
|
+
fi
|
|
189
|
+
if [ -e "$dest" ] && [ ! -d "$dest" ]; then
|
|
190
|
+
echo "安装失败:技能路径已存在且不是目录:$dest" >&2
|
|
191
|
+
return 1
|
|
192
|
+
fi
|
|
193
|
+
assert_safe_target "$dest" || return 1
|
|
194
|
+
if [ "$dry" = "1" ]; then
|
|
195
|
+
echo "[dry-run] 将安装到 -> $dest"
|
|
196
|
+
return 0
|
|
197
|
+
fi
|
|
198
|
+
clean_residue "$dest"
|
|
199
|
+
mkdir -p "$dest" || return 1
|
|
200
|
+
copy_tree "$SOURCE_DIR" "$dest" 1 || return 1
|
|
201
|
+
if [ ! -f "$dest/SKILL.md" ]; then
|
|
202
|
+
echo "安装失败:结果缺少 SKILL.md" >&2
|
|
203
|
+
return 1
|
|
204
|
+
fi
|
|
205
|
+
echo "installed -> $dest"
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
list() {
|
|
209
|
+
echo "智能体 -> 默认技能目录(--agent <name> 装到第一个,用户级):"
|
|
210
|
+
local a dirs first
|
|
211
|
+
for a in claude cursor codex gemini goose amp opencode windsurf workbuddy kiro trae trae-cn qwen comate codebuddy kimi agents; do
|
|
212
|
+
dirs="$(dirs_for "$a")"
|
|
213
|
+
first="${dirs%% *}"
|
|
214
|
+
case "$first" in
|
|
215
|
+
__CODEX__) first=".codex/skills" ;;
|
|
216
|
+
__OPENCODE__) first=".config/opencode/skills" ;;
|
|
217
|
+
esac
|
|
218
|
+
if [ "$_IS_WINDOWS" = "1" ]; then
|
|
219
|
+
first="%USERPROFILE%\\${first//\//\\}"
|
|
220
|
+
else
|
|
221
|
+
first="~/$first"
|
|
222
|
+
fi
|
|
223
|
+
printf ' %-10s %s\n' "$a" "$first"
|
|
224
|
+
done
|
|
225
|
+
echo '说明:Windows 用 %USERPROFILE%,Linux/macOS 用 ~;仅收录有官方默认目录的智能体。'
|
|
226
|
+
echo '改了目录的请用 --dir <路径>,不要依赖默认位置;若设置了 CODEX_HOME / XDG_CONFIG_HOME,安装自动以该变量为准。'
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
main() {
|
|
230
|
+
local agent="" dir="" global=0 show_list=0 show_help=0 show_version=0 dry=0
|
|
231
|
+
while [ $# -gt 0 ]; do
|
|
232
|
+
case "$1" in
|
|
233
|
+
--help|-h) show_help=1 ;;
|
|
234
|
+
--version|-v) show_version=1 ;;
|
|
235
|
+
--list|-l) show_list=1 ;;
|
|
236
|
+
-g|--global) global=1 ;;
|
|
237
|
+
--dry-run) dry=1 ;;
|
|
238
|
+
--yes|-y) : ;;
|
|
239
|
+
--agent)
|
|
240
|
+
shift
|
|
241
|
+
if [ $# -eq 0 ] || [ -z "${1:-}" ]; then
|
|
242
|
+
echo "用法错误: --agent 需要一个非空名称" >&2
|
|
243
|
+
exit 2
|
|
244
|
+
fi
|
|
245
|
+
agent="$1"
|
|
246
|
+
;;
|
|
247
|
+
--dir)
|
|
248
|
+
shift
|
|
249
|
+
if [ $# -eq 0 ] || [ -z "${1:-}" ]; then
|
|
250
|
+
echo "用法错误: --dir 需要一个非空路径" >&2
|
|
251
|
+
exit 2
|
|
252
|
+
fi
|
|
253
|
+
dir="$1"
|
|
254
|
+
;;
|
|
255
|
+
*) echo "未知参数: $1" >&2; exit 2 ;;
|
|
256
|
+
esac
|
|
257
|
+
shift
|
|
258
|
+
done
|
|
259
|
+
|
|
260
|
+
if [ "$show_help" = "1" ]; then usage; return 0; fi
|
|
261
|
+
if [ "$show_version" = "1" ]; then
|
|
262
|
+
local v
|
|
263
|
+
v="$(skill_version)"
|
|
264
|
+
if [ -z "$v" ]; then
|
|
265
|
+
echo "$SKILL_NAME 版本未知(未找到 package.json)" >&2
|
|
266
|
+
exit 1
|
|
267
|
+
fi
|
|
268
|
+
echo "$SKILL_NAME v$v"
|
|
269
|
+
return 0
|
|
270
|
+
fi
|
|
271
|
+
if [ "$show_list" = "1" ]; then list; return 0; fi
|
|
272
|
+
|
|
273
|
+
local selected=0
|
|
274
|
+
[ -n "$dir" ] && selected=$((selected + 1))
|
|
275
|
+
[ -n "$agent" ] && selected=$((selected + 1))
|
|
276
|
+
[ "$global" = "1" ] && selected=$((selected + 1))
|
|
277
|
+
if [ "$selected" -gt 1 ]; then
|
|
278
|
+
echo "用法错误: --dir / --agent / -g 只能选一个" >&2
|
|
279
|
+
exit 2
|
|
280
|
+
fi
|
|
281
|
+
|
|
282
|
+
if [ -n "$dir" ]; then
|
|
283
|
+
install_to "$dir" "$dry" || exit 1
|
|
284
|
+
[ "$dry" = "1" ] || echo "完成。"
|
|
285
|
+
return 0
|
|
286
|
+
fi
|
|
287
|
+
if [ -n "$agent" ]; then
|
|
288
|
+
local dirs first
|
|
289
|
+
if ! dirs="$(dirs_for "$agent")"; then
|
|
290
|
+
echo "未收录智能体: $agent。可用 --list 查看,或 --dir <路径> 指定。" >&2
|
|
291
|
+
exit 2
|
|
292
|
+
fi
|
|
293
|
+
first="${dirs%% *}"
|
|
294
|
+
install_to "$(resolve_user "$first")" "$dry" || exit 1
|
|
295
|
+
[ "$dry" = "1" ] || echo "完成。"
|
|
296
|
+
return 0
|
|
297
|
+
fi
|
|
298
|
+
if [ "$global" = "1" ]; then
|
|
299
|
+
local a dirs rel seen=" "
|
|
300
|
+
for a in claude cursor codex gemini goose amp opencode windsurf workbuddy kiro trae trae-cn qwen comate codebuddy kimi agents; do
|
|
301
|
+
dirs="$(dirs_for "$a")" || continue
|
|
302
|
+
for rel in $dirs; do
|
|
303
|
+
case "$seen" in
|
|
304
|
+
*" $rel "*) continue ;;
|
|
305
|
+
esac
|
|
306
|
+
seen="$seen$rel "
|
|
307
|
+
install_to "$(resolve_user "$rel")" "$dry" || exit 1
|
|
308
|
+
done
|
|
309
|
+
done
|
|
310
|
+
[ "$dry" = "1" ] || echo "完成。"
|
|
311
|
+
return 0
|
|
312
|
+
fi
|
|
313
|
+
local installed=0 d
|
|
314
|
+
for d in .claude/skills .cursor/skills .codex/skills .config/goose/skills .config/agents/skills .opencode/skills .codeium/windsurf/skills .workbuddy/skills .kiro/skills .traecli/skills .gemini/skills .trae-cn/skills .qwen/skills .comate/skills .codebuddy/skills .kimi/skills .agents/skills; do
|
|
315
|
+
if [ -d "$d" ]; then
|
|
316
|
+
install_to "$d" "$dry" || exit 1
|
|
317
|
+
installed=1
|
|
318
|
+
fi
|
|
319
|
+
done
|
|
320
|
+
if [ "$installed" = "0" ]; then
|
|
321
|
+
echo "目标错误: 未检测到项目级智能体目录。可用 --agent <name> 装到用户级,或用 --dir <路径> 指定目录。" >&2
|
|
322
|
+
exit 4
|
|
323
|
+
fi
|
|
324
|
+
[ "$dry" = "1" ] || echo "完成。"
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
main "$@"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yottameta/yotta-dev-mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "YuanKai (yotta-dev-mcp) - deterministic local development tools exposed over stdio MCP: repo_map, system_model, architecture_review, impact_analysis, verify_change, self_test, run_adapter, find_code, compress_output, review_code, review_diff, mcp_doctor, scan_secrets, scan_dependencies, check_publish_readiness, run_checks, scaffold_skill and workflow_state. Reads an optional .yotta/architecture.json contract plus .yotta/verification.json policy for architecture reviews, change impact cones and the L0-L5 evidence ledger. Optional explicit adapters integrate import-linter, dependency-cruiser and Repomix without auto-install or network access. Python 3.8+ standard library, offline by default; writes and command execution require explicit flags.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
package/scripts/dev_engine.py
CHANGED
package/scripts/dev_selftest.py
CHANGED
|
@@ -52,22 +52,23 @@ def _protocol_tool_contracts(source_text):
|
|
|
52
52
|
tree = ast.parse(source_text)
|
|
53
53
|
function = None
|
|
54
54
|
for node in tree.body:
|
|
55
|
-
|
|
55
|
+
# v0.2.2 起工具清单字面量在 _all_tools();mcp_tools(profile) 只做分档过滤。
|
|
56
|
+
if isinstance(node, ast.FunctionDef) and node.name in ("_all_tools", "mcp_tools"):
|
|
56
57
|
function = node
|
|
57
58
|
break
|
|
58
59
|
if function is None:
|
|
59
|
-
raise ValueError("
|
|
60
|
+
raise ValueError("tool list function not found")
|
|
60
61
|
list_node = None
|
|
61
62
|
for node in ast.walk(function):
|
|
62
63
|
if isinstance(node, ast.Return) and isinstance(node.value, ast.List):
|
|
63
64
|
list_node = node.value
|
|
64
65
|
break
|
|
65
66
|
if list_node is None:
|
|
66
|
-
raise ValueError("
|
|
67
|
+
raise ValueError("tool list function does not return a literal tool list")
|
|
67
68
|
contracts = []
|
|
68
69
|
for element in list_node.elts:
|
|
69
70
|
if not isinstance(element, ast.Dict):
|
|
70
|
-
raise ValueError("
|
|
71
|
+
raise ValueError("tool list contains a non-literal tool entry")
|
|
71
72
|
values = {}
|
|
72
73
|
for key, value in zip(element.keys, element.values):
|
|
73
74
|
if _string_constant(key):
|
package/scripts/yotta_dev_mcp.py
CHANGED
|
@@ -25,6 +25,25 @@ MCP_PROTOCOL_MODERN = "2026-07-28"
|
|
|
25
25
|
MCP_PROTOCOL_LEGACY = "2025-11-25"
|
|
26
26
|
SERVER_INFO = {"name": TOOL_NAME, "version": VERSION}
|
|
27
27
|
|
|
28
|
+
# 工具分组(v0.2.2):与元忆 `--tools core|full` 同口径。core 是给「需要在上下文里
|
|
29
|
+
# 常驻工具表」的宿主用小档(5 工具,schema 远低于 8000 字符硬上限);full 保持 18 工具。
|
|
30
|
+
CORE_TOOL_NAMES = (
|
|
31
|
+
"repo_map",
|
|
32
|
+
"find_code",
|
|
33
|
+
"review_code",
|
|
34
|
+
"review_diff",
|
|
35
|
+
"verify_change",
|
|
36
|
+
)
|
|
37
|
+
TOOL_PROFILES = ("core", "full")
|
|
38
|
+
DEFAULT_TOOL_PROFILE = "full"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def normalize_tool_profile(value):
|
|
42
|
+
profile = str(value or "").strip().lower() or DEFAULT_TOOL_PROFILE
|
|
43
|
+
if profile not in TOOL_PROFILES:
|
|
44
|
+
raise ValueError("未知工具分组: %s(可用: core / full)" % value)
|
|
45
|
+
return profile
|
|
46
|
+
|
|
28
47
|
|
|
29
48
|
def _text_result(payload):
|
|
30
49
|
return {
|
|
@@ -63,7 +82,7 @@ TOOL_HANDLERS = {
|
|
|
63
82
|
}
|
|
64
83
|
|
|
65
84
|
|
|
66
|
-
def
|
|
85
|
+
def _all_tools():
|
|
67
86
|
return [
|
|
68
87
|
{
|
|
69
88
|
"name": "repo_map",
|
|
@@ -521,6 +540,15 @@ def mcp_tools():
|
|
|
521
540
|
]
|
|
522
541
|
|
|
523
542
|
|
|
543
|
+
def mcp_tools(profile=None):
|
|
544
|
+
"""按工具分档返回 tools/list 内容(core 5 个 / full 18 个)。"""
|
|
545
|
+
tools = _all_tools()
|
|
546
|
+
if normalize_tool_profile(profile) == "core":
|
|
547
|
+
by_name = {tool["name"]: tool for tool in tools}
|
|
548
|
+
return [by_name[name] for name in CORE_TOOL_NAMES]
|
|
549
|
+
return tools
|
|
550
|
+
|
|
551
|
+
|
|
524
552
|
def _req_version(params):
|
|
525
553
|
meta = (params or {}).get("_meta") or {}
|
|
526
554
|
return meta.get("io.modelcontextprotocol/protocolVersion")
|
|
@@ -548,7 +576,7 @@ def _unsupported_version(rid, protocol_version):
|
|
|
548
576
|
}
|
|
549
577
|
|
|
550
578
|
|
|
551
|
-
def handle_message(msg):
|
|
579
|
+
def handle_message(msg, tool_profile=None):
|
|
552
580
|
if not isinstance(msg, dict) or msg.get("jsonrpc") != "2.0":
|
|
553
581
|
rid = msg.get("id") if isinstance(msg, dict) else None
|
|
554
582
|
return {"jsonrpc": "2.0", "id": rid,
|
|
@@ -578,7 +606,9 @@ def handle_message(msg):
|
|
|
578
606
|
"verify_change for change-centered evidence, self_test for integrity "
|
|
579
607
|
"checks, and run_adapter to probe or explicitly run optional local "
|
|
580
608
|
"architecture adapters. Tools are offline and read-only unless an "
|
|
581
|
-
"explicit write or execute flag is set."
|
|
609
|
+
"explicit write or execute flag is set. Current tool profile: "
|
|
610
|
+
+ normalize_tool_profile(tool_profile)
|
|
611
|
+
+ " (start with --tools core|full)."
|
|
582
612
|
),
|
|
583
613
|
}, (3600000, "public")),
|
|
584
614
|
}
|
|
@@ -588,11 +618,20 @@ def handle_message(msg):
|
|
|
588
618
|
return {
|
|
589
619
|
"jsonrpc": "2.0",
|
|
590
620
|
"id": rid,
|
|
591
|
-
"result": _modern_ok({"tools": mcp_tools()}, (300000, "public")),
|
|
621
|
+
"result": _modern_ok({"tools": mcp_tools(tool_profile)}, (300000, "public")),
|
|
592
622
|
}
|
|
593
623
|
if method == "tools/call":
|
|
594
624
|
name = params.get("name")
|
|
595
625
|
arguments = params.get("arguments") or {}
|
|
626
|
+
if (normalize_tool_profile(tool_profile) == "core"
|
|
627
|
+
and name not in CORE_TOOL_NAMES):
|
|
628
|
+
return {
|
|
629
|
+
"jsonrpc": "2.0",
|
|
630
|
+
"id": rid,
|
|
631
|
+
"result": _modern_ok(_tool_error(
|
|
632
|
+
"工具 %s 属于 full 分组,当前以 --tools core 启动。"
|
|
633
|
+
"需要完整工具集时请用 --tools full 重新启动。" % name)),
|
|
634
|
+
}
|
|
596
635
|
handler = TOOL_HANDLERS.get(name)
|
|
597
636
|
if not handler:
|
|
598
637
|
return {
|
|
@@ -626,10 +665,16 @@ def handle_message(msg):
|
|
|
626
665
|
if method == "ping":
|
|
627
666
|
return {"jsonrpc": "2.0", "id": rid, "result": {}}
|
|
628
667
|
if method == "tools/list":
|
|
629
|
-
return {"jsonrpc": "2.0", "id": rid,
|
|
668
|
+
return {"jsonrpc": "2.0", "id": rid,
|
|
669
|
+
"result": {"tools": mcp_tools(tool_profile)}}
|
|
630
670
|
if method == "tools/call":
|
|
631
671
|
name = params.get("name")
|
|
632
672
|
arguments = params.get("arguments") or {}
|
|
673
|
+
if (normalize_tool_profile(tool_profile) == "core"
|
|
674
|
+
and name not in CORE_TOOL_NAMES):
|
|
675
|
+
return {"jsonrpc": "2.0", "id": rid, "result": _tool_error(
|
|
676
|
+
"工具 %s 属于 full 分组,当前以 --tools core 启动。"
|
|
677
|
+
"需要完整工具集时请用 --tools full 重新启动。" % name)}
|
|
633
678
|
handler = TOOL_HANDLERS.get(name)
|
|
634
679
|
if not handler:
|
|
635
680
|
return {
|
|
@@ -642,7 +687,42 @@ def handle_message(msg):
|
|
|
642
687
|
"error": {"code": -32601, "message": "Method not found: " + str(method)}}
|
|
643
688
|
|
|
644
689
|
|
|
690
|
+
def parse_args(argv):
|
|
691
|
+
"""解析启动参数:`--tools core|full`(默认 full)、`--version`、`--help`。"""
|
|
692
|
+
profile = DEFAULT_TOOL_PROFILE
|
|
693
|
+
index = 0
|
|
694
|
+
while index < len(argv):
|
|
695
|
+
arg = argv[index]
|
|
696
|
+
if arg == "--tools":
|
|
697
|
+
index += 1
|
|
698
|
+
if index >= len(argv):
|
|
699
|
+
sys.stderr.write("用法错误: --tools 需要一个值(core / full)\n")
|
|
700
|
+
raise SystemExit(2)
|
|
701
|
+
raw = argv[index]
|
|
702
|
+
elif arg.startswith("--tools="):
|
|
703
|
+
raw = arg.split("=", 1)[1]
|
|
704
|
+
elif arg in ("--version", "-v"):
|
|
705
|
+
print("%s %s" % (TOOL_NAME, VERSION))
|
|
706
|
+
raise SystemExit(0)
|
|
707
|
+
elif arg in ("--help", "-h"):
|
|
708
|
+
print("用法: yotta-dev-mcp [--tools core|full]")
|
|
709
|
+
print(" core: 5 工具(%s)" % " / ".join(CORE_TOOL_NAMES))
|
|
710
|
+
print(" full: 18 工具(默认)")
|
|
711
|
+
raise SystemExit(0)
|
|
712
|
+
else:
|
|
713
|
+
sys.stderr.write("用法错误: 未知参数 %s\n" % arg)
|
|
714
|
+
raise SystemExit(2)
|
|
715
|
+
try:
|
|
716
|
+
profile = normalize_tool_profile(raw)
|
|
717
|
+
except ValueError as exc:
|
|
718
|
+
sys.stderr.write("用法错误: %s\n" % exc)
|
|
719
|
+
raise SystemExit(2)
|
|
720
|
+
index += 1
|
|
721
|
+
return profile
|
|
722
|
+
|
|
723
|
+
|
|
645
724
|
def main():
|
|
725
|
+
tool_profile = parse_args(sys.argv[1:])
|
|
646
726
|
try:
|
|
647
727
|
sys.stdin.reconfigure(encoding="utf-8")
|
|
648
728
|
sys.stdout.reconfigure(encoding="utf-8")
|
|
@@ -659,7 +739,7 @@ def main():
|
|
|
659
739
|
response = {"jsonrpc": "2.0", "id": None,
|
|
660
740
|
"error": {"code": -32700, "message": "parse error"}}
|
|
661
741
|
else:
|
|
662
|
-
response = handle_message(message)
|
|
742
|
+
response = handle_message(message, tool_profile)
|
|
663
743
|
if response is not None:
|
|
664
744
|
sys.stdout.write(json.dumps(response, ensure_ascii=False) + "\n")
|
|
665
745
|
sys.stdout.flush()
|
package/server.json
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
"url": "https://github.com/YottaMeta/yotta-dev-mcp",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "0.2.
|
|
9
|
+
"version": "0.2.3",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "@yottameta/yotta-dev-mcp",
|
|
14
|
-
"version": "0.2.
|
|
14
|
+
"version": "0.2.3",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
}
|