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/worker.js CHANGED
@@ -1,16 +1,20 @@
1
1
  #!/usr/bin/env node
2
- // sessionpipe 0.1.0 · Apache-2.0 · https://sessionpipe.org
2
+ // sessionpipe 0.2.0 · Apache-2.0 · https://sessionpipe.org
3
3
 
4
4
  // src/worker.ts
5
- import { readFileSync as readFileSync9, unlinkSync as unlinkSync2 } from "node:fs";
6
-
7
- // src/run.ts
8
- 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";
9
- import os2 from "node:os";
10
- import path11 from "node:path";
5
+ import { readFileSync as readFileSync9, unlinkSync as unlinkSync5 } from "node:fs";
11
6
 
12
7
  // ../core/src/adapters/antigravity.ts
13
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, realpathSync, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
8
+ import {
9
+ existsSync as existsSync2,
10
+ mkdirSync as mkdirSync2,
11
+ readdirSync as readdirSync2,
12
+ readFileSync as readFileSync2,
13
+ realpathSync,
14
+ statSync as statSync2,
15
+ unlinkSync as unlinkSync2,
16
+ writeFileSync as writeFileSync2
17
+ } from "node:fs";
14
18
  import path4 from "node:path";
15
19
 
16
20
  // ../core/src/paths.ts
@@ -237,28 +241,67 @@ function readAntigravity(lines, from) {
237
241
  }
238
242
 
239
243
  // ../core/src/adapters/claude-shaped.ts
240
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
244
+ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
241
245
  import path3 from "node:path";
