@fanchao8609/agent_brain_sync 0.2.0 → 0.3.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/package.json +1 -1
- package/src/install.js +83 -13
- package/src/store.js +13 -0
package/package.json
CHANGED
package/src/install.js
CHANGED
|
@@ -241,31 +241,101 @@ async function uninstallCodex() {
|
|
|
241
241
|
|
|
242
242
|
// ============================ Opencode / Pi (TS 插件) ============================
|
|
243
243
|
function opencodePluginSource() {
|
|
244
|
-
//
|
|
244
|
+
// Opencode 官方插件 API (据 @opencode-ai/plugin 类型定义实核):
|
|
245
|
+
// export const Plugin: Plugin = async ({ client, directory }) => ({ event: async ({ event }) => {...} })
|
|
246
|
+
// 坑: event 回调入参是 { event }, 事件名在 event.type —— 曾错写成 ({ name }) 导致 name 恒 undefined,
|
|
247
|
+
// 插件从未触发过(0 条日志), 静默失效。
|
|
248
|
+
// 收尾注入: session.idle (= 每轮结束) 经 client.session.promptAsync 注入收尾指令,
|
|
249
|
+
// 与 pi 的 agent_end 同策略(真改过文件 + .brain 存在 + log.md 今日无记录, 每会话一次)。
|
|
245
250
|
return `/**
|
|
246
|
-
* abs (agent-brain-sync) —
|
|
251
|
+
* abs (agent-brain-sync) — Opencode plugin。
|
|
247
252
|
* 纯触发: 生命周期事件 → 技术日志一行 (~/.abs/log/hooks.log, ABS_LOG_DIR 可覆盖)。fire-and-forget。
|
|
248
253
|
* 纪律: hook 事件只进技术日志, 不进图谱 log.md (log.md 只收工作成果沉淀, 与 event.sh 同纪律)。
|
|
249
|
-
*
|
|
254
|
+
* Opencode 事件: session.created / session.idle / session.deleted (idle = 每轮结束, 对应 pi 的 agent_end)。
|
|
255
|
+
* 收尾注入: session.idle 且本会话真改过文件 + .brain 今日无记录 → promptAsync 推 agent 走收尾循环。
|
|
250
256
|
*/
|
|
251
|
-
import { appendFile, mkdir } from "node:fs/promises"
|
|
257
|
+
import { appendFile, mkdir, readFile, stat } from "node:fs/promises"
|
|
252
258
|
import { homedir } from "node:os"
|
|
253
|
-
import { join } from "node:path"
|
|
259
|
+
import { join, dirname } from "node:path"
|
|
254
260
|
|
|
255
|
-
export const AbsPlugin = async () =>
|
|
256
|
-
|
|
257
|
-
|
|
261
|
+
export const AbsPlugin = async ({ client, directory }) => {
|
|
262
|
+
// 本会话是否真改过文件 (由 tool.execute.after 观测)
|
|
263
|
+
let wroteFiles = false
|
|
264
|
+
let nudged = false
|
|
265
|
+
|
|
266
|
+
async function logHook(evt) {
|
|
258
267
|
try {
|
|
259
268
|
const dir = process.env.ABS_LOG_DIR || join(homedir(), ".abs", "log")
|
|
260
269
|
const d = new Date()
|
|
261
|
-
const pad = (n
|
|
270
|
+
const pad = (n) => String(n).padStart(2, "0")
|
|
262
271
|
const stamp = d.getFullYear() + "-" + pad(d.getMonth() + 1) + "-" + pad(d.getDate()) + " " + pad(d.getHours()) + ":" + pad(d.getMinutes()) + ":" + pad(d.getSeconds())
|
|
263
|
-
const line = "[" + stamp + "] opencode:" + name + "\\n"
|
|
264
272
|
await mkdir(dir, { recursive: true })
|
|
265
|
-
await appendFile(join(dir, "hooks.log"),
|
|
273
|
+
await appendFile(join(dir, "hooks.log"), "[" + stamp + "] opencode:" + evt + "\\n")
|
|
266
274
|
} catch {} // fire-and-forget: 永不阻塞宿主
|
|
267
|
-
}
|
|
268
|
-
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// 从 cwd 向上找最近含 .brain/ 的祖先目录
|
|
278
|
+
async function findBrain(cwd) {
|
|
279
|
+
let d = cwd || process.cwd()
|
|
280
|
+
for (;;) {
|
|
281
|
+
try { const st = await stat(join(d, ".brain")); if (st.isDirectory()) return join(d, ".brain") } catch {}
|
|
282
|
+
const up = dirname(d)
|
|
283
|
+
if (up === d) return null
|
|
284
|
+
d = up
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async function loggedToday(brain) {
|
|
289
|
+
try {
|
|
290
|
+
const txt = await readFile(join(brain, "log.md"), "utf8")
|
|
291
|
+
const d = new Date()
|
|
292
|
+
const pad = (n) => String(n).padStart(2, "0")
|
|
293
|
+
return txt.includes(d.getFullYear() + "-" + pad(d.getMonth() + 1) + "-" + pad(d.getDate()))
|
|
294
|
+
} catch { return false }
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const TEARDOWN_MSG =
|
|
298
|
+
"[abs 收尾提醒] 本会话改过文件但 .brain/ 今天还没有记录。请立即走收尾循环:\\n" +
|
|
299
|
+
"1) 跑 abs load 看 Today 还有哪些未完成;\\n" +
|
|
300
|
+
"2) 实际做完漏登记的 abs task done <id>,做到一半的 abs task note <id> --note \\"断点\\";\\n" +
|
|
301
|
+
"3) 值得留的经验 abs note \\"...\\"(宁少勿滥,能从代码 grep 到的不记);\\n" +
|
|
302
|
+
"4) abs log \\"完成 X:...\\" 记一行工作成果,新页同步进 index。\\n" +
|
|
303
|
+
"简洁执行,不要复述本条提醒。若本次确实没有可沉淀产出,直接回一句\\"无可沉淀\\"即可。"
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
// 观测真实写操作: write/edit/patch 类工具成功即标记
|
|
307
|
+
"tool.execute.after": async ({ tool }) => {
|
|
308
|
+
const t = String(tool || "").toLowerCase()
|
|
309
|
+
if (["write", "edit", "patch", "multiedit", "apply_patch"].includes(t)) wroteFiles = true
|
|
310
|
+
},
|
|
311
|
+
|
|
312
|
+
event: async ({ event }) => {
|
|
313
|
+
const type = event && event.type
|
|
314
|
+
if (!type) return
|
|
315
|
+
if (type === "session.created" || type === "session.deleted") {
|
|
316
|
+
await logHook(type)
|
|
317
|
+
return
|
|
318
|
+
}
|
|
319
|
+
if (type !== "session.idle") return // idle = 每轮结束, 不记日志(太吵), 只做收尾判定
|
|
320
|
+
if (nudged || !wroteFiles) return
|
|
321
|
+
const sessionID = event.properties && event.properties.sessionID
|
|
322
|
+
if (!sessionID) return
|
|
323
|
+
const cwd = directory || process.cwd()
|
|
324
|
+
const brain = await findBrain(cwd)
|
|
325
|
+
if (!brain) return // 无图谱=不在这项目沉淀, 不打扰
|
|
326
|
+
if (await loggedToday(brain)) return // 今天已收尾过
|
|
327
|
+
nudged = true
|
|
328
|
+
await logHook("session.idle:teardown-nudge")
|
|
329
|
+
try {
|
|
330
|
+
await client.session.promptAsync({
|
|
331
|
+
path: { id: sessionID },
|
|
332
|
+
query: { directory: cwd },
|
|
333
|
+
body: { parts: [{ type: "text", text: TEARDOWN_MSG }] },
|
|
334
|
+
})
|
|
335
|
+
} catch {} // 注入失败不阻塞宿主
|
|
336
|
+
},
|
|
337
|
+
}
|
|
338
|
+
}
|
|
269
339
|
${MARK}
|
|
270
340
|
`;
|
|
271
341
|
}
|
package/src/store.js
CHANGED
|
@@ -422,6 +422,12 @@ export async function cmdLint({ dir }) {
|
|
|
422
422
|
const pages = await listPages(vault);
|
|
423
423
|
const names = new Set(pages.map((p) => p.slug));
|
|
424
424
|
const linkedNames = new Set(pages.flatMap((p) => p.links));
|
|
425
|
+
// index.md 里列的 [[x]] —— 用于反向查死引用(列了但页不存在)
|
|
426
|
+
let indexLinks = [];
|
|
427
|
+
try {
|
|
428
|
+
const idx = await fs.readFile(join(vault, 'index.md'), 'utf8');
|
|
429
|
+
indexLinks = [...idx.matchAll(/\[\[([^\]|#]+)/g)].map((m) => m[1].trim());
|
|
430
|
+
} catch { /* 无 index 则不查 */ }
|
|
425
431
|
const issues = [];
|
|
426
432
|
|
|
427
433
|
for (const pg of pages) {
|
|
@@ -447,6 +453,13 @@ export async function cmdLint({ dir }) {
|
|
|
447
453
|
}
|
|
448
454
|
}
|
|
449
455
|
|
|
456
|
+
// index.md 反向检查: 列了 [[x]] 但 x 页不存在 —— 删页/归档 source 后忘了清 index 的残留。
|
|
457
|
+
// (page→index 的 INDEX-MISSING 已有, 这里补 index→page, 否则死引用静默留在入口文件里)
|
|
458
|
+
for (const ln of indexLinks) {
|
|
459
|
+
if (/slug|Name|name|Date|页面名$/.test(ln)) continue; // 模板占位行
|
|
460
|
+
if (!names.has(ln)) issues.push(`INDEX-DEAD-LINK: index.md -> [[${ln}]] (该页不存在, 删页后忘清 index?)`);
|
|
461
|
+
}
|
|
462
|
+
|
|
450
463
|
const nsrc = pages.filter((p) => p.dir === 'sources').length;
|
|
451
464
|
if (nsrc > 10) issues.push(`SOURCES-PILED-UP: sources/ has ${nsrc} files > 10; 提炼归档旧 source`);
|
|
452
465
|
|