@mingxy/cerebro-claude-code 0.3.9 → 0.3.11
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/.claude-plugin/plugin.json +1 -1
- package/hooks/common.mjs +46 -17
- package/hooks/session-end.mjs +3 -3
- package/hooks/session-start.mjs +14 -20
- package/package.json +1 -1
- package/scripts/web-server.mjs +41 -2
- package/web/assets/index-BITUpTtU.js +165 -0
- package/web/index.html +1 -1
package/hooks/common.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Ported from common.sh — no bash/curl/python3 dependency. Pure Node.
|
|
3
3
|
// Config cascade: env > ~/.config/cerebro/config.json > builtin defaults
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
5
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync, appendFileSync, unlinkSync, copyFileSync } from "node:fs";
|
|
5
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, appendFileSync, unlinkSync, copyFileSync, readdirSync } from "node:fs";
|
|
6
6
|
import { join, dirname, resolve } from "node:path";
|
|
7
7
|
import { execSync } from "node:child_process";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
@@ -33,6 +33,7 @@ const DEF = {
|
|
|
33
33
|
logEnabled: true,
|
|
34
34
|
profileTimeoutMs: 2000,
|
|
35
35
|
recentTimeoutMs: 6000,
|
|
36
|
+
webGraceMs: 60000,
|
|
36
37
|
};
|
|
37
38
|
|
|
38
39
|
// ─── Config cascade ──────────────────────────────────────────────────────────
|
|
@@ -62,6 +63,7 @@ function loadConfig() {
|
|
|
62
63
|
const i = cfg.injection || {};
|
|
63
64
|
const ct = cfg.content || {};
|
|
64
65
|
const lg = cfg.logging || {};
|
|
66
|
+
const w = cfg.web || {};
|
|
65
67
|
|
|
66
68
|
const num = (env, cfgVal, def) => {
|
|
67
69
|
const v = process.env[env];
|
|
@@ -87,6 +89,7 @@ function loadConfig() {
|
|
|
87
89
|
: DEF.logEnabled,
|
|
88
90
|
profileTimeoutMs: i.profileTimeoutMs || DEF.profileTimeoutMs,
|
|
89
91
|
recentTimeoutMs: i.recentTimeoutMs || DEF.recentTimeoutMs,
|
|
92
|
+
webGraceMs: num("OMEM_WEB_GRACE_MS", w.graceMs, DEF.webGraceMs),
|
|
90
93
|
};
|
|
91
94
|
}
|
|
92
95
|
|
|
@@ -220,31 +223,57 @@ function _log(level, msg) {
|
|
|
220
223
|
} catch {}
|
|
221
224
|
}
|
|
222
225
|
|
|
223
|
-
// ─── Web server
|
|
224
|
-
|
|
226
|
+
// ─── Web server liveness (truth = OS process table) ──────────────────────────
|
|
227
|
+
// Refcount bookkeeping leaked to 169 and never hit zero — counting events is
|
|
228
|
+
// unreliable (killed terminals never decrement). Instead: each CC session
|
|
229
|
+
// drops sessions/<sid>.live holding its CC main pid; anyone interested sweeps
|
|
230
|
+
// the dir with kill(pid, 0) — dead pids get swept, no ledger to rot.
|
|
231
|
+
const SESSIONS_DIR = join(HOME, ".config/cerebro/sessions");
|
|
225
232
|
const WEB_PID_FILE = join(HOME, ".config/cerebro/web-server.pid");
|
|
226
233
|
|
|
227
|
-
export function
|
|
234
|
+
export function writeLive(sid) {
|
|
235
|
+
if (!sid) return;
|
|
228
236
|
try {
|
|
229
|
-
|
|
230
|
-
writeFileSync(
|
|
237
|
+
mkdirSync(SESSIONS_DIR, { recursive: true });
|
|
238
|
+
writeFileSync(join(SESSIONS_DIR, `${sid}.live`), String(process.ppid));
|
|
231
239
|
} catch {}
|
|
232
240
|
}
|
|
233
241
|
|
|
234
|
-
export function
|
|
242
|
+
export function removeLive(sid) {
|
|
243
|
+
if (!sid) return;
|
|
244
|
+
try { unlinkSync(join(SESSIONS_DIR, `${sid}.live`)); } catch {}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function countLiveCC() {
|
|
248
|
+
let alive = 0;
|
|
235
249
|
try {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
250
|
+
for (const f of readdirSync(SESSIONS_DIR)) {
|
|
251
|
+
if (!f.endsWith(".live")) continue;
|
|
252
|
+
const p = join(SESSIONS_DIR, f);
|
|
239
253
|
try {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
} catch {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
254
|
+
process.kill(parseInt(readFileSync(p, "utf-8").trim(), 10), 0); // throws if dead
|
|
255
|
+
alive++;
|
|
256
|
+
} catch (e) {
|
|
257
|
+
// EPERM = process exists but is root's — still alive; only ESRCH = dead
|
|
258
|
+
if (e && e.code === "EPERM") alive++;
|
|
259
|
+
else { try { unlinkSync(p); } catch {} } // stale — sweep
|
|
260
|
+
}
|
|
247
261
|
}
|
|
262
|
+
} catch {} // dir missing = no session ever
|
|
263
|
+
return alive;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Kill a stale web-server daemon so a fresh one can bind (rebirth on every
|
|
267
|
+
// session start — no version sniffing). pid verified via /proc cmdline to
|
|
268
|
+
// survive pid reuse; non-Linux (no /proc) degrades to no-op.
|
|
269
|
+
export function killStaleWebServer() {
|
|
270
|
+
try {
|
|
271
|
+
const pid = parseInt(readFileSync(WEB_PID_FILE, "utf-8").trim(), 10);
|
|
272
|
+
if (!pid || pid === process.pid) return;
|
|
273
|
+
const cmdline = readFileSync(`/proc/${pid}/cmdline`, "utf-8");
|
|
274
|
+
if (!cmdline.includes("web-server.mjs")) return; // not ours — don't touch
|
|
275
|
+
process.kill(pid, "SIGTERM");
|
|
276
|
+
setTimeout(() => { try { process.kill(pid, "SIGKILL"); } catch {} }, 500).unref();
|
|
248
277
|
} catch {}
|
|
249
278
|
}
|
|
250
279
|
|
package/hooks/session-end.mjs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// Fix: spawn detached child process, parent exits immediately.
|
|
6
6
|
import { spawn } from "node:child_process";
|
|
7
7
|
import { join } from "node:path";
|
|
8
|
-
import { config, PLUGIN_ROOT,
|
|
8
|
+
import { config, PLUGIN_ROOT, removeLive, parseStdinJSON, emit, logDebug, writePendingClearFlush } from "./common.mjs";
|
|
9
9
|
|
|
10
10
|
if (!config.apiKey) { emit({}); process.exit(0); }
|
|
11
11
|
|
|
@@ -20,7 +20,7 @@ logDebug(`session-end: sid=${sid} reason=${reason} tp=${tp ? tp.slice(-40) : "EM
|
|
|
20
20
|
// flush to it so the result lands in that hook's toast (detached flush is mute).
|
|
21
21
|
if (reason === "clear" && tp && sid) {
|
|
22
22
|
writePendingClearFlush(tp, sid);
|
|
23
|
-
|
|
23
|
+
removeLive(sid);
|
|
24
24
|
emit({});
|
|
25
25
|
process.exit(0);
|
|
26
26
|
}
|
|
@@ -38,7 +38,7 @@ if (tp && sid) {
|
|
|
38
38
|
} catch {}
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
removeLive(sid);
|
|
42
42
|
emit({
|
|
43
43
|
systemMessage: `🧠 Cerebro · Session flush in background`,
|
|
44
44
|
});
|
package/hooks/session-start.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { spawn } from "node:child_process";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { readFileSync } from "node:fs";
|
|
6
6
|
import {
|
|
7
|
-
config, PLUGIN_ROOT, PLUGIN_VERSION, detectProjectPath, parseStdinJSON, emit, buildMemoryInjection, postRecallEvent,
|
|
7
|
+
config, PLUGIN_ROOT, PLUGIN_VERSION, detectProjectPath, parseStdinJSON, emit, buildMemoryInjection, postRecallEvent, writeLive, killStaleWebServer, readCompactResult, readPendingClearFlush, clearPendingClearFlush, flushSessionIngest, injectionConfig,
|
|
8
8
|
} from "./common.mjs";
|
|
9
9
|
import { judgeMaterial, readState, writeStateForReport, fetchOrphanResult } from "./dream.mjs";
|
|
10
10
|
|
|
@@ -61,24 +61,18 @@ const input = parseStdinJSON();
|
|
|
61
61
|
const sid = input.session_id || "";
|
|
62
62
|
const startSource = input.source || ""; // "startup" | "resume" | "clear" | "compact"
|
|
63
63
|
|
|
64
|
-
// ─── web server
|
|
65
|
-
|
|
64
|
+
// ─── web server 换血(杀旧起新,无条件——升级即生效,无版本探测分支)──────────
|
|
65
|
+
killStaleWebServer();
|
|
66
|
+
await new Promise((r) => setTimeout(r, 700)); // old daemon TERM→KILL, port frees
|
|
66
67
|
try {
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
[join(PLUGIN_ROOT, "scripts", "web-server.mjs")],
|
|
76
|
-
{ detached: true, stdio: "ignore", env: { ...process.env } },
|
|
77
|
-
);
|
|
78
|
-
child.unref();
|
|
79
|
-
} catch {}
|
|
80
|
-
}
|
|
81
|
-
refCountInc();
|
|
68
|
+
const child = spawn(
|
|
69
|
+
process.execPath,
|
|
70
|
+
[join(PLUGIN_ROOT, "scripts", "web-server.mjs")],
|
|
71
|
+
{ detached: true, stdio: "ignore", env: { ...process.env, OMEM_WEB_GRACE_MS: String(config.webGraceMs) } },
|
|
72
|
+
);
|
|
73
|
+
child.unref();
|
|
74
|
+
} catch {}
|
|
75
|
+
writeLive(sid);
|
|
82
76
|
|
|
83
77
|
// ─── API key 检查 ────────────────────────────────────────────────────────────
|
|
84
78
|
if (!config.apiKey) {
|
|
@@ -119,8 +113,8 @@ out = out.replace("[CEREBRO-MEMORY]", `[CEREBRO-MEMORY]\n${timeLine}`);
|
|
|
119
113
|
// CEREBRO-STATUS 通过 systemMessage 显示给用户(Q2: toast 替代方案)
|
|
120
114
|
const memCount = injection.projectMemoryCount + injection.searchCount;
|
|
121
115
|
let statusMsg = injection.recentFailed
|
|
122
|
-
? `🧠 Cerebro v${PLUGIN_VERSION} · Recent ✗ (timeout) · Profile ${injection.profileCount > 0 ? "✓" : "✗"}`
|
|
123
|
-
: `🧠 Cerebro v${PLUGIN_VERSION} · Connected · ${memCount} memories · Profile ${injection.profileCount > 0 ? "✓" : "✗"}`;
|
|
116
|
+
? `🧠 Cerebro v${PLUGIN_VERSION} · Recent ✗ (timeout) · Global ${injection.globalCount} · Profile ${injection.profileCount > 0 ? "✓" : "✗"}`
|
|
117
|
+
: `🧠 Cerebro v${PLUGIN_VERSION} · Connected · ${memCount} memories · Global ${injection.globalCount} · Profile ${injection.profileCount > 0 ? "✓" : "✗"}`;
|
|
124
118
|
|
|
125
119
|
// After compact, PostCompact toast gets overridden by SessionStart:compact toast.
|
|
126
120
|
// Merge PostCompact ingest result into this toast so user sees it.
|
package/package.json
CHANGED
package/scripts/web-server.mjs
CHANGED
|
@@ -128,5 +128,44 @@ server.listen(PORT, "127.0.0.1", () => {
|
|
|
128
128
|
console.log(`[cerebro web-server] serving ${WEB_DIR} at http://localhost:${PORT} (pid=${process.pid})`);
|
|
129
129
|
});
|
|
130
130
|
|
|
131
|
-
|
|
132
|
-
|
|
131
|
+
// ── CC-liveness watchdog ─────────────────────────────────────────────────────
|
|
132
|
+
// Truth = OS process table: sessions/<sid>.live maps each CC session to its
|
|
133
|
+
// CC main pid. Sweep on an interval, kill(pid, 0) each, sweep stale ones.
|
|
134
|
+
// Zero CC sessions alive → count down the grace window → self-exit. Survives
|
|
135
|
+
// killed terminals (no SessionEnd needed) — refcount bookkeeping is gone.
|
|
136
|
+
const SESSIONS_DIR = path.join(process.env.HOME || process.env.USERPROFILE || "", ".config/cerebro/sessions");
|
|
137
|
+
const GRACE_MS = parseInt(process.env.OMEM_WEB_GRACE_MS || "", 10) || 60_000;
|
|
138
|
+
const WATCH_MS = parseInt(process.env.OMEM_WEB_WATCH_MS || "", 10) || 60_000;
|
|
139
|
+
let zeroSince = 0; // 0 = CC alive (or unknown)
|
|
140
|
+
|
|
141
|
+
setInterval(() => {
|
|
142
|
+
let alive = 0;
|
|
143
|
+
try {
|
|
144
|
+
for (const f of fs.readdirSync(SESSIONS_DIR)) {
|
|
145
|
+
if (!f.endsWith(".live")) continue;
|
|
146
|
+
const fp = path.join(SESSIONS_DIR, f);
|
|
147
|
+
try {
|
|
148
|
+
process.kill(parseInt(fs.readFileSync(fp, "utf-8").trim(), 10), 0);
|
|
149
|
+
alive++;
|
|
150
|
+
} catch (e) {
|
|
151
|
+
if (e && e.code === "EPERM") alive++; // exists, root-owned — alive
|
|
152
|
+
else { try { fs.unlinkSync(fp); } catch {} }
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
} catch {} // dir missing = no CC session ever
|
|
156
|
+
if (alive > 0) { zeroSince = 0; return; }
|
|
157
|
+
if (!zeroSince) zeroSince = Date.now();
|
|
158
|
+
if (Date.now() - zeroSince >= GRACE_MS) {
|
|
159
|
+
console.log(`[cerebro web-server] no CC sessions alive for ${GRACE_MS / 1000}s, self-exiting`);
|
|
160
|
+
try { fs.unlinkSync(PID_FILE); } catch {}
|
|
161
|
+
process.exit(0);
|
|
162
|
+
}
|
|
163
|
+
}, WATCH_MS);
|
|
164
|
+
|
|
165
|
+
const shutdown = () => {
|
|
166
|
+
try { server.closeAllConnections(); } catch {} // Node ≥18.2; old node degrades
|
|
167
|
+
server.close(() => { try { unlinkSync(PID_FILE); } catch {} process.exit(0); });
|
|
168
|
+
setTimeout(() => process.exit(0), 1000).unref();
|
|
169
|
+
};
|
|
170
|
+
process.on("SIGTERM", shutdown);
|
|
171
|
+
process.on("SIGINT", shutdown);
|