@fanchao8609/agent_brain_sync 1.5.4 → 1.5.5
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/README.md +2 -2
- package/bin/mcp.js +12 -12
- package/package.json +1 -1
- package/skill/SKILL.md +10 -5
- package/src/index.js +15 -15
- package/src/install.js +7 -20
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
**跨会话 AI 编码记忆。** 让 AI 会话不再「重开就失忆」:任务进度、经验、踩坑都落盘到项目里的 markdown 图谱,下一个会话开机能直接读回来。
|
|
4
4
|
|
|
5
5
|
- 图谱纯 markdown,放在项目根 `.brain/`,**可以进 git**,人和 AI 读同一份
|
|
6
|
-
-
|
|
6
|
+
- 多项目自动隔离:只在当前目录找 `.brain/`,不向上穿透
|
|
7
7
|
- 并发安全:CLI / MCP / hook 同时写不会互相覆盖
|
|
8
8
|
|
|
9
9
|
npm 包名 `@fanchao8609/agent_brain_sync`,全局命令 **`abs`**。
|
|
@@ -130,7 +130,7 @@ npm run pack:check # 预览 npm 发布产物
|
|
|
130
130
|
agent_brain_sync/
|
|
131
131
|
├── bin/abs.js CLI 入口
|
|
132
132
|
├── bin/mcp.js MCP server (stdio)
|
|
133
|
-
├── src/index.js
|
|
133
|
+
├── src/index.js 图谱定位(只认当前目录的 .brain/)
|
|
134
134
|
├── src/lock.js 并发写保护(原子锁 + 排队 + SKIP)
|
|
135
135
|
├── src/brainio.js 统一读写收口
|
|
136
136
|
├── src/todo.js todo.md 分区读写
|
package/bin/mcp.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// bin/mcp.js — abs MCP server (stdio)。
|
|
3
3
|
// 职责只有两个(PLAN.md v2 定稿):
|
|
4
|
-
// ① resolve_project:
|
|
4
|
+
// ① resolve_project: 用代码写死逻辑定位当前项目目录(只认 cwd 本身的 .brain/)
|
|
5
5
|
// ② 转接调用 CLI 读写(todo/board/query 的具体实现复用 src/)
|
|
6
6
|
// 无状态:每次调用从入参 cwd 重新解析,多项目各归各位。
|
|
7
7
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
@@ -70,12 +70,12 @@ function tool(name, description, schema, handler) {
|
|
|
70
70
|
// 工具面(narrow on purpose — 只暴露读/查/记状态,不做深提炼)
|
|
71
71
|
tool(
|
|
72
72
|
'abs_resolve_project',
|
|
73
|
-
'定位当前项目的 .brain
|
|
74
|
-
{ cwd: z.string().describe('
|
|
73
|
+
'定位当前项目的 .brain 图谱根(只认给定目录本身的 .brain/,不向上搜索)。多项目隔离的唯一入口。',
|
|
74
|
+
{ cwd: z.string().describe('项目根目录(.brain/ 所在处)') },
|
|
75
75
|
async ({ cwd }) => {
|
|
76
76
|
const root = await findBrainRoot(cwd || process.cwd());
|
|
77
77
|
if (!root) {
|
|
78
|
-
return { content: [{ type: 'text', text: `未找到 .brain
|
|
78
|
+
return { content: [{ type: 'text', text: `未找到 .brain/(${cwd} 下没有,不向上搜索)。先在该目录运行: abs init` }] };
|
|
79
79
|
}
|
|
80
80
|
return { content: [{ type: 'text', text: root }] };
|
|
81
81
|
}
|
|
@@ -84,7 +84,7 @@ tool(
|
|
|
84
84
|
tool(
|
|
85
85
|
'abs_board',
|
|
86
86
|
'读取当前项目 todo 看板(含断点/Next-Step)。先 resolve 得到项目,再传其根目录。',
|
|
87
|
-
{ cwd: z.string().describe('
|
|
87
|
+
{ cwd: z.string().describe('项目根目录(.brain/ 所在处)') },
|
|
88
88
|
async ({ cwd }) => {
|
|
89
89
|
const root = await findBrainRoot(cwd || process.cwd());
|
|
90
90
|
if (!root) return err('未找到 .brain/,先 abs init');
|
|
@@ -95,7 +95,7 @@ tool(
|
|
|
95
95
|
tool(
|
|
96
96
|
'abs_load',
|
|
97
97
|
'开机读状态:index 路线 + todo 看板 + 最近 log。跨会话续接的入口。',
|
|
98
|
-
{ cwd: z.string().describe('
|
|
98
|
+
{ cwd: z.string().describe('项目根目录(.brain/ 所在处)') },
|
|
99
99
|
async ({ cwd }) => {
|
|
100
100
|
const root = await findBrainRoot(cwd || process.cwd());
|
|
101
101
|
if (!root) return err('未找到 .brain/,先 abs init');
|
|
@@ -109,7 +109,7 @@ tool(
|
|
|
109
109
|
{
|
|
110
110
|
action: z.enum(['add', 'start', 'done', 'note', 'blocked']),
|
|
111
111
|
id: z.string().describe('任务幂等键,如 TASK-xxx 或子任务名'),
|
|
112
|
-
cwd: z.string().describe('
|
|
112
|
+
cwd: z.string().describe('项目根目录(.brain/ 所在处)'),
|
|
113
113
|
note: z.string().optional().describe('add/start=做什么; note=断点(文件/到哪步); blocked=卡点原因'),
|
|
114
114
|
},
|
|
115
115
|
async ({ action, id, cwd, note }) => {
|
|
@@ -124,7 +124,7 @@ tool(
|
|
|
124
124
|
tool(
|
|
125
125
|
'abs_status',
|
|
126
126
|
'当前项目 + 图谱概要(各类页数)。',
|
|
127
|
-
{ cwd: z.string().describe('
|
|
127
|
+
{ cwd: z.string().describe('项目根目录(.brain/ 所在处)') },
|
|
128
128
|
async ({ cwd }) => {
|
|
129
129
|
const root = await findBrainRoot(cwd || process.cwd());
|
|
130
130
|
if (!root) return err('未找到 .brain/,先 abs init');
|
|
@@ -135,7 +135,7 @@ tool(
|
|
|
135
135
|
tool(
|
|
136
136
|
'abs_query',
|
|
137
137
|
'检索当前项目 .brain/ 知识页(多词 OR):以前踩过什么坑、哪页记了 X。',
|
|
138
|
-
{ cwd: z.string().describe('
|
|
138
|
+
{ cwd: z.string().describe('项目根目录(.brain/ 所在处)'), terms: z.array(z.string()).min(1).describe('检索词') },
|
|
139
139
|
async ({ cwd, terms }) => {
|
|
140
140
|
const root = await findBrainRoot(cwd || process.cwd());
|
|
141
141
|
if (!root) return err('未找到 .brain/,先 abs init');
|
|
@@ -146,7 +146,7 @@ tool(
|
|
|
146
146
|
tool(
|
|
147
147
|
'abs_lint',
|
|
148
148
|
'图谱体检:死链/孤岛/缺 frontmatter/超尺寸/sources 堆积/index 漏列。',
|
|
149
|
-
{ cwd: z.string().describe('
|
|
149
|
+
{ cwd: z.string().describe('项目根目录(.brain/ 所在处)') },
|
|
150
150
|
async ({ cwd }) => {
|
|
151
151
|
const root = await findBrainRoot(cwd || process.cwd());
|
|
152
152
|
if (!root) return err('未找到 .brain/,先 abs init');
|
|
@@ -158,7 +158,7 @@ tool(
|
|
|
158
158
|
'abs_note',
|
|
159
159
|
'经验实时暂存:把刚踩的坑/技巧/结论一句话落进 sources/(防 context 断了流失)。Teardown 时再提炼进 concepts/。',
|
|
160
160
|
{
|
|
161
|
-
cwd: z.string().describe('
|
|
161
|
+
cwd: z.string().describe('项目根目录(.brain/ 所在处)'),
|
|
162
162
|
text: z.string().min(1).describe('经验/坑/技巧一句话'),
|
|
163
163
|
tags: z.string().optional().describe('逗号分隔标签,如 "docker,坑"'),
|
|
164
164
|
},
|
|
@@ -172,7 +172,7 @@ tool(
|
|
|
172
172
|
tool(
|
|
173
173
|
'abs_wrapup',
|
|
174
174
|
'收尾保险:把当前 todo 未完成任务快照到 ~/.abs/log/wrapup.log(下会话 load 时展示滞留对账)。',
|
|
175
|
-
{ cwd: z.string().describe('
|
|
175
|
+
{ cwd: z.string().describe('项目根目录(.brain/ 所在处)') },
|
|
176
176
|
async ({ cwd }) => {
|
|
177
177
|
const root = await findBrainRoot(cwd || process.cwd());
|
|
178
178
|
if (!root) return err('未找到 .brain/,先 abs init');
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -16,7 +16,9 @@ Obsidian 可直接打开的 Markdown 图谱(`.brain/`)做统一落点。
|
|
|
16
16
|
技能被触发(用户问话、开新会话、或说 `abs ...`)时,**第一步永远是下面这条链**,
|
|
17
17
|
然后再看用户真正要什么:
|
|
18
18
|
|
|
19
|
-
1.
|
|
19
|
+
1. **找图谱**:只看**当前目录**是否含 `.brain/`(一个项目一个 `.brain/`)。
|
|
20
|
+
**不向上搜索**:向上爬会从子目录甚至 vault 一路命中家目录 `~/.brain` 的无关图谱,
|
|
21
|
+
静默把项目挂错地方。所以必须 cd 到项目根再跑,子目录不会自动归属。
|
|
20
22
|
2. **没有?按意图决定建档与否**:
|
|
21
23
|
- 用户意图是**沉淀**(收尾/「把这次记下来」)或**即将开工的长期任务**(会跨会话,
|
|
22
24
|
用词如「帮我做 X / 开工 / 继续开发 / 修 X」)→ **先建档**:`abs init`。别在没图谱时就开写。
|
|
@@ -59,11 +61,14 @@ Obsidian 可直接打开的 Markdown 图谱(`.brain/`)做统一落点。
|
|
|
59
61
|
## 图谱定位(一个项目一个 `.brain/`,abs 自动定位不用手工指定路径)
|
|
60
62
|
|
|
61
63
|
`.brain/` 放项目根,一个项目只建一份。所有 `abs` 命令(`abs load/todo/note/task/query/lint...`)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
+
**只认当前目录的 `.brain/`**——必须在项目根目录(即 `.brain/` 所在处)运行,
|
|
65
|
+
不用传路径。
|
|
64
66
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
**不在子目录自动归属,也不向上搜索**。原因是上爬会命中家目录 `~/.brain`
|
|
68
|
+
(vault / 临时目录 / 任意路径都可能爬到),把无关项目静默挂到别人图谱上。
|
|
69
|
+
宁可报错也不猜。多项目各自独立,各自 `cd` 到自己的根再跑。
|
|
70
|
+
|
|
71
|
+
monorepo 若多个子包各自独立交付,可各建一份 `.brain/`。`abs status` 显示当前定位到哪个项目。
|
|
67
72
|
|
|
68
73
|
## 图谱长什么样(都在 `.brain/` 下)
|
|
69
74
|
|
package/src/index.js
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
// src/index.js — 图谱定位:多项目隔离的核心。
|
|
2
|
-
//
|
|
2
|
+
// 只看给定目录本身有没有 `.brain/`,不向上搜索。
|
|
3
3
|
// 这是唯一"项目定位"逻辑,被 CLI / MCP / hook 共用,代码确定、不靠猜。
|
|
4
4
|
import { promises as fs } from 'node:fs';
|
|
5
|
-
import { join,
|
|
5
|
+
import { join, resolve, basename } from 'node:path';
|
|
6
6
|
|
|
7
7
|
export const BRAIN_DIR = '.brain';
|
|
8
8
|
|
|
9
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* 定位图谱根:仅当 `dir` 本身含 `.brain/` 才命中,否则返回 null。
|
|
11
|
+
* 故意不向上搜索:向上会从任意目录爬到 ~ 命中家目录图谱(如 ~/.brain),
|
|
12
|
+
* 把无关项目静默挂到别的图谱上。宁可报错,不猜。
|
|
13
|
+
*/
|
|
10
14
|
export async function findBrainRoot(startDir) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const parent = dirname(dir);
|
|
18
|
-
if (parent === dir) return null; // reached filesystem root
|
|
19
|
-
dir = parent;
|
|
20
|
-
}
|
|
15
|
+
const dir = resolve(startDir || process.cwd());
|
|
16
|
+
try {
|
|
17
|
+
const st = await fs.stat(join(dir, BRAIN_DIR));
|
|
18
|
+
if (st.isDirectory()) return dir;
|
|
19
|
+
} catch { /* 本目录没有图谱 */ }
|
|
20
|
+
return null;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
/** 解析图谱内文件的绝对路径。brainRoot 须已定位。 */
|
|
@@ -30,8 +30,8 @@ export async function requireBrain(startDir) {
|
|
|
30
30
|
const root = await findBrainRoot(startDir);
|
|
31
31
|
if (!root) {
|
|
32
32
|
throw new Error(
|
|
33
|
-
`abs:
|
|
34
|
-
`
|
|
33
|
+
`abs: 目录 ${resolve(startDir)} 下没有 .brain/ 图谱(不向上搜索)。\n` +
|
|
34
|
+
` 请在该目录运行: abs init`
|
|
35
35
|
);
|
|
36
36
|
}
|
|
37
37
|
return root;
|
package/src/install.js
CHANGED
|
@@ -310,15 +310,10 @@ const server = async ({ client, directory }) => {
|
|
|
310
310
|
} catch {} // fire-and-forget: 永不阻塞宿主
|
|
311
311
|
}
|
|
312
312
|
|
|
313
|
-
//
|
|
313
|
+
// 定位当前项目的 .brain/:只看 cwd 本身,不向上搜索(防爬到家目录图谱)
|
|
314
314
|
async function findBrain(cwd) {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
try { const st = await stat(join(d, ".brain")); if (st.isDirectory()) return join(d, ".brain") } catch {}
|
|
318
|
-
const up = dirname(d)
|
|
319
|
-
if (up === d) return null
|
|
320
|
-
d = up
|
|
321
|
-
}
|
|
315
|
+
const d = cwd || process.cwd()
|
|
316
|
+
try { const st = await stat(join(d, ".brain")); return st.isDirectory() ? join(d, ".brain") : null } catch { return null }
|
|
322
317
|
}
|
|
323
318
|
|
|
324
319
|
// 判定"今天是否收尾过": 必须匹配 log.md 的条目头 '## [YYYY-MM-DD HH:MM]'。
|
|
@@ -436,7 +431,7 @@ async function logHook(evt: string): Promise<void> {
|
|
|
436
431
|
}
|
|
437
432
|
|
|
438
433
|
// 会话结束时快照当前项目滞留任务到 wrapup.log(detached fire-and-forget,wrapup 自身幂等去重不刷屏)。
|
|
439
|
-
// cwd 用事件 ctx.cwd(当前项目),让 abs
|
|
434
|
+
// cwd 用事件 ctx.cwd(当前项目),让 abs 在该目录定位 .brain/。
|
|
440
435
|
function snapshotWrapup(cwd: string): void {
|
|
441
436
|
try {
|
|
442
437
|
const child = spawn(process.execPath, [ABS_BIN, "wrapup"], {
|
|
@@ -466,18 +461,10 @@ function hasWriteWork(toolResults: any[]): boolean {
|
|
|
466
461
|
return false
|
|
467
462
|
}
|
|
468
463
|
|
|
469
|
-
/**
|
|
464
|
+
/** 定位当前项目的 .brain/:只看 cwd 本身,不向上搜索(防爬到家目录图谱)。 */
|
|
470
465
|
async function findBrain(cwd: string): Promise<string | null> {
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
try {
|
|
474
|
-
const st = await stat(join(d, ".brain"))
|
|
475
|
-
if (st.isDirectory()) return join(d, ".brain")
|
|
476
|
-
} catch {}
|
|
477
|
-
const up = dirname(d)
|
|
478
|
-
if (up === d) return null
|
|
479
|
-
d = up
|
|
480
|
-
}
|
|
466
|
+
const d = cwd || process.cwd()
|
|
467
|
+
try { const st = await stat(join(d, ".brain")); return st.isDirectory() ? join(d, ".brain") : null } catch { return null }
|
|
481
468
|
}
|
|
482
469
|
|
|
483
470
|
/**
|