sessionpipe-core 0.6.1 → 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.1 · 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
  }
@@ -649,11 +652,288 @@ function antigravityFacts(file, opts) {
649
652
  return out;
650
653
  }
651
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
+
652
932
  // src/adapters/claude-code.ts
653
933
  import { execFileSync as execFileSync2 } from "node:child_process";
654
934
  import { createHash } from "node:crypto";
655
- import { existsSync as existsSync3, readdirSync as readdirSync3, readFileSync as readFileSync3, statSync as statSync3 } from "node:fs";
656
- 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";
657
937
 
658
938
  // src/readers/git.ts
659
939
  import { execFileSync } from "node:child_process";
@@ -720,23 +1000,23 @@ function claudeDirs(env = process.env, home = env.HOME || HOME) {
720
1000
  const out = [];
721
1001
  const add = (d) => {
722
1002
  if (!d) return;
723
- const r = path5.resolve(d);
1003
+ const r = path7.resolve(d);
724
1004
  if (!out.includes(r)) out.push(r);
725
1005
  };
726
- add(path5.join(home, ".claude"));
1006
+ add(path7.join(home, ".claude"));
727
1007
  add(env.CLAUDE_CONFIG_DIR);
728
1008
  try {
729
1009
  for (const e of readdirSync3(home, { withFileTypes: true })) {
730
1010
  if (!e.isDirectory() || !/^\.claude[-_.]/.test(e.name)) continue;
731
- const d = path5.join(home, e.name);
732
- 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);
733
1013
  }
734
1014
  } catch {
735
1015
  }
736
1016
  return out.filter((d, i) => i === 0 || existsSync3(d));
737
1017
  }
738
- var settingsIn = (dir) => path5.join(dir, "settings.json");
739
- 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)));
740
1020
  var validTool2 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
741
1021
  var endReason = (r) => r === "clear" || r === "logout" || r === "exit" ? r : r === "prompt_input_exit" ? "exit" : "other";
742
1022
  var notifKind = (t) => t === "permission_prompt" ? "permission" : t === "idle_prompt" ? "idle" : t === "elicitation_dialog" ? "elicitation" : "question";
@@ -908,11 +1188,11 @@ var claudeCode = {
908
1188
  const rows = [];
909
1189
  const files = [];
910
1190
  for (const d of claudeDirs(env))
911
- 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)));
912
1192
  const noState = { get: () => ({}), save: () => {
913
1193
  } };
