jvibe 1.1.8 → 1.1.9
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 +30 -0
- package/README.md +39 -0
- package/bin/jvibe.js +14 -12
- package/lib/migrations/index.js +1 -8
- package/lib/plugins/core-tools.js +398 -0
- package/lib/plugins/plugins-yaml.js +49 -0
- package/lib/plugins/registry.json +396 -10
- package/package.json +1 -1
- package/scripts/init.js +37 -0
- package/scripts/plugins.js +159 -0
- package/scripts/setup.js +116 -58
- package/scripts/upgrade.js +29 -106
- package/template/.claude/hooks/load-jvibe-full-context.sh +77 -0
- package/template/.claude/hooks/sync-jvibe-context.sh +15 -9
- package/template/.claude/settings.json +2 -2
- package/template/.claude/skills/agent-browser/SKILL.md +252 -0
- package/template/docs/.jvibe/plugins.yaml +0 -1
- package/template/docs/core/Feature-List.md +0 -3
- package/template/docs/core/Standards.md +0 -3
package/CHANGELOG.md
CHANGED
|
@@ -17,6 +17,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
17
17
|
- **插件管理配置** (`plugins.yaml`)
|
|
18
18
|
- 新增 `docs/.jvibe/plugins.yaml` 管理工具与插件启用状态
|
|
19
19
|
- 区分 `core_plugins`(核心工具)与 `project_plugins`(项目按需)
|
|
20
|
+
- SessionStart hook 会从 `plugins.yaml` 注入 Core Tools 摘要到上下文
|
|
21
|
+
- 新增 `jvibe plugins core`:将缺失的 Core MCP Server 追加写入 `.claude/settings.local.json`(已存在则跳过)
|
|
20
22
|
|
|
21
23
|
- **上下文最小化原则**
|
|
22
24
|
- 所有 Agent 新增"硬规则"章节,禁止全仓库扫描
|
|
@@ -60,6 +62,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
60
62
|
- 改为 `|| echo 0` 确保输出为数字,避免后续算术运算错误
|
|
61
63
|
- 影响文件:`load-context.sh`、`load-jvibe-full-context.sh`、`sync-jvibe-context.sh`
|
|
62
64
|
|
|
65
|
+
- **UserPromptSubmit hook 错误处理**
|
|
66
|
+
- 移除 `set -eo pipefail`,改用 fail-open trap 机制
|
|
67
|
+
- 添加 `_JVIBE_HOOK_SUCCESS` 标记避免重复输出
|
|
68
|
+
- 修复 Project.md 摘要提取时 grep 管道无匹配导致的错误
|
|
69
|
+
- 确保非 JVibe 项目也能正常放行
|
|
70
|
+
|
|
71
|
+
- **TUI 插件选择写回**
|
|
72
|
+
- 修复已有 `docs/.jvibe/plugins.yaml` 时项目插件勾选不生效的问题
|
|
73
|
+
- 默认更新 `project_plugins`;勾选 Force Overwrite 会重置整个文件
|
|
74
|
+
|
|
63
75
|
## [1.1.7] - 2026-01-18
|
|
64
76
|
|
|
65
77
|
### Added
|
|
@@ -86,3 +98,21 @@ This release introduces a major architectural improvement focused on **context m
|
|
|
86
98
|
2. **Dual-mode Testing**: `targeted` (precise) vs `discover` (user-reported issues without F-XXX)
|
|
87
99
|
3. **Smarter Dispatch**: Main agent no longer directly fixes code; uses `bugfix` for complex issues
|
|
88
100
|
4. **Robust Hooks**: Fixed shell script arithmetic errors with proper fallback values
|
|
101
|
+
|
|
102
|
+
## [1.1.9] - 2026-01-20
|
|
103
|
+
|
|
104
|
+
### Added
|
|
105
|
+
|
|
106
|
+
- **Project Tools 注册表**:新增 `lib/plugins/registry.json` 的 Project Tools 完整条目(含 MCP 模板与依赖)
|
|
107
|
+
- **Core Tools: Skill 支持**:支持 Core Tools 以 `skill` 形态集成(Agent Browser)
|
|
108
|
+
- 固定 Agent Browser Skill 到 `v0.6.0`,并将 `SKILL.md` vendoring 到 `template/.claude/skills/agent-browser/SKILL.md`
|
|
109
|
+
|
|
110
|
+
### Changed
|
|
111
|
+
|
|
112
|
+
- **Core Tools 配置输出**:分离 MCP Server 与 Skill 的计数与提示(init/setup/plugins core)
|
|
113
|
+
|
|
114
|
+
### Fixed
|
|
115
|
+
|
|
116
|
+
- **Core Tools env 占位符**:写入 `.claude/settings.local.json` 时自动填充当前环境变量,并避免写入未解析的 `{{VAR}}` 占位符
|
|
117
|
+
- **Skill 下载鲁棒性**:为 Skill 下载增加超时/重定向/体积上限,避免卡死或异常大文件
|
|
118
|
+
- **非交互 stdin**:`jvibe plugins core` 在 CI/重定向 stdin 下默认跳过交互安装提示,避免挂起
|
package/README.md
CHANGED
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
- [文档体系](#-文档体系)
|
|
30
30
|
- [Agent 架构](#-agent-架构)
|
|
31
31
|
- [CLI 命令](#-cli-命令)
|
|
32
|
+
- [Core Tools 维护](#-core-tools-维护)
|
|
32
33
|
- [核心原则](#-核心原则)
|
|
33
34
|
- [���见问题](#-常见问题)
|
|
34
35
|
- [文档](#-文档)
|
|
@@ -329,6 +330,44 @@ TODO 完成情况 → 功能状态
|
|
|
329
330
|
|
|
330
331
|
---
|
|
331
332
|
|
|
333
|
+
## 🔌 Core Tools 维护
|
|
334
|
+
|
|
335
|
+
### Agent Browser Skill 更新
|
|
336
|
+
|
|
337
|
+
Agent Browser Skill 当前固定在 **v0.6.0** 版本,以确保稳定性。如需更新到最新版本:
|
|
338
|
+
|
|
339
|
+
#### 方法 1:手动更新(推荐)
|
|
340
|
+
|
|
341
|
+
```bash
|
|
342
|
+
# 1. 访问 GitHub Releases 页面查看最新版本
|
|
343
|
+
# https://github.com/vercel-labs/agent-browser/releases
|
|
344
|
+
|
|
345
|
+
# 2. 下载最新版本的 SKILL.md
|
|
346
|
+
curl -o .claude/skills/agent-browser/SKILL.md \
|
|
347
|
+
https://raw.githubusercontent.com/vercel-labs/agent-browser/v0.7.0/skills/agent-browser/SKILL.md
|
|
348
|
+
|
|
349
|
+
# 3. 更新 CLI 到对应版本
|
|
350
|
+
npm install -g agent-browser@latest
|
|
351
|
+
agent-browser install
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
#### 方法 2:修改 registry.json(高级用户)
|
|
355
|
+
|
|
356
|
+
如果你想让 `jvibe plugins core` 自动使用新版本:
|
|
357
|
+
|
|
358
|
+
1. 编辑 `lib/plugins/registry.json`
|
|
359
|
+
2. 找到 `agent-browser` 配置(约第 80 行)
|
|
360
|
+
3. 修改 `skillSource` 指向新版本:
|
|
361
|
+
```json
|
|
362
|
+
"skillSource": "https://raw.githubusercontent.com/vercel-labs/agent-browser/v0.7.0/skills/agent-browser/SKILL.md"
|
|
363
|
+
```
|
|
364
|
+
4. 删除现有 Skill 文件:`rm -rf .claude/skills/agent-browser`
|
|
365
|
+
5. 重新运行:`jvibe plugins core`
|
|
366
|
+
|
|
367
|
+
> **注意**:更新前请查看 [Agent Browser Changelog](https://github.com/vercel-labs/agent-browser/releases) 确认兼容性。
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
332
371
|
## 🎯 核心原则
|
|
333
372
|
|
|
334
373
|
JVibe 基于以下原则设计:
|
package/bin/jvibe.js
CHANGED
|
@@ -69,25 +69,14 @@ program
|
|
|
69
69
|
// upgrade 命令
|
|
70
70
|
program
|
|
71
71
|
.command('upgrade')
|
|
72
|
-
.description('
|
|
72
|
+
.description('升级到最新版本(卸载重装)')
|
|
73
73
|
.option('--check', '仅检查更新,不执行升级', false)
|
|
74
74
|
.option('--force', '强制升级,跳过确认', false)
|
|
75
|
-
.option('--migrate', '仅执行迁移,不更新到最新版本', false)
|
|
76
75
|
.action(async (options) => {
|
|
77
76
|
const upgrade = require('../scripts/upgrade');
|
|
78
77
|
await upgrade(options);
|
|
79
78
|
});
|
|
80
79
|
|
|
81
|
-
// migrate 命令(upgrade --migrate 的别名)
|
|
82
|
-
program
|
|
83
|
-
.command('migrate')
|
|
84
|
-
.description('迁移旧版本配置到新格式')
|
|
85
|
-
.option('--force', '强制迁移,跳过确认', false)
|
|
86
|
-
.action(async (options) => {
|
|
87
|
-
const upgrade = require('../scripts/upgrade');
|
|
88
|
-
await upgrade({ ...options, migrate: true });
|
|
89
|
-
});
|
|
90
|
-
|
|
91
80
|
// uninstall 命令
|
|
92
81
|
program
|
|
93
82
|
.command('uninstall')
|
|
@@ -120,6 +109,19 @@ program
|
|
|
120
109
|
await validate();
|
|
121
110
|
});
|
|
122
111
|
|
|
112
|
+
// plugins 命令
|
|
113
|
+
const plugins = program
|
|
114
|
+
.command('plugins')
|
|
115
|
+
.description('插件相关操作(Core Tools 等)');
|
|
116
|
+
|
|
117
|
+
plugins
|
|
118
|
+
.command('core')
|
|
119
|
+
.description('配置 Core Tools(写入缺失的 MCP Server 到 .claude/settings.local.json)')
|
|
120
|
+
.action(async () => {
|
|
121
|
+
const { configureCore } = require('../scripts/plugins');
|
|
122
|
+
await configureCore();
|
|
123
|
+
});
|
|
124
|
+
|
|
123
125
|
(async () => {
|
|
124
126
|
try {
|
|
125
127
|
const handled = await maybeRunSetup();
|
package/lib/migrations/index.js
CHANGED
|
@@ -81,13 +81,6 @@ const MIGRATIONS = [
|
|
|
81
81
|
format: '**优先级**:P0 | P1 | P2 | P3',
|
|
82
82
|
example: '**优先级**:P1'
|
|
83
83
|
},
|
|
84
|
-
{
|
|
85
|
-
file: 'docs/core/Feature-List.md',
|
|
86
|
-
field: '预估工时',
|
|
87
|
-
description: '功能条目新增预估工时字段',
|
|
88
|
-
format: '**预估工时**:Xh | Xd',
|
|
89
|
-
example: '**预估工时**:4h'
|
|
90
|
-
},
|
|
91
84
|
{
|
|
92
85
|
file: 'docs/core/Feature-List.md',
|
|
93
86
|
field: '关联模块',
|
|
@@ -108,7 +101,7 @@ const MIGRATIONS = [
|
|
|
108
101
|
field: '功能条目格式',
|
|
109
102
|
description: '功能条目格式扩展,支持更多元数据',
|
|
110
103
|
oldFormat: '## F-XXX [状态] 功能名称\n\n**描述**:...\n\n**TODO**\n- [ ] ...',
|
|
111
|
-
newFormat: '## F-XXX [状态] 功能名称\n\n**描述**:...\n**优先级**:P1\n
|
|
104
|
+
newFormat: '## F-XXX [状态] 功能名称\n\n**描述**:...\n**优先级**:P1\n**关联模块**:ModuleName\n\n**TODO**\n- [ ] ...'
|
|
112
105
|
}
|
|
113
106
|
],
|
|
114
107
|
removed: [],
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { parsePluginListsFromYaml } = require('./plugins-yaml');
|
|
4
|
+
|
|
5
|
+
const PLUGIN_REGISTRY_PATH = path.join(__dirname, 'registry.json');
|
|
6
|
+
|
|
7
|
+
const DEFAULT_CORE_PLUGINS = [
|
|
8
|
+
'serena',
|
|
9
|
+
'filesystem-mcp',
|
|
10
|
+
'github-mcp',
|
|
11
|
+
'context7',
|
|
12
|
+
'agent-browser'
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
async function readJsonIfExists(filePath) {
|
|
16
|
+
try {
|
|
17
|
+
if (!await fs.pathExists(filePath)) return null;
|
|
18
|
+
return await fs.readJson(filePath);
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function substituteTemplateString(value, variables) {
|
|
25
|
+
if (typeof value !== 'string') return value;
|
|
26
|
+
return value.replace(/\{\{\s*([A-Za-z0-9_]+)\s*\}\}/g, (match, key) => (
|
|
27
|
+
Object.prototype.hasOwnProperty.call(variables, key) ? String(variables[key]) : match
|
|
28
|
+
));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function substituteTemplateDeep(value, variables) {
|
|
32
|
+
if (Array.isArray(value)) {
|
|
33
|
+
return value.map(item => substituteTemplateDeep(item, variables));
|
|
34
|
+
}
|
|
35
|
+
if (value && typeof value === 'object') {
|
|
36
|
+
const out = {};
|
|
37
|
+
for (const [k, v] of Object.entries(value)) {
|
|
38
|
+
out[k] = substituteTemplateDeep(v, variables);
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
return substituteTemplateString(value, variables);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function extractNpxPackageArg(template) {
|
|
46
|
+
if (!template || typeof template !== 'object') return null;
|
|
47
|
+
if (template.command !== 'npx') return null;
|
|
48
|
+
const args = Array.isArray(template.args) ? template.args : null;
|
|
49
|
+
if (!args) return null;
|
|
50
|
+
|
|
51
|
+
for (const arg of args) {
|
|
52
|
+
if (typeof arg !== 'string') continue;
|
|
53
|
+
if (arg === '-y') continue;
|
|
54
|
+
if (arg.startsWith('-')) continue;
|
|
55
|
+
if (arg.includes('{{')) continue;
|
|
56
|
+
return arg;
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function getRequiredEnvKeys(plugin) {
|
|
62
|
+
const keys = plugin && plugin.requires && Array.isArray(plugin.requires.env)
|
|
63
|
+
? plugin.requires.env
|
|
64
|
+
: [];
|
|
65
|
+
return keys.filter(k => typeof k === 'string' && k.length > 0);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function buildTemplateVariables(plugin, cwd) {
|
|
69
|
+
const requiredEnvKeys = getRequiredEnvKeys(plugin);
|
|
70
|
+
const missingEnvKeys = [];
|
|
71
|
+
const variables = { project_root: cwd };
|
|
72
|
+
|
|
73
|
+
for (const key of requiredEnvKeys) {
|
|
74
|
+
const value = process.env[key];
|
|
75
|
+
if (typeof value === 'string' && value.length > 0) {
|
|
76
|
+
variables[key] = value;
|
|
77
|
+
} else {
|
|
78
|
+
missingEnvKeys.push(key);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return { variables, missingEnvKeys };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function stripUnresolvedTemplateEnv(serverConfig) {
|
|
86
|
+
if (!serverConfig || typeof serverConfig !== 'object') return serverConfig;
|
|
87
|
+
if (!serverConfig.env || typeof serverConfig.env !== 'object') return serverConfig;
|
|
88
|
+
|
|
89
|
+
const envEntries = Object.entries(serverConfig.env);
|
|
90
|
+
const cleanedEnv = {};
|
|
91
|
+
|
|
92
|
+
for (const [key, value] of envEntries) {
|
|
93
|
+
if (typeof value === 'string' && value.includes('{{') && value.includes('}}')) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
cleanedEnv[key] = value;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (Object.keys(cleanedEnv).length === envEntries.length) return serverConfig;
|
|
100
|
+
const out = { ...serverConfig };
|
|
101
|
+
if (Object.keys(cleanedEnv).length === 0) {
|
|
102
|
+
delete out.env;
|
|
103
|
+
} else {
|
|
104
|
+
out.env = cleanedEnv;
|
|
105
|
+
}
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function loadPluginRegistry() {
|
|
110
|
+
try {
|
|
111
|
+
const registry = await fs.readJson(PLUGIN_REGISTRY_PATH);
|
|
112
|
+
const plugins = Array.isArray(registry.plugins) ? registry.plugins : [];
|
|
113
|
+
return { ...registry, plugins };
|
|
114
|
+
} catch {
|
|
115
|
+
return { version: 1, plugins: [] };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function getCorePluginIdsFromRegistry(registry) {
|
|
120
|
+
const plugins = Array.isArray(registry.plugins) ? registry.plugins : [];
|
|
121
|
+
const core = plugins
|
|
122
|
+
.filter(p => p && typeof p.id === 'string' && p.default_tier === 'core')
|
|
123
|
+
.map(p => p.id);
|
|
124
|
+
return core.length > 0 ? core : DEFAULT_CORE_PLUGINS;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function getCorePluginIdsFromProject(cwd, registry) {
|
|
128
|
+
const pluginsPath = path.join(cwd, 'docs', '.jvibe', 'plugins.yaml');
|
|
129
|
+
try {
|
|
130
|
+
if (await fs.pathExists(pluginsPath)) {
|
|
131
|
+
const raw = await fs.readFile(pluginsPath, 'utf-8');
|
|
132
|
+
const parsed = parsePluginListsFromYaml(raw);
|
|
133
|
+
const core = Array.isArray(parsed.core_plugins) ? parsed.core_plugins : [];
|
|
134
|
+
if (core.length > 0) return core;
|
|
135
|
+
}
|
|
136
|
+
} catch {
|
|
137
|
+
// ignore
|
|
138
|
+
}
|
|
139
|
+
return getCorePluginIdsFromRegistry(registry);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function fetchSkillFile(url, redirectCount = 0) {
|
|
143
|
+
const https = require('https');
|
|
144
|
+
const http = require('http');
|
|
145
|
+
const MAX_REDIRECTS = 5;
|
|
146
|
+
const TIMEOUT_MS = 10000;
|
|
147
|
+
const MAX_SIZE_BYTES = 10 * 1024 * 1024; // 10MB
|
|
148
|
+
|
|
149
|
+
if (redirectCount > MAX_REDIRECTS) {
|
|
150
|
+
throw new Error(`Too many redirects (>${MAX_REDIRECTS})`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return new Promise((resolve, reject) => {
|
|
154
|
+
const client = url.startsWith('https') ? https : http;
|
|
155
|
+
const req = client.get(url, { timeout: TIMEOUT_MS }, (res) => {
|
|
156
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
157
|
+
return fetchSkillFile(res.headers.location, redirectCount + 1).then(resolve).catch(reject);
|
|
158
|
+
}
|
|
159
|
+
if (res.statusCode !== 200) {
|
|
160
|
+
return reject(new Error(`HTTP ${res.statusCode}`));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let data = '';
|
|
164
|
+
let size = 0;
|
|
165
|
+
|
|
166
|
+
res.on('data', chunk => {
|
|
167
|
+
size += chunk.length;
|
|
168
|
+
if (size > MAX_SIZE_BYTES) {
|
|
169
|
+
req.destroy();
|
|
170
|
+
return reject(new Error(`Response too large (>${MAX_SIZE_BYTES} bytes)`));
|
|
171
|
+
}
|
|
172
|
+
data += chunk;
|
|
173
|
+
});
|
|
174
|
+
res.on('end', () => resolve(data));
|
|
175
|
+
res.on('error', reject);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
req.on('timeout', () => {
|
|
179
|
+
req.destroy();
|
|
180
|
+
reject(new Error(`Request timeout (>${TIMEOUT_MS}ms)`));
|
|
181
|
+
});
|
|
182
|
+
req.on('error', reject);
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function checkCommandExists(command) {
|
|
187
|
+
if (!command || typeof command !== 'string') return false;
|
|
188
|
+
|
|
189
|
+
// If it's an explicit path, check it directly.
|
|
190
|
+
if (command.includes('/') || command.includes('\\')) {
|
|
191
|
+
try {
|
|
192
|
+
return fs.statSync(command).isFile();
|
|
193
|
+
} catch {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const pathValue = process.env.PATH || '';
|
|
199
|
+
const pathEntries = pathValue.split(path.delimiter).filter(Boolean);
|
|
200
|
+
const isWindows = process.platform === 'win32';
|
|
201
|
+
const pathExts = isWindows
|
|
202
|
+
? (process.env.PATHEXT ? process.env.PATHEXT.split(';') : ['.EXE', '.CMD', '.BAT', '.COM'])
|
|
203
|
+
: [''];
|
|
204
|
+
const hasWindowsExt = isWindows && /\.[A-Za-z0-9]+$/.test(command);
|
|
205
|
+
|
|
206
|
+
for (const dir of pathEntries) {
|
|
207
|
+
if (hasWindowsExt) {
|
|
208
|
+
const fullPath = path.join(dir, command);
|
|
209
|
+
try {
|
|
210
|
+
if (fs.statSync(fullPath).isFile()) return true;
|
|
211
|
+
} catch {
|
|
212
|
+
// continue
|
|
213
|
+
}
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
for (const ext of pathExts) {
|
|
218
|
+
const fullPath = path.join(dir, isWindows ? `${command}${ext}` : command);
|
|
219
|
+
try {
|
|
220
|
+
if (fs.statSync(fullPath).isFile()) return true;
|
|
221
|
+
} catch {
|
|
222
|
+
// continue
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function configureSkillPlugin(cwd, plugin, result) {
|
|
231
|
+
const claudeConfig = plugin.claude;
|
|
232
|
+
if (!claudeConfig || !claudeConfig.skillDir || !claudeConfig.skillSource) {
|
|
233
|
+
result.missingTemplates.push(plugin.id);
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const skillDir = path.join(cwd, claudeConfig.skillDir);
|
|
238
|
+
const skillFile = path.join(skillDir, 'SKILL.md');
|
|
239
|
+
const cliCommand = claudeConfig.skillName || plugin.id;
|
|
240
|
+
const globalInstall = claudeConfig.globalInstall || null;
|
|
241
|
+
|
|
242
|
+
// 检查 Skill 是否已存在
|
|
243
|
+
if (await fs.pathExists(skillFile)) {
|
|
244
|
+
if (globalInstall && cliCommand && !checkCommandExists(cliCommand)) {
|
|
245
|
+
result.skillsNeedingCli.push({
|
|
246
|
+
pluginId: plugin.id,
|
|
247
|
+
skillDir: claudeConfig.skillDir,
|
|
248
|
+
cliCommand,
|
|
249
|
+
globalInstall
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
result.skipped += 1;
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// 下载 SKILL.md
|
|
257
|
+
try {
|
|
258
|
+
const content = await fetchSkillFile(claudeConfig.skillSource);
|
|
259
|
+
await fs.ensureDir(skillDir);
|
|
260
|
+
await fs.writeFile(skillFile, content, 'utf-8');
|
|
261
|
+
result.added += 1;
|
|
262
|
+
result.skillsAdded.push({ pluginId: plugin.id, skillDir: claudeConfig.skillDir });
|
|
263
|
+
|
|
264
|
+
if (globalInstall && cliCommand && !checkCommandExists(cliCommand)) {
|
|
265
|
+
result.skillsNeedingCli.push({
|
|
266
|
+
pluginId: plugin.id,
|
|
267
|
+
skillDir: claudeConfig.skillDir,
|
|
268
|
+
cliCommand,
|
|
269
|
+
globalInstall
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
return true;
|
|
273
|
+
} catch (e) {
|
|
274
|
+
result.missingTemplates.push(plugin.id);
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function configureClaudeCoreTools(cwd, registry) {
|
|
280
|
+
const claudeDir = path.join(cwd, '.claude');
|
|
281
|
+
if (!await fs.pathExists(claudeDir)) {
|
|
282
|
+
return { added: 0, skipped: 0, missingTemplates: [], missingEnv: [], mcpAdded: 0, skillsAdded: [], skillsNeedingCli: [] };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
286
|
+
const settingsLocalPath = path.join(claudeDir, 'settings.local.json');
|
|
287
|
+
|
|
288
|
+
const settings = await readJsonIfExists(settingsPath) || {};
|
|
289
|
+
const settingsLocal = await readJsonIfExists(settingsLocalPath);
|
|
290
|
+
|
|
291
|
+
if (await fs.pathExists(settingsLocalPath) && settingsLocal === null) {
|
|
292
|
+
return { added: 0, skipped: 0, missingTemplates: [], missingEnv: [], mcpAdded: 0, skillsAdded: [], skillsNeedingCli: [], error: 'settings.local.json 解析失败,已跳过自动配置' };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const local = settingsLocal || {};
|
|
296
|
+
const settingsServers = settings.mcpServers || {};
|
|
297
|
+
const localServers = local.mcpServers || {};
|
|
298
|
+
const existingServers = new Set([
|
|
299
|
+
...Object.keys(settingsServers),
|
|
300
|
+
...Object.keys(localServers)
|
|
301
|
+
]);
|
|
302
|
+
const existingServerConfigs = [
|
|
303
|
+
...Object.values(settingsServers),
|
|
304
|
+
...Object.values(localServers)
|
|
305
|
+
].filter(Boolean);
|
|
306
|
+
|
|
307
|
+
const corePluginIds = await getCorePluginIdsFromProject(cwd, registry);
|
|
308
|
+
|
|
309
|
+
const result = {
|
|
310
|
+
added: 0,
|
|
311
|
+
skipped: 0,
|
|
312
|
+
missingTemplates: [],
|
|
313
|
+
missingEnv: [],
|
|
314
|
+
mcpAdded: 0,
|
|
315
|
+
skillsNeedingCli: [],
|
|
316
|
+
skillsAdded: []
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
let mcpAdded = 0;
|
|
320
|
+
|
|
321
|
+
for (const pluginId of corePluginIds) {
|
|
322
|
+
const plugin = registry.plugins.find(p => p && p.id === pluginId);
|
|
323
|
+
if (!plugin) continue;
|
|
324
|
+
|
|
325
|
+
const integrationType = plugin.integration && plugin.integration.type;
|
|
326
|
+
|
|
327
|
+
// 处理 Skill 类型插件
|
|
328
|
+
if (integrationType === 'skill') {
|
|
329
|
+
await configureSkillPlugin(cwd, plugin, result);
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// 处理 MCP 类型插件
|
|
334
|
+
if (integrationType !== 'mcp') continue;
|
|
335
|
+
|
|
336
|
+
const { variables, missingEnvKeys } = buildTemplateVariables(plugin, cwd);
|
|
337
|
+
if (missingEnvKeys.length > 0) {
|
|
338
|
+
result.missingEnv.push({ pluginId, keys: missingEnvKeys });
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const serverName = plugin.claude && typeof plugin.claude.mcpServerName === 'string'
|
|
342
|
+
? plugin.claude.mcpServerName
|
|
343
|
+
: pluginId;
|
|
344
|
+
const aliasNames = new Set([
|
|
345
|
+
pluginId,
|
|
346
|
+
serverName,
|
|
347
|
+
...((plugin.claude && Array.isArray(plugin.claude.mcpAliases)) ? plugin.claude.mcpAliases : [])
|
|
348
|
+
]);
|
|
349
|
+
|
|
350
|
+
const alreadyConfigured = [...aliasNames].some(name => existingServers.has(name));
|
|
351
|
+
if (alreadyConfigured) {
|
|
352
|
+
result.skipped += 1;
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const template = plugin.claude && plugin.claude.mcpServer ? plugin.claude.mcpServer : null;
|
|
357
|
+
if (!template) {
|
|
358
|
+
result.missingTemplates.push(pluginId);
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const npxPackageArg = extractNpxPackageArg(template);
|
|
363
|
+
const alreadyConfiguredBySignature = npxPackageArg
|
|
364
|
+
? existingServerConfigs.some(cfg => (
|
|
365
|
+
cfg &&
|
|
366
|
+
typeof cfg === 'object' &&
|
|
367
|
+
cfg.command === 'npx' &&
|
|
368
|
+
Array.isArray(cfg.args) &&
|
|
369
|
+
cfg.args.includes(npxPackageArg)
|
|
370
|
+
))
|
|
371
|
+
: false;
|
|
372
|
+
if (alreadyConfiguredBySignature) {
|
|
373
|
+
result.skipped += 1;
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const resolved = stripUnresolvedTemplateEnv(substituteTemplateDeep(template, variables));
|
|
378
|
+
local.mcpServers = local.mcpServers || {};
|
|
379
|
+
local.mcpServers[serverName] = resolved;
|
|
380
|
+
existingServers.add(serverName);
|
|
381
|
+
result.added += 1;
|
|
382
|
+
mcpAdded += 1;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (mcpAdded > 0) {
|
|
386
|
+
await fs.writeJson(settingsLocalPath, local, { spaces: 2 });
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
result.mcpAdded = mcpAdded;
|
|
390
|
+
return result;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
module.exports = {
|
|
394
|
+
loadPluginRegistry,
|
|
395
|
+
getCorePluginIdsFromRegistry,
|
|
396
|
+
getCorePluginIdsFromProject,
|
|
397
|
+
configureClaudeCoreTools
|
|
398
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
function stripYamlComment(line) {
|
|
2
|
+
const index = line.indexOf('#');
|
|
3
|
+
return index === -1 ? line : line.slice(0, index);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function parsePluginListsFromYaml(content) {
|
|
7
|
+
const result = {};
|
|
8
|
+
let currentKey = null;
|
|
9
|
+
|
|
10
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
11
|
+
const line = stripYamlComment(rawLine).trim();
|
|
12
|
+
if (!line) {
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const keyMatch = line.match(/^([A-Za-z0-9_]+):\s*(.*)$/);
|
|
17
|
+
if (keyMatch) {
|
|
18
|
+
const key = keyMatch[1];
|
|
19
|
+
const value = keyMatch[2].trim();
|
|
20
|
+
currentKey = null;
|
|
21
|
+
|
|
22
|
+
if (value === '' || value === '[]') {
|
|
23
|
+
result[key] = [];
|
|
24
|
+
if (value === '') {
|
|
25
|
+
currentKey = key;
|
|
26
|
+
}
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
result[key] = value.replace(/^['"]|['"]$/g, '');
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const itemMatch = line.match(/^-+\s*(.+)$/);
|
|
35
|
+
if (itemMatch && currentKey) {
|
|
36
|
+
const item = itemMatch[1].trim().replace(/^['"]|['"]$/g, '');
|
|
37
|
+
if (item) {
|
|
38
|
+
result[currentKey].push(item);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = {
|
|
47
|
+
stripYamlComment,
|
|
48
|
+
parsePluginListsFromYaml
|
|
49
|
+
};
|