@fanchao8609/agent_brain_sync 1.5.5 → 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/bin/abs.js CHANGED
@@ -69,8 +69,11 @@ function parseArgv(args) {
69
69
  else if (a === '--id') { o.id = args[++i]; }
70
70
  else if (a === '--section') { o.section = args[++i]; }
71
71
  else if (a === '--note') { o.note = args[++i]; }
72
+ else if (a === '--as') { o.as = args[++i]; }
72
73
  else if (a === '--payload') { o.payload = args[++i]; }
73
74
  else if (a === '--keep-days') { o.keepDays = args[++i]; }
75
+ else if (a === '--tags') { o.tags = args[++i]; }
76
+ else if (a === '--help') { o.help = true; }
74
77
  else if (a === '--dry-run') { o.dryRun = true; }
75
78
  else if (a === '--yes') { o.yes = true; }
76
79
  else if (a === '--repair') { o.repair = true; }
@@ -95,7 +98,9 @@ const usage = `abs — agent-brain-sync 记忆工具
95
98
  abs todo add <id> [--note ..] [--section ..] 登记任务 (start 同义)
96
99
  abs todo note <id> --note "断点/进度" 实时落 ↳ 断点 行
97
100
  abs todo blocked <id> --note "卡点原因" 移入 Blocked 区
98
- abs todo done <id> 完成并归位 Done
101
+ abs todo done <id> [--as 落地|否决|仅方案] 完成;结语标明到底"做成了没有"
102
+ 默认 落地。否决=评估后不做(含做了又撤);仅方案=只设计过
103
+ 不加结语或结语失真会让下一个会话把"想过"当成"做完了"。
99
104
  abs log "完成 X:…" 记一行工作成果 (无参=查看)
100
105
  abs note "经验一句话" [--tags 坑,docker] 经验实时暂存 → sources/
101
106
 
@@ -115,6 +120,47 @@ const usage = `abs — agent-brain-sync 记忆工具
115
120
  注: abs wrapup / abs teardown-check 是 hook 内部命令, 不需手动调用。
116
121
  `;
117
122
 
123
+ /** 子命令级用法(abs <cmd> --help 时打印)。 */
124
+ const subUsage = {
125
+ install: [
126
+ 'abs install — 把 MCP + hook + skill 安装到 AI 编码宿主',
127
+ '',
128
+ '用法:',
129
+ ' abs install [--agent <宿主>] [--yes] [--no-mcp] [--no-skill]',
130
+ '',
131
+ '参数:',
132
+ ' --agent <宿主> 只装一个宿主: claude-code | codex | opencode | pi',
133
+ ' (不传则在 TTY 下交互多选; 非 TTY / --yes 时为全部)',
134
+ ' --yes 不交互,直接装全部宿主(自动化用)',
135
+ ' --no-mcp 不注册 MCP server',
136
+ ' --no-skill 不装 skill',
137
+ '',
138
+ '说明:',
139
+ ' • 幂等: 重复安装不会叠加,也不会覆盖宿主的其它配置',
140
+ ' • 配置文件解析失败时会中止(不会清空你的配置)',
141
+ ' • 安装前会备份原配置(同日多份保留最近 5 份)',
142
+ '',
143
+ '例:',
144
+ ' abs install 交互选择',
145
+ ' abs install --yes 全部宿主',
146
+ ' abs install --agent pi --yes 只装 pi',
147
+ ].join('\n'),
148
+ uninstall: [
149
+ 'abs uninstall — 从 AI 编码宿主移除 abs 的 MCP / hook / skill',
150
+ '',
151
+ '用法:',
152
+ ' abs uninstall [--agent <宿主>] [--yes]',
153
+ '',
154
+ '参数:',
155
+ ' --agent <宿主> 只卸一个宿主: claude-code | codex | opencode | pi(不传=全部)',
156
+ ' --yes 不交互',
157
+ '',
158
+ '说明:',
159
+ ' • 只删 abs 自己装的东西,保留你其它的 hook / MCP / 配置字段',
160
+ ' • 配置文件无法解析时会跳过该文件(不覆盖)但仍清理 abs 的脚本与 skill',
161
+ ].join('\n'),
162
+ };
163
+
118
164
  /**
119
165
  * 拒绝多余的未知位置参数。
120
166
  * 坑: 以前只读命令(如 abs todo)会静默忽略多余词 —— `abs todo add x` 照样打印看板、
@@ -201,17 +247,29 @@ async function main() {
201
247
  ` 登记任务: abs todo add <id> --note "做什么"`,
202
248
  );
203
249
  }
204
- rejectExtra(rest2, `abs todo ${sub} <id>${action === 'done' ? '' : ' --note "…"'}`);
205
- if (!id) throw new Error(`✗ 缺 <id>\n 用法: abs todo ${sub} <id>${action === 'done' ? '' : ' --note "…"'}`);
206
- console.log(await cmdTask({ dir: opts.dir, action, id, section: opts.section, note: opts.note }));
250
+ rejectExtra(rest2, `abs todo ${sub} <id>${action === 'done' ? ' [--as 落地|否决|仅方案]' : ' --note "…"'}`);
251
+ if (!id) throw new Error(`✗ 缺 <id>\n 用法: abs todo ${sub} <id>${action === 'done' ? ' [--as 落地|否决|仅方案]' : ' --note "…"'}`);
252
+ console.log(await cmdTask({ dir: opts.dir, action, id, section: opts.section, note: opts.note, as: opts.as }));
207
253
  break;
208
254
  }
209
255
  case 'note': {
210
256
  console.log(await cmdNote({ dir: opts.dir, text: opts._.join(' '), tags: opts.tags }));
211
257
  break;
212
258
  }
213
- case 'install': await runInstall({ agent: opts.agent, mcp: opts.mcp !== false, skill: opts.skill !== false, yes: opts.yes }); break;
214
- case 'uninstall': await runUninstall({ agent: opts.agent, yes: opts.yes }); break;
259
+ case 'install':
260
+ case 'uninstall': {
261
+ // 子命令级 --help: 打印用法后直接返回。
262
+ // 坑(INSTALL-HELP-FOOTGUN): 以前 --help 只在顶层命令被识别,跟在 install 后面时
263
+ // 落到 parseArgv 的通用分支,而 install 分支根本不读它 → 用户想看帮助,
264
+ // 实际执行了全量安装(幂等不炸,但确实改了四宿主配置,写了 15 个文件)。
265
+ if (opts.help) { console.log(subUsage[cmd]); break; }
266
+ if (cmd === 'install') {
267
+ await runInstall({ agent: opts.agent, mcp: opts.mcp !== false, skill: opts.skill !== false, yes: opts.yes });
268
+ } else {
269
+ await runUninstall({ agent: opts.agent, yes: opts.yes });
270
+ }
271
+ break;
272
+ }
215
273
  case 'agents': console.log(installSummary()); break;
216
274
  case 'query': {
217
275
  console.log(await cmdQuery({ dir: opts.dir, terms: opts._ }));
@@ -0,0 +1,113 @@
1
+ /**
2
+ * abs (agent-brain-sync) — Op​encode plugin。
3
+ * 纯触发: 生命周期事件 → 技术日志一行 (~/.abs/log/hooks.log, ABS_LOG_DIR 可覆盖)。fire-and-forget。
4
+ * 纪律: hook 事件只进技术日志, 不进图谱 log.md (log.md 只收工作成果沉淀, 与 event.sh 同纪律)。
5
+ * Op​encode 事件: session.created / session.idle / session.deleted (idle = 每轮结束, 对应 pi 的 agent_end)。
6
+ * 收尾注入: session.idle 且本会话真改过文件 + .brain 今日无记录 → promptAsync 推 agent 走收尾循环。
7
+ */
8
+ import { appendFile, mkdir, readFile, stat } from "node:fs/promises"
9
+ import { homedir } from "node:os"
10
+ import { join, dirname } from "node:path"
11
+
12
+ const server = async ({ client, directory }) => {
13
+ // 本会话是否真改过文件 (由 tool.execute.after 观测)
14
+ let wroteFiles = false
15
+ let nudged = false
16
+ let idleSeen = false
17
+
18
+ async function logHook(evt) {
19
+ try {
20
+ const dir = process.env.ABS_LOG_DIR || join(homedir(), ".abs", "log")
21
+ const d = new Date()
22
+ const pad = (n) => String(n).padStart(2, "0")
23
+ const stamp = d.getFullYear() + "-" + pad(d.getMonth() + 1) + "-" + pad(d.getDate()) + " " + pad(d.getHours()) + ":" + pad(d.getMinutes()) + ":" + pad(d.getSeconds())
24
+ await mkdir(dir, { recursive: true })
25
+ await appendFile(join(dir, "hooks.log"), "[" + stamp + "] opencode:" + evt + "\n")
26
+ } catch {} // fire-and-forget: 永不阻塞宿主
27
+ }
28
+
29
+ // 定位当前项目的 .brain/:只看 cwd 本身,不向上搜索(防爬到家目录图谱)
30
+ async function findBrain(cwd) {
31
+ const d = cwd || process.cwd()
32
+ try { const st = await stat(join(d, ".brain")); return st.isDirectory() ? join(d, ".brain") : null } catch { return null }
33
+ }
34
+
35
+ // 判定"今天是否收尾过": 必须匹配 log.md 的条目头 '## [YYYY-MM-DD HH:MM]'。
36
+ // 坑: 曾用裸日期 substring(includes('2026-09-10')), 结果正文/任务行里任何一处
37
+ // 提到今天就能把 nudge 永久压掉(误判为已收尾), 守卫形同虚设。
38
+ async function loggedToday(brain) {
39
+ try {
40
+ const txt = await readFile(join(brain, "log.md"), "utf8")
41
+ const d = new Date()
42
+ const pad = (n) => String(n).padStart(2, "0")
43
+ const today = d.getFullYear() + "-" + pad(d.getMonth() + 1) + "-" + pad(d.getDate())
44
+ return new RegExp("^## \\[" + today + " \\d{2}:\\d{2}\\]", "m").test(txt)
45
+ } catch { return false }
46
+ }
47
+
48
+ const TEARDOWN_MSG =
49
+ "[abs 收尾提醒] 本会话改过文件但 .brain/ 今天还没有记录。请立即走收尾循环:\n" +
50
+ "1) 跑 abs load 看 Today 还有哪些未完成;\n" +
51
+ "2) 实际做完漏登记的 abs todo done <id>,做到一半的 abs todo note <id> --note \"断点\";\n" +
52
+ "3) 值得留的经验 abs note \"...\"(宁少勿滥,能从代码 grep 到的不记);\n" +
53
+ "4) abs log \"完成 X:...\" 记一行工作成果,新页同步进 index。\n" +
54
+ "简洁执行,不要复述本条提醒。若本次确实没有可沉淀产出,直接回一句\"无可沉淀\"即可。"
55
+
56
+ // bash 里只跑查询类命令不算改文件 (与 pi 侧 READONLY_CMD 同义, 但生成代码里要写进模板串)
57
+ const READONLY_CMD = /^\s*(ls|cat|grep|rg|find|head|tail|wc|git\s+(status|log|diff|show|branch)|pwd|which|echo|node\s+-v|npm\s+(ls|view)|curl)\b/
58
+
59
+ return {
60
+ // 观测真实写操作: write/edit/patch 类工具成功即标记。
61
+ // 坑: 曾漏掉 bash —— op​encode 里很多修改是经 bash(heredoc/sed) 完成的, 纯 bash 会话
62
+ // 永远不置位 wroteFiles, 于是收尾提醒静默不发。与 pi 侧 WRITE_TOOLS 保持一致。
63
+ "tool.execute.after": async ({ tool, args }) => {
64
+ const t = String(tool || "").toLowerCase()
65
+ if (["write", "edit", "patch", "multiedit", "apply_patch"].includes(t)) {
66
+ wroteFiles = true
67
+ return
68
+ }
69
+ // bash 里只有非只读命令算改文件(ls/cat/git status 之类不算)
70
+ if (t === "bash") {
71
+ const cmd = String((args && (args.command || args.cmd)) || "")
72
+ if (cmd && !READONLY_CMD.test(cmd)) wroteFiles = true
73
+ }
74
+ },
75
+
76
+ event: async ({ event }) => {
77
+ const type = event && event.type
78
+ if (!type) return
79
+ if (type === "session.created" || type === "session.deleted") {
80
+ await logHook(type)
81
+ return
82
+ }
83
+ if (type !== "session.idle") return // idle = 每轮结束, 不记日志(太吵), 只做收尾判定
84
+ const cwd = directory || process.cwd()
85
+ const brain = await findBrain(cwd)
86
+ // 可观测性: 首次 idle 无条件留一行痕(否则无法区分“事件没触发”与“被守卫拦下”)
87
+ if (!idleSeen) {
88
+ idleSeen = true
89
+ await logHook(`session.idle:seen cwd=${cwd} brain=${brain || 'none'}`)
90
+ }
91
+ if (nudged || !wroteFiles) return
92
+ const sessionID = event.properties && event.properties.sessionID
93
+ if (!sessionID) return
94
+ if (!brain) return // 无图谱=不在这项目沉淀, 不打扰
95
+ if (await loggedToday(brain)) return // 今天已收尾过
96
+ nudged = true
97
+ await logHook("session.idle:teardown-nudge")
98
+ try {
99
+ await client.session.promptAsync({
100
+ path: { id: sessionID },
101
+ query: { directory: cwd },
102
+ body: { parts: [{ type: "text", text: TEARDOWN_MSG }] },
103
+ })
104
+ } catch {} // 注入失败不阻塞宿主
105
+ },
106
+ }
107
+ }
108
+
109
+ export default {
110
+ id: "abs",
111
+ server,
112
+ }
113
+ @@MARK@@
@@ -0,0 +1,130 @@
1
+ /**
2
+ * abs (agent-brain-sync) — Pi extension。
3
+ * 纯触发: 会话生命周期事件 → 技术日志一行 (~/.abs/log/hooks.log, ABS_LOG_DIR 可覆盖)。fire-and-forget。
4
+ * 纪律: hook 事件只进技术日志, 不进图谱 log.md (log.md 只收工作成果沉淀, 与 event.sh 同纪律)。
5
+ * Pi 事件: session_start / session_shutdown (对应 host hook 的 SessionStart/SessionEnd)。
6
+ * session_shutdown 额外触发 abs wrapup: 把当前项目未完成任务快照到 wrapup.log (跨会话收尾保险)。
7
+ * agent_end 收尾注入: 本会话真改过文件 + .brain 今日无记录 → 注入一条收尾指令, 逼 agent 走收尾循环
8
+ * (被动记日志不够 —— 没人提醒就不会有人收尾)。每会话最多一次, 且已收尾后不再打扰。
9
+ */
10
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"
11
+ import { appendFile, mkdir, readFile, stat } from "node:fs/promises"
12
+ import { homedir } from "node:os"
13
+ import { join, dirname } from "node:path"
14
+ import { spawn } from "node:child_process"
15
+
16
+ const ABS_BIN = "@@ABS_BIN@@"
17
+
18
+ async function logHook(evt: string): Promise<void> {
19
+ const dir = process.env.ABS_LOG_DIR || join(homedir(), ".abs", "log")
20
+ const d = new Date()
21
+ const pad = (n: number) => String(n).padStart(2, "0")
22
+ const stamp = d.getFullYear() + "-" + pad(d.getMonth() + 1) + "-" + pad(d.getDate()) + " " + pad(d.getHours()) + ":" + pad(d.getMinutes()) + ":" + pad(d.getSeconds())
23
+ const line = "[" + stamp + "] pi:" + evt + "\n"
24
+ await mkdir(dir, { recursive: true })
25
+ await appendFile(join(dir, "hooks.log"), line)
26
+ }
27
+
28
+ // 会话结束时快照当前项目滞留任务到 wrapup.log(detached fire-and-forget,wrapup 自身幂等去重不刷屏)。
29
+ // cwd 用事件 ctx.cwd(当前项目),让 abs 在该目录定位 .brain/。
30
+ function snapshotWrapup(cwd: string): void {
31
+ try {
32
+ const child = spawn(process.execPath, [ABS_BIN, "wrapup"], {
33
+ cwd: cwd || process.cwd(), stdio: "ignore", detached: true,
34
+ })
35
+ child.unref()
36
+ } catch {} // fire-and-forget
37
+ }
38
+
39
+ // ---- 收尾注入判定 ----
40
+ // 真改过文件的工具(read/grep/ls 不算, 那些不产生可沉淀的产出)
41
+ const WRITE_TOOLS = new Set(["write", "edit", "multi_edit", "apply_patch", "bash"])
42
+ // bash 里只跑查询类命令不算改文件
43
+ const READONLY_CMD = /^\s*(ls|cat|grep|rg|find|head|tail|wc|git\s+(status|log|diff|show|branch)|pwd|which|echo|node\s+-v|npm\s+(ls|view)|curl)\b/
44
+
45
+ function hasWriteWork(toolResults: any[]): boolean {
46
+ for (const r of toolResults || []) {
47
+ const name = String(r?.toolName || "")
48
+ if (!WRITE_TOOLS.has(name)) continue
49
+ if (r?.isError) continue
50
+ if (name === "bash") {
51
+ const cmd = String(r?.input?.command ?? r?.args?.command ?? "")
52
+ if (READONLY_CMD.test(cmd)) continue
53
+ }
54
+ return true
55
+ }
56
+ return false
57
+ }
58
+
59
+ /** 定位当前项目的 .brain/:只看 cwd 本身,不向上搜索(防爬到家目录图谱)。 */
60
+ async function findBrain(cwd: string): Promise<string | null> {
61
+ const d = cwd || process.cwd()
62
+ try { const st = await stat(join(d, ".brain")); return st.isDirectory() ? join(d, ".brain") : null } catch { return null }
63
+ }
64
+
65
+ /**
66
+ * .brain/log.md 今天有记录吗? 有=已收尾, 不再打扰。
67
+ * 必须匹配条目头 '## [YYYY-MM-DD HH:MM]' —— 裸日期 substring 会被正文里任意一处
68
+ * 今天的日期误命中, 导致"已收尾"误判、nudge 永不触发。
69
+ */
70
+ async function loggedToday(brain: string): Promise<boolean> {
71
+ try {
72
+ const txt = await readFile(join(brain, "log.md"), "utf8")
73
+ const d = new Date()
74
+ const pad = (n: number) => String(n).padStart(2, "0")
75
+ const today = d.getFullYear() + "-" + pad(d.getMonth() + 1) + "-" + pad(d.getDate())
76
+ // 注意: 这里是真实 TS, 不是生成代码的模板字符串。用拼接形式避免反斜杠逃逸歧义。
77
+ return new RegExp("^## \\[" + today + " \\d{2}:\\d{2}\\]", "m").test(txt)
78
+ } catch { return false }
79
+ }
80
+
81
+ export default function absPiHook(pi: ExtensionAPI): void {
82
+ pi.on("session_start", () => logHook("session_start").catch(() => {}))
83
+
84
+ // 收尾注入: 每个会话最多一次, 避免反复打扰。
85
+ let teardownNudged = false
86
+ let agentEndSeen = false
87
+ pi.on("agent_end", async (event: any, ctx: any) => {
88
+ if (teardownNudged) return
89
+ const cwd = (ctx && ctx.cwd) || process.cwd()
90
+ const brain = await findBrain(cwd)
91
+ // 可观测性: 本会话首次 agent_end 无条件留一行痕。
92
+ // 没有它就无法区分三种状态: ①事件没触发 ②触发了但被守卫拦下 ③真注入了。
93
+ if (!agentEndSeen) {
94
+ agentEndSeen = true
95
+ // 带上 cwd 与命中的 brain —— 否则事后无法解释“为何这条 nudge 会出现”
96
+ await logHook(`agent_end:seen cwd=${cwd} brain=${brain || 'none'}`).catch(() => {})
97
+ }
98
+ if (!brain) return // 无图谱=不在这项目沉淀, 不打扰
99
+ if (!hasWriteWork(event?.messages ? collectToolResults(event.messages) : [])) return
100
+ if (await loggedToday(brain)) return // 今天已收尾过
101
+ teardownNudged = true
102
+ await logHook("agent_end:teardown-nudge").catch(() => {})
103
+ try {
104
+ pi.sendUserMessage(
105
+ "[abs 收尾提醒] 本会话改过文件但 .brain/ 今天还没有记录。请立即走收尾循环:\n" +
106
+ "1) 跑 abs load 看 Today 还有哪些未完成;\n" +
107
+ "2) 实际做完漏登记的 abs todo done <id>,做到一半的 abs todo note <id> --note \"断点\";\n" +
108
+ "3) 值得留的经验 abs note \"...\"(宁少勿滥,能从代码 grep 到的不记);\n" +
109
+ "4) abs log \"完成 X:...\" 记一行工作成果,新页同步进 index。\n" +
110
+ "简洁执行,不要复述本条提醒。若本次确实没有可沉淀产出,直接回一句\"无可沉淀\"即可。",
111
+ { deliverAs: "followUp" },
112
+ )
113
+ } catch {}
114
+ })
115
+
116
+ pi.on("session_shutdown", (_e: any, ctx: any) => {
117
+ snapshotWrapup((ctx && ctx.cwd) || process.cwd())
118
+ logHook("session_shutdown").catch(() => {})
119
+ })
120
+ }
121
+
122
+ /** agent_end 的 event.messages 里翻出 toolResult 消息。 */
123
+ function collectToolResults(messages: any[]): any[] {
124
+ const out: any[] = []
125
+ for (const m of messages || []) {
126
+ if (m && m.role === "toolResult") out.push(m)
127
+ }
128
+ return out
129
+ }
130
+ @@MARK@@
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fanchao8609/agent_brain_sync",
3
- "version": "1.5.5",
3
+ "version": "1.5.7",
4
4
  "description": "agent-brain-sync: 跨会话 AI 编码记忆 — hook 纯触发 + CLI/MCP 读写 .brain markdown 图谱, 防并发写保护。",
5
5
  "type": "module",
6
6
  "license": "MIT",