@fanchao8609/agent_brain_sync 1.20.1 → 1.22.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/README.md CHANGED
@@ -17,14 +17,17 @@ npm install -g @fanchao8609/agent_brain_sync
17
17
  abs install
18
18
  ```
19
19
 
20
- `abs install` 会交互式让你勾选智能体;也可以直接指定:
20
+ **默认只装 pi**(其余宿主 opt-in)——装到不用的宿主上会往那边配置文件塞 hook/MCP 条目。
21
21
 
22
22
  ```bash
23
- abs install --agent pi # 只装 pi
24
- abs install # 全部: claude-code / codex / opencode / pi / omp
23
+ abs install # 默认: 只装 pi
24
+ abs install --all # 全装: claude-code / codex / opencode / pi / omp
25
+ abs install --agent claude-code # 只装指定宿主
25
26
  abs install --agent pi --no-mcp # 只装 hook + skill,不要 MCP
26
27
  ```
27
28
 
29
+ 交互模式下回车 = 只装 pi;输入 `all` 才全装。
30
+
28
31
  装了什么:
29
32
 
30
33
  | 宿主 | hook | MCP | skill |
@@ -117,6 +120,13 @@ abs todo done TASK-1
117
120
  abs note "一句话经验" --tags 坑,docker # 经验暂存 → sources/
118
121
  abs log "完成 X" # 记一行流水;abs log 无参 = 查看
119
122
  abs query <词> # 检索图谱(多词 OR);已被推翻的经验默认隐藏(--all 可看)
123
+ abs capture [--layer 1-7] "一句话" # 按七层判层后直接落到位(不传 = L4 认知)
124
+ abs db rebuild # 扫所有登记项目 → 建图数据库(~/.abs/graph.db)
125
+ abs db search <词> # 走库检索:FTS 拿种子 + 亮度扩散,L1 铁律恒在最前
126
+ abs db status # 库体检:七层分布 / 悬挂链接 / 已入库项目
127
+ abs db add # 把当前项目登记进 ~/.abs/projects.json
128
+ abs db evolve # 知识进化体检:被推翻/断链/主题堆积/僵化页
129
+ abs summon [--mark|--status] # 会话总结托底:列出未总结 / 有更新的会话(不写盘)
120
130
  abs resolve <页名或id> # 反查页面路径(页改名后 id 不变)
121
131
  abs supersede <页名> [--by <新页>] # 标记经验已失效(不删文件,保留历史;query 默认不再返回)
122
132
  abs status # 当前项目 + 图谱概要
package/bin/abs.js CHANGED
@@ -3,6 +3,9 @@
3
3
  // abs <cmd> [args]
4
4
  // 命令: init / board / status / load / task / install / uninstall / help
5
5
  import { cmdInit, cmdStatus, cmdLoad, cmdTask, cmdLog, cmdQuery, cmdLint, cmdNote, cmdConcept, cmdShow, cmdRepair, cmdWrapup, cmdRule, cmdTeardownCheck, cmdTodoArchive, cmdResolve, cmdSupersede, cmdReview } from '../src/store.js';
6
+ import { cmdDbRebuild, cmdDbSearch, cmdDbStatus, cmdDbEvolve, addProject } from '../src/brain/db-cmd.js';
7
+ import { cmdCapture } from '../src/brain/capture.js';
8
+ import { cmdSummon } from '../src/brain/summon-cmd.js';
6
9
  import { setUser, getUser, userConfigPath } from '../src/userconfig.js';
7
10
  import { runInstall, runUninstall } from '../src/install.js';
8
11
  import { readFileSync } from 'node:fs';
@@ -101,6 +104,13 @@ const FLAG_SPEC = {
101
104
  'session': { type: 'string' },
102
105
  'payload': { type: 'string' },
103
106
  'tags': { type: 'string' },
107
+ // db search 用:最多返回几页(亮度排序后的截断)。未声明会被静默吞成 true。
108
+ 'limit': { type: 'string' },
109
+ // capture 用:1-7 的层号。未声明会被静默吞成 true。
110
+ 'layer': { type: 'string' },
111
+ // summon 用:布尔开关。未声明会被静默吞成 true(--mark 会变成真值即生效,但 --status 同理)。
112
+ 'mark': { type: 'boolean' },
113
+ 'status': { type: 'boolean' },
104
114
  'port': { type: 'string' },
105
115
  // note 的触发条件(何时该读这条经验)—— 供 load 的相关页推荐匹配。
106
116
  'when': { type: 'string' },
@@ -198,6 +208,10 @@ function parseArgv(args) {
198
208
  all: !!values.all,
199
209
  payload: values.payload,
200
210
  tags: values.tags,
211
+ limit: values.limit === undefined ? undefined : Number(values.limit),
212
+ layer: values.layer,
213
+ mark: !!values.mark,
214
+ status: !!values.status,
201
215
  title: values.title,
202
216
  desc: values.desc,
203
217
  help: !!values.help,
@@ -231,8 +245,11 @@ const usage = `abs — 跨会话记忆工具(.brain/ 图谱)
231
245
  todo 任务看板 (add/note/state/done/archive)
