@fanchao8609/agent_brain_sync 0.1.0

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/skill/SKILL.md ADDED
@@ -0,0 +1,200 @@
1
+ ---
2
+ name: abs-agent-brain-sync
3
+ description: abs (agent-brain-sync) 跨会话 AI 编码记忆与任务续接。开场续接状态(abs load/MCP abs_load),干活中任务/经验实时落盘(abs_task/abs_note),每轮结束走收尾循环(读todo→判未登记→沉淀→更新index/log)。解决会话无状态:经验/进度/坑碎片化、重开失忆。
4
+ ---
5
+
6
+ # abs — 跨会话记忆 (agent-brain-sync)
7
+
8
+ 把 AI 编码经验从会话沙盒里救出来。每个会话都是无状态的——Cl​aude、Op​enCode、Cursor
9
+ 各开一堆会话,经验/进度/踩坑全碎片化,重开像失忆。本技能用一个放**项目根目录**、
10
+ Obsidian 可直接打开的 Markdown 图谱(`.brain/`)做统一落点。
11
+ **骨架/任务/暂存/检索/体检走 abs 工具(不手工建骨架、不手工登记任务);深提炼(把暂存经验
12
+ 写成 concept/entity 页)必须手工——那是判断力,abs 不替你判断什么值得沉淀。**
13
+
14
+ ## 触发总入口(每次命中技能,第一步先走这里)
15
+
16
+ 技能被触发(用户问话、开新会话、或说 `abs ...`)时,**第一步永远是下面这条链**,
17
+ 然后再看用户真正要什么:
18
+
19
+ 1. **找图谱**:从当前目录向上到最近含 `.brain/` 的祖先目录即命中(一个项目一个 `.brain/`)。
20
+ 2. **没有?按意图决定建档与否**:
21
+ - 用户意图是**沉淀**(收尾/「把这次记下来」)或**即将开工的长期任务**(会跨会话,
22
+ 用词如「帮我做 X / 开工 / 继续开发 / 修 X」)→ **先建档**:`abs init`。别在没图谱时就开写。
23
+ - 用户是**闲聊 / 一次性问句 / 明说不要建档 / 目录只读** → **不建档**,当普通会话处理,
24
+ 避免在无关项目乱落文件。
25
+ 3. **有/刚建好?分析用户意图**,分流:
26
+
27
+ | 用户意图 | 走哪 |
28
+ |---|---|
29
+ | 总结经验 / 结束了 / "把这次记下来" | 收尾 Teardown(沉淀) |
30
+ | 查询坑 / "我上次怎么解决 X" | `abs query` 检索 |
31
+ | 体检图谱 / `abs lint` | `abs lint` 跑体检 |
32
+ | 开新任务 / 继续开发 / "帮我做 X" | 开场 Init Sync(续接),之后正常做 |
33
+ | 意图不明 / 默认 | **续 todo**:读未完成项开工续做 |
34
+
35
+ **主心骨**:意图不明且项目有 `.brain/` 时,默认**续 todo**——任何情况下先接上未完成工作,
36
+ 不是停在闲聊。
37
+
38
+ ## 图谱定位(一个项目一个 `.brain/`,abs 自动定位不用手工指定路径)
39
+
40
+ `.brain/` 放项目根,一个项目只建一份。所有 `abs` 命令(`abs load/todo/note/task/query/lint...`)
41
+ **自动从当前目录向上找最近含 `.brain/` 的祖先目录即命中**——你在项目内任何子目录跑都行,
42
+ 不用传路径。多项目各自独立,abs 按你所在目录归属。
43
+
44
+ monorepo 若多个子包各自独立交付,可各建一份 `.brain/`;横向规律放最上层图谱 syntheses/。
45
+ `abs status` 显示当前定位到哪个项目。
46
+
47
+ ## 图谱长什么样(都在 `.brain/` 下)
48
+
49
+ ```
50
+ .brain/
51
+ ├── index.md # 总索引 + 当前路线(Roadmap)。入口。
52
+ ├── log.md # 工作成果流水:完成 X 的一行摘要,倒序。不写工具动作。
53
+ ├── todo.md # 动态看板:进行中/待办/阻塞/已完成。进度唯一真源。
54
+ ├── entities/ # 实体页:一个"具名事物"一页。
55
+ ├── concepts/ # 概念页:一个"可复用规律/坑"一页。
56
+ ├── sources/ # 暂存页:实时经验(abs note)落点。提炼完即归档/删。
57
+ ├── syntheses/ # 综合页:跨实体横向判断/选型/路线。
58
+ └── sessions/ # 会话快照 + Next Session Hook。
59
+ ```
60
+
61
+ **骨架由 `abs init` 生成,不手工建。** 每个 `.brain/` 建一份,不逐子目录乱建。结构不完整时
62
+ `abs init --repair` 只补缺、不覆盖已有文件。
63
+
64
+ ### 归类规则(新知识进哪类——判断力)
65
+
66
+ | 类别 | 放什么 | 命名 |
67
+ |------|--------|------|
68
+ | `entities/` | 具名的**事物**:`docker`、`auth-module` | TitleCase:`Docker.md` |
69
+ | `concepts/` | 可复用的**规律/坑**:`docker-prisma-429` | kebab-case |
70
+ | `sources/` | 实时经验**暂存**(`abs note` 自动落这里) | `YYYY-MM-DD-slug.md` |
71
+ | `syntheses/` | **横向综合**:选型、架构取舍 | `synthesis-slug.md` |
72
+
73
+ 判断一问:能说"它是什么"→ entities;能说"这么做就避坑"→ concepts;卡住默认 concepts。
74
+
75
+ ## 容量纪律(最重要的节——别什么都往里扔)
76
+
77
+ 图谱贵在**精**不在全。写页前过四关,不过就不写或压缩:
78
+
79
+ 1. **再命中测试**:下个会话不知道这条,会不会踩同坑/重做同决定?会→存;不会→不存。
80
+ 能从代码 grep 读出的细节一律不记。
81
+ 2. **单页硬上限**:`entities/ concepts/ syntheses/` 单页 <150 行/<5KB。超了拆或外链。
82
+ 3. **sources 是暂存不是存档**:提炼成规律后删/归档 source(同步清引用,防死链)。
83
+ 4. **写前压缩三问**:规律还是噪音?不记会怎样?能不能用一行链接已有页代替新增整页?
84
+
85
+ `abs lint` 检查单页超限、sources 堆积、死链、index 漏列。
86
+
87
+ ## 开场:Init Sync(开工 / 默认续 todo)
88
+
89
+ 图谱已存在;收到第一个核心开发指令**之前**走这条链载入上下文:
90
+
91
+ 1. **读状态**:`abs load`(或 MCP `abs_load`)读 index 路线 + todo 看板 + 最近 log。
92
+ 2. **对账滞留(强制,别跳过)**:若 `abs load` 顶部出现 `⏳ 上会话滞留`,说明上会话有任务做完/做到一半就断了。**先收尾再开工**:
93
+ - 快照里的任务现在真做完了 → `abs task done <id>`(done 后下次 load 滞留自动消失);
94
+ - 还没做完 → `abs task note <id> --note "接到哪/改到哪个文件"` 补断点(别空手续接)。
95
+ 滞留没清完就不算接上了状态——这是「任务做完没进 Done」的根治动作。
96
+ 3. **读命中页**:按关键词在 index 定位 → 读对应 concepts/entities 全文。
97
+ 4. **续 todo**:默认续 todo 分支 → 把顶部未完成项当当前任务开做。
98
+ 5. **登记新任务**:有明确新任务而 todo 没有 → `abs task start <id> --note 做什么` 登记
99
+ 再动工。不登记,会话一切断就丢。
100
+
101
+ ## 进行中:todo 是活看板 + 经验实时落(最重要的纪律)
102
+
103
+ **todo 不是收尾仪式,是干活中随改随写的活看板。** 每个任务边界立即更新,和 git commit
104
+ 同一个反射,别等收尾。用工具(MCP `abs_task` / CLI `abs task`):
105
+
106
+ | 时机 | 动作 |
107
+ |---|---|
108
+ | 认领新任务 | `abs task start <id> --note "做什么"` |
109
+ | 子任务做完 | `abs task done <id>`(自动归位 Done 对应 `### YYYY-MM-DD` 分组顶部,新完成在前;断点随迁) |
110
+ | 碰壁/阻塞 | `abs task blocked <id> --note "卡点原因"`(移 Blocked) |
111
+ | 被打断/改向/干到一半停 | `abs task note <id> --note "改到哪个文件/到哪步"`(补 ↳ 断点 行) |
112
+
113
+ > **跨会话任务只用 abs task,别用宿主原生 todo。** Cl​aude TodoWrite/Task、co​dex todo-list、
114
+ > Op​enCode todowrite、pi `/list`/goal 各有各的原生任务——但**多是会话内临时**,不会写进
115
+ > `.brain/todo.md`。若用原生 todo 建了跨会话任务,它就会「只在界面 0/N 里、abs 看不到」,
116
+ > 下会话接不上、收尾没影。**分工**:跨会话/会被打断的任务 → `abs task start`(唯一真源);
117
+ > 原生 todo 顶多记「本会话内不跨断点的临时拆解草稿」。
118
+
119
+ **经验/坑刚冒出来就落**:`abs note "一句话经验" --tags 坑,docker`(MCP `abs_note`)——
120
+ 暂存进 sources/(幂等去重、自动进 index/log),防 context 爆/截断流失。宁少勿滥。
121
+
122
+ ## 每轮结束:收尾循环(Stop/告一段落后必做)
123
+
124
+ **每个任务边界、每轮被 Stop/打断、告一段落时,别停半空——走收尾循环。**
125
+ 这是"开场接上状态、结束落回状态"的闭环,否则下会话接不上、经验流失。
126
+
127
+ > 触发信号:Stop/会话结束 时 hook 会把「当前项目仍未完成任务 + 断点」快照进 `~/.abs/log/wrapup.log`
128
+ > (经 `abs wrapup`,机械、幂等去重,不替你做判断)。**下会话 `abs load` 会自动把滞留顶到顶部**
129
+ > (`⏳ 上会话滞留`),所以收尾不是靠自觉记日志,而是开场被强制接上。要不要把某个任务标 done,
130
+ > 仍由你判断(快照只记录「哪些还开着」,不猜完成)。
131
+
132
+ 收到 Stop / "结束/先这样/切别的事" / 长任务告一段落,立即执行(快、准、不啰嗦):
133
+
134
+ 1. **读 todo** → `abs load`,看 Today 还有哪些没完成。
135
+ 2. **判有没有做完没登记** → 实际完成了漏登记的 `abs task done <id>`;做到一半补
136
+ `abs task note <id> --note 断点`;碰壁 `abs task blocked`。别让干完的事还停 Today。
137
+ 3. **沉淀经验(该沉淀才沉淀)** → 踩了值得记的坑/有可复用技巧/跨会话判断 → `abs note`
138
+ 暂存;值得深提炼的(规律/坑/决策)按 Teardown 走完整流程。
139
+ 4. **更新 index/log/todo** → 新页同步进 index;`log.md` 倒序记一行**工作成果**摘要
140
+ (`abs log "完成 X:..."`,不是工具动作);todo 对账。跑 `abs lint` 确认自洽。
141
+
142
+ **完成标准**:看板反映真实状态(Done 无滞留半成品)、该沉淀已落、index/log/todo 与事实一致。
143
+
144
+ ## 收尾:Teardown Sync(深提炼,工具不替判断)
145
+
146
+ 任务告一段落/结束前,把**真实发生**写回图谱。只写做过/跑过/测过的事实,禁止脑补。按序:
147
+
148
+ 1. **暂存线索**:`abs note`(或建 `sources/YYYY-MM-DD-slug.md`)记做了什么、改哪些文件、验证命令。
149
+ 2. **抽规律**:值得留的 → `concepts/<kebab-slug>.md`:触发场景/❌表现/🛠根因+解法+验证。挂双链。
150
+ 3. **沉淀实体**:碰了重要未记录的事物 → `entities/<TitleCase>.md`。
151
+ 4. **对账 todo**:滞留 Today 归位(Done 标日期 / Backlog 补断点);遗留 bug 写 Backlog/Blocked。
152
+ 5. **综合(可选)**:推进了选型/取舍 → `syntheses/`。
153
+ 6. **收拢 sources**:提炼成规律的删 source,**同步清指向它的引用**(防死链)。
154
+ 7. **修 index + 记 log**:新页同步 index;`log.md` 倒序记一行摘要。
155
+ 8. **留接力棒**:`sessions/log-YYYY-MM-DD.md`,强制写 `## 🪝 Next Session Hook`。
156
+
157
+ **完成标准**:每条过了容量纪律的知识一处落点;index 与事实一致;sessions 有带 Hook 快照。
158
+
159
+ ## 知识页格式(concepts/entities/syntheses)
160
+
161
+ 所有页统一 frontmatter 三项:`tags / updated / status`(无额外字段)。
162
+
163
+ ```markdown
164
+ ---
165
+ tags: [concept, 领域] # 首标签 ∈ entity|concept|source|synthesis|session-log
166
+ updated: YYYY-MM-DD
167
+ status: draft # 或 reviewed(仅指知识冲突裁决结案)
168
+ ---
169
+ ```
170
+
171
+ - **关联连接区**:每页必须有 `## 关联连接`,用 `[[页面名]]` 链相关页。严禁孤岛页。
172
+ - **知识冲突**:与旧页矛盾不静默覆盖。加 `## 知识冲突` 两版都留、标来源时间,交人工裁决。
173
+ - **命名即链接**:`[[Docker]]` 落 entities/Docker.md;`[[docker-prisma-429]]` 落 concepts/。别建别名层。
174
+
175
+ 概念页核心结构(坑):`触发场景 / ❌表现(贴报错) / 🛠解法(根因+修复+验证命令) / 关联连接`。
176
+
177
+ ## 维护:query / lint
178
+
179
+ - **query(检索)**:`abs query <词>`(或先读 index 定位)→ 读命中页 → 答用 `[[页面名]]` 标来源。
180
+ **代码问题(符号在哪/谁调用)答案不在 .brain,直接读源码**;.brain 只答"踩过什么坑/上次做到哪"。
181
+ - **lint(体检)**:`abs lint`。查死链/孤岛/缺 frontmatter/模板残留/未决冲突/超尺寸/sources 堆积/
182
+ index 漏列。按报告修(死链→补链;孤岛→补关联;超大→拆;sources 积压→提炼归档)。
183
+
184
+ ## 分工:hook 机械记 / 工具实时落 / skill 深提炼(装了 abs 的项目)
185
+
186
+ | 层 | 干什么 | 靠什么 |
187
+ |---|---|---|
188
+ | **hook(机械)** | SessionStart/UserPromptSubmit/Stop/SessionEnd 自动记**技术日志**(~/.abs/log/) | 宿主 hook 配置。你不写技术日志。 |
189
+ | **CLI/MCP(实时)** | 任务/经验**实时落盘**:`abs task start/note/blocked/done`、`abs note` | 每个任务边界立即调;经验随时 abs note。 |
190
+ | **skill(自觉)** | **深提炼**(sources→concepts)+ 收尾循环 + 修 index | 判断什么值得沉淀,工具不替你判断。 |
191
+
192
+ 实时层解决"断了就丢";自觉层解决"噪音污染"。分工明确:骨架/任务/暂存/检索/体检走 abs 工具
193
+ (`abs init`/`abs_task`/`abs note`/`abs query`/`abs lint`);**深提炼(sources→concept/entity 页)
194
+ 手工写**——那是判断力,工具不替。改完 `abs lint` 确认自洽。
195
+
196
+ ## 自我约束
197
+
198
+ - 只读写 `.brain/` 与目标代码,不动全局配置(一次性接入安装除外)。
199
+ - 内容基于真实发生的事实;遵守容量纪律宁缺毋滥。
200
+ - 双链/frontmatter/index 必须自洽——图谱给下个会话读,坏链=掰断接力棒。
package/src/brainio.js ADDED
@@ -0,0 +1,66 @@
1
+ // src/brainio.js — .brain 文档统一读写收口。
2
+ // 目的:所有需读写 .brain 文档的地方(todo/log/index/source/…)都走这一个方法,
3
+ // 调用方不必各自拼 brainPath + lock.editFile/裸 readFile,也天然带上防重复/防并发锁。
4
+ // - readBrain(brainRoot, rel) 读 .brain/<rel> 文本(文件不存在返回 null)
5
+ // - writeBrain(brainRoot, rel, transform) 锁内读改写(自动 lock.editFile 防并发);transform 返回新文本或 null=不改
6
+ // - appendBrain(brainRoot, rel, lines) 锁内追加多行到文末
7
+ // 纯 IO 封装:路径由 brainPath 统一算,写由 lock.editFile 统一带锁。无业务逻辑。
8
+ import { promises as fs } from 'node:fs';
9
+ import { brainPath } from './index.js';
10
+ import { editFile, SKIP } from './lock.js';
11
+
12
+ /** 读 .brain/<rel> 全文。不存在返回 null。读不经锁(读旧内容无害)。 */
13
+ export async function readBrain(brainRoot, rel) {
14
+ const p = brainPath(brainRoot, rel);
15
+ try {
16
+ return await fs.readFile(p, 'utf8');
17
+ } catch {
18
+ return null; // 尚未建该页
19
+ }
20
+ }
21
+
22
+ /** 锁内读-改-写 .brain/<rel>。transform(currentText) 返回新文本;返回 null/undefined 表示不改(不落盘)。
23
+ * 返回:落盘后的新文本(transform 不改时返回原文本)。自动防并发(同文件多进程同时写不覆盖)。
24
+ * 注:本方法写的是「整体新文本」;若文件不存在 currentText 传 null。 */
25
+ export async function writeBrain(brainRoot, rel, transform) {
26
+ const p = brainPath(brainRoot, rel);
27
+ const out = await editFile(p, (current) => {
28
+ const next = transform(current);
29
+ if (next === null || next === undefined) return SKIP; // 不改 → 不落盘
30
+ const text = typeof next === 'string' ? next : next.text;
31
+ return text === current ? SKIP : text; // 与当前相同也不落盘(editFile 幂等)
32
+ });
33
+ // out 是 editFile 返回的字符串(新文本)或 SKIP(未改)。未改时返回读到的当前文本。
34
+ if (out === SKIP) return currentTextOf(brainPath(brainRoot, rel));
35
+ return out;
36
+ }
37
+
38
+ /** 读文件文本(不经锁); 供 writeBrain 未改时回读当前值。 */
39
+ async function currentTextOf(p) {
40
+ try {
41
+ return await fs.readFile(p, 'utf8');
42
+ } catch {
43
+ return null;
44
+ }
45
+ }
46
+
47
+ /** 锁内往 .brain/<rel> 文末追加若干行(自动补换行)。若文件不存在则创建。 */
48
+ export async function appendBrain(brainRoot, rel, lines) {
49
+ const arr = Array.isArray(lines) ? lines : [lines];
50
+ return writeBrain(brainRoot, rel, (cur) => {
51
+ const base = cur == null ? '' : cur.replace(/\s*$/, '');
52
+ const body = arr.join('\n');
53
+ return base ? `${base}\n${body}\n` : `${body}\n`;
54
+ });
55
+ }
56
+
57
+ /** 原子建新 .brain/<rel>(tmp+rename, 不经锁——新文件写唯一内容无并发读者竞争)。 */
58
+ export async function createBrainFile(brainRoot, rel, content) {
59
+ const p = brainPath(brainRoot, rel);
60
+ const { dirname } = await import('node:path');
61
+ await fs.mkdir(dirname(p), { recursive: true });
62
+ const tmp = `${p}.abs-tmp-${process.pid}-${Date.now()}`;
63
+ await fs.writeFile(tmp, content, 'utf8');
64
+ await fs.rename(tmp, p);
65
+ return p;
66
+ }
package/src/hosts.js ADDED
@@ -0,0 +1,65 @@
1
+ // src/hosts.js — 四宿主的接入机制定义。
2
+ // 关键事实(来自 ai-memory 学习):
3
+ // - Claude Code / Codex: JSON hooks 配置,可指向 shell 脚本 → 纯 shell hook 可行
4
+ // - OpenCode / Pi: 只吃 TS plugin/extension,无 shell-hook 配置 → 需生成 TS
5
+ // 本文件集中每个宿主的:home 目录、MCP 注册 schema、hook 配置方式、skill 落点。
6
+ import { homedir } from 'node:os';
7
+ import { join } from 'node:path';
8
+
9
+ const HOME = homedir();
10
+
11
+ // MCP 注册:我们的 MCP server 由 `node <abs>/bin/mcp.js` 启动(stdio transport)。
12
+ export function mcpServerEntry(absDir) {
13
+ return {
14
+ command: process.execPath, // node
15
+ args: [join(absDir, 'bin', 'mcp.js')],
16
+ };
17
+ }
18
+
19
+ export const HOSTS = [
20
+ {
21
+ key: 'claude-code',
22
+ label: 'Claude Code',
23
+ // 配置文件: ~/.claude/settings.json(config 根可经 env 覆盖,用于测试/自定义)
24
+ configRoot: () => process.env.CLAUDE_CONFIG_DIR || join(HOME, '.claude'),
25
+ skillSub: 'skills', // 相对 configRoot 的 skill 目录
26
+ settingsPath: () => join(process.env.CLAUDE_CONFIG_DIR || join(HOME, '.claude'), 'settings.json'),
27
+ // 事件 → 我们的 shell hook 脚本(从 hooks/ 拷到 .claude 侧后执行)
28
+ events: ['SessionStart', 'UserPromptSubmit', 'Stop', 'SessionEnd'],
29
+ hookKind: 'shell-json', // settings.json 的 hooks 对象
30
+ },
31
+ {
32
+ key: 'codex',
33
+ label: 'Codex (OpenAI)',
34
+ configRoot: () => process.env.CODEX_HOME || join(HOME, '.codex'),
35
+ skillSub: 'skills', // 相对 configRoot 的 skill 目录
36
+ settingsPath: () => join(process.env.CODEX_HOME || join(HOME, '.codex'), 'hooks.json'),
37
+ events: ['SessionStart', 'UserPromptSubmit', 'Stop'],
38
+ hookKind: 'codex-hooks', // ~/.codex/hooks.json: { hooks: [...] }
39
+ },
40
+ {
41
+ key: 'opencode',
42
+ label: 'OpenCode',
43
+ // 只吃 TS plugin:~/.config/opencode/plugins/abs.ts(config 根可经 env 覆盖,用于测试)
44
+ configRoot: () => process.env.ABS_OPENCODE_HOME || join(HOME, '.config', 'opencode'),
45
+ skillSub: 'skills', // 相对 configRoot 的 skill 目录
46
+ settingsPath: () => join(process.env.ABS_OPENCODE_HOME || join(HOME, '.config', 'opencode')),
47
+ events: ['SessionStart', 'UserPromptSubmit', 'Stop'],
48
+ hookKind: 'ts-plugin',
49
+ },
50
+ {
51
+ key: 'pi',
52
+ label: 'Pi',
53
+ configRoot: () => process.env.ABS_PI_HOME || join(HOME, '.pi'),
54
+ skillSub: join('agent', 'skills'), // pi 用户级 skill 在 ~/.pi/agent/skills (非 ~/.pi/skills)
55
+ settingsPath: () => join(process.env.ABS_PI_HOME || join(HOME, '.pi'), 'agent', 'extensions'),
56
+ events: ['SessionStart', 'UserPromptSubmit', 'Stop'],
57
+ hookKind: 'ts-extension',
58
+ },
59
+ ];
60
+
61
+ export function hostByKey(key) {
62
+ const h = HOSTS.find((x) => x.key === key);
63
+ if (!h) throw new Error(`未知 agent: ${key} (可用: ${HOSTS.map((x) => x.key).join(', ')})`);
64
+ return h;
65
+ }
package/src/index.js ADDED
@@ -0,0 +1,40 @@
1
+ // src/index.js — 图谱定位:多项目隔离的核心。
2
+ // 从给定 cwd 向上找最近含 `.brain/` 的祖先目录即命中。
3
+ // 这是唯一"项目定位"逻辑,被 CLI / MCP / hook 共用,代码确定、不靠猜。
4
+ import { promises as fs } from 'node:fs';
5
+ import { join, dirname, resolve, basename } from 'node:path';
6
+
7
+ export const BRAIN_DIR = '.brain';
8
+
9
+ /** 向上找最近含 .brain/ 的祖先目录。找不到返回 null。 */
10
+ 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
+ }
21
+ }
22
+
23
+ /** 解析图谱内文件的绝对路径。brainRoot 须已定位。 */
24
+ export function brainPath(brainRoot, ...rel) {
25
+ return join(brainRoot, BRAIN_DIR, ...rel);
26
+ }
27
+
28
+ /** 断言 .brain/ 存在,否则抛错(宁可失败不落错项目)。 */
29
+ export async function requireBrain(startDir) {
30
+ const root = await findBrainRoot(startDir);
31
+ if (!root) {
32
+ throw new Error(
33
+ `abs: 找不到 .brain/ 图谱(从 ${resolve(startDir)} 向上搜索无果)。\n` +
34
+ ` 请在项目根先运行: abs init`
35
+ );
36
+ }
37
+ return root;
38
+ }
39
+
40
+ export { basename };