@mingxy/cerebro-claude-code 0.3.11 → 0.3.13
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 +36 -8
- package/package.json +1 -1
- package/scripts/web-server.mjs +12 -2
package/hooks/common.mjs
CHANGED
|
@@ -231,11 +231,28 @@ function _log(level, msg) {
|
|
|
231
231
|
const SESSIONS_DIR = join(HOME, ".config/cerebro/sessions");
|
|
232
232
|
const WEB_PID_FILE = join(HOME, ".config/cerebro/web-server.pid");
|
|
233
233
|
|
|
234
|
+
// CC runs hooks through a bash wrapper — process.ppid is the wrapper, which
|
|
235
|
+
// dies the moment the hook exits. Climb past shells to the CC main process;
|
|
236
|
+
// non-Linux (no /proc) or unclimbable chain falls back to the old behavior.
|
|
237
|
+
function ccMainPid() {
|
|
238
|
+
let pid = process.ppid;
|
|
239
|
+
for (let i = 0; i < 4 && pid > 1; i++) {
|
|
240
|
+
try {
|
|
241
|
+
const comm = readFileSync(`/proc/${pid}/comm`, "utf-8").trim();
|
|
242
|
+
if (!/^(bash|sh|dash|zsh)$/.test(comm)) return pid;
|
|
243
|
+
const m = readFileSync(`/proc/${pid}/status`, "utf-8").match(/^PPid:\s+(\d+)/m);
|
|
244
|
+
if (!m) break;
|
|
245
|
+
pid = parseInt(m[1], 10);
|
|
246
|
+
} catch { break; }
|
|
247
|
+
}
|
|
248
|
+
return process.ppid;
|
|
249
|
+
}
|
|
250
|
+
|
|
234
251
|
export function writeLive(sid) {
|
|
235
252
|
if (!sid) return;
|
|
236
253
|
try {
|
|
237
254
|
mkdirSync(SESSIONS_DIR, { recursive: true });
|
|
238
|
-
writeFileSync(join(SESSIONS_DIR, `${sid}.live`), String(
|
|
255
|
+
writeFileSync(join(SESSIONS_DIR, `${sid}.live`), String(ccMainPid()));
|
|
239
256
|
} catch {}
|
|
240
257
|
}
|
|
241
258
|
|
|
@@ -266,15 +283,26 @@ export function countLiveCC() {
|
|
|
266
283
|
// Kill a stale web-server daemon so a fresh one can bind (rebirth on every
|
|
267
284
|
// session start — no version sniffing). pid verified via /proc cmdline to
|
|
268
285
|
// survive pid reuse; non-Linux (no /proc) degrades to no-op.
|
|
269
|
-
export function killStaleWebServer() {
|
|
286
|
+
export function killStaleWebServer(port = 5212) {
|
|
287
|
+
const pids = new Set();
|
|
288
|
+
// Path 1: port occupant via ss — catches zcode's web-server.js too (it never
|
|
289
|
+
// writes our PID_FILE; left alive it starves our spawn via EADDRINUSE and the
|
|
290
|
+
// port stays with a daemon that has no .live liveness model).
|
|
270
291
|
try {
|
|
271
|
-
const
|
|
272
|
-
|
|
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();
|
|
292
|
+
const out = execSync(`ss -ltnpH 'sport = :${port}'`, { timeout: 2000 }).toString();
|
|
293
|
+
for (const m of out.matchAll(/pid=(\d+)/g)) pids.add(parseInt(m[1], 10));
|
|
277
294
|
} catch {}
|
|
295
|
+
// Path 2: our own PID_FILE (the .mjs daemon writes it at listen-time).
|
|
296
|
+
try { pids.add(parseInt(readFileSync(WEB_PID_FILE, "utf-8").trim(), 10)); } catch {}
|
|
297
|
+
for (const pid of pids) {
|
|
298
|
+
if (!pid || pid === process.pid || Number.isNaN(pid)) continue;
|
|
299
|
+
try {
|
|
300
|
+
const cmdline = readFileSync(`/proc/${pid}/cmdline`, "utf-8");
|
|
301
|
+
if (!/web-server\.m?js/.test(cmdline)) continue; // not a cerebro web-server — don't touch
|
|
302
|
+
process.kill(pid, "SIGTERM");
|
|
303
|
+
setTimeout(() => { try { process.kill(pid, "SIGKILL"); } catch {} }, 500).unref();
|
|
304
|
+
} catch {}
|
|
305
|
+
}
|
|
278
306
|
}
|
|
279
307
|
|
|
280
308
|
// ─── Compact result handoff (PostCompact → SessionStart:compact) ─────────────
|
package/package.json
CHANGED
package/scripts/web-server.mjs
CHANGED
|
@@ -83,7 +83,12 @@ function serveFile(res, filePath) {
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
const indexPath = path.join(WEB_DIR, "index.html");
|
|
86
|
+
// Traffic timestamp — any request refreshes it; the watchdog's idle escape
|
|
87
|
+
// hatch (below) uses it to survive with zero CC sessions while the UI is in use.
|
|
88
|
+
let lastActivity = Date.now();
|
|
86
89
|
const server = http.createServer((req, res) => {
|
|
90
|
+
lastActivity = Date.now(); // traffic = the UI is in use; feeds the idle escape hatch
|
|
91
|
+
|
|
87
92
|
if (req.url === "/health" || req.url === "/health/") {
|
|
88
93
|
res.writeHead(200, { ...COMMON, "Content-Type": "application/json" });
|
|
89
94
|
res.end(JSON.stringify({ status: "ok", service: "cerebro", port: PORT }));
|
|
@@ -133,9 +138,14 @@ server.listen(PORT, "127.0.0.1", () => {
|
|
|
133
138
|
// CC main pid. Sweep on an interval, kill(pid, 0) each, sweep stale ones.
|
|
134
139
|
// Zero CC sessions alive → count down the grace window → self-exit. Survives
|
|
135
140
|
// killed terminals (no SessionEnd needed) — refcount bookkeeping is gone.
|
|
141
|
+
// Traffic escape hatch: zcode reuses this daemon and keeps it alive by pinging
|
|
142
|
+
// /health at every turn-end; an open web panel polls too. Requests mean
|
|
143
|
+
// someone is using the UI — survive with zero live CC sessions until traffic
|
|
144
|
+
// stops for IDLE_MS (mirrors the zcode daemon's idle semantics).
|
|
136
145
|
const SESSIONS_DIR = path.join(process.env.HOME || process.env.USERPROFILE || "", ".config/cerebro/sessions");
|
|
137
146
|
const GRACE_MS = parseInt(process.env.OMEM_WEB_GRACE_MS || "", 10) || 60_000;
|
|
138
147
|
const WATCH_MS = parseInt(process.env.OMEM_WEB_WATCH_MS || "", 10) || 60_000;
|
|
148
|
+
const IDLE_MS = parseInt(process.env.OMEM_WEB_IDLE_MS || "", 10) || 300_000;
|
|
139
149
|
let zeroSince = 0; // 0 = CC alive (or unknown)
|
|
140
150
|
|
|
141
151
|
setInterval(() => {
|
|
@@ -153,10 +163,10 @@ setInterval(() => {
|
|
|
153
163
|
}
|
|
154
164
|
}
|
|
155
165
|
} catch {} // dir missing = no CC session ever
|
|
156
|
-
if (alive > 0) { zeroSince = 0; return; }
|
|
166
|
+
if (alive > 0 || Date.now() - lastActivity < IDLE_MS) { zeroSince = 0; return; }
|
|
157
167
|
if (!zeroSince) zeroSince = Date.now();
|
|
158
168
|
if (Date.now() - zeroSince >= GRACE_MS) {
|
|
159
|
-
console.log(`[cerebro web-server] no CC sessions
|
|
169
|
+
console.log(`[cerebro web-server] no CC sessions or traffic for ${IDLE_MS / 1000}s, self-exiting`);
|
|
160
170
|
try { fs.unlinkSync(PID_FILE); } catch {}
|
|
161
171
|
process.exit(0);
|
|
162
172
|
}
|