232
246
  index | log | status 索引 / 流水 / 概要
233
247
  note <经验> [--tags a,b] 经验暂存 → sources/
248
+ capture [--layer 1-7] "…" 按七层判定落点后直接写入 (不传=L4)
249
+ summon [--mark|--status] 会话总结托底: 列出未总结/有更新的会话 (不写盘)
234
250
  concept <slug> --title … 建概念页骨架
235
251
  query <词…> [--all] 检索知识页 (多词 OR)
252
+ db rebuild|search|status|evolve 图数据库: 建库 / 亮度搜索 / 体检 / 进化体检
236
253
  resolve <页名…> 反查页面路径
237
254
  supersede <页名> [--by 页] 标经验已失效
238
255
  review [--accept 页…|--reject 页…] 待确认页队列 (draft 升 active/否决)
@@ -512,6 +529,14 @@ async function main() {
512
529
  console.log(await cmdTask({ dir: opts.dir, action, id, section: opts.section, note: opts.note, as: opts.as, session: opts.session }));
513
530
  break;
514
531
  }
532
+ case 'summon': {
533
+ console.log(await cmdSummon({ dir: opts.dir, mark: !!opts.mark, full: !!opts.full, status: !!opts.status, limit: Number(opts.limit) || 15 }));
534
+ break;
535
+ }
536
+ case 'capture': {
537
+ console.log(await cmdCapture({ dir: opts.dir, text: opts._.join(' '), layer: opts.layer, tags: opts.tags, title: opts.title }));
538
+ break;
539
+ }
515
540
  case 'note': {
516
541
  console.log(await cmdNote({ dir: opts.dir, text: opts._.join(' '), tags: opts.tags, when: opts.when, impact: opts.impact, type: opts.type }));
517
542
  break;
@@ -531,7 +556,7 @@ async function main() {
531
556
  case 'uninstall': {
532
557
  // --help 已在 switch 前统一拦截(subUsage)。
533
558
  if (cmd === 'install') {
534
- await runInstall({ agent: opts.agent, mcp: opts.mcp !== false, skill: opts.skill !== false, yes: opts.yes });
559
+ await runInstall({ agent: opts.agent, all: !!opts.all, mcp: opts.mcp !== false, skill: opts.skill !== false, yes: opts.yes });
535
560
  } else {
536
561
  await runUninstall({ agent: opts.agent, yes: opts.yes });
537
562
  }
@@ -550,6 +575,21 @@ async function main() {
550
575
  console.log(await cmdRule({ dir: opts.dir, action: isAdd ? 'add' : 'list', text: rest3.join(' ') }));
551
576
  break;
552
577
  }
578
+ case 'db': {
579
+ const sub = opts._[0];
580
+ const rest = opts._.slice(1);
581
+ if (sub === 'rebuild') { console.log(await cmdDbRebuild({ dir: opts.dir })); break; }
582
+ if (sub === 'search') { console.log(await cmdDbSearch({ query: rest.join(' '), limit: Number(opts.limit) || 8 })); break; }
583
+ if (sub === 'status') { console.log(await cmdDbStatus()); break; }
584
+ if (sub === 'evolve') { console.log(await cmdDbEvolve({ staleDays: Number(opts['stale-days']) || 90 })); break; }
585
+ if (sub === 'add') {
586
+ const r = await addProject(opts.dir || process.cwd());
587
+ console.log(`✓ 已登记项目: ${r}\n 建库: abs db rebuild`);
588
+ break;
589
+ }
590
+ console.log('用法: abs db rebuild | search <词> | status | evolve | add [--dir <项目根>]');
591
+ break;
592
+ }
553
593
  case 'query': {
554
594
  console.log(await cmdQuery({ dir: opts.dir, terms: opts._, includeSuperseded: opts.all }));
555
595
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fanchao8609/agent_brain_sync",
3
- "version": "1.20.1",
3
+ "version": "1.22.0",
4
4
  "description": "agent-brain-sync: 跨会话 AI 编码记忆 — hook 纯触发 + CLI/MCP 读写 .brain markdown 图谱, 防并发写保护。",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -19,7 +19,8 @@ description: "USE FOR abs / agent-brain-sync 跨会话记忆与任务续接。
19
19
  abs load # 开机读状态
20
20
  abs query <词> # 检索;abs resolve <页名> 反查路径
21
21
  abs todo add <id> --note "做什么" # 登记;note/memo/state/done 见「进行中」
22
- abs note "经验" [--tags a,b] # 暂存经验
22
+ abs capture [--layer 1-7] "…" # 按七层判定落点后直接写入(不传=L4)
23
+ abs note "经验" [--tags a,b] # 暂存经验(一律 L7 混沌层)
23
24
  abs concept <slug> --title "…" # 建概念页骨架
24
25
  abs rule add "一句话" # 加 Rules;abs lint 体检;abs init 建图谱
25
26
  ```
@@ -93,6 +94,24 @@ abs rule add "一句话" # 加 Rules;abs lint 体检;abs init
93
94
  3. **读命中页**:`abs query <词>` → `abs resolve <页名或id>` 拿路径,**只读命中那几页**,绝不通读 `.brain/`;多文件汇总交 `ctx_execute` 在沙箱处理,只打印结论。
94
95
  4. **续 todo**:把顶部未完成项当当前任务开做;有新任务而 todo 没有 → `abs todo add <id> --note 做什么` 再动工。
95
96
 
97
+ ## 按七层总结(总结前必读)
98
+
99
+ 七层不是分类好看,是**检索权重不同**:L1 永不衰减,L7 几乎不参与。所以总结时必须先判层,判完直接落到位 —— 一律塞 sources/ 会让 44% 的知识按错误的权重参与检索。
100
+
101
+ | 层 | 判据 | 落点 | 怎么落 |
102
+ |---|---|---|---|
103
+ | L1 铁律 | 违反就必须停下 | index 的 `## Rules` | `abs capture --layer 1`(≤42 字、禁链接) |
104
+ | L2 规则 | 这个项目要遵守的做法 | `concepts/` + `layer: 2` | `abs capture --layer 2` |
105
+ | L3 工具 | 某工具怎么用、有什么坑 | `concepts/` + `layer: 3` | `abs capture --layer 3` |
106
+ | L4 认知 | 踩坑的根因与解法 | `concepts/` + `layer: 4` | `abs capture "…"`(默认) |
107
+ | L5 会话 | 这次做了什么 | `log.md` / `sessions/` | `abs log` / `abs todo done` |
108
+ | L6 断点 | 双链词 | 不立页,机器自动抽 | 不接手写 |
109
+ | L7 混沌 | 还没想清的碎片 | `sources/` | `abs capture --layer 7` |
110
+
111
+ 判据不明时用 L4 —— 但**知道是工具经验就写 L3**:实测算过,本仓 52 页 concepts 里 23 页其实是 L3。
112
+
113
+ `abs capture` 之后跑 `abs db rebuild` 让图数据库收录,否则检索拿不到它。
114
+
96
115
  ## 进行中:什么时候动它
97
116
 
98
117
  todo 是随改随写的活看板,和 git commit 同反射。
@@ -104,7 +123,7 @@ todo 是随改随写的活看板,和 git commit 同反射。
104
123
  | 被打断 / 干到一半 | `note` + 改到哪个文件哪一步 | 任务下追加一条子项(**一句话**) |
105
124
  | 干活中随手记多句发现 | `memo` + 原文 | `sessions/` 当日笔记,**不上看板** |
106
125
  | 子任务做完 | `done` + note 结语 + as 落地\|否决\|仅方案 | Done |
107
- | 总结出经验/坑/规律 | `abs_note` text + tags | `sources/` |
126
+ | 总结出经验/坑/规律 | `abs capture --layer N`(先按上表判层) | 按层落到位 |
108
127
 
109
128
  **四个落点的区别**:
110
129
 
@@ -0,0 +1,175 @@
1
+ // src/brain/capture.js — 按七层判定落点的统一总结入口。
2
+ //
3
+ // 为何需要它(2026-11-01 实测):七层建好了,但总结这个动作还是老的一根管道 ——
4
+ // `abs note` 一律进 sources/(L7),`abs concept` 一律进 concepts/(L4),
5
+ // `abs rule` 一律进 ## Rules(L1)。落点由**命令**写死,不由**内容**决定。
6
+ // 结果是 52 页 concepts 里 23 页(44%)其实是工具经验,全按 L4 的 0.6 衰减算 ——
7
+ // 分层在检索权重上的作用被抹平了。
8
+ //
9
+ // 本命令让判层发生在「总结时」,判完直接落到位,不留二次分拣。
10
+ // 沿用 note.js 的两条既有约定:整条相等才算重复(不是子串)、tmp+rename 原子落盘。
11
+
12
+ import { promises as fs } from 'node:fs';
13
+ import { join } from 'node:path';
14
+ import { requireBrain, brainPath } from '../index.js';
15
+ import { requireUser, atTag } from '../userconfig.js';
16
+ import { today } from '../todo.js';
17
+ import { registerInIndex } from '../note.js';
18
+ import { cmdLog } from '../note.js';
19
+ import { clip, slugOf } from '../text.js';
20
+ import { LAYERS, layerOf, DEFAULT_LAYER } from './layers.js';
21
+ import { autoIndex } from './db-cmd.js';
22
+ import { summonTail } from './summon.js';
23
+
24
+ /** 层 → 落点目录。L1/L6 不立页,由下面的分支单独处理。 */
25
+ const LAYER_DIR = {
26
+ 2: 'concepts',
27
+ 3: 'concepts',
28
+ 4: 'concepts',
29
+ 5: 'sessions',
30
+ 7: 'sources',
31
+ };
32
+
33
+ /** 判据表:输入时就看得到,不用记。 */
34
+ export function layerGuide() {
35
+ const rows = [
36
+ ['1', '铁律', '违反就必须停下的事', 'abs capture --layer 1 "…"'],
37
+ ['2', '规则', '这个项目要遵守的做法', 'abs capture --layer 2 "…"'],
38
+ ['3', '工具', '某工具怎么用、有什么坑', 'abs capture --layer 3 "…"'],
39
+ ['4', '认知', '踩坑的根因与解法(默认)', 'abs capture "…"'],
40
+ ['5', '会话', '这次会话做了什么', 'abs log / abs todo done'],
41
+ ['6', '断点', '双链词,机器自动抽,不用手写', '(无)'],
42
+ ['7', '混沌', '还没想清的碎片', 'abs capture --layer 7 "…"'],
43
+ ];
44
+ return [
45
+ '按七层判定这一句该落在哪:',
46
+ '',
47
+ '层 名称 判据 命令',
48
+ ...rows.map(([id, name, when, cmd]) => `L${id} ${name} ${when.padEnd(26)} ${cmd}`),
49
+ '',
50
+ '不传 --layer 时默认 L4(认知)。',
51
+ 'L1 走铁律门槛(≤42 字、禁链接);L6 是机器自动抽的双链词,不接手动。',
52
+ ].join('\n');
53
+ }
54
+
55
+ export async function cmdCapture({ dir, text, layer, tags, title }) {
56
+ const clean = String(text || '').trim();
57
+ const layerArg = String(layer || '').trim();
58
+
59
+ if (!clean) {
60
+ return [
61
+ '用法: abs capture [--layer 1-7] "一句话" [--tags a,b] [--title "页标题"]',
62
+ '',
63
+ layerGuide(),
64
+ ].join('\n');
65
+ }
66
+
67
+ // 层号校验:非法直接拒,不静默当默认层(否则记错层没人知道)
68
+ let L = DEFAULT_LAYER;
69
+ if (layerArg) {
70
+ const n = Number(layerArg);
71
+ if (!Number.isInteger(n) || !layerOf(n)) {
72
+ return `✗ --layer 非法(读到 "${layerArg}")。只能是 1-7 的整数。\n\n${layerGuide()}`;
73
+ }
74
+ L = n;
75
+ }
76
+
77
+ // L6 是机器自动抽的双链词,不接受手动写入 —— 手写会让断点词表失真
78
+ if (L === 6) {
79
+ return '✗ L6 断点层是机器从双链里自动抽的词,不接手写。\n 想加索引词:在页面正文写 [[词]],abs db rebuild 时会自动收录。';
80
+ }
81
+
82
+ // L1 铁律:复用 cmdRule 的门槛(≤42 字、禁链接),不另起一套
83
+ if (L === 1) {
84
+ const { cmdRule } = await import('./rules.js');
85
+ const r = await cmdRule({ dir, action: 'add', text: clean });
86
+ return r;
87
+ }
88
+
89
+ let root;
90
+ try {
91
+ root = await requireBrain(dir || process.cwd());
92
+ } catch {
93
+ return '未找到 .brain/ 图谱。先在项目根运行: abs init';
94
+ }
95
+ const who = await requireUser();
96
+ const { ensurePersonPage } = await import('../note.js');
97
+ await ensurePersonPage(root, who);
98
+
99
+ const dirName = LAYER_DIR[L];
100
+ if (!dirName) return `✗ L${L} 没有对应目录(这层不立页)。`;
101
+
102
+ const targetDir = brainPath(root, dirName);
103
+ await fs.mkdir(targetDir, { recursive: true });
104
+
105
+ // 幂等:整条相等才算重复(子串判据会把「新经验恰好是旧经验的子串」丢掉)
106
+ const titleLine = `TITLE: ${clean}`;
107
+ for (const f of (await fs.readdir(targetDir).catch(() => [])).filter((x) => x.endsWith('.md'))) {
108
+ const body = await fs.readFile(join(targetDir, f), 'utf8').catch(() => '');
109
+ if (body.split('\n').some((l) => l.trim() === titleLine)) {
110
+ return `• 已落过同文本 → ${dirName}/${f} (跳过重复)`;
111
+ }
112
+ }
113
+
114
+ const head = String(title || '').trim() || clip(clean, 80);
115
+ const tagList = String(tags || '').split(',').map((t) => t.trim()).filter(Boolean);
116
+ const meta = layerOf(L);
117
+ const slugSrc = slugOf(clean);
118
+ const file = `${today()}-${slugSrc || 'capture'}.md`;
119
+
120
+ const body = [
121
+ '---',
122
+ `tags: [${['concept', ...tagList].join(', ')}]`,
123
+ `layer: ${L}`,
124
+ `author: ${who}`,
125
+ `updated: ${today()}`,
126
+ 'status: draft',
127
+ '---',
128
+ '',
129
+ `# ${meta.name}:${head}`,
130
+ '',
131
+ `TITLE: ${clean}`,
132
+ '',
133
+ '## 记录',
134
+ `- ${clean}`,
135
+ '',
136
+ // 「验证」是本仓硬要求(lint 的 NO-TAIL):没尾巴的经验只能被相信、不能被验证。
137
+ // 占位注释不算内容(hasTail 明确跳过 <!-- -->),所以这里给的是**可填的实际位置**,
138
+ // 而不是一句注释就交差 —— 那会让新页天生就带 NO-TAIL。
139
+ '## 验证',
140
+ '- 做完怎么确认:跑什么命令 / 看什么信号 / 按什么判据',
141
+ '',
142
+ '## 关联连接',
143
+ `- ${atTag(who)} — 本页沉淀者`,
144
+ '(挂相关页双链,别留孤岛;本页无人链入时 lint 会报 NO-INBOUND)',
145
+ '',
146
+ ].join('\n');
147
+
148
+ // tmp + rename 原子落盘(并发读不会读到半写文件)
149
+ const dst = join(targetDir, file);
150
+ const tmp = join(targetDir, `.${file}.tmp-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`);
151
+ await fs.writeFile(tmp, body, 'utf8');
152
+ await fs.rename(tmp, dst);
153
+
154
+ const slug = file.replace(/\.md$/, '');
155
+ const section = dirName === 'concepts' ? 'Concepts' : dirName === 'sessions' ? 'Sessions' : 'Sources';
156
+ await registerInIndex(root, section, slug, head);
157
+ await cmdLog({ dir: root, title: clean, kind: 'note', summon: false });
158
+
159
+ // 落盘即入库 —— 否则用户以为记下了,检索却空手而归(静默失效)。
160
+ // autoIndex 永不抛错也不阻塞:库坏了只是检索旧一点,不能让记笔记失败。
161
+ const idx = await autoIndex();
162
+
163
+ // 每次总结都顺手查一遍会话历史,避免漏下没总结的会话(2026-11-02 用户要求)。
164
+ // 只给线索不写盘 —— 落哪一条仍是人的判断。
165
+ const tail = await summonTail(root);
166
+
167
+ return [
168
+ `✓ 已落入 L${L} ${meta.name}层 → ${dirName}/${file}`,
169
+ ` ${clip(clean, 60)} ${atTag(who)}`,
170
+ ` 该层衰减 ${meta.decay}(${meta.note})`,
171
+ idx.ok ? ' 已同步入库,abs db search 可查到' : ` (未入库:${idx.reason})`,
172
+ ].join('\n') + tail;
173
+ }
174
+
175
+ export { LAYERS };