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 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();
@@ -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**预估工时**:4h\n**关联模块**:ModuleName\n\n**TODO**\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
+ };