sessionpipe-core 0.6.0 → 0.7.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,4 +1,4 @@
1
- // sessionpipe-core 0.6.0 · Apache-2.0 · https://sessionpipe.org
1
+ // sessionpipe-core 0.7.0 · Apache-2.0 · https://sessionpipe.org
2
2
  var __defProp = Object.defineProperty;
3
3
  var __export = (target, all) => {
4
4
  for (var name in all)
@@ -191,6 +191,9 @@ function readClaude(lines, from) {
191
191
  }
192
192
  return pairTurns(msgs, endOf(lines));
193
193
  }
194
+ function codexInjected(text) {
195
+ return /^<[a-z_-]+>/i.test(text) || /^# AGENTS\.md instructions\b/i.test(text);
196
+ }
194
197
  function readCodex(lines, from) {
195
198
  const msgs = [];
196
199
  for (let i = from; i < lines.length; i++) {
@@ -202,7 +205,7 @@ function readCodex(lines, from) {
202
205
  const text = textParts(p.content, ["input_text", "output_text"]).trim();
203
206
  if (!text) continue;
204
207
  if (p.role === "user") {
205
- if (/^<[a-z_-]+>/i.test(text)) continue;
208
+ if (codexInjected(text)) continue;
206
209
  msgs.push({ role: "user", text, at, line: i });
207
210
  } else if (p.role === "assistant") msgs.push({ role: "assistant", text, at, line: i });
208
211
  }
@@ -373,20 +376,19 @@ function installedClaudeShaped(file, harness, events, cmd, o) {
373
376
  // src/adapters/antigravity.ts
374
377
  var NAME = "antigravity";
375
378
  var HOOK_NAME = "sessionpipe";
376
- var AG_EVENTS = ["PreInvocation", "PostInvocation", "PreToolUse", "PostToolUse", "Stop"];
379
+ var AG_EVENTS = ["PreInvocation", "PostInvocation", "PostToolUse", "Stop"];
380
+ var AG_STALE_PRETOOL = '{"decision":"ask","reason":"sessionpipe: an outdated PreToolUse hook is still loaded. Restart this conversation (or run sessionpipe install) to drop it."}\n';
381
+ var antigravityAnswer = (event) => event === "PreToolUse" ? AG_STALE_PRETOOL : "{}\n";
377
382
  var gemini = (env) => env.GEMINI_CLI_HOME || path4.join(HOME, ".gemini");
378
383
  var hooksFile = (env) => path4.join(gemini(env), "config", "hooks.json");
379
384
  var projectsDir = (env) => path4.join(gemini(env), "config", "projects");
380
385
  var dataDirs = (env) => ["antigravity", "antigravity-cli", "antigravity-ide"].map((d) => path4.join(gemini(env), d));
381
386
  var ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
382
387
  var validTool = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
383
- function entry(cmd, lean) {
388
+ function entry(cmd) {
384
389
  const handler = (e) => ({ type: "command", command: cmd([NAME, e]).command, timeout: 5 });
385
390
  const out = { enabled: true };
386
- for (const e of AG_EVENTS) {
387
- if (lean && e === "PreToolUse") continue;
388
- out[e] = /Tool/.test(e) ? [{ matcher: "*", hooks: [handler(e)] }] : [handler(e)];
389
- }
391
+ for (const e of AG_EVENTS) out[e] = /Tool/.test(e) ? [{ matcher: "*", hooks: [handler(e)] }] : [handler(e)];
390
392
  return out;
391
393
  }
392
394
  var antigravity = {
@@ -394,12 +396,12 @@ var antigravity = {
394
396
  events: AG_EVENTS,
395
397
  detect: (env = process.env) => existsSync2(path4.join(gemini(env), "config")) || dataDirs(env).some((d) => existsSync2(d)),
396
398
  configFiles: (env = process.env) => [hooksFile(env)],
397
- install(cmd, env = process.env, opts = {}) {
399
+ install(cmd, env = process.env) {
398
400
  const file = hooksFile(env);
399
401
  const f = readJsonFile(file);
400
402
  if (!f.ok) return [{ file, changed: false, skipped: true, note: `${f.reason}; not touched` }];
401
403
  const hooks = f.value;
402
- const e = entry(cmd, opts.lean);
404
+ const e = entry(cmd);
403
405
  if (JSON.stringify(hooks[HOOK_NAME]) === JSON.stringify(e)) return [{ file, changed: false }];
404
406
  hooks[HOOK_NAME] = e;
405
407
  mkdirSync2(path4.dirname(file), { recursive: true });
@@ -428,14 +430,14 @@ var antigravity = {
428
430
  writeFileSync2(file, formatJson(hooks, f));
429
431
  return [{ file, changed: true }];
430
432
  },
431
- installed(cmd, env = process.env, opts = {}) {
433
+ installed(cmd, env = process.env) {
432
434
  const file = hooksFile(env);
433
435
  const f = readJsonFile(file);
434
436
  const have = f.ok ? f.value[HOOK_NAME] : void 0;
435
437
  return [
436
438
  {
437
439
  file,
438
- state: !have ? "missing" : JSON.stringify(have) === JSON.stringify(entry(cmd, opts.lean)) ? "current" : "stale"
440
+ state: !have ? "missing" : JSON.stringify(have) === JSON.stringify(entry(cmd)) ? "current" : "stale"
439
441
  }
440
442
  ];
441
443
  },
@@ -447,7 +449,8 @@ var antigravity = {
447
449
  } catch {
448
450
  }
449
451
  const id = s.conversationId ?? s.session_id ?? s.sessionId;
450
- if (!id) return { session: { id: "" }, events: [], stdout: "{}\n" };
452
+ const stdout = antigravityAnswer(event);
453
+ if (!id) return { session: { id: "" }, events: [], stdout };
451
454
  const workspaces = agWorkspaces(s.workspacePaths);
452
455
  const session = { id: String(id), cwd: workspaces[0] ?? input.cwd };
453
456
  if (typeof s.modelName === "string" && s.modelName !== "auto") session.model = s.modelName;
@@ -468,7 +471,8 @@ var antigravity = {
468
471
  if (tool) events.push(ev("tool.started", { tool, input: toolCall?.args }));
469
472
  break;
470
473
  case "PostToolUse":
471
- if (tool)
474
+ if (!tool) events.push(ev("session.heartbeat", {}));
475
+ else
472
476
  events.push(
473
477
  ev("tool.ended", {
474
478
  tool,
@@ -482,9 +486,9 @@ var antigravity = {
482
486
  events.push(ev("turn.ended", { reason: s.error ? "error" : "stop" }));
483
487
  break;
484
488
  default:
485
- return { session, events: [], stdout: "{}\n" };
489
+ return { session, events: [], stdout };
486
490
  }
487
- const r = { session, events, stdout: "{}\n", hints: { workspaces } };
491
+ const r = { session, events, stdout, hints: { workspaces } };
488
492
  if (typeof s.transcriptPath === "string") r.transcript = s.transcriptPath;
489
493
  return r;
490
494
  },
@@ -648,11 +652,288 @@ function antigravityFacts(file, opts) {
648
652
  return out;
649
653
  }
650
654
 
655
+ // src/adapters/antigravity-control.ts
656
+ var antigravity_control_exports = {};
657
+ __export(antigravity_control_exports, {
658
+ AgyReading: () => AgyReading,
659
+ agyArgs: () => agyArgs,
660
+ agyEnv: () => agyEnv,
661
+ agyError: () => agyError,
662
+ agyLine: () => agyLine,
663
+ agyModeFlags: () => agyModeFlags,
664
+ findAgy: () => findAgy,
665
+ reading: () => reading
666
+ });
667
+ import os2 from "node:os";
668
+ import path6 from "node:path";
669
+
670
+ // src/adapters/headless.ts
671
+ import { spawn } from "node:child_process";
672
+ import { accessSync, constants, statSync as statSync3 } from "node:fs";
673
+ import path5 from "node:path";
674
+ var JOB_TIMEOUT_MS = 30 * 6e4;
675
+ var OUT_CAP = 1024 * 1024;
676
+ function isExe(p) {
677
+ try {
678
+ if (!statSync3(p).isFile()) return false;
679
+ accessSync(p, constants.X_OK);
680
+ return true;
681
+ } catch {
682
+ return false;
683
+ }
684
+ }
685
+ function findBinary(o) {
686
+ if (o.override && isExe(o.override)) return o.override;
687
+ for (const d of String(o.env?.PATH || "").split(path5.delimiter)) {
688
+ if (!d || !path5.isAbsolute(d)) continue;
689
+ for (const n of o.names) if (isExe(path5.join(d, n))) return path5.join(d, n);
690
+ }
691
+ for (const d of o.dirs) for (const n of o.names) if (isExe(path5.join(d, n))) return path5.join(d, n);
692
+ return null;
693
+ }
694
+ function jobEnv(env = process.env) {
695
+ return { ...env, SESSIONPIPE_CONTROL_JOB: "1" };
696
+ }
697
+ function runHeadless(bin, args, o) {
698
+ return new Promise((resolve) => {
699
+ let out = [];
700
+ let outLen = 0;
701
+ let tooBig = false;
702
+ let errTail = "";
703
+ let timedOut = false;
704
+ let settled = false;
705
+ let child;
706
+ try {
707
+ child = spawn(bin, args, {
708
+ cwd: o.cwd,
709
+ env: o.env,
710
+ stdio: ["ignore", "pipe", "pipe"],
711
+ detached: process.platform !== "win32",
712
+ windowsHide: true
713
+ });
714
+ } catch (e) {
715
+ resolve({ error: e.message });
716
+ return;
717
+ }
718
+ const kill = (sig) => {
719
+ try {
720
+ if (process.platform !== "win32" && child.pid) process.kill(-child.pid, sig);
721
+ else child.kill(sig);
722
+ } catch {
723
+ try {
724
+ child.kill(sig);
725
+ } catch {
726
+ }
727
+ }
728
+ };
729
+ const keep = (line) => {
730
+ if (!o.keep?.(line)) return;
731
+ out = [Buffer.from(line)];
732
+ outLen = line.length;
733
+ };
734
+ let partial = "";
735
+ child.stdout?.on("data", (b) => {
736
+ if (o.onLine) {
737
+ const chunk = partial + b.toString("utf8");
738
+ const lines = chunk.split("\n");
739
+ partial = lines.pop() ?? "";
740
+ for (const line of lines) {
741
+ if (!line.trim()) continue;
742
+ try {
743
+ o.onLine(line);
744
+ } catch {
745
+ }
746
+ keep(line);
747
+ }
748
+ if (partial.length > OUT_CAP) partial = "";
749
+ return;
750
+ }
751
+ if (tooBig) return;
752
+ if (outLen + b.length > OUT_CAP) {
753
+ tooBig = true;
754
+ out = [];
755
+ return;
756
+ }
757
+ out.push(b);
758
+ outLen += b.length;
759
+ });
760
+ child.stderr?.on("data", (b) => {
761
+ errTail = (errTail + b.toString("utf8")).slice(-4e3);
762
+ });
763
+ const timer = setTimeout(() => {
764
+ timedOut = true;
765
+ kill("SIGTERM");
766
+ setTimeout(() => kill("SIGKILL"), 1e4).unref();
767
+ }, o.timeoutMs ?? JOB_TIMEOUT_MS);
768
+ const result = (code, signal) => ({
769
+ code,
770
+ signal,
771
+ stdout: tooBig ? null : Buffer.concat(out).toString("utf8"),
772
+ tooBig,
773
+ stderr: errTail,
774
+ timedOut
775
+ });
776
+ const finish = (r) => {
777
+ if (settled) return;
778
+ settled = true;
779
+ clearTimeout(timer);
780
+ resolve(r);
781
+ };
782
+ child.on("error", (e) => finish({ error: e.message }));
783
+ child.on("exit", (code, signal) => setTimeout(() => finish(result(code, signal)), 2e3).unref());
784
+ child.on("close", (code, signal) => {
785
+ if (o.onLine && partial.trim()) {
786
+ try {
787
+ o.onLine(partial);
788
+ } catch {
789
+ }
790
+ keep(partial);
791
+ partial = "";
792
+ }
793
+ finish(result(code, signal));
794
+ });
795
+ });
796
+ }
797
+ function stderrTail(stderr, n = 3) {
798
+ return String(stderr || "").trim().split("\n").slice(-n).join(" ").trim();
799
+ }
800
+ function deniedNote(names, count) {
801
+ if (!count) return "";
802
+ return `
803
+
804
+ (Not allowed on this machine, since nobody was there to approve: ${names.join(", ") || `${count} action(s)`}.)`;
805
+ }
806
+ function runFailure(label, res) {
807
+ if (res.error) return `couldn't start ${label}: ${res.error}`;
808
+ if (res.timedOut) return `${label} was still working after 30 minutes, so it was stopped`;
809
+ return null;
810
+ }
811
+
812
+ // src/adapters/antigravity-control.ts
813
+ var LABEL = "Antigravity";
814
+ function findAgy(env = process.env, home = os2.homedir()) {
815
+ return findBinary({
816
+ override: env.SESSIONPIPE_AGY,
817
+ names: process.platform === "win32" ? ["agy.exe", "agy.cmd"] : ["agy"],
818
+ env,
819
+ dirs: [path6.join(home, ".local", "bin"), "/opt/homebrew/bin", "/usr/local/bin"]
820
+ });
821
+ }
822
+ function agyModeFlags(mode) {
823
+ return mode === "auto" ? ["--mode", "accept-edits"] : [];
824
+ }
825
+ function agyArgs(o) {
826
+ const prompt = o.prompt.startsWith("-") ? ` ${o.prompt}` : o.prompt;
827
+ return [
828
+ ...o.resume ? ["--conversation", o.resume] : [],
829
+ "-p",
830
+ prompt,
831
+ "--output-format",
832
+ "stream-json",
833
+ ...agyModeFlags(o.mode)
834
+ ];
835
+ }
836
+ var agyEnv = (env = process.env) => jobEnv(env);
837
+ function agyLine(line) {
838
+ let j;
839
+ try {
840
+ j = JSON.parse(line);
841
+ } catch {
842
+ return null;
843
+ }
844
+ if (!j || typeof j !== "object") return null;
845
+ const str = (v) => typeof v === "string" ? v : "";
846
+ const out = {};
847
+ if (j.event === "init") {
848
+ if (str(j.conversation_id)) out.session = str(j.conversation_id);
849
+ } else if (j.event === "step_update") {
850
+ const s = j.step_update ?? {};
851
+ if (str(s.conversation_id)) out.session = str(s.conversation_id);
852
+ if (s.step_type === "agent_response" && str(s.text_delta))
853
+ out.delta = { step: Number(s.step_index ?? 0), text: str(s.text_delta) };
854
+ if (s.step_type === "error_message") {
855
+ const why = str(s.text_delta) || str(s.error) || str(s.message);
856
+ if (why) out.note = why;
857
+ }
858
+ } else if (j.event === "result") {
859
+ const r = j.result ?? {};
860
+ if (str(r.conversation_id)) out.session = str(r.conversation_id);
861
+ out.result = {
862
+ status: str(r.status),
863
+ response: str(r.response),
864
+ error: str(r.error),
865
+ denied: Array.isArray(r.denied_actions) ? r.denied_actions : []
866
+ };
867
+ } else return null;
868
+ return Object.keys(out).length ? out : null;
869
+ }
870
+ function deniedName(d) {
871
+ if (typeof d === "string") return d.slice(0, 80);
872
+ if (!d || typeof d !== "object") return null;
873
+ const o = d;
874
+ for (const k of ["tool_name", "toolName", "name", "tool", "action", "type"])
875
+ if (typeof o[k] === "string" && o[k]) return o[k].slice(0, 80);
876
+ return null;
877
+ }
878
+ function agyError(stderr) {
879
+ const s = String(stderr || "");
880
+ const i = s.lastIndexOf("AGY_ERROR:");
881
+ if (i < 0) return "";
882
+ const raw = s.slice(i + "AGY_ERROR:".length).trim();
883
+ try {
884
+ const j = JSON.parse(raw);
885
+ for (const k of ["message", "error", "detail"]) if (typeof j[k] === "string" && j[k]) return j[k];
886
+ } catch {
887
+ }
888
+ return raw.split("\n")[0]?.slice(0, 300) ?? "";
889
+ }
890
+ var AgyReading = class {
891
+ session = null;
892
+ lines = 0;
893
+ buf = "";
894
+ step = null;
895
+ result = null;
896
+ note = null;
897
+ get text() {
898
+ return this.buf;
899
+ }
900
+ feed(line) {
901
+ this.lines++;
902
+ const p = agyLine(line);
903
+ if (!p) return false;
904
+ if (p.session && !this.session) this.session = p.session;
905
+ if (p.note) this.note = p.note;
906
+ if (p.result) this.result = p.result;
907
+ if (!p.delta) return false;
908
+ if (this.step !== null && p.delta.step !== this.step && this.buf && !this.buf.endsWith("\n\n")) this.buf += "\n\n";
909
+ this.step = p.delta.step;
910
+ this.buf += p.delta.text;
911
+ return true;
912
+ }
913
+ outcome(res) {
914
+ const broke = runFailure(LABEL, res);
915
+ if (broke) return { failed: broke };
916
+ const r = this.result;
917
+ if (!r) {
918
+ const why = agyError(res.stderr) || this.note || "";
919
+ return {
920
+ failed: `Antigravity exited (${res.signal || `code ${res.code}`}) without an answer${why ? `: ${why}` : ""}`
921
+ };
922
+ }
923
+ if (r.status !== "SUCCESS")
924
+ return { failed: r.error || agyError(res.stderr) || this.note || "Antigravity reported an error" };
925
+ const names = [...new Set(r.denied.map(deniedName).filter((n) => !!n))];
926
+ const text = r.response.trim() ? r.response.trim() : this.buf.trim() || "(Antigravity finished without a written reply.)";
927
+ return { reply: text + deniedNote(names, r.denied.length) };
928
+ }
929
+ };
930
+ var reading = () => new AgyReading();
931
+
651
932
  // src/adapters/claude-code.ts
652
933
  import { execFileSync as execFileSync2 } from "node:child_process";
653
934
  import { createHash } from "node:crypto";
654
- import { existsSync as existsSync3, readdirSync as readdirSync3, readFileSync as readFileSync3, statSync as statSync3 } from "node:fs";
655
- import path5 from "node:path";
935
+ import { existsSync as existsSync3, readdirSync as readdirSync3, readFileSync as readFileSync3, statSync as statSync4 } from "node:fs";
936
+ import path7 from "node:path";
656
937
 
657
938
  // src/readers/git.ts
658
939
  import { execFileSync } from "node:child_process";
@@ -719,23 +1000,23 @@ function claudeDirs(env = process.env, home = env.HOME || HOME) {
719
1000
  const out = [];
720
1001
  const add = (d) => {
721
1002
  if (!d) return;
722
- const r = path5.resolve(d);
1003
+ const r = path7.resolve(d);
723
1004
  if (!out.includes(r)) out.push(r);
724
1005
  };
725
- add(path5.join(home, ".claude"));
1006
+ add(path7.join(home, ".claude"));
726
1007
  add(env.CLAUDE_CONFIG_DIR);
727
1008
  try {
728
1009
  for (const e of readdirSync3(home, { withFileTypes: true })) {
729
1010
  if (!e.isDirectory() || !/^\.claude[-_.]/.test(e.name)) continue;
730
- const d = path5.join(home, e.name);
731
- if (existsSync3(path5.join(d, "projects")) || existsSync3(path5.join(d, "settings.json"))) add(d);
1011
+ const d = path7.join(home, e.name);
1012
+ if (existsSync3(path7.join(d, "projects")) || existsSync3(path7.join(d, "settings.json"))) add(d);
732
1013
  }
733
1014
  } catch {
734
1015
  }
735
1016
  return out.filter((d, i) => i === 0 || existsSync3(d));
736
1017
  }
737
- var settingsIn = (dir) => path5.join(dir, "settings.json");
738
- var configDirOf = (transcript) => path5.dirname(path5.dirname(path5.dirname(transcript)));
1018
+ var settingsIn = (dir) => path7.join(dir, "settings.json");
1019
+ var configDirOf = (transcript) => path7.dirname(path7.dirname(path7.dirname(transcript)));
739
1020
  var validTool2 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
740
1021
  var endReason = (r) => r === "clear" || r === "logout" || r === "exit" ? r : r === "prompt_input_exit" ? "exit" : "other";
741
1022
  var notifKind = (t) => t === "permission_prompt" ? "permission" : t === "idle_prompt" ? "idle" : t === "elicitation_dialog" ? "elicitation" : "question";
@@ -907,11 +1188,11 @@ var claudeCode = {
907
1188
  const rows = [];
908
1189
  const files = [];
909
1190
  for (const d of claudeDirs(env))
910
- files.push(...recentFiles(path5.join(d, "projects"), 1, sinceMs, (n) => /^[0-9a-f-]{36}\.jsonl$/.test(n)));
1191
+ files.push(...recentFiles(path7.join(d, "projects"), 1, sinceMs, (n) => /^[0-9a-f-]{36}\.jsonl$/.test(n)));
911
1192
  const noState = { get: () => ({}), save: () => {
912
1193
  } };
913
1194
  for (const f of files) {
914
- const id = path5.basename(f, ".jsonl");
1195
+ const id = path7.basename(f, ".jsonl");
915
1196
  const facts = claudeFacts(f, id, {}, noState);
916
1197
  if (!facts.started_at || !facts.last_at) continue;
917
1198
  const { started_at, last_at, first_ask: _fa, harness_version: _hv, ...rest } = facts;
@@ -971,12 +1252,12 @@ function claudeLive(dir) {
971
1252
  const map = /* @__PURE__ */ new Map();
972
1253
  let names = [];
973
1254
  try {
974
- names = readdirSync3(path5.join(dir, "sessions")).filter((n) => n.endsWith(".json"));
1255
+ names = readdirSync3(path7.join(dir, "sessions")).filter((n) => n.endsWith(".json"));
975
1256
  } catch {
976
1257
  }
977
1258
  for (const n of names) {
978
1259
  try {
979
- const e = registryEntry(JSON.parse(readFileSync3(path5.join(dir, "sessions", n), "utf8")));
1260
+ const e = registryEntry(JSON.parse(readFileSync3(path7.join(dir, "sessions", n), "utf8")));
980
1261
  if (e?.sessionId) map.set(e.sessionId, e);
981
1262
  } catch {
982
1263
  }
@@ -987,7 +1268,7 @@ function claudeLive(dir) {
987
1268
  function claudeLiveByPid(dir, pid) {
988
1269
  if (!pid) return null;
989
1270
  try {
990
- return registryEntry(JSON.parse(readFileSync3(path5.join(dir, "sessions", `${pid}.json`), "utf8")));
1271
+ return registryEntry(JSON.parse(readFileSync3(path7.join(dir, "sessions", `${pid}.json`), "utf8")));
991
1272
  } catch {
992
1273
  return null;
993
1274
  }
@@ -995,7 +1276,7 @@ function claudeLiveByPid(dir, pid) {
995
1276
  function claudePid(env = process.env) {
996
1277
  try {
997
1278
  const dirs = claudeDirs(env);
998
- const has = (pid2) => dirs.some((d) => existsSync3(path5.join(d, "sessions", `${pid2}.json`)));
1279
+ const has = (pid2) => dirs.some((d) => existsSync3(path7.join(d, "sessions", `${pid2}.json`)));
999
1280
  let pid = process.ppid;
1000
1281
  if (has(pid)) return pid;
1001
1282
  const parent = parentOf();
@@ -1038,8 +1319,8 @@ var accountCache = /* @__PURE__ */ new Map();
1038
1319
  function claudeAccount(dir) {
1039
1320
  const hit = accountCache.get(dir);
1040
1321
  if (hit !== void 0) return hit;
1041
- const files = [path5.join(dir, ".claude.json")];
1042
- if (path5.resolve(dir) === path5.join(HOME, ".claude")) files.unshift(path5.join(HOME, ".claude.json"));
1322
+ const files = [path7.join(dir, ".claude.json")];
1323
+ if (path7.resolve(dir) === path7.join(HOME, ".claude")) files.unshift(path7.join(HOME, ".claude.json"));
1043
1324
  let acct = null;
1044
1325
  for (const f of files) {
1045
1326
  try {
@@ -1091,7 +1372,7 @@ function scanTitles(file, from, found) {
1091
1372
  const out = { custom: found.custom ?? null, ai: found.ai ?? null, upto: from };
1092
1373
  let size = 0;
1093
1374
  try {
1094
- size = statSync3(file).size;
1375
+ size = statSync4(file).size;
1095
1376
  } catch {
1096
1377
  return out;
1097
1378
  }
@@ -1124,7 +1405,7 @@ function claudeFacts(file, sessionId, hints, state) {
1124
1405
  const H = parseLines(head);
1125
1406
  const T = parseLines(tail);
1126
1407
  const out = {};
1127
- const id = sessionId || path5.basename(file, ".jsonl");
1408
+ const id = sessionId || path7.basename(file, ".jsonl");
1128
1409
  const mem = state.get(NAME2, id);
1129
1410
  let custom = null;
1130
1411
  let ai = null;
@@ -1134,7 +1415,7 @@ function claudeFacts(file, sessionId, hints, state) {
1134
1415
  }
1135
1416
  let size = 0;
1136
1417
  try {
1137
- size = statSync3(file).size;
1418
+ size = statSync4(file).size;
1138
1419
  } catch {
1139
1420
  }
1140
1421
  if (!custom && !ai && size > 64 * 1024 + 256 * 1024) {
@@ -1204,548 +1485,644 @@ function claudeFacts(file, sessionId, hints, state) {
1204
1485
  return out;
1205
1486
  }
1206
1487
 
1207
- // src/adapters/codex.ts
1208
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
1209
- import path6 from "node:path";
1210
-
1211
- // src/adapters/codex-config.ts
1212
- var NOTIFY_LINE = /^[ \t]*notify[ \t]*=[ \t]*(\[.*\])[ \t]*(?:#.*)?$/m;
1213
- var NOTIFY_KEY = /^[ \t]*notify[ \t]*=/m;
1214
- var OURS2 = /(?:hook\.js|sessionpipe-hook)",\s*"codex",\s*"notify"/;
1215
- function scan(line, st) {
1216
- let i = 0;
1217
- while (i < line.length) {
1218
- if (st.str) {
1219
- const j = line.indexOf(st.str, i);
1220
- if (j < 0) return;
1221
- i = j + 3;
1222
- st.str = null;
1223
- continue;
1224
- }
1225
- const ch = line[i];
1226
- if (ch === "#") return;
1227
- if (line.startsWith('"""', i) || line.startsWith("'''", i)) {
1228
- st.str = line.slice(i, i + 3);
1229
- i += 3;
1230
- continue;
1231
- }
1232
- if (ch === '"') {
1233
- i++;
1234
- while (i < line.length && line[i] !== '"') i += line[i] === "\\" ? 2 : 1;
1235
- i++;
1236
- continue;
1237
- }
1238
- if (ch === "'") {
1239
- const j = line.indexOf("'", i + 1);
1240
- i = j < 0 ? line.length : j + 1;
1241
- continue;
1242
- }
1243
- if (ch === "[") st.depth++;
1244
- else if (ch === "]") st.depth = Math.max(0, st.depth - 1);
1245
- i++;
1246
- }
1247
- }
1248
- function firstTableAt(text) {
1249
- const st = { depth: 0, str: null };
1250
- let pos = 0;
1251
- for (const line of text.split("\n")) {
1252
- if (!st.str && st.depth === 0 && /^[ \t]*\[/.test(line)) return pos;
1253
- scan(line, st);
1254
- pos += line.length + 1;
1488
+ // src/adapters/claude-code-control.ts
1489
+ var claude_code_control_exports = {};
1490
+ __export(claude_code_control_exports, {
1491
+ JOB_TIMEOUT_MS: () => JOB_TIMEOUT_MS,
1492
+ claudeEnv: () => claudeEnv,
1493
+ claudeLogin: () => claudeLogin,
1494
+ detectCaps: () => detectCaps,
1495
+ findClaude: () => findClaude,
1496
+ findTranscript: () => findTranscript,
1497
+ isUuid: () => isUuid,
1498
+ liveProcess: () => liveProcess,
1499
+ loginCommand: () => loginCommand,
1500
+ loginHint: () => loginHint,
1501
+ modeFlags: () => modeFlags,
1502
+ parseAuthStatus: () => parseAuthStatus,
1503
+ parseCaps: () => parseCaps,
1504
+ parseResult: () => parseResult,
1505
+ recentlyWritten: () => recentlyWritten,
1506
+ runClaude: () => runClaude,
1507
+ sessionFolder: () => sessionFolder
1508
+ });
1509
+ import { execFile, execFileSync as execFileSync3 } from "node:child_process";
1510
+ import { closeSync as closeSync2, openSync as openSync2, readdirSync as readdirSync4, readFileSync as readFileSync4, readSync as readSync2, statSync as statSync5 } from "node:fs";
1511
+ import os3 from "node:os";
1512
+ import path8 from "node:path";
1513
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1514
+ var LIVE_WRITE_MS = 9e4;
1515
+ var cmpVersion = (a, b) => {
1516
+ const pa = a.split(".").map(Number);
1517
+ const pb = b.split(".").map(Number);
1518
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++)
1519
+ if ((pa[i] ?? 0) !== (pb[i] ?? 0)) return (pa[i] ?? 0) - (pb[i] ?? 0);
1520
+ return 0;
1521
+ };
1522
+ function findClaude(env = process.env, home = os3.homedir()) {
1523
+ if (env.SESSIONPIPE_CLAUDE && isExe(env.SESSIONPIPE_CLAUDE)) return env.SESSIONPIPE_CLAUDE;
1524
+ const names = process.platform === "win32" ? ["claude.exe", "claude.cmd"] : ["claude"];
1525
+ for (const d of String(env.PATH || "").split(path8.delimiter)) {
1526
+ if (!d || !path8.isAbsolute(d)) continue;
1527
+ for (const n of names) if (isExe(path8.join(d, n))) return path8.join(d, n);
1255
1528
  }
1256
- return -1;
1257
- }
1258
- function split(text) {
1259
- const at = firstTableAt(text);
1260
- return at < 0 ? { head: text, tail: "", at: text.length } : { head: text.slice(0, at), tail: text.slice(at), at };
1261
- }
1262
- function parseArgv(raw) {
1529
+ for (const p of [
1530
+ path8.join(home, ".claude", "local", "claude"),
1531
+ path8.join(home, ".local", "bin", "claude"),
1532
+ "/opt/homebrew/bin/claude",
1533
+ "/usr/local/bin/claude"
1534
+ ])
1535
+ if (isExe(p)) return p;
1536
+ const app = path8.join(home, "Library", "Application Support", "Claude", "claude-code");
1537
+ let versions = [];
1263
1538
  try {
1264
- const a = JSON.parse(raw);
1265
- return Array.isArray(a) && a.every((x) => typeof x === "string") ? a : null;
1539
+ versions = readdirSync4(app).filter((n) => /^\d+(\.\d+)*$/.test(n)).sort(cmpVersion).reverse();
1266
1540
  } catch {
1267
- return null;
1268
1541
  }
1542
+ for (const v of versions) {
1543
+ const p = path8.join(app, v, "claude.app", "Contents", "MacOS", "claude");
1544
+ if (isExe(p)) return p;
1545
+ }
1546
+ return null;
1269
1547
  }
1270
- function readNotify(text) {
1271
- const { head, tail, at } = split(text);
1272
- const m = NOTIFY_LINE.exec(head);
1273
- if (m)
1274
- return {
1275
- raw: m[1] ?? "",
1276
- argv: parseArgv(m[1] ?? ""),
1277
- index: m.index,
1278
- length: m[0].length,
1279
- misplaced: false,
1280
- other: !OURS2.test(m[1] ?? "")
1281
- };
1282
- const k = NOTIFY_KEY.exec(head);
1283
- if (k) {
1284
- const rest = head.slice(k.index).split("\n")[0] ?? "";
1285
- return {
1286
- raw: rest.replace(NOTIFY_KEY, "").trim(),
1287
- argv: null,
1288
- index: k.index,
1289
- length: 0,
1290
- misplaced: false,
1291
- other: true
1292
- };
1293
- }
1294
- const t = NOTIFY_LINE.exec(tail);
1295
- if (t && OURS2.test(t[1] ?? ""))
1296
- return {
1297
- raw: t[1] ?? "",
1298
- argv: parseArgv(t[1] ?? ""),
1299
- index: at + t.index,
1300
- length: t[0].length,
1301
- misplaced: true,
1302
- other: false
1303
- };
1304
- return null;
1305
- }
1306
- function dropLine(text, index, length) {
1307
- return text.slice(0, index) + text.slice(index + length).replace(/^\r?\n/, "");
1308
- }
1309
- function insertTopLevel(text, line) {
1310
- const { head, tail } = split(text);
1311
- if (!tail) return (head.trim() ? head.replace(/\s*$/, "\n") : "") + line + "\n";
1312
- return `${(head.trim() ? head.replace(/\s*$/, "\n") : "") + line}
1313
-
1314
- ${tail}`;
1315
- }
1316
- function withNotify(text, line) {
1317
- const n = readNotify(text);
1318
- if (n && !n.misplaced && !n.other) return text.slice(0, n.index) + line + text.slice(n.index + n.length);
1319
- if (n?.misplaced) return insertTopLevel(dropLine(text, n.index, n.length), line);
1320
- return insertTopLevel(text, line);
1321
- }
1322
- function replaceNotify(text, line) {
1323
- const n = readNotify(text);
1324
- if (!n || n.misplaced || !n.length) return null;
1325
- return text.slice(0, n.index) + line + text.slice(n.index + n.length);
1326
- }
1327
- function withoutNotify(text) {
1328
- const n = readNotify(text);
1329
- if (!n || n.other || !n.length) return text;
1330
- return dropLine(text, n.index, n.length);
1548
+ function optionBlock(help, flag) {
1549
+ const lines = help.split("\n");
1550
+ const re = new RegExp(`^\\s{0,4}(?:-\\w, )?${flag.replace(/-/g, "\\-")}(?![\\w-])`);
1551
+ const start = lines.findIndex((l) => re.test(l));
1552
+ if (start < 0) return null;
1553
+ const out = [lines[start]];
1554
+ for (let i = start + 1; i < lines.length && lines[i]?.trim() && !/^\s{0,4}-/.test(lines[i]); i++)
1555
+ out.push(lines[i]);
1556
+ return out.join(" ");
1331
1557
  }
1332
- var PREVIOUS = "--previous-notify";
1333
- function previousNotify(argv) {
1334
- if (!Array.isArray(argv)) return null;
1335
- const i = argv.indexOf(PREVIOUS);
1336
- return i >= 0 && typeof argv[i + 1] === "string" ? parseArgv(argv[i + 1]) : null;
1558
+ function parseCaps(help) {
1559
+ const pm = optionBlock(help, "--permission-mode");
1560
+ const pp = optionBlock(help, "--permission-prompts");
1561
+ return {
1562
+ hasMode: !!pm,
1563
+ modes: pm ? [...pm.matchAll(/"([A-Za-z]+)"/g)].map((m) => m[1]) : [],
1564
+ promptsNone: !!pp && /"none"/.test(pp),
1565
+ fork: !!optionBlock(help, "--fork-session"),
1566
+ stream: !!optionBlock(help, "--include-partial-messages")
1567
+ };
1337
1568
  }
1338
- function stripPreviousRaw(raw) {
1339
- return raw.replace(/,\s*"--previous-notify",\s*"(?:[^"\\]|\\.)*"\s*(?=\])/, "");
1569
+ function modeFlags(mode, caps) {
1570
+ if (mode === "auto") {
1571
+ if (!caps.modes.includes("auto"))
1572
+ return { error: "this machine is in auto mode, but its Claude Code has no auto permission mode" };
1573
+ return { args: ["--permission-mode", "auto"] };
1574
+ }
1575
+ if (caps.modes.includes("dontAsk")) return { args: ["--permission-mode", "dontAsk"] };
1576
+ const args = [];
1577
+ if (caps.hasMode)
1578
+ args.push(
1579
+ "--permission-mode",
1580
+ caps.modes.includes("manual") && !caps.modes.includes("default") ? "manual" : "default"
1581
+ );
1582
+ if (caps.promptsNone) args.push("--permission-prompts", "none");
1583
+ return { args };
1340
1584
  }
1341
- function withPreviousRaw(raw, prev) {
1342
- const base = stripPreviousRaw(raw).replace(/\s*\]\s*$/, "");
1343
- return `${base}, "--previous-notify", ${JSON.stringify(JSON.stringify(prev))}]`;
1585
+ function claudeEnv(configDir, env = process.env) {
1586
+ const e = { ...env };
1587
+ for (const k of [
1588
+ "CLAUDECODE",
1589
+ "CLAUDE_CODE_SESSION_ID",
1590
+ "CLAUDE_CODE_HOST_SESSION_ID",
1591
+ "CLAUDE_CODE_ENTRYPOINT",
1592
+ "CLAUDE_CODE_SESSION_ATTENDED",
1593
+ "CLAUDE_CODE_SSE_PORT"
1594
+ ])
1595
+ delete e[k];
1596
+ e.SESSIONPIPE_CONTROL_JOB = "1";
1597
+ if (configDir !== void 0) {
1598
+ if (path8.resolve(configDir) === path8.resolve(os3.homedir(), ".claude")) delete e.CLAUDE_CONFIG_DIR;
1599
+ else e.CLAUDE_CONFIG_DIR = configDir;
1600
+ }
1601
+ return e;
1344
1602
  }
1345
- var isComputerUse = (argv) => Array.isArray(argv) && typeof argv[0] === "string" && /(^|\/)SkyComputerUseClient$/.test(argv[0]);
1346
-
1347
- // src/adapters/codex.ts
1348
- var NAME3 = "codex";
1349
- var CODEX_EVENTS = [
1350
- "SessionStart",
1351
- "UserPromptSubmit",
1352
- "PreToolUse",
1353
- "PostToolUse",
1354
- "PermissionRequest",
1355
- "Stop",
1356
- "Interrupt",
1357
- "SubagentStart",
1358
- "SubagentStop",
1359
- "PreCompact",
1360
- "PostCompact",
1361
- "SessionEnd"
1362
- ];
1363
- var OPTS2 = { matcherFor: (e) => /Tool|Permission/.test(e) ? "" : void 0, timeout: 5 };
1364
- var eventsFor2 = (lean) => lean ? CODEX_EVENTS.filter((e) => e !== "PreToolUse") : [...CODEX_EVENTS];
1365
- var codexHome = (env) => env.CODEX_HOME || path6.join(HOME, ".codex");
1366
- var hooksFile2 = (env) => path6.join(codexHome(env), "hooks.json");
1367
- var configFile2 = (env) => path6.join(codexHome(env), "config.toml");
1368
- var sessionsDir = (env) => path6.join(codexHome(env), "sessions");
1369
- var validTool3 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
1370
- var endReason2 = (r) => r === "clear" || r === "logout" || r === "exit" ? r : "other";
1371
- function installNotify(cmd, env) {
1372
- const file = configFile2(env);
1373
- let text = "";
1603
+ function parseAuthStatus(stdout) {
1604
+ const text = String(stdout || "").trim();
1605
+ const start = text.indexOf("{");
1606
+ if (start < 0) return { loggedIn: null };
1374
1607
  try {
1375
- text = readFileSync4(file, "utf8");
1376
- } catch {
1377
- }
1378
- const argv = cmd([NAME3, "notify"]).argv;
1379
- const line = `notify = ${JSON.stringify(argv)}`;
1380
- const n = readNotify(text);
1381
- const write = (next) => {
1382
- mkdirSync3(path6.dirname(file), { recursive: true });
1383
- writeFileSync3(file, next);
1384
- };
1385
- if (n && !n.other) {
1386
- if (!n.misplaced && JSON.stringify(n.argv) === JSON.stringify(argv))
1387
- return { file, changed: false, note: "notify current" };
1388
- write(withNotify(text, line));
1389
- return {
1390
- file,
1391
- changed: true,
1392
- note: n.misplaced ? "notify moved to the top level (it sat inside a [table], where Codex never reads it)" : "notify updated"
1393
- };
1394
- }
1395
- if (n?.other) {
1396
- const prev = previousNotify(n.argv);
1397
- if (isComputerUse(n.argv)) {
1398
- if (prev && OURS2.test(JSON.stringify(prev))) {
1399
- if (JSON.stringify(prev) === JSON.stringify(argv))
1400
- return { file, changed: false, note: "notify current (after Codex Computer Use)" };
1401
- }
1402
- if (!prev || OURS2.test(JSON.stringify(prev))) {
1403
- const next = replaceNotify(text, `notify = ${withPreviousRaw(n.raw, argv)}`);
1404
- if (next) {
1405
- write(next);
1406
- return { file, changed: true, note: "chained after Codex Computer Use (--previous-notify)" };
1407
- }
1408
- }
1409
- }
1608
+ const j = JSON.parse(text.slice(start));
1609
+ if (typeof j.loggedIn !== "boolean") return { loggedIn: null };
1410
1610
  return {
1411
- file,
1412
- changed: false,
1413
- skipped: true,
1414
- 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`
1611
+ loggedIn: j.loggedIn,
1612
+ ...typeof j.authMethod === "string" ? { method: j.authMethod } : {},
1613
+ ...typeof j.subscriptionType === "string" ? { subscription: j.subscriptionType } : {}
1415
1614
  };
1615
+ } catch {
1616
+ return { loggedIn: null };
1416
1617
  }
1417
- write(withNotify(text, line));
1418
- return { file, changed: true, note: "notify set at the top level" };
1419
1618
  }
1420
- function uninstallNotify(env) {
1421
- const file = configFile2(env);
1422
- let text;
1619
+ function claudeLogin(bin, configDir, env = process.env) {
1620
+ return new Promise((resolve) => {
1621
+ execFile(
1622
+ bin,
1623
+ ["auth", "status", "--json"],
1624
+ { encoding: "utf8", timeout: 2e4, env: claudeEnv(configDir, env), maxBuffer: 256 * 1024 },
1625
+ // `auth status` may exit non-zero when signed out; its JSON still says so.
1626
+ (_err, stdout) => resolve(parseAuthStatus(stdout))
1627
+ );
1628
+ });
1629
+ }
1630
+ function loginCommand(configDir) {
1631
+ const home = os3.homedir();
1632
+ const isDefault = !configDir || path8.resolve(configDir) === path8.resolve(home, ".claude");
1633
+ return isDefault ? "`claude auth login`" : `\`CLAUDE_CONFIG_DIR=${configDir.replace(home, "~")} claude auth login\``;
1634
+ }
1635
+ function loginHint(configDir, platform = process.platform) {
1636
+ const shown = configDir ? configDir.replace(os3.homedir(), "~") : "~/.claude";
1637
+ const login = loginCommand(configDir);
1638
+ if (platform === "darwin")
1639
+ return `Claude Code (${shown}) isn't signed in as this machine's background service sees it. On a Mac its login lives in the login keychain, which a background service can read only while someone is logged in at the screen: log in at the Mac, or run ${login} there.`;
1640
+ return `Claude Code (${shown}) isn't signed in on this machine: run ${login} there.`;
1641
+ }
1642
+ function detectCaps(bin) {
1643
+ let help = "";
1423
1644
  try {
1424
- text = readFileSync4(file, "utf8");
1425
- } catch {
1426
- return { file, changed: false };
1427
- }
1428
- const n = readNotify(text);
1429
- if (!n) return { file, changed: false };
1430
- if (!n.other) {
1431
- writeFileSync3(file, withoutNotify(text));
1432
- return { file, changed: true };
1433
- }
1434
- const prev = previousNotify(n.argv);
1435
- if (isComputerUse(n.argv) && prev && OURS2.test(JSON.stringify(prev))) {
1436
- const next = replaceNotify(text, `notify = ${stripPreviousRaw(n.raw)}`);
1437
- if (next) writeFileSync3(file, next);
1438
- return { file, changed: true };
1645
+ help = execFileSync3(bin, ["--help"], {
1646
+ encoding: "utf8",
1647
+ timeout: 3e4,
1648
+ env: claudeEnv(),
1649
+ stdio: ["ignore", "pipe", "pipe"],
1650
+ maxBuffer: 4 * 1024 * 1024
1651
+ });
1652
+ } catch (e) {
1653
+ help = String(e?.stdout || "");
1439
1654
  }
1440
- return { file, changed: false };
1655
+ return parseCaps(help);
1441
1656
  }
1442
- function findRollout(threadId, env) {
1443
- let best = null;
1444
- const walk = (dir, depth) => {
1445
- let names = [];
1657
+ function findTranscript(session, dirs) {
1658
+ if (!UUID_RE.test(session)) return null;
1659
+ for (const dir of dirs) {
1660
+ let projects = [];
1446
1661
  try {
1447
- names = readdirSync4(dir);
1662
+ projects = readdirSync4(path8.join(dir, "projects"), { withFileTypes: true });
1448
1663
  } catch {
1449
- return;
1664
+ continue;
1450
1665
  }
1451
- for (const n of names.sort().reverse()) {
1452
- const p = path6.join(dir, n);
1453
- if (n.endsWith(".jsonl") && n.includes(threadId)) {
1454
- best = p;
1455
- return;
1456
- }
1457
- if (depth < 3) {
1458
- walk(p, depth + 1);
1459
- if (best) return;
1666
+ for (const p of projects) {
1667
+ if (!p.isDirectory()) continue;
1668
+ const file = path8.join(dir, "projects", p.name, `${session}.jsonl`);
1669
+ try {
1670
+ if (statSync5(file).isFile()) return { file, configDir: dir, project: p.name };
1671
+ } catch {
1460
1672
  }
1461
1673
  }
1462
- };
1463
- walk(sessionsDir(env), 0);
1464
- return best;
1674
+ }
1675
+ return null;
1465
1676
  }
1466
- var codex = {
1467
- name: NAME3,
1468
- events: CODEX_EVENTS,
1469
- detect: (env = process.env) => existsSync4(codexHome(env)),
1470
- configFiles: (env = process.env) => [hooksFile2(env), configFile2(env)],
1471
- install: (cmd, env = process.env, opts = {}) => [
1472
- installClaudeShaped(hooksFile2(env), NAME3, eventsFor2(opts.lean), cmd, OPTS2),
1473
- installNotify(cmd, env)
1474
- ],
1475
- uninstall: (env = process.env, opts = {}) => [
1476
- uninstallClaudeShaped(hooksFile2(env), CODEX_EVENTS, { created: opts.created?.includes(hooksFile2(env)) }),
1477
- uninstallNotify(env)
1478
- ],
1479
- installed: (cmd, env = process.env, opts = {}) => {
1480
- const out = [
1481
- {
1482
- file: hooksFile2(env),
1483
- state: installedClaudeShaped(hooksFile2(env), NAME3, eventsFor2(opts.lean), cmd, OPTS2),
1484
- note: "Codex runs non-managed hooks only after the review in `codex` (/hooks); until then nothing is reported unless the notify fallback is ours"
1485
- }
1486
- ];
1487
- let text = "";
1677
+ var projectName = (cwd) => cwd.replace(/[^a-zA-Z0-9]/g, "-");
1678
+ function sessionFolder(t, cap2 = 8 * 1024 * 1024) {
1679
+ let first = null;
1680
+ let count = 0;
1681
+ let fd;
1682
+ const look = (line) => {
1683
+ if (line.indexOf('"cwd"') < 0) return null;
1684
+ let j;
1488
1685
  try {
1489
- text = readFileSync4(configFile2(env), "utf8");
1686
+ j = JSON.parse(line.toString("utf8"));
1490
1687
  } catch {
1688
+ return null;
1491
1689
  }
1492
- const n = readNotify(text);
1493
- const argv = cmd([NAME3, "notify"]).argv;
1494
- const prev = n ? previousNotify(n.argv) : null;
1495
- const ours = n && (!n.other || prev && JSON.stringify(prev) === JSON.stringify(argv));
1496
- out.push({
1497
- file: configFile2(env),
1498
- state: !n || n.other && !ours ? "missing" : n.misplaced ? "misplaced" : JSON.stringify(n.argv) === JSON.stringify(argv) || prev && JSON.stringify(prev) === JSON.stringify(argv) ? "current" : "stale"
1499
- });
1500
- return out;
1501
- },
1502
- fromHook(input) {
1503
- const event = input.argv[1] ?? "";
1504
- let s = {};
1505
- if (event === "notify") {
1506
- const raw = input.argv.find((a) => a.startsWith("{")) ?? input.stdin;
1690
+ if (!j || typeof j.cwd !== "string" || !path8.isAbsolute(j.cwd)) return null;
1691
+ if (projectName(j.cwd) === t.project) return j.cwd;
1692
+ first ??= j.cwd;
1693
+ count++;
1694
+ return null;
1695
+ };
1696
+ try {
1697
+ fd = openSync2(t.file, "r");
1698
+ const buf = Buffer.alloc(256 * 1024);
1699
+ let pos = 0;
1700
+ let rest = Buffer.alloc(0);
1701
+ let skipping = false;
1702
+ while (pos < cap2 && count < 50) {
1703
+ const n = readSync2(fd, buf, 0, buf.length, pos);
1704
+ if (!n) {
1705
+ if (!skipping && rest.length) {
1706
+ const hit = look(rest);
1707
+ if (hit) return hit;
1708
+ }
1709
+ break;
1710
+ }
1711
+ pos += n;
1712
+ let chunk = rest.length ? Buffer.concat([rest, buf.subarray(0, n)]) : buf.subarray(0, n);
1713
+ let nl = chunk.indexOf(10);
1714
+ while (nl >= 0) {
1715
+ const line = chunk.subarray(0, nl);
1716
+ chunk = chunk.subarray(nl + 1);
1717
+ if (skipping) skipping = false;
1718
+ else {
1719
+ const hit = look(line);
1720
+ if (hit) return hit;
1721
+ }
1722
+ nl = chunk.indexOf(10);
1723
+ }
1724
+ if (chunk.length > 2 * 1024 * 1024) {
1725
+ skipping = true;
1726
+ rest = Buffer.alloc(0);
1727
+ } else rest = skipping ? Buffer.alloc(0) : Buffer.from(chunk);
1728
+ }
1729
+ } catch {
1730
+ } finally {
1731
+ if (fd !== void 0)
1507
1732
  try {
1508
- s = raw ? JSON.parse(raw) : {};
1733
+ closeSync2(fd);
1509
1734
  } catch {
1510
1735
  }
1511
- const id2 = s["thread-id"] ?? s.thread_id ?? s["session-id"] ?? s.session_id;
1512
- if (!id2) return null;
1513
- const r2 = {
1514
- session: { id: String(id2), cwd: typeof s.cwd === "string" ? s.cwd : input.cwd },
1515
- events: [
1516
- {
1517
- type: "turn.ended",
1518
- data: { reason: "stop", ...typeof s["turn-id"] === "string" ? { turn_id: s["turn-id"] } : {} },
1519
- harnessEvent: "notify"
1520
- }
1521
- ],
1522
- hints: {}
1523
- };
1524
- const file = findRollout(String(id2), input.env);
1525
- if (file) r2.transcript = file;
1526
- return r2;
1527
- }
1736
+ }
1737
+ return first;
1738
+ }
1739
+ function pidAlive(pid) {
1740
+ if (!Number.isSafeInteger(pid) || pid <= 1) return false;
1741
+ try {
1742
+ process.kill(pid, 0);
1743
+ return true;
1744
+ } catch (e) {
1745
+ return e.code === "EPERM";
1746
+ }
1747
+ }
1748
+ function liveProcess(session, configDir) {
1749
+ let names = [];
1750
+ try {
1751
+ names = readdirSync4(path8.join(configDir, "sessions")).filter((n) => /^\d+\.json$/.test(n));
1752
+ } catch {
1753
+ return false;
1754
+ }
1755
+ for (const n of names) {
1528
1756
  try {
1529
- s = input.stdin ? JSON.parse(input.stdin) : {};
1757
+ const j = JSON.parse(readFileSync4(path8.join(configDir, "sessions", n), "utf8"));
1758
+ if (String(j.sessionId) !== session) continue;
1759
+ if (pidAlive(Number(j.pid ?? n.slice(0, -5)))) return true;
1530
1760
  } catch {
1531
1761
  }
1532
- const id = s.session_id;
1533
- if (!id) return null;
1534
- const session = { id: String(id), cwd: typeof s.cwd === "string" ? s.cwd : input.cwd };
1535
- if (typeof s.model === "string") session.model = s.model;
1536
- const turn_id = typeof s.turn_id === "string" ? s.turn_id : void 0;
1537
- const tool = validTool3(s.tool_name) ? s.tool_name : void 0;
1538
- const call_id = typeof s.tool_use_id === "string" ? s.tool_use_id : void 0;
1539
- const ev = (type, data) => ({ type, data: prune3(data), harnessEvent: event });
1540
- const events = [];
1541
- switch (event) {
1542
- case "SessionStart":
1543
- events.push(
1544
- ev("session.started", {
1545
- source: typeof s.source === "string" && ["startup", "resume", "clear", "compact", "fork"].includes(s.source) ? s.source : "unknown"
1546
- })
1547
- );
1548
- break;
1549
- case "UserPromptSubmit":
1550
- events.push(
1551
- ev("turn.started", {
1552
- turn_id,
1553
- prompt_chars: typeof s.prompt === "string" ? [...s.prompt].length : void 0
1554
- })
1555
- );
1556
- break;
1557
- case "PreToolUse":
1558
- if (tool) events.push(ev("tool.started", { tool, call_id, turn_id, input: s.tool_input }));
1559
- break;
1560
- case "PostToolUse":
1561
- if (tool)
1562
- events.push(
1563
- ev("tool.ended", { tool, call_id, turn_id, ok: true, input: s.tool_input, output: s.tool_response })
1564
- );
1565
- break;
1566
- case "PermissionRequest":
1567
- events.push(
1568
- ev("attention.needed", { attention_id: call_id ?? `perm-${Date.now()}`, kind: "permission", tool })
1569
- );
1570
- break;
1571
- case "Stop":
1572
- events.push(ev("turn.ended", { turn_id, reason: "stop" }));
1573
- break;
1574
- case "Interrupt":
1575
- events.push(ev("turn.ended", { turn_id, reason: "interrupt" }));
1576
- break;
1577
- case "SubagentStart":
1578
- events.push(
1579
- ev("subagent.started", {
1580
- agent_id: String(s.agent_id ?? s.subagent_id ?? ""),
1581
- agent_type: typeof s.agent_type === "string" ? s.agent_type : void 0
1582
- })
1583
- );
1584
- break;
1585
- case "SubagentStop":
1586
- events.push(
1587
- ev("subagent.ended", {
1588
- agent_id: String(s.agent_id ?? s.subagent_id ?? ""),
1589
- agent_type: typeof s.agent_type === "string" ? s.agent_type : void 0
1590
- })
1591
- );
1592
- break;
1593
- case "PreCompact":
1594
- events.push(ev("context.compacted", { trigger: s.trigger === "manual" ? "manual" : "auto" }));
1595
- break;
1596
- case "PostCompact":
1597
- break;
1598
- case "SessionEnd":
1599
- events.push(ev("session.ended", { reason: endReason2(s.reason) }));
1600
- break;
1601
- default:
1602
- return null;
1762
+ }
1763
+ return false;
1764
+ }
1765
+ function recentlyWritten(file, ourLastEnd, now = Date.now()) {
1766
+ let m;
1767
+ try {
1768
+ m = statSync5(file).mtimeMs;
1769
+ } catch {
1770
+ return false;
1771
+ }
1772
+ if (now - m > LIVE_WRITE_MS) return false;
1773
+ return !(ourLastEnd && m <= ourLastEnd + 2e3);
1774
+ }
1775
+ function runClaude(bin, args, o) {
1776
+ return runHeadless(bin, args, { ...o, keep: o.keep ?? ((line) => line.includes('"type":"result"')) });
1777
+ }
1778
+ function parseResult(stdout) {
1779
+ const s = String(stdout || "").trim();
1780
+ if (!s) return null;
1781
+ const pick = (j) => {
1782
+ if (Array.isArray(j)) {
1783
+ for (let i = j.length - 1; i >= 0; i--) if (j[i]?.type === "result") return j[i];
1784
+ return null;
1603
1785
  }
1604
- const r = { session, events, hints: {} };
1605
- if (typeof s.transcript_path === "string") r.transcript = s.transcript_path;
1606
- return r;
1607
- },
1608
- readTranscript(file, fromLine) {
1609
- let text = "";
1786
+ return j && typeof j === "object" ? j : null;
1787
+ };
1788
+ try {
1789
+ return pick(JSON.parse(s));
1790
+ } catch {
1791
+ }
1792
+ const lines = s.split("\n");
1793
+ for (let i = lines.length - 1; i >= 0; i--) {
1610
1794
  try {
1611
- text = readFileSync4(file, "utf8");
1795
+ const j = pick(JSON.parse(lines[i]));
1796
+ if (j && (j.type === "result" || "result" in j)) return j;
1612
1797
  } catch {
1613
- return { turns: [], line: fromLine };
1614
1798
  }
1615
- return readCodex(text.split("\n"), fromLine);
1616
- },
1617
- facts(session, transcript) {
1618
- const out = {};
1619
- if (transcript) Object.assign(out, codexFacts(transcript));
1620
- return out;
1621
- },
1622
- backfill(sinceMs, env = process.env) {
1623
- const rows = [];
1624
- for (const f of recentFiles(sessionsDir(env), 3, sinceMs, (n) => /^rollout-.*\.jsonl$/.test(n))) {
1625
- const m = /([0-9a-f]{8}-[0-9a-f-]{27,})\.jsonl$/i.exec(path6.basename(f));
1626
- if (!m?.[1]) continue;
1627
- const facts = codexFacts(f);
1628
- if (!facts.started_at || !facts.last_at) continue;
1629
- const { started_at, last_at, first_ask: _fa, harness_version: _hv, ...rest } = facts;
1630
- rows.push({ session: { id: m[1], ...rest }, started_at, last_at });
1799
+ }
1800
+ return null;
1801
+ }
1802
+ var isUuid = (s) => typeof s === "string" && UUID_RE.test(s);
1803
+
1804
+ // src/adapters/codex.ts
1805
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
1806
+ import path9 from "node:path";
1807
+
1808
+ // src/adapters/codex-config.ts
1809
+ var NOTIFY_LINE = /^[ \t]*notify[ \t]*=[ \t]*(\[.*\])[ \t]*(?:#.*)?$/m;
1810
+ var NOTIFY_KEY = /^[ \t]*notify[ \t]*=/m;
1811
+ var OURS2 = /(?:hook\.js|sessionpipe-hook)",\s*"codex",\s*"notify"/;
1812
+ function scan(line, st) {
1813
+ let i = 0;
1814
+ while (i < line.length) {
1815
+ if (st.str) {
1816
+ const j = line.indexOf(st.str, i);
1817
+ if (j < 0) return;
1818
+ i = j + 3;
1819
+ st.str = null;
1820
+ continue;
1631
1821
  }
1632
- return rows;
1822
+ const ch = line[i];
1823
+ if (ch === "#") return;
1824
+ if (line.startsWith('"""', i) || line.startsWith("'''", i)) {
1825
+ st.str = line.slice(i, i + 3);
1826
+ i += 3;
1827
+ continue;
1828
+ }
1829
+ if (ch === '"') {
1830
+ i++;
1831
+ while (i < line.length && line[i] !== '"') i += line[i] === "\\" ? 2 : 1;
1832
+ i++;
1833
+ continue;
1834
+ }
1835
+ if (ch === "'") {
1836
+ const j = line.indexOf("'", i + 1);
1837
+ i = j < 0 ? line.length : j + 1;
1838
+ continue;
1839
+ }
1840
+ if (ch === "[") st.depth++;
1841
+ else if (ch === "]") st.depth = Math.max(0, st.depth - 1);
1842
+ i++;
1633
1843
  }
1634
- };
1635
- function prune3(o) {
1636
- for (const k of Object.keys(o)) if (o[k] === void 0) delete o[k];
1637
- return o;
1638
1844
  }
1639
- function codexFacts(file) {
1640
- const { head, tail } = ends(file, 128 * 1024, 64 * 1024);
1641
- const H = parseLines(head);
1642
- const T = parseLines(tail);
1643
- const out = {};
1644
- const meta = H.find((j) => j.type === "session_meta" && j.payload);
1645
- if (meta?.payload) {
1646
- const p = meta.payload;
1647
- if (p.cwd) out.cwd = p.cwd;
1648
- if (p.cli_version) out.harness_version = String(p.cli_version);
1649
- if (p.git?.branch) out.branch = p.git.branch;
1650
- if (p.git?.repository_url) out.repo = String(p.git.repository_url).replace(/\/\/[^/@]*@/, "//");
1845
+ function firstTableAt(text) {
1846
+ const st = { depth: 0, str: null };
1847
+ let pos = 0;
1848
+ for (const line of text.split("\n")) {
1849
+ if (!st.str && st.depth === 0 && /^[ \t]*\[/.test(line)) return pos;
1850
+ scan(line, st);
1851
+ pos += line.length + 1;
1651
1852
  }
1652
- const ctx = H.concat(T).reverse().find((j) => j.type === "turn_context" && j.payload?.model);
1653
- if (ctx?.payload?.model) out.model = ctx.payload.model;
1654
- for (const j of H) {
1655
- const p = j.payload;
1656
- if (j.type !== "response_item" || !p || p.type !== "message" || p.role !== "user") continue;
1657
- const text = (p.content ?? []).filter((b) => b && b.type === "input_text" && typeof b.text === "string").map((b) => b.text).join("\n").trim();
1658
- if (!text || /^<[a-z_-]+>/i.test(text)) continue;
1659
- const line = text.split("\n").find((l) => l.trim()) ?? "";
1660
- out.title = line.length > 80 ? `${line.slice(0, 80).replace(/\s+\S*$/, "")}\u2026` : line;
1661
- out.title_source = "first-ask";
1662
- break;
1853
+ return -1;
1854
+ }
1855
+ function split(text) {
1856
+ const at = firstTableAt(text);
1857
+ return at < 0 ? { head: text, tail: "", at: text.length } : { head: text.slice(0, at), tail: text.slice(at), at };
1858
+ }
1859
+ function parseArgv(raw) {
1860
+ try {
1861
+ const a = JSON.parse(raw);
1862
+ return Array.isArray(a) && a.every((x) => typeof x === "string") ? a : null;
1863
+ } catch {
1864
+ return null;
1663
1865
  }
1664
- const times = H.concat(T).map((j) => Date.parse(String(j.timestamp ?? ""))).filter((n) => n > 0);
1665
- if (times.length) {
1666
- out.started_at = Math.min(...times);
1667
- out.last_at = Math.max(...times);
1866
+ }
1867
+ function readNotify(text) {
1868
+ const { head, tail, at } = split(text);
1869
+ const m = NOTIFY_LINE.exec(head);
1870
+ if (m)
1871
+ return {
1872
+ raw: m[1] ?? "",
1873
+ argv: parseArgv(m[1] ?? ""),
1874
+ index: m.index,
1875
+ length: m[0].length,
1876
+ misplaced: false,
1877
+ other: !OURS2.test(m[1] ?? "")
1878
+ };
1879
+ const k = NOTIFY_KEY.exec(head);
1880
+ if (k) {
1881
+ const rest = head.slice(k.index).split("\n")[0] ?? "";
1882
+ return {
1883
+ raw: rest.replace(NOTIFY_KEY, "").trim(),
1884
+ argv: null,
1885
+ index: k.index,
1886
+ length: 0,
1887
+ misplaced: false,
1888
+ other: true
1889
+ };
1668
1890
  }
1669
- return out;
1891
+ const t = NOTIFY_LINE.exec(tail);
1892
+ if (t && OURS2.test(t[1] ?? ""))
1893
+ return {
1894
+ raw: t[1] ?? "",
1895
+ argv: parseArgv(t[1] ?? ""),
1896
+ index: at + t.index,
1897
+ length: t[0].length,
1898
+ misplaced: true,
1899
+ other: false
1900
+ };
1901
+ return null;
1902
+ }
1903
+ function dropLine(text, index, length) {
1904
+ return text.slice(0, index) + text.slice(index + length).replace(/^\r?\n/, "");
1670
1905
  }
1906
+ function insertTopLevel(text, line) {
1907
+ const { head, tail } = split(text);
1908
+ if (!tail) return (head.trim() ? head.replace(/\s*$/, "\n") : "") + line + "\n";
1909
+ return `${(head.trim() ? head.replace(/\s*$/, "\n") : "") + line}
1671
1910
 
1672
- // src/adapters/gemini-cli.ts
1673
- import { execFileSync as execFileSync3 } from "node:child_process";
1674
- import { existsSync as existsSync5, readFileSync as readFileSync5 } from "node:fs";
1675
- import path7 from "node:path";
1676
- var NAME4 = "gemini-cli";
1677
- var GEMINI_EVENTS = [
1911
+ ${tail}`;
1912
+ }
1913
+ function withNotify(text, line) {
1914
+ const n = readNotify(text);
1915
+ if (n && !n.misplaced && !n.other) return text.slice(0, n.index) + line + text.slice(n.index + n.length);
1916
+ if (n?.misplaced) return insertTopLevel(dropLine(text, n.index, n.length), line);
1917
+ return insertTopLevel(text, line);
1918
+ }
1919
+ function replaceNotify(text, line) {
1920
+ const n = readNotify(text);
1921
+ if (!n || n.misplaced || !n.length) return null;
1922
+ return text.slice(0, n.index) + line + text.slice(n.index + n.length);
1923
+ }
1924
+ function withoutNotify(text) {
1925
+ const n = readNotify(text);
1926
+ if (!n || n.other || !n.length) return text;
1927
+ return dropLine(text, n.index, n.length);
1928
+ }
1929
+ var PREVIOUS = "--previous-notify";
1930
+ function previousNotify(argv) {
1931
+ if (!Array.isArray(argv)) return null;
1932
+ const i = argv.indexOf(PREVIOUS);
1933
+ return i >= 0 && typeof argv[i + 1] === "string" ? parseArgv(argv[i + 1]) : null;
1934
+ }
1935
+ function stripPreviousRaw(raw) {
1936
+ return raw.replace(/,\s*"--previous-notify",\s*"(?:[^"\\]|\\.)*"\s*(?=\])/, "");
1937
+ }
1938
+ function withPreviousRaw(raw, prev) {
1939
+ const base = stripPreviousRaw(raw).replace(/\s*\]\s*$/, "");
1940
+ return `${base}, "--previous-notify", ${JSON.stringify(JSON.stringify(prev))}]`;
1941
+ }
1942
+ var isComputerUse = (argv) => Array.isArray(argv) && typeof argv[0] === "string" && /(^|\/)SkyComputerUseClient$/.test(argv[0]);
1943
+
1944
+ // src/adapters/codex.ts
1945
+ var NAME3 = "codex";
1946
+ var CODEX_EVENTS = [
1678
1947
  "SessionStart",
1679
- "BeforeAgent",
1680
- "BeforeTool",
1681
- "AfterTool",
1682
- "Notification",
1683
- "AfterAgent",
1684
- "PreCompress",
1948
+ "UserPromptSubmit",
1949
+ "PreToolUse",
1950
+ "PostToolUse",
1951
+ "PermissionRequest",
1952
+ "Stop",
1953
+ "Interrupt",
1954
+ "SubagentStart",
1955
+ "SubagentStop",
1956
+ "PreCompact",
1957
+ "PostCompact",
1685
1958
  "SessionEnd"
1686
1959
  ];
1687
- var OPTS3 = { matcherFor: (e) => /Tool/.test(e) ? ".*" : void 0, timeout: 5e3 };
1688
- var geminiDir = (env) => env.GEMINI_CLI_HOME || path7.join(HOME, ".gemini");
1689
- var settings = (env) => path7.join(geminiDir(env), "settings.json");
1690
- var validTool4 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
1691
- var eventsFor3 = (lean) => lean ? GEMINI_EVENTS.filter((e) => e !== "BeforeTool") : [...GEMINI_EVENTS];
1692
- function geminiHooksActive(settingsFile) {
1693
- let tools;
1960
+ var OPTS2 = { matcherFor: (e) => /Tool|Permission/.test(e) ? "" : void 0, timeout: 5 };
1961
+ var eventsFor2 = (lean) => lean ? CODEX_EVENTS.filter((e) => e !== "PreToolUse") : [...CODEX_EVENTS];
1962
+ var codexHome = (env) => env.CODEX_HOME || path9.join(HOME, ".codex");
1963
+ var hooksFile2 = (env) => path9.join(codexHome(env), "hooks.json");
1964
+ var configFile2 = (env) => path9.join(codexHome(env), "config.toml");
1965
+ var sessionsDir = (env) => path9.join(codexHome(env), "sessions");
1966
+ var validTool3 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
1967
+ var endReason2 = (r) => r === "clear" || r === "logout" || r === "exit" ? r : "other";
1968
+ function installNotify(cmd, env) {
1969
+ const file = configFile2(env);
1970
+ let text = "";
1694
1971
  try {
1695
- tools = JSON.parse(readFileSync5(settingsFile, "utf8")).tools;
1972
+ text = readFileSync5(file, "utf8");
1696
1973
  } catch {
1697
1974
  }
1698
- if (tools?.enableHooks === true) return { active: true };
1699
- if (tools?.enableHooks === false)
1700
- return { active: false, note: "inactive: tools.enableHooks is false in settings.json" };
1701
- let version = "";
1702
- try {
1703
- version = execFileSync3("gemini", ["--version"], {
1704
- encoding: "utf8",
1705
- timeout: 4e3,
1706
- stdio: ["ignore", "pipe", "ignore"]
1707
- }).trim();
1708
- } catch {
1975
+ const argv = cmd([NAME3, "notify"]).argv;
1976
+ const line = `notify = ${JSON.stringify(argv)}`;
1977
+ const n = readNotify(text);
1978
+ const write = (next) => {
1979
+ mkdirSync3(path9.dirname(file), { recursive: true });
1980
+ writeFileSync3(file, next);
1981
+ };
1982
+ if (n && !n.other) {
1983
+ if (!n.misplaced && JSON.stringify(n.argv) === JSON.stringify(argv))
1984
+ return { file, changed: false, note: "notify current" };
1985
+ write(withNotify(text, line));
1709
1986
  return {
1710
- active: true,
1711
- note: "could not run `gemini --version`; hooks need tools.enableHooks on versions before 0.46"
1987
+ file,
1988
+ changed: true,
1989
+ note: n.misplaced ? "notify moved to the top level (it sat inside a [table], where Codex never reads it)" : "notify updated"
1712
1990
  };
1713
1991
  }
1714
- const m = /(\d+)\.(\d+)/.exec(version);
1715
- if (m && (Number(m[1]) > 0 || Number(m[2]) >= 46)) return { active: true };
1716
- return {
1717
- active: false,
1718
- note: `inactive: Gemini CLI ${version || "?"} runs hooks only with "tools": {"enableHooks": true} in settings.json`
1719
- };
1720
- }
1721
- var geminiCli = {
1722
- name: NAME4,
1723
- events: GEMINI_EVENTS,
1724
- // ~/.gemini also belongs to Antigravity; Gemini CLI is here when its settings or its chat logs exist.
1725
- detect: (env = process.env) => existsSync5(settings(env)) || existsSync5(path7.join(geminiDir(env), "tmp")),
1726
- configFiles: (env = process.env) => [settings(env)],
1727
- install: (cmd, env = process.env, opts = {}) => {
1728
- const r = installClaudeShaped(settings(env), NAME4, eventsFor3(opts.lean), cmd, OPTS3);
1729
- const a = geminiHooksActive(settings(env));
1730
- return [a.note && !r.skipped ? { ...r, note: [r.note, a.note].filter(Boolean).join("; ") } : r];
1731
- },
1732
- uninstall: (env = process.env, opts = {}) => [
1733
- uninstallClaudeShaped(settings(env), GEMINI_EVENTS, { created: opts.created?.includes(settings(env)) })
1992
+ if (n?.other) {
1993
+ const prev = previousNotify(n.argv);
1994
+ if (isComputerUse(n.argv)) {
1995
+ if (prev && OURS2.test(JSON.stringify(prev))) {
1996
+ if (JSON.stringify(prev) === JSON.stringify(argv))
1997
+ return { file, changed: false, note: "notify current (after Codex Computer Use)" };
1998
+ }
1999
+ if (!prev || OURS2.test(JSON.stringify(prev))) {
2000
+ const next = replaceNotify(text, `notify = ${withPreviousRaw(n.raw, argv)}`);
2001
+ if (next) {
2002
+ write(next);
2003
+ return { file, changed: true, note: "chained after Codex Computer Use (--previous-notify)" };
2004
+ }
2005
+ }
2006
+ }
2007
+ return {
2008
+ file,
2009
+ changed: false,
2010
+ skipped: true,
2011
+ 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`
2012
+ };
2013
+ }
2014
+ write(withNotify(text, line));
2015
+ return { file, changed: true, note: "notify set at the top level" };
2016
+ }
2017
+ function uninstallNotify(env) {
2018
+ const file = configFile2(env);
2019
+ let text;
2020
+ try {
2021
+ text = readFileSync5(file, "utf8");
2022
+ } catch {
2023
+ return { file, changed: false };
2024
+ }
2025
+ const n = readNotify(text);
2026
+ if (!n) return { file, changed: false };
2027
+ if (!n.other) {
2028
+ writeFileSync3(file, withoutNotify(text));
2029
+ return { file, changed: true };
2030
+ }
2031
+ const prev = previousNotify(n.argv);
2032
+ if (isComputerUse(n.argv) && prev && OURS2.test(JSON.stringify(prev))) {
2033
+ const next = replaceNotify(text, `notify = ${stripPreviousRaw(n.raw)}`);
2034
+ if (next) writeFileSync3(file, next);
2035
+ return { file, changed: true };
2036
+ }
2037
+ return { file, changed: false };
2038
+ }
2039
+ function findRollout(threadId, env = process.env) {
2040
+ if (!/^[A-Za-z0-9-]{8,128}$/.test(threadId)) return null;
2041
+ let best = null;
2042
+ const walk = (dir, depth) => {
2043
+ let names = [];
2044
+ try {
2045
+ names = readdirSync5(dir);
2046
+ } catch {
2047
+ return;
2048
+ }
2049
+ for (const n of names.sort().reverse()) {
2050
+ const p = path9.join(dir, n);
2051
+ if (n.endsWith(`-${threadId}.jsonl`)) {
2052
+ best = p;
2053
+ return;
2054
+ }
2055
+ if (depth < 3) {
2056
+ walk(p, depth + 1);
2057
+ if (best) return;
2058
+ }
2059
+ }
2060
+ };
2061
+ walk(sessionsDir(env), 0);
2062
+ return best;
2063
+ }
2064
+ var codex = {
2065
+ name: NAME3,
2066
+ events: CODEX_EVENTS,
2067
+ detect: (env = process.env) => existsSync4(codexHome(env)),
2068
+ configFiles: (env = process.env) => [hooksFile2(env), configFile2(env)],
2069
+ install: (cmd, env = process.env, opts = {}) => [
2070
+ installClaudeShaped(hooksFile2(env), NAME3, eventsFor2(opts.lean), cmd, OPTS2),
2071
+ installNotify(cmd, env)
2072
+ ],
2073
+ uninstall: (env = process.env, opts = {}) => [
2074
+ uninstallClaudeShaped(hooksFile2(env), CODEX_EVENTS, { created: opts.created?.includes(hooksFile2(env)) }),
2075
+ uninstallNotify(env)
1734
2076
  ],
1735
2077
  installed: (cmd, env = process.env, opts = {}) => {
1736
- const state = installedClaudeShaped(settings(env), NAME4, eventsFor3(opts.lean), cmd, OPTS3);
1737
- const a = geminiHooksActive(settings(env));
1738
- return [
2078
+ const out = [
1739
2079
  {
1740
- file: settings(env),
1741
- state: state === "current" && !a.active ? "inactive" : state,
1742
- ...a.note ? { note: a.note } : {}
2080
+ file: hooksFile2(env),
2081
+ state: installedClaudeShaped(hooksFile2(env), NAME3, eventsFor2(opts.lean), cmd, OPTS2),
2082
+ note: "Codex runs non-managed hooks only after the review in `codex` (/hooks); until then nothing is reported unless the notify fallback is ours"
1743
2083
  }
1744
2084
  ];
2085
+ let text = "";
2086
+ try {
2087
+ text = readFileSync5(configFile2(env), "utf8");
2088
+ } catch {
2089
+ }
2090
+ const n = readNotify(text);
2091
+ const argv = cmd([NAME3, "notify"]).argv;
2092
+ const prev = n ? previousNotify(n.argv) : null;
2093
+ const ours = n && (!n.other || prev && JSON.stringify(prev) === JSON.stringify(argv));
2094
+ out.push({
2095
+ file: configFile2(env),
2096
+ state: !n || n.other && !ours ? "missing" : n.misplaced ? "misplaced" : JSON.stringify(n.argv) === JSON.stringify(argv) || prev && JSON.stringify(prev) === JSON.stringify(argv) ? "current" : "stale"
2097
+ });
2098
+ return out;
1745
2099
  },
1746
2100
  fromHook(input) {
1747
2101
  const event = input.argv[1] ?? "";
1748
2102
  let s = {};
2103
+ if (event === "notify") {
2104
+ const raw = input.argv.find((a) => a.startsWith("{")) ?? input.stdin;
2105
+ try {
2106
+ s = raw ? JSON.parse(raw) : {};
2107
+ } catch {
2108
+ }
2109
+ const id2 = s["thread-id"] ?? s.thread_id ?? s["session-id"] ?? s.session_id;
2110
+ if (!id2) return null;
2111
+ const r2 = {
2112
+ session: { id: String(id2), cwd: typeof s.cwd === "string" ? s.cwd : input.cwd },
2113
+ events: [
2114
+ {
2115
+ type: "turn.ended",
2116
+ data: { reason: "stop", ...typeof s["turn-id"] === "string" ? { turn_id: s["turn-id"] } : {} },
2117
+ harnessEvent: "notify"
2118
+ }
2119
+ ],
2120
+ hints: {}
2121
+ };
2122
+ const file = findRollout(String(id2), input.env);
2123
+ if (file) r2.transcript = file;
2124
+ return r2;
2125
+ }
1749
2126
  try {
1750
2127
  s = input.stdin ? JSON.parse(input.stdin) : {};
1751
2128
  } catch {
@@ -1753,62 +2130,72 @@ var geminiCli = {
1753
2130
  const id = s.session_id;
1754
2131
  if (!id) return null;
1755
2132
  const session = { id: String(id), cwd: typeof s.cwd === "string" ? s.cwd : input.cwd };
1756
- const tool = validTool4(s.tool_name) ? s.tool_name : void 0;
1757
- const ev = (type, data) => ({ type, data: prune4(data), harnessEvent: event });
2133
+ if (typeof s.model === "string") session.model = s.model;
2134
+ const turn_id = typeof s.turn_id === "string" ? s.turn_id : void 0;
2135
+ const tool = validTool3(s.tool_name) ? s.tool_name : void 0;
2136
+ const call_id = typeof s.tool_use_id === "string" ? s.tool_use_id : void 0;
2137
+ const ev = (type, data) => ({ type, data: prune3(data), harnessEvent: event });
1758
2138
  const events = [];
1759
2139
  switch (event) {
1760
2140
  case "SessionStart":
1761
2141
  events.push(
1762
2142
  ev("session.started", {
1763
- source: typeof s.source === "string" && ["startup", "resume", "clear"].includes(s.source) ? s.source : "unknown"
2143
+ source: typeof s.source === "string" && ["startup", "resume", "clear", "compact", "fork"].includes(s.source) ? s.source : "unknown"
1764
2144
  })
1765
2145
  );
1766
2146
  break;
1767
- case "BeforeAgent":
2147
+ case "UserPromptSubmit":
1768
2148
  events.push(
1769
- ev("turn.started", { prompt_chars: typeof s.prompt === "string" ? [...s.prompt].length : void 0 })
2149
+ ev("turn.started", {
2150
+ turn_id,
2151
+ prompt_chars: typeof s.prompt === "string" ? [...s.prompt].length : void 0
2152
+ })
1770
2153
  );
1771
2154
  break;
1772
- case "BeforeTool":
1773
- if (tool) events.push(ev("tool.started", { tool, input: s.tool_input }));
2155
+ case "PreToolUse":
2156
+ if (tool) events.push(ev("tool.started", { tool, call_id, turn_id, input: s.tool_input }));
1774
2157
  break;
1775
- case "AfterTool": {
1776
- const resp = s.tool_response;
1777
- const err = resp && typeof resp === "object" && resp.error ? String(typeof resp.error === "string" ? resp.error : JSON.stringify(resp.error)).slice(0, 2e3) : void 0;
2158
+ case "PostToolUse":
1778
2159
  if (tool)
1779
2160
  events.push(
1780
- ev("tool.ended", {
1781
- tool,
1782
- ok: !err,
1783
- error: err,
1784
- input: s.tool_input,
1785
- output: resp?.llmContent ?? s.tool_response
1786
- })
2161
+ ev("tool.ended", { tool, call_id, turn_id, ok: true, input: s.tool_input, output: s.tool_response })
1787
2162
  );
1788
2163
  break;
1789
- }
1790
- case "Notification":
2164
+ case "PermissionRequest":
1791
2165
  events.push(
1792
- ev("attention.needed", {
1793
- attention_id: `notif-${Date.now()}`,
1794
- kind: s.notification_type === "ToolPermission" ? "permission" : "question",
1795
- message: typeof s.message === "string" ? s.message.slice(0, 200) : void 0
1796
- })
2166
+ ev("attention.needed", { attention_id: call_id ?? `perm-${Date.now()}`, kind: "permission", tool })
1797
2167
  );
1798
2168
  break;
1799
- case "AfterAgent":
1800
- events.push(ev("turn.ended", { reason: "stop" }));
2169
+ case "Stop":
2170
+ events.push(ev("turn.ended", { turn_id, reason: "stop" }));
1801
2171
  break;
1802
- case "PreCompress":
1803
- events.push(ev("context.compacted", { trigger: s.trigger === "manual" ? "manual" : "auto" }));
2172
+ case "Interrupt":
2173
+ events.push(ev("turn.ended", { turn_id, reason: "interrupt" }));
1804
2174
  break;
1805
- case "SessionEnd":
2175
+ case "SubagentStart":
1806
2176
  events.push(
1807
- ev("session.ended", {
1808
- reason: s.reason === "clear" || s.reason === "logout" || s.reason === "exit" ? s.reason : s.reason === "prompt_input_exit" ? "exit" : "other"
2177
+ ev("subagent.started", {
2178
+ agent_id: String(s.agent_id ?? s.subagent_id ?? ""),
2179
+ agent_type: typeof s.agent_type === "string" ? s.agent_type : void 0
2180
+ })
2181
+ );
2182
+ break;
2183
+ case "SubagentStop":
2184
+ events.push(
2185
+ ev("subagent.ended", {
2186
+ agent_id: String(s.agent_id ?? s.subagent_id ?? ""),
2187
+ agent_type: typeof s.agent_type === "string" ? s.agent_type : void 0
1809
2188
  })
1810
2189
  );
1811
2190
  break;
2191
+ case "PreCompact":
2192
+ events.push(ev("context.compacted", { trigger: s.trigger === "manual" ? "manual" : "auto" }));
2193
+ break;
2194
+ case "PostCompact":
2195
+ break;
2196
+ case "SessionEnd":
2197
+ events.push(ev("session.ended", { reason: endReason2(s.reason) }));
2198
+ break;
1812
2199
  default:
1813
2200
  return null;
1814
2201
  }
@@ -1823,469 +2210,396 @@ var geminiCli = {
1823
2210
  } catch {
1824
2211
  return { turns: [], line: fromLine };
1825
2212
  }
1826
- return readGemini(text.split("\n"), fromLine);
2213
+ return readCodex(text.split("\n"), fromLine);
1827
2214
  },
1828
- facts(_session, transcript) {
2215
+ facts(session, transcript) {
1829
2216
  const out = {};
1830
- if (!transcript) return out;
1831
- const { head, tail } = ends(transcript, 64 * 1024, 64 * 1024);
1832
- const H = parseLines(head);
1833
- const T = parseLines(tail);
1834
- const first = H.find((j) => j.type === "user");
1835
- const raw = first ? first.content ?? first.message ?? first.text : void 0;
1836
- const text = typeof raw === "string" ? raw.trim() : "";
1837
- if (text) {
1838
- const line = text.split("\n").find((l) => l.trim()) ?? "";
1839
- out.title = line.length > 80 ? `${line.slice(0, 80).replace(/\s+\S*$/, "")}\u2026` : line;
1840
- out.title_source = "first-ask";
1841
- }
1842
- const times = H.concat(T).map((j) => Date.parse(String(j.timestamp ?? ""))).filter((n) => n > 0);
1843
- if (times.length) {
1844
- out.started_at = Math.min(...times);
1845
- out.last_at = Math.max(...times);
1846
- }
2217
+ if (transcript) Object.assign(out, codexFacts(transcript));
1847
2218
  return out;
1848
2219
  },
1849
- backfill: () => []
1850
- // chat logs are keyed by a project hash, not a session id we can name; the first hook of a resumed session reports it
1851
- };
1852
- function prune4(o) {
1853
- for (const k of Object.keys(o)) if (o[k] === void 0) delete o[k];
1854
- return o;
1855
- }
1856
-
1857
- // src/adapters/claude-code-control.ts
1858
- var claude_code_control_exports = {};
1859
- __export(claude_code_control_exports, {
1860
- JOB_TIMEOUT_MS: () => JOB_TIMEOUT_MS,
1861
- claudeEnv: () => claudeEnv,
1862
- claudeLogin: () => claudeLogin,
1863
- detectCaps: () => detectCaps,
1864
- findClaude: () => findClaude,
1865
- findTranscript: () => findTranscript,
1866
- isUuid: () => isUuid,
1867
- liveProcess: () => liveProcess,
1868
- loginCommand: () => loginCommand,
1869
- loginHint: () => loginHint,
1870
- modeFlags: () => modeFlags,
1871
- parseAuthStatus: () => parseAuthStatus,
1872
- parseCaps: () => parseCaps,
1873
- parseResult: () => parseResult,
1874
- recentlyWritten: () => recentlyWritten,
1875
- runClaude: () => runClaude,
1876
- sessionFolder: () => sessionFolder
1877
- });
1878
- import { execFile, execFileSync as execFileSync4, spawn } from "node:child_process";
1879
- import { accessSync, closeSync as closeSync2, constants, openSync as openSync2, readdirSync as readdirSync5, readFileSync as readFileSync6, readSync as readSync2, statSync as statSync4 } from "node:fs";
1880
- import os2 from "node:os";
1881
- import path8 from "node:path";
1882
- var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1883
- var JOB_TIMEOUT_MS = 30 * 6e4;
1884
- var OUT_CAP = 1024 * 1024;
1885
- var LIVE_WRITE_MS = 9e4;
1886
- function isExe(p) {
1887
- try {
1888
- if (!statSync4(p).isFile()) return false;
1889
- accessSync(p, constants.X_OK);
1890
- return true;
1891
- } catch {
1892
- return false;
1893
- }
1894
- }
1895
- var cmpVersion = (a, b) => {
1896
- const pa = a.split(".").map(Number);
1897
- const pb = b.split(".").map(Number);
1898
- for (let i = 0; i < Math.max(pa.length, pb.length); i++)
1899
- if ((pa[i] ?? 0) !== (pb[i] ?? 0)) return (pa[i] ?? 0) - (pb[i] ?? 0);
1900
- return 0;
1901
- };
1902
- function findClaude(env = process.env, home = os2.homedir()) {
1903
- if (env.SESSIONPIPE_CLAUDE && isExe(env.SESSIONPIPE_CLAUDE)) return env.SESSIONPIPE_CLAUDE;
1904
- const names = process.platform === "win32" ? ["claude.exe", "claude.cmd"] : ["claude"];
1905
- for (const d of String(env.PATH || "").split(path8.delimiter)) {
1906
- if (!d || !path8.isAbsolute(d)) continue;
1907
- for (const n of names) if (isExe(path8.join(d, n))) return path8.join(d, n);
1908
- }
1909
- for (const p of [
1910
- path8.join(home, ".claude", "local", "claude"),
1911
- path8.join(home, ".local", "bin", "claude"),
1912
- "/opt/homebrew/bin/claude",
1913
- "/usr/local/bin/claude"
1914
- ])
1915
- if (isExe(p)) return p;
1916
- const app = path8.join(home, "Library", "Application Support", "Claude", "claude-code");
1917
- let versions = [];
1918
- try {
1919
- versions = readdirSync5(app).filter((n) => /^\d+(\.\d+)*$/.test(n)).sort(cmpVersion).reverse();
1920
- } catch {
1921
- }
1922
- for (const v of versions) {
1923
- const p = path8.join(app, v, "claude.app", "Contents", "MacOS", "claude");
1924
- if (isExe(p)) return p;
1925
- }
1926
- return null;
1927
- }
1928
- function optionBlock(help, flag) {
1929
- const lines = help.split("\n");
1930
- const re = new RegExp(`^\\s{0,4}(?:-\\w, )?${flag.replace(/-/g, "\\-")}(?![\\w-])`);
1931
- const start = lines.findIndex((l) => re.test(l));
1932
- if (start < 0) return null;
1933
- const out = [lines[start]];
1934
- for (let i = start + 1; i < lines.length && lines[i]?.trim() && !/^\s{0,4}-/.test(lines[i]); i++)
1935
- out.push(lines[i]);
1936
- return out.join(" ");
1937
- }
1938
- function parseCaps(help) {
1939
- const pm = optionBlock(help, "--permission-mode");
1940
- const pp = optionBlock(help, "--permission-prompts");
1941
- return {
1942
- hasMode: !!pm,
1943
- modes: pm ? [...pm.matchAll(/"([A-Za-z]+)"/g)].map((m) => m[1]) : [],
1944
- promptsNone: !!pp && /"none"/.test(pp),
1945
- fork: !!optionBlock(help, "--fork-session"),
1946
- stream: !!optionBlock(help, "--include-partial-messages")
1947
- };
1948
- }
1949
- function modeFlags(mode, caps) {
1950
- if (mode === "auto") {
1951
- if (!caps.modes.includes("auto"))
1952
- return { error: "this machine is in auto mode, but its Claude Code has no auto permission mode" };
1953
- return { args: ["--permission-mode", "auto"] };
1954
- }
1955
- if (caps.modes.includes("dontAsk")) return { args: ["--permission-mode", "dontAsk"] };
1956
- const args = [];
1957
- if (caps.hasMode)
1958
- args.push(
1959
- "--permission-mode",
1960
- caps.modes.includes("manual") && !caps.modes.includes("default") ? "manual" : "default"
1961
- );
1962
- if (caps.promptsNone) args.push("--permission-prompts", "none");
1963
- return { args };
1964
- }
1965
- function claudeEnv(configDir, env = process.env) {
1966
- const e = { ...env };
1967
- for (const k of [
1968
- "CLAUDECODE",
1969
- "CLAUDE_CODE_SESSION_ID",
1970
- "CLAUDE_CODE_HOST_SESSION_ID",
1971
- "CLAUDE_CODE_ENTRYPOINT",
1972
- "CLAUDE_CODE_SESSION_ATTENDED",
1973
- "CLAUDE_CODE_SSE_PORT"
1974
- ])
1975
- delete e[k];
1976
- e.SESSIONPIPE_CONTROL_JOB = "1";
1977
- if (configDir !== void 0) {
1978
- if (path8.resolve(configDir) === path8.resolve(os2.homedir(), ".claude")) delete e.CLAUDE_CONFIG_DIR;
1979
- else e.CLAUDE_CONFIG_DIR = configDir;
1980
- }
1981
- return e;
1982
- }
1983
- function parseAuthStatus(stdout) {
1984
- const text = String(stdout || "").trim();
1985
- const start = text.indexOf("{");
1986
- if (start < 0) return { loggedIn: null };
1987
- try {
1988
- const j = JSON.parse(text.slice(start));
1989
- if (typeof j.loggedIn !== "boolean") return { loggedIn: null };
1990
- return {
1991
- loggedIn: j.loggedIn,
1992
- ...typeof j.authMethod === "string" ? { method: j.authMethod } : {},
1993
- ...typeof j.subscriptionType === "string" ? { subscription: j.subscriptionType } : {}
1994
- };
1995
- } catch {
1996
- return { loggedIn: null };
1997
- }
1998
- }
1999
- function claudeLogin(bin, configDir, env = process.env) {
2000
- return new Promise((resolve) => {
2001
- execFile(
2002
- bin,
2003
- ["auth", "status", "--json"],
2004
- { encoding: "utf8", timeout: 2e4, env: claudeEnv(configDir, env), maxBuffer: 256 * 1024 },
2005
- // `auth status` may exit non-zero when signed out; its JSON still says so.
2006
- (_err, stdout) => resolve(parseAuthStatus(stdout))
2007
- );
2008
- });
2009
- }
2010
- function loginCommand(configDir) {
2011
- const home = os2.homedir();
2012
- const isDefault = !configDir || path8.resolve(configDir) === path8.resolve(home, ".claude");
2013
- return isDefault ? "`claude auth login`" : `\`CLAUDE_CONFIG_DIR=${configDir.replace(home, "~")} claude auth login\``;
2014
- }
2015
- function loginHint(configDir, platform = process.platform) {
2016
- const shown = configDir ? configDir.replace(os2.homedir(), "~") : "~/.claude";
2017
- const login = loginCommand(configDir);
2018
- if (platform === "darwin")
2019
- return `Claude Code (${shown}) isn't signed in as this machine's background service sees it. On a Mac its login lives in the login keychain, which a background service can read only while someone is logged in at the screen: log in at the Mac, or run ${login} there.`;
2020
- return `Claude Code (${shown}) isn't signed in on this machine: run ${login} there.`;
2021
- }
2022
- function detectCaps(bin) {
2023
- let help = "";
2024
- try {
2025
- help = execFileSync4(bin, ["--help"], {
2026
- encoding: "utf8",
2027
- timeout: 3e4,
2028
- env: claudeEnv(),
2029
- stdio: ["ignore", "pipe", "pipe"],
2030
- maxBuffer: 4 * 1024 * 1024
2031
- });
2032
- } catch (e) {
2033
- help = String(e?.stdout || "");
2034
- }
2035
- return parseCaps(help);
2036
- }
2037
- function findTranscript(session, dirs) {
2038
- if (!UUID_RE.test(session)) return null;
2039
- for (const dir of dirs) {
2040
- let projects = [];
2041
- try {
2042
- projects = readdirSync5(path8.join(dir, "projects"), { withFileTypes: true });
2043
- } catch {
2044
- continue;
2045
- }
2046
- for (const p of projects) {
2047
- if (!p.isDirectory()) continue;
2048
- const file = path8.join(dir, "projects", p.name, `${session}.jsonl`);
2049
- try {
2050
- if (statSync4(file).isFile()) return { file, configDir: dir, project: p.name };
2051
- } catch {
2052
- }
2053
- }
2054
- }
2055
- return null;
2056
- }
2057
- var projectName = (cwd) => cwd.replace(/[^a-zA-Z0-9]/g, "-");
2058
- function sessionFolder(t, cap2 = 8 * 1024 * 1024) {
2059
- let first = null;
2060
- let count = 0;
2061
- let fd;
2062
- const look = (line) => {
2063
- if (line.indexOf('"cwd"') < 0) return null;
2064
- let j;
2065
- try {
2066
- j = JSON.parse(line.toString("utf8"));
2067
- } catch {
2068
- return null;
2069
- }
2070
- if (!j || typeof j.cwd !== "string" || !path8.isAbsolute(j.cwd)) return null;
2071
- if (projectName(j.cwd) === t.project) return j.cwd;
2072
- first ??= j.cwd;
2073
- count++;
2074
- return null;
2075
- };
2076
- try {
2077
- fd = openSync2(t.file, "r");
2078
- const buf = Buffer.alloc(256 * 1024);
2079
- let pos = 0;
2080
- let rest = Buffer.alloc(0);
2081
- let skipping = false;
2082
- while (pos < cap2 && count < 50) {
2083
- const n = readSync2(fd, buf, 0, buf.length, pos);
2084
- if (!n) {
2085
- if (!skipping && rest.length) {
2086
- const hit = look(rest);
2087
- if (hit) return hit;
2088
- }
2089
- break;
2090
- }
2091
- pos += n;
2092
- let chunk = rest.length ? Buffer.concat([rest, buf.subarray(0, n)]) : buf.subarray(0, n);
2093
- let nl = chunk.indexOf(10);
2094
- while (nl >= 0) {
2095
- const line = chunk.subarray(0, nl);
2096
- chunk = chunk.subarray(nl + 1);
2097
- if (skipping) skipping = false;
2098
- else {
2099
- const hit = look(line);
2100
- if (hit) return hit;
2101
- }
2102
- nl = chunk.indexOf(10);
2103
- }
2104
- if (chunk.length > 2 * 1024 * 1024) {
2105
- skipping = true;
2106
- rest = Buffer.alloc(0);
2107
- } else rest = skipping ? Buffer.alloc(0) : Buffer.from(chunk);
2108
- }
2109
- } catch {
2110
- } finally {
2111
- if (fd !== void 0)
2112
- try {
2113
- closeSync2(fd);
2114
- } catch {
2115
- }
2116
- }
2117
- return first;
2118
- }
2119
- function pidAlive(pid) {
2120
- if (!Number.isSafeInteger(pid) || pid <= 1) return false;
2121
- try {
2122
- process.kill(pid, 0);
2123
- return true;
2124
- } catch (e) {
2125
- return e.code === "EPERM";
2126
- }
2127
- }
2128
- function liveProcess(session, configDir) {
2129
- let names = [];
2130
- try {
2131
- names = readdirSync5(path8.join(configDir, "sessions")).filter((n) => /^\d+\.json$/.test(n));
2132
- } catch {
2133
- return false;
2134
- }
2135
- for (const n of names) {
2136
- try {
2137
- const j = JSON.parse(readFileSync6(path8.join(configDir, "sessions", n), "utf8"));
2138
- if (String(j.sessionId) !== session) continue;
2139
- if (pidAlive(Number(j.pid ?? n.slice(0, -5)))) return true;
2140
- } catch {
2141
- }
2142
- }
2143
- return false;
2144
- }
2145
- function recentlyWritten(file, ourLastEnd, now = Date.now()) {
2146
- let m;
2147
- try {
2148
- m = statSync4(file).mtimeMs;
2149
- } catch {
2150
- return false;
2151
- }
2152
- if (now - m > LIVE_WRITE_MS) return false;
2153
- return !(ourLastEnd && m <= ourLastEnd + 2e3);
2154
- }
2155
- function runClaude(bin, args, o) {
2156
- return new Promise((resolve) => {
2157
- let out = [];
2158
- let outLen = 0;
2159
- let tooBig = false;
2160
- let errTail = "";
2161
- let timedOut = false;
2162
- let settled = false;
2163
- let child;
2164
- try {
2165
- child = spawn(bin, args, {
2166
- cwd: o.cwd,
2167
- env: o.env,
2168
- stdio: ["ignore", "pipe", "pipe"],
2169
- detached: process.platform !== "win32",
2170
- windowsHide: true
2171
- });
2172
- } catch (e) {
2173
- resolve({ error: e.message });
2174
- return;
2175
- }
2176
- const kill = (sig) => {
2177
- try {
2178
- if (process.platform !== "win32" && child.pid) process.kill(-child.pid, sig);
2179
- else child.kill(sig);
2180
- } catch {
2181
- try {
2182
- child.kill(sig);
2183
- } catch {
2184
- }
2185
- }
2186
- };
2187
- let partial = "";
2188
- child.stdout?.on("data", (b) => {
2189
- if (o.onLine) {
2190
- const chunk = partial + b.toString("utf8");
2191
- const lines = chunk.split("\n");
2192
- partial = lines.pop() ?? "";
2193
- for (const line of lines) {
2194
- if (!line.trim()) continue;
2195
- try {
2196
- o.onLine(line);
2197
- } catch {
2198
- }
2199
- if (line.includes('"type":"result"')) {
2200
- out = [Buffer.from(line)];
2201
- outLen = line.length;
2202
- }
2203
- }
2204
- if (partial.length > OUT_CAP) partial = "";
2205
- return;
2206
- }
2207
- if (tooBig) return;
2208
- if (outLen + b.length > OUT_CAP) {
2209
- tooBig = true;
2210
- out = [];
2211
- return;
2212
- }
2213
- out.push(b);
2214
- outLen += b.length;
2215
- });
2216
- child.stderr?.on("data", (b) => {
2217
- errTail = (errTail + b.toString("utf8")).slice(-4e3);
2218
- });
2219
- const timer = setTimeout(() => {
2220
- timedOut = true;
2221
- kill("SIGTERM");
2222
- setTimeout(() => kill("SIGKILL"), 1e4).unref();
2223
- }, o.timeoutMs ?? JOB_TIMEOUT_MS);
2224
- const result = (code, signal) => ({
2225
- code,
2226
- signal,
2227
- stdout: tooBig ? null : Buffer.concat(out).toString("utf8"),
2228
- tooBig,
2229
- stderr: errTail,
2230
- timedOut
2231
- });
2232
- const finish = (r) => {
2233
- if (settled) return;
2234
- settled = true;
2235
- clearTimeout(timer);
2236
- resolve(r);
2237
- };
2238
- child.on("error", (e) => finish({ error: e.message }));
2239
- child.on("exit", (code, signal) => setTimeout(() => finish(result(code, signal)), 2e3).unref());
2240
- child.on("close", (code, signal) => {
2241
- if (o.onLine && partial.trim()) {
2242
- try {
2243
- o.onLine(partial);
2244
- } catch {
2245
- }
2246
- if (partial.includes('"type":"result"')) {
2247
- out = [Buffer.from(partial)];
2248
- outLen = partial.length;
2249
- }
2250
- partial = "";
2220
+ backfill(sinceMs, env = process.env) {
2221
+ const rows = [];
2222
+ for (const f of recentFiles(sessionsDir(env), 3, sinceMs, (n) => /^rollout-.*\.jsonl$/.test(n))) {
2223
+ const m = /([0-9a-f]{8}-[0-9a-f-]{27,})\.jsonl$/i.exec(path9.basename(f));
2224
+ if (!m?.[1]) continue;
2225
+ const facts = codexFacts(f);
2226
+ if (!facts.started_at || !facts.last_at) continue;
2227
+ const { started_at, last_at, first_ask: _fa, harness_version: _hv, ...rest } = facts;
2228
+ rows.push({ session: { id: m[1], ...rest }, started_at, last_at });
2229
+ }
2230
+ return rows;
2231
+ }
2232
+ };
2233
+ function prune3(o) {
2234
+ for (const k of Object.keys(o)) if (o[k] === void 0) delete o[k];
2235
+ return o;
2236
+ }
2237
+ function codexFacts(file) {
2238
+ const { head, tail } = ends(file, 128 * 1024, 64 * 1024);
2239
+ const H = parseLines(head);
2240
+ const T = parseLines(tail);
2241
+ const out = {};
2242
+ const meta = H.find((j) => j.type === "session_meta" && j.payload);
2243
+ if (meta?.payload) {
2244
+ const p = meta.payload;
2245
+ if (p.cwd) out.cwd = p.cwd;
2246
+ if (p.cli_version) out.harness_version = String(p.cli_version);
2247
+ if (p.git?.branch) out.branch = p.git.branch;
2248
+ if (p.git?.repository_url) out.repo = String(p.git.repository_url).replace(/\/\/[^/@]*@/, "//");
2249
+ }
2250
+ const ctx = H.concat(T).reverse().find((j) => j.type === "turn_context" && j.payload?.model);
2251
+ if (ctx?.payload?.model) out.model = ctx.payload.model;
2252
+ for (const j of H) {
2253
+ const p = j.payload;
2254
+ if (j.type !== "response_item" || !p || p.type !== "message" || p.role !== "user") continue;
2255
+ const text = (p.content ?? []).filter((b) => b && b.type === "input_text" && typeof b.text === "string").map((b) => b.text).join("\n").trim();
2256
+ if (!text || codexInjected(text)) continue;
2257
+ const line = text.split("\n").find((l) => l.trim()) ?? "";
2258
+ out.title = line.length > 80 ? `${line.slice(0, 80).replace(/\s+\S*$/, "")}\u2026` : line;
2259
+ out.title_source = "first-ask";
2260
+ break;
2261
+ }
2262
+ const times = H.concat(T).map((j) => Date.parse(String(j.timestamp ?? ""))).filter((n) => n > 0);
2263
+ if (times.length) {
2264
+ out.started_at = Math.min(...times);
2265
+ out.last_at = Math.max(...times);
2266
+ }
2267
+ return out;
2268
+ }
2269
+
2270
+ // src/adapters/codex-control.ts
2271
+ var codex_control_exports = {};
2272
+ __export(codex_control_exports, {
2273
+ CodexReading: () => CodexReading,
2274
+ LOGIN_HINT: () => LOGIN_HINT,
2275
+ codexArgs: () => codexArgs,
2276
+ codexEnv: () => codexEnv,
2277
+ codexFacts: () => codexFacts,
2278
+ codexLine: () => codexLine,
2279
+ codexLogin: () => codexLogin,
2280
+ codexModeFlags: () => codexModeFlags,
2281
+ findCodex: () => findCodex,
2282
+ findRollout: () => findRollout,
2283
+ reading: () => reading2
2284
+ });
2285
+ import { execFile as execFile2 } from "node:child_process";
2286
+ import os4 from "node:os";
2287
+ import path10 from "node:path";
2288
+ var LABEL2 = "Codex";
2289
+ function findCodex(env = process.env, home = os4.homedir(), nodeDir = path10.dirname(process.execPath)) {
2290
+ return findBinary({
2291
+ override: env.SESSIONPIPE_CODEX,
2292
+ names: process.platform === "win32" ? ["codex.exe", "codex.cmd"] : ["codex"],
2293
+ env,
2294
+ dirs: [nodeDir, path10.join(home, ".local", "bin"), "/opt/homebrew/bin", "/usr/local/bin"]
2295
+ });
2296
+ }
2297
+ function codexModeFlags(mode, resume = false) {
2298
+ if (resume)
2299
+ return mode === "auto" ? [
2300
+ "-c",
2301
+ 'sandbox_mode="workspace-write"',
2302
+ "-c",
2303
+ 'approval_policy="on-request"',
2304
+ "-c",
2305
+ 'approvals_reviewer="auto_review"'
2306
+ ] : ["-c", 'sandbox_mode="workspace-write"'];
2307
+ return mode === "auto" ? ["--approve-for-me"] : ["-s", "workspace-write"];
2308
+ }
2309
+ function codexArgs(o) {
2310
+ const common = ["--json", "--skip-git-repo-check"];
2311
+ if (o.resume) return ["exec", "resume", ...common, ...codexModeFlags(o.mode, true), "--", o.resume, o.prompt];
2312
+ return ["exec", ...common, ...o.cwd ? ["-C", o.cwd] : [], ...codexModeFlags(o.mode), "--", o.prompt];
2313
+ }
2314
+ var codexEnv = (env = process.env) => jobEnv(env);
2315
+ var LOGIN_HINT = "Codex isn't signed in on this machine: run `codex login --device-auth` there.";
2316
+ function codexLogin(bin, env = process.env) {
2317
+ if (env.CODEX_API_KEY) return Promise.resolve({ loggedIn: true, method: "api key" });
2318
+ return new Promise((resolve) => {
2319
+ execFile2(
2320
+ bin,
2321
+ ["login", "status"],
2322
+ { encoding: "utf8", timeout: 2e4, env: codexEnv(env), maxBuffer: 256 * 1024 },
2323
+ (err) => {
2324
+ if (!err) return resolve({ loggedIn: true });
2325
+ const code = err.code;
2326
+ resolve(code === 1 ? { loggedIn: false } : { loggedIn: null });
2251
2327
  }
2252
- finish(result(code, signal));
2253
- });
2328
+ );
2254
2329
  });
2255
2330
  }
2256
- function parseResult(stdout) {
2257
- const s = String(stdout || "").trim();
2258
- if (!s) return null;
2259
- const pick = (j) => {
2260
- if (Array.isArray(j)) {
2261
- for (let i = j.length - 1; i >= 0; i--) if (j[i]?.type === "result") return j[i];
2331
+ function codexLine(line) {
2332
+ let j;
2333
+ try {
2334
+ j = JSON.parse(line);
2335
+ } catch {
2336
+ return null;
2337
+ }
2338
+ if (!j || typeof j !== "object") return null;
2339
+ const str = (v) => typeof v === "string" ? v : "";
2340
+ switch (j.type) {
2341
+ case "thread.started":
2342
+ return str(j.thread_id) ? { session: str(j.thread_id) } : null;
2343
+ case "item.completed": {
2344
+ const it = j.item;
2345
+ if (it?.type === "agent_message" && str(it.text)) return { message: str(it.text) };
2346
+ if (it?.type === "error" && str(it.message)) return { warning: str(it.message) };
2262
2347
  return null;
2263
2348
  }
2264
- return j && typeof j === "object" ? j : null;
2265
- };
2349
+ case "turn.completed":
2350
+ return { done: true };
2351
+ case "turn.failed":
2352
+ return { done: true, failed: str(j.error?.message) || "Codex reported an error" };
2353
+ case "error":
2354
+ return str(j.message) ? { note: str(j.message) } : null;
2355
+ default:
2356
+ return null;
2357
+ }
2358
+ }
2359
+ var CodexReading = class {
2360
+ session = null;
2361
+ lines = 0;
2362
+ warnings = [];
2363
+ messages = [];
2364
+ done = false;
2365
+ failed = null;
2366
+ note = null;
2367
+ get text() {
2368
+ return this.messages.join("\n\n");
2369
+ }
2370
+ feed(line) {
2371
+ this.lines++;
2372
+ const p = codexLine(line);
2373
+ if (!p) return false;
2374
+ if (p.session && !this.session) this.session = p.session;
2375
+ if (p.note) this.note = p.note;
2376
+ if (p.warning && this.warnings.length < 20) this.warnings.push(p.warning);
2377
+ if (p.failed) this.failed = p.failed;
2378
+ if (p.done) this.done = true;
2379
+ if (p.message?.trim()) {
2380
+ this.messages.push(p.message.trim());
2381
+ return true;
2382
+ }
2383
+ return false;
2384
+ }
2385
+ /** A failure is `turn.failed`, or a run that exits non-zero having said nothing; an
2386
+ * `error` item before a turn that then succeeds is only a warning. */
2387
+ outcome(res) {
2388
+ const broke = runFailure(LABEL2, res);
2389
+ if (broke) return { failed: broke };
2390
+ if (this.failed) return { failed: this.failed };
2391
+ if (this.done || res.code === 0 || this.messages.length)
2392
+ return { reply: this.text || "(Codex finished without a written reply.)" };
2393
+ const why = this.note || codexStderr(res.stderr) || this.warnings.at(-1) || "";
2394
+ return { failed: `Codex exited (${res.signal || `code ${res.code}`}) without an answer${why ? `: ${why}` : ""}` };
2395
+ }
2396
+ };
2397
+ function codexStderr(stderr) {
2398
+ return stderrTail(
2399
+ String(stderr || "").split("\n").filter((l) => !/^Reading additional input from stdin/.test(l.trim())).join("\n")
2400
+ );
2401
+ }
2402
+ var reading2 = () => new CodexReading();
2403
+
2404
+ // src/adapters/gemini-cli.ts
2405
+ import { execFileSync as execFileSync4 } from "node:child_process";
2406
+ import { existsSync as existsSync5, readFileSync as readFileSync6 } from "node:fs";
2407
+ import path11 from "node:path";
2408
+ var NAME4 = "gemini-cli";
2409
+ var GEMINI_EVENTS = [
2410
+ "SessionStart",
2411
+ "BeforeAgent",
2412
+ "BeforeTool",
2413
+ "AfterTool",
2414
+ "Notification",
2415
+ "AfterAgent",
2416
+ "PreCompress",
2417
+ "SessionEnd"
2418
+ ];
2419
+ var OPTS3 = { matcherFor: (e) => /Tool/.test(e) ? ".*" : void 0, timeout: 5e3 };
2420
+ var geminiDir = (env) => env.GEMINI_CLI_HOME || path11.join(HOME, ".gemini");
2421
+ var settings = (env) => path11.join(geminiDir(env), "settings.json");
2422
+ var validTool4 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
2423
+ var eventsFor3 = (lean) => lean ? GEMINI_EVENTS.filter((e) => e !== "BeforeTool") : [...GEMINI_EVENTS];
2424
+ function geminiHooksActive(settingsFile) {
2425
+ let tools;
2266
2426
  try {
2267
- return pick(JSON.parse(s));
2427
+ tools = JSON.parse(readFileSync6(settingsFile, "utf8")).tools;
2268
2428
  } catch {
2269
2429
  }
2270
- const lines = s.split("\n");
2271
- for (let i = lines.length - 1; i >= 0; i--) {
2430
+ if (tools?.enableHooks === true) return { active: true };
2431
+ if (tools?.enableHooks === false)
2432
+ return { active: false, note: "inactive: tools.enableHooks is false in settings.json" };
2433
+ let version = "";
2434
+ try {
2435
+ version = execFileSync4("gemini", ["--version"], {
2436
+ encoding: "utf8",
2437
+ timeout: 4e3,
2438
+ stdio: ["ignore", "pipe", "ignore"]
2439
+ }).trim();
2440
+ } catch {
2441
+ return {
2442
+ active: true,
2443
+ note: "could not run `gemini --version`; hooks need tools.enableHooks on versions before 0.46"
2444
+ };
2445
+ }
2446
+ const m = /(\d+)\.(\d+)/.exec(version);
2447
+ if (m && (Number(m[1]) > 0 || Number(m[2]) >= 46)) return { active: true };
2448
+ return {
2449
+ active: false,
2450
+ note: `inactive: Gemini CLI ${version || "?"} runs hooks only with "tools": {"enableHooks": true} in settings.json`
2451
+ };
2452
+ }
2453
+ var geminiCli = {
2454
+ name: NAME4,
2455
+ events: GEMINI_EVENTS,
2456
+ // ~/.gemini also belongs to Antigravity; Gemini CLI is here when its settings or its chat logs exist.
2457
+ detect: (env = process.env) => existsSync5(settings(env)) || existsSync5(path11.join(geminiDir(env), "tmp")),
2458
+ configFiles: (env = process.env) => [settings(env)],
2459
+ install: (cmd, env = process.env, opts = {}) => {
2460
+ const r = installClaudeShaped(settings(env), NAME4, eventsFor3(opts.lean), cmd, OPTS3);
2461
+ const a = geminiHooksActive(settings(env));
2462
+ return [a.note && !r.skipped ? { ...r, note: [r.note, a.note].filter(Boolean).join("; ") } : r];
2463
+ },
2464
+ uninstall: (env = process.env, opts = {}) => [
2465
+ uninstallClaudeShaped(settings(env), GEMINI_EVENTS, { created: opts.created?.includes(settings(env)) })
2466
+ ],
2467
+ installed: (cmd, env = process.env, opts = {}) => {
2468
+ const state = installedClaudeShaped(settings(env), NAME4, eventsFor3(opts.lean), cmd, OPTS3);
2469
+ const a = geminiHooksActive(settings(env));
2470
+ return [
2471
+ {
2472
+ file: settings(env),
2473
+ state: state === "current" && !a.active ? "inactive" : state,
2474
+ ...a.note ? { note: a.note } : {}
2475
+ }
2476
+ ];
2477
+ },
2478
+ fromHook(input) {
2479
+ const event = input.argv[1] ?? "";
2480
+ let s = {};
2272
2481
  try {
2273
- const j = pick(JSON.parse(lines[i]));
2274
- if (j && (j.type === "result" || "result" in j)) return j;
2482
+ s = input.stdin ? JSON.parse(input.stdin) : {};
2275
2483
  } catch {
2276
2484
  }
2277
- }
2278
- return null;
2485
+ const id = s.session_id;
2486
+ if (!id) return null;
2487
+ const session = { id: String(id), cwd: typeof s.cwd === "string" ? s.cwd : input.cwd };
2488
+ const tool = validTool4(s.tool_name) ? s.tool_name : void 0;
2489
+ const ev = (type, data) => ({ type, data: prune4(data), harnessEvent: event });
2490
+ const events = [];
2491
+ switch (event) {
2492
+ case "SessionStart":
2493
+ events.push(
2494
+ ev("session.started", {
2495
+ source: typeof s.source === "string" && ["startup", "resume", "clear"].includes(s.source) ? s.source : "unknown"
2496
+ })
2497
+ );
2498
+ break;
2499
+ case "BeforeAgent":
2500
+ events.push(
2501
+ ev("turn.started", { prompt_chars: typeof s.prompt === "string" ? [...s.prompt].length : void 0 })
2502
+ );
2503
+ break;
2504
+ case "BeforeTool":
2505
+ if (tool) events.push(ev("tool.started", { tool, input: s.tool_input }));
2506
+ break;
2507
+ case "AfterTool": {
2508
+ const resp = s.tool_response;
2509
+ const err = resp && typeof resp === "object" && resp.error ? String(typeof resp.error === "string" ? resp.error : JSON.stringify(resp.error)).slice(0, 2e3) : void 0;
2510
+ if (tool)
2511
+ events.push(
2512
+ ev("tool.ended", {
2513
+ tool,
2514
+ ok: !err,
2515
+ error: err,
2516
+ input: s.tool_input,
2517
+ output: resp?.llmContent ?? s.tool_response
2518
+ })
2519
+ );
2520
+ break;
2521
+ }
2522
+ case "Notification":
2523
+ events.push(
2524
+ ev("attention.needed", {
2525
+ attention_id: `notif-${Date.now()}`,
2526
+ kind: s.notification_type === "ToolPermission" ? "permission" : "question",
2527
+ message: typeof s.message === "string" ? s.message.slice(0, 200) : void 0
2528
+ })
2529
+ );
2530
+ break;
2531
+ case "AfterAgent":
2532
+ events.push(ev("turn.ended", { reason: "stop" }));
2533
+ break;
2534
+ case "PreCompress":
2535
+ events.push(ev("context.compacted", { trigger: s.trigger === "manual" ? "manual" : "auto" }));
2536
+ break;
2537
+ case "SessionEnd":
2538
+ events.push(
2539
+ ev("session.ended", {
2540
+ reason: s.reason === "clear" || s.reason === "logout" || s.reason === "exit" ? s.reason : s.reason === "prompt_input_exit" ? "exit" : "other"
2541
+ })
2542
+ );
2543
+ break;
2544
+ default:
2545
+ return null;
2546
+ }
2547
+ const r = { session, events, hints: {} };
2548
+ if (typeof s.transcript_path === "string") r.transcript = s.transcript_path;
2549
+ return r;
2550
+ },
2551
+ readTranscript(file, fromLine) {
2552
+ let text = "";
2553
+ try {
2554
+ text = readFileSync6(file, "utf8");
2555
+ } catch {
2556
+ return { turns: [], line: fromLine };
2557
+ }
2558
+ return readGemini(text.split("\n"), fromLine);
2559
+ },
2560
+ facts(_session, transcript) {
2561
+ const out = {};
2562
+ if (!transcript) return out;
2563
+ const { head, tail } = ends(transcript, 64 * 1024, 64 * 1024);
2564
+ const H = parseLines(head);
2565
+ const T = parseLines(tail);
2566
+ const first = H.find((j) => j.type === "user");
2567
+ const raw = first ? first.content ?? first.message ?? first.text : void 0;
2568
+ const text = typeof raw === "string" ? raw.trim() : "";
2569
+ if (text) {
2570
+ const line = text.split("\n").find((l) => l.trim()) ?? "";
2571
+ out.title = line.length > 80 ? `${line.slice(0, 80).replace(/\s+\S*$/, "")}\u2026` : line;
2572
+ out.title_source = "first-ask";
2573
+ }
2574
+ const times = H.concat(T).map((j) => Date.parse(String(j.timestamp ?? ""))).filter((n) => n > 0);
2575
+ if (times.length) {
2576
+ out.started_at = Math.min(...times);
2577
+ out.last_at = Math.max(...times);
2578
+ }
2579
+ return out;
2580
+ },
2581
+ backfill: () => []
2582
+ // chat logs are keyed by a project hash, not a session id we can name; the first hook of a resumed session reports it
2583
+ };
2584
+ function prune4(o) {
2585
+ for (const k of Object.keys(o)) if (o[k] === void 0) delete o[k];
2586
+ return o;
2279
2587
  }
2280
- var isUuid = (s) => typeof s === "string" && UUID_RE.test(s);
2281
2588
 
2282
2589
  // src/adapters/index.ts
2283
2590
  var ADAPTERS = [claudeCode, codex, geminiCli, antigravity];
2284
2591
  var adapterByName = (name) => ADAPTERS.find((a) => a.name === name);
2592
+ function drivableHarnesses(env = process.env) {
2593
+ return [
2594
+ ...findClaude(env) ? ["claude-code"] : [],
2595
+ ...findCodex(env) ? ["codex"] : [],
2596
+ ...findAgy(env) ? ["antigravity"] : []
2597
+ ];
2598
+ }
2285
2599
 
2286
2600
  // src/config.ts
2287
2601
  import { chmodSync, existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
2288
- import path9 from "node:path";
2602
+ import path12 from "node:path";
2289
2603
  var DEFAULT_CONFIG = { sinks: [], harnesses: {}, update_check: true, keep_days: 30 };
2290
2604
  function readConfig(file = configFile()) {
2291
2605
  try {
@@ -2301,7 +2615,7 @@ function readConfig(file = configFile()) {
2301
2615
  }
2302
2616
  }
2303
2617
  function writeConfig(c, file = configFile()) {
2304
- mkdirSync4(path9.dirname(file), { recursive: true, mode: 448 });
2618
+ mkdirSync4(path12.dirname(file), { recursive: true, mode: 448 });
2305
2619
  writeFileSync4(file, `${JSON.stringify(c, null, 2)}
2306
2620
  `, { mode: 384 });
2307
2621
  if (process.platform !== "win32" && existsSync6(file)) {
@@ -2378,24 +2692,24 @@ import {
2378
2692
  readFileSync as readFileSync8,
2379
2693
  readSync as readSync3,
2380
2694
  renameSync,
2381
- statSync as statSync5,
2695
+ statSync as statSync6,
2382
2696
  unlinkSync as unlinkSync3,
2383
2697
  writeFileSync as writeFileSync5
2384
2698
  } from "node:fs";
2385
- import path10 from "node:path";
2699
+ import path13 from "node:path";
2386
2700
  var Outbox = class {
2387
2701
  constructor(root) {
2388
2702
  this.root = root;
2389
2703
  }
2390
2704
  root;
2391
2705
  dir(harness) {
2392
- return path10.join(this.root, "outbox", safeId(harness));
2706
+ return path13.join(this.root, "outbox", safeId(harness));
2393
2707
  }
2394
2708
  file(ref) {
2395
- return path10.join(this.dir(ref.harness), `${safeId(ref.session)}.jsonl`);
2709
+ return path13.join(this.dir(ref.harness), `${safeId(ref.session)}.jsonl`);
2396
2710
  }
2397
2711
  seqFile(ref) {
2398
- return path10.join(this.dir(ref.harness), `${safeId(ref.session)}.seq`);
2712
+ return path13.join(this.dir(ref.harness), `${safeId(ref.session)}.seq`);
2399
2713
  }
2400
2714
  /** The next seq for a session, persisted (survives a hibernating machine and a
2401
2715
  * restart; an in-memory counter would restart at 0 and collide). */
@@ -2407,7 +2721,7 @@ var Outbox = class {
2407
2721
  } catch {
2408
2722
  n = this.lastSeqInFile(ref) + 1;
2409
2723
  }
2410
- mkdirSync5(path10.dirname(f), { recursive: true, mode: 448 });
2724
+ mkdirSync5(path13.dirname(f), { recursive: true, mode: 448 });
2411
2725
  writeFileSync5(f, String(n + 1), { mode: 384 });
2412
2726
  return n;
2413
2727
  }
@@ -2433,10 +2747,10 @@ var Outbox = class {
2433
2747
  /** Every session file, newest first. */
2434
2748
  sessions() {
2435
2749
  const out = [];
2436
- const base = path10.join(this.root, "outbox");
2750
+ const base = path13.join(this.root, "outbox");
2437
2751
  if (!existsSync7(base)) return [];
2438
2752
  for (const h of readdirSync6(base)) {
2439
- const d = path10.join(base, h);
2753
+ const d = path13.join(base, h);
2440
2754
  let names = [];
2441
2755
  try {
2442
2756
  names = readdirSync6(d).filter((n) => n.endsWith(".jsonl"));
@@ -2445,7 +2759,7 @@ var Outbox = class {
2445
2759
  }
2446
2760
  for (const n of names) {
2447
2761
  try {
2448
- out.push({ ref: { harness: h, session: n.slice(0, -6) }, mtime: statSync5(path10.join(d, n)).mtimeMs });
2762
+ out.push({ ref: { harness: h, session: n.slice(0, -6) }, mtime: statSync6(path13.join(d, n)).mtimeMs });
2449
2763
  } catch {
2450
2764
  }
2451
2765
  }
@@ -2457,7 +2771,7 @@ var Outbox = class {
2457
2771
  read(ref, from, maxEvents = 500) {
2458
2772
  const f = this.file(ref);
2459
2773
  if (!existsSync7(f)) return { events: [], offset: from };
2460
- const size = statSync5(f).size;
2774
+ const size = statSync6(f).size;
2461
2775
  if (from >= size) return { events: [], offset: from };
2462
2776
  const fd = openSync3(f, "r");
2463
2777
  try {
@@ -2491,7 +2805,7 @@ var Outbox = class {
2491
2805
  for (const ref of this.sessions()) {
2492
2806
  const f = this.file(ref);
2493
2807
  try {
2494
- if (statSync5(f).mtimeMs < cutoff) {
2808
+ if (statSync6(f).mtimeMs < cutoff) {
2495
2809
  unlinkSync3(f);
2496
2810
  try {
2497
2811
  unlinkSync3(this.seqFile(ref));
@@ -2521,7 +2835,7 @@ var Cursors = class {
2521
2835
  root;
2522
2836
  sink;
2523
2837
  file() {
2524
- return path10.join(this.root, "cursors", `${safeId(this.sink)}.json`);
2838
+ return path13.join(this.root, "cursors", `${safeId(this.sink)}.json`);
2525
2839
  }
2526
2840
  read() {
2527
2841
  try {
@@ -2532,7 +2846,7 @@ var Cursors = class {
2532
2846
  }
2533
2847
  write(c) {
2534
2848
  const f = this.file();
2535
- mkdirSync5(path10.dirname(f), { recursive: true, mode: 448 });
2849
+ mkdirSync5(path13.dirname(f), { recursive: true, mode: 448 });
2536
2850
  const tmp = `${f}.${process.pid}.tmp`;
2537
2851
  writeFileSync5(tmp, JSON.stringify(c), { mode: 384 });
2538
2852
  renameSync(tmp, f);
@@ -2542,11 +2856,11 @@ var Cursors = class {
2542
2856
  }
2543
2857
  };
2544
2858
  function takeLock(root, ref, staleMs = 5 * 6e4) {
2545
- const dir = path10.join(root, "locks");
2859
+ const dir = path13.join(root, "locks");
2546
2860
  mkdirSync5(dir, { recursive: true, mode: 448 });
2547
- const f = path10.join(dir, `${safeId(ref.harness)}-${safeId(ref.session)}.lock`);
2861
+ const f = path13.join(dir, `${safeId(ref.harness)}-${safeId(ref.session)}.lock`);
2548
2862
  try {
2549
- if (Date.now() - statSync5(f).mtimeMs > staleMs) unlinkSync3(f);
2863
+ if (Date.now() - statSync6(f).mtimeMs > staleMs) unlinkSync3(f);
2550
2864
  else return null;
2551
2865
  } catch {
2552
2866
  }
@@ -2863,7 +3177,7 @@ function filterEvent(event, tier, pii, opts = {}) {
2863
3177
 
2864
3178
  // src/sinks/file.ts
2865
3179
  import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync6 } from "node:fs";
2866
- import path11 from "node:path";
3180
+ import path14 from "node:path";
2867
3181
  var FileSink = class {
2868
3182
  constructor(name, file, tier, pii) {
2869
3183
  this.name = name;
@@ -2877,7 +3191,7 @@ var FileSink = class {
2877
3191
  pii;
2878
3192
  async send(events) {
2879
3193
  try {
2880
- mkdirSync6(path11.dirname(this.file), { recursive: true });
3194
+ mkdirSync6(path14.dirname(this.file), { recursive: true });
2881
3195
  appendFileSync2(this.file, `${events.map((e) => JSON.stringify(e)).join("\n")}
2882
3196
  `);
2883
3197
  return { ok: true, acknowledged: events.length, accepted: events.length, duplicates: 0, rejected: [] };
@@ -2910,10 +3224,12 @@ var StdoutSink = class {
2910
3224
  var PROTOCOL_VERSION = 1;
2911
3225
  export {
2912
3226
  ADAPTERS,
3227
+ AG_STALE_PRETOOL,
2913
3228
  Cursors,
2914
3229
  DEFAULT_CONFIG,
2915
3230
  FileSink,
2916
3231
  HOME,
3232
+ JOB_TIMEOUT_MS,
2917
3233
  Outbox,
2918
3234
  PROTOCOL_VERSION,
2919
3235
  REDACTED,
@@ -2923,11 +3239,16 @@ export {
2923
3239
  TYPE_TIER,
2924
3240
  adapterByName,
2925
3241
  antigravity,
3242
+ antigravityAnswer,
3243
+ antigravity_control_exports as antigravityControl,
2926
3244
  claudeCode,
2927
3245
  claude_code_control_exports as claudeControl,
2928
3246
  claudeDirs,
2929
3247
  codex,
3248
+ codex_control_exports as codexControl,
2930
3249
  configFile,
3250
+ deniedNote,
3251
+ drivableHarnesses,
2931
3252
  filterEvent,
2932
3253
  geminiCli,
2933
3254
  gitFacts,
@@ -2936,6 +3257,7 @@ export {
2936
3257
  isInjected,
2937
3258
  isSecretName,
2938
3259
  isUlid,
3260
+ jobEnv,
2939
3261
  machineName,
2940
3262
  makeEvent,
2941
3263
  pairTurns,
@@ -2948,6 +3270,7 @@ export {
2948
3270
  redactSecrets,
2949
3271
  reducePii,
2950
3272
  reducePiiDeep,
3273
+ runHeadless,
2951
3274
  safeId,
2952
3275
  stateDir,
2953
3276
  stripCredentials,