sessionpipe 0.1.0 → 0.2.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/dist/index.js CHANGED
@@ -1,8 +1,11 @@
1
- // sessionpipe 0.1.0 · Apache-2.0 · https://sessionpipe.org
1
+ // sessionpipe 0.2.0 · Apache-2.0 · https://sessionpipe.org
2
2
 
3
3
  // src/http-sink.ts
4
- import { createHmac, randomUUID } from "node:crypto";
4
+ import { createHash, createHmac } from "node:crypto";
5
5
  var TIMEOUT_MS = 15e3;
6
+ function webhookId(events) {
7
+ return `msg_${createHash("sha256").update(events.map((e) => e.id).join(",")).digest("base64url").slice(0, 32)}`;
8
+ }
6
9
  var HttpSink = class {
7
10
  constructor(cfg, tier) {
8
11
  this.cfg = cfg;
@@ -25,7 +28,7 @@ var HttpSink = class {
25
28
  const headers = { "content-type": "application/json", "sessionpipe-protocol": "1" };
26
29
  if (this.cfg.token) headers.authorization = `Bearer ${this.cfg.token}`;
27
30
  if (this.cfg.secret?.startsWith("whsec_")) {
28
- const id = `msg_${randomUUID()}`;
31
+ const id = webhookId(events);
29
32
  const ts = Math.floor(Date.now() / 1e3);
30
33
  const key = Buffer.from(this.cfg.secret.slice(6), "base64");
31
34
  const sig = createHmac("sha256", key).update(`${id}.${ts}.${body}`).digest("base64");
@@ -86,12 +89,31 @@ var HttpSink = class {
86
89
  };
87
90
 
88
91
  // src/run.ts
89
- import { appendFileSync as appendFileSync3, existsSync as existsSync8, mkdirSync as mkdirSync7, readFileSync as readFileSync8, renameSync as renameSync2, statSync as statSync5, writeFileSync as writeFileSync6 } from "node:fs";
90
- import os2 from "node:os";
91
- import path11 from "node:path";
92
+ import {
93
+ appendFileSync as appendFileSync3,
94
+ existsSync as existsSync8,
95
+ mkdirSync as mkdirSync7,
96
+ readdirSync as readdirSync6,
97
+ readFileSync as readFileSync8,
98
+ renameSync as renameSync2,
99
+ statSync as statSync5,
100
+ unlinkSync as unlinkSync4,
101
+ writeFileSync as writeFileSync6
102
+ } from "node:fs";
103
+ import os3 from "node:os";
104
+ import path12 from "node:path";
92
105
 
93
106
  // ../core/src/adapters/antigravity.ts
94
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, realpathSync, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
107
+ import {
108
+ existsSync as existsSync2,
109
+ mkdirSync as mkdirSync2,
110
+ readdirSync as readdirSync2,
111
+ readFileSync as readFileSync2,
112
+ realpathSync,
113
+ statSync as statSync2,
114
+ unlinkSync as unlinkSync2,
115
+ writeFileSync as writeFileSync2
116
+ } from "node:fs";
95
117
  import path4 from "node:path";
96
118
 
97
119
  // ../core/src/paths.ts
@@ -318,28 +340,67 @@ function readAntigravity(lines, from) {
318
340
  }
319
341
 
320
342
  // ../core/src/adapters/claude-shaped.ts
321
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
343
+ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
322
344
  import path3 from "node:path";
323
345
  var OURS = /sessionpipe[^"\n]*[\\/]hook\.js"?\s+[a-z][a-z-]*\s+[A-Za-z]+\s*$/;
324
346
  var isOurs = (h) => Array.isArray(h?.hooks) && h.hooks.some((x) => typeof x.command === "string" && OURS.test(x.command));
325
- function readJson(file) {
347
+ function readJsonFile(file) {
348
+ let raw;
326
349
  try {
327
- return JSON.parse(readFileSync(file, "utf8"));
350
+ raw = readFileSync(file, "utf8");
328
351
  } catch {
329
- return {};
352
+ return { ok: true, existed: false, value: {}, raw: "", indent: " ", eol: "\n", finalNewline: true };
330
353
  }
354
+ let value;
355
+ try {
356
+ value = JSON.parse(raw);
357
+ } catch {
358
+ const hint = /^\s*\/\/|\/\*/m.test(raw) ? "not plain JSON (it has comments)" : "not plain JSON";
359
+ return { ok: false, existed: true, reason: hint };
360
+ }
361
+ if (!value || typeof value !== "object" || Array.isArray(value))
362
+ return { ok: false, existed: true, reason: "not a JSON object" };
363
+ const eol = raw.includes("\r\n") ? "\r\n" : "\n";
364
+ const m = /^( +|\t+)\S/m.exec(raw);
365
+ const indent = m?.[1] ?? " ";
366
+ return {
367
+ ok: true,
368
+ existed: true,
369
+ value,
370
+ raw,
371
+ indent,
372
+ eol,
373
+ finalNewline: /\r?\n$/.test(raw)
374
+ };
375
+ }
376
+ function formatJson(value, f) {
377
+ const text = JSON.stringify(value, null, f.indent).replace(/\n/g, f.eol);
378
+ return f.finalNewline ? text + f.eol : text;
331
379
  }
332
380
  function entryFor(event, harness, cmd, o) {
333
381
  const matcher = o.matcherFor(event);
334
382
  return {
335
383
  ...matcher !== void 0 ? { matcher } : {},
336
- hooks: [{ type: "command", command: cmd([harness, event]).command, timeout: o.timeout }]
384
+ hooks: [{ type: "command", command: cmd([harness, event]).command, timeout: o.timeoutFor?.(event) ?? o.timeout }]
337
385
  };
338
386
  }
339
387
  function installClaudeShaped(file, harness, events, cmd, o) {
340
- const settings2 = readJson(file);
388
+ const f = readJsonFile(file);
389
+ if (!f.ok) return { file, changed: false, skipped: true, note: `${f.reason}; not touched` };
390
+ const settings2 = f.value;
341
391
  const hooks = settings2.hooks && typeof settings2.hooks === "object" ? settings2.hooks : {};
342
392
  let changed = false;
393
+ const want = new Set(events);
394
+ for (const ev of Object.keys(hooks)) {
395
+ if (want.has(ev)) continue;
396
+ const list = Array.isArray(hooks[ev]) ? hooks[ev] : [];
397
+ const kept = list.filter((h) => !isOurs(h));
398
+ if (kept.length !== list.length) {
399
+ changed = true;
400
+ if (kept.length) hooks[ev] = kept;
401
+ else delete hooks[ev];
402
+ }
403
+ }
343
404
  for (const ev of events) {
344
405
  const list = Array.isArray(hooks[ev]) ? hooks[ev] : [];
345
406
  const entry2 = entryFor(ev, harness, cmd, o);
@@ -358,19 +419,19 @@ function installClaudeShaped(file, harness, events, cmd, o) {
358
419
  if (changed) {
359
420
  settings2.hooks = hooks;
360
421
  mkdirSync(path3.dirname(file), { recursive: true });
361
- writeFileSync(file, `${JSON.stringify(settings2, null, 2)}
362
- `);
422
+ writeFileSync(file, formatJson(settings2, f));
363
423
  }
364
- return { file, changed };
424
+ return { file, changed, created: !f.existed && changed };
365
425
  }
366
- function uninstallClaudeShaped(file, events) {
426
+ function uninstallClaudeShaped(file, events, opts = {}) {
367
427
  if (!existsSync(file)) return { file, changed: false };
368
- const raw = readFileSync(file, "utf8");
369
- const settings2 = readJson(file);
428
+ const f = readJsonFile(file);
429
+ if (!f.ok) return { file, changed: false, skipped: true, note: `${f.reason}; not touched` };
430
+ const settings2 = f.value;
370
431
  const hooks = settings2.hooks;
371
432
  if (!hooks || typeof hooks !== "object") return { file, changed: false };
372
433
  let changed = false;
373
- for (const ev of events) {
434
+ for (const ev of [...events, ...Object.keys(hooks)]) {
374
435
  const list = Array.isArray(hooks[ev]) ? hooks[ev] : [];
375
436
  const kept = list.filter((h) => !isOurs(h));
376
437
  if (kept.length !== list.length) {
@@ -381,13 +442,17 @@ function uninstallClaudeShaped(file, events) {
381
442
  }
382
443
  if (!changed) return { file, changed: false };
383
444
  if (!Object.keys(hooks).length) delete settings2.hooks;
384
- const next = `${JSON.stringify(settings2, null, 2)}
385
- `;
386
- if (next !== raw) writeFileSync(file, next);
445
+ if (opts.created && !Object.keys(settings2).length) {
446
+ unlinkSync(file);
447
+ return { file, changed: true, note: "removed (sessionpipe created it)" };
448
+ }
449
+ const next = formatJson(settings2, f);
450
+ if (next !== f.raw) writeFileSync(file, next);
387
451
  return { file, changed: true };
388
452
  }
389
453
  function installedClaudeShaped(file, harness, events, cmd, o) {
390
- const hooks = readJson(file).hooks ?? {};
454
+ const f = readJsonFile(file);
455
+ const hooks = f.ok ? f.value.hooks ?? {} : {};
391
456
  let present = 0;
392
457
  let current = 0;
393
458
  for (const ev of events) {
@@ -396,6 +461,9 @@ function installedClaudeShaped(file, harness, events, cmd, o) {
396
461
  if (mine.length) present++;
397
462
  if (mine.length === 1 && JSON.stringify(mine[0]) === JSON.stringify(entryFor(ev, harness, cmd, o))) current++;
398
463
  }
464
+ for (const ev of Object.keys(hooks))
465
+ if (!events.includes(ev) && (Array.isArray(hooks[ev]) ? hooks[ev] : []).some((h) => isOurs(h)))
466
+ return "stale";
399
467
  if (!present) return "missing";
400
468
  return current === events.length ? "current" : "stale";
401
469
  }
@@ -410,10 +478,13 @@ var projectsDir = (env) => path4.join(gemini(env), "config", "projects");
410
478
  var dataDirs = (env) => ["antigravity", "antigravity-cli", "antigravity-ide"].map((d) => path4.join(gemini(env), d));
411
479
  var ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
412
480
  var validTool = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
413
- function entry(cmd) {
481
+ function entry(cmd, lean) {
414
482
  const handler = (e) => ({ type: "command", command: cmd([NAME, e]).command, timeout: 5 });
415
483
  const out = { enabled: true };
416
- for (const e of AG_EVENTS) out[e] = /Tool/.test(e) ? [{ matcher: "*", hooks: [handler(e)] }] : [handler(e)];
484
+ for (const e of AG_EVENTS) {
485
+ if (lean && e === "PreToolUse") continue;
486
+ out[e] = /Tool/.test(e) ? [{ matcher: "*", hooks: [handler(e)] }] : [handler(e)];
487
+ }
417
488
  return out;
418
489
  }
419
490
  var antigravity = {
@@ -421,34 +492,49 @@ var antigravity = {
421
492
  events: AG_EVENTS,
422
493
  detect: (env = process.env) => existsSync2(path4.join(gemini(env), "config")) || dataDirs(env).some((d) => existsSync2(d)),
423
494
  configFiles: (env = process.env) => [hooksFile(env)],
424
- install(cmd, env = process.env) {
495
+ install(cmd, env = process.env, opts = {}) {
425
496
  const file = hooksFile(env);
426
- const hooks = readJson(file);
427
- const e = entry(cmd);
497
+ const f = readJsonFile(file);
498
+ if (!f.ok) return [{ file, changed: false, skipped: true, note: `${f.reason}; not touched` }];
499
+ const hooks = f.value;
500
+ const e = entry(cmd, opts.lean);
428
501
  if (JSON.stringify(hooks[HOOK_NAME]) === JSON.stringify(e)) return [{ file, changed: false }];
429
502
  hooks[HOOK_NAME] = e;
430
503
  mkdirSync2(path4.dirname(file), { recursive: true });
431
- writeFileSync2(file, `${JSON.stringify(hooks, null, 2)}
432
- `);
504
+ writeFileSync2(file, formatJson(hooks, f));
433
505
  return [
434
- { file, changed: true, note: "hooks.json is read when a conversation starts; open ones report after a restart" }
506
+ {
507
+ file,
508
+ changed: true,
509
+ created: !f.existed,
510
+ note: "hooks.json is read when a conversation starts; open ones report after a restart"
511
+ }
435
512
  ];
436
513
  },
437
- uninstall(env = process.env) {
514
+ uninstall(env = process.env, opts = {}) {
438
515
  const file = hooksFile(env);
439
516
  if (!existsSync2(file)) return [{ file, changed: false }];
440
- const hooks = readJson(file);
517
+ const f = readJsonFile(file);
518
+ if (!f.ok) return [{ file, changed: false, skipped: true, note: `${f.reason}; not touched` }];
519
+ const hooks = f.value;
441
520
  if (!(HOOK_NAME in hooks)) return [{ file, changed: false }];
442
521
  delete hooks[HOOK_NAME];
443
- writeFileSync2(file, `${JSON.stringify(hooks, null, 2)}
444
- `);
522
+ if (opts.created?.includes(file) && !Object.keys(hooks).length) {
523
+ unlinkSync2(file);
524
+ return [{ file, changed: true, note: "removed (sessionpipe created it)" }];
525
+ }
526
+ writeFileSync2(file, formatJson(hooks, f));
445
527
  return [{ file, changed: true }];
446
528
  },
447
- installed(cmd, env = process.env) {
529
+ installed(cmd, env = process.env, opts = {}) {
448
530
  const file = hooksFile(env);
449
- const have = readJson(file)[HOOK_NAME];
531
+ const f = readJsonFile(file);
532
+ const have = f.ok ? f.value[HOOK_NAME] : void 0;
450
533
  return [
451
- { file, state: !have ? "missing" : JSON.stringify(have) === JSON.stringify(entry(cmd)) ? "current" : "stale" }
534
+ {
535
+ file,
536
+ state: !have ? "missing" : JSON.stringify(have) === JSON.stringify(entry(cmd, opts.lean)) ? "current" : "stale"
537
+ }
452
538
  ];
453
539
  },
454
540
  fromHook(input) {
@@ -662,7 +748,7 @@ function antigravityFacts(file, opts) {
662
748
 
663
749
  // ../core/src/adapters/claude-code.ts
664
750
  import { execFileSync as execFileSync2 } from "node:child_process";
665
- import { createHash } from "node:crypto";
751
+ import { createHash as createHash2 } from "node:crypto";
666
752
  import { existsSync as existsSync3, readdirSync as readdirSync3, readFileSync as readFileSync3, statSync as statSync3 } from "node:fs";
667
753
  import path5 from "node:path";
668
754
 
@@ -714,9 +800,18 @@ var CLAUDE_EVENTS = [
714
800
  "SubagentStop",
715
801
  "PreCompact",
716
802
  "PostCompact",
717
- "SessionEnd"
803
+ "SessionEnd",
804
+ "StopFailure"
718
805
  ];
719
- var OPTS = { matcherFor: (e) => /Tool|Permission/.test(e) ? "" : void 0, timeout: 5 };
806
+ var LEAN_SKIP = /* @__PURE__ */ new Set(["PreToolUse", "PostToolUseFailure"]);
807
+ var eventsFor = (lean) => lean ? CLAUDE_EVENTS.filter((e) => !LEAN_SKIP.has(e)) : [...CLAUDE_EVENTS];
808
+ var OPTS = {
809
+ matcherFor: (e) => /Tool|Permission/.test(e) ? "" : void 0,
810
+ timeout: 5,
811
+ // The hook waits up to 125 s for a verified control answer, and only when control
812
+ // is paired on this machine; everywhere else it exits in milliseconds as before.
813
+ timeoutFor: (e) => e === "PermissionRequest" ? 130 : void 0
814
+ };
720
815
  var DESKTOP_ID = /^local_[A-Za-z0-9-]{1,64}$/;
721
816
  function claudeDirs(env = process.env, home = HOME) {
722
817
  const out = [];
@@ -747,11 +842,13 @@ var claudeCode = {
747
842
  events: CLAUDE_EVENTS,
748
843
  detect: (env = process.env) => claudeDirs(env).some((d) => existsSync3(d)),
749
844
  configFiles: (env = process.env) => claudeDirs(env).map(settingsIn),
750
- install: (cmd, env = process.env) => claudeDirs(env).map((d) => installClaudeShaped(settingsIn(d), NAME2, CLAUDE_EVENTS, cmd, OPTS)),
751
- uninstall: (env = process.env) => claudeDirs(env).map((d) => uninstallClaudeShaped(settingsIn(d), CLAUDE_EVENTS)),
752
- installed: (cmd, env = process.env) => claudeDirs(env).map((d) => ({
845
+ install: (cmd, env = process.env, opts = {}) => claudeDirs(env).map((d) => installClaudeShaped(settingsIn(d), NAME2, eventsFor(opts.lean), cmd, OPTS)),
846
+ uninstall: (env = process.env, opts = {}) => claudeDirs(env).map(
847
+ (d) => uninstallClaudeShaped(settingsIn(d), CLAUDE_EVENTS, { created: opts.created?.includes(settingsIn(d)) })
848
+ ),
849
+ installed: (cmd, env = process.env, opts = {}) => claudeDirs(env).map((d) => ({
753
850
  file: settingsIn(d),
754
- state: installedClaudeShaped(settingsIn(d), NAME2, CLAUDE_EVENTS, cmd, OPTS)
851
+ state: installedClaudeShaped(settingsIn(d), NAME2, eventsFor(opts.lean), cmd, OPTS)
755
852
  })),
756
853
  fromHook(input) {
757
854
  const event = input.argv[1] ?? "";
@@ -771,7 +868,7 @@ var claudeCode = {
771
868
  const call_id = typeof s.tool_use_id === "string" ? s.tool_use_id : void 0;
772
869
  const ms = typeof s.duration_ms === "number" ? Math.round(s.duration_ms) : void 0;
773
870
  const agent_id = typeof s.agent_id === "string" ? s.agent_id : void 0;
774
- const permissionId = () => `perm-${createHash("sha256").update(`${turn_id ?? ""}|${tool ?? ""}|${JSON.stringify(s.tool_input ?? null)}`).digest("hex").slice(0, 16)}`;
871
+ const permissionId = () => `perm-${createHash2("sha256").update(`${turn_id ?? ""}|${tool ?? ""}|${JSON.stringify(s.tool_input ?? null)}`).digest("hex").slice(0, 16)}`;
775
872
  const ev = (type, data) => ({ type, data: prune2(data), harnessEvent: event });
776
873
  const events = [];
777
874
  switch (event) {
@@ -842,6 +939,15 @@ var claudeCode = {
842
939
  case "Stop":
843
940
  events.push(ev("turn.ended", { turn_id, reason: "stop" }));
844
941
  break;
942
+ case "StopFailure":
943
+ events.push(
944
+ ev("turn.ended", {
945
+ turn_id,
946
+ reason: "error",
947
+ error: typeof s.error === "string" ? s.error.slice(0, 200) : void 0
948
+ })
949
+ );
950
+ break;
845
951
  case "SubagentStart":
846
952
  events.push(
847
953
  ev("subagent.started", {
@@ -1149,9 +1255,9 @@ function claudeFacts(file, sessionId, hints, state) {
1149
1255
  }
1150
1256
  const name = live?.name ? { title: live.name, source: live.name_source } : typeof mem.name === "string" ? { title: mem.name, source: mem.name_source } : null;
1151
1257
  const pick = name && name.source === "custom" && name || custom && { title: custom, source: "custom" } || name || ai && { title: ai, source: "harness" };
1152
- let ask = typeof mem.first_ask === "string" ? mem.first_ask : firstAsk(H);
1153
- if (!ask && size > 64 * 1024 && !mem.first_ask_none) {
1154
- ask = scanLines(file, (l) => {
1258
+ let ask2 = typeof mem.first_ask === "string" ? mem.first_ask : firstAsk(H);
1259
+ if (!ask2 && size > 64 * 1024 && !mem.first_ask_none) {
1260
+ ask2 = scanLines(file, (l) => {
1155
1261
  try {
1156
1262
  const t = askText(JSON.parse(l));
1157
1263
  return t ? appTitle(t) : null;
@@ -1159,17 +1265,17 @@ function claudeFacts(file, sessionId, hints, state) {
1159
1265
  return null;
1160
1266
  }
1161
1267
  });
1162
- if (!ask && size > 16 * 1024 * 1024) state.save(NAME2, id, { first_ask_none: true });
1268
+ if (!ask2 && size > 16 * 1024 * 1024) state.save(NAME2, id, { first_ask_none: true });
1163
1269
  }
1164
- if (ask) {
1165
- out.first_ask = ask;
1166
- if (!mem.first_ask) state.save(NAME2, id, { first_ask: ask });
1270
+ if (ask2) {
1271
+ out.first_ask = ask2;
1272
+ if (!mem.first_ask) state.save(NAME2, id, { first_ask: ask2 });
1167
1273
  }
1168
1274
  if (pick) {
1169
1275
  out.title = String(pick.title).slice(0, 120);
1170
1276
  out.title_source = pick.source;
1171
- } else if (ask) {
1172
- out.title = ask;
1277
+ } else if (ask2) {
1278
+ out.title = ask2;
1173
1279
  out.title_source = "first-ask";
1174
1280
  }
1175
1281
  const acct = claudeAccount(dir);
@@ -1353,6 +1459,7 @@ var CODEX_EVENTS = [
1353
1459
  "SessionEnd"
1354
1460
  ];
1355
1461
  var OPTS2 = { matcherFor: (e) => /Tool|Permission/.test(e) ? "" : void 0, timeout: 5 };
1462
+ var eventsFor2 = (lean) => lean ? CODEX_EVENTS.filter((e) => e !== "PreToolUse") : [...CODEX_EVENTS];
1356
1463
  var codexHome = (env) => env.CODEX_HOME || path6.join(HOME, ".codex");
1357
1464
  var hooksFile2 = (env) => path6.join(codexHome(env), "hooks.json");
1358
1465
  var configFile2 = (env) => path6.join(codexHome(env), "config.toml");
@@ -1401,7 +1508,8 @@ function installNotify(cmd, env) {
1401
1508
  return {
1402
1509
  file,
1403
1510
  changed: false,
1404
- note: `left alone: notify runs ${n.raw.slice(0, 80)} (hooks.json carries the events; the notify fallback is off)`
1511
+ skipped: true,
1512
+ 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`
1405
1513
  };
1406
1514
  }
1407
1515
  write(withNotify(text, line));
@@ -1458,14 +1566,21 @@ var codex = {
1458
1566
  events: CODEX_EVENTS,
1459
1567
  detect: (env = process.env) => existsSync4(codexHome(env)),
1460
1568
  configFiles: (env = process.env) => [hooksFile2(env), configFile2(env)],
1461
- install: (cmd, env = process.env) => [
1462
- installClaudeShaped(hooksFile2(env), NAME3, CODEX_EVENTS, cmd, OPTS2),
1569
+ install: (cmd, env = process.env, opts = {}) => [
1570
+ installClaudeShaped(hooksFile2(env), NAME3, eventsFor2(opts.lean), cmd, OPTS2),
1463
1571
  installNotify(cmd, env)
1464
1572
  ],
1465
- uninstall: (env = process.env) => [uninstallClaudeShaped(hooksFile2(env), CODEX_EVENTS), uninstallNotify(env)],
1466
- installed: (cmd, env = process.env) => {
1573
+ uninstall: (env = process.env, opts = {}) => [
1574
+ uninstallClaudeShaped(hooksFile2(env), CODEX_EVENTS, { created: opts.created?.includes(hooksFile2(env)) }),
1575
+ uninstallNotify(env)
1576
+ ],
1577
+ installed: (cmd, env = process.env, opts = {}) => {
1467
1578
  const out = [
1468
- { file: hooksFile2(env), state: installedClaudeShaped(hooksFile2(env), NAME3, CODEX_EVENTS, cmd, OPTS2) }
1579
+ {
1580
+ file: hooksFile2(env),
1581
+ state: installedClaudeShaped(hooksFile2(env), NAME3, eventsFor2(opts.lean), cmd, OPTS2),
1582
+ note: "Codex runs non-managed hooks only after the review in `codex` (/hooks); until then nothing is reported unless the notify fallback is ours"
1583
+ }
1469
1584
  ];
1470
1585
  let text = "";
1471
1586
  try {
@@ -1653,6 +1768,7 @@ function codexFacts(file) {
1653
1768
  }
1654
1769
 
1655
1770
  // ../core/src/adapters/gemini-cli.ts
1771
+ import { execFileSync as execFileSync3 } from "node:child_process";
1656
1772
  import { existsSync as existsSync5, readFileSync as readFileSync5 } from "node:fs";
1657
1773
  import path7 from "node:path";
1658
1774
  var NAME4 = "gemini-cli";
@@ -1670,17 +1786,61 @@ var OPTS3 = { matcherFor: (e) => /Tool/.test(e) ? ".*" : void 0, timeout: 5e3 };
1670
1786
  var geminiDir = (env) => env.GEMINI_CLI_HOME || path7.join(HOME, ".gemini");
1671
1787
  var settings = (env) => path7.join(geminiDir(env), "settings.json");
1672
1788
  var validTool4 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
1789
+ var eventsFor3 = (lean) => lean ? GEMINI_EVENTS.filter((e) => e !== "BeforeTool") : [...GEMINI_EVENTS];
1790
+ function geminiHooksActive(settingsFile) {
1791
+ let tools;
1792
+ try {
1793
+ tools = JSON.parse(readFileSync5(settingsFile, "utf8")).tools;
1794
+ } catch {
1795
+ }
1796
+ if (tools?.enableHooks === true) return { active: true };
1797
+ if (tools?.enableHooks === false)
1798
+ return { active: false, note: "inactive: tools.enableHooks is false in settings.json" };
1799
+ let version = "";
1800
+ try {
1801
+ version = execFileSync3("gemini", ["--version"], {
1802
+ encoding: "utf8",
1803
+ timeout: 4e3,
1804
+ stdio: ["ignore", "pipe", "ignore"]
1805
+ }).trim();
1806
+ } catch {
1807
+ return {
1808
+ active: true,
1809
+ note: "could not run `gemini --version`; hooks need tools.enableHooks on versions before 0.46"
1810
+ };
1811
+ }
1812
+ const m = /(\d+)\.(\d+)/.exec(version);
1813
+ if (m && (Number(m[1]) > 0 || Number(m[2]) >= 46)) return { active: true };
1814
+ return {
1815
+ active: false,
1816
+ note: `inactive: Gemini CLI ${version || "?"} runs hooks only with "tools": {"enableHooks": true} in settings.json`
1817
+ };
1818
+ }
1673
1819
  var geminiCli = {
1674
1820
  name: NAME4,
1675
1821
  events: GEMINI_EVENTS,
1676
1822
  // ~/.gemini also belongs to Antigravity; Gemini CLI is here when its settings or its chat logs exist.
1677
1823
  detect: (env = process.env) => existsSync5(settings(env)) || existsSync5(path7.join(geminiDir(env), "tmp")),
1678
1824
  configFiles: (env = process.env) => [settings(env)],
1679
- install: (cmd, env = process.env) => [installClaudeShaped(settings(env), NAME4, GEMINI_EVENTS, cmd, OPTS3)],
1680
- uninstall: (env = process.env) => [uninstallClaudeShaped(settings(env), GEMINI_EVENTS)],
1681
- installed: (cmd, env = process.env) => [
1682
- { file: settings(env), state: installedClaudeShaped(settings(env), NAME4, GEMINI_EVENTS, cmd, OPTS3) }
1825
+ install: (cmd, env = process.env, opts = {}) => {
1826
+ const r = installClaudeShaped(settings(env), NAME4, eventsFor3(opts.lean), cmd, OPTS3);
1827
+ const a = geminiHooksActive(settings(env));
1828
+ return [a.note && !r.skipped ? { ...r, note: [r.note, a.note].filter(Boolean).join("; ") } : r];
1829
+ },
1830
+ uninstall: (env = process.env, opts = {}) => [
1831
+ uninstallClaudeShaped(settings(env), GEMINI_EVENTS, { created: opts.created?.includes(settings(env)) })
1683
1832
  ],
1833
+ installed: (cmd, env = process.env, opts = {}) => {
1834
+ const state = installedClaudeShaped(settings(env), NAME4, eventsFor3(opts.lean), cmd, OPTS3);
1835
+ const a = geminiHooksActive(settings(env));
1836
+ return [
1837
+ {
1838
+ file: settings(env),
1839
+ state: state === "current" && !a.active ? "inactive" : state,
1840
+ ...a.note ? { note: a.note } : {}
1841
+ }
1842
+ ];
1843
+ },
1684
1844
  fromHook(input) {
1685
1845
  const event = input.argv[1] ?? "";
1686
1846
  let s = {};
@@ -1891,7 +2051,7 @@ import {
1891
2051
  readSync as readSync2,
1892
2052
  renameSync,
1893
2053
  statSync as statSync4,
1894
- unlinkSync,
2054
+ unlinkSync as unlinkSync3,
1895
2055
  writeFileSync as writeFileSync5
1896
2056
  } from "node:fs";
1897
2057
  import path9 from "node:path";
@@ -1919,8 +2079,8 @@ var Outbox = class {
1919
2079
  } catch {
1920
2080
  n = this.lastSeqInFile(ref) + 1;
1921
2081
  }
1922
- mkdirSync5(path9.dirname(f), { recursive: true });
1923
- writeFileSync5(f, String(n + 1));
2082
+ mkdirSync5(path9.dirname(f), { recursive: true, mode: 448 });
2083
+ writeFileSync5(f, String(n + 1), { mode: 384 });
1924
2084
  return n;
1925
2085
  }
1926
2086
  lastSeqInFile(ref) {
@@ -1938,9 +2098,9 @@ var Outbox = class {
1938
2098
  }
1939
2099
  append(ref, events) {
1940
2100
  if (!events.length) return;
1941
- mkdirSync5(this.dir(ref.harness), { recursive: true });
2101
+ mkdirSync5(this.dir(ref.harness), { recursive: true, mode: 448 });
1942
2102
  appendFileSync(this.file(ref), `${events.map((e) => JSON.stringify(e)).join("\n")}
1943
- `);
2103
+ `, { mode: 384 });
1944
2104
  }
1945
2105
  /** Every session file, newest first. */
1946
2106
  sessions() {
@@ -2004,9 +2164,9 @@ var Outbox = class {
2004
2164
  const f = this.file(ref);
2005
2165
  try {
2006
2166
  if (statSync4(f).mtimeMs < cutoff) {
2007
- unlinkSync(f);
2167
+ unlinkSync3(f);
2008
2168
  try {
2009
- unlinkSync(this.seqFile(ref));
2169
+ unlinkSync3(this.seqFile(ref));
2010
2170
  } catch {
2011
2171
  }
2012
2172
  n++;
@@ -2019,7 +2179,7 @@ var Outbox = class {
2019
2179
  delete(ref) {
2020
2180
  for (const f of [this.file(ref), this.seqFile(ref)]) {
2021
2181
  try {
2022
- unlinkSync(f);
2182
+ unlinkSync3(f);
2023
2183
  } catch {
2024
2184
  }
2025
2185
  }
@@ -2044,9 +2204,9 @@ var Cursors = class {
2044
2204
  }
2045
2205
  write(c) {
2046
2206
  const f = this.file();
2047
- mkdirSync5(path9.dirname(f), { recursive: true });
2207
+ mkdirSync5(path9.dirname(f), { recursive: true, mode: 448 });
2048
2208
  const tmp = `${f}.${process.pid}.tmp`;
2049
- writeFileSync5(tmp, JSON.stringify(c));
2209
+ writeFileSync5(tmp, JSON.stringify(c), { mode: 384 });
2050
2210
  renameSync(tmp, f);
2051
2211
  }
2052
2212
  key(ref) {
@@ -2055,10 +2215,10 @@ var Cursors = class {
2055
2215
  };
2056
2216
  function takeLock(root, ref, staleMs = 5 * 6e4) {
2057
2217
  const dir = path9.join(root, "locks");
2058
- mkdirSync5(dir, { recursive: true });
2218
+ mkdirSync5(dir, { recursive: true, mode: 448 });
2059
2219
  const f = path9.join(dir, `${safeId(ref.harness)}-${safeId(ref.session)}.lock`);
2060
2220
  try {
2061
- if (Date.now() - statSync4(f).mtimeMs > staleMs) unlinkSync(f);
2221
+ if (Date.now() - statSync4(f).mtimeMs > staleMs) unlinkSync3(f);
2062
2222
  else return null;
2063
2223
  } catch {
2064
2224
  }
@@ -2069,14 +2229,14 @@ function takeLock(root, ref, staleMs = 5 * 6e4) {
2069
2229
  }
2070
2230
  return () => {
2071
2231
  try {
2072
- unlinkSync(f);
2232
+ unlinkSync3(f);
2073
2233
  } catch {
2074
2234
  }
2075
2235
  };
2076
2236
  }
2077
2237
 
2078
2238
  // ../core/src/privacy/pii.ts
2079
- import { createHash as createHash2 } from "node:crypto";
2239
+ import { createHash as createHash3 } from "node:crypto";
2080
2240
  var RULESET_PII = "pii@1";
2081
2241
  var EMAIL = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g;
2082
2242
  var PHONE = /(?<![\w.])(?:\+\d{1,3}[ .-]?)?(?:\(\d{2,4}\)[ .-]?)?\d{2,4}(?:[ .-]?\d{2,4}){1,3}(?!\w)/g;
@@ -2098,7 +2258,7 @@ function reducePii(text, home) {
2098
2258
  return out;
2099
2259
  }
2100
2260
  function hashMachine(name) {
2101
- return createHash2("sha256").update(name).digest("hex").slice(0, 8);
2261
+ return createHash3("sha256").update(name).digest("hex").slice(0, 8);
2102
2262
  }
2103
2263
  function reducePiiDeep(v, home) {
2104
2264
  if (typeof v === "string") return reducePii(v, home);
@@ -2205,7 +2365,7 @@ function redactAssignments(text) {
2205
2365
  function isSecretName(name) {
2206
2366
  if (/^[A-Z0-9_]+_KEY$/.test(name)) return true;
2207
2367
  const flat = name.toLowerCase().replace(/[_-]/g, "");
2208
- return /(?:pass(?:word|wd|phrase|code)?|pwd|secret|secretkey|secretaccesskey|clientsecret|apikey|accesskey|accesstoken|authtoken|refreshtoken|privatekey|token|credential|creds)s?$/.test(
2368
+ return /(?:pass(?:word|wd|phrase|code)|pwd|secret|secretkey|secretaccesskey|clientsecret|apikey|accesskey|accesstoken|authtoken|refreshtoken|privatekey|token|credential|creds)s?$/.test(
2209
2369
  flat
2210
2370
  );
2211
2371
  }
@@ -2294,7 +2454,7 @@ var DATA_ALLOW = {
2294
2454
  "session.backfill": { 0: ["started_at", "last_at", "turns"] },
2295
2455
  "session.forgotten": { 0: [] },
2296
2456
  "turn.started": { 0: ["turn_id"], 1: ["prompt_chars"] },
2297
- "turn.ended": { 0: ["turn_id", "reason", "ms"] },
2457
+ "turn.ended": { 0: ["turn_id", "reason", "ms"], 1: ["error"] },
2298
2458
  "turn.transcript": { 2: ["turn", "user", "assistant", "at"] },
2299
2459
  "tool.started": { 1: ["tool", "call_id", "turn_id", "agent_id"], 3: ["input"] },
2300
2460
  "tool.ended": { 1: ["tool", "call_id", "turn_id", "agent_id", "ms", "ok", "error"], 3: ["input", "output"] },
@@ -2415,11 +2575,72 @@ var StdoutSink = class {
2415
2575
  }
2416
2576
  };
2417
2577
 
2578
+ // src/control/local.ts
2579
+ import net from "node:net";
2580
+ import os2 from "node:os";
2581
+ import path11 from "node:path";
2582
+ function socketPath(state, env = process.env) {
2583
+ if (env.SESSIONPIPE_SOCKET) return env.SESSIONPIPE_SOCKET;
2584
+ if (process.platform === "win32") return `\\\\.\\pipe\\sessionpipe-control-${os2.userInfo().username}`;
2585
+ return path11.join(state, "control.sock");
2586
+ }
2587
+ function ask(sock, req, timeoutMs) {
2588
+ return new Promise((resolve) => {
2589
+ let buf = "";
2590
+ let done = false;
2591
+ const s = net.connect(sock);
2592
+ const finish = (r) => {
2593
+ if (done) return;
2594
+ done = true;
2595
+ clearTimeout(t);
2596
+ s.destroy();
2597
+ resolve(r);
2598
+ };
2599
+ const t = setTimeout(() => finish(null), timeoutMs);
2600
+ s.on("connect", () => s.write(`${JSON.stringify(req)}
2601
+ `));
2602
+ s.on("data", (d) => {
2603
+ buf += d.toString("utf8");
2604
+ const i = buf.indexOf("\n");
2605
+ if (i < 0) return;
2606
+ try {
2607
+ finish(JSON.parse(buf.slice(0, i)));
2608
+ } catch {
2609
+ finish(null);
2610
+ }
2611
+ });
2612
+ s.on("error", () => finish(null));
2613
+ s.on("close", () => finish(null));
2614
+ });
2615
+ }
2616
+
2418
2617
  // src/run.ts
2618
+ async function tellDaemon(state, job, session, transcript) {
2619
+ const sock = socketPath(state);
2620
+ if (process.platform !== "win32" && !existsSync8(sock)) return;
2621
+ await ask(
2622
+ sock,
2623
+ {
2624
+ op: "event",
2625
+ session: `claude-code:${session}`,
2626
+ event: job.event,
2627
+ cwd: job.cwd,
2628
+ ...transcript ? { transcript } : {}
2629
+ },
2630
+ 300
2631
+ );
2632
+ }
2633
+ function enqueueJob(state, job) {
2634
+ const jobs = jobsDir(state);
2635
+ mkdirSync7(jobs, { recursive: true, mode: 448 });
2636
+ const f = path12.join(jobs, `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}.json`);
2637
+ writeFileSync6(f, JSON.stringify(job), { mode: 384 });
2638
+ return f;
2639
+ }
2419
2640
  function log(state, line) {
2420
2641
  try {
2421
- mkdirSync7(state, { recursive: true });
2422
- const f = path11.join(state, "log");
2642
+ mkdirSync7(state, { recursive: true, mode: 448 });
2643
+ const f = path12.join(state, "log");
2423
2644
  try {
2424
2645
  if (statSync5(f).size > 1024 * 1024) renameSync2(f, `${f}.1`);
2425
2646
  } catch {
@@ -2430,7 +2651,7 @@ function log(state, line) {
2430
2651
  }
2431
2652
  }
2432
2653
  function factsState(state) {
2433
- const file = (h, s) => path11.join(state, "facts", safeId(h), `${safeId(s)}.json`);
2654
+ const file = (h, s) => path12.join(state, "facts", safeId(h), `${safeId(s)}.json`);
2434
2655
  return {
2435
2656
  get(h, s) {
2436
2657
  try {
@@ -2443,8 +2664,8 @@ function factsState(state) {
2443
2664
  const cur = this.get(h, s);
2444
2665
  const next = { ...cur, ...patch };
2445
2666
  if (JSON.stringify(next) === JSON.stringify(cur)) return;
2446
- mkdirSync7(path11.dirname(file(h, s)), { recursive: true });
2447
- writeFileSync6(file(h, s), JSON.stringify(next));
2667
+ mkdirSync7(path12.dirname(file(h, s)), { recursive: true, mode: 448 });
2668
+ writeFileSync6(file(h, s), JSON.stringify(next), { mode: 384 });
2448
2669
  }
2449
2670
  };
2450
2671
  }
@@ -2455,7 +2676,7 @@ function buildSinks(sinks) {
2455
2676
  const tier = Math.min(s.tier, s.max_tier ?? 3);
2456
2677
  if (s.url === "stdout") out.push(new StdoutSink(s.name, tier, !!s.pii));
2457
2678
  else if (s.url.startsWith("file:"))
2458
- out.push(new FileSink(s.name, s.url.slice(5).replace(/^~(?=\/|$)/, os2.homedir()), tier, !!s.pii));
2679
+ out.push(new FileSink(s.name, s.url.slice(5).replace(/^~(?=\/|$)/, os3.homedir()), tier, !!s.pii));
2459
2680
  else if (/^https?:\/\//.test(s.url)) out.push(new HttpSink(s, tier));
2460
2681
  }
2461
2682
  return out;
@@ -2467,14 +2688,17 @@ async function runJob(job, opts = {}) {
2467
2688
  if (!adapter) return { events: [], delivered: {} };
2468
2689
  const r = adapter.fromHook({ argv: job.argv, stdin: job.stdin, env: { ...process.env, ...job.env }, cwd: job.cwd });
2469
2690
  if (!r || !r.session.id) return { events: [], delivered: {} };
2691
+ if (job.harness === "claude-code") await tellDaemon(state, job, r.session.id, r.transcript);
2470
2692
  const outbox = new Outbox(state);
2471
2693
  const ref = { harness: job.harness, session: r.session.id };
2472
- const release = takeLock(state, ref);
2694
+ let release = takeLock(state, ref);
2695
+ for (let wait = 50; !release && wait <= 3200; wait *= 2) {
2696
+ await new Promise((res) => setTimeout(res, wait));
2697
+ release = takeLock(state, ref);
2698
+ }
2473
2699
  if (!release) {
2474
- log(state, `${job.harness} ${job.event}: session ${r.session.id} locked, re-queued`);
2475
- const jobs = path11.join(state, "jobs");
2476
- mkdirSync7(jobs, { recursive: true });
2477
- writeFileSync6(path11.join(jobs, `${Date.now().toString(36)}-requeue.json`), JSON.stringify(job));
2700
+ log(state, `${job.harness} ${job.event}: session ${r.session.id} locked for 6 s, re-queued`);
2701
+ enqueueJob(state, job);
2478
2702
  return { events: [], delivered: {} };
2479
2703
  }
2480
2704
  try {
@@ -2499,7 +2723,7 @@ async function runJob(job, opts = {}) {
2499
2723
  ...learned.branch ? { branch: learned.branch } : {},
2500
2724
  id: r.session.id,
2501
2725
  seq: 0,
2502
- machine: machineName(cfg, os2.hostname()),
2726
+ machine: machineName(cfg, os3.hostname()),
2503
2727
  ...cwdRaw ? { cwd: tilde(cwdRaw) } : {},
2504
2728
  ...r.session.model ? { model: r.session.model } : {}
2505
2729
  };
@@ -2538,7 +2762,10 @@ async function runJob(job, opts = {}) {
2538
2762
  if (read.turns.length)
2539
2763
  facts.save(job.harness, r.session.id, { transcript_line: read.line, transcript_turns: turnNo });
2540
2764
  }
2541
- outbox.append(ref, events);
2765
+ outbox.append(
2766
+ ref,
2767
+ events.map((e) => ({ ...e, session: redactDeep(e.session), data: redactDeep(e.data) }))
2768
+ );
2542
2769
  log(
2543
2770
  state,
2544
2771
  `${job.harness} ${job.event}: ${events.map((e) => e.type).join(",") || "nothing"} (${r.session.id.slice(0, 8)})`
@@ -2564,34 +2791,57 @@ async function flush(state, sinkConfigs, outbox, first) {
2564
2791
  const cursors = new Cursors(state, sink.name);
2565
2792
  const c = cursors.read();
2566
2793
  let sent = 0;
2794
+ let stalled = false;
2795
+ let batch = [];
2796
+ let pending = {};
2797
+ const send = async () => {
2798
+ if (!batch.length) {
2799
+ Object.assign(c, pending);
2800
+ pending = {};
2801
+ cursors.write(c);
2802
+ return true;
2803
+ }
2804
+ const res = await sink.send(batch);
2805
+ if (!res.ok) {
2806
+ log(state, `sink ${sink.name}: ${res.status ?? ""} ${res.error ?? ""} \u2192 ${res.action ?? "retry"}`);
2807
+ if (res.action === "pause" || res.action === "lower-tier") applySinkVerdict(sink.name, res);
2808
+ if (res.action !== "drop") {
2809
+ batch = [];
2810
+ pending = {};
2811
+ return false;
2812
+ }
2813
+ }
2814
+ sent += batch.length;
2815
+ Object.assign(c, pending);
2816
+ cursors.write(c);
2817
+ batch = [];
2818
+ pending = {};
2819
+ return true;
2820
+ };
2567
2821
  for (const ref of refs) {
2568
- if (sent >= MAX_PER_RUN) break;
2569
- let offset = c[cursors.key(ref)] ?? 0;
2570
- let stalled = false;
2571
- while (sent < MAX_PER_RUN) {
2572
- const { events, offset: next } = outbox.read(ref, offset, BATCH);
2822
+ if (stalled || sent >= MAX_PER_RUN) break;
2823
+ let offset = pending[cursors.key(ref)] ?? c[cursors.key(ref)] ?? 0;
2824
+ for (; ; ) {
2825
+ const room = BATCH - batch.length;
2826
+ const { events, offset: next } = outbox.read(ref, offset, room);
2573
2827
  if (!events.length) break;
2574
- const filtered = events.map((e) => filterEvent(e, sink.tier, sink.pii, { home: os2.homedir() })).filter((e) => !!e);
2575
- let ok = true;
2576
- if (filtered.length) {
2577
- const res = await sink.send(filtered);
2578
- ok = res.ok;
2579
- if (!ok) {
2580
- log(state, `sink ${sink.name}: ${res.status ?? ""} ${res.error ?? ""} \u2192 ${res.action ?? "retry"}`);
2581
- if (res.action === "pause" || res.action === "lower-tier") applySinkVerdict(sink.name, res);
2582
- if (res.action === "drop") ok = true;
2583
- }
2584
- if (ok) sent += filtered.length;
2585
- }
2586
- if (!ok) {
2587
- stalled = true;
2588
- break;
2828
+ for (const e of events) {
2829
+ const f = filterEvent(e, sink.tier, sink.pii, { home: os3.homedir() });
2830
+ if (f) batch.push(f);
2589
2831
  }
2590
2832
  offset = next;
2591
- c[cursors.key(ref)] = offset;
2592
- cursors.write(c);
2833
+ pending[cursors.key(ref)] = offset;
2834
+ if (batch.length >= BATCH) {
2835
+ if (!await send()) {
2836
+ stalled = true;
2837
+ break;
2838
+ }
2839
+ if (sent >= MAX_PER_RUN) break;
2840
+ }
2593
2841
  }
2594
- if (stalled) break;
2842
+ }
2843
+ if (!stalled && (batch.length || Object.keys(pending).length)) {
2844
+ if (!await send()) stalled = true;
2595
2845
  }
2596
2846
  delivered[sink.name] = sent;
2597
2847
  }
@@ -2608,6 +2858,7 @@ function applySinkVerdict(name, res) {
2608
2858
  } catch {
2609
2859
  }
2610
2860
  }
2861
+ var jobsDir = (state) => path12.join(state, "jobs");
2611
2862
  export {
2612
2863
  HttpSink,
2613
2864
  buildSinks,