914
1194
  for (const f of files) {
915
- const id = path5.basename(f, ".jsonl");
1195
+ const id = path7.basename(f, ".jsonl");
916
1196
  const facts = claudeFacts(f, id, {}, noState);
917
1197
  if (!facts.started_at || !facts.last_at) continue;
918
1198
  const { started_at, last_at, first_ask: _fa, harness_version: _hv, ...rest } = facts;
@@ -972,12 +1252,12 @@ function claudeLive(dir) {
972
1252
  const map = /* @__PURE__ */ new Map();
973
1253
  let names = [];
974
1254
  try {
975
- names = readdirSync3(path5.join(dir, "sessions")).filter((n) => n.endsWith(".json"));
1255
+ names = readdirSync3(path7.join(dir, "sessions")).filter((n) => n.endsWith(".json"));
976
1256
  } catch {
977
1257
  }
978
1258
  for (const n of names) {
979
1259
  try {
980
- 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")));
981
1261
  if (e?.sessionId) map.set(e.sessionId, e);
982
1262
  } catch {
983
1263
  }
@@ -988,7 +1268,7 @@ function claudeLive(dir) {
988
1268
  function claudeLiveByPid(dir, pid) {
989
1269
  if (!pid) return null;
990
1270
  try {
991
- 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")));
992
1272
  } catch {
993
1273
  return null;
994
1274
  }
@@ -996,7 +1276,7 @@ function claudeLiveByPid(dir, pid) {
996
1276
  function claudePid(env = process.env) {
997
1277
  try {
998
1278
  const dirs = claudeDirs(env);
999
- 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`)));
1000
1280
  let pid = process.ppid;
1001
1281
  if (has(pid)) return pid;
1002
1282
  const parent = parentOf();
@@ -1039,8 +1319,8 @@ var accountCache = /* @__PURE__ */ new Map();
1039
1319
  function claudeAccount(dir) {
1040
1320
  const hit = accountCache.get(dir);
1041
1321
  if (hit !== void 0) return hit;
1042
- const files = [path5.join(dir, ".claude.json")];
1043
- 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"));
1044
1324
  let acct = null;
1045
1325
  for (const f of files) {
1046
1326
  try {
@@ -1092,7 +1372,7 @@ function scanTitles(file, from, found) {
1092
1372
  const out = { custom: found.custom ?? null, ai: found.ai ?? null, upto: from };
1093
1373
  let size = 0;
1094
1374
  try {
1095
- size = statSync3(file).size;
1375
+ size = statSync4(file).size;
1096
1376
  } catch {
1097
1377
  return out;
1098
1378
  }
@@ -1125,7 +1405,7 @@ function claudeFacts(file, sessionId, hints, state) {
1125
1405
  const H = parseLines(head);
1126
1406
  const T = parseLines(tail);
1127
1407
  const out = {};
1128
- const id = sessionId || path5.basename(file, ".jsonl");
1408
+ const id = sessionId || path7.basename(file, ".jsonl");
1129
1409
  const mem = state.get(NAME2, id);
1130
1410
  let custom = null;
1131
1411
  let ai = null;
@@ -1135,7 +1415,7 @@ function claudeFacts(file, sessionId, hints, state) {
1135
1415
  }
1136
1416
  let size = 0;
1137
1417
  try {
1138
- size = statSync3(file).size;
1418
+ size = statSync4(file).size;
1139
1419
  } catch {
1140
1420
  }
1141
1421
  if (!custom && !ai && size > 64 * 1024 + 256 * 1024) {
@@ -1205,548 +1485,644 @@ function claudeFacts(file, sessionId, hints, state) {
1205
1485
  return out;
1206
1486
  }
1207
1487
 
1208
- // src/adapters/codex.ts
1209
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
1210
- import path6 from "node:path";
1211
-
1212
- // src/adapters/codex-config.ts
1213
- var NOTIFY_LINE = /^[ \t]*notify[ \t]*=[ \t]*(\[.*\])[ \t]*(?:#.*)?$/m;
1214
- var NOTIFY_KEY = /^[ \t]*notify[ \t]*=/m;
1215
- var OURS2 = /(?:hook\.js|sessionpipe-hook)",\s*"codex",\s*"notify"/;
1216
- function scan(line, st) {
1217
- let i = 0;
1218
- while (i < line.length) {
1219
- if (st.str) {
1220
- const j = line.indexOf(st.str, i);
1221
- if (j < 0) return;
1222
- i = j + 3;
1223
- st.str = null;
1224
- continue;
1225
- }
1226
- const ch = line[i];
1227
- if (ch === "#") return;
1228
- if (line.startsWith('"""', i) || line.startsWith("'''", i)) {
1229
- st.str = line.slice(i, i + 3);
1230
- i += 3;
1231
- continue;
1232
- }
1233
- if (ch === '"') {
1234
- i++;
1235
- while (i < line.length && line[i] !== '"') i += line[i] === "\\" ? 2 : 1;
1236
- i++;
1237
- continue;
1238
- }
1239
- if (ch === "'") {
1240
- const j = line.indexOf("'", i + 1);
1241
- i = j < 0 ? line.length : j + 1;
1242
- continue;
1243
- }
1244
- if (ch === "[") st.depth++;
1245
- else if (ch === "]") st.depth = Math.max(0, st.depth - 1);
1246
- i++;
1247
- }
1248
- }
1249
- function firstTableAt(text) {
1250
- const st = { depth: 0, str: null };
1251
- let pos = 0;
1252
- for (const line of text.split("\n")) {
1253
- if (!st.str && st.depth === 0 && /^[ \t]*\[/.test(line)) return pos;
1254
- scan(line, st);
1255
- 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);
1256
1528
  }
1257
- return -1;
1258
- }
1259
- function split(text) {
1260
- const at = firstTableAt(text);
1261
- return at < 0 ? { head: text, tail: "", at: text.length } : { head: text.slice(0, at), tail: text.slice(at), at };
1262
- }
1263
- 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 = [];
1264
1538
  try {
1265
- const a = JSON.parse(raw);
1266
- 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();
1267
1540
  } catch {
1268
- return null;
1269
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;
1270
1547
  }
1271
- function readNotify(text) {
1272
- const { head, tail, at } = split(text);
1273
- const m = NOTIFY_LINE.exec(head);
1274
- if (m)
1275
- return {
1276
- raw: m[1] ?? "",
1277
- argv: parseArgv(m[1] ?? ""),
1278
- index: m.index,
1279
- length: m[0].length,
1280
- misplaced: false,
1281
- other: !OURS2.test(m[1] ?? "")
1282
- };
1283
- const k = NOTIFY_KEY.exec(head);
1284
- if (k) {
1285
- const rest = head.slice(k.index).split("\n")[0] ?? "";
1286
- return {
1287
- raw: rest.replace(NOTIFY_KEY, "").trim(),
1288
- argv: null,
1289
- index: k.index,
1290
- length: 0,
1291
- misplaced: false,
1292
- other: true
1293
- };
1294
- }
1295
- const t = NOTIFY_LINE.exec(tail);
1296
- if (t && OURS2.test(t[1] ?? ""))
1297
- return {
1298
- raw: t[1] ?? "",
1299
- argv: parseArgv(t[1] ?? ""),
1300
- index: at + t.index,
1301
- length: t[0].length,
1302
- misplaced: true,
1303
- other: false
1304
- };
1305
- return null;
1306
- }
1307
- function dropLine(text, index, length) {
1308
- return text.slice(0, index) + text.slice(index + length).replace(/^\r?\n/, "");
1309
- }
1310
- function insertTopLevel(text, line) {
1311
- const { head, tail } = split(text);
1312
- if (!tail) return (head.trim() ? head.replace(/\s*$/, "\n") : "") + line + "\n";
1313
- return `${(head.trim() ? head.replace(/\s*$/, "\n") : "") + line}
1314
-
1315
- ${tail}`;
1316
- }
1317
- function withNotify(text, line) {
1318
- const n = readNotify(text);
1319
- if (n && !n.misplaced && !n.other) return text.slice(0, n.index) + line + text.slice(n.index + n.length);
1320
- if (n?.misplaced) return insertTopLevel(dropLine(text, n.index, n.length), line);
1321
- return insertTopLevel(text, line);
1322
- }
1323
- function replaceNotify(text, line) {
1324
- const n = readNotify(text);
1325
- if (!n || n.misplaced || !n.length) return null;
1326
- return text.slice(0, n.index) + line + text.slice(n.index + n.length);
1327
- }
1328
- function withoutNotify(text) {
1329
- const n = readNotify(text);
1330
- if (!n || n.other || !n.length) return text;
1331
- 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(" ");
1332
1557
  }
1333
- var PREVIOUS = "--previous-notify";
1334
- function previousNotify(argv) {
1335
- if (!Array.isArray(argv)) return null;
1336
- const i = argv.indexOf(PREVIOUS);
1337
- 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
+ };
1338
1568
  }
1339
- function stripPreviousRaw(raw) {
1340
- 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 };
1341
1584
  }
1342
- function withPreviousRaw(raw, prev) {
1343
- const base = stripPreviousRaw(raw).replace(/\s*\]\s*$/, "");
1344
- 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;
1345
1602
  }
1346
- var isComputerUse = (argv) => Array.isArray(argv) && typeof argv[0] === "string" && /(^|\/)SkyComputerUseClient$/.test(argv[0]);
1347
-
1348
- // src/adapters/codex.ts
1349
- var NAME3 = "codex";
1350
- var CODEX_EVENTS = [
1351
- "SessionStart",
1352
- "UserPromptSubmit",
1353
- "PreToolUse",
1354
- "PostToolUse",
1355
- "PermissionRequest",
1356
- "Stop",
1357
- "Interrupt",
1358
- "SubagentStart",
1359
- "SubagentStop",
1360
- "PreCompact",
1361
- "PostCompact",
1362
- "SessionEnd"
1363
- ];
1364
- var OPTS2 = { matcherFor: (e) => /Tool|Permission/.test(e) ? "" : void 0, timeout: 5 };
1365
- var eventsFor2 = (lean) => lean ? CODEX_EVENTS.filter((e) => e !== "PreToolUse") : [...CODEX_EVENTS];
1366
- var codexHome = (env) => env.CODEX_HOME || path6.join(HOME, ".codex");
1367
- var hooksFile2 = (env) => path6.join(codexHome(env), "hooks.json");
1368
- var configFile2 = (env) => path6.join(codexHome(env), "config.toml");
1369
- var sessionsDir = (env) => path6.join(codexHome(env), "sessions");
1370
- var validTool3 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
1371
- var endReason2 = (r) => r === "clear" || r === "logout" || r === "exit" ? r : "other";
1372
- function installNotify(cmd, env) {
1373
- const file = configFile2(env);
1374
- let text = "";
1603
+ function parseAuthStatus(stdout) {
1604
+ const text = String(stdout || "").trim();
1605
+ const start = text.indexOf("{");
1606
+ if (start < 0) return { loggedIn: null };
1375
1607
  try {
1376
- text = readFileSync4(file, "utf8");
1377
- } catch {
1378
- }
1379
- const argv = cmd([NAME3, "notify"]).argv;
1380
- const line = `notify = ${JSON.stringify(argv)}`;
1381
- const n = readNotify(text);
1382
- const write = (next) => {
1383
- mkdirSync3(path6.dirname(file), { recursive: true });
1384
- writeFileSync3(file, next);
1385
- };
1386
- if (n && !n.other) {
1387
- if (!n.misplaced && JSON.stringify(n.argv) === JSON.stringify(argv))
1388
- return { file, changed: false, note: "notify current" };
1389
- write(withNotify(text, line));
1390
- return {
1391
- file,
1392
- changed: true,
1393
- note: n.misplaced ? "notify moved to the top level (it sat inside a [table], where Codex never reads it)" : "notify updated"
1394
- };
1395
- }
1396
- if (n?.other) {
1397
- const prev = previousNotify(n.argv);
1398
- if (isComputerUse(n.argv)) {
1399
- if (prev && OURS2.test(JSON.stringify(prev))) {
1400
- if (JSON.stringify(prev) === JSON.stringify(argv))
1401
- return { file, changed: false, note: "notify current (after Codex Computer Use)" };
1402
- }
1403
- if (!prev || OURS2.test(JSON.stringify(prev))) {
1404
- const next = replaceNotify(text, `notify = ${withPreviousRaw(n.raw, argv)}`);
1405
- if (next) {
1406
- write(next);
1407
- return { file, changed: true, note: "chained after Codex Computer Use (--previous-notify)" };
1408
- }
1409
- }
1410
- }
1608
+ const j = JSON.parse(text.slice(start));
1609
+ if (typeof j.loggedIn !== "boolean") return { loggedIn: null };
1411
1610
  return {
1412
- file,
1413
- changed: false,
1414
- skipped: true,
1415
- 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 } : {}
1416
1614
  };
1615
+ } catch {
1616
+ return { loggedIn: null };
1417
1617
  }
1418
- write(withNotify(text, line));
1419
- return { file, changed: true, note: "notify set at the top level" };
1420
1618
  }
1421
- function uninstallNotify(env) {
1422
- const file = configFile2(env);
1423
- 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 = "";
1424
1644
  try {
1425
- text = readFileSync4(file, "utf8");
1426
- } catch {
1427
- return { file, changed: false };
1428
- }
1429
- const n = readNotify(text);
1430
- if (!n) return { file, changed: false };
1431
- if (!n.other) {
1432
- writeFileSync3(file, withoutNotify(text));
1433
- return { file, changed: true };
1434
- }
1435
- const prev = previousNotify(n.argv);
1436
- if (isComputerUse(n.argv) && prev && OURS2.test(JSON.stringify(prev))) {
1437
- const next = replaceNotify(text, `notify = ${stripPreviousRaw(n.raw)}`);
1438
- if (next) writeFileSync3(file, next);
1439
- 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 || "");
1440
1654
  }
1441
- return { file, changed: false };
1655
+ return parseCaps(help);
1442
1656
  }
1443
- function findRollout(threadId, env) {
1444
- let best = null;
1445
- const walk = (dir, depth) => {
1446
- let names = [];
1657
+ function findTranscript(session, dirs) {
1658
+ if (!UUID_RE.test(session)) return null;
1659
+ for (const dir of dirs) {
1660
+ let projects = [];
1447
1661
  try {
1448
- names = readdirSync4(dir);
1662
+ projects = readdirSync4(path8.join(dir, "projects"), { withFileTypes: true });
1449
1663
  } catch {
1450
- return;
1664
+ continue;
1451
1665
  }
1452
- for (const n of names.sort().reverse()) {
1453
- const p = path6.join(dir, n);
1454
- if (n.endsWith(".jsonl") && n.includes(threadId)) {
1455
- best = p;
1456
- return;
1457
- }
1458
- if (depth < 3) {
1459
- walk(p, depth + 1);
1460
- 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 {
1461
1672
  }
1462
1673
  }
1463
- };
1464
- walk(sessionsDir(env), 0);
1465
- return best;
1674
+ }
1675
+ return null;
1466
1676
  }
1467
- var codex = {
1468
- name: NAME3,
1469
- events: CODEX_EVENTS,
1470
- detect: (env = process.env) => existsSync4(codexHome(env)),
1471
- configFiles: (env = process.env) => [hooksFile2(env), configFile2(env)],
1472
- install: (cmd, env = process.env, opts = {}) => [
1473
- installClaudeShaped(hooksFile2(env), NAME3, eventsFor2(opts.lean), cmd, OPTS2),
1474
- installNotify(cmd, env)
1475
- ],
1476
- uninstall: (env = process.env, opts = {}) => [
1477
- uninstallClaudeShaped(hooksFile2(env), CODEX_EVENTS, { created: opts.created?.includes(hooksFile2(env)) }),
1478
- uninstallNotify(env)
1479
- ],
1480
- installed: (cmd, env = process.env, opts = {}) => {
1481
- const out = [
1482
- {
1483
- file: hooksFile2(env),
1484
- state: installedClaudeShaped(hooksFile2(env), NAME3, eventsFor2(opts.lean), cmd, OPTS2),
1485
- note: "Codex runs non-managed hooks only after the review in `codex` (/hooks); until then nothing is reported unless the notify fallback is ours"
1486
- }
1487
- ];
1488
- 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;
1489
1685
  try {
1490
- text = readFileSync4(configFile2(env), "utf8");
1686
+ j = JSON.parse(line.toString("utf8"));
1491
1687
  } catch {
1688
+ return null;
1492
1689
  }
1493
- const n = readNotify(text);
1494
- const argv = cmd([NAME3, "notify"]).argv;
1495
- const prev = n ? previousNotify(n.argv) : null;
1496
- const ours = n && (!n.other || prev && JSON.stringify(prev) === JSON.stringify(argv));
1497
- out.push({
1498
- file: configFile2(env),
1499
- 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"
1500
- });
1501
- return out;
1502
- },
1503
- fromHook(input) {
1504
- const event = input.argv[1] ?? "";
1505
- let s = {};
1506
- if (event === "notify") {
1507
- 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)
1508
1732
  try {
1509
- s = raw ? JSON.parse(raw) : {};
1733
+ closeSync2(fd);
1510
1734
  } catch {
1511
1735
  }
1512
- const id2 = s["thread-id"] ?? s.thread_id ?? s["session-id"] ?? s.session_id;
1513
- if (!id2) return null;
1514
- const r2 = {
1515
- session: { id: String(id2), cwd: typeof s.cwd === "string" ? s.cwd : input.cwd },
1516
- events: [
1517
- {
1518
- type: "turn.ended",
1519
- data: { reason: "stop", ...typeof s["turn-id"] === "string" ? { turn_id: s["turn-id"] } : {} },
1520
- harnessEvent: "notify"
1521
- }
1522
- ],
1523
- hints: {}
1524
- };
1525
- const file = findRollout(String(id2), input.env);
1526
- if (file) r2.transcript = file;
1527
- return r2;
1528
- }
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) {
1529
1756
  try {
1530
- 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;
1531
1760
  } catch {
1532
1761
  }
1533
- const id = s.session_id;
1534
- if (!id) return null;
1535
- const session = { id: String(id), cwd: typeof s.cwd === "string" ? s.cwd : input.cwd };
1536
- if (typeof s.model === "string") session.model = s.model;
1537
- const turn_id = typeof s.turn_id === "string" ? s.turn_id : void 0;
1538
- const tool = validTool3(s.tool_name) ? s.tool_name : void 0;
1539
- const call_id = typeof s.tool_use_id === "string" ? s.tool_use_id : void 0;
1540
- const ev = (type, data) => ({ type, data: prune3(data), harnessEvent: event });
1541
- const events = [];
1542
- switch (event) {
1543
- case "SessionStart":
1544
- events.push(
1545
- ev("session.started", {
1546
- source: typeof s.source === "string" && ["startup", "resume", "clear", "compact", "fork"].includes(s.source) ? s.source : "unknown"
1547
- })
1548
- );
1549
- break;
1550
- case "UserPromptSubmit":
1551
- events.push(
1552
- ev("turn.started", {
1553
- turn_id,
1554
- prompt_chars: typeof s.prompt === "string" ? [...s.prompt].length : void 0
1555
- })
1556
- );
1557
- break;
1558
- case "PreToolUse":
1559
- if (tool) events.push(ev("tool.started", { tool, call_id, turn_id, input: s.tool_input }));
1560
- break;
1561
- case "PostToolUse":
1562
- if (tool)
1563
- events.push(
1564
- ev("tool.ended", { tool, call_id, turn_id, ok: true, input: s.tool_input, output: s.tool_response })
1565
- );
1566
- break;
1567
- case "PermissionRequest":
1568
- events.push(
1569
- ev("attention.needed", { attention_id: call_id ?? `perm-${Date.now()}`, kind: "permission", tool })
1570
- );
1571
- break;
1572
- case "Stop":
1573
- events.push(ev("turn.ended", { turn_id, reason: "stop" }));
1574
- break;
1575
- case "Interrupt":
1576
- events.push(ev("turn.ended", { turn_id, reason: "interrupt" }));
1577
- break;
1578
- case "SubagentStart":
1579
- events.push(
1580
- ev("subagent.started", {
1581
- agent_id: String(s.agent_id ?? s.subagent_id ?? ""),
1582
- agent_type: typeof s.agent_type === "string" ? s.agent_type : void 0
1583
- })
1584
- );
1585
- break;
1586
- case "SubagentStop":
1587
- events.push(
1588
- ev("subagent.ended", {
1589
- agent_id: String(s.agent_id ?? s.subagent_id ?? ""),
1590
- agent_type: typeof s.agent_type === "string" ? s.agent_type : void 0
1591
- })
1592
- );
1593
- break;
1594
- case "PreCompact":
1595
- events.push(ev("context.compacted", { trigger: s.trigger === "manual" ? "manual" : "auto" }));
1596
- break;
1597
- case "PostCompact":
1598
- break;
1599
- case "SessionEnd":
1600
- events.push(ev("session.ended", { reason: endReason2(s.reason) }));
1601
- break;
1602
- default:
1603
- 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;
1604
1785
  }
1605
- const r = { session, events, hints: {} };
1606
- if (typeof s.transcript_path === "string") r.transcript = s.transcript_path;
1607
- return r;
1608
- },
1609
- readTranscript(file, fromLine) {
1610
- 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--) {
1611
1794
  try {
1612
- text = readFileSync4(file, "utf8");
1795
+ const j = pick(JSON.parse(lines[i]));
1796
+ if (j && (j.type === "result" || "result" in j)) return j;
1613
1797
  } catch {
1614
- return { turns: [], line: fromLine };
1615
1798
  }
1616
- return readCodex(text.split("\n"), fromLine);
1617
- },
1618
- facts(session, transcript) {
1619
- const out = {};
1620
- if (transcript) Object.assign(out, codexFacts(transcript));
1621
- return out;
1622
- },
1623
- backfill(sinceMs, env = process.env) {
1624
- const rows = [];
1625
- for (const f of recentFiles(sessionsDir(env), 3, sinceMs, (n) => /^rollout-.*\.jsonl$/.test(n))) {
1626
- const m = /([0-9a-f]{8}-[0-9a-f-]{27,})\.jsonl$/i.exec(path6.basename(f));
1627
- if (!m?.[1]) continue;
1628
- const facts = codexFacts(f);
1629
- if (!facts.started_at || !facts.last_at) continue;
1630
- const { started_at, last_at, first_ask: _fa, harness_version: _hv, ...rest } = facts;
1631
- 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;
1632
1821
  }
1633
- 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++;
1634
1843
  }
1635
- };
1636
- function prune3(o) {
1637
- for (const k of Object.keys(o)) if (o[k] === void 0) delete o[k];
1638
- return o;
1639
1844
  }
1640
- function codexFacts(file) {
1641
- const { head, tail } = ends(file, 128 * 1024, 64 * 1024);
1642
- const H = parseLines(head);
1643
- const T = parseLines(tail);
1644
- const out = {};
1645
- const meta = H.find((j) => j.type === "session_meta" && j.payload);
1646
- if (meta?.payload) {
1647
- const p = meta.payload;
1648
- if (p.cwd) out.cwd = p.cwd;
1649
- if (p.cli_version) out.harness_version = String(p.cli_version);
1650
- if (p.git?.branch) out.branch = p.git.branch;
1651
- 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;
1652
1852
  }
1653
- const ctx = H.concat(T).reverse().find((j) => j.type === "turn_context" && j.payload?.model);
1654
- if (ctx?.payload?.model) out.model = ctx.payload.model;
1655
- for (const j of H) {
1656
- const p = j.payload;
1657
- if (j.type !== "response_item" || !p || p.type !== "message" || p.role !== "user") continue;
1658
- const text = (p.content ?? []).filter((b) => b && b.type === "input_text" && typeof b.text === "string").map((b) => b.text).join("\n").trim();
1659
- if (!text || /^<[a-z_-]+>/i.test(text)) continue;
1660
- const line = text.split("\n").find((l) => l.trim()) ?? "";
1661
- out.title = line.length > 80 ? `${line.slice(0, 80).replace(/\s+\S*$/, "")}\u2026` : line;
1662
- out.title_source = "first-ask";
1663
- 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;
1664
1865
  }
1665
- const times = H.concat(T).map((j) => Date.parse(String(j.timestamp ?? ""))).filter((n) => n > 0);
1666
- if (times.length) {
1667
- out.started_at = Math.min(...times);
1668
- 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
+ };
1669
1890
  }
1670
- 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/, "");
1671
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}
1672
1910
 
1673
- // src/adapters/gemini-cli.ts
1674
- import { execFileSync as execFileSync3 } from "node:child_process";
1675
- import { existsSync as existsSync5, readFileSync as readFileSync5 } from "node:fs";
1676
- import path7 from "node:path";
1677
- var NAME4 = "gemini-cli";
1678
- 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 = [
1679
1947
  "SessionStart",
1680
- "BeforeAgent",
1681
- "BeforeTool",
1682
- "AfterTool",
1683
- "Notification",
1684
- "AfterAgent",
1685
- "PreCompress",
1948
+ "UserPromptSubmit",
1949
+ "PreToolUse",
1950
+ "PostToolUse",
1951
+ "PermissionRequest",
1952
+ "Stop",
1953
+ "Interrupt",
1954
+ "SubagentStart",
1955
+ "SubagentStop",
1956
+ "PreCompact",
1957
+ "PostCompact",
1686
1958
  "SessionEnd"
1687
1959
  ];
1688
- var OPTS3 = { matcherFor: (e) => /Tool/.test(e) ? ".*" : void 0, timeout: 5e3 };
1689
- var geminiDir = (env) => env.GEMINI_CLI_HOME || path7.join(HOME, ".gemini");
1690
- var settings = (env) => path7.join(geminiDir(env), "settings.json");
1691
- var validTool4 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
1692
- var eventsFor3 = (lean) => lean ? GEMINI_EVENTS.filter((e) => e !== "BeforeTool") : [...GEMINI_EVENTS];
1693
- function geminiHooksActive(settingsFile) {
1694
- 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 = "";
1695
1971
  try {
1696
- tools = JSON.parse(readFileSync5(settingsFile, "utf8")).tools;
1972
+ text = readFileSync5(file, "utf8");
1697
1973
  } catch {
1698
1974
  }
1699
- if (tools?.enableHooks === true) return { active: true };
1700
- if (tools?.enableHooks === false)
1701
- return { active: false, note: "inactive: tools.enableHooks is false in settings.json" };
1702
- let version = "";
1703
- try {
1704
- version = execFileSync3("gemini", ["--version"], {
1705
- encoding: "utf8",
1706
- timeout: 4e3,
1707
- stdio: ["ignore", "pipe", "ignore"]
1708
- }).trim();
1709
- } 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));
1710
1986
  return {
1711
- active: true,
1712
- 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"
1713
1990
  };
1714
1991
  }
1715
- const m = /(\d+)\.(\d+)/.exec(version);
1716
- if (m && (Number(m[1]) > 0 || Number(m[2]) >= 46)) return { active: true };
1717
- return {
1718
- active: false,
1719
- note: `inactive: Gemini CLI ${version || "?"} runs hooks only with "tools": {"enableHooks": true} in settings.json`
1720
- };
1721
- }
1722
- var geminiCli = {
1723
- name: NAME4,
1724
- events: GEMINI_EVENTS,
1725
- // ~/.gemini also belongs to Antigravity; Gemini CLI is here when its settings or its chat logs exist.
1726
- detect: (env = process.env) => existsSync5(settings(env)) || existsSync5(path7.join(geminiDir(env), "tmp")),
1727
- configFiles: (env = process.env) => [settings(env)],
1728
- install: (cmd, env = process.env, opts = {}) => {
1729
- const r = installClaudeShaped(settings(env), NAME4, eventsFor3(opts.lean), cmd, OPTS3);
1730
- const a = geminiHooksActive(settings(env));
1731
- return [a.note && !r.skipped ? { ...r, note: [r.note, a.note].filter(Boolean).join("; ") } : r];
1732
- },
1733
- uninstall: (env = process.env, opts = {}) => [
1734
- uninstallClaudeShaped(settings(env), GEMINI_EVENTS, { created: opts.created?.includes(settings(env)) })
1735
- ],
1736
- installed: (cmd, env = process.env, opts = {}) => {
1737
- const state = installedClaudeShaped(settings(env), NAME4, eventsFor3(opts.lean), cmd, OPTS3);
1738
- const a = geminiHooksActive(settings(env));
1739
- return [
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)
2076
+ ],
2077
+ installed: (cmd, env = process.env, opts = {}) => {
2078
+ const out = [
1740
2079
  {
1741
- file: settings(env),
1742
- state: state === "current" && !a.active ? "inactive" : state,
1743
- ...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"
1744
2083
  }
1745
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;
1746
2099
  },
1747
2100
  fromHook(input) {
1748
2101
  const event = input.argv[1] ?? "";
1749
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
+ }
1750
2126
  try {
1751
2127
  s = input.stdin ? JSON.parse(input.stdin) : {};
1752
2128
  } catch {
@@ -1754,62 +2130,72 @@ var geminiCli = {
1754
2130
  const id = s.session_id;
1755
2131
  if (!id) return null;
1756
2132
  const session = { id: String(id), cwd: typeof s.cwd === "string" ? s.cwd : input.cwd };
1757
- const tool = validTool4(s.tool_name) ? s.tool_name : void 0;
1758
- 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 });
1759
2138
  const events = [];
1760
2139
  switch (event) {
1761
2140
  case "SessionStart":
1762
2141
  events.push(
1763
2142
  ev("session.started", {
1764
- 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"
1765
2144
  })
1766
2145
  );
1767
2146
  break;
1768
- case "BeforeAgent":
2147
+ case "UserPromptSubmit":
1769
2148
  events.push(
1770
- 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
+ })
1771
2153
  );
1772
2154
  break;
1773
- case "BeforeTool":
1774
- 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 }));
1775
2157
  break;
1776
- case "AfterTool": {
1777
- const resp = s.tool_response;
1778
- 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":
1779
2159
  if (tool)
1780
2160
  events.push(
1781
- ev("tool.ended", {
1782
- tool,
1783
- ok: !err,
1784
- error: err,
1785
- input: s.tool_input,
1786
- output: resp?.llmContent ?? s.tool_response
1787
- })
2161
+ ev("tool.ended", { tool, call_id, turn_id, ok: true, input: s.tool_input, output: s.tool_response })
1788
2162
  );
1789
2163
  break;
1790
- }
1791
- case "Notification":
2164
+ case "PermissionRequest":
1792
2165
  events.push(
1793
- ev("attention.needed", {
1794
- attention_id: `notif-${Date.now()}`,
1795
- kind: s.notification_type === "ToolPermission" ? "permission" : "question",
1796
- message: typeof s.message === "string" ? s.message.slice(0, 200) : void 0
1797
- })
2166
+ ev("attention.needed", { attention_id: call_id ?? `perm-${Date.now()}`, kind: "permission", tool })
1798
2167
  );
1799
2168
  break;
1800
- case "AfterAgent":
1801
- events.push(ev("turn.ended", { reason: "stop" }));
2169
+ case "Stop":
2170
+ events.push(ev("turn.ended", { turn_id, reason: "stop" }));
1802
2171
  break;
1803
- case "PreCompress":
1804
- events.push(ev("context.compacted", { trigger: s.trigger === "manual" ? "manual" : "auto" }));
2172
+ case "Interrupt":
2173
+ events.push(ev("turn.ended", { turn_id, reason: "interrupt" }));
1805
2174
  break;
1806
- case "SessionEnd":
2175
+ case "SubagentStart":
1807
2176
  events.push(
1808
- ev("session.ended", {
1809
- 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
1810
2188
  })
1811
2189
  );
1812
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;
1813
2199
  default:
1814
2200
  return null;
1815
2201
  }
@@ -1824,469 +2210,396 @@ var geminiCli = {
1824
2210
  } catch {
1825
2211
  return { turns: [], line: fromLine };
1826
2212
  }
1827
- return readGemini(text.split("\n"), fromLine);
2213
+ return readCodex(text.split("\n"), fromLine);
1828
2214
  },
1829
- facts(_session, transcript) {
2215
+ facts(session, transcript) {
1830
2216
  const out = {};
1831
- if (!transcript) return out;
1832
- const { head, tail } = ends(transcript, 64 * 1024, 64 * 1024);
1833
- const H = parseLines(head);
1834
- const T = parseLines(tail);
1835
- const first = H.find((j) => j.type === "user");
1836
- const raw = first ? first.content ?? first.message ?? first.text : void 0;
1837
- const text = typeof raw === "string" ? raw.trim() : "";
1838
- if (text) {
1839
- const line = text.split("\n").find((l) => l.trim()) ?? "";
1840
- out.title = line.length > 80 ? `${line.slice(0, 80).replace(/\s+\S*$/, "")}\u2026` : line;
1841
- out.title_source = "first-ask";
1842
- }
1843
- const times = H.concat(T).map((j) => Date.parse(String(j.timestamp ?? ""))).filter((n) => n > 0);
1844
- if (times.length) {
1845
- out.started_at = Math.min(...times);
1846
- out.last_at = Math.max(...times);
1847
- }
2217
+ if (transcript) Object.assign(out, codexFacts(transcript));
1848
2218
  return out;
1849
2219
  },
1850
- backfill: () => []
1851
- // chat logs are keyed by a project hash, not a session id we can name; the first hook of a resumed session reports it
1852
- };
1853
- function prune4(o) {
1854
- for (const k of Object.keys(o)) if (o[k] === void 0) delete o[k];
1855
- return o;
1856
- }
1857
-
1858
- // src/adapters/claude-code-control.ts
1859
- var claude_code_control_exports = {};
1860
- __export(claude_code_control_exports, {
1861
- JOB_TIMEOUT_MS: () => JOB_TIMEOUT_MS,
1862
- claudeEnv: () => claudeEnv,
1863
- claudeLogin: () => claudeLogin,
1864
- detectCaps: () => detectCaps,
1865
- findClaude: () => findClaude,
1866
- findTranscript: () => findTranscript,
1867
- isUuid: () => isUuid,
1868
- liveProcess: () => liveProcess,
1869
- loginCommand: () => loginCommand,
1870
- loginHint: () => loginHint,
1871
- modeFlags: () => modeFlags,
1872
- parseAuthStatus: () => parseAuthStatus,
1873
- parseCaps: () => parseCaps,
1874
- parseResult: () => parseResult,
1875
- recentlyWritten: () => recentlyWritten,
1876
- runClaude: () => runClaude,
1877
- sessionFolder: () => sessionFolder
1878
- });
1879
- import { execFile, execFileSync as execFileSync4, spawn } from "node:child_process";
1880
- import { accessSync, closeSync as closeSync2, constants, openSync as openSync2, readdirSync as readdirSync5, readFileSync as readFileSync6, readSync as readSync2, statSync as statSync4 } from "node:fs";
1881
- import os2 from "node:os";
1882
- import path8 from "node:path";
1883
- var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1884
- var JOB_TIMEOUT_MS = 30 * 6e4;
1885
- var OUT_CAP = 1024 * 1024;
1886
- var LIVE_WRITE_MS = 9e4;
1887
- function isExe(p) {
1888
- try {
1889
- if (!statSync4(p).isFile()) return false;
1890
- accessSync(p, constants.X_OK);
1891
- return true;
1892
- } catch {
1893
- return false;
1894
- }
1895
- }
1896
- var cmpVersion = (a, b) => {
1897
- const pa = a.split(".").map(Number);
1898
- const pb = b.split(".").map(Number);
1899
- for (let i = 0; i < Math.max(pa.length, pb.length); i++)
1900
- if ((pa[i] ?? 0) !== (pb[i] ?? 0)) return (pa[i] ?? 0) - (pb[i] ?? 0);
1901
- return 0;
1902
- };
1903
- function findClaude(env = process.env, home = os2.homedir()) {
1904
- if (env.SESSIONPIPE_CLAUDE && isExe(env.SESSIONPIPE_CLAUDE)) return env.SESSIONPIPE_CLAUDE;
1905
- const names = process.platform === "win32" ? ["claude.exe", "claude.cmd"] : ["claude"];
1906
- for (const d of String(env.PATH || "").split(path8.delimiter)) {
1907
- if (!d || !path8.isAbsolute(d)) continue;
1908
- for (const n of names) if (isExe(path8.join(d, n))) return path8.join(d, n);
1909
- }
1910
- for (const p of [
1911
- path8.join(home, ".claude", "local", "claude"),
1912
- path8.join(home, ".local", "bin", "claude"),
1913
- "/opt/homebrew/bin/claude",
1914
- "/usr/local/bin/claude"
1915
- ])
1916
- if (isExe(p)) return p;
1917
- const app = path8.join(home, "Library", "Application Support", "Claude", "claude-code");
1918
- let versions = [];
1919
- try {
1920
- versions = readdirSync5(app).filter((n) => /^\d+(\.\d+)*$/.test(n)).sort(cmpVersion).reverse();
1921
- } catch {
1922
- }
1923
- for (const v of versions) {
1924
- const p = path8.join(app, v, "claude.app", "Contents", "MacOS", "claude");
1925
- if (isExe(p)) return p;
1926
- }
1927
- return null;
1928
- }
1929
- function optionBlock(help, flag) {
1930
- const lines = help.split("\n");
1931
- const re = new RegExp(`^\\s{0,4}(?:-\\w, )?${flag.replace(/-/g, "\\-")}(?![\\w-])`);
1932
- const start = lines.findIndex((l) => re.test(l));
1933
- if (start < 0) return null;
1934
- const out = [lines[start]];
1935
- for (let i = start + 1; i < lines.length && lines[i]?.trim() && !/^\s{0,4}-/.test(lines[i]); i++)
1936
- out.push(lines[i]);
1937
- return out.join(" ");
1938
- }
1939
- function parseCaps(help) {
1940
- const pm = optionBlock(help, "--permission-mode");
1941
- const pp = optionBlock(help, "--permission-prompts");
1942
- return {
1943
- hasMode: !!pm,
1944
- modes: pm ? [...pm.matchAll(/"([A-Za-z]+)"/g)].map((m) => m[1]) : [],
1945
- promptsNone: !!pp && /"none"/.test(pp),
1946
- fork: !!optionBlock(help, "--fork-session"),
1947
- stream: !!optionBlock(help, "--include-partial-messages")
1948
- };
1949
- }
1950
- function modeFlags(mode, caps) {
1951
- if (mode === "auto") {
1952
- if (!caps.modes.includes("auto"))
1953
- return { error: "this machine is in auto mode, but its Claude Code has no auto permission mode" };
1954
- return { args: ["--permission-mode", "auto"] };
1955
- }
1956
- if (caps.modes.includes("dontAsk")) return { args: ["--permission-mode", "dontAsk"] };
1957
- const args = [];
1958
- if (caps.hasMode)
1959
- args.push(
1960
- "--permission-mode",
1961
- caps.modes.includes("manual") && !caps.modes.includes("default") ? "manual" : "default"
1962
- );
1963
- if (caps.promptsNone) args.push("--permission-prompts", "none");
1964
- return { args };
1965
- }
1966
- function claudeEnv(configDir, env = process.env) {
1967
- const e = { ...env };
1968
- for (const k of [
1969
- "CLAUDECODE",
1970
- "CLAUDE_CODE_SESSION_ID",
1971
- "CLAUDE_CODE_HOST_SESSION_ID",
1972
- "CLAUDE_CODE_ENTRYPOINT",
1973
- "CLAUDE_CODE_SESSION_ATTENDED",
1974
- "CLAUDE_CODE_SSE_PORT"
1975
- ])
1976
- delete e[k];
1977
- e.SESSIONPIPE_CONTROL_JOB = "1";
1978
- if (configDir !== void 0) {
1979
- if (path8.resolve(configDir) === path8.resolve(os2.homedir(), ".claude")) delete e.CLAUDE_CONFIG_DIR;
1980
- else e.CLAUDE_CONFIG_DIR = configDir;
1981
- }
1982
- return e;
1983
- }
1984
- function parseAuthStatus(stdout) {
1985
- const text = String(stdout || "").trim();
1986
- const start = text.indexOf("{");
1987
- if (start < 0) return { loggedIn: null };
1988
- try {
1989
- const j = JSON.parse(text.slice(start));
1990
- if (typeof j.loggedIn !== "boolean") return { loggedIn: null };
1991
- return {
1992
- loggedIn: j.loggedIn,
1993
- ...typeof j.authMethod === "string" ? { method: j.authMethod } : {},
1994
- ...typeof j.subscriptionType === "string" ? { subscription: j.subscriptionType } : {}
1995
- };
1996
- } catch {
1997
- return { loggedIn: null };
1998
- }
1999
- }
2000
- function claudeLogin(bin, configDir, env = process.env) {
2001
- return new Promise((resolve) => {
2002
- execFile(
2003
- bin,
2004
- ["auth", "status", "--json"],
2005
- { encoding: "utf8", timeout: 2e4, env: claudeEnv(configDir, env), maxBuffer: 256 * 1024 },
2006
- // `auth status` may exit non-zero when signed out; its JSON still says so.
2007
- (_err, stdout) => resolve(parseAuthStatus(stdout))
2008
- );
2009
- });
2010
- }
2011
- function loginCommand(configDir) {
2012
- const home = os2.homedir();
2013
- const isDefault = !configDir || path8.resolve(configDir) === path8.resolve(home, ".claude");
2014
- return isDefault ? "`claude auth login`" : `\`CLAUDE_CONFIG_DIR=${configDir.replace(home, "~")} claude auth login\``;
2015
- }
2016
- function loginHint(configDir, platform = process.platform) {
2017
- const shown = configDir ? configDir.replace(os2.homedir(), "~") : "~/.claude";
2018
- const login = loginCommand(configDir);
2019
- if (platform === "darwin")
2020
- 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.`;
2021
- return `Claude Code (${shown}) isn't signed in on this machine: run ${login} there.`;
2022
- }
2023
- function detectCaps(bin) {
2024
- let help = "";
2025
- try {
2026
- help = execFileSync4(bin, ["--help"], {
2027
- encoding: "utf8",
2028
- timeout: 3e4,
2029
- env: claudeEnv(),
2030
- stdio: ["ignore", "pipe", "pipe"],
2031
- maxBuffer: 4 * 1024 * 1024
2032
- });
2033
- } catch (e) {
2034
- help = String(e?.stdout || "");
2035
- }
2036
- return parseCaps(help);
2037
- }
2038
- function findTranscript(session, dirs) {
2039
- if (!UUID_RE.test(session)) return null;
2040
- for (const dir of dirs) {
2041
- let projects = [];
2042
- try {
2043
- projects = readdirSync5(path8.join(dir, "projects"), { withFileTypes: true });
2044
- } catch {
2045
- continue;
2046
- }
2047
- for (const p of projects) {
2048
- if (!p.isDirectory()) continue;
2049
- const file = path8.join(dir, "projects", p.name, `${session}.jsonl`);
2050
- try {
2051
- if (statSync4(file).isFile()) return { file, configDir: dir, project: p.name };
2052
- } catch {
2053
- }
2054
- }
2055
- }
2056
- return null;
2057
- }
2058
- var projectName = (cwd) => cwd.replace(/[^a-zA-Z0-9]/g, "-");
2059
- function sessionFolder(t, cap2 = 8 * 1024 * 1024) {
2060
- let first = null;
2061
- let count = 0;
2062
- let fd;
2063
- const look = (line) => {
2064
- if (line.indexOf('"cwd"') < 0) return null;
2065
- let j;
2066
- try {
2067
- j = JSON.parse(line.toString("utf8"));
2068
- } catch {
2069
- return null;
2070
- }
2071
- if (!j || typeof j.cwd !== "string" || !path8.isAbsolute(j.cwd)) return null;
2072
- if (projectName(j.cwd) === t.project) return j.cwd;
2073
- first ??= j.cwd;
2074
- count++;
2075
- return null;
2076
- };
2077
- try {
2078
- fd = openSync2(t.file, "r");
2079
- const buf = Buffer.alloc(256 * 1024);
2080
- let pos = 0;
2081
- let rest = Buffer.alloc(0);
2082
- let skipping = false;
2083
- while (pos < cap2 && count < 50) {
2084
- const n = readSync2(fd, buf, 0, buf.length, pos);
2085
- if (!n) {
2086
- if (!skipping && rest.length) {
2087
- const hit = look(rest);
2088
- if (hit) return hit;
2089
- }
2090
- break;
2091
- }
2092
- pos += n;
2093
- let chunk = rest.length ? Buffer.concat([rest, buf.subarray(0, n)]) : buf.subarray(0, n);
2094
- let nl = chunk.indexOf(10);
2095
- while (nl >= 0) {
2096
- const line = chunk.subarray(0, nl);
2097
- chunk = chunk.subarray(nl + 1);
2098
- if (skipping) skipping = false;
2099
- else {
2100
- const hit = look(line);
2101
- if (hit) return hit;
2102
- }
2103
- nl = chunk.indexOf(10);
2104
- }
2105
- if (chunk.length > 2 * 1024 * 1024) {
2106
- skipping = true;
2107
- rest = Buffer.alloc(0);
2108
- } else rest = skipping ? Buffer.alloc(0) : Buffer.from(chunk);
2109
- }
2110
- } catch {
2111
- } finally {
2112
- if (fd !== void 0)
2113
- try {
2114
- closeSync2(fd);
2115
- } catch {
2116
- }
2117
- }
2118
- return first;
2119
- }
2120
- function pidAlive(pid) {
2121
- if (!Number.isSafeInteger(pid) || pid <= 1) return false;
2122
- try {
2123
- process.kill(pid, 0);
2124
- return true;
2125
- } catch (e) {
2126
- return e.code === "EPERM";
2127
- }
2128
- }
2129
- function liveProcess(session, configDir) {
2130
- let names = [];
2131
- try {
2132
- names = readdirSync5(path8.join(configDir, "sessions")).filter((n) => /^\d+\.json$/.test(n));
2133
- } catch {
2134
- return false;
2135
- }
2136
- for (const n of names) {
2137
- try {
2138
- const j = JSON.parse(readFileSync6(path8.join(configDir, "sessions", n), "utf8"));
2139
- if (String(j.sessionId) !== session) continue;
2140
- if (pidAlive(Number(j.pid ?? n.slice(0, -5)))) return true;
2141
- } catch {
2142
- }
2143
- }
2144
- return false;
2145
- }
2146
- function recentlyWritten(file, ourLastEnd, now = Date.now()) {
2147
- let m;
2148
- try {
2149
- m = statSync4(file).mtimeMs;
2150
- } catch {
2151
- return false;
2152
- }
2153
- if (now - m > LIVE_WRITE_MS) return false;
2154
- return !(ourLastEnd && m <= ourLastEnd + 2e3);
2155
- }
2156
- function runClaude(bin, args, o) {
2157
- return new Promise((resolve) => {
2158
- let out = [];
2159
- let outLen = 0;
2160
- let tooBig = false;
2161
- let errTail = "";
2162
- let timedOut = false;
2163
- let settled = false;
2164
- let child;
2165
- try {
2166
- child = spawn(bin, args, {
2167
- cwd: o.cwd,
2168
- env: o.env,
2169
- stdio: ["ignore", "pipe", "pipe"],
2170
- detached: process.platform !== "win32",
2171
- windowsHide: true
2172
- });
2173
- } catch (e) {
2174
- resolve({ error: e.message });
2175
- return;
2176
- }
2177
- const kill = (sig) => {
2178
- try {
2179
- if (process.platform !== "win32" && child.pid) process.kill(-child.pid, sig);
2180
- else child.kill(sig);
2181
- } catch {
2182
- try {
2183
- child.kill(sig);
2184
- } catch {
2185
- }
2186
- }
2187
- };
2188
- let partial = "";
2189
- child.stdout?.on("data", (b) => {
2190
- if (o.onLine) {
2191
- const chunk = partial + b.toString("utf8");
2192
- const lines = chunk.split("\n");
2193
- partial = lines.pop() ?? "";
2194
- for (const line of lines) {
2195
- if (!line.trim()) continue;
2196
- try {
2197
- o.onLine(line);
2198
- } catch {
2199
- }
2200
- if (line.includes('"type":"result"')) {
2201
- out = [Buffer.from(line)];
2202
- outLen = line.length;
2203
- }
2204
- }
2205
- if (partial.length > OUT_CAP) partial = "";
2206
- return;
2207
- }
2208
- if (tooBig) return;
2209
- if (outLen + b.length > OUT_CAP) {
2210
- tooBig = true;
2211
- out = [];
2212
- return;
2213
- }
2214
- out.push(b);
2215
- outLen += b.length;
2216
- });
2217
- child.stderr?.on("data", (b) => {
2218
- errTail = (errTail + b.toString("utf8")).slice(-4e3);
2219
- });
2220
- const timer = setTimeout(() => {
2221
- timedOut = true;
2222
- kill("SIGTERM");
2223
- setTimeout(() => kill("SIGKILL"), 1e4).unref();
2224
- }, o.timeoutMs ?? JOB_TIMEOUT_MS);
2225
- const result = (code, signal) => ({
2226
- code,
2227
- signal,
2228
- stdout: tooBig ? null : Buffer.concat(out).toString("utf8"),
2229
- tooBig,
2230
- stderr: errTail,
2231
- timedOut
2232
- });
2233
- const finish = (r) => {
2234
- if (settled) return;
2235
- settled = true;
2236
- clearTimeout(timer);
2237
- resolve(r);
2238
- };
2239
- child.on("error", (e) => finish({ error: e.message }));
2240
- child.on("exit", (code, signal) => setTimeout(() => finish(result(code, signal)), 2e3).unref());
2241
- child.on("close", (code, signal) => {
2242
- if (o.onLine && partial.trim()) {
2243
- try {
2244
- o.onLine(partial);
2245
- } catch {
2246
- }
2247
- if (partial.includes('"type":"result"')) {
2248
- out = [Buffer.from(partial)];
2249
- outLen = partial.length;
2250
- }
2251
- 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 });
2252
2327
  }
2253
- finish(result(code, signal));
2254
- });
2328
+ );
2255
2329
  });
2256
2330
  }
2257
- function parseResult(stdout) {
2258
- const s = String(stdout || "").trim();
2259
- if (!s) return null;
2260
- const pick = (j) => {
2261
- if (Array.isArray(j)) {
2262
- 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) };
2263
2347
  return null;
2264
2348
  }
2265
- return j && typeof j === "object" ? j : null;
2266
- };
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;
2267
2426
  try {
2268
- return pick(JSON.parse(s));
2427
+ tools = JSON.parse(readFileSync6(settingsFile, "utf8")).tools;
2269
2428
  } catch {
2270
2429
  }
2271
- const lines = s.split("\n");
2272
- 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 = {};
2273
2481
  try {
2274
- const j = pick(JSON.parse(lines[i]));
2275
- if (j && (j.type === "result" || "result" in j)) return j;
2482
+ s = input.stdin ? JSON.parse(input.stdin) : {};
2276
2483
  } catch {
2277
2484
  }
2278
- }
2279
- 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;
2280
2587
  }
2281
- var isUuid = (s) => typeof s === "string" && UUID_RE.test(s);
2282
2588
 
2283
2589
  // src/adapters/index.ts
2284
2590
  var ADAPTERS = [claudeCode, codex, geminiCli, antigravity];
2285
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
+ }
2286
2599
 
