@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/README.md +112 -0
- package/bin/abs.js +112 -0
- package/bin/mcp.js +167 -0
- package/hooks/event.sh +58 -0
- package/package.json +33 -0
- package/skill/SKILL.md +200 -0
- package/src/brainio.js +66 -0
- package/src/hosts.js +65 -0
- package/src/index.js +40 -0
- package/src/install.js +416 -0
- package/src/lock.js +78 -0
- package/src/store.js +499 -0
- package/src/todo.js +308 -0
- package/src/wrapup.js +137 -0
package/README.md
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# abs — agent-brain-sync 跨会话 AI 编码记忆
|
|
2
|
+
|
|
3
|
+
跨会话记忆的机械落盘:**hook 纯触发 → CLI/MCP 读写 → markdown 图谱(`.brain/`,可进 git)**。
|
|
4
|
+
让每个会话不再无状态——经验/进度/踩坑有统一落点,重开不"失忆"。
|
|
5
|
+
参考 [ai-memory](https://github.com/akitaonrails/ai-memory) 的接入机制,砍掉全部重引擎。
|
|
6
|
+
|
|
7
|
+
> npm 发布名 `@fanchao8609/agent_brain_sync`(因 `agent-brain-sync` 已存在、相似名被 npm 拦截);全局命令 `abs`(包名与命令名独立)。
|
|
8
|
+
|
|
9
|
+
## 架构
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
hook (宿主事件, 纯触发) ──▶ abs log (一行技术流水, fire-and-forget, 不阻塞)
|
|
13
|
+
agent (会话中) ──▶ MCP (abs_load/abs_board/abs_task/…) ──▶ 读写 .brain/
|
|
14
|
+
人 (终端) ──▶ abs CLI 同一套读写
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
- **唯一真源**: `<项目>/.brain/`(index/todo/log + concepts/entities/sources/syntheses/sessions)
|
|
18
|
+
- **多项目隔离**: 任何读写先从 cwd 向上找最近 `.brain/`(代码写死,MCP 无状态,各会话各归各位)
|
|
19
|
+
- **三层分工**:
|
|
20
|
+
- **hook(机械层)**:宿主生命周期事件 → 只写技术日志(`~/.abs/log/`),不碰图谱 todo/log.md
|
|
21
|
+
- **CLI/MCP(实时层)**:任务/经验实时落盘(`abs task start/note/blocked/done`、`abs note`)
|
|
22
|
+
- **skill(自觉层)**:深提炼(sources→concepts)+ 收尾循环(详见 skill/SKILL.md)
|
|
23
|
+
- **并发写保护**: 所有 .brain 读改写经 `lock.editFile`(`open wx` 原子锁 + 排队等锁 + SKIP 哨兵),跨进程(CLI/MCP/hook)同写不覆盖丢更新
|
|
24
|
+
|
|
25
|
+
## 安装
|
|
26
|
+
|
|
27
|
+
### 方式一:npm 全局安装(产品模式,推荐)
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install -g @fanchao8609/agent_brain_sync
|
|
31
|
+
abs install # 交互式选智能体(或 --agent 指定)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### 方式二:从源码(开发)
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
git clone <repo> && cd <repo> && npm install
|
|
38
|
+
node bin/abs.js install # 或先 npm link 使 `abs` 全局可用
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### 安装到宿主(hook + MCP + skill)
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
abs install # 全部: claude-code codex opencode pi
|
|
45
|
+
abs install --agent claude-code # 只装 Claude Code (MCP+hook+skill)
|
|
46
|
+
abs install --agent claude-code --no-mcp # 只 hook+skill
|
|
47
|
+
|
|
48
|
+
abs uninstall --agent claude-code # 卸载(只删 abs 装的,保留其它共存 hook)
|
|
49
|
+
abs uninstall # 全部
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
安装内容:
|
|
53
|
+
| 宿主 | hook | MCP | skill |
|
|
54
|
+
|---|---|---|---|
|
|
55
|
+
| claude-code | `~/.claude/settings.json` hooks(与 moshi-hook 等**共存追加**,不覆盖) | settings.json `mcpServers.abs` (stdio) | `~/.claude/skills/abs-agent-brain-sync/` |
|
|
56
|
+
| codex | `~/.codex/hooks.json` | config.toml `[mcp_servers.abs]` | `~/.codex/skills/` |
|
|
57
|
+
| opencode | `~/.config/opencode/plugins/abs.ts` | opencode.json mcp.abs | skills/ |
|
|
58
|
+
| pi | `~/.pi/agent/extensions/abs.ts` | extension 内桥接 | skills/ |
|
|
59
|
+
|
|
60
|
+
幂等:重复安装=更新;写入前自动备份;卸载只删 abs 的条目、保留其它共存 hook。
|
|
61
|
+
|
|
62
|
+
## 项目里用
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
abs init # 项目根建 .brain/ 图谱(一次)
|
|
66
|
+
abs load # 开机读状态(index 路线 + todo 看板 + 最近 log)
|
|
67
|
+
abs todo # 看板(Today/Backlog/Blocked/Done)
|
|
68
|
+
abs task start TASK-1 --note "做什么" # 登记(幂等)
|
|
69
|
+
abs task note TASK-1 --note "改到X文件L40" # 实时断点(↳ 断点: 行,幂等)
|
|
70
|
+
abs task blocked TASK-1 --note "卡点原因" # 碰壁移 Blocked
|
|
71
|
+
abs task done TASK-1 # 完成归位 Done
|
|
72
|
+
abs note "经验一句话" --tags 坑,docker # 经验实时暂存 → sources/
|
|
73
|
+
abs log "完成X:…" # 记一行工作成果流水;abs log 无参=查看 log.md
|
|
74
|
+
abs query <词> # 检索图谱(多词 OR)
|
|
75
|
+
abs lint # 图谱体检(死链/孤岛/超尺寸/堆积/index 漏列)
|
|
76
|
+
abs status # 当前项目 + 图谱概要
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
实时化分工:**hook 自动记技术流水;任务/经验经 CLI/MCP 实时落盘(每个任务边界立即调);
|
|
80
|
+
深提炼(sources→concepts)归收尾自觉层,工具不替你判断什么值得沉淀。** 详见 `skill/SKILL.md`。
|
|
81
|
+
|
|
82
|
+
## 核心能力
|
|
83
|
+
|
|
84
|
+
- **并发写保护(lock)**: `.brain` 是"读-改-整写回",跨进程并发会互相覆盖。`src/lock.js` 用同目录 `.lock` 文件 `open('wx')` 原子抢占 + 排队等锁(预算 30s,指数退避)+ `SKIP` 哨兵,把 todo/index/log 的写串行化,避免丢失更新。
|
|
85
|
+
- **统一读写收口(brainio)**: `src/brainio.js` 提供 `readBrain / writeBrain / appendBrain / createBrainFile`——所有需读写 .brain 文档的地方统一走它,写自动带 lock 防并发(新代码遵循此入口)。
|
|
86
|
+
- **收尾自动化**: Stop 时 hook 在 `~/.abs/log/wrapup.log` 留 wrapup 提醒;下会话开头走"收尾循环"(对账 todo / 沉淀经验 / 修 index/log)。
|
|
87
|
+
- **多宿主共存**: install 分区合并追加,不会顶掉同事件的其它 hook(如 moshi-hook)。
|
|
88
|
+
|
|
89
|
+
## 开发
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
npm test # 全量 110+ 单测(四层: CLI/install/MCP/hook + lock/brainio)
|
|
93
|
+
npm run pack:check # 预览 npm 发布产物(files 白名单)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## 目录
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
agent_brain_sync/
|
|
100
|
+
├── bin/abs.js CLI 入口
|
|
101
|
+
├── bin/mcp.js MCP server (stdio, 官方 SDK, 窄工具面)
|
|
102
|
+
├── src/index.js 图谱定位(向上找最近 .brain/)
|
|
103
|
+
├── src/lock.js .brain 并发写保护(原子锁 + 排队 + SKIP)
|
|
104
|
+
├── src/brainio.js 统一读写收口(readBrain/writeBrain/appendBrain)
|
|
105
|
+
├── src/todo.js todo.md 分区读写
|
|
106
|
+
├── src/store.js CLI 命令实现 (init/load/todo/task/log/query/lint/status/…)
|
|
107
|
+
├── src/hosts.js 四宿主接入定义
|
|
108
|
+
├── src/install.js 安装/卸载向导(分区共存合并)
|
|
109
|
+
├── hooks/event.sh hook 模板(纯触发 → 技术日志 + Stop wrapup 提醒)
|
|
110
|
+
├── skill/SKILL.md 技能(安装到各智能体)
|
|
111
|
+
└── test/ 单测(node:test,零外部测试依赖)
|
|
112
|
+
```
|
package/bin/abs.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// bin/abs.js — abs CLI 入口。
|
|
3
|
+
// abs <cmd> [args]
|
|
4
|
+
// 命令: init / board / status / load / task / install / uninstall / help
|
|
5
|
+
import { cmdInit, cmdBoard, cmdStatus, cmdLoad, cmdTask, cmdLog, cmdQuery, cmdLint, cmdNote, cmdShow, cmdRepair, cmdWrapup } from '../src/store.js';
|
|
6
|
+
import { runInstall, runUninstall, installSummary } from '../src/install.js';
|
|
7
|
+
|
|
8
|
+
const [,, cmd, ...rest] = process.argv;
|
|
9
|
+
|
|
10
|
+
function parseArgv(args) {
|
|
11
|
+
const o = { _: [] };
|
|
12
|
+
for (let i = 0; i < args.length; i++) {
|
|
13
|
+
const a = args[i];
|
|
14
|
+
if (a === '--dir') { o.dir = args[++i]; }
|
|
15
|
+
else if (a === '--agent') { o.agent = args[++i]; }
|
|
16
|
+
else if (a === '--id') { o.id = args[++i]; }
|
|
17
|
+
else if (a === '--section') { o.section = args[++i]; }
|
|
18
|
+
else if (a === '--note') { o.note = args[++i]; }
|
|
19
|
+
else if (a === '--yes') { o.yes = true; }
|
|
20
|
+
else if (a === '--repair') { o.repair = true; }
|
|
21
|
+
else if (a === '--no-mcp') { o.mcp = false; }
|
|
22
|
+
else if (a === '--no-skill') { o.skill = false; }
|
|
23
|
+
else if (a.startsWith('--')) { o[a.slice(2)] = true; }
|
|
24
|
+
else o._.push(a);
|
|
25
|
+
}
|
|
26
|
+
return o;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const usage = `abs — agent-brain-sync 记忆工具
|
|
30
|
+
|
|
31
|
+
用法:
|
|
32
|
+
abs init [--repair] 建 .brain/ 图谱;结构不完整时报明细,--repair 只补缺不覆盖
|
|
33
|
+
abs load 开机读状态 (index/todo/log)
|
|
34
|
+
abs todo 查看任务看板 (board 的正名)
|
|
35
|
+
abs index 查看图谱索引 index.md
|
|
36
|
+
abs log 无参查看流水 log.md;带参追加一行: abs log "标题" (仅用户/AI 主动记, hook 事件走 ~/.abs/log/hooks.log)
|
|
37
|
+
abs status 显示当前项目 + 图谱概要
|
|
38
|
+
abs task start <id> [--note ..] [--section Today / In Progress] 登记任务
|
|
39
|
+
abs task note <id> --note "断点/进度" 实时落 ↳ 断点 行
|
|
40
|
+
abs task blocked <id> --note "卡点原因" 移入 Blocked 区
|
|
41
|
+
abs task done <id> 完成并归位 Done
|
|
42
|
+
abs note "经验一句话" [--tags 坑,docker] 经验实时暂存 → sources/
|
|
43
|
+
abs query <词1> [词2 …] 检索 .brain/ 知识页 (多词 OR)
|
|
44
|
+
abs lint 图谱体检 (死链/孤岛/超尺寸/堆积)
|
|
45
|
+
abs wrapup 快照当前未完成任务到 ~/.abs/log/wrapup.log (收尾保险)
|
|
46
|
+
abs install [--agent <claude-code|opencode|codex|pi>] 安装 MCP+hook+skill
|
|
47
|
+
abs uninstall [--agent <...>] 卸载
|
|
48
|
+
abs help 本帮助
|
|
49
|
+
`;
|
|
50
|
+
|
|
51
|
+
async function main() {
|
|
52
|
+
const opts = parseArgv(rest);
|
|
53
|
+
try {
|
|
54
|
+
switch (cmd) {
|
|
55
|
+
case 'init': {
|
|
56
|
+
const msg = opts.repair
|
|
57
|
+
? await cmdRepair({ dir: opts.dir })
|
|
58
|
+
: await cmdInit({ dir: opts.dir });
|
|
59
|
+
console.log(msg);
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
case 'load': console.log(await cmdLoad({ dir: opts.dir })); break;
|
|
63
|
+
case 'board': console.log(await cmdShow({ dir: opts.dir, view: 'todo' })); break;
|
|
64
|
+
case 'todo': console.log(await cmdShow({ dir: opts.dir, view: 'todo' })); break;
|
|
65
|
+
case 'index': console.log(await cmdShow({ dir: opts.dir, view: 'index' })); break;
|
|
66
|
+
case 'log': {
|
|
67
|
+
// 无参=查看 log.md;带参=追加一行 (用户/AI 主动记; hook 生命周期事件走技术日志 hooks.log, 不经这里)
|
|
68
|
+
if (opts._.length) {
|
|
69
|
+
console.log(await cmdLog({ dir: opts.dir, title: opts._.join(' ') }));
|
|
70
|
+
} else {
|
|
71
|
+
console.log(await cmdShow({ dir: opts.dir, view: 'log' }));
|
|
72
|
+
}
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
case 'status': console.log(await cmdStatus({ dir: opts.dir })); break;
|
|
76
|
+
case 'task': {
|
|
77
|
+
const [action, id] = opts._;
|
|
78
|
+
if (!action || !['start', 'done', 'blocked', 'note'].includes(action)) {
|
|
79
|
+
throw new Error('abs task <start|done|blocked|note> <id>');
|
|
80
|
+
}
|
|
81
|
+
console.log(await cmdTask({ dir: opts.dir, action, id, section: opts.section, note: opts.note }));
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
case 'note': {
|
|
85
|
+
console.log(await cmdNote({ dir: opts.dir, text: opts._.join(' '), tags: opts.tags }));
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
case 'install': await runInstall({ agent: opts.agent, mcp: opts.mcp !== false, skill: opts.skill !== false, yes: opts.yes }); break;
|
|
89
|
+
case 'uninstall': await runUninstall({ agent: opts.agent, yes: opts.yes }); break;
|
|
90
|
+
case 'agents': console.log(installSummary()); break;
|
|
91
|
+
case 'query': {
|
|
92
|
+
console.log(await cmdQuery({ dir: opts.dir, terms: opts._ }));
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
case 'lint': {
|
|
96
|
+
console.log(await cmdLint({ dir: opts.dir }));
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
case 'wrapup': {
|
|
100
|
+
console.log(await cmdWrapup({ dir: opts.dir }));
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
case 'help': case undefined: case '--help': console.log(usage); break;
|
|
104
|
+
default: throw new Error(`未知命令: ${cmd}\n\n${usage}`);
|
|
105
|
+
}
|
|
106
|
+
} catch (e) {
|
|
107
|
+
console.error(String(e && e.message ? e.message : e));
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
main();
|
package/bin/mcp.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// bin/mcp.js — abs MCP server (stdio)。
|
|
3
|
+
// 职责只有两个(PLAN.md v2 定稿):
|
|
4
|
+
// ① resolve_project: 用代码写死逻辑定位当前项目目录(从 cwd 向上找最近 .brain/)
|
|
5
|
+
// ② 转接调用 CLI 读写(todo/board/query 的具体实现复用 src/)
|
|
6
|
+
// 无状态:每次调用从入参 cwd 重新解析,多项目各归各位。
|
|
7
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
8
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
import { appendFile, mkdir } from 'node:fs/promises';
|
|
11
|
+
import { homedir } from 'node:os';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { findBrainRoot } from '../src/index.js';
|
|
14
|
+
import { cmdBoard, cmdLoad, cmdStatus, cmdTask, cmdQuery, cmdLint, cmdNote, cmdWrapup } from '../src/store.js';
|
|
15
|
+
|
|
16
|
+
// ---------- 技术日志: MCP 请求跟踪(调试用, 与图谱 log.md 完全分开) ----------
|
|
17
|
+
// 落 ~/.abs/log/mcp.log: 每次工具调用一行 [时间] tool cwd 参数摘要 → 耗时/结果摘要。
|
|
18
|
+
// 关闭: ABS_LOG=0。stderr 不用(stdio transport 会污染协议)。
|
|
19
|
+
const MCP_LOG = process.env.ABS_LOG !== '0';
|
|
20
|
+
const LOG_DIR = process.env.ABS_LOG_DIR || join(homedir(), '.abs', 'log');
|
|
21
|
+
async function tlog(line) {
|
|
22
|
+
if (!MCP_LOG) return;
|
|
23
|
+
try {
|
|
24
|
+
await mkdir(LOG_DIR, { recursive: true });
|
|
25
|
+
await appendFile(join(LOG_DIR, 'mcp.log'), `[${new Date().toISOString()}] ${line}\n`);
|
|
26
|
+
} catch { /* 日志失败不影响服务 */ }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// 包一层: 记录每个工具调用的入参摘要与耗时
|
|
30
|
+
function withTrace(name, handler) {
|
|
31
|
+
return async (args) => {
|
|
32
|
+
const t0 = Date.now();
|
|
33
|
+
const argBrief = JSON.stringify(args || {}).slice(0, 200);
|
|
34
|
+
try {
|
|
35
|
+
const r = await handler(args);
|
|
36
|
+
const text = r?.content?.[0]?.text || '';
|
|
37
|
+
const brief = text.replace(/\n/g, '⏎').slice(0, 120);
|
|
38
|
+
await tlog(`tool=${name} cwd=${args?.cwd || '-'} args=${argBrief} → ${r?.isError ? 'ERR' : 'OK'} ${Date.now() - t0}ms | ${brief}`);
|
|
39
|
+
return r;
|
|
40
|
+
} catch (e) {
|
|
41
|
+
await tlog(`tool=${name} cwd=${args?.cwd || '-'} args=${argBrief} → THROW ${Date.now() - t0}ms | ${String(e?.message || e).slice(0, 200)}`);
|
|
42
|
+
throw e;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const server = new McpServer({
|
|
48
|
+
name: 'abs',
|
|
49
|
+
version: '0.1.0',
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// 工具面(narrow on purpose — 只暴露读/查/记状态,不做深提炼)
|
|
53
|
+
server.tool(
|
|
54
|
+
'abs_resolve_project',
|
|
55
|
+
'定位当前项目的 .brain 图谱根(从给定目录向上找最近 .brain/)。多项目隔离的唯一入口。',
|
|
56
|
+
{ cwd: z.string().describe('当前工作目录,默认进程 cwd') },
|
|
57
|
+
async ({ cwd }) => {
|
|
58
|
+
const root = await findBrainRoot(cwd || process.cwd());
|
|
59
|
+
if (!root) {
|
|
60
|
+
return { content: [{ type: 'text', text: `未找到 .brain/(从 ${cwd} 向上无果)。先在该项目运行: abs init` }] };
|
|
61
|
+
}
|
|
62
|
+
return { content: [{ type: 'text', text: root }] };
|
|
63
|
+
}
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
server.tool(
|
|
67
|
+
'abs_board',
|
|
68
|
+
'读取当前项目 todo 看板(含断点/Next-Step)。先 resolve 得到项目,再传其根目录。',
|
|
69
|
+
{ cwd: z.string().describe('项目内任意目录') },
|
|
70
|
+
async ({ cwd }) => {
|
|
71
|
+
const root = await findBrainRoot(cwd || process.cwd());
|
|
72
|
+
if (!root) return err('未找到 .brain/,先 abs init');
|
|
73
|
+
return { content: [{ type: 'text', text: await cmdBoard({ dir: root }) }] };
|
|
74
|
+
}
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
server.tool(
|
|
78
|
+
'abs_load',
|
|
79
|
+
'开机读状态:index 路线 + todo 看板 + 最近 log。跨会话续接的入口。',
|
|
80
|
+
{ cwd: z.string().describe('项目内任意目录') },
|
|
81
|
+
async ({ cwd }) => {
|
|
82
|
+
const root = await findBrainRoot(cwd || process.cwd());
|
|
83
|
+
if (!root) return err('未找到 .brain/,先 abs init');
|
|
84
|
+
return { content: [{ type: 'text', text: await cmdLoad({ dir: root }) }] };
|
|
85
|
+
}
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
server.tool(
|
|
89
|
+
'abs_task',
|
|
90
|
+
'任务实时落盘(幂等键 = id)。start 登记 / note 补断点(改到哪个文件哪行) / blocked 碰壁 / done 完成归位。',
|
|
91
|
+
{
|
|
92
|
+
action: z.enum(['start', 'done', 'note', 'blocked']),
|
|
93
|
+
id: z.string().describe('任务幂等键,如 TASK-xxx 或子任务名'),
|
|
94
|
+
cwd: z.string().describe('项目内任意目录'),
|
|
95
|
+
note: z.string().optional().describe('start=做什么; note=断点(文件/到哪步); blocked=卡点原因'),
|
|
96
|
+
},
|
|
97
|
+
async ({ action, id, cwd, note }) => {
|
|
98
|
+
const root = await findBrainRoot(cwd || process.cwd());
|
|
99
|
+
if (!root) return err('未找到 .brain/,先 abs init');
|
|
100
|
+
return { content: [{ type: 'text', text: await cmdTask({ dir: root, action, id, note }) }] };
|
|
101
|
+
}
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
server.tool(
|
|
105
|
+
'abs_status',
|
|
106
|
+
'当前项目 + 图谱概要(各类页数)。',
|
|
107
|
+
{ cwd: z.string().describe('项目内任意目录') },
|
|
108
|
+
async ({ cwd }) => {
|
|
109
|
+
const root = await findBrainRoot(cwd || process.cwd());
|
|
110
|
+
if (!root) return err('未找到 .brain/,先 abs init');
|
|
111
|
+
return { content: [{ type: 'text', text: await cmdStatus({ dir: root }) }] };
|
|
112
|
+
}
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
server.tool(
|
|
116
|
+
'abs_query',
|
|
117
|
+
'检索当前项目 .brain/ 知识页(多词 OR):以前踩过什么坑、哪页记了 X。',
|
|
118
|
+
{ cwd: z.string().describe('项目内任意目录'), terms: z.array(z.string()).min(1).describe('检索词') },
|
|
119
|
+
async ({ cwd, terms }) => {
|
|
120
|
+
const root = await findBrainRoot(cwd || process.cwd());
|
|
121
|
+
if (!root) return err('未找到 .brain/,先 abs init');
|
|
122
|
+
return { content: [{ type: 'text', text: await cmdQuery({ dir: root, terms }) }] };
|
|
123
|
+
}
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
server.tool(
|
|
127
|
+
'abs_lint',
|
|
128
|
+
'图谱体检:死链/孤岛/缺 frontmatter/超尺寸/sources 堆积/index 漏列。',
|
|
129
|
+
{ cwd: z.string().describe('项目内任意目录') },
|
|
130
|
+
async ({ cwd }) => {
|
|
131
|
+
const root = await findBrainRoot(cwd || process.cwd());
|
|
132
|
+
if (!root) return err('未找到 .brain/,先 abs init');
|
|
133
|
+
return { content: [{ type: 'text', text: await cmdLint({ dir: root }) }] };
|
|
134
|
+
}
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
server.tool(
|
|
138
|
+
'abs_note',
|
|
139
|
+
'经验实时暂存:把刚踩的坑/技巧/结论一句话落进 sources/(防 context 断了流失)。Teardown 时再提炼进 concepts/。',
|
|
140
|
+
{
|
|
141
|
+
cwd: z.string().describe('项目内任意目录'),
|
|
142
|
+
text: z.string().min(1).describe('经验/坑/技巧一句话'),
|
|
143
|
+
tags: z.string().optional().describe('逗号分隔标签,如 "docker,坑"'),
|
|
144
|
+
},
|
|
145
|
+
async ({ cwd, text, tags }) => {
|
|
146
|
+
const root = await findBrainRoot(cwd || process.cwd());
|
|
147
|
+
if (!root) return err('未找到 .brain/,先 abs init');
|
|
148
|
+
return { content: [{ type: 'text', text: await cmdNote({ dir: root, text, tags }) }] };
|
|
149
|
+
}
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
server.tool(
|
|
153
|
+
'abs_wrapup',
|
|
154
|
+
'收尾保险:把当前 todo 未完成任务快照到 ~/.abs/log/wrapup.log(下会话 load 时展示滞留对账)。',
|
|
155
|
+
{ cwd: z.string().describe('项目内任意目录') },
|
|
156
|
+
async ({ cwd }) => {
|
|
157
|
+
const root = await findBrainRoot(cwd || process.cwd());
|
|
158
|
+
if (!root) return err('未找到 .brain/,先 abs init');
|
|
159
|
+
return { content: [{ type: 'text', text: await cmdWrapup({ dir: root }) }] };
|
|
160
|
+
}
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
function err(msg) {
|
|
164
|
+
return { content: [{ type: 'text', text: msg }], isError: true };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
await server.connect(new StdioServerTransport());
|
package/hooks/event.sh
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# abs hook — 纯触发层。宿主生命周期事件 → 技术日志一行(~/.abs/log/hooks.log)。
|
|
3
|
+
# 纪律:
|
|
4
|
+
# fire-and-forget: 绝不阻塞 agent 热路径; 失败留痕不中断;
|
|
5
|
+
# 事件只进技术日志, 不进图谱 log.md (log.md 是活动流水: 任务/沉淀, 不收琐碎请求);
|
|
6
|
+
# 不写 todo (todo 由 agent/用户经 MCP/CLI 主动登记)。
|
|
7
|
+
# Stop 额外在 wrapup.log 留一行「疑似未收尾」提醒(低噪声、独立文件)——仍只进技术日志,
|
|
8
|
+
# 不碰图谱; 由 agent/用户下会话自觉跑收尾循环, hook 不替 agent 判断什么该沉淀。
|
|
9
|
+
# 用法: 由宿主 hook 配置以 stdin JSON 调起; __ABS_BIN__ 安装时替换为 abs CLI 绝对路径,
|
|
10
|
+
# __NODE_BIN__ 替换为 node 绝对路径 (经 node 调起, 不依赖 exec 位)。
|
|
11
|
+
set +e
|
|
12
|
+
ABS_BIN="__ABS_BIN__"
|
|
13
|
+
NODE_BIN="__NODE_BIN__"
|
|
14
|
+
EVENT="__EVENT__"
|
|
15
|
+
|
|
16
|
+
PAYLOAD=$(cat 2>/dev/null | head -c 500)
|
|
17
|
+
|
|
18
|
+
# 后台执行,hook 立即返回——宿主热路径零阻塞。
|
|
19
|
+
(
|
|
20
|
+
# 幂等: 同一 payload 指纹在 60s 内只落一行 (防重复触发)。
|
|
21
|
+
# mark 目录可经 ABS_MARK_DIR 覆盖(默认 /tmp)——测试注入沙盒目录隔离, 避免与真实/并发残留互扰。
|
|
22
|
+
FINGER=$(printf '%s' "$PAYLOAD" | cksum | cut -d' ' -f1)
|
|
23
|
+
STAMP=$(date +%Y%m%d%H%M)
|
|
24
|
+
MARK_DIR="${ABS_MARK_DIR:-/tmp}"
|
|
25
|
+
mkdir -p "$MARK_DIR" 2>/dev/null
|
|
26
|
+
MARK="$MARK_DIR/abs-hook-${FINGER}-${STAMP}.mark"
|
|
27
|
+
[ -e "$MARK" ] && exit 0
|
|
28
|
+
: > "$MARK" 2>/dev/null
|
|
29
|
+
|
|
30
|
+
# 定位项目由 CLI 完成 (向上找 .brain/, 代码写死); 找不到图谱则静默放弃。
|
|
31
|
+
# 纪律: hook 事件只进技术日志 (~/.abs/log/), 不进图谱 log.md (那是活动流水, 不收琐碎请求)。
|
|
32
|
+
# 用 awk 截前 120 字符 (#/bin/sh 兼容: 无 ${var:0:n} bash 扩展)。
|
|
33
|
+
# 经 node 调起 (bin 文件可能无 exec 位); 失败留痕到 /tmp 便于排查 (不静默到底)。
|
|
34
|
+
SHORT=$(printf '%s' "$PAYLOAD" | awk '{print substr($0, 1, 120)}')
|
|
35
|
+
# 取 session_id 供 wrapup 提醒标注 (payload 截 500 内、id 靠前; 取不到则留空不中断)。
|
|
36
|
+
SESSION_ID=$(printf '%s' "$PAYLOAD" | sed -n 's/.*"session_id":"\([^"]*\)".*/\1/p' | head -1)
|
|
37
|
+
LOG_DIR="${ABS_LOG_DIR:-$HOME/.abs/log}"
|
|
38
|
+
mkdir -p "$LOG_DIR" 2>/dev/null
|
|
39
|
+
{
|
|
40
|
+
printf '[%s] %s %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$EVENT" "$SHORT"
|
|
41
|
+
} >> "$LOG_DIR/hooks.log" 2>/dev/null || true
|
|
42
|
+
|
|
43
|
+
# Stop 专用: 低噪声 wrap-up 提醒 (独立文件, 不进 per-event hooks.log, 不碰图谱 todo/log.md)。
|
|
44
|
+
# 纯机械信号——"一个会话结束了"; 是否真有滞留任务/经验由 agent 自觉判断, hook 不替做。
|
|
45
|
+
if [ "$EVENT" = "Stop" ]; then
|
|
46
|
+
{
|
|
47
|
+
printf '[%s] wrapup-remind 会话已结束(%s): 若 Today 有滞留任务或经验未沉淀, 下会话开头走收尾循环\n' \
|
|
48
|
+
"$(date '+%Y-%m-%d %H:%M:%S')" "$SESSION_ID"
|
|
49
|
+
} >> "$LOG_DIR/wrapup.log" 2>/dev/null || true
|
|
50
|
+
# 机械快照滞留清单: abs wrapup 把当前项目未完成任务+断点追加到 wrapup.log (跨会话收尾保险)。
|
|
51
|
+
# abs wrapup 已自行写 wrapup.log (honor ABS_LOG_DIR); 这里 stdout 只进 hooks.log 作痕迹, 避免污染 wrapup.log。
|
|
52
|
+
"$NODE_BIN" "$ABS_BIN" wrapup >>"$LOG_DIR/hooks.log" 2>/dev/null || true
|
|
53
|
+
fi
|
|
54
|
+
) &
|
|
55
|
+
|
|
56
|
+
# Claude Code 等宿主要求 stdout 以 { 开头才不告警。
|
|
57
|
+
printf '{}\n'
|
|
58
|
+
exit 0
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fanchao8609/agent_brain_sync",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "agent-brain-sync: 跨会话 AI 编码记忆 — hook 纯触发 + CLI/MCP 读写 .brain markdown 图谱, 防并发写保护。",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"bin": {
|
|
8
|
+
"abs": "bin/abs.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin/",
|
|
12
|
+
"src/",
|
|
13
|
+
"hooks/",
|
|
14
|
+
"skill/",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"keywords": [
|
|
18
|
+
"memory", "agent", "brain", "markdown", "cross-session", "claude", "mcp", "knowledge-graph", "obsidian"
|
|
19
|
+
],
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=18"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@modelcontextprotocol/sdk": "^1.30.0"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"abs": "node bin/abs.js",
|
|
28
|
+
"mcp": "node bin/mcp.js",
|
|
29
|
+
"test": "node --test 'test/*.test.js'",
|
|
30
|
+
"prepack": "node -e \"require('fs').chmodSync('bin/abs.js',0o755);require('fs').chmodSync('bin/mcp.js',0o755)\"",
|
|
31
|
+
"pack:check": "npm pack --dry-run"
|
|
32
|
+
}
|
|
33
|
+
}
|