242
246
  var OURS = /sessionpipe[^"\n]*[\\/]hook\.js"?\s+[a-z][a-z-]*\s+[A-Za-z]+\s*$/;
243
247
  var isOurs = (h) => Array.isArray(h?.hooks) && h.hooks.some((x) => typeof x.command === "string" && OURS.test(x.command));
244
- function readJson(file) {
248
+ function readJsonFile(file) {
249
+ let raw;
245
250
  try {
246
- return JSON.parse(readFileSync(file, "utf8"));
251
+ raw = readFileSync(file, "utf8");
247
252
  } catch {
248
- return {};
253
+ return { ok: true, existed: false, value: {}, raw: "", indent: " ", eol: "\n", finalNewline: true };
249
254
  }
255
+ let value;
256
+ try {
257
+ value = JSON.parse(raw);
258
+ } catch {
259
+ const hint = /^\s*\/\/|\/\*/m.test(raw) ? "not plain JSON (it has comments)" : "not plain JSON";
260
+ return { ok: false, existed: true, reason: hint };
261
+ }
262
+ if (!value || typeof value !== "object" || Array.isArray(value))
263
+ return { ok: false, existed: true, reason: "not a JSON object" };
264
+ const eol = raw.includes("\r\n") ? "\r\n" : "\n";
265
+ const m = /^( +|\t+)\S/m.exec(raw);
266
+ const indent = m?.[1] ?? " ";
267
+ return {
268
+ ok: true,
269
+ existed: true,
270
+ value,
271
+ raw,
272
+ indent,
273
+ eol,
274
+ finalNewline: /\r?\n$/.test(raw)
275
+ };
276
+ }
277
+ function formatJson(value, f) {
278
+ const text = JSON.stringify(value, null, f.indent).replace(/\n/g, f.eol);
279
+ return f.finalNewline ? text + f.eol : text;
250
280
  }
251
281
  function entryFor(event, harness, cmd, o) {
252
282
  const matcher = o.matcherFor(event);
253
283
  return {
254
284
  ...matcher !== void 0 ? { matcher } : {},
255
- hooks: [{ type: "command", command: cmd([harness, event]).command, timeout: o.timeout }]
285
+ hooks: [{ type: "command", command: cmd([harness, event]).command, timeout: o.timeoutFor?.(event) ?? o.timeout }]
256
286
  };
257
287
  }
258
288
  function installClaudeShaped(file, harness, events, cmd, o) {
259
- const settings2 = readJson(file);
289
+ const f = readJsonFile(file);
290
+ if (!f.ok) return { file, changed: false, skipped: true, note: `${f.reason}; not touched` };
291
+ const settings2 = f.value;
260
292
  const hooks = settings2.hooks && typeof settings2.hooks === "object" ? settings2.hooks : {};
261
293
  let changed = false;
294
+ const want = new Set(events);
295
+ for (const ev of Object.keys(hooks)) {
296
+ if (want.has(ev)) continue;
297
+ const list = Array.isArray(hooks[ev]) ? hooks[ev] : [];
298
+ const kept = list.filter((h) => !isOurs(h));
299
+ if (kept.length !== list.length) {
300
+ changed = true;
301
+ if (kept.length) hooks[ev] = kept;
302
+ else delete hooks[ev];
303
+ }
304
+ }
262
305
  for (const ev of events) {
263
306
  const list = Array.isArray(hooks[ev]) ? hooks[ev] : [];
264
307
  const entry2 = entryFor(ev, harness, cmd, o);
@@ -277,19 +320,19 @@ function installClaudeShaped(file, harness, events, cmd, o) {
277
320
  if (changed) {
278
321
  settings2.hooks = hooks;
279
322
  mkdirSync(path3.dirname(file), { recursive: true });
280
- writeFileSync(file, `${JSON.stringify(settings2, null, 2)}
281
- `);
323
+ writeFileSync(file, formatJson(settings2, f));
282
324
  }
283
- return { file, changed };
325
+ return { file, changed, created: !f.existed && changed };
284
326
  }
285
- function uninstallClaudeShaped(file, events) {
327
+ function uninstallClaudeShaped(file, events, opts = {}) {
286
328
  if (!existsSync(file)) return { file, changed: false };
287
- const raw = readFileSync(file, "utf8");
288
- const settings2 = readJson(file);
329
+ const f = readJsonFile(file);
330
+ if (!f.ok) return { file, changed: false, skipped: true, note: `${f.reason}; not touched` };
331
+ const settings2 = f.value;
289
332
  const hooks = settings2.hooks;
290
333
  if (!hooks || typeof hooks !== "object") return { file, changed: false };
291
334
  let changed = false;
292
- for (const ev of events) {
335
+ for (const ev of [...events, ...Object.keys(hooks)]) {
293
336
  const list = Array.isArray(hooks[ev]) ? hooks[ev] : [];
294
337
  const kept = list.filter((h) => !isOurs(h));
295
338
  if (kept.length !== list.length) {
@@ -300,13 +343,17 @@ function uninstallClaudeShaped(file, events) {
300
343
  }
301
344
  if (!changed) return { file, changed: false };
302
345
  if (!Object.keys(hooks).length) delete settings2.hooks;
303
- const next = `${JSON.stringify(settings2, null, 2)}
304
- `;
305
- if (next !== raw) writeFileSync(file, next);
346
+ if (opts.created && !Object.keys(settings2).length) {
347
+ unlinkSync(file);
348
+ return { file, changed: true, note: "removed (sessionpipe created it)" };
349
+ }
350
+ const next = formatJson(settings2, f);
351
+ if (next !== f.raw) writeFileSync(file, next);
306
352
  return { file, changed: true };
307
353
  }
308
354
  function installedClaudeShaped(file, harness, events, cmd, o) {
309
- const hooks = readJson(file).hooks ?? {};
355
+ const f = readJsonFile(file);
356
+ const hooks = f.ok ? f.value.hooks ?? {} : {};
310
357
  let present = 0;
311
358
  let current = 0;
312
359
  for (const ev of events) {
@@ -315,6 +362,9 @@ function installedClaudeShaped(file, harness, events, cmd, o) {
315
362
  if (mine.length) present++;
316
363
  if (mine.length === 1 && JSON.stringify(mine[0]) === JSON.stringify(entryFor(ev, harness, cmd, o))) current++;
317
364
  }
365
+ for (const ev of Object.keys(hooks))
366
+ if (!events.includes(ev) && (Array.isArray(hooks[ev]) ? hooks[ev] : []).some((h) => isOurs(h)))
367
+ return "stale";
318
368
  if (!present) return "missing";
319
369
  return current === events.length ? "current" : "stale";
320
370
  }
@@ -329,10 +379,13 @@ var projectsDir = (env) => path4.join(gemini(env), "config", "projects");
329
379
  var dataDirs = (env) => ["antigravity", "antigravity-cli", "antigravity-ide"].map((d) => path4.join(gemini(env), d));
330
380
  var ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
331
381
  var validTool = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
332
- function entry(cmd) {
382
+ function entry(cmd, lean) {
333
383
  const handler = (e) => ({ type: "command", command: cmd([NAME, e]).command, timeout: 5 });
334
384
  const out = { enabled: true };
335
- for (const e of AG_EVENTS) out[e] = /Tool/.test(e) ? [{ matcher: "*", hooks: [handler(e)] }] : [handler(e)];
385
+ for (const e of AG_EVENTS) {
386
+ if (lean && e === "PreToolUse") continue;
387
+ out[e] = /Tool/.test(e) ? [{ matcher: "*", hooks: [handler(e)] }] : [handler(e)];
388
+ }
336
389
  return out;
337
390
  }
338
391
  var antigravity = {
@@ -340,34 +393,49 @@ var antigravity = {
340
393
  events: AG_EVENTS,
341
394
  detect: (env = process.env) => existsSync2(path4.join(gemini(env), "config")) || dataDirs(env).some((d) => existsSync2(d)),
342
395
  configFiles: (env = process.env) => [hooksFile(env)],
343
- install(cmd, env = process.env) {
396
+ install(cmd, env = process.env, opts = {}) {
344
397
  const file = hooksFile(env);
345
- const hooks = readJson(file);
346
- const e = entry(cmd);
398
+ const f = readJsonFile(file);
399
+ if (!f.ok) return [{ file, changed: false, skipped: true, note: `${f.reason}; not touched` }];
400
+ const hooks = f.value;
401
+ const e = entry(cmd, opts.lean);
347
402
  if (JSON.stringify(hooks[HOOK_NAME]) === JSON.stringify(e)) return [{ file, changed: false }];
348
403
  hooks[HOOK_NAME] = e;
349
404
  mkdirSync2(path4.dirname(file), { recursive: true });
350
- writeFileSync2(file, `${JSON.stringify(hooks, null, 2)}
351
- `);
405
+ writeFileSync2(file, formatJson(hooks, f));
352
406
  return [
353
- { file, changed: true, note: "hooks.json is read when a conversation starts; open ones report after a restart" }
407
+ {
408
+ file,
409
+ changed: true,
410
+ created: !f.existed,
411
+ note: "hooks.json is read when a conversation starts; open ones report after a restart"
412
+ }
354
413
  ];
355
414
  },
356
- uninstall(env = process.env) {
415
+ uninstall(env = process.env, opts = {}) {
357
416
  const file = hooksFile(env);
358
417
  if (!existsSync2(file)) return [{ file, changed: false }];
359
- const hooks = readJson(file);
418
+ const f = readJsonFile(file);
419
+ if (!f.ok) return [{ file, changed: false, skipped: true, note: `${f.reason}; not touched` }];
420
+ const hooks = f.value;
360
421
  if (!(HOOK_NAME in hooks)) return [{ file, changed: false }];
361
422
  delete hooks[HOOK_NAME];
362
- writeFileSync2(file, `${JSON.stringify(hooks, null, 2)}
363
- `);
423
+ if (opts.created?.includes(file) && !Object.keys(hooks).length) {
424
+ unlinkSync2(file);
425
+ return [{ file, changed: true, note: "removed (sessionpipe created it)" }];
426
+ }
427
+ writeFileSync2(file, formatJson(hooks, f));
364
428
  return [{ file, changed: true }];
365
429
  },
366
- installed(cmd, env = process.env) {
430
+ installed(cmd, env = process.env, opts = {}) {
367
431
  const file = hooksFile(env);
368
- const have = readJson(file)[HOOK_NAME];
432
+ const f = readJsonFile(file);
433
+ const have = f.ok ? f.value[HOOK_NAME] : void 0;
369
434
  return [
370
- { file, state: !have ? "missing" : JSON.stringify(have) === JSON.stringify(entry(cmd)) ? "current" : "stale" }
435
+ {
436
+ file,
437
+ state: !have ? "missing" : JSON.stringify(have) === JSON.stringify(entry(cmd, opts.lean)) ? "current" : "stale"
438
+ }
371
439
  ];
372
440
  },
373
441
  fromHook(input) {
@@ -633,9 +701,18 @@ var CLAUDE_EVENTS = [
633
701
  "SubagentStop",
634
702
  "PreCompact",
635
703
  "PostCompact",
636
- "SessionEnd"
704
+ "SessionEnd",
705
+ "StopFailure"
637
706
  ];
638
- var OPTS = { matcherFor: (e) => /Tool|Permission/.test(e) ? "" : void 0, timeout: 5 };
707
+ var LEAN_SKIP = /* @__PURE__ */ new Set(["PreToolUse", "PostToolUseFailure"]);
708
+ var eventsFor = (lean) => lean ? CLAUDE_EVENTS.filter((e) => !LEAN_SKIP.has(e)) : [...CLAUDE_EVENTS];
709
+ var OPTS = {
710
+ matcherFor: (e) => /Tool|Permission/.test(e) ? "" : void 0,
711
+ timeout: 5,
712
+ // The hook waits up to 125 s for a verified control answer, and only when control
713
+ // is paired on this machine; everywhere else it exits in milliseconds as before.
714
+ timeoutFor: (e) => e === "PermissionRequest" ? 130 : void 0
715
+ };
639
716
  var DESKTOP_ID = /^local_[A-Za-z0-9-]{1,64}$/;
640
717
  function claudeDirs(env = process.env, home = HOME) {
641
718
  const out = [];
@@ -666,11 +743,13 @@ var claudeCode = {
666
743
  events: CLAUDE_EVENTS,
667
744
  detect: (env = process.env) => claudeDirs(env).some((d) => existsSync3(d)),
668
745
  configFiles: (env = process.env) => claudeDirs(env).map(settingsIn),
669
- install: (cmd, env = process.env) => claudeDirs(env).map((d) => installClaudeShaped(settingsIn(d), NAME2, CLAUDE_EVENTS, cmd, OPTS)),
670
- uninstall: (env = process.env) => claudeDirs(env).map((d) => uninstallClaudeShaped(settingsIn(d), CLAUDE_EVENTS)),
671
- installed: (cmd, env = process.env) => claudeDirs(env).map((d) => ({
746
+ install: (cmd, env = process.env, opts = {}) => claudeDirs(env).map((d) => installClaudeShaped(settingsIn(d), NAME2, eventsFor(opts.lean), cmd, OPTS)),
747
+ uninstall: (env = process.env, opts = {}) => claudeDirs(env).map(
748
+ (d) => uninstallClaudeShaped(settingsIn(d), CLAUDE_EVENTS, { created: opts.created?.includes(settingsIn(d)) })
749
+ ),
750
+ installed: (cmd, env = process.env, opts = {}) => claudeDirs(env).map((d) => ({
672
751
  file: settingsIn(d),
673
- state: installedClaudeShaped(settingsIn(d), NAME2, CLAUDE_EVENTS, cmd, OPTS)
752
+ state: installedClaudeShaped(settingsIn(d), NAME2, eventsFor(opts.lean), cmd, OPTS)
674
753
  })),
675
754
  fromHook(input) {
676
755
  const event = input.argv[1] ?? "";
@@ -761,6 +840,15 @@ var claudeCode = {
761
840
  case "Stop":
762
841
  events.push(ev("turn.ended", { turn_id, reason: "stop" }));
763
842
  break;
843
+ case "StopFailure":
844
+ events.push(
845
+ ev("turn.ended", {
846
+ turn_id,
847
+ reason: "error",
848
+ error: typeof s.error === "string" ? s.error.slice(0, 200) : void 0
849
+ })
850
+ );
851
+ break;
764
852
  case "SubagentStart":
765
853
  events.push(
766
854
  ev("subagent.started", {
@@ -1068,9 +1156,9 @@ function claudeFacts(file, sessionId, hints, state) {
1068
1156
  }
1069
1157
  const name = live?.name ? { title: live.name, source: live.name_source } : typeof mem.name === "string" ? { title: mem.name, source: mem.name_source } : null;
1070
1158
  const pick = name && name.source === "custom" && name || custom && { title: custom, source: "custom" } || name || ai && { title: ai, source: "harness" };
1071
- let ask = typeof mem.first_ask === "string" ? mem.first_ask : firstAsk(H);
1072
- if (!ask && size > 64 * 1024 && !mem.first_ask_none) {
1073
- ask = scanLines(file, (l) => {
1159
+ let ask2 = typeof mem.first_ask === "string" ? mem.first_ask : firstAsk(H);
1160
+ if (!ask2 && size > 64 * 1024 && !mem.first_ask_none) {
1161
+ ask2 = scanLines(file, (l) => {
1074
1162
  try {
1075
1163
  const t = askText(JSON.parse(l));
1076
1164
  return t ? appTitle(t) : null;
@@ -1078,17 +1166,17 @@ function claudeFacts(file, sessionId, hints, state) {
1078
1166
  return null;
1079
1167
  }
1080
1168
  });
1081
- if (!ask && size > 16 * 1024 * 1024) state.save(NAME2, id, { first_ask_none: true });
1169
+ if (!ask2 && size > 16 * 1024 * 1024) state.save(NAME2, id, { first_ask_none: true });
1082
1170
  }
1083
- if (ask) {
1084
- out.first_ask = ask;
1085
- if (!mem.first_ask) state.save(NAME2, id, { first_ask: ask });
1171
+ if (ask2) {
1172
+ out.first_ask = ask2;
1173
+ if (!mem.first_ask) state.save(NAME2, id, { first_ask: ask2 });
1086
1174
  }
1087
1175
  if (pick) {
1088
1176
  out.title = String(pick.title).slice(0, 120);
1089
1177
  out.title_source = pick.source;
1090
- } else if (ask) {
1091
- out.title = ask;
1178
+ } else if (ask2) {
1179
+ out.title = ask2;
1092
1180
  out.title_source = "first-ask";
1093
1181
  }
1094
1182
  const acct = claudeAccount(dir);
@@ -1272,6 +1360,7 @@ var CODEX_EVENTS = [
1272
1360
  "SessionEnd"
1273
1361
  ];
1274
1362
  var OPTS2 = { matcherFor: (e) => /Tool|Permission/.test(e) ? "" : void 0, timeout: 5 };
1363
+ var eventsFor2 = (lean) => lean ? CODEX_EVENTS.filter((e) => e !== "PreToolUse") : [...CODEX_EVENTS];
1275
1364
  var codexHome = (env) => env.CODEX_HOME || path6.join(HOME, ".codex");
1276
1365
  var hooksFile2 = (env) => path6.join(codexHome(env), "hooks.json");
1277
1366
  var configFile2 = (env) => path6.join(codexHome(env), "config.toml");
@@ -1320,7 +1409,8 @@ function installNotify(cmd, env) {
1320
1409
  return {
1321
1410
  file,
1322
1411
  changed: false,
1323
- note: `left alone: notify runs ${n.raw.slice(0, 80)} (hooks.json carries the events; the notify fallback is off)`
1412
+ skipped: true,
1413
+ 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`
1324
1414
  };
1325
1415
  }
1326
1416
  write(withNotify(text, line));
@@ -1377,14 +1467,21 @@ var codex = {
1377
1467
  events: CODEX_EVENTS,
1378
1468
  detect: (env = process.env) => existsSync4(codexHome(env)),
1379
1469
  configFiles: (env = process.env) => [hooksFile2(env), configFile2(env)],
1380
- install: (cmd, env = process.env) => [
1381
- installClaudeShaped(hooksFile2(env), NAME3, CODEX_EVENTS, cmd, OPTS2),
1470
+ install: (cmd, env = process.env, opts = {}) => [
1471
+ installClaudeShaped(hooksFile2(env), NAME3, eventsFor2(opts.lean), cmd, OPTS2),
1382
1472
  installNotify(cmd, env)
1383
1473
  ],
1384
- uninstall: (env = process.env) => [uninstallClaudeShaped(hooksFile2(env), CODEX_EVENTS), uninstallNotify(env)],
1385
- installed: (cmd, env = process.env) => {
1474
+ uninstall: (env = process.env, opts = {}) => [
1475
+ uninstallClaudeShaped(hooksFile2(env), CODEX_EVENTS, { created: opts.created?.includes(hooksFile2(env)) }),
1476
+ uninstallNotify(env)
1477
+ ],
1478
+ installed: (cmd, env = process.env, opts = {}) => {
1386
1479
  const out = [
1387
- { file: hooksFile2(env), state: installedClaudeShaped(hooksFile2(env), NAME3, CODEX_EVENTS, cmd, OPTS2) }
1480
+ {
1481
+ file: hooksFile2(env),
1482
+ state: installedClaudeShaped(hooksFile2(env), NAME3, eventsFor2(opts.lean), cmd, OPTS2),
1483
+ note: "Codex runs non-managed hooks only after the review in `codex` (/hooks); until then nothing is reported unless the notify fallback is ours"
1484
+ }
1388
1485
  ];
1389
1486
  let text = "";
1390
1487
  try {
@@ -1572,6 +1669,7 @@ function codexFacts(file) {
1572
1669
  }
1573
1670
 
1574
1671
  // ../core/src/adapters/gemini-cli.ts
1672
+ import { execFileSync as execFileSync3 } from "node:child_process";
1575
1673
  import { existsSync as existsSync5, readFileSync as readFileSync5 } from "node:fs";
1576
1674
  import path7 from "node:path";
1577
1675
  var NAME4 = "gemini-cli";
@@ -1589,17 +1687,61 @@ var OPTS3 = { matcherFor: (e) => /Tool/.test(e) ? ".*" : void 0, timeout: 5e3 };
1589
1687
  var geminiDir = (env) => env.GEMINI_CLI_HOME || path7.join(HOME, ".gemini");
1590
1688
  var settings = (env) => path7.join(geminiDir(env), "settings.json");
1591
1689
  var validTool4 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
1690
+ var eventsFor3 = (lean) => lean ? GEMINI_EVENTS.filter((e) => e !== "BeforeTool") : [...GEMINI_EVENTS];
1691
+ function geminiHooksActive(settingsFile) {
1692
+ let tools;
1693
+ try {
1694
+ tools = JSON.parse(readFileSync5(settingsFile, "utf8")).tools;
1695
+ } catch {
1696
+ }
1697
+ if (tools?.enableHooks === true) return { active: true };
1698
+ if (tools?.enableHooks === false)
1699
+ return { active: false, note: "inactive: tools.enableHooks is false in settings.json" };
1700
+ let version = "";
1701
+ try {
1702
+ version = execFileSync3("gemini", ["--version"], {
1703
+ encoding: "utf8",
1704
+ timeout: 4e3,
1705
+ stdio: ["ignore", "pipe", "ignore"]
1706
+ }).trim();
1707
+ } catch {
1708
+ return {
1709
+ active: true,
1710
+ note: "could not run `gemini --version`; hooks need tools.enableHooks on versions before 0.46"
1711
+ };
1712
+ }
1713
+ const m = /(\d+)\.(\d+)/.exec(version);
1714
+ if (m && (Number(m[1]) > 0 || Number(m[2]) >= 46)) return { active: true };
1715
+ return {
1716
+ active: false,
1717
+ note: `inactive: Gemini CLI ${version || "?"} runs hooks only with "tools": {"enableHooks": true} in settings.json`
1718
+ };
1719
+ }
1592
1720
  var geminiCli = {
1593
1721
  name: NAME4,
1594
1722
  events: GEMINI_EVENTS,
1595
1723
  // ~/.gemini also belongs to Antigravity; Gemini CLI is here when its settings or its chat logs exist.
1596
1724
  detect: (env = process.env) => existsSync5(settings(env)) || existsSync5(path7.join(geminiDir(env), "tmp")),
1597
1725
  configFiles: (env = process.env) => [settings(env)],
1598
- install: (cmd, env = process.env) => [installClaudeShaped(settings(env), NAME4, GEMINI_EVENTS, cmd, OPTS3)],
1599
- uninstall: (env = process.env) => [uninstallClaudeShaped(settings(env), GEMINI_EVENTS)],
1600
- installed: (cmd, env = process.env) => [
1601
- { file: settings(env), state: installedClaudeShaped(settings(env), NAME4, GEMINI_EVENTS, cmd, OPTS3) }
1726
+ install: (cmd, env = process.env, opts = {}) => {
1727
+ const r = installClaudeShaped(settings(env), NAME4, eventsFor3(opts.lean), cmd, OPTS3);
1728
+ const a = geminiHooksActive(settings(env));
1729
+ return [a.note && !r.skipped ? { ...r, note: [r.note, a.note].filter(Boolean).join("; ") } : r];
1730
+ },
1731
+ uninstall: (env = process.env, opts = {}) => [
1732
+ uninstallClaudeShaped(settings(env), GEMINI_EVENTS, { created: opts.created?.includes(settings(env)) })
1602
1733
  ],
1734
+ installed: (cmd, env = process.env, opts = {}) => {
1735
+ const state = installedClaudeShaped(settings(env), NAME4, eventsFor3(opts.lean), cmd, OPTS3);
1736
+ const a = geminiHooksActive(settings(env));
1737
+ return [
1738
+ {
1739
+ file: settings(env),
1740
+ state: state === "current" && !a.active ? "inactive" : state,
1741
+ ...a.note ? { note: a.note } : {}
1742
+ }
1743
+ ];
1744
+ },
1603
1745
  fromHook(input) {
1604
1746
  const event = input.argv[1] ?? "";
1605
1747
  let s = {};
@@ -1810,7 +1952,7 @@ import {
1810
1952
  readSync as readSync2,
1811
1953
  renameSync,
1812
1954
  statSync as statSync4,
1813
- unlinkSync,
1955
+ unlinkSync as unlinkSync3,
1814
1956
  writeFileSync as writeFileSync5
1815
1957
  } from "node:fs";
1816
1958
  import path9 from "node:path";
@@ -1838,8 +1980,8 @@ var Outbox = class {
1838
1980
  } catch {
1839
1981
  n = this.lastSeqInFile(ref) + 1;
1840
1982
  }
1841
- mkdirSync5(path9.dirname(f), { recursive: true });
1842
- writeFileSync5(f, String(n + 1));
1983
+ mkdirSync5(path9.dirname(f), { recursive: true, mode: 448 });
1984
+ writeFileSync5(f, String(n + 1), { mode: 384 });
1843
1985
  return n;
1844
1986
  }
1845
1987
  lastSeqInFile(ref) {
@@ -1857,9 +1999,9 @@ var Outbox = class {
1857
1999
  }
1858
2000
  append(ref, events) {
1859
2001
  if (!events.length) return;
1860
- mkdirSync5(this.dir(ref.harness), { recursive: true });
2002
+ mkdirSync5(this.dir(ref.harness), { recursive: true, mode: 448 });
1861
2003
  appendFileSync(this.file(ref), `${events.map((e) => JSON.stringify(e)).join("\n")}
1862
- `);
2004
+ `, { mode: 384 });
1863
2005
  }
1864
2006
  /** Every session file, newest first. */
1865
2007
  sessions() {
@@ -1923,9 +2065,9 @@ var Outbox = class {
1923
2065
  const f = this.file(ref);
1924
2066
  try {
1925
2067
  if (statSync4(f).mtimeMs < cutoff) {
1926
- unlinkSync(f);
2068
+ unlinkSync3(f);
1927
2069
  try {
1928
- unlinkSync(this.seqFile(ref));
2070
+ unlinkSync3(this.seqFile(ref));
1929
2071
  } catch {
1930
2072
  }
1931
2073
  n++;
@@ -1938,7 +2080,7 @@ var Outbox = class {
1938
2080
  delete(ref) {
1939
2081
  for (const f of [this.file(ref), this.seqFile(ref)]) {
1940
2082
  try {
1941
- unlinkSync(f);
2083
+ unlinkSync3(f);
1942
2084
  } catch {
1943
2085
  }
1944
2086
  }
@@ -1963,9 +2105,9 @@ var Cursors = class {
1963
2105
  }
1964
2106
  write(c) {
1965
2107
  const f = this.file();
1966
- mkdirSync5(path9.dirname(f), { recursive: true });
2108
+ mkdirSync5(path9.dirname(f), { recursive: true, mode: 448 });
1967
2109
  const tmp = `${f}.${process.pid}.tmp`;
1968
- writeFileSync5(tmp, JSON.stringify(c));
2110
+ writeFileSync5(tmp, JSON.stringify(c), { mode: 384 });
1969
2111
  renameSync(tmp, f);
1970
2112
  }
1971
2113
  key(ref) {
@@ -1974,10 +2116,10 @@ var Cursors = class {
1974
2116
  };
1975
2117
  function takeLock(root, ref, staleMs = 5 * 6e4) {
1976
2118
  const dir = path9.join(root, "locks");
1977
- mkdirSync5(dir, { recursive: true });
2119
+ mkdirSync5(dir, { recursive: true, mode: 448 });
1978
2120
  const f = path9.join(dir, `${safeId(ref.harness)}-${safeId(ref.session)}.lock`);
1979
2121
  try {
1980
- if (Date.now() - statSync4(f).mtimeMs > staleMs) unlinkSync(f);
2122
+ if (Date.now() - statSync4(f).mtimeMs > staleMs) unlinkSync3(f);
1981
2123
  else return null;
1982
2124
  } catch {
1983
2125
  }
@@ -1988,7 +2130,7 @@ function takeLock(root, ref, staleMs = 5 * 6e4) {
1988
2130
  }
1989
2131
  return () => {
1990
2132
  try {
1991
- unlinkSync(f);
2133
+ unlinkSync3(f);
1992
2134
  } catch {
1993
2135
  }
1994
2136
  };
@@ -2124,7 +2266,7 @@ function redactAssignments(text) {
2124
2266
  function isSecretName(name) {
2125
2267
  if (/^[A-Z0-9_]+_KEY$/.test(name)) return true;
2126
2268
  const flat = name.toLowerCase().replace(/[_-]/g, "");
2127
- return /(?:pass(?:word|wd|phrase|code)?|pwd|secret|secretkey|secretaccesskey|clientsecret|apikey|accesskey|accesstoken|authtoken|refreshtoken|privatekey|token|credential|creds)s?$/.test(
2269
+ return /(?:pass(?:word|wd|phrase|code)|pwd|secret|secretkey|secretaccesskey|clientsecret|apikey|accesskey|accesstoken|authtoken|refreshtoken|privatekey|token|credential|creds)s?$/.test(
2128
2270
  flat
2129
2271
  );
2130
2272
  }
@@ -2213,7 +2355,7 @@ var DATA_ALLOW = {
2213
2355
  "session.backfill": { 0: ["started_at", "last_at", "turns"] },
2214
2356
  "session.forgotten": { 0: [] },
2215
2357
  "turn.started": { 0: ["turn_id"], 1: ["prompt_chars"] },
2216
- "turn.ended": { 0: ["turn_id", "reason", "ms"] },
2358
+ "turn.ended": { 0: ["turn_id", "reason", "ms"], 1: ["error"] },
2217
2359
  "turn.transcript": { 2: ["turn", "user", "assistant", "at"] },
2218
2360
  "tool.started": { 1: ["tool", "call_id", "turn_id", "agent_id"], 3: ["input"] },
2219
2361
  "tool.ended": { 1: ["tool", "call_id", "turn_id", "agent_id", "ms", "ok", "error"], 3: ["input", "output"] },
@@ -2334,9 +2476,66 @@ var StdoutSink = class {
2334
2476
  }
2335
2477
  };
2336
2478
 
2479
+ // src/run.ts
2480
+ import {
2481
+ appendFileSync as appendFileSync3,
2482
+ existsSync as existsSync8,
2483
+ mkdirSync as mkdirSync7,
2484
+ readdirSync as readdirSync6,
2485
+ readFileSync as readFileSync8,
2486
+ renameSync as renameSync2,
2487
+ statSync as statSync5,
2488
+ unlinkSync as unlinkSync4,
2489
+ writeFileSync as writeFileSync6
2490
+ } from "node:fs";
2491
+ import os3 from "node:os";
2492
+ import path12 from "node:path";
2493
+
2494
+ // src/control/local.ts
2495
+ import net from "node:net";
2496
+ import os2 from "node:os";
2497
+ import path11 from "node:path";
2498
+ function socketPath(state, env = process.env) {
2499
+ if (env.SESSIONPIPE_SOCKET) return env.SESSIONPIPE_SOCKET;
2500
+ if (process.platform === "win32") return `\\\\.\\pipe\\sessionpipe-control-${os2.userInfo().username}`;
2501
+ return path11.join(state, "control.sock");
2502
+ }
2503
+ function ask(sock, req, timeoutMs) {
2504
+ return new Promise((resolve) => {
2505
+ let buf = "";
2506
+ let done = false;
2507
+ const s = net.connect(sock);
2508
+ const finish = (r) => {
2509
+ if (done) return;
2510
+ done = true;
2511
+ clearTimeout(t);
2512
+ s.destroy();
2513
+ resolve(r);
2514
+ };
2515
+ const t = setTimeout(() => finish(null), timeoutMs);
2516
+ s.on("connect", () => s.write(`${JSON.stringify(req)}
2517
+ `));
2518
+ s.on("data", (d) => {
2519
+ buf += d.toString("utf8");
2520
+ const i = buf.indexOf("\n");
2521
+ if (i < 0) return;
2522
+ try {
2523
+ finish(JSON.parse(buf.slice(0, i)));
2524
+ } catch {
2525
+ finish(null);
2526
+ }
2527
+ });
2528
+ s.on("error", () => finish(null));
2529
+ s.on("close", () => finish(null));
2530
+ });
2531
+ }
2532
+
2337
2533
  // src/http-sink.ts
2338
- import { createHmac, randomUUID } from "node:crypto";
2534
+ import { createHash as createHash3, createHmac } from "node:crypto";
2339
2535
  var TIMEOUT_MS = 15e3;
2536
+ function webhookId(events) {
2537
+ return `msg_${createHash3("sha256").update(events.map((e) => e.id).join(",")).digest("base64url").slice(0, 32)}`;
2538
+ }
2340
2539
  var HttpSink = class {
2341
2540
  constructor(cfg, tier) {
2342
2541
  this.cfg = cfg;
@@ -2359,7 +2558,7 @@ var HttpSink = class {
2359
2558
  const headers = { "content-type": "application/json", "sessionpipe-protocol": "1" };
2360
2559
  if (this.cfg.token) headers.authorization = `Bearer ${this.cfg.token}`;
2361
2560
  if (this.cfg.secret?.startsWith("whsec_")) {
2362
- const id = `msg_${randomUUID()}`;
2561
+ const id = webhookId(events);
2363
2562
  const ts = Math.floor(Date.now() / 1e3);
2364
2563
  const key = Buffer.from(this.cfg.secret.slice(6), "base64");
2365
2564
  const sig = createHmac("sha256", key).update(`${id}.${ts}.${body}`).digest("base64");
@@ -2420,10 +2619,57 @@ var HttpSink = class {
2420
2619
  };
2421
2620
 
2422
2621
  // src/run.ts
2622
+ async function tellDaemon(state, job2, session, transcript) {
2623
+ const sock = socketPath(state);
2624
+ if (process.platform !== "win32" && !existsSync8(sock)) return;
2625
+ await ask(
2626
+ sock,
2627
+ {
2628
+ op: "event",
2629
+ session: `claude-code:${session}`,
2630
+ event: job2.event,
2631
+ cwd: job2.cwd,
2632
+ ...transcript ? { transcript } : {}
2633
+ },
2634
+ 300
2635
+ );
2636
+ }
2637
+ function enqueueJob(state, job2) {
2638
+ const jobs = jobsDir(state);
2639
+ mkdirSync7(jobs, { recursive: true, mode: 448 });
2640
+ const f = path12.join(jobs, `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}.json`);
2641
+ writeFileSync6(f, JSON.stringify(job2), { mode: 384 });
2642
+ return f;
2643
+ }
2644
+ async function sweepJobs(state, olderThanMs = 3e3, limit = 20) {
2645
+ const jobs = jobsDir(state);
2646
+ let names = [];
2647
+ try {
2648
+ names = readdirSync6(jobs).filter((n) => n.endsWith(".json"));
2649
+ } catch {
2650
+ return 0;
2651
+ }
2652
+ let ran = 0;
2653
+ for (const n of names.sort().slice(0, limit)) {
2654
+ const f = path12.join(jobs, n);
2655
+ let job2 = null;
2656
+ try {
2657
+ if (Date.now() - statSync5(f).mtimeMs < olderThanMs) continue;
2658
+ job2 = JSON.parse(readFileSync8(f, "utf8"));
2659
+ unlinkSync4(f);
2660
+ } catch {
2661
+ continue;
2662
+ }
2663
+ await runJob(job2, { state }).catch(() => {
2664
+ });
2665
+ ran++;
2666
+ }
2667
+ return ran;
2668
+ }
2423
2669
  function log(state, line) {
2424
2670
  try {
2425
- mkdirSync7(state, { recursive: true });
2426
- const f = path11.join(state, "log");
2671
+ mkdirSync7(state, { recursive: true, mode: 448 });
2672
+ const f = path12.join(state, "log");
2427
2673
  try {
2428
2674
  if (statSync5(f).size > 1024 * 1024) renameSync2(f, `${f}.1`);
2429
2675
  } catch {
@@ -2434,7 +2680,7 @@ function log(state, line) {
2434
2680
  }
2435
2681
  }
2436
2682
  function factsState(state) {
2437
- const file = (h, s) => path11.join(state, "facts", safeId(h), `${safeId(s)}.json`);
2683
+ const file = (h, s) => path12.join(state, "facts", safeId(h), `${safeId(s)}.json`);
2438
2684
  return {
2439
2685
  get(h, s) {
2440
2686
  try {
@@ -2447,8 +2693,8 @@ function factsState(state) {
2447
2693
  const cur = this.get(h, s);
2448
2694
  const next = { ...cur, ...patch };
2449
2695
  if (JSON.stringify(next) === JSON.stringify(cur)) return;
2450
- mkdirSync7(path11.dirname(file(h, s)), { recursive: true });
2451
- writeFileSync6(file(h, s), JSON.stringify(next));
2696
+ mkdirSync7(path12.dirname(file(h, s)), { recursive: true, mode: 448 });
2697
+ writeFileSync6(file(h, s), JSON.stringify(next), { mode: 384 });
2452
2698
  }
2453
2699
  };
2454
2700
  }
@@ -2459,7 +2705,7 @@ function buildSinks(sinks) {
2459
2705
  const tier = Math.min(s.tier, s.max_tier ?? 3);
2460
2706
  if (s.url === "stdout") out.push(new StdoutSink(s.name, tier, !!s.pii));
2461
2707
  else if (s.url.startsWith("file:"))
2462
- out.push(new FileSink(s.name, s.url.slice(5).replace(/^~(?=\/|$)/, os2.homedir()), tier, !!s.pii));
2708
+ out.push(new FileSink(s.name, s.url.slice(5).replace(/^~(?=\/|$)/, os3.homedir()), tier, !!s.pii));
2463
2709
  else if (/^https?:\/\//.test(s.url)) out.push(new HttpSink(s, tier));
2464
2710
  }
2465
2711
  return out;
@@ -2471,14 +2717,17 @@ async function runJob(job2, opts = {}) {
2471
2717
  if (!adapter) return { events: [], delivered: {} };
2472
2718
  const r = adapter.fromHook({ argv: job2.argv, stdin: job2.stdin, env: { ...process.env, ...job2.env }, cwd: job2.cwd });
2473
2719
  if (!r || !r.session.id) return { events: [], delivered: {} };
2720
+ if (job2.harness === "claude-code") await tellDaemon(state, job2, r.session.id, r.transcript);
2474
2721
  const outbox = new Outbox(state);
2475
2722
  const ref = { harness: job2.harness, session: r.session.id };
2476
- const release = takeLock(state, ref);
2723
+ let release = takeLock(state, ref);
2724
+ for (let wait = 50; !release && wait <= 3200; wait *= 2) {
2725
+ await new Promise((res) => setTimeout(res, wait));
2726
+ release = takeLock(state, ref);
2727
+ }
2477
2728
  if (!release) {
2478
- log(state, `${job2.harness} ${job2.event}: session ${r.session.id} locked, re-queued`);
2479
- const jobs = path11.join(state, "jobs");
2480
- mkdirSync7(jobs, { recursive: true });
2481
- writeFileSync6(path11.join(jobs, `${Date.now().toString(36)}-requeue.json`), JSON.stringify(job2));
2729
+ log(state, `${job2.harness} ${job2.event}: session ${r.session.id} locked for 6 s, re-queued`);
2730
+ enqueueJob(state, job2);
2482
2731
  return { events: [], delivered: {} };
2483
2732
  }
2484
2733
  try {
@@ -2503,7 +2752,7 @@ async function runJob(job2, opts = {}) {
2503
2752
  ...learned.branch ? { branch: learned.branch } : {},
2504
2753
  id: r.session.id,
2505
2754
  seq: 0,
2506
- machine: machineName(cfg, os2.hostname()),
2755
+ machine: machineName(cfg, os3.hostname()),
2507
2756
  ...cwdRaw ? { cwd: tilde(cwdRaw) } : {},
2508
2757
  ...r.session.model ? { model: r.session.model } : {}
2509
2758
  };
@@ -2542,7 +2791,10 @@ async function runJob(job2, opts = {}) {
2542
2791
  if (read.turns.length)
2543
2792
  facts.save(job2.harness, r.session.id, { transcript_line: read.line, transcript_turns: turnNo });
2544
2793
  }
2545
- outbox.append(ref, events);
2794
+ outbox.append(
2795
+ ref,
2796
+ events.map((e) => ({ ...e, session: redactDeep(e.session), data: redactDeep(e.data) }))
2797
+ );
2546
2798
  log(
2547
2799
  state,
2548
2800
  `${job2.harness} ${job2.event}: ${events.map((e) => e.type).join(",") || "nothing"} (${r.session.id.slice(0, 8)})`
@@ -2568,34 +2820,57 @@ async function flush(state, sinkConfigs, outbox, first) {
2568
2820
  const cursors = new Cursors(state, sink.name);
2569
2821
  const c = cursors.read();
2570
2822
  let sent = 0;
2823
+ let stalled = false;
2824
+ let batch = [];
2825
+ let pending = {};
2826
+ const send = async () => {
2827
+ if (!batch.length) {
2828
+ Object.assign(c, pending);
2829
+ pending = {};
2830
+ cursors.write(c);
2831
+ return true;
2832
+ }
2833
+ const res = await sink.send(batch);
2834
+ if (!res.ok) {
2835
+ log(state, `sink ${sink.name}: ${res.status ?? ""} ${res.error ?? ""} \u2192 ${res.action ?? "retry"}`);
2836
+ if (res.action === "pause" || res.action === "lower-tier") applySinkVerdict(sink.name, res);
2837
+ if (res.action !== "drop") {
2838
+ batch = [];
2839
+ pending = {};
2840
+ return false;
2841
+ }
2842
+ }
2843
+ sent += batch.length;
2844
+ Object.assign(c, pending);
2845
+ cursors.write(c);
2846
+ batch = [];
2847
+ pending = {};
2848
+ return true;
2849
+ };
2571
2850
  for (const ref of refs) {
2572
- if (sent >= MAX_PER_RUN) break;
2573
- let offset = c[cursors.key(ref)] ?? 0;
2574
- let stalled = false;
2575
- while (sent < MAX_PER_RUN) {
2576
- const { events, offset: next } = outbox.read(ref, offset, BATCH);
2851
+ if (stalled || sent >= MAX_PER_RUN) break;
2852
+ let offset = pending[cursors.key(ref)] ?? c[cursors.key(ref)] ?? 0;
2853
+ for (; ; ) {
2854
+ const room = BATCH - batch.length;
2855
+ const { events, offset: next } = outbox.read(ref, offset, room);
2577
2856
  if (!events.length) break;
2578
- const filtered = events.map((e) => filterEvent(e, sink.tier, sink.pii, { home: os2.homedir() })).filter((e) => !!e);
2579
- let ok = true;
2580
- if (filtered.length) {
2581
- const res = await sink.send(filtered);
2582
- ok = res.ok;
2583
- if (!ok) {
2584
- log(state, `sink ${sink.name}: ${res.status ?? ""} ${res.error ?? ""} \u2192 ${res.action ?? "retry"}`);
2585
- if (res.action === "pause" || res.action === "lower-tier") applySinkVerdict(sink.name, res);
2586
- if (res.action === "drop") ok = true;
2587
- }
2588
- if (ok) sent += filtered.length;
2589
- }
2590
- if (!ok) {
2591
- stalled = true;
2592
- break;
2857
+ for (const e of events) {
2858
+ const f = filterEvent(e, sink.tier, sink.pii, { home: os3.homedir() });
2859
+ if (f) batch.push(f);
2593
2860
  }
2594
2861
  offset = next;
2595
- c[cursors.key(ref)] = offset;
2596
- cursors.write(c);
2862
+ pending[cursors.key(ref)] = offset;
2863
+ if (batch.length >= BATCH) {
2864
+ if (!await send()) {
2865
+ stalled = true;
2866
+ break;
2867
+ }
2868
+ if (sent >= MAX_PER_RUN) break;
2869
+ }
2597
2870
  }
2598
- if (stalled) break;
2871
+ }
2872
+ if (!stalled && (batch.length || Object.keys(pending).length)) {
2873
+ if (!await send()) stalled = true;
2599
2874
  }
2600
2875
  delivered[sink.name] = sent;
2601
2876
  }
@@ -2612,6 +2887,7 @@ function applySinkVerdict(name, res) {
2612
2887
  } catch {
2613
2888
  }
2614
2889
  }
2890
+ var jobsDir = (state) => path12.join(state, "jobs");
2615
2891
 
2616
2892
  // src/worker.ts
2617
2893
  var jobFile = process.argv[2];
@@ -2622,12 +2898,13 @@ try {
2622
2898
  } catch {
2623
2899
  }
2624
2900
  try {
2625
- unlinkSync2(jobFile);
2901
+ unlinkSync5(jobFile);
2626
2902
  } catch {
2627
2903
  }
2628
2904
  if (!job) process.exit(0);
2629
2905
  var deadline = setTimeout(() => process.exit(0), 15e3);
2630
2906
  deadline.unref();
2631
2907
  runJob(job).catch(() => {
2908
+ }).then(() => sweepJobs(stateDir())).catch(() => {
2632
2909
  }).finally(() => process.exit(0));
2633
2910
  //# sourceMappingURL=worker.js.map