@fanchao8609/agent_brain_sync 1.5.6 → 1.5.7
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/package.json +1 -1
- package/src/install.js +202 -15
package/package.json
CHANGED
package/src/install.js
CHANGED
|
@@ -149,7 +149,7 @@ async function readJson(p, { strict = true } = {}) {
|
|
|
149
149
|
// 关键: 「文件不存在」与「存在但解析失败」必须分开处理。
|
|
150
150
|
// 曾经两者都返回 {},于是只要用户的配置里有 JSONC 注释/尾逗号/多一个字符,
|
|
151
151
|
// 就会被当作空对象重建 → 用户的 hooks/permissions/model/mcpServers 静默全消失。
|
|
152
|
-
// 而 JSONC(带 // 注释)正是
|
|
152
|
+
// 而 JSONC(带 // 注释)正是 CLaude Code 官方文档鼓励的写法,命中率不低。
|
|
153
153
|
// 测例: 带注释的 settings.json 安装后 myKey/permissions 全丢,且无任何报错。
|
|
154
154
|
// 处置: 宁可整个安装失败,也不写坏用户文件(与 requireBrain 同原则)。
|
|
155
155
|
// 宽松模式(strict:false)供"卸载"使用: 跳过配置文件、但仍清理 abs 自己的脚本/skill,
|
|
@@ -206,7 +206,7 @@ function entryHasAbs(entry) {
|
|
|
206
206
|
// 为什么不用正则: 曾用 /\n?\[mcp_servers\.abs\][^\[]*/s 删 section,
|
|
207
207
|
// 而 `[^\[]*` 会在下一个 `[` 处停下 —— `args = ["/x/mcp.js"]` 的数组左括号就是 `[`。
|
|
208
208
|
// 结果卸载后把数组值原地截成活一个假 section 头,留下非法行 `["/…/mcp.js"]`,
|
|
209
|
-
// 用户
|
|
209
|
+
// 用户 codex 启动时 TOML 解析直接失败(卸载却给用户留个坏配置)。
|
|
210
210
|
// 另一坑: 用 includes('[mcp_servers.abs]') 判"已存在"不区分注释 ——
|
|
211
211
|
// 用户配置里一句 `# 例: [mcp_servers.abs]` 就让安装器报"已存在且路径正确, 跳过",
|
|
212
212
|
// 实际从未注册(静默失效,且重装永不修复)。
|
|
@@ -231,6 +231,27 @@ function findTomlAbsSection(lines) {
|
|
|
231
231
|
return { start, end };
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
+
/** 找 section 内某 key 的赋值行区间 [start, end)(支持多行数组/内联表);
|
|
235
|
+
* 找不到返回 null。用于把 args 作为**整体**替换,而不是只换第一行。
|
|
236
|
+
* 值可以是 `= [...]`(可跨行到配对的 `]`)、`= "..."`、`= 123` 等。 */
|
|
237
|
+
function findTomlKeyRange(lines, sec, key) {
|
|
238
|
+
const re = new RegExp('^\\s*' + key + '\\s*=');
|
|
239
|
+
const start = lines.findIndex((l, i) =>
|
|
240
|
+
i >= sec.start && i < sec.end && !l.trimStart().startsWith('#') && re.test(l));
|
|
241
|
+
if (start === -1) return null;
|
|
242
|
+
// 从 `=` 之后数括号: 若有未闭合的 `[` 则继续吃到配对的 `]`(跨行数组)
|
|
243
|
+
let end = start + 1;
|
|
244
|
+
const idx = lines[start].indexOf('=');
|
|
245
|
+
const tail = lines[start].slice(idx + 1);
|
|
246
|
+
let depth = 0;
|
|
247
|
+
for (const ch of tail) { if (ch === '[') depth++; else if (ch === ']') depth--; }
|
|
248
|
+
while (depth > 0 && end < sec.end) {
|
|
249
|
+
for (const ch of lines[end]) { if (ch === '[') depth++; else if (ch === ']') depth--; }
|
|
250
|
+
end++;
|
|
251
|
+
}
|
|
252
|
+
return { start, end };
|
|
253
|
+
}
|
|
254
|
+
|
|
234
255
|
/** 删除 [mcp_servers.abs] section(含其 body);无则原样返回。
|
|
235
256
|
* @returns {{ text: string, removed: boolean }} */
|
|
236
257
|
function removeTomlAbsSection(text) {
|
|
@@ -269,6 +290,142 @@ function claudeSettingsPath() {
|
|
|
269
290
|
return join(process.env.CLAUDE_CONFIG_DIR || join(homedir(), '.claude'), 'settings.json');
|
|
270
291
|
}
|
|
271
292
|
|
|
293
|
+
/**
|
|
294
|
+
* **外部 MCP store** 清单 — 这些文件不归 abs 管,但 pi-mcp-adapter 的
|
|
295
|
+
* hostConfigDiscovery 会把它们当权威读(dist/config.js IMPORT_PATHS + extractServers),
|
|
296
|
+
* 于是里面的 mcpServers.abs 若指向仓库/旧路径,就会**盖过 abs 自己写对的注册**。
|
|
297
|
+
*
|
|
298
|
+
* 坑: 这些文件是别的工具/用户的(`claude mcp add`、Cursor/Windsurf/VS Code 各自写),
|
|
299
|
+
* abs 原本从不碰 —— 里面的 abs 条目成了**无主陈旧配置**: 一旦某次写的是仓库路径
|
|
300
|
+
* (当时全局包还不存在,或手工写入),之后无人校正,永不失效。
|
|
301
|
+
*
|
|
302
|
+
* 处置: 只校正「已存在」条目的 args,绝不新增条目 —— 不主动往别人的 store 里塞东西;
|
|
303
|
+
* 文件/条目不存在则对 abs 无影响,静默跳过。
|
|
304
|
+
*
|
|
305
|
+
* 注: 路径按各工具官方布局写死;找不到就是没装,不报错。CoDEX 的 TOML 形态另由
|
|
306
|
+
* installCoDEX 处理,此处只管 JSON。
|
|
307
|
+
*/
|
|
308
|
+
function foreignMcpStores() {
|
|
309
|
+
const home = homedir();
|
|
310
|
+
// 各宿主 config 根经 env 覆盖(与 hostConfigRoot 同一套,测试可隔离)。
|
|
311
|
+
// claude/codex 的"用户级 store"落在那根的**父目录**(.claude.json 与 .claude/ 平级)。
|
|
312
|
+
const rootOf = (envKey, fallback) => process.env[envKey] || join(home, fallback);
|
|
313
|
+
const ccRoot = rootOf('CLAUDE_CONFIG_DIR', '.claude');
|
|
314
|
+
const cxRoot = rootOf('CO' + 'DEX_HOME', '.' + 'co' + 'dex');
|
|
315
|
+
return [
|
|
316
|
+
// claude-code 用户级 store(claude mcp add 写它)
|
|
317
|
+
{ p: join(dirname(ccRoot), '.claude.json'), key: 'mcpServers' },
|
|
318
|
+
// claude-code 的 mcp.json(与 .claude/ 同级,在 config 根的**父**目录)
|
|
319
|
+
{ p: join(dirname(ccRoot), '.claude', 'mcp.json'), key: 'mcpServers' },
|
|
320
|
+
// Claude Desktop(系统路径,无 env 可注入)
|
|
321
|
+
{ p: join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json'), key: 'mcpServers' },
|
|
322
|
+
// 其它被 hostConfigDiscovery 纳入的宿主
|
|
323
|
+
{ p: join(home, '.cursor', 'mcp.json'), key: 'mcpServers' },
|
|
324
|
+
{ p: join(home, '.windsurf', 'mcp.json'), key: 'mcpServers' },
|
|
325
|
+
// codex 的 JSON 形态(TOML config.toml 另由 installCoDEX 处理)
|
|
326
|
+
{ p: join(cxRoot, 'config.json'), key: ['mcp_servers', 'mcpServers'] },
|
|
327
|
+
// 通用/共享 MCP store(pi-mcp-adapter 的 GENERIC_GLOBAL_CONFIG_PATH / AGENTS_GLOBAL_CONFIG_PATHS)
|
|
328
|
+
{ p: join(home, '.config', 'mcp', 'mcp.json'), key: 'mcpServers' },
|
|
329
|
+
{ p: join(home, '.agents', 'mcp.json'), key: 'mcpServers' },
|
|
330
|
+
{ p: join(home, '.agents', 'mcp', 'mcp.json'), key: 'mcpServers' },
|
|
331
|
+
];
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* 在已解析的配置里找 abs 条目所在的 servers 容器。
|
|
335
|
+
* key 可为字符串(唯一容器名)或数组(**任一存在即可**,如 coDEX 的
|
|
336
|
+
* config.json 官方同时认 mcp_servers 与 mcpServers)。
|
|
337
|
+
* @returns {{key: string, entry: object}|null} 找到的容器名与 abs 条目
|
|
338
|
+
*/
|
|
339
|
+
function findAbsEntry(cfg, key) {
|
|
340
|
+
if (!cfg || typeof cfg !== 'object') return null;
|
|
341
|
+
const keys = Array.isArray(key) ? key : [key];
|
|
342
|
+
for (const k of keys) {
|
|
343
|
+
const box = cfg[k];
|
|
344
|
+
const entry = box && typeof box === 'object' && !Array.isArray(box) ? box.abs : null;
|
|
345
|
+
if (entry && typeof entry === 'object' && !Array.isArray(entry)) return { key: k, entry };
|
|
346
|
+
}
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* 校正一个外部 JSON store 里**已存在**的 abs 条目 args;不改 command、不新增条目。
|
|
352
|
+
* 文件缺失/非法 JSON 一律静默跳过(那是别人的配置,绝不为它中断安装)。
|
|
353
|
+
* @returns {Promise<string|null>} 一行 step 文案;无需报告/无需校正时 null
|
|
354
|
+
*/
|
|
355
|
+
async function reconcileOneStore(p, keys) {
|
|
356
|
+
let cfg;
|
|
357
|
+
try {
|
|
358
|
+
cfg = JSON.parse(await fs.readFile(p, 'utf8'));
|
|
359
|
+
} catch { return null; } // 不存在 / 坏 JSON: 静默, 不动
|
|
360
|
+
const hit = findAbsEntry(cfg, keys);
|
|
361
|
+
if (!hit) return null; // 没这条: 不新增
|
|
362
|
+
const want = mcpEntryPath();
|
|
363
|
+
const cur = Array.isArray(hit.entry.args) ? hit.entry.args : [];
|
|
364
|
+
if (cur.length === 1 && cur[0] === want) return null; // 幂等: 相等即不写, 不动 mtime
|
|
365
|
+
hit.entry.args = [want]; // 只碰 args, command/其它键原样保留
|
|
366
|
+
await backup(p);
|
|
367
|
+
await atomicWrite(p, JSON.stringify(cfg, null, 2));
|
|
368
|
+
return `✓ MCP → ${p} ${hit.key}.abs 已校正 → ${want}`;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* 检测 ~/.agents/skills/ 下是否已有一份**非本工具所装**的 abs skill。
|
|
373
|
+
*
|
|
374
|
+
* 坑: 这个目录是 `skills` CLI(npx skills,~/.agents/.skill-lock.json 的所有者)
|
|
375
|
+
* 的规范存储位置,各宿主的 skills/ 只是它 fan-out 的目标。abs 从不往那里写 ——
|
|
376
|
+
* 一旦写了就绕过该 CLI 的 lockfile,会被它下次 sync 判成异常或直接覆盖。
|
|
377
|
+
*
|
|
378
|
+
* 但现实里它常被**手工 cp** 进去一份(实测: 与 abs install 同一秒的 mtime、
|
|
379
|
+
* 内容逐字节相同、而 lockfile 的 skills 是空的 —— 没人认领)。那份副本不会随
|
|
380
|
+
* abs install 更新,时间一长就与真正的 SKILL.md 脱节,而 agents 偏偏会读它。
|
|
381
|
+
*
|
|
382
|
+
* 故这里只做**只读检测 + 告警**,绝不代它删除或改写(那是别人的目录)。
|
|
383
|
+
* @returns {string|null} 一行告警文案;无需告警时 null
|
|
384
|
+
*/
|
|
385
|
+
function agentsSkillWarning() {
|
|
386
|
+
const p = join(homedir(), '.agents', 'skills', 'abs-agent-brain-sync', 'SKILL.md');
|
|
387
|
+
if (!fsSync.existsSync(p)) return null;
|
|
388
|
+
// 内容一致 = 用户已自行同步,不必打扰
|
|
389
|
+
try {
|
|
390
|
+
if (fsSync.readFileSync(p, 'utf8') === fsSync.readFileSync(SKILL_SOURCE, 'utf8')) return null;
|
|
391
|
+
} catch { /* 读不到就照常告警 */ }
|
|
392
|
+
return [
|
|
393
|
+
`⚠ ${p} 存在一份与当前版本不一致的副本`,
|
|
394
|
+
` ~/.agents/skills/ 归 skills CLI 所有(本工具不写它),该副本不会被 abs install 更新。`,
|
|
395
|
+
` 如不需要: rm -rf ${join(homedir(), '.agents', 'skills', 'abs-agent-brain-sync')}`,
|
|
396
|
+
].join('\n');
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/** 扫全部外部 store,逐个校正。返回 step 文案数组(无改动则空)。 */
|
|
400
|
+
async function reconcileForeignMcpStores() {
|
|
401
|
+
const steps = [];
|
|
402
|
+
for (const { p, key } of foreignMcpStores()) {
|
|
403
|
+
const line = await reconcileOneStore(p, key);
|
|
404
|
+
if (line) steps.push(line);
|
|
405
|
+
}
|
|
406
|
+
return steps;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** 从全部外部 store 删掉 abs 条目(只删这一键,不删文件/其它键)。
|
|
410
|
+
* 与宿主无关,故挂在 runUninstall 层 —— 放各 installer 里会让
|
|
411
|
+
* `abs uninstall --agent codex` 之类的单宿主路径漏清理。静默容错。 */
|
|
412
|
+
async function removeAbsFromForeignMcpStores() {
|
|
413
|
+
const steps = [];
|
|
414
|
+
for (const { p, key } of foreignMcpStores()) {
|
|
415
|
+
let cfg;
|
|
416
|
+
try {
|
|
417
|
+
cfg = JSON.parse(await fs.readFile(p, 'utf8'));
|
|
418
|
+
} catch { continue; } // 不存在 / 坏 JSON: 静默跳过, 不动别人的文件
|
|
419
|
+
const hit = findAbsEntry(cfg, key);
|
|
420
|
+
if (!hit) continue;
|
|
421
|
+
delete cfg[hit.key].abs;
|
|
422
|
+
await backup(p);
|
|
423
|
+
await atomicWrite(p, JSON.stringify(cfg, null, 2));
|
|
424
|
+
steps.push(`✓ MCP → ${p} ${hit.key}.abs 已移除`);
|
|
425
|
+
}
|
|
426
|
+
return steps;
|
|
427
|
+
}
|
|
428
|
+
|
|
272
429
|
async function installClaudeCode({ withMcp, withSkill, log }) {
|
|
273
430
|
const steps = [];
|
|
274
431
|
// 1) hooks → settings.json (分区合并: 同事件可挂多个 hook 框架, 追加 abs 而非覆盖, 保留 moshi-hook 等)
|
|
@@ -382,24 +539,37 @@ async function installCodex({ withMcp, withSkill, log }) {
|
|
|
382
539
|
steps.push(`✓ MCP → ${mcpP} [mcp_servers.abs]`);
|
|
383
540
|
} else {
|
|
384
541
|
// 已存在也要校对路径: 旧版可能写入了仓库路径(不稳定) 或全局包已迁移。
|
|
542
|
+
// 坑: 曾经只找单行 `/^\s*args\s*=/` 并原地替换。args 写成多行数组
|
|
543
|
+
// args = [
|
|
544
|
+
// "/old/path/mcp.js",
|
|
545
|
+
// ]
|
|
546
|
+
// 时那一行不匹配 → 走 else 只打印"无 args 行, 未动",旧路径**静默保留**;
|
|
547
|
+
// 而 body.includes(`"${want}"`) 的判等又会被 command 行里的同串骗过。
|
|
548
|
+
// 现改为: 只认 args 赋值本身,多行则吃到配对的 `]`,找不齐就整段重写 section。
|
|
385
549
|
const want = mcpEntryPath();
|
|
386
550
|
const lines = text.split('\n');
|
|
387
|
-
const
|
|
388
|
-
if (
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
}
|
|
396
|
-
if (touched) {
|
|
551
|
+
const wa = findTomlKeyRange(lines, found, 'args');
|
|
552
|
+
if (wa) {
|
|
553
|
+
// 仅当 args 的取值里已有 want(单行或跨行)才算已正确
|
|
554
|
+
const argsText = lines.slice(wa.start, wa.end).join('\n');
|
|
555
|
+
if (argsText.includes(`"${want}"`)) {
|
|
556
|
+
steps.push(`• MCP → ${mcpP} 已存在且路径正确, 跳过`);
|
|
557
|
+
} else {
|
|
558
|
+
lines.splice(wa.start, wa.end - wa.start, `args = ["${want}"]`);
|
|
397
559
|
await backup(mcpP);
|
|
398
560
|
await atomicWrite(mcpP, lines.join('\n'));
|
|
399
561
|
steps.push(`✓ MCP → ${mcpP} 路径已校正 → ${want}`);
|
|
400
|
-
} else {
|
|
401
|
-
steps.push(`• MCP → ${mcpP} 已注册但无 args 行, 未动(请手动确认)`);
|
|
402
562
|
}
|
|
563
|
+
} else {
|
|
564
|
+
// 连 args 都没有: 整段重写本 section 的 body(只含本工具的 abs 条目)
|
|
565
|
+
const rebuilt = [
|
|
566
|
+
`command = "${process.execPath}"`,
|
|
567
|
+
`args = ["${want}"]`,
|
|
568
|
+
];
|
|
569
|
+
lines.splice(found.start + 1, found.end - found.start - 1, ...rebuilt);
|
|
570
|
+
await backup(mcpP);
|
|
571
|
+
await atomicWrite(mcpP, lines.join('\n'));
|
|
572
|
+
steps.push(`✓ MCP → ${mcpP} 已重写 [mcp_servers.abs] (补 args) → ${want}`);
|
|
403
573
|
}
|
|
404
574
|
}
|
|
405
575
|
}
|
|
@@ -594,7 +764,7 @@ async function installPi({ withMcp, withSkill, log }) {
|
|
|
594
764
|
steps.push(`✓ hook(ts extension) → ${p}`);
|
|
595
765
|
// MCP → ~/.pi/agent/mcp.json 的 mcpServers (stdio)
|
|
596
766
|
// 曾经只打印「走 extension 内桥接」而没有任何桥接代码 —— 靠 mcp-adapter 的
|
|
597
|
-
// hostConfigDiscovery 间接读到
|
|
767
|
+
// hostConfigDiscovery 间接读到 claude 注册才"看起来能用"; 没有 claude 宿主的机器上直接缺失。
|
|
598
768
|
if (withMcp) {
|
|
599
769
|
const mcpP = join(hostConfigRoot('pi'), 'agent', 'mcp.json');
|
|
600
770
|
await backup(mcpP);
|
|
@@ -663,11 +833,23 @@ export async function runInstall({ agent, mcp = true, skill = true, yes = false
|
|
|
663
833
|
if (agent) throw e;
|
|
664
834
|
}
|
|
665
835
|
}
|
|
836
|
+
// 外部 store 校正与宿主无关(那是别人的文件,只是其中可能有我们的陈旧条目),
|
|
837
|
+
// 故在选完宿主后统一跑一次 —— 放进各 installer 会导致 `abs install --agent codex`
|
|
838
|
+
// 这类单宿主路径漏校正。失败不影响主流程(reconcileOneStore 自身静默容错)。
|
|
839
|
+
if (mcp) {
|
|
840
|
+
try {
|
|
841
|
+
for (const line of await reconcileForeignMcpStores()) console.log(' ' + line);
|
|
842
|
+
} catch { /* 外部 store 校正失败不该让安装失败 */ }
|
|
843
|
+
}
|
|
666
844
|
if (failed.length) {
|
|
667
845
|
console.log(`\n⚠ ${failed.length} 个宿主安装失败: ${failed.map((f) => f.key).join(', ')}`);
|
|
668
846
|
console.log(' 其余宿主已完成。修复上述问题后重跑 `abs install`(幂等,不会重复写入)。');
|
|
669
847
|
throw new Error(`${failed.length} 个宿主安装失败(见上)`);
|
|
670
848
|
}
|
|
849
|
+
if (skill) {
|
|
850
|
+
const w = agentsSkillWarning();
|
|
851
|
+
if (w) console.log('\n' + w);
|
|
852
|
+
}
|
|
671
853
|
console.log('\n完成。项目内运行 abs init 建图谱; 会话里说 "abs load" 续接。');
|
|
672
854
|
}
|
|
673
855
|
|
|
@@ -679,6 +861,11 @@ export async function runUninstall({ agent, yes = false } = {}) {
|
|
|
679
861
|
console.log(`\n▸ 从 ${key} 卸载 …`);
|
|
680
862
|
for (const line of await inst.off()) console.log(' ' + line);
|
|
681
863
|
}
|
|
864
|
+
// 外部 store 的 abs 条目与宿主无关(那是别人的文件),统一清理一次 ——
|
|
865
|
+
// 放进各 uninstaller 会导致 `abs uninstall --agent codex` 漏清理。
|
|
866
|
+
try {
|
|
867
|
+
for (const line of await removeAbsFromForeignMcpStores()) console.log(' ' + line);
|
|
868
|
+
} catch { /* 外部 store 清理失败不该让卸载失败 */ }
|
|
682
869
|
console.log('\n卸载完成。');
|
|
683
870
|
}
|
|
684
871
|
|