2287
2600
  // src/config.ts
2288
2601
  import { chmodSync, existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
2289
- import path9 from "node:path";
2602
+ import path12 from "node:path";
2290
2603
  var DEFAULT_CONFIG = { sinks: [], harnesses: {}, update_check: true, keep_days: 30 };
2291
2604
  function readConfig(file = configFile()) {
2292
2605
  try {
@@ -2302,7 +2615,7 @@ function readConfig(file = configFile()) {
2302
2615
  }
2303
2616
  }
2304
2617
  function writeConfig(c, file = configFile()) {
2305
- mkdirSync4(path9.dirname(file), { recursive: true, mode: 448 });
2618
+ mkdirSync4(path12.dirname(file), { recursive: true, mode: 448 });
2306
2619
  writeFileSync4(file, `${JSON.stringify(c, null, 2)}
2307
2620
  `, { mode: 384 });
2308
2621
  if (process.platform !== "win32" && existsSync6(file)) {
@@ -2379,24 +2692,24 @@ import {
2379
2692
  readFileSync as readFileSync8,
2380
2693
  readSync as readSync3,
2381
2694
  renameSync,
2382
- statSync as statSync5,
2695
+ statSync as statSync6,
2383
2696
  unlinkSync as unlinkSync3,
2384
2697
  writeFileSync as writeFileSync5
2385
2698
  } from "node:fs";
2386
- import path10 from "node:path";
2699
+ import path13 from "node:path";
2387
2700
  var Outbox = class {
2388
2701
  constructor(root) {
2389
2702
  this.root = root;
2390
2703
  }
2391
2704
  root;
2392
2705
  dir(harness) {
2393
- return path10.join(this.root, "outbox", safeId(harness));
2706
+ return path13.join(this.root, "outbox", safeId(harness));
2394
2707
  }
2395
2708
  file(ref) {
2396
- return path10.join(this.dir(ref.harness), `${safeId(ref.session)}.jsonl`);
2709
+ return path13.join(this.dir(ref.harness), `${safeId(ref.session)}.jsonl`);
2397
2710
  }
2398
2711
  seqFile(ref) {
2399
- return path10.join(this.dir(ref.harness), `${safeId(ref.session)}.seq`);
2712
+ return path13.join(this.dir(ref.harness), `${safeId(ref.session)}.seq`);
2400
2713
  }
2401
2714
  /** The next seq for a session, persisted (survives a hibernating machine and a
2402
2715
  * restart; an in-memory counter would restart at 0 and collide). */
@@ -2408,7 +2721,7 @@ var Outbox = class {
2408
2721
  } catch {
2409
2722
  n = this.lastSeqInFile(ref) + 1;
2410
2723
  }
2411
- mkdirSync5(path10.dirname(f), { recursive: true, mode: 448 });
2724
+ mkdirSync5(path13.dirname(f), { recursive: true, mode: 448 });
2412
2725
  writeFileSync5(f, String(n + 1), { mode: 384 });
2413
2726
  return n;
2414
2727
  }
@@ -2434,10 +2747,10 @@ var Outbox = class {
2434
2747
  /** Every session file, newest first. */
2435
2748
  sessions() {
2436
2749
  const out = [];
2437
- const base = path10.join(this.root, "outbox");
2750
+ const base = path13.join(this.root, "outbox");
2438
2751
  if (!existsSync7(base)) return [];
2439
2752
  for (const h of readdirSync6(base)) {
2440
- const d = path10.join(base, h);
2753
+ const d = path13.join(base, h);
2441
2754
  let names = [];
2442
2755
  try {
2443
2756
  names = readdirSync6(d).filter((n) => n.endsWith(".jsonl"));
@@ -2446,7 +2759,7 @@ var Outbox = class {
2446
2759
  }
2447
2760
  for (const n of names) {
2448
2761
  try {
2449
- 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 });
2450
2763
  } catch {
2451
2764
  }
2452
2765
  }
@@ -2458,7 +2771,7 @@ var Outbox = class {
2458
2771
  read(ref, from, maxEvents = 500) {
2459
2772
  const f = this.file(ref);
2460
2773
  if (!existsSync7(f)) return { events: [], offset: from };
2461
- const size = statSync5(f).size;
2774
+ const size = statSync6(f).size;
2462
2775
  if (from >= size) return { events: [], offset: from };
2463
2776
  const fd = openSync3(f, "r");
2464
2777
  try {
@@ -2492,7 +2805,7 @@ var Outbox = class {
2492
2805
  for (const ref of this.sessions()) {
2493
2806
  const f = this.file(ref);
2494
2807
  try {
2495
- if (statSync5(f).mtimeMs < cutoff) {
2808
+ if (statSync6(f).mtimeMs < cutoff) {
2496
2809
  unlinkSync3(f);
2497
2810
  try {
2498
2811
  unlinkSync3(this.seqFile(ref));
@@ -2522,7 +2835,7 @@ var Cursors = class {
2522
2835
  root;
2523
2836
  sink;
2524
2837
  file() {
2525
- return path10.join(this.root, "cursors", `${safeId(this.sink)}.json`);
2838
+ return path13.join(this.root, "cursors", `${safeId(this.sink)}.json`);
2526
2839
  }
2527
2840
  read() {
2528
2841
  try {
@@ -2533,7 +2846,7 @@ var Cursors = class {
2533
2846
  }
2534
2847
  write(c) {
2535
2848
  const f = this.file();
2536
- mkdirSync5(path10.dirname(f), { recursive: true, mode: 448 });
2849
+ mkdirSync5(path13.dirname(f), { recursive: true, mode: 448 });
2537
2850
  const tmp = `${f}.${process.pid}.tmp`;
2538
2851
  writeFileSync5(tmp, JSON.stringify(c), { mode: 384 });
2539
2852
  renameSync(tmp, f);
@@ -2543,11 +2856,11 @@ var Cursors = class {
2543
2856
  }
2544
2857
  };
2545
2858
  function takeLock(root, ref, staleMs = 5 * 6e4) {
2546
- const dir = path10.join(root, "locks");
2859
+ const dir = path13.join(root, "locks");
2547
2860
  mkdirSync5(dir, { recursive: true, mode: 448 });
2548
- 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`);
2549
2862
  try {
2550
- if (Date.now() - statSync5(f).mtimeMs > staleMs) unlinkSync3(f);
2863
+ if (Date.now() - statSync6(f).mtimeMs > staleMs) unlinkSync3(f);
2551
2864
  else return null;
2552
2865
  } catch {
2553
2866
  }
@@ -2864,7 +3177,7 @@ function filterEvent(event, tier, pii, opts = {}) {
2864
3177
 
2865
3178
  // src/sinks/file.ts
2866
3179
  import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync6 } from "node:fs";
2867
- import path11 from "node:path";
3180
+ import path14 from "node:path";
2868
3181
  var FileSink = class {
2869
3182
  constructor(name, file, tier, pii) {
2870
3183
  this.name = name;
@@ -2878,7 +3191,7 @@ var FileSink = class {
2878
3191
  pii;
2879
3192
  async send(events) {
2880
3193
  try {
2881
- mkdirSync6(path11.dirname(this.file), { recursive: true });
3194
+ mkdirSync6(path14.dirname(this.file), { recursive: true });
2882
3195
  appendFileSync2(this.file, `${events.map((e) => JSON.stringify(e)).join("\n")}
2883
3196
  `);
2884
3197
  return { ok: true, acknowledged: events.length, accepted: events.length, duplicates: 0, rejected: [] };
@@ -2916,6 +3229,7 @@ export {
2916
3229
  DEFAULT_CONFIG,
2917
3230
  FileSink,
2918
3231
  HOME,
3232
+ JOB_TIMEOUT_MS,
2919
3233
  Outbox,
2920
3234
  PROTOCOL_VERSION,
2921
3235
  REDACTED,
@@ -2926,11 +3240,15 @@ export {
2926
3240
  adapterByName,
2927
3241
  antigravity,
2928
3242
  antigravityAnswer,
3243
+ antigravity_control_exports as antigravityControl,
2929
3244
  claudeCode,
2930
3245
  claude_code_control_exports as claudeControl,
2931
3246
  claudeDirs,
2932
3247
  codex,
3248
+ codex_control_exports as codexControl,
2933
3249
  configFile,
3250
+ deniedNote,
3251
+ drivableHarnesses,
2934
3252
  filterEvent,
2935
3253
  geminiCli,
2936
3254
  gitFacts,
@@ -2939,6 +3257,7 @@ export {
2939
3257
  isInjected,
2940
3258
  isSecretName,
2941
3259
  isUlid,
3260
+ jobEnv,
2942
3261
  machineName,
2943
3262
  makeEvent,
2944
3263
  pairTurns,
@@ -2951,6 +3270,7 @@ export {
2951
3270
  redactSecrets,
2952
3271
  reducePii,
2953
3272
  reducePiiDeep,
3273
+ runHeadless,
2954
3274
  safeId,
2955
3275
  stateDir,
2956
3276
  stripCredentials,