sessionpipe 0.1.0 → 0.1.1
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/dist/cli.js +417 -140
- package/dist/cli.js.map +3 -3
- package/dist/hook.js +5 -5
- package/dist/hook.js.map +2 -2
- package/dist/index.js +283 -96
- package/dist/index.js.map +3 -3
- package/dist/run.d.ts +13 -1
- package/dist/run.d.ts.map +1 -1
- package/dist/worker.js +315 -102
- package/dist/worker.js.map +4 -4
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// sessionpipe 0.1.
|
|
2
|
+
// sessionpipe 0.1.1 · Apache-2.0 · https://sessionpipe.org
|
|
3
3
|
|
|
4
4
|
// src/cli.ts
|
|
5
|
-
import { execFileSync as
|
|
6
|
-
import { readdirSync as
|
|
5
|
+
import { execFileSync as execFileSync4, spawn, spawnSync } from "node:child_process";
|
|
6
|
+
import { mkdtempSync, readdirSync as readdirSync7, readFileSync as readFileSync9, realpathSync as realpathSync2, rmSync, statSync as statSync6, unlinkSync as unlinkSync5 } from "node:fs";
|
|
7
7
|
import os3 from "node:os";
|
|
8
8
|
import path12 from "node:path";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
10
|
|
|
11
11
|
// ../core/src/adapters/antigravity.ts
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
existsSync as existsSync2,
|
|
14
|
+
mkdirSync as mkdirSync2,
|
|
15
|
+
readdirSync as readdirSync2,
|
|
16
|
+
readFileSync as readFileSync2,
|
|
17
|
+
realpathSync,
|
|
18
|
+
statSync as statSync2,
|
|
19
|
+
unlinkSync as unlinkSync2,
|
|
20
|
+
writeFileSync as writeFileSync2
|
|
21
|
+
} from "node:fs";
|
|
13
22
|
import path4 from "node:path";
|
|
14
23
|
|
|
15
24
|
// ../core/src/paths.ts
|
|
@@ -236,16 +245,42 @@ function readAntigravity(lines, from) {
|
|
|
236
245
|
}
|
|
237
246
|
|
|
238
247
|
// ../core/src/adapters/claude-shaped.ts
|
|
239
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
248
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
240
249
|
import path3 from "node:path";
|
|
241
250
|
var OURS = /sessionpipe[^"\n]*[\\/]hook\.js"?\s+[a-z][a-z-]*\s+[A-Za-z]+\s*$/;
|
|
242
251
|
var isOurs = (h) => Array.isArray(h?.hooks) && h.hooks.some((x) => typeof x.command === "string" && OURS.test(x.command));
|
|
243
|
-
function
|
|
252
|
+
function readJsonFile(file) {
|
|
253
|
+
let raw;
|
|
244
254
|
try {
|
|
245
|
-
|
|
255
|
+
raw = readFileSync(file, "utf8");
|
|
246
256
|
} catch {
|
|
247
|
-
return {};
|
|
257
|
+
return { ok: true, existed: false, value: {}, raw: "", indent: " ", eol: "\n", finalNewline: true };
|
|
248
258
|
}
|
|
259
|
+
let value;
|
|
260
|
+
try {
|
|
261
|
+
value = JSON.parse(raw);
|
|
262
|
+
} catch {
|
|
263
|
+
const hint = /^\s*\/\/|\/\*/m.test(raw) ? "not plain JSON (it has comments)" : "not plain JSON";
|
|
264
|
+
return { ok: false, existed: true, reason: hint };
|
|
265
|
+
}
|
|
266
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
267
|
+
return { ok: false, existed: true, reason: "not a JSON object" };
|
|
268
|
+
const eol = raw.includes("\r\n") ? "\r\n" : "\n";
|
|
269
|
+
const m = /^( +|\t+)\S/m.exec(raw);
|
|
270
|
+
const indent = m?.[1] ?? " ";
|
|
271
|
+
return {
|
|
272
|
+
ok: true,
|
|
273
|
+
existed: true,
|
|
274
|
+
value,
|
|
275
|
+
raw,
|
|
276
|
+
indent,
|
|
277
|
+
eol,
|
|
278
|
+
finalNewline: /\r?\n$/.test(raw)
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
function formatJson(value, f) {
|
|
282
|
+
const text = JSON.stringify(value, null, f.indent).replace(/\n/g, f.eol);
|
|
283
|
+
return f.finalNewline ? text + f.eol : text;
|
|
249
284
|
}
|
|
250
285
|
function entryFor(event, harness, cmd2, o) {
|
|
251
286
|
const matcher = o.matcherFor(event);
|
|
@@ -255,9 +290,22 @@ function entryFor(event, harness, cmd2, o) {
|
|
|
255
290
|
};
|
|
256
291
|
}
|
|
257
292
|
function installClaudeShaped(file, harness, events, cmd2, o) {
|
|
258
|
-
const
|
|
293
|
+
const f = readJsonFile(file);
|
|
294
|
+
if (!f.ok) return { file, changed: false, skipped: true, note: `${f.reason}; not touched` };
|
|
295
|
+
const settings2 = f.value;
|
|
259
296
|
const hooks = settings2.hooks && typeof settings2.hooks === "object" ? settings2.hooks : {};
|
|
260
297
|
let changed = false;
|
|
298
|
+
const want = new Set(events);
|
|
299
|
+
for (const ev of Object.keys(hooks)) {
|
|
300
|
+
if (want.has(ev)) continue;
|
|
301
|
+
const list = Array.isArray(hooks[ev]) ? hooks[ev] : [];
|
|
302
|
+
const kept = list.filter((h) => !isOurs(h));
|
|
303
|
+
if (kept.length !== list.length) {
|
|
304
|
+
changed = true;
|
|
305
|
+
if (kept.length) hooks[ev] = kept;
|
|
306
|
+
else delete hooks[ev];
|
|
307
|
+
}
|
|
308
|
+
}
|
|
261
309
|
for (const ev of events) {
|
|
262
310
|
const list = Array.isArray(hooks[ev]) ? hooks[ev] : [];
|
|
263
311
|
const entry2 = entryFor(ev, harness, cmd2, o);
|
|
@@ -276,19 +324,19 @@ function installClaudeShaped(file, harness, events, cmd2, o) {
|
|
|
276
324
|
if (changed) {
|
|
277
325
|
settings2.hooks = hooks;
|
|
278
326
|
mkdirSync(path3.dirname(file), { recursive: true });
|
|
279
|
-
writeFileSync(file,
|
|
280
|
-
`);
|
|
327
|
+
writeFileSync(file, formatJson(settings2, f));
|
|
281
328
|
}
|
|
282
|
-
return { file, changed };
|
|
329
|
+
return { file, changed, created: !f.existed && changed };
|
|
283
330
|
}
|
|
284
|
-
function uninstallClaudeShaped(file, events) {
|
|
331
|
+
function uninstallClaudeShaped(file, events, opts = {}) {
|
|
285
332
|
if (!existsSync(file)) return { file, changed: false };
|
|
286
|
-
const
|
|
287
|
-
|
|
333
|
+
const f = readJsonFile(file);
|
|
334
|
+
if (!f.ok) return { file, changed: false, skipped: true, note: `${f.reason}; not touched` };
|
|
335
|
+
const settings2 = f.value;
|
|
288
336
|
const hooks = settings2.hooks;
|
|
289
337
|
if (!hooks || typeof hooks !== "object") return { file, changed: false };
|
|
290
338
|
let changed = false;
|
|
291
|
-
for (const ev of events) {
|
|
339
|
+
for (const ev of [...events, ...Object.keys(hooks)]) {
|
|
292
340
|
const list = Array.isArray(hooks[ev]) ? hooks[ev] : [];
|
|
293
341
|
const kept = list.filter((h) => !isOurs(h));
|
|
294
342
|
if (kept.length !== list.length) {
|
|
@@ -299,13 +347,17 @@ function uninstallClaudeShaped(file, events) {
|
|
|
299
347
|
}
|
|
300
348
|
if (!changed) return { file, changed: false };
|
|
301
349
|
if (!Object.keys(hooks).length) delete settings2.hooks;
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
350
|
+
if (opts.created && !Object.keys(settings2).length) {
|
|
351
|
+
unlinkSync(file);
|
|
352
|
+
return { file, changed: true, note: "removed (sessionpipe created it)" };
|
|
353
|
+
}
|
|
354
|
+
const next = formatJson(settings2, f);
|
|
355
|
+
if (next !== f.raw) writeFileSync(file, next);
|
|
305
356
|
return { file, changed: true };
|
|
306
357
|
}
|
|
307
358
|
function installedClaudeShaped(file, harness, events, cmd2, o) {
|
|
308
|
-
const
|
|
359
|
+
const f = readJsonFile(file);
|
|
360
|
+
const hooks = f.ok ? f.value.hooks ?? {} : {};
|
|
309
361
|
let present = 0;
|
|
310
362
|
let current = 0;
|
|
311
363
|
for (const ev of events) {
|
|
@@ -314,6 +366,9 @@ function installedClaudeShaped(file, harness, events, cmd2, o) {
|
|
|
314
366
|
if (mine.length) present++;
|
|
315
367
|
if (mine.length === 1 && JSON.stringify(mine[0]) === JSON.stringify(entryFor(ev, harness, cmd2, o))) current++;
|
|
316
368
|
}
|
|
369
|
+
for (const ev of Object.keys(hooks))
|
|
370
|
+
if (!events.includes(ev) && (Array.isArray(hooks[ev]) ? hooks[ev] : []).some((h) => isOurs(h)))
|
|
371
|
+
return "stale";
|
|
317
372
|
if (!present) return "missing";
|
|
318
373
|
return current === events.length ? "current" : "stale";
|
|
319
374
|
}
|
|
@@ -328,10 +383,13 @@ var projectsDir = (env) => path4.join(gemini(env), "config", "projects");
|
|
|
328
383
|
var dataDirs = (env) => ["antigravity", "antigravity-cli", "antigravity-ide"].map((d) => path4.join(gemini(env), d));
|
|
329
384
|
var ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
|
|
330
385
|
var validTool = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
|
|
331
|
-
function entry(cmd2) {
|
|
386
|
+
function entry(cmd2, lean) {
|
|
332
387
|
const handler = (e) => ({ type: "command", command: cmd2([NAME, e]).command, timeout: 5 });
|
|
333
388
|
const out2 = { enabled: true };
|
|
334
|
-
for (const e of AG_EVENTS)
|
|
389
|
+
for (const e of AG_EVENTS) {
|
|
390
|
+
if (lean && e === "PreToolUse") continue;
|
|
391
|
+
out2[e] = /Tool/.test(e) ? [{ matcher: "*", hooks: [handler(e)] }] : [handler(e)];
|
|
392
|
+
}
|
|
335
393
|
return out2;
|
|
336
394
|
}
|
|
337
395
|
var antigravity = {
|
|
@@ -339,34 +397,49 @@ var antigravity = {
|
|
|
339
397
|
events: AG_EVENTS,
|
|
340
398
|
detect: (env = process.env) => existsSync2(path4.join(gemini(env), "config")) || dataDirs(env).some((d) => existsSync2(d)),
|
|
341
399
|
configFiles: (env = process.env) => [hooksFile(env)],
|
|
342
|
-
install(cmd2, env = process.env) {
|
|
400
|
+
install(cmd2, env = process.env, opts = {}) {
|
|
343
401
|
const file = hooksFile(env);
|
|
344
|
-
const
|
|
345
|
-
|
|
402
|
+
const f = readJsonFile(file);
|
|
403
|
+
if (!f.ok) return [{ file, changed: false, skipped: true, note: `${f.reason}; not touched` }];
|
|
404
|
+
const hooks = f.value;
|
|
405
|
+
const e = entry(cmd2, opts.lean);
|
|
346
406
|
if (JSON.stringify(hooks[HOOK_NAME]) === JSON.stringify(e)) return [{ file, changed: false }];
|
|
347
407
|
hooks[HOOK_NAME] = e;
|
|
348
408
|
mkdirSync2(path4.dirname(file), { recursive: true });
|
|
349
|
-
writeFileSync2(file,
|
|
350
|
-
`);
|
|
409
|
+
writeFileSync2(file, formatJson(hooks, f));
|
|
351
410
|
return [
|
|
352
|
-
{
|
|
411
|
+
{
|
|
412
|
+
file,
|
|
413
|
+
changed: true,
|
|
414
|
+
created: !f.existed,
|
|
415
|
+
note: "hooks.json is read when a conversation starts; open ones report after a restart"
|
|
416
|
+
}
|
|
353
417
|
];
|
|
354
418
|
},
|
|
355
|
-
uninstall(env = process.env) {
|
|
419
|
+
uninstall(env = process.env, opts = {}) {
|
|
356
420
|
const file = hooksFile(env);
|
|
357
421
|
if (!existsSync2(file)) return [{ file, changed: false }];
|
|
358
|
-
const
|
|
422
|
+
const f = readJsonFile(file);
|
|
423
|
+
if (!f.ok) return [{ file, changed: false, skipped: true, note: `${f.reason}; not touched` }];
|
|
424
|
+
const hooks = f.value;
|
|
359
425
|
if (!(HOOK_NAME in hooks)) return [{ file, changed: false }];
|
|
360
426
|
delete hooks[HOOK_NAME];
|
|
361
|
-
|
|
362
|
-
|
|
427
|
+
if (opts.created?.includes(file) && !Object.keys(hooks).length) {
|
|
428
|
+
unlinkSync2(file);
|
|
429
|
+
return [{ file, changed: true, note: "removed (sessionpipe created it)" }];
|
|
430
|
+
}
|
|
431
|
+
writeFileSync2(file, formatJson(hooks, f));
|
|
363
432
|
return [{ file, changed: true }];
|
|
364
433
|
},
|
|
365
|
-
installed(cmd2, env = process.env) {
|
|
434
|
+
installed(cmd2, env = process.env, opts = {}) {
|
|
366
435
|
const file = hooksFile(env);
|
|
367
|
-
const
|
|
436
|
+
const f = readJsonFile(file);
|
|
437
|
+
const have = f.ok ? f.value[HOOK_NAME] : void 0;
|
|
368
438
|
return [
|
|
369
|
-
{
|
|
439
|
+
{
|
|
440
|
+
file,
|
|
441
|
+
state: !have ? "missing" : JSON.stringify(have) === JSON.stringify(entry(cmd2, opts.lean)) ? "current" : "stale"
|
|
442
|
+
}
|
|
370
443
|
];
|
|
371
444
|
},
|
|
372
445
|
fromHook(input) {
|
|
@@ -632,8 +705,11 @@ var CLAUDE_EVENTS = [
|
|
|
632
705
|
"SubagentStop",
|
|
633
706
|
"PreCompact",
|
|
634
707
|
"PostCompact",
|
|
635
|
-
"SessionEnd"
|
|
708
|
+
"SessionEnd",
|
|
709
|
+
"StopFailure"
|
|
636
710
|
];
|
|
711
|
+
var LEAN_SKIP = /* @__PURE__ */ new Set(["PreToolUse", "PostToolUseFailure"]);
|
|
712
|
+
var eventsFor = (lean) => lean ? CLAUDE_EVENTS.filter((e) => !LEAN_SKIP.has(e)) : [...CLAUDE_EVENTS];
|
|
637
713
|
var OPTS = { matcherFor: (e) => /Tool|Permission/.test(e) ? "" : void 0, timeout: 5 };
|
|
638
714
|
var DESKTOP_ID = /^local_[A-Za-z0-9-]{1,64}$/;
|
|
639
715
|
function claudeDirs(env = process.env, home = HOME) {
|
|
@@ -665,11 +741,13 @@ var claudeCode = {
|
|
|
665
741
|
events: CLAUDE_EVENTS,
|
|
666
742
|
detect: (env = process.env) => claudeDirs(env).some((d) => existsSync3(d)),
|
|
667
743
|
configFiles: (env = process.env) => claudeDirs(env).map(settingsIn),
|
|
668
|
-
install: (cmd2, env = process.env) => claudeDirs(env).map((d) => installClaudeShaped(settingsIn(d), NAME2,
|
|
669
|
-
uninstall: (env = process.env) => claudeDirs(env).map(
|
|
670
|
-
|
|
744
|
+
install: (cmd2, env = process.env, opts = {}) => claudeDirs(env).map((d) => installClaudeShaped(settingsIn(d), NAME2, eventsFor(opts.lean), cmd2, OPTS)),
|
|
745
|
+
uninstall: (env = process.env, opts = {}) => claudeDirs(env).map(
|
|
746
|
+
(d) => uninstallClaudeShaped(settingsIn(d), CLAUDE_EVENTS, { created: opts.created?.includes(settingsIn(d)) })
|
|
747
|
+
),
|
|
748
|
+
installed: (cmd2, env = process.env, opts = {}) => claudeDirs(env).map((d) => ({
|
|
671
749
|
file: settingsIn(d),
|
|
672
|
-
state: installedClaudeShaped(settingsIn(d), NAME2,
|
|
750
|
+
state: installedClaudeShaped(settingsIn(d), NAME2, eventsFor(opts.lean), cmd2, OPTS)
|
|
673
751
|
})),
|
|
674
752
|
fromHook(input) {
|
|
675
753
|
const event = input.argv[1] ?? "";
|
|
@@ -760,6 +838,15 @@ var claudeCode = {
|
|
|
760
838
|
case "Stop":
|
|
761
839
|
events.push(ev("turn.ended", { turn_id, reason: "stop" }));
|
|
762
840
|
break;
|
|
841
|
+
case "StopFailure":
|
|
842
|
+
events.push(
|
|
843
|
+
ev("turn.ended", {
|
|
844
|
+
turn_id,
|
|
845
|
+
reason: "error",
|
|
846
|
+
error: typeof s.error === "string" ? s.error.slice(0, 200) : void 0
|
|
847
|
+
})
|
|
848
|
+
);
|
|
849
|
+
break;
|
|
763
850
|
case "SubagentStart":
|
|
764
851
|
events.push(
|
|
765
852
|
ev("subagent.started", {
|
|
@@ -1271,6 +1358,7 @@ var CODEX_EVENTS = [
|
|
|
1271
1358
|
"SessionEnd"
|
|
1272
1359
|
];
|
|
1273
1360
|
var OPTS2 = { matcherFor: (e) => /Tool|Permission/.test(e) ? "" : void 0, timeout: 5 };
|
|
1361
|
+
var eventsFor2 = (lean) => lean ? CODEX_EVENTS.filter((e) => e !== "PreToolUse") : [...CODEX_EVENTS];
|
|
1274
1362
|
var codexHome = (env) => env.CODEX_HOME || path6.join(HOME, ".codex");
|
|
1275
1363
|
var hooksFile2 = (env) => path6.join(codexHome(env), "hooks.json");
|
|
1276
1364
|
var configFile2 = (env) => path6.join(codexHome(env), "config.toml");
|
|
@@ -1319,7 +1407,8 @@ function installNotify(cmd2, env) {
|
|
|
1319
1407
|
return {
|
|
1320
1408
|
file,
|
|
1321
1409
|
changed: false,
|
|
1322
|
-
|
|
1410
|
+
skipped: true,
|
|
1411
|
+
note: `notify belongs to ${(n.argv?.[0] ?? n.raw).replace(/^.*\//, "").slice(0, 40)}; left alone. Codex will report nothing until you approve the hooks: start \`codex\` and accept the hook review`
|
|
1323
1412
|
};
|
|
1324
1413
|
}
|
|
1325
1414
|
write(withNotify(text, line));
|
|
@@ -1376,14 +1465,21 @@ var codex = {
|
|
|
1376
1465
|
events: CODEX_EVENTS,
|
|
1377
1466
|
detect: (env = process.env) => existsSync4(codexHome(env)),
|
|
1378
1467
|
configFiles: (env = process.env) => [hooksFile2(env), configFile2(env)],
|
|
1379
|
-
install: (cmd2, env = process.env) => [
|
|
1380
|
-
installClaudeShaped(hooksFile2(env), NAME3,
|
|
1468
|
+
install: (cmd2, env = process.env, opts = {}) => [
|
|
1469
|
+
installClaudeShaped(hooksFile2(env), NAME3, eventsFor2(opts.lean), cmd2, OPTS2),
|
|
1381
1470
|
installNotify(cmd2, env)
|
|
1382
1471
|
],
|
|
1383
|
-
uninstall: (env = process.env) => [
|
|
1384
|
-
|
|
1472
|
+
uninstall: (env = process.env, opts = {}) => [
|
|
1473
|
+
uninstallClaudeShaped(hooksFile2(env), CODEX_EVENTS, { created: opts.created?.includes(hooksFile2(env)) }),
|
|
1474
|
+
uninstallNotify(env)
|
|
1475
|
+
],
|
|
1476
|
+
installed: (cmd2, env = process.env, opts = {}) => {
|
|
1385
1477
|
const out2 = [
|
|
1386
|
-
{
|
|
1478
|
+
{
|
|
1479
|
+
file: hooksFile2(env),
|
|
1480
|
+
state: installedClaudeShaped(hooksFile2(env), NAME3, eventsFor2(opts.lean), cmd2, OPTS2),
|
|
1481
|
+
note: "Codex runs non-managed hooks only after the review in `codex` (/hooks); until then nothing is reported unless the notify fallback is ours"
|
|
1482
|
+
}
|
|
1387
1483
|
];
|
|
1388
1484
|
let text = "";
|
|
1389
1485
|
try {
|
|
@@ -1571,6 +1667,7 @@ function codexFacts(file) {
|
|
|
1571
1667
|
}
|
|
1572
1668
|
|
|
1573
1669
|
// ../core/src/adapters/gemini-cli.ts
|
|
1670
|
+
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
1574
1671
|
import { existsSync as existsSync5, readFileSync as readFileSync5 } from "node:fs";
|
|
1575
1672
|
import path7 from "node:path";
|
|
1576
1673
|
var NAME4 = "gemini-cli";
|
|
@@ -1588,17 +1685,61 @@ var OPTS3 = { matcherFor: (e) => /Tool/.test(e) ? ".*" : void 0, timeout: 5e3 };
|
|
|
1588
1685
|
var geminiDir = (env) => env.GEMINI_CLI_HOME || path7.join(HOME, ".gemini");
|
|
1589
1686
|
var settings = (env) => path7.join(geminiDir(env), "settings.json");
|
|
1590
1687
|
var validTool4 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
|
|
1688
|
+
var eventsFor3 = (lean) => lean ? GEMINI_EVENTS.filter((e) => e !== "BeforeTool") : [...GEMINI_EVENTS];
|
|
1689
|
+
function geminiHooksActive(settingsFile) {
|
|
1690
|
+
let tools;
|
|
1691
|
+
try {
|
|
1692
|
+
tools = JSON.parse(readFileSync5(settingsFile, "utf8")).tools;
|
|
1693
|
+
} catch {
|
|
1694
|
+
}
|
|
1695
|
+
if (tools?.enableHooks === true) return { active: true };
|
|
1696
|
+
if (tools?.enableHooks === false)
|
|
1697
|
+
return { active: false, note: "inactive: tools.enableHooks is false in settings.json" };
|
|
1698
|
+
let version = "";
|
|
1699
|
+
try {
|
|
1700
|
+
version = execFileSync3("gemini", ["--version"], {
|
|
1701
|
+
encoding: "utf8",
|
|
1702
|
+
timeout: 4e3,
|
|
1703
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
1704
|
+
}).trim();
|
|
1705
|
+
} catch {
|
|
1706
|
+
return {
|
|
1707
|
+
active: true,
|
|
1708
|
+
note: "could not run `gemini --version`; hooks need tools.enableHooks on versions before 0.46"
|
|
1709
|
+
};
|
|
1710
|
+
}
|
|
1711
|
+
const m = /(\d+)\.(\d+)/.exec(version);
|
|
1712
|
+
if (m && (Number(m[1]) > 0 || Number(m[2]) >= 46)) return { active: true };
|
|
1713
|
+
return {
|
|
1714
|
+
active: false,
|
|
1715
|
+
note: `inactive: Gemini CLI ${version || "?"} runs hooks only with "tools": {"enableHooks": true} in settings.json`
|
|
1716
|
+
};
|
|
1717
|
+
}
|
|
1591
1718
|
var geminiCli = {
|
|
1592
1719
|
name: NAME4,
|
|
1593
1720
|
events: GEMINI_EVENTS,
|
|
1594
1721
|
// ~/.gemini also belongs to Antigravity; Gemini CLI is here when its settings or its chat logs exist.
|
|
1595
1722
|
detect: (env = process.env) => existsSync5(settings(env)) || existsSync5(path7.join(geminiDir(env), "tmp")),
|
|
1596
1723
|
configFiles: (env = process.env) => [settings(env)],
|
|
1597
|
-
install: (cmd2, env = process.env
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1724
|
+
install: (cmd2, env = process.env, opts = {}) => {
|
|
1725
|
+
const r = installClaudeShaped(settings(env), NAME4, eventsFor3(opts.lean), cmd2, OPTS3);
|
|
1726
|
+
const a = geminiHooksActive(settings(env));
|
|
1727
|
+
return [a.note && !r.skipped ? { ...r, note: [r.note, a.note].filter(Boolean).join("; ") } : r];
|
|
1728
|
+
},
|
|
1729
|
+
uninstall: (env = process.env, opts = {}) => [
|
|
1730
|
+
uninstallClaudeShaped(settings(env), GEMINI_EVENTS, { created: opts.created?.includes(settings(env)) })
|
|
1601
1731
|
],
|
|
1732
|
+
installed: (cmd2, env = process.env, opts = {}) => {
|
|
1733
|
+
const state2 = installedClaudeShaped(settings(env), NAME4, eventsFor3(opts.lean), cmd2, OPTS3);
|
|
1734
|
+
const a = geminiHooksActive(settings(env));
|
|
1735
|
+
return [
|
|
1736
|
+
{
|
|
1737
|
+
file: settings(env),
|
|
1738
|
+
state: state2 === "current" && !a.active ? "inactive" : state2,
|
|
1739
|
+
...a.note ? { note: a.note } : {}
|
|
1740
|
+
}
|
|
1741
|
+
];
|
|
1742
|
+
},
|
|
1602
1743
|
fromHook(input) {
|
|
1603
1744
|
const event = input.argv[1] ?? "";
|
|
1604
1745
|
let s = {};
|
|
@@ -1809,7 +1950,7 @@ import {
|
|
|
1809
1950
|
readSync as readSync2,
|
|
1810
1951
|
renameSync,
|
|
1811
1952
|
statSync as statSync4,
|
|
1812
|
-
unlinkSync,
|
|
1953
|
+
unlinkSync as unlinkSync3,
|
|
1813
1954
|
writeFileSync as writeFileSync5
|
|
1814
1955
|
} from "node:fs";
|
|
1815
1956
|
import path9 from "node:path";
|
|
@@ -1837,8 +1978,8 @@ var Outbox = class {
|
|
|
1837
1978
|
} catch {
|
|
1838
1979
|
n = this.lastSeqInFile(ref) + 1;
|
|
1839
1980
|
}
|
|
1840
|
-
mkdirSync5(path9.dirname(f), { recursive: true });
|
|
1841
|
-
writeFileSync5(f, String(n + 1));
|
|
1981
|
+
mkdirSync5(path9.dirname(f), { recursive: true, mode: 448 });
|
|
1982
|
+
writeFileSync5(f, String(n + 1), { mode: 384 });
|
|
1842
1983
|
return n;
|
|
1843
1984
|
}
|
|
1844
1985
|
lastSeqInFile(ref) {
|
|
@@ -1856,9 +1997,9 @@ var Outbox = class {
|
|
|
1856
1997
|
}
|
|
1857
1998
|
append(ref, events) {
|
|
1858
1999
|
if (!events.length) return;
|
|
1859
|
-
mkdirSync5(this.dir(ref.harness), { recursive: true });
|
|
2000
|
+
mkdirSync5(this.dir(ref.harness), { recursive: true, mode: 448 });
|
|
1860
2001
|
appendFileSync(this.file(ref), `${events.map((e) => JSON.stringify(e)).join("\n")}
|
|
1861
|
-
|
|
2002
|
+
`, { mode: 384 });
|
|
1862
2003
|
}
|
|
1863
2004
|
/** Every session file, newest first. */
|
|
1864
2005
|
sessions() {
|
|
@@ -1922,9 +2063,9 @@ var Outbox = class {
|
|
|
1922
2063
|
const f = this.file(ref);
|
|
1923
2064
|
try {
|
|
1924
2065
|
if (statSync4(f).mtimeMs < cutoff) {
|
|
1925
|
-
|
|
2066
|
+
unlinkSync3(f);
|
|
1926
2067
|
try {
|
|
1927
|
-
|
|
2068
|
+
unlinkSync3(this.seqFile(ref));
|
|
1928
2069
|
} catch {
|
|
1929
2070
|
}
|
|
1930
2071
|
n++;
|
|
@@ -1937,7 +2078,7 @@ var Outbox = class {
|
|
|
1937
2078
|
delete(ref) {
|
|
1938
2079
|
for (const f of [this.file(ref), this.seqFile(ref)]) {
|
|
1939
2080
|
try {
|
|
1940
|
-
|
|
2081
|
+
unlinkSync3(f);
|
|
1941
2082
|
} catch {
|
|
1942
2083
|
}
|
|
1943
2084
|
}
|
|
@@ -1962,9 +2103,9 @@ var Cursors = class {
|
|
|
1962
2103
|
}
|
|
1963
2104
|
write(c) {
|
|
1964
2105
|
const f = this.file();
|
|
1965
|
-
mkdirSync5(path9.dirname(f), { recursive: true });
|
|
2106
|
+
mkdirSync5(path9.dirname(f), { recursive: true, mode: 448 });
|
|
1966
2107
|
const tmp = `${f}.${process.pid}.tmp`;
|
|
1967
|
-
writeFileSync5(tmp, JSON.stringify(c));
|
|
2108
|
+
writeFileSync5(tmp, JSON.stringify(c), { mode: 384 });
|
|
1968
2109
|
renameSync(tmp, f);
|
|
1969
2110
|
}
|
|
1970
2111
|
key(ref) {
|
|
@@ -1973,10 +2114,10 @@ var Cursors = class {
|
|
|
1973
2114
|
};
|
|
1974
2115
|
function takeLock(root, ref, staleMs = 5 * 6e4) {
|
|
1975
2116
|
const dir = path9.join(root, "locks");
|
|
1976
|
-
mkdirSync5(dir, { recursive: true });
|
|
2117
|
+
mkdirSync5(dir, { recursive: true, mode: 448 });
|
|
1977
2118
|
const f = path9.join(dir, `${safeId(ref.harness)}-${safeId(ref.session)}.lock`);
|
|
1978
2119
|
try {
|
|
1979
|
-
if (Date.now() - statSync4(f).mtimeMs > staleMs)
|
|
2120
|
+
if (Date.now() - statSync4(f).mtimeMs > staleMs) unlinkSync3(f);
|
|
1980
2121
|
else return null;
|
|
1981
2122
|
} catch {
|
|
1982
2123
|
}
|
|
@@ -1987,7 +2128,7 @@ function takeLock(root, ref, staleMs = 5 * 6e4) {
|
|
|
1987
2128
|
}
|
|
1988
2129
|
return () => {
|
|
1989
2130
|
try {
|
|
1990
|
-
|
|
2131
|
+
unlinkSync3(f);
|
|
1991
2132
|
} catch {
|
|
1992
2133
|
}
|
|
1993
2134
|
};
|
|
@@ -2123,7 +2264,7 @@ function redactAssignments(text) {
|
|
|
2123
2264
|
function isSecretName(name) {
|
|
2124
2265
|
if (/^[A-Z0-9_]+_KEY$/.test(name)) return true;
|
|
2125
2266
|
const flat = name.toLowerCase().replace(/[_-]/g, "");
|
|
2126
|
-
return /(?:pass(?:word|wd|phrase|code)
|
|
2267
|
+
return /(?:pass(?:word|wd|phrase|code)|pwd|secret|secretkey|secretaccesskey|clientsecret|apikey|accesskey|accesstoken|authtoken|refreshtoken|privatekey|token|credential|creds)s?$/.test(
|
|
2127
2268
|
flat
|
|
2128
2269
|
);
|
|
2129
2270
|
}
|
|
@@ -2212,7 +2353,7 @@ var DATA_ALLOW = {
|
|
|
2212
2353
|
"session.backfill": { 0: ["started_at", "last_at", "turns"] },
|
|
2213
2354
|
"session.forgotten": { 0: [] },
|
|
2214
2355
|
"turn.started": { 0: ["turn_id"], 1: ["prompt_chars"] },
|
|
2215
|
-
"turn.ended": { 0: ["turn_id", "reason", "ms"] },
|
|
2356
|
+
"turn.ended": { 0: ["turn_id", "reason", "ms"], 1: ["error"] },
|
|
2216
2357
|
"turn.transcript": { 2: ["turn", "user", "assistant", "at"] },
|
|
2217
2358
|
"tool.started": { 1: ["tool", "call_id", "turn_id", "agent_id"], 3: ["input"] },
|
|
2218
2359
|
"tool.ended": { 1: ["tool", "call_id", "turn_id", "agent_id", "ms", "ok", "error"], 3: ["input", "output"] },
|
|
@@ -2334,7 +2475,17 @@ var StdoutSink = class {
|
|
|
2334
2475
|
};
|
|
2335
2476
|
|
|
2336
2477
|
// src/run.ts
|
|
2337
|
-
import {
|
|
2478
|
+
import {
|
|
2479
|
+
appendFileSync as appendFileSync3,
|
|
2480
|
+
existsSync as existsSync8,
|
|
2481
|
+
mkdirSync as mkdirSync7,
|
|
2482
|
+
readdirSync as readdirSync6,
|
|
2483
|
+
readFileSync as readFileSync8,
|
|
2484
|
+
renameSync as renameSync2,
|
|
2485
|
+
statSync as statSync5,
|
|
2486
|
+
unlinkSync as unlinkSync4,
|
|
2487
|
+
writeFileSync as writeFileSync6
|
|
2488
|
+
} from "node:fs";
|
|
2338
2489
|
import os2 from "node:os";
|
|
2339
2490
|
import path11 from "node:path";
|
|
2340
2491
|
|
|
@@ -2424,10 +2575,17 @@ var HttpSink = class {
|
|
|
2424
2575
|
};
|
|
2425
2576
|
|
|
2426
2577
|
// src/run.ts
|
|
2427
|
-
var VERSION = true ? "0.1.
|
|
2578
|
+
var VERSION = true ? "0.1.1" : "0.0.0";
|
|
2579
|
+
function enqueueJob(state2, job) {
|
|
2580
|
+
const jobs = jobsDir(state2);
|
|
2581
|
+
mkdirSync7(jobs, { recursive: true, mode: 448 });
|
|
2582
|
+
const f = path11.join(jobs, `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}.json`);
|
|
2583
|
+
writeFileSync6(f, JSON.stringify(job), { mode: 384 });
|
|
2584
|
+
return f;
|
|
2585
|
+
}
|
|
2428
2586
|
function log(state2, line) {
|
|
2429
2587
|
try {
|
|
2430
|
-
mkdirSync7(state2, { recursive: true });
|
|
2588
|
+
mkdirSync7(state2, { recursive: true, mode: 448 });
|
|
2431
2589
|
const f = path11.join(state2, "log");
|
|
2432
2590
|
try {
|
|
2433
2591
|
if (statSync5(f).size > 1024 * 1024) renameSync2(f, `${f}.1`);
|
|
@@ -2452,8 +2610,8 @@ function factsState(state2) {
|
|
|
2452
2610
|
const cur = this.get(h, s);
|
|
2453
2611
|
const next = { ...cur, ...patch };
|
|
2454
2612
|
if (JSON.stringify(next) === JSON.stringify(cur)) return;
|
|
2455
|
-
mkdirSync7(path11.dirname(file(h, s)), { recursive: true });
|
|
2456
|
-
writeFileSync6(file(h, s), JSON.stringify(next));
|
|
2613
|
+
mkdirSync7(path11.dirname(file(h, s)), { recursive: true, mode: 448 });
|
|
2614
|
+
writeFileSync6(file(h, s), JSON.stringify(next), { mode: 384 });
|
|
2457
2615
|
}
|
|
2458
2616
|
};
|
|
2459
2617
|
}
|
|
@@ -2478,12 +2636,14 @@ async function runJob(job, opts = {}) {
|
|
|
2478
2636
|
if (!r || !r.session.id) return { events: [], delivered: {} };
|
|
2479
2637
|
const outbox = new Outbox(state2);
|
|
2480
2638
|
const ref = { harness: job.harness, session: r.session.id };
|
|
2481
|
-
|
|
2639
|
+
let release = takeLock(state2, ref);
|
|
2640
|
+
for (let wait = 50; !release && wait <= 3200; wait *= 2) {
|
|
2641
|
+
await new Promise((res) => setTimeout(res, wait));
|
|
2642
|
+
release = takeLock(state2, ref);
|
|
2643
|
+
}
|
|
2482
2644
|
if (!release) {
|
|
2483
|
-
log(state2, `${job.harness} ${job.event}: session ${r.session.id} locked, re-queued`);
|
|
2484
|
-
|
|
2485
|
-
mkdirSync7(jobs, { recursive: true });
|
|
2486
|
-
writeFileSync6(path11.join(jobs, `${Date.now().toString(36)}-requeue.json`), JSON.stringify(job));
|
|
2645
|
+
log(state2, `${job.harness} ${job.event}: session ${r.session.id} locked for 6 s, re-queued`);
|
|
2646
|
+
enqueueJob(state2, job);
|
|
2487
2647
|
return { events: [], delivered: {} };
|
|
2488
2648
|
}
|
|
2489
2649
|
try {
|
|
@@ -2547,7 +2707,10 @@ async function runJob(job, opts = {}) {
|
|
|
2547
2707
|
if (read.turns.length)
|
|
2548
2708
|
facts.save(job.harness, r.session.id, { transcript_line: read.line, transcript_turns: turnNo });
|
|
2549
2709
|
}
|
|
2550
|
-
outbox.append(
|
|
2710
|
+
outbox.append(
|
|
2711
|
+
ref,
|
|
2712
|
+
events.map((e) => ({ ...e, session: redactDeep(e.session), data: redactDeep(e.data) }))
|
|
2713
|
+
);
|
|
2551
2714
|
log(
|
|
2552
2715
|
state2,
|
|
2553
2716
|
`${job.harness} ${job.event}: ${events.map((e) => e.type).join(",") || "nothing"} (${r.session.id.slice(0, 8)})`
|
|
@@ -2573,34 +2736,57 @@ async function flush(state2, sinkConfigs, outbox, first) {
|
|
|
2573
2736
|
const cursors = new Cursors(state2, sink2.name);
|
|
2574
2737
|
const c = cursors.read();
|
|
2575
2738
|
let sent = 0;
|
|
2739
|
+
let stalled = false;
|
|
2740
|
+
let batch = [];
|
|
2741
|
+
let pending = {};
|
|
2742
|
+
const send = async () => {
|
|
2743
|
+
if (!batch.length) {
|
|
2744
|
+
Object.assign(c, pending);
|
|
2745
|
+
pending = {};
|
|
2746
|
+
cursors.write(c);
|
|
2747
|
+
return true;
|
|
2748
|
+
}
|
|
2749
|
+
const res = await sink2.send(batch);
|
|
2750
|
+
if (!res.ok) {
|
|
2751
|
+
log(state2, `sink ${sink2.name}: ${res.status ?? ""} ${res.error ?? ""} \u2192 ${res.action ?? "retry"}`);
|
|
2752
|
+
if (res.action === "pause" || res.action === "lower-tier") applySinkVerdict(sink2.name, res);
|
|
2753
|
+
if (res.action !== "drop") {
|
|
2754
|
+
batch = [];
|
|
2755
|
+
pending = {};
|
|
2756
|
+
return false;
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2759
|
+
sent += batch.length;
|
|
2760
|
+
Object.assign(c, pending);
|
|
2761
|
+
cursors.write(c);
|
|
2762
|
+
batch = [];
|
|
2763
|
+
pending = {};
|
|
2764
|
+
return true;
|
|
2765
|
+
};
|
|
2576
2766
|
for (const ref of refs) {
|
|
2577
|
-
if (sent >= MAX_PER_RUN) break;
|
|
2578
|
-
let offset = c[cursors.key(ref)] ?? 0;
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
const { events, offset: next } = outbox.read(ref, offset,
|
|
2767
|
+
if (stalled || sent >= MAX_PER_RUN) break;
|
|
2768
|
+
let offset = pending[cursors.key(ref)] ?? c[cursors.key(ref)] ?? 0;
|
|
2769
|
+
for (; ; ) {
|
|
2770
|
+
const room = BATCH - batch.length;
|
|
2771
|
+
const { events, offset: next } = outbox.read(ref, offset, room);
|
|
2582
2772
|
if (!events.length) break;
|
|
2583
|
-
const
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
const res = await sink2.send(filtered);
|
|
2587
|
-
ok = res.ok;
|
|
2588
|
-
if (!ok) {
|
|
2589
|
-
log(state2, `sink ${sink2.name}: ${res.status ?? ""} ${res.error ?? ""} \u2192 ${res.action ?? "retry"}`);
|
|
2590
|
-
if (res.action === "pause" || res.action === "lower-tier") applySinkVerdict(sink2.name, res);
|
|
2591
|
-
if (res.action === "drop") ok = true;
|
|
2592
|
-
}
|
|
2593
|
-
if (ok) sent += filtered.length;
|
|
2594
|
-
}
|
|
2595
|
-
if (!ok) {
|
|
2596
|
-
stalled = true;
|
|
2597
|
-
break;
|
|
2773
|
+
for (const e of events) {
|
|
2774
|
+
const f = filterEvent(e, sink2.tier, sink2.pii, { home: os2.homedir() });
|
|
2775
|
+
if (f) batch.push(f);
|
|
2598
2776
|
}
|
|
2599
2777
|
offset = next;
|
|
2600
|
-
|
|
2601
|
-
|
|
2778
|
+
pending[cursors.key(ref)] = offset;
|
|
2779
|
+
if (batch.length >= BATCH) {
|
|
2780
|
+
if (!await send()) {
|
|
2781
|
+
stalled = true;
|
|
2782
|
+
break;
|
|
2783
|
+
}
|
|
2784
|
+
if (sent >= MAX_PER_RUN) break;
|
|
2785
|
+
}
|
|
2602
2786
|
}
|
|
2603
|
-
|
|
2787
|
+
}
|
|
2788
|
+
if (!stalled && (batch.length || Object.keys(pending).length)) {
|
|
2789
|
+
if (!await send()) stalled = true;
|
|
2604
2790
|
}
|
|
2605
2791
|
delivered[sink2.name] = sent;
|
|
2606
2792
|
}
|
|
@@ -2725,9 +2911,23 @@ async function install() {
|
|
|
2725
2911
|
if (viaNpx) {
|
|
2726
2912
|
out(` Installing sessionpipe@${VERSION} globally, so the hooks start in milliseconds\u2026`);
|
|
2727
2913
|
try {
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2914
|
+
let prefix = "";
|
|
2915
|
+
try {
|
|
2916
|
+
prefix = execFileSync4("npm", ["prefix", "-g"], {
|
|
2917
|
+
encoding: "utf8",
|
|
2918
|
+
timeout: 15e3,
|
|
2919
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
2920
|
+
}).trim();
|
|
2921
|
+
} catch {
|
|
2922
|
+
}
|
|
2923
|
+
const target = /\/Cellar\/node(?:@\d+)?\/[^/]+/.test(prefix) ? path12.join(os3.homedir(), ".local") : prefix;
|
|
2924
|
+
execFileSync4("npm", ["install", "-g", `sessionpipe@${VERSION}`, ...target ? ["--prefix", target] : []], {
|
|
2925
|
+
stdio: ["ignore", "ignore", "inherit"]
|
|
2926
|
+
});
|
|
2927
|
+
const bin = process.platform === "win32" ? path12.join(target, "sessionpipe.cmd") : target ? path12.join(target, "bin", "sessionpipe") : "sessionpipe";
|
|
2928
|
+
if (target && target !== prefix)
|
|
2929
|
+
out(` Installed into ${target}; make sure ${path12.join(target, "bin")} is on your PATH.`);
|
|
2930
|
+
execFileSync4(bin, args, { stdio: "inherit", shell: process.platform === "win32" });
|
|
2731
2931
|
return;
|
|
2732
2932
|
} catch {
|
|
2733
2933
|
out(
|
|
@@ -2739,6 +2939,8 @@ async function install() {
|
|
|
2739
2939
|
const cfg = readConfig();
|
|
2740
2940
|
const machine = flag("--machine");
|
|
2741
2941
|
if (machine) cfg.machine = machine.slice(0, 80);
|
|
2942
|
+
const sinkUrl = flag("--sink");
|
|
2943
|
+
if (sinkUrl) await sinkAdd(sinkUrl, cfg);
|
|
2742
2944
|
const adapters = selectedAdapters();
|
|
2743
2945
|
if (!adapters.length) {
|
|
2744
2946
|
out(
|
|
@@ -2747,24 +2949,13 @@ async function install() {
|
|
|
2747
2949
|
return;
|
|
2748
2950
|
}
|
|
2749
2951
|
out(` Machine: ${machineName(cfg, os3.hostname())}`);
|
|
2750
|
-
const
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
` ${r.changed ? "\u2713" : "="} ${a.name}: ${tilde(r.file)}${r.note ? ` \u2014 ${r.note}` : r.changed ? " written" : " already current"}`
|
|
2757
|
-
);
|
|
2758
|
-
if (a.name === "gemini-cli")
|
|
2759
|
-
out(" note: Gemini CLI's own telemetry defaults logPrompts on; that is Google's setting, not sessionpipe's.");
|
|
2760
|
-
if (a.name === "codex")
|
|
2761
|
-
out(
|
|
2762
|
-
" note: Codex runs non-managed hooks only after you trust them: open Codex and run /hooks once. The notify fallback reports turn ends meanwhile."
|
|
2763
|
-
);
|
|
2764
|
-
}
|
|
2952
|
+
const lean = isLean(cfg);
|
|
2953
|
+
if (lean)
|
|
2954
|
+
out(
|
|
2955
|
+
" No sink above tier 0: hooking one tool event per harness (the heartbeat's). `sink add \u2026 --tier 1` or higher hooks the rest."
|
|
2956
|
+
);
|
|
2957
|
+
installHarnesses(adapters, cfg, lean);
|
|
2765
2958
|
writeConfig(cfg);
|
|
2766
|
-
const sinkUrl = flag("--sink");
|
|
2767
|
-
if (sinkUrl) await sinkAdd(sinkUrl, cfg);
|
|
2768
2959
|
const days = Number(flag("--backfill") ?? 30);
|
|
2769
2960
|
if (days > 0) backfillRows(adapters, days, cfg);
|
|
2770
2961
|
out("");
|
|
@@ -2776,10 +2967,41 @@ async function install() {
|
|
|
2776
2967
|
);
|
|
2777
2968
|
out(" Sessions already running pick up hooks on their next start (Claude Code \u2265 2.1.280 picks them up live).");
|
|
2778
2969
|
}
|
|
2970
|
+
function isLean(cfg) {
|
|
2971
|
+
return !cfg.sinks.some((s) => !s.paused && Math.min(s.tier, s.max_tier ?? 3) >= 1);
|
|
2972
|
+
}
|
|
2973
|
+
function installHarnesses(adapters, cfg, lean) {
|
|
2974
|
+
const hc = hookCommand();
|
|
2975
|
+
for (const a of adapters) {
|
|
2976
|
+
const prev = cfg.harnesses[a.name] ?? {};
|
|
2977
|
+
const created = new Set(prev.created ?? []);
|
|
2978
|
+
const reports = a.install(hc, process.env, { lean, created: [...created] });
|
|
2979
|
+
for (const r of reports) {
|
|
2980
|
+
if (r.created) created.add(r.file);
|
|
2981
|
+
const mark = r.skipped ? "!" : r.changed ? "\u2713" : "=";
|
|
2982
|
+
out(
|
|
2983
|
+
` ${mark} ${a.name}: ${tilde(r.file)}${r.note ? ` \u2014 ${r.note}` : r.changed ? " written" : " already current"}`
|
|
2984
|
+
);
|
|
2985
|
+
}
|
|
2986
|
+
cfg.harnesses[a.name] = { enabled: true, ...created.size ? { created: [...created] } : {} };
|
|
2987
|
+
if (a.name === "gemini-cli")
|
|
2988
|
+
out(" note: Gemini CLI's own telemetry defaults logPrompts on; that is Google's setting, not sessionpipe's.");
|
|
2989
|
+
if (a.name === "codex" && !reports.some((r) => r.skipped))
|
|
2990
|
+
out(
|
|
2991
|
+
" note: Codex runs non-managed hooks only after you approve them: start `codex` and accept the hook review. The notify fallback reports turn ends meanwhile."
|
|
2992
|
+
);
|
|
2993
|
+
}
|
|
2994
|
+
}
|
|
2779
2995
|
function uninstall() {
|
|
2996
|
+
const cfg = readConfig();
|
|
2780
2997
|
for (const a of ADAPTERS) {
|
|
2781
|
-
|
|
2998
|
+
const created = cfg.harnesses[a.name]?.created ?? [];
|
|
2999
|
+
for (const r of a.uninstall(process.env, { created }))
|
|
3000
|
+
if (r.changed) out(` \u2713 ${a.name}: ${r.note ?? `hooks removed from ${tilde(r.file)}`}`);
|
|
3001
|
+
else if (r.skipped) out(` ! ${a.name}: ${tilde(r.file)} \u2014 ${r.note}`);
|
|
3002
|
+
delete cfg.harnesses[a.name];
|
|
2782
3003
|
}
|
|
3004
|
+
writeConfig(cfg);
|
|
2783
3005
|
if (!has("--keep-state"))
|
|
2784
3006
|
out(
|
|
2785
3007
|
` Outbox and state kept under ${tilde(state)} (delete it yourself, or pass nothing: it prunes after ${readConfig().keep_days ?? 30} days).`
|
|
@@ -2810,16 +3032,17 @@ async function sink() {
|
|
|
2810
3032
|
if (!s) return out(` no sink named ${args[2]}`);
|
|
2811
3033
|
const [sk] = buildSinks([s]);
|
|
2812
3034
|
if (!sk) return out(" sink type not supported yet");
|
|
2813
|
-
const
|
|
2814
|
-
|
|
2815
|
-
{
|
|
2816
|
-
|
|
2817
|
-
session: { id: `test-${Date.now()}`, seq: 0, machine: machineName(cfg, os3.hostname()) }
|
|
2818
|
-
}
|
|
3035
|
+
const session = { id: `test-${Date.now()}`, machine: machineName(cfg, os3.hostname()) };
|
|
3036
|
+
const mk = (type, seq) => makeEvent(
|
|
3037
|
+
{ type, harnessEvent: "test", data: {} },
|
|
3038
|
+
{ harness: { name: "sessionpipe" }, session: { ...session, seq } }
|
|
2819
3039
|
);
|
|
2820
|
-
const r = await sk.send([
|
|
3040
|
+
const r = await sk.send([
|
|
3041
|
+
filterEvent(mk("session.heartbeat", 0), sk.tier, sk.pii),
|
|
3042
|
+
filterEvent(mk("session.forgotten", 1), sk.tier, sk.pii)
|
|
3043
|
+
]);
|
|
2821
3044
|
out(
|
|
2822
|
-
r.ok ? ` \u2713 ${s.name}: ${r.status ?? "ok"} accepted ${r.accepted ??
|
|
3045
|
+
r.ok ? ` \u2713 ${s.name}: ${r.status ?? "ok"} accepted ${r.accepted ?? 2} (a heartbeat and its forget)` : ` \u2717 ${s.name}: ${r.status ?? ""} ${r.error ?? ""} (${r.action})`
|
|
2823
3046
|
);
|
|
2824
3047
|
return;
|
|
2825
3048
|
}
|
|
@@ -2946,7 +3169,10 @@ function status() {
|
|
|
2946
3169
|
out(` machine: ${machineName(cfg, os3.hostname())} sessionpipe ${VERSION} state ${tilde(state)}`);
|
|
2947
3170
|
for (const a of ADAPTERS) {
|
|
2948
3171
|
if (!a.detect() && !cfg.harnesses[a.name]) continue;
|
|
2949
|
-
for (const r of a.installed(hc
|
|
3172
|
+
for (const r of a.installed(hc, process.env, { lean: isLean(cfg) }))
|
|
3173
|
+
out(
|
|
3174
|
+
` ${a.name.padEnd(12)} ${tilde(r.file).padEnd(44)} ${r.state}${r.state !== "current" && r.note ? ` \u2014 ${r.note}` : ""}`
|
|
3175
|
+
);
|
|
2950
3176
|
}
|
|
2951
3177
|
if (!cfg.sinks.length) out(" sinks: none (events stay in the local outbox)");
|
|
2952
3178
|
for (const s of cfg.sinks) {
|
|
@@ -2965,14 +3191,49 @@ function status() {
|
|
|
2965
3191
|
);
|
|
2966
3192
|
}
|
|
2967
3193
|
const t = timing();
|
|
2968
|
-
if (t)
|
|
3194
|
+
if (t)
|
|
3195
|
+
out(
|
|
3196
|
+
` hook: in-process p50 ${t.p50} ms over ${t.n} runs (\`sessionpipe doctor\` measures the wall clock the harness waits)`
|
|
3197
|
+
);
|
|
2969
3198
|
const jobs = jobsDir(state);
|
|
2970
3199
|
try {
|
|
2971
|
-
const n =
|
|
3200
|
+
const n = readdirSync7(jobs).length;
|
|
2972
3201
|
if (n) out(` jobs: ${n} waiting`);
|
|
2973
3202
|
} catch {
|
|
2974
3203
|
}
|
|
2975
3204
|
}
|
|
3205
|
+
function hookWall(runs = 8) {
|
|
3206
|
+
const hook = path12.join(distDir, "hook.js");
|
|
3207
|
+
const tmp = mkdtempSync(path12.join(os3.tmpdir(), "sessionpipe-doctor-"));
|
|
3208
|
+
const env = {
|
|
3209
|
+
...process.env,
|
|
3210
|
+
SESSIONPIPE_STATE: tmp,
|
|
3211
|
+
SESSIONPIPE_CONFIG: path12.join(tmp, "config.json"),
|
|
3212
|
+
SESSIONPIPE_NO_WORKER: "1"
|
|
3213
|
+
};
|
|
3214
|
+
const input = JSON.stringify({
|
|
3215
|
+
session_id: "doctor",
|
|
3216
|
+
hook_event_name: "PostToolUse",
|
|
3217
|
+
tool_name: "Bash",
|
|
3218
|
+
cwd: process.cwd()
|
|
3219
|
+
});
|
|
3220
|
+
const times = [];
|
|
3221
|
+
try {
|
|
3222
|
+
for (let i = 0; i < runs; i++) {
|
|
3223
|
+
const t0 = process.hrtime.bigint();
|
|
3224
|
+
const r = spawnSync(process.execPath, [hook, "claude-code", "PostToolUse"], { input, env, encoding: "utf8" });
|
|
3225
|
+
if (r.status !== 0) return null;
|
|
3226
|
+
times.push(Number(process.hrtime.bigint() - t0) / 1e6);
|
|
3227
|
+
}
|
|
3228
|
+
} catch {
|
|
3229
|
+
return null;
|
|
3230
|
+
} finally {
|
|
3231
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
3232
|
+
}
|
|
3233
|
+
times.sort((a, b) => a - b);
|
|
3234
|
+
const q = (p) => Math.round(times[Math.min(times.length - 1, Math.floor(p * times.length))] ?? 0);
|
|
3235
|
+
return { n: times.length, p50: q(0.5), p95: q(0.95) };
|
|
3236
|
+
}
|
|
2976
3237
|
function timing() {
|
|
2977
3238
|
try {
|
|
2978
3239
|
const lines = readFileSync9(path12.join(state, "timing.jsonl"), "utf8").trim().split("\n").slice(-100);
|
|
@@ -2997,6 +3258,7 @@ function doctor() {
|
|
|
2997
3258
|
harnesses: {},
|
|
2998
3259
|
sinks: [],
|
|
2999
3260
|
timing: timing(),
|
|
3261
|
+
hook_wall_ms: hookWall(),
|
|
3000
3262
|
warnings: []
|
|
3001
3263
|
};
|
|
3002
3264
|
const warnings = report.warnings;
|
|
@@ -3006,8 +3268,15 @@ function doctor() {
|
|
|
3006
3268
|
);
|
|
3007
3269
|
for (const a of ADAPTERS) {
|
|
3008
3270
|
if (!a.detect()) continue;
|
|
3009
|
-
const st = a.installed(hc);
|
|
3010
|
-
report.harnesses[a.name] = st.map((r) => ({
|
|
3271
|
+
const st = a.installed(hc, process.env, { lean: isLean(cfg) });
|
|
3272
|
+
report.harnesses[a.name] = st.map((r) => ({
|
|
3273
|
+
file: tilde(r.file),
|
|
3274
|
+
state: r.state,
|
|
3275
|
+
...r.note ? { note: r.note } : {}
|
|
3276
|
+
}));
|
|
3277
|
+
for (const r of st)
|
|
3278
|
+
if (r.state === "inactive")
|
|
3279
|
+
warnings.push(`${a.name}: hooks are written but the harness will not run them \u2014 ${r.note ?? ""}`);
|
|
3011
3280
|
for (const r of st)
|
|
3012
3281
|
if (r.state === "stale")
|
|
3013
3282
|
warnings.push(
|
|
@@ -3030,9 +3299,11 @@ function doctor() {
|
|
|
3030
3299
|
paused: s.paused ?? null,
|
|
3031
3300
|
token: s.token ? `${s.token.slice(0, 4)}\u2026` : null
|
|
3032
3301
|
});
|
|
3033
|
-
const
|
|
3034
|
-
if (
|
|
3035
|
-
warnings.push(
|
|
3302
|
+
const w = report.hook_wall_ms;
|
|
3303
|
+
if (w && w.p50 > 150)
|
|
3304
|
+
warnings.push(
|
|
3305
|
+
`the harness waits ${w.p50} ms per hook (p50, target < 150): that is Node's own startup on this machine. A lean install (no sink above tier 0) hooks fewer events.`
|
|
3306
|
+
);
|
|
3036
3307
|
if (has("--json")) {
|
|
3037
3308
|
out(JSON.stringify(report, null, 2));
|
|
3038
3309
|
return;
|
|
@@ -3045,8 +3316,14 @@ function doctor() {
|
|
|
3045
3316
|
out(
|
|
3046
3317
|
` sink ${s.name.padEnd(16)} tier ${s.tier}${s.max_tier !== null ? ` (receiver max ${s.max_tier})` : ""}${s.paused ? ` PAUSED ${s.paused}` : ""}`
|
|
3047
3318
|
);
|
|
3048
|
-
|
|
3049
|
-
|
|
3319
|
+
const t = report.timing;
|
|
3320
|
+
out(
|
|
3321
|
+
w ? ` hook wall clock (what the harness waits): p50 ${w.p50} ms \xB7 p95 ${w.p95} ms over ${w.n} spawns` : " hook wall clock: could not spawn dist/hook.js"
|
|
3322
|
+
);
|
|
3323
|
+
out(
|
|
3324
|
+
t ? ` hook in-process: p50 ${t.p50} ms \xB7 p95 ${t.p95} ms over the last ${t.n} runs` : " hook in-process: no runs yet"
|
|
3325
|
+
);
|
|
3326
|
+
for (const w2 of warnings) out(` ! ${w2}`);
|
|
3050
3327
|
if (!warnings.length) out(" no warnings");
|
|
3051
3328
|
}
|
|
3052
3329
|
async function tail() {
|
|
@@ -3120,7 +3397,7 @@ async function workerFromArgv() {
|
|
|
3120
3397
|
if (!f) return;
|
|
3121
3398
|
const job = JSON.parse(readFileSync9(f, "utf8"));
|
|
3122
3399
|
try {
|
|
3123
|
-
|
|
3400
|
+
unlinkSync5(f);
|
|
3124
3401
|
} catch {
|
|
3125
3402
|
}
|
|
3126
3403
|
await runJob(job);
|
|
@@ -3134,7 +3411,7 @@ async function update() {
|
|
|
3134
3411
|
if (j.version === VERSION) return out(` sessionpipe ${VERSION} is current`);
|
|
3135
3412
|
out(` ${j.version} is available (you run ${VERSION}); installing into this copy's prefix\u2026`);
|
|
3136
3413
|
const prefix = prefixOf(distDir);
|
|
3137
|
-
|
|
3414
|
+
execFileSync4("npm", ["install", "-g", `sessionpipe@${j.version}`, ...prefix ? ["--prefix", prefix] : []], {
|
|
3138
3415
|
stdio: "inherit"
|
|
3139
3416
|
});
|
|
3140
3417
|
out(" re-arming hooks\u2026");
|