@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 CHANGED
@@ -3,7 +3,7 @@
3
3
  **跨会话 AI 编码记忆。** 让 AI 会话不再「重开就失忆」:任务进度、经验、踩坑都落盘到项目里的 markdown 图谱,下一个会话开机能直接读回来。
4
4
 
5
5
  - 图谱纯 markdown,放在项目根 `.brain/`,**可以进 git**,人和 AI 读同一份
6
- - 多项目自动隔离:从当前目录向上找最近的 `.brain/`
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 图谱定位(向上找最近 .brain/)
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: 用代码写死逻辑定位当前项目目录(从 cwd 向上找最近 .brain/)
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 图谱根(从给定目录向上找最近 .brain/)。多项目隔离的唯一入口。',
74
- { cwd: z.string().describe('当前工作目录,默认进程 cwd') },
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/(从 ${cwd} 向上无果)。先在该项目运行: abs init` }] };
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('项目内任意目录'), terms: z.array(z.string()).min(1).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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fanchao8609/agent_brain_sync",
3
- "version": "1.5.4",
3
+ "version": "1.5.5",
4
4
  "description": "agent-brain-sync: 跨会话 AI 编码记忆 — hook 纯触发 + CLI/MCP 读写 .brain markdown 图谱, 防并发写保护。",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/skill/SKILL.md CHANGED
@@ -16,7 +16,9 @@ Obsidian 可直接打开的 Markdown 图谱(`.brain/`)做统一落点。
16
16
  技能被触发(用户问话、开新会话、或说 `abs ...`)时,**第一步永远是下面这条链**,
17
17
  然后再看用户真正要什么:
18
18
 
19
- 1. **找图谱**:从当前目录向上到最近含 `.brain/` 的祖先目录即命中(一个项目一个 `.brain/`)。
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
- **自动从当前目录向上找最近含 `.brain/` 的祖先目录即命中**——你在项目内任何子目录跑都行,
63
- 不用传路径。多项目各自独立,abs 按你所在目录归属。
64
+ **只认当前目录的 `.brain/`**——必须在项目根目录(即 `.brain/` 所在处)运行,
65
+ 不用传路径。
64
66
 
65
- monorepo 若多个子包各自独立交付,可各建一份 `.brain/`;横向规律放最上层图谱 syntheses/。
66
- `abs status` 显示当前定位到哪个项目。
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
- // 从给定 cwd 向上找最近含 `.brain/` 的祖先目录即命中。
2
+ // 只看给定目录本身有没有 `.brain/`,不向上搜索。
3
3
  // 这是唯一"项目定位"逻辑,被 CLI / MCP / hook 共用,代码确定、不靠猜。
4
4
  import { promises as fs } from 'node:fs';
5
- import { join, dirname, resolve, basename } from 'node:path';
5
+ import { join, resolve, basename } from 'node:path';
6
6
 
7
7
  export const BRAIN_DIR = '.brain';
8
8
 
9
- /** 向上找最近含 .brain/ 的祖先目录。找不到返回 null。 */
9
+ /**
10
+ * 定位图谱根:仅当 `dir` 本身含 `.brain/` 才命中,否则返回 null。
11
+ * 故意不向上搜索:向上会从任意目录爬到 ~ 命中家目录图谱(如 ~/.brain),
12
+ * 把无关项目静默挂到别的图谱上。宁可报错,不猜。
13
+ */
10
14
  export async function findBrainRoot(startDir) {
11
- let dir = resolve(startDir || process.cwd());
12
- for (;;) {
13
- try {
14
- const st = await fs.stat(join(dir, BRAIN_DIR));
15
- if (st.isDirectory()) return dir;
16
- } catch { /* not here, keep walking up */ }
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: 找不到 .brain/ 图谱(从 ${resolve(startDir)} 向上搜索无果)。\n` +
34
- ` 请在项目根先运行: abs init`
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
- // 从 cwd 向上找最近含 .brain/ 的祖先目录
313
+ // 定位当前项目的 .brain/:只看 cwd 本身,不向上搜索(防爬到家目录图谱)
314
314
  async function findBrain(cwd) {
315
- let d = cwd || process.cwd()
316
- for (;;) {
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 从该目录向上定位 .brain/。
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
- /** 从 cwd 向上找最近含 .brain/ 的祖先目录。 */
464
+ /** 定位当前项目的 .brain/:只看 cwd 本身,不向上搜索(防爬到家目录图谱)。 */
470
465
  async function findBrain(cwd: string): Promise<string | null> {
471
- let d = cwd || process.cwd()
472
- for (;;) {
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
  /**