sessionpipe-conformance 0.7.0 → 0.9.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/cli.js CHANGED
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
- // sessionpipe-conformance 0.7.0 · Apache-2.0 · https://sessionpipe.org
2
+ // sessionpipe-conformance 0.9.0 · Apache-2.0 · https://sessionpipe.org
3
3
 
4
4
  // src/cli.ts
5
- import { readdirSync as readdirSync5, statSync as statSync4 } from "node:fs";
6
- import path7 from "node:path";
5
+ import { readdirSync as readdirSync5, realpathSync as realpathSync2, statSync as statSync4 } from "node:fs";
6
+ import path8 from "node:path";
7
+ import { pathToFileURL } from "node:url";
7
8
 
8
9
  // src/index.ts
9
10
  import { readFileSync as readFileSync6 } from "node:fs";
@@ -23,7 +24,7 @@ import path3 from "node:path";
23
24
 
24
25
  // ../core/src/paths.ts
25
26
  import os from "node:os";
26
- var HOME = os.homedir();
27
+ var homeDir = () => os.homedir();
27
28
 
28
29
  // ../core/src/readers/files.ts
29
30
  import { closeSync, fstatSync, openSync, readdirSync, readSync, statSync } from "node:fs";
@@ -72,8 +73,8 @@ function scanLines(file, visit, cap = 16 * 1024 * 1024) {
72
73
  let chunk = Buffer.concat([rest, buf.subarray(0, n)]);
73
74
  let nl = chunk.indexOf(10);
74
75
  while (nl >= 0) {
75
- const r2 = visit(chunk.subarray(0, nl).toString("utf8"));
76
- if (r2 !== null) return r2;
76
+ const r = visit(chunk.subarray(0, nl).toString("utf8"));
77
+ if (r !== null) return r;
77
78
  chunk = chunk.subarray(nl + 1);
78
79
  nl = chunk.indexOf(10);
79
80
  }
@@ -115,6 +116,19 @@ function recentFiles(root, depth, sinceMs, match) {
115
116
  return out;
116
117
  }
117
118
 
119
+ // ../core/src/adapters/codex-rollout.ts
120
+ function codexSubagent(meta) {
121
+ if (meta?.type !== "session_meta") return false;
122
+ const p = meta.payload;
123
+ return !!p?.source?.subagent;
124
+ }
125
+ function codexReplayBoundary(meta) {
126
+ if (meta?.type !== "session_meta") return void 0;
127
+ const p = meta.payload;
128
+ if (!p || !p.forked_from_id && !p.source?.subagent) return void 0;
129
+ return (record) => typeof p.id === "string" && !!p.id && record?.type === "event_msg" && record.payload?.thread_id === p.id;
130
+ }
131
+
118
132
  // ../core/src/readers/transcripts.ts
119
133
  function pairTurns(msgs, endLine) {
120
134
  const turns = [];
@@ -137,10 +151,14 @@ ${m.text}`;
137
151
  }
138
152
  if (cur?.assistant) turns.push(cur);
139
153
  const line = cur && !cur.assistant ? cur.startLine : endLine;
140
- return { turns: turns.map((t) => ({ user: t.user, assistant: t.assistant, at: t.at, endLine: t.endLine })), line };
154
+ const out = {
155
+ turns: turns.map((t) => ({ user: t.user, assistant: t.assistant, at: t.at, endLine: t.endLine })),
156
+ line
157
+ };
158
+ if (cur && !cur.assistant && cur.user) out.open = { user: cur.user, at: cur.at };
159
+ return out;
141
160
  }
142
- var isInjected = (text) => /^<[a-z_-]+>/i.test(text) && /<\/[a-z_-]+>\s*$/i.test(text) || /^<!--\s/.test(text) || /^<command-/.test(text);
143
- var endOf = (lines) => lines[lines.length - 1] === "" ? lines.length - 1 : lines.length;
161
+ var isInjected = (text) => /^<[a-z_-]+(?:\s[^>]*)?>/i.test(text) && /<\/[a-z_-]+>\s*$/i.test(text) || /^<!--\s/.test(text) || /^<command-/.test(text);
144
162
  var parse = (raw) => {
145
163
  try {
146
164
  return JSON.parse(raw);
@@ -148,6 +166,11 @@ var parse = (raw) => {
148
166
  return null;
149
167
  }
150
168
  };
169
+ var endOf = (lines) => {
170
+ const last = lines[lines.length - 1];
171
+ if (last === void 0) return 0;
172
+ return last === "" || parse(last) === null ? lines.length - 1 : lines.length;
173
+ };
151
174
  var textParts = (c, kinds) => {
152
175
  if (typeof c === "string") return c;
153
176
  if (!Array.isArray(c)) return "";
@@ -155,13 +178,21 @@ var textParts = (c, kinds) => {
155
178
  (b) => b && typeof b === "object" && kinds.includes(b.type ?? "") && typeof b.text === "string"
156
179
  ).map((b) => b.text).join("\n");
157
180
  };
181
+ var queuedPrompt = (a) => {
182
+ if (!a || typeof a !== "object") return "";
183
+ const q = a;
184
+ if (q.type !== "queued_command" || (q.commandMode ?? "prompt") !== "prompt") return "";
185
+ if (q.origin?.kind && q.origin.kind !== "human") return "";
186
+ return textParts(q.prompt, ["text"]).trim();
187
+ };
158
188
  function readClaude(lines, from) {
159
189
  const msgs = [];
160
190
  for (let i = from; i < lines.length; i++) {
161
191
  const j = lines[i] ? parse(lines[i]) : null;
162
192
  if (!j || j.isSidechain) continue;
163
193
  const origin = j.origin;
164
- if (j.type === "user" && (j.isMeta || origin?.kind && origin.kind !== "human")) continue;
194
+ if (j.type === "user" && (j.isMeta || j.isCompactSummary || j.repeatsCompactSummary || origin?.kind && origin.kind !== "human"))
195
+ continue;
165
196
  const at = Date.parse(String(j.timestamp ?? "")) || Date.now();
166
197
  const c = j.message?.content;
167
198
  if (j.type === "user") {
@@ -171,6 +202,9 @@ function readClaude(lines, from) {
171
202
  } else if (j.type === "assistant" && Array.isArray(c)) {
172
203
  const text = textParts(c, ["text"]).trim();
173
204
  if (text) msgs.push({ role: "assistant", text, at, line: i });
205
+ } else if (j.type === "attachment") {
206
+ const text = queuedPrompt(j.attachment);
207
+ if (text && !isInjected(text)) msgs.push({ role: "user", text, at, line: i });
174
208
  }
175
209
  }
176
210
  return pairTurns(msgs, endOf(lines));
@@ -180,7 +214,10 @@ function codexInjected(text) {
180
214
  }
181
215
  function readCodex(lines, from) {
182
216
  const msgs = [];
183
- for (let i = from; i < lines.length; i++) {
217
+ const boundary = codexReplayBoundary(parse(lines.find((line) => line.trim()) ?? ""));
218
+ const start = boundary ? lines.findIndex((line) => boundary(parse(line))) : 0;
219
+ if (start < 0) return pairTurns(msgs, endOf(lines));
220
+ for (let i = Math.max(from, start); i < lines.length; i++) {
184
221
  const j = lines[i] ? parse(lines[i]) : null;
185
222
  if (!j || j.type !== "response_item") continue;
186
223
  const p = j.payload;
@@ -218,7 +255,7 @@ function readAntigravity(lines, from) {
218
255
  const at = Date.parse(String(j.created_at ?? j.timestamp ?? "")) || Date.now();
219
256
  if (j.type === "USER_INPUT") {
220
257
  const m = /<USER_REQUEST>([\s\S]*?)(?:<\/USER_REQUEST>|$)/.exec(j.content);
221
- const text = (m ? m[1] ?? "" : j.content).trim();
258
+ const text = (m ? m[1] : j.content).trim();
222
259
  if (text) msgs.push({ role: "user", text, at, line: i });
223
260
  } else if (j.type === "PLANNER_RESPONSE") {
224
261
  const text = j.content.trim();
@@ -266,6 +303,10 @@ function formatJson(value, f) {
266
303
  const text = JSON.stringify(value, null, f.indent).replace(/\n/g, f.eol);
267
304
  return f.finalNewline ? text + f.eol : text;
268
305
  }
306
+ function readJson(file) {
307
+ const f = readJsonFile(file);
308
+ return f.ok ? f.value : {};
309
+ }
269
310
  function entryFor(event, harness, cmd, o) {
270
311
  const matcher = o.matcherFor(event);
271
312
  return {
@@ -335,8 +376,7 @@ function uninstallClaudeShaped(file, events, opts = {}) {
335
376
  unlinkSync(file);
336
377
  return { file, changed: true, note: "removed (sessionpipe created it)" };
337
378
  }
338
- const next = formatJson(settings2, f);
339
- if (next !== f.raw) writeFileSync(file, next);
379
+ writeFileSync(file, formatJson(settings2, f));
340
380
  return { file, changed: true };
341
381
  }
342
382
  function installedClaudeShaped(file, harness, events, cmd, o) {
@@ -363,7 +403,7 @@ var HOOK_NAME = "sessionpipe";
363
403
  var AG_EVENTS = ["PreInvocation", "PostInvocation", "PostToolUse", "Stop"];
364
404
  var AG_STALE_PRETOOL = '{"decision":"ask","reason":"sessionpipe: an outdated PreToolUse hook is still loaded. Restart this conversation (or run sessionpipe install) to drop it."}\n';
365
405
  var antigravityAnswer = (event) => event === "PreToolUse" ? AG_STALE_PRETOOL : "{}\n";
366
- var gemini = (env) => env.GEMINI_CLI_HOME || path3.join(HOME, ".gemini");
406
+ var gemini = (env) => env.GEMINI_CLI_HOME || path3.join(homeDir(), ".gemini");
367
407
  var hooksFile = (env) => path3.join(gemini(env), "config", "hooks.json");
368
408
  var projectsDir = (env) => path3.join(gemini(env), "config", "projects");
369
409
  var dataDirs = (env) => ["antigravity", "antigravity-cli", "antigravity-ide"].map((d) => path3.join(gemini(env), d));
@@ -429,7 +469,8 @@ var antigravity = {
429
469
  const event = input.argv[1] ?? "";
430
470
  let s = {};
431
471
  try {
432
- s = input.stdin ? JSON.parse(input.stdin) : {};
472
+ const j = input.stdin ? JSON.parse(input.stdin) : {};
473
+ if (j && typeof j === "object" && !Array.isArray(j)) s = j;
433
474
  } catch {
434
475
  }
435
476
  const id = s.conversationId ?? s.session_id ?? s.sessionId;
@@ -472,9 +513,9 @@ var antigravity = {
472
513
  default:
473
514
  return { session, events: [], stdout };
474
515
  }
475
- const r2 = { session, events, stdout, hints: { workspaces } };
476
- if (typeof s.transcriptPath === "string") r2.transcript = s.transcriptPath;
477
- return r2;
516
+ const r = { session, events, stdout, hints: { workspaces } };
517
+ if (typeof s.transcriptPath === "string") r.transcript = s.transcriptPath;
518
+ return r;
478
519
  },
479
520
  readTranscript(file, fromLine) {
480
521
  let text = "";
@@ -485,19 +526,16 @@ var antigravity = {
485
526
  }
486
527
  return readAntigravity(text.split("\n"), fromLine);
487
528
  },
488
- facts(session, transcript, hints, state) {
529
+ facts(session, transcript, hints) {
489
530
  if (!transcript) return {};
490
531
  return antigravityFacts(transcript, {
491
532
  conversation: session.id,
492
533
  workspaces: hints.workspaces ?? (session.cwd ? [session.cwd] : []),
493
- state,
494
534
  env: process.env
495
535
  });
496
536
  },
497
537
  backfill(sinceMs, env = process.env) {
498
- const rows = [];
499
- const noState = { get: () => ({}), save: () => {
500
- } };
538
+ const rows2 = [];
501
539
  for (const data of dataDirs(env)) {
502
540
  let ids = [];
503
541
  try {
@@ -511,13 +549,13 @@ var antigravity = {
511
549
  } catch {
512
550
  continue;
513
551
  }
514
- const facts = antigravityFacts(f, { conversation: id, workspaces: [], state: noState, env });
552
+ const facts = antigravityFacts(f, { conversation: id, workspaces: [], env });
515
553
  if (!facts.started_at || !facts.last_at) continue;
516
554
  const { started_at, last_at, first_ask: _fa, harness_version: _hv, ...rest } = facts;
517
- rows.push({ session: { id, ...rest }, started_at, last_at });
555
+ rows2.push({ session: { id, ...rest }, started_at, last_at });
518
556
  }
519
557
  }
520
- return rows;
558
+ return rows2;
521
559
  }
522
560
  };
523
561
  function prune(o) {
@@ -542,8 +580,8 @@ var agWorkspaces = (paths) => (Array.isArray(paths) ? paths : []).map(agPath).fi
542
580
  function agVariants(p) {
543
581
  const out = [p];
544
582
  try {
545
- const r2 = realpathSync.native(p);
546
- if (r2 !== p) out.push(r2);
583
+ const r = realpathSync.native(p);
584
+ if (r !== p) out.push(r);
547
585
  } catch {
548
586
  }
549
587
  return out;
@@ -566,8 +604,8 @@ function agProjects(dir) {
566
604
  continue;
567
605
  }
568
606
  const folders = [];
569
- for (const r2 of j?.projectResources?.resources ?? []) {
570
- const f = agPath(r2?.folderUri);
607
+ for (const r of j?.projectResources?.resources ?? []) {
608
+ const f = agPath(r?.folderUri ?? r?.gitFolder?.folderUri);
571
609
  if (f) folders.push(...agVariants(f));
572
610
  }
573
611
  if (folders.length) out.push({ id, folders });
@@ -621,7 +659,7 @@ function antigravityFacts(file, opts) {
621
659
  for (const j of H) {
622
660
  if (j.type !== "USER_INPUT" || typeof j.content !== "string") continue;
623
661
  const m = /<USER_REQUEST>([\s\S]*?)(?:<\/USER_REQUEST>|$)/.exec(j.content);
624
- const text = (m ? m[1] ?? "" : j.content).trim();
662
+ const text = (m ? m[1] : j.content).trim();
625
663
  const line = (text.split("\n").find((l) => l.trim()) ?? "").trim();
626
664
  if (!line || /^<[a-z_-]+>/i.test(line)) continue;
627
665
  out.title = line.length > 80 ? `${line.slice(0, 80).replace(/\s+\S*$/, "")}\u2026` : line;
@@ -669,10 +707,9 @@ function gitFacts(cwd) {
669
707
  return out;
670
708
  }
671
709
  function stripCredentials(url) {
672
- let u = url.replace(/\/\/[^/@]*@/, "//");
673
- const ssh = /^(?:ssh:\/\/)?git@([^:/]+)[:/](.+?)(?:\.git)?$/.exec(u);
674
- if (ssh) u = `${ssh[1]}/${ssh[2]}`;
675
- return u.replace(/^https?:\/\//, "").replace(/\.git$/, "");
710
+ const ssh = /^(?:ssh:\/\/)?git@([^:/]+)[:/](.+?)(?:\.git)?$/.exec(url);
711
+ if (ssh) return `${ssh[1]}/${ssh[2]}`;
712
+ return url.replace(/\/\/[^/@]*@/, "//").replace(/^https?:\/\//, "").replace(/\.git$/, "");
676
713
  }
677
714
 
678
715
  // ../core/src/adapters/claude-code.ts
@@ -703,12 +740,12 @@ var OPTS = {
703
740
  timeoutFor: (e) => e === "PermissionRequest" ? 130 : void 0
704
741
  };
705
742
  var DESKTOP_ID = /^local_[A-Za-z0-9-]{1,64}$/;
706
- function claudeDirs(env = process.env, home = env.HOME || HOME) {
743
+ function claudeDirs(env = process.env, home = env.HOME || homeDir()) {
707
744
  const out = [];
708
745
  const add = (d) => {
709
746
  if (!d) return;
710
- const r2 = path4.resolve(d);
711
- if (!out.includes(r2)) out.push(r2);
747
+ const r = path4.resolve(d);
748
+ if (!out.includes(r)) out.push(r);
712
749
  };
713
750
  add(path4.join(home, ".claude"));
714
751
  add(env.CLAUDE_CONFIG_DIR);
@@ -725,7 +762,7 @@ function claudeDirs(env = process.env, home = env.HOME || HOME) {
725
762
  var settingsIn = (dir) => path4.join(dir, "settings.json");
726
763
  var configDirOf = (transcript) => path4.dirname(path4.dirname(path4.dirname(transcript)));
727
764
  var validTool2 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
728
- var endReason = (r2) => r2 === "clear" || r2 === "logout" || r2 === "exit" ? r2 : r2 === "prompt_input_exit" ? "exit" : "other";
765
+ var endReason = (r) => r === "clear" || r === "logout" || r === "exit" ? r : r === "prompt_input_exit" ? "exit" : "other";
729
766
  var notifKind = (t) => t === "permission_prompt" ? "permission" : t === "idle_prompt" ? "idle" : t === "elicitation_dialog" ? "elicitation" : "question";
730
767
  var claudeCode = {
731
768
  name: NAME2,
@@ -744,11 +781,13 @@ var claudeCode = {
744
781
  const event = input.argv[1] ?? "";
745
782
  let s = {};
746
783
  try {
747
- s = input.stdin ? JSON.parse(input.stdin) : {};
784
+ const j = input.stdin ? JSON.parse(input.stdin) : {};
785
+ if (j && typeof j === "object" && !Array.isArray(j)) s = j;
748
786
  } catch {
749
787
  }
750
788
  const id = s.session_id ?? s.sessionId;
751
789
  if (!id) return null;
790
+ if (notClaudeCode(String(id), s)) return null;
752
791
  const session = { id: String(id), cwd: typeof s.cwd === "string" ? s.cwd : input.cwd };
753
792
  if (typeof s.model === "string") session.model = s.model;
754
793
  if (typeof s.agent_id === "string" && event.startsWith("Subagent") === false && s.agent_id)
@@ -867,12 +906,13 @@ var claudeCode = {
867
906
  return null;
868
907
  }
869
908
  const hints = {};
909
+ if (event === "UserPromptSubmit" && typeof s.prompt === "string") hints.prompt = s.prompt;
870
910
  const host = input.env.CLAUDE_CODE_HOST_SESSION_ID;
871
911
  if (host && DESKTOP_ID.test(host)) hints.desktop_id = host;
872
912
  if (event !== "PreToolUse" && event !== "PostToolUse") hints.claude_pid = claudePid(input.env);
873
- const r2 = { session, events, hints };
874
- if (typeof s.transcript_path === "string") r2.transcript = s.transcript_path;
875
- return r2;
913
+ const r = { session, events, hints };
914
+ if (typeof s.transcript_path === "string") r.transcript = s.transcript_path;
915
+ return r;
876
916
  },
877
917
  readTranscript(file, fromLine) {
878
918
  let text = "";
@@ -892,7 +932,7 @@ var claudeCode = {
892
932
  return out;
893
933
  },
894
934
  backfill(sinceMs, env = process.env) {
895
- const rows = [];
935
+ const rows2 = [];
896
936
  const files = [];
897
937
  for (const d of claudeDirs(env))
898
938
  files.push(...recentFiles(path4.join(d, "projects"), 1, sinceMs, (n) => /^[0-9a-f-]{36}\.jsonl$/.test(n)));
@@ -904,19 +944,23 @@ var claudeCode = {
904
944
  if (!facts.started_at || !facts.last_at) continue;
905
945
  const { started_at, last_at, first_ask: _fa, harness_version: _hv, ...rest } = facts;
906
946
  const cwd = rest.cwd;
907
- rows.push({
947
+ rows2.push({
908
948
  session: { id, ...rest, ...gitFacts(cwd), ...rest.branch ? { branch: rest.branch } : {} },
909
949
  started_at,
910
950
  last_at
911
951
  });
912
952
  }
913
- return rows;
953
+ return rows2;
914
954
  }
915
955
  };
916
956
  function prune2(o) {
917
957
  for (const k of Object.keys(o)) if (o[k] === void 0) delete o[k];
918
958
  return o;
919
959
  }
960
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
961
+ function notClaudeCode(id, s) {
962
+ return typeof s.transcript_path !== "string" && !UUID.test(id);
963
+ }
920
964
  function pickSource(v) {
921
965
  return typeof v === "string" && ["startup", "resume", "clear", "compact", "fork"].includes(v) ? v : "unknown";
922
966
  }
@@ -943,13 +987,13 @@ var NAME_RANK = {
943
987
  };
944
988
  function registryEntry(j) {
945
989
  if (!j || typeof j !== "object") return null;
946
- const r2 = j;
947
- const source = r2.nameSource ? NAME_RANK[r2.nameSource] : void 0;
990
+ const r = j;
991
+ const source = r.nameSource ? NAME_RANK[r.nameSource] : void 0;
948
992
  return {
949
- sessionId: r2.sessionId ? String(r2.sessionId) : null,
950
- name: source && typeof r2.name === "string" && r2.name.trim() ? r2.name.trim() : null,
993
+ sessionId: r.sessionId ? String(r.sessionId) : null,
994
+ name: source && typeof r.name === "string" && r.name.trim() ? r.name.trim() : null,
951
995
  name_source: source ?? null,
952
- bridge: typeof r2.bridgeSessionId === "string" ? r2.bridgeSessionId : null
996
+ bridge: typeof r.bridgeSessionId === "string" ? r.bridgeSessionId : null
953
997
  };
954
998
  }
955
999
  var liveCache = /* @__PURE__ */ new Map();
@@ -1022,23 +1066,27 @@ function parentOf() {
1022
1066
  }
1023
1067
  return (pid) => table.get(pid) || 0;
1024
1068
  }
1025
- var accountCache = /* @__PURE__ */ new Map();
1026
- function claudeAccount(dir) {
1027
- const hit = accountCache.get(dir);
1028
- if (hit !== void 0) return hit;
1069
+ var EMAIL_RE = /^[^@\s]{1,64}@[^@\s]{1,189}$/;
1070
+ function claudeLogin(dir, home = homeDir()) {
1029
1071
  const files = [path4.join(dir, ".claude.json")];
1030
- if (path4.resolve(dir) === path4.join(HOME, ".claude")) files.unshift(path4.join(HOME, ".claude.json"));
1031
- let acct = null;
1072
+ if (path4.resolve(dir) === path4.join(home, ".claude")) files.unshift(path4.join(home, ".claude.json"));
1032
1073
  for (const f of files) {
1033
1074
  try {
1034
1075
  const a = JSON.parse(readFileSync3(f, "utf8")).oauthAccount;
1035
- if (a?.accountUuid) {
1036
- acct = String(a.accountUuid);
1037
- break;
1076
+ if (typeof a?.accountUuid === "string" && a.accountUuid) {
1077
+ const email = typeof a.emailAddress === "string" && EMAIL_RE.test(a.emailAddress) ? a.emailAddress : void 0;
1078
+ return { id: a.accountUuid.slice(0, 128), ...email ? { email } : {} };
1038
1079
  }
1039
1080
  } catch {
1040
1081
  }
1041
1082
  }
1083
+ return null;
1084
+ }
1085
+ var accountCache = /* @__PURE__ */ new Map();
1086
+ function claudeAccount(dir) {
1087
+ const hit = accountCache.get(dir);
1088
+ if (hit !== void 0) return hit;
1089
+ const acct = claudeLogin(dir)?.id ?? null;
1042
1090
  accountCache.set(dir, acct);
1043
1091
  return acct;
1044
1092
  }
@@ -1258,18 +1306,13 @@ function parseArgv(raw) {
1258
1306
  function readNotify(text) {
1259
1307
  const { head, tail, at } = split(text);
1260
1308
  const m = NOTIFY_LINE.exec(head);
1261
- if (m)
1262
- return {
1263
- raw: m[1] ?? "",
1264
- argv: parseArgv(m[1] ?? ""),
1265
- index: m.index,
1266
- length: m[0].length,
1267
- misplaced: false,
1268
- other: !OURS2.test(m[1] ?? "")
1269
- };
1309
+ if (m) {
1310
+ const raw = m[1];
1311
+ return { raw, argv: parseArgv(raw), index: m.index, length: m[0].length, misplaced: false, other: !OURS2.test(raw) };
1312
+ }
1270
1313
  const k = NOTIFY_KEY.exec(head);
1271
1314
  if (k) {
1272
- const rest = head.slice(k.index).split("\n")[0] ?? "";
1315
+ const rest = head.slice(k.index).split("\n")[0];
1273
1316
  return {
1274
1317
  raw: rest.replace(NOTIFY_KEY, "").trim(),
1275
1318
  argv: null,
@@ -1280,15 +1323,11 @@ function readNotify(text) {
1280
1323
  };
1281
1324
  }
1282
1325
  const t = NOTIFY_LINE.exec(tail);
1283
- if (t && OURS2.test(t[1] ?? ""))
1284
- return {
1285
- raw: t[1] ?? "",
1286
- argv: parseArgv(t[1] ?? ""),
1287
- index: at + t.index,
1288
- length: t[0].length,
1289
- misplaced: true,
1290
- other: false
1291
- };
1326
+ if (t) {
1327
+ const raw = t[1];
1328
+ if (OURS2.test(raw))
1329
+ return { raw, argv: parseArgv(raw), index: at + t.index, length: t[0].length, misplaced: true, other: false };
1330
+ }
1292
1331
  return null;
1293
1332
  }
1294
1333
  function dropLine(text, index, length) {
@@ -1307,9 +1346,7 @@ function withNotify(text, line) {
1307
1346
  if (n?.misplaced) return insertTopLevel(dropLine(text, n.index, n.length), line);
1308
1347
  return insertTopLevel(text, line);
1309
1348
  }
1310
- function replaceNotify(text, line) {
1311
- const n = readNotify(text);
1312
- if (!n || n.misplaced || !n.length) return null;
1349
+ function replaceNotify(text, n, line) {
1313
1350
  return text.slice(0, n.index) + line + text.slice(n.index + n.length);
1314
1351
  }
1315
1352
  function withoutNotify(text) {
@@ -1350,12 +1387,12 @@ var CODEX_EVENTS = [
1350
1387
  ];
1351
1388
  var OPTS2 = { matcherFor: (e) => /Tool|Permission/.test(e) ? "" : void 0, timeout: 5 };
1352
1389
  var eventsFor2 = (lean) => lean ? CODEX_EVENTS.filter((e) => e !== "PreToolUse") : [...CODEX_EVENTS];
1353
- var codexHome = (env) => env.CODEX_HOME || path5.join(HOME, ".codex");
1390
+ var codexHome = (env) => env.CODEX_HOME || path5.join(homeDir(), ".codex");
1354
1391
  var hooksFile2 = (env) => path5.join(codexHome(env), "hooks.json");
1355
1392
  var configFile = (env) => path5.join(codexHome(env), "config.toml");
1356
1393
  var sessionsDir = (env) => path5.join(codexHome(env), "sessions");
1357
1394
  var validTool3 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
1358
- var endReason2 = (r2) => r2 === "clear" || r2 === "logout" || r2 === "exit" ? r2 : "other";
1395
+ var endReason2 = (r) => r === "clear" || r === "logout" || r === "exit" ? r : "other";
1359
1396
  function installNotify(cmd, env) {
1360
1397
  const file = configFile(env);
1361
1398
  let text = "";
@@ -1388,11 +1425,8 @@ function installNotify(cmd, env) {
1388
1425
  return { file, changed: false, note: "notify current (after Codex Computer Use)" };
1389
1426
  }
1390
1427
  if (!prev || OURS2.test(JSON.stringify(prev))) {
1391
- const next = replaceNotify(text, `notify = ${withPreviousRaw(n.raw, argv)}`);
1392
- if (next) {
1393
- write(next);
1394
- return { file, changed: true, note: "chained after Codex Computer Use (--previous-notify)" };
1395
- }
1428
+ write(replaceNotify(text, n, `notify = ${withPreviousRaw(n.raw, argv)}`));
1429
+ return { file, changed: true, note: "chained after Codex Computer Use (--previous-notify)" };
1396
1430
  }
1397
1431
  }
1398
1432
  return {
@@ -1421,8 +1455,7 @@ function uninstallNotify(env) {
1421
1455
  }
1422
1456
  const prev = previousNotify(n.argv);
1423
1457
  if (isComputerUse(n.argv) && prev && OURS2.test(JSON.stringify(prev))) {
1424
- const next = replaceNotify(text, `notify = ${stripPreviousRaw(n.raw)}`);
1425
- if (next) writeFileSync3(file, next);
1458
+ writeFileSync3(file, replaceNotify(text, n, `notify = ${stripPreviousRaw(n.raw)}`));
1426
1459
  return { file, changed: true };
1427
1460
  }
1428
1461
  return { file, changed: false };
@@ -1452,6 +1485,13 @@ function findRollout(threadId, env = process.env) {
1452
1485
  walk2(sessionsDir(env), 0);
1453
1486
  return best;
1454
1487
  }
1488
+ function rolloutMeta(file) {
1489
+ return scanLines(file, (line) => parseLines([line])[0] ?? {});
1490
+ }
1491
+ function ofAnotherThread(file, id) {
1492
+ const m = /-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl(?:\.zst)?$/i.exec(file);
1493
+ return !!m?.[1] && m[1].toLowerCase() !== id.toLowerCase();
1494
+ }
1455
1495
  var codex = {
1456
1496
  name: NAME3,
1457
1497
  events: CODEX_EVENTS,
@@ -1494,12 +1534,15 @@ var codex = {
1494
1534
  if (event === "notify") {
1495
1535
  const raw = input.argv.find((a) => a.startsWith("{")) ?? input.stdin;
1496
1536
  try {
1497
- s = raw ? JSON.parse(raw) : {};
1537
+ const j = raw ? JSON.parse(raw) : {};
1538
+ if (j && typeof j === "object" && !Array.isArray(j)) s = j;
1498
1539
  } catch {
1499
1540
  }
1500
1541
  const id2 = s["thread-id"] ?? s.thread_id ?? s["session-id"] ?? s.session_id;
1501
1542
  if (!id2) return null;
1502
- const r3 = {
1543
+ const file = findRollout(String(id2), input.env);
1544
+ if (!file || codexSubagent(rolloutMeta(file))) return null;
1545
+ const r2 = {
1503
1546
  session: { id: String(id2), cwd: typeof s.cwd === "string" ? s.cwd : input.cwd },
1504
1547
  events: [
1505
1548
  {
@@ -1508,18 +1551,19 @@ var codex = {
1508
1551
  harnessEvent: "notify"
1509
1552
  }
1510
1553
  ],
1511
- hints: {}
1554
+ hints: {},
1555
+ transcript: file
1512
1556
  };
1513
- const file = findRollout(String(id2), input.env);
1514
- if (file) r3.transcript = file;
1515
- return r3;
1557
+ return r2;
1516
1558
  }
1517
1559
  try {
1518
- s = input.stdin ? JSON.parse(input.stdin) : {};
1560
+ const j = input.stdin ? JSON.parse(input.stdin) : {};
1561
+ if (j && typeof j === "object" && !Array.isArray(j)) s = j;
1519
1562
  } catch {
1520
1563
  }
1521
1564
  const id = s.session_id;
1522
1565
  if (!id) return null;
1566
+ if (s.transcript_path === null) return null;
1523
1567
  const session = { id: String(id), cwd: typeof s.cwd === "string" ? s.cwd : input.cwd };
1524
1568
  if (typeof s.model === "string") session.model = s.model;
1525
1569
  const turn_id = typeof s.turn_id === "string" ? s.turn_id : void 0;
@@ -1590,9 +1634,10 @@ var codex = {
1590
1634
  default:
1591
1635
  return null;
1592
1636
  }
1593
- const r2 = { session, events, hints: {} };
1594
- if (typeof s.transcript_path === "string") r2.transcript = s.transcript_path;
1595
- return r2;
1637
+ const r = { session, events, hints: {} };
1638
+ if (typeof s.transcript_path === "string" && !ofAnotherThread(s.transcript_path, String(id)))
1639
+ r.transcript = s.transcript_path;
1640
+ return r;
1596
1641
  },
1597
1642
  readTranscript(file, fromLine) {
1598
1643
  let text = "";
@@ -1609,16 +1654,17 @@ var codex = {
1609
1654
  return out;
1610
1655
  },
1611
1656
  backfill(sinceMs, env = process.env) {
1612
- const rows = [];
1657
+ const rows2 = [];
1613
1658
  for (const f of recentFiles(sessionsDir(env), 3, sinceMs, (n) => /^rollout-.*\.jsonl$/.test(n))) {
1614
1659
  const m = /([0-9a-f]{8}-[0-9a-f-]{27,})\.jsonl$/i.exec(path5.basename(f));
1615
1660
  if (!m?.[1]) continue;
1661
+ if (codexSubagent(rolloutMeta(f))) continue;
1616
1662
  const facts = codexFacts(f);
1617
1663
  if (!facts.started_at || !facts.last_at) continue;
1618
1664
  const { started_at, last_at, first_ask: _fa, harness_version: _hv, ...rest } = facts;
1619
- rows.push({ session: { id: m[1], ...rest }, started_at, last_at });
1665
+ rows2.push({ session: { id: m[1], ...rest }, started_at, last_at });
1620
1666
  }
1621
- return rows;
1667
+ return rows2;
1622
1668
  }
1623
1669
  };
1624
1670
  function prune3(o) {
@@ -1627,10 +1673,11 @@ function prune3(o) {
1627
1673
  }
1628
1674
  function codexFacts(file) {
1629
1675
  const { head, tail } = ends(file, 128 * 1024, 64 * 1024);
1630
- const H = parseLines(head);
1631
- const T = parseLines(tail);
1676
+ let H = parseLines(head);
1677
+ let T = parseLines(tail);
1632
1678
  const out = {};
1633
1679
  const meta = H.find((j) => j.type === "session_meta" && j.payload);
1680
+ const boundary = codexReplayBoundary(meta);
1634
1681
  if (meta?.payload) {
1635
1682
  const p = meta.payload;
1636
1683
  if (p.cwd) out.cwd = p.cwd;
@@ -1638,6 +1685,13 @@ function codexFacts(file) {
1638
1685
  if (p.git?.branch) out.branch = p.git.branch;
1639
1686
  if (p.git?.repository_url) out.repo = String(p.git.repository_url).replace(/\/\/[^/@]*@/, "//");
1640
1687
  }
1688
+ if (boundary) {
1689
+ const h = H.findIndex(boundary);
1690
+ const t = T.findIndex(boundary);
1691
+ H = h < 0 ? [] : H.slice(h);
1692
+ const found = h >= 0 || t >= 0 || scanLines(file, (line) => boundary(parseLines([line])[0] ?? null) ? true : null, Number.POSITIVE_INFINITY);
1693
+ T = t >= 0 ? T.slice(t) : found ? T : [];
1694
+ }
1641
1695
  const ctx = H.concat(T).reverse().find((j) => j.type === "turn_context" && j.payload?.model);
1642
1696
  if (ctx?.payload?.model) out.model = ctx.payload.model;
1643
1697
  for (const j of H) {
@@ -1645,12 +1699,12 @@ function codexFacts(file) {
1645
1699
  if (j.type !== "response_item" || !p || p.type !== "message" || p.role !== "user") continue;
1646
1700
  const text = (p.content ?? []).filter((b) => b && b.type === "input_text" && typeof b.text === "string").map((b) => b.text).join("\n").trim();
1647
1701
  if (!text || codexInjected(text)) continue;
1648
- const line = text.split("\n").find((l) => l.trim()) ?? "";
1702
+ const line = text.split("\n")[0];
1649
1703
  out.title = line.length > 80 ? `${line.slice(0, 80).replace(/\s+\S*$/, "")}\u2026` : line;
1650
1704
  out.title_source = "first-ask";
1651
1705
  break;
1652
1706
  }
1653
- const times = H.concat(T).map((j) => Date.parse(String(j.timestamp ?? ""))).filter((n) => n > 0);
1707
+ const times = (boundary && meta ? [meta, ...H, ...T] : H.concat(T)).map((j) => Date.parse(String(j.timestamp ?? ""))).filter((n) => n > 0);
1654
1708
  if (times.length) {
1655
1709
  out.started_at = Math.min(...times);
1656
1710
  out.last_at = Math.max(...times);
@@ -1658,11 +1712,432 @@ function codexFacts(file) {
1658
1712
  return out;
1659
1713
  }
1660
1714
 
1661
- // ../core/src/adapters/gemini-cli.ts
1662
- import { execFileSync as execFileSync3 } from "node:child_process";
1663
- import { existsSync as existsSync5, readFileSync as readFileSync5 } from "node:fs";
1715
+ // ../core/src/adapters/devin.ts
1716
+ import { createHash as createHash2 } from "node:crypto";
1717
+ import { existsSync as existsSync6, readlinkSync } from "node:fs";
1664
1718
  import path6 from "node:path";
1665
- var NAME4 = "gemini-cli";
1719
+
1720
+ // ../core/src/readers/sqlite.ts
1721
+ import { execFileSync as execFileSync3 } from "node:child_process";
1722
+ import { existsSync as existsSync5 } from "node:fs";
1723
+ import { createRequire } from "node:module";
1724
+ function quiet(fn) {
1725
+ const original = process.emitWarning;
1726
+ try {
1727
+ process.emitWarning = ((warning, ...rest) => {
1728
+ const kind = typeof rest[0] === "string" ? rest[0] : rest[0]?.type;
1729
+ if (kind === "ExperimentalWarning" && /sqlite/i.test(String(warning))) return;
1730
+ original.call(process, warning, ...rest);
1731
+ });
1732
+ return fn();
1733
+ } finally {
1734
+ process.emitWarning = original;
1735
+ }
1736
+ }
1737
+ var ctorCache;
1738
+ function dbCtor() {
1739
+ if (ctorCache !== void 0) return ctorCache;
1740
+ ctorCache = null;
1741
+ try {
1742
+ const req = createRequire(import.meta.url);
1743
+ const m = quiet(() => req("node:sqlite"));
1744
+ if (typeof m?.DatabaseSync === "function") ctorCache = m.DatabaseSync;
1745
+ } catch {
1746
+ }
1747
+ return ctorCache;
1748
+ }
1749
+ function plain(row) {
1750
+ if (!row || typeof row !== "object") return null;
1751
+ const out = {};
1752
+ for (const [k, v] of Object.entries(row)) {
1753
+ if (typeof v === "bigint") out[k] = Number(v);
1754
+ else if (v instanceof Uint8Array) out[k] = null;
1755
+ else out[k] = v;
1756
+ }
1757
+ return out;
1758
+ }
1759
+ var rows = (v) => {
1760
+ if (!Array.isArray(v)) return null;
1761
+ const out = [];
1762
+ for (const r of v) {
1763
+ const p = plain(r);
1764
+ if (p) out.push(p);
1765
+ }
1766
+ return out;
1767
+ };
1768
+ function viaNode(file, sql, params, o) {
1769
+ const C = dbCtor();
1770
+ if (!C) return null;
1771
+ let db;
1772
+ try {
1773
+ db = quiet(() => new C(file, { readOnly: true, timeout: o.timeoutMs }));
1774
+ } catch {
1775
+ return null;
1776
+ }
1777
+ try {
1778
+ try {
1779
+ db.exec(`PRAGMA busy_timeout = ${Math.round(o.timeoutMs)}`);
1780
+ } catch {
1781
+ }
1782
+ return quiet(() => rows(db.prepare(sql).all(...params)));
1783
+ } catch {
1784
+ return null;
1785
+ } finally {
1786
+ try {
1787
+ db.close();
1788
+ } catch {
1789
+ }
1790
+ }
1791
+ }
1792
+ var CHILD = `const { DatabaseSync } = require("node:sqlite");
1793
+ const [, file, sql, params, ms] = process.argv;
1794
+ const db = new DatabaseSync(file, { readOnly: true, timeout: Number(ms) });
1795
+ try { db.exec("PRAGMA busy_timeout = " + Number(ms)); } catch {}
1796
+ const out = db.prepare(sql).all(...JSON.parse(params));
1797
+ db.close();
1798
+ process.stdout.write(JSON.stringify(out));`;
1799
+ function viaChild(file, sql, params, o) {
1800
+ try {
1801
+ const out = execFileSync3(
1802
+ process.execPath,
1803
+ ["--experimental-sqlite", "--no-warnings", "-e", CHILD, file, sql, JSON.stringify(params), String(o.timeoutMs)],
1804
+ {
1805
+ encoding: "utf8",
1806
+ timeout: o.timeoutMs + 3e3,
1807
+ stdio: ["ignore", "pipe", "ignore"],
1808
+ maxBuffer: o.maxBuffer,
1809
+ windowsHide: true
1810
+ }
1811
+ );
1812
+ return rows(JSON.parse(out || "null"));
1813
+ } catch {
1814
+ return null;
1815
+ }
1816
+ }
1817
+ var SAFE = /^[A-Za-z0-9 ._:@/+-]{0,256}$/;
1818
+ function inlineParams(sql, params) {
1819
+ if (sql.includes("'")) return null;
1820
+ const holes = sql.split("?").length - 1;
1821
+ if (holes !== params.length) return null;
1822
+ let i = 0;
1823
+ let bad = false;
1824
+ const out = sql.replace(/\?/g, () => {
1825
+ const p = params[i++];
1826
+ if (typeof p === "number") {
1827
+ if (!Number.isFinite(p)) {
1828
+ bad = true;
1829
+ return "NULL";
1830
+ }
1831
+ return String(p);
1832
+ }
1833
+ if (typeof p !== "string" || !SAFE.test(p)) {
1834
+ bad = true;
1835
+ return "NULL";
1836
+ }
1837
+ return `'${p}'`;
1838
+ });
1839
+ return bad ? null : out;
1840
+ }
1841
+ function viaCli(file, sql, params, o) {
1842
+ const text = inlineParams(sql, params);
1843
+ if (!text) return null;
1844
+ try {
1845
+ const out = execFileSync3("sqlite3", ["-readonly", "-json", file, text], {
1846
+ encoding: "utf8",
1847
+ timeout: o.timeoutMs + 3e3,
1848
+ stdio: ["ignore", "pipe", "ignore"],
1849
+ maxBuffer: o.maxBuffer,
1850
+ windowsHide: true
1851
+ }).trim();
1852
+ if (!out) return [];
1853
+ return rows(JSON.parse(out));
1854
+ } catch {
1855
+ return null;
1856
+ }
1857
+ }
1858
+ function querySqlite(file, sql, params = [], opts = {}) {
1859
+ const o = {
1860
+ timeoutMs: opts.timeoutMs ?? 2e3,
1861
+ engine: opts.engine ?? "auto",
1862
+ maxBuffer: opts.maxBuffer ?? 32 * 1024 * 1024
1863
+ };
1864
+ if (!file) return null;
1865
+ try {
1866
+ if (!existsSync5(file)) return null;
1867
+ } catch {
1868
+ return null;
1869
+ }
1870
+ const ways = o.engine === "auto" ? [viaNode, viaChild, viaCli] : o.engine === "node" ? [viaNode] : o.engine === "child" ? [viaChild] : [viaCli];
1871
+ for (const way of ways) {
1872
+ const r = way(file, sql, params, o);
1873
+ if (r) return r;
1874
+ }
1875
+ return null;
1876
+ }
1877
+
1878
+ // ../core/src/adapters/devin.ts
1879
+ var NAME4 = "devin";
1880
+ var DEVIN_EVENTS = [
1881
+ "SessionStart",
1882
+ "UserPromptSubmit",
1883
+ "PreToolUse",
1884
+ "PostToolUse",
1885
+ "PermissionRequest",
1886
+ "Stop",
1887
+ "PostCompaction",
1888
+ "SessionEnd"
1889
+ ];
1890
+ var OPTS3 = { matcherFor: (e) => /Tool|Permission/.test(e) ? "" : void 0, timeout: 5 };
1891
+ var eventsFor3 = (lean) => lean ? DEVIN_EVENTS.filter((e) => e !== "PreToolUse") : [...DEVIN_EVENTS];
1892
+ var INSTALL_NOTE = "Devin drops a whole hooks file on one event name it doesn't know; `/hooks` in `devin` lists what loaded";
1893
+ var CLAUDE_NOTE = "Devin also reads ~/.claude/settings.json hooks (read_config_from.claude); a Claude-only event name there makes it ignore that file whole";
1894
+ var deadFileNote = (bad) => `Devin loads NO hooks from this file: "${bad}" is not one of its event names and it rejects the file whole \u2014 remove that key (\`/hooks\` in \`devin\` lists what loaded)`;
1895
+ function foreignEvent(file) {
1896
+ const hooks = readJson(file).hooks;
1897
+ if (!hooks || typeof hooks !== "object" || Array.isArray(hooks)) return void 0;
1898
+ const ok = DEVIN_EVENTS;
1899
+ return Object.keys(hooks).find((k) => !ok.includes(k));
1900
+ }
1901
+ var homeOf = (env) => env.HOME || homeDir();
1902
+ var appData = (env) => env.APPDATA || path6.join(homeOf(env), "AppData", "Roaming");
1903
+ var devinConfigDir = (env = process.env) => process.platform === "win32" ? path6.join(appData(env), "devin") : path6.join(env.XDG_CONFIG_HOME || path6.join(homeOf(env), ".config"), "devin");
1904
+ var configFile2 = (env) => path6.join(devinConfigDir(env), "config.json");
1905
+ var devinDataDir = (env = process.env) => process.platform === "win32" ? path6.join(appData(env), "devin", "cli") : path6.join(env.XDG_DATA_HOME || path6.join(homeOf(env), ".local", "share"), "devin", "cli");
1906
+ var sessionsDb = (env) => path6.join(devinDataDir(env), "sessions.db");
1907
+ var SESSION_ID = /^[A-Za-z0-9._:-]{1,128}$/;
1908
+ var validTool4 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
1909
+ var endReason3 = (r) => r === "clear" || r === "logout" || r === "exit" ? r : "other";
1910
+ var pickSource2 = (v) => typeof v === "string" && ["startup", "resume", "clear", "compact", "fork"].includes(v) ? v : "unknown";
1911
+ function errText2(v) {
1912
+ if (typeof v === "string" && v) return v.slice(0, 2e3);
1913
+ if (v && typeof v === "object") {
1914
+ const e = v;
1915
+ if (typeof e.message === "string" && e.message) return e.message.slice(0, 2e3);
1916
+ if (typeof e.error === "string" && e.error) return e.error.slice(0, 2e3);
1917
+ }
1918
+ return void 0;
1919
+ }
1920
+ var cut = (s, n) => s.length > n ? `${s.slice(0, n).replace(/\s+\S*$/, "")}\u2026` : s;
1921
+ var devin = {
1922
+ name: NAME4,
1923
+ events: DEVIN_EVENTS,
1924
+ detect: (env = process.env) => existsSync6(devinConfigDir(env)) || existsSync6(devinDataDir(env)),
1925
+ configFiles: (env = process.env) => [configFile2(env)],
1926
+ install: (cmd, env = process.env, opts = {}) => {
1927
+ const file = configFile2(env);
1928
+ const r = installClaudeShaped(file, NAME4, eventsFor3(opts.lean), cmd, OPTS3);
1929
+ if (r.skipped) return [r];
1930
+ const bad = foreignEvent(file);
1931
+ return [{ ...r, note: bad ? deadFileNote(bad) : INSTALL_NOTE }];
1932
+ },
1933
+ uninstall: (env = process.env, opts = {}) => [
1934
+ uninstallClaudeShaped(configFile2(env), DEVIN_EVENTS, { created: opts.created?.includes(configFile2(env)) })
1935
+ ],
1936
+ installed: (cmd, env = process.env, opts = {}) => {
1937
+ const file = configFile2(env);
1938
+ const state = installedClaudeShaped(file, NAME4, eventsFor3(opts.lean), cmd, OPTS3);
1939
+ const bad = state === "missing" ? void 0 : foreignEvent(file);
1940
+ return [{ file, state: bad ? "inactive" : state, note: bad ? deadFileNote(bad) : CLAUDE_NOTE }];
1941
+ },
1942
+ fromHook(input) {
1943
+ const event = input.argv[1] ?? "";
1944
+ let s = {};
1945
+ try {
1946
+ const j = input.stdin ? JSON.parse(input.stdin) : {};
1947
+ if (j && typeof j === "object" && !Array.isArray(j)) s = j;
1948
+ } catch {
1949
+ }
1950
+ const id = typeof s.session_id === "string" ? s.session_id : "";
1951
+ if (!id) return null;
1952
+ const session = { id, cwd: input.env.DEVIN_PROJECT_DIR ?? input.cwd };
1953
+ const turn_id = typeof s.prompt_id === "string" ? s.prompt_id : void 0;
1954
+ const tool = validTool4(s.tool_name) ? s.tool_name : void 0;
1955
+ const call_id = typeof s.tool_use_id === "string" ? s.tool_use_id : void 0;
1956
+ const response = s.tool_response && typeof s.tool_response === "object" && !Array.isArray(s.tool_response) ? s.tool_response : void 0;
1957
+ const permissionId = () => `perm-${createHash2("sha256").update(`${turn_id ?? ""}|${tool ?? ""}|${JSON.stringify(s.tool_input ?? null)}`).digest("hex").slice(0, 16)}`;
1958
+ const ev = (type, data) => ({ type, data: prune4(data), harnessEvent: event });
1959
+ const events = [];
1960
+ switch (event) {
1961
+ case "SessionStart":
1962
+ events.push(ev("session.started", { source: pickSource2(s.source) }));
1963
+ break;
1964
+ case "UserPromptSubmit":
1965
+ events.push(
1966
+ ev("turn.started", {
1967
+ turn_id,
1968
+ prompt_chars: typeof s.prompt === "string" ? [...s.prompt].length : void 0
1969
+ })
1970
+ );
1971
+ break;
1972
+ case "PreToolUse":
1973
+ if (tool) events.push(ev("tool.started", { tool, call_id, turn_id, input: s.tool_input }));
1974
+ break;
1975
+ case "PostToolUse":
1976
+ if (tool)
1977
+ events.push(
1978
+ ev("tool.ended", {
1979
+ tool,
1980
+ call_id,
1981
+ turn_id,
1982
+ // `ok` is required on tool.ended (PROTOCOL.md §events), so a response
1983
+ // that carries no success flag at all reads as "nothing said it failed".
1984
+ ok: response?.success !== false,
1985
+ error: errText2(response?.error),
1986
+ input: s.tool_input,
1987
+ output: response ? response.output : s.tool_response
1988
+ })
1989
+ );
1990
+ break;
1991
+ case "PermissionRequest":
1992
+ events.push(ev("attention.needed", { attention_id: call_id ?? permissionId(), kind: "permission", tool }));
1993
+ break;
1994
+ case "Stop":
1995
+ events.push(ev("turn.ended", { turn_id, reason: "stop" }));
1996
+ break;
1997
+ case "PostCompaction":
1998
+ events.push(ev("context.compacted", { trigger: "auto" }));
1999
+ break;
2000
+ case "SessionEnd":
2001
+ events.push(ev("session.ended", { reason: endReason3(s.reason) }));
2002
+ break;
2003
+ default:
2004
+ return null;
2005
+ }
2006
+ return {
2007
+ session,
2008
+ events,
2009
+ hints: {},
2010
+ transcript: transcriptRef(sessionsDb(input.env), id)
2011
+ };
2012
+ },
2013
+ readTranscript(file, fromLine) {
2014
+ const ref = splitRef(file);
2015
+ if (!ref) return { turns: [], line: fromLine };
2016
+ return readDevinChain(ref.db, ref.session, fromLine);
2017
+ },
2018
+ facts(session, transcript, _hints, _state, env = process.env) {
2019
+ const out = {};
2020
+ const version = devinVersion(env);
2021
+ if (version) out.harness_version = version;
2022
+ const account = devinAccount(env);
2023
+ if (account) out.account_id = account;
2024
+ const ref = splitRef(transcript) ?? { db: sessionsDb(env), session: session.id };
2025
+ if (!SESSION_ID.test(ref.session)) return out;
2026
+ const rows2 = querySqlite(ref.db, SESSION_SQL, [ref.session]);
2027
+ const row = rows2?.[0];
2028
+ if (!row) return out;
2029
+ Object.assign(out, sessionFacts(row));
2030
+ return out;
2031
+ },
2032
+ backfill(sinceMs, env = process.env) {
2033
+ const rows2 = querySqlite(sessionsDb(env), BACKFILL_SQL, ["text", Math.floor(sinceMs / 1e3)]);
2034
+ if (!rows2) return [];
2035
+ const out = [];
2036
+ for (const row of rows2) {
2037
+ const id = String(row.id);
2038
+ if (!SESSION_ID.test(id)) continue;
2039
+ const { started_at, last_at, ...rest } = sessionFacts(row);
2040
+ if (!started_at || !last_at) continue;
2041
+ out.push({ session: { id, ...rest }, started_at, last_at });
2042
+ }
2043
+ return out;
2044
+ }
2045
+ };
2046
+ function prune4(o) {
2047
+ for (const k of Object.keys(o)) if (o[k] === void 0) delete o[k];
2048
+ return o;
2049
+ }
2050
+ var transcriptRef = (db, session) => `${db}#${session}`;
2051
+ function splitRef(ref) {
2052
+ if (!ref) return null;
2053
+ const at = ref.lastIndexOf("#");
2054
+ if (at <= 0) return null;
2055
+ const db = ref.slice(0, at);
2056
+ const session = ref.slice(at + 1);
2057
+ return db && SESSION_ID.test(session) ? { db, session } : null;
2058
+ }
2059
+ var CHAIN_LIMIT = 4e3;
2060
+ var CHAIN_SQL = `WITH RECURSIVE chain(node_id, parent_node_id, chat_message, created_at) AS (
2061
+ SELECT node_id, parent_node_id, chat_message, created_at FROM message_nodes
2062
+ WHERE session_id = ? AND node_id = (SELECT main_chain_id FROM sessions WHERE id = ?)
2063
+ UNION ALL
2064
+ SELECT m.node_id, m.parent_node_id, m.chat_message, m.created_at FROM message_nodes m
2065
+ JOIN chain c ON m.node_id = c.parent_node_id AND m.session_id = ? AND m.node_id >= ?
2066
+ )
2067
+ SELECT node_id, chat_message, created_at FROM chain ORDER BY node_id DESC LIMIT ${CHAIN_LIMIT}`;
2068
+ var SESSION_FIELDS = `s.model, s.working_directory, s.created_at, s.last_activity_at,
2069
+ (SELECT p.content FROM prompt_history p WHERE p.session_id = s.id AND p.is_shell = 0
2070
+ ORDER BY p.timestamp ASC, p.id ASC LIMIT 1) AS first_ask`;
2071
+ var SESSION_SQL = `SELECT ${SESSION_FIELDS} FROM sessions s WHERE s.id = ? LIMIT 1`;
2072
+ var BACKFILL_SQL = `SELECT s.id, ${SESSION_FIELDS} FROM sessions s
2073
+ WHERE s.hidden = 0 AND typeof(s.id) = ? AND s.last_activity_at >= ?
2074
+ ORDER BY s.last_activity_at DESC LIMIT 500`;
2075
+ function sessionFacts(row) {
2076
+ const out = {};
2077
+ if (typeof row.model === "string" && row.model) out.model = row.model;
2078
+ if (typeof row.working_directory === "string" && path6.isAbsolute(row.working_directory))
2079
+ out.cwd = row.working_directory;
2080
+ const started = Number(row.created_at);
2081
+ const last = Number(row.last_activity_at);
2082
+ if (Number.isFinite(started) && started > 0) out.started_at = Math.round(started * 1e3);
2083
+ if (Number.isFinite(last) && last > 0) out.last_at = Math.round(last * 1e3);
2084
+ if (typeof row.first_ask === "string") {
2085
+ const line = row.first_ask.split("\n").find((l) => l.trim()) ?? "";
2086
+ if (line.trim()) {
2087
+ out.title = cut(line.trim(), 80);
2088
+ out.title_source = "first-ask";
2089
+ }
2090
+ }
2091
+ return out;
2092
+ }
2093
+ function readDevinChain(db, session, fromLine) {
2094
+ if (!SESSION_ID.test(session)) return { turns: [], line: fromLine };
2095
+ const floor = Number.isFinite(fromLine) && fromLine > 0 ? Math.floor(fromLine) : 0;
2096
+ const rows2 = querySqlite(db, CHAIN_SQL, [session, session, session, floor]);
2097
+ if (!rows2?.length) return { turns: [], line: fromLine };
2098
+ const msgs = [];
2099
+ let end = fromLine;
2100
+ for (const row of rows2) {
2101
+ const node = Number(row.node_id);
2102
+ if (!Number.isFinite(node)) continue;
2103
+ if (node + 1 > end) end = node + 1;
2104
+ if (node < fromLine) continue;
2105
+ let j;
2106
+ try {
2107
+ j = JSON.parse(String(row.chat_message ?? ""));
2108
+ } catch {
2109
+ continue;
2110
+ }
2111
+ const text = typeof j.content === "string" ? j.content.trim() : "";
2112
+ if (!text) continue;
2113
+ const secs = Number(row.created_at);
2114
+ const at = Date.parse(String(j.metadata?.created_at ?? "")) || (Number.isFinite(secs) && secs > 0 ? Math.round(secs * 1e3) : 0) || Date.now();
2115
+ if (j.role === "user" && j.metadata?.is_user_input === true) msgs.push({ role: "user", text, at, line: node });
2116
+ else if (j.role === "assistant") msgs.push({ role: "assistant", text, at, line: node });
2117
+ }
2118
+ msgs.sort((a, b) => a.line - b.line);
2119
+ return pairTurns(msgs, end);
2120
+ }
2121
+ function devinVersion(env = process.env) {
2122
+ try {
2123
+ const target = readlinkSync(path6.join(devinDataDir(env), "_versions", "current"));
2124
+ const name = path6.basename(target);
2125
+ return /^[0-9][0-9A-Za-z.+-]{0,63}$/.test(name) ? name : void 0;
2126
+ } catch {
2127
+ return void 0;
2128
+ }
2129
+ }
2130
+ function devinAccount(env = process.env) {
2131
+ const d = readJson(configFile2(env)).devin;
2132
+ const id = d && typeof d === "object" ? d.org_id : void 0;
2133
+ return typeof id === "string" && /^[A-Za-z0-9_.:-]{1,128}$/.test(id) ? id : void 0;
2134
+ }
2135
+
2136
+ // ../core/src/adapters/gemini-cli.ts
2137
+ import { execFileSync as execFileSync4 } from "node:child_process";
2138
+ import { existsSync as existsSync7, readFileSync as readFileSync5 } from "node:fs";
2139
+ import path7 from "node:path";
2140
+ var NAME5 = "gemini-cli";
1666
2141
  var GEMINI_EVENTS = [
1667
2142
  "SessionStart",
1668
2143
  "BeforeAgent",
@@ -1673,11 +2148,11 @@ var GEMINI_EVENTS = [
1673
2148
  "PreCompress",
1674
2149
  "SessionEnd"
1675
2150
  ];
1676
- var OPTS3 = { matcherFor: (e) => /Tool/.test(e) ? ".*" : void 0, timeout: 5e3 };
1677
- var geminiDir = (env) => env.GEMINI_CLI_HOME || path6.join(HOME, ".gemini");
1678
- var settings = (env) => path6.join(geminiDir(env), "settings.json");
1679
- var validTool4 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
1680
- var eventsFor3 = (lean) => lean ? GEMINI_EVENTS.filter((e) => e !== "BeforeTool") : [...GEMINI_EVENTS];
2151
+ var OPTS4 = { matcherFor: (e) => /Tool/.test(e) ? ".*" : void 0, timeout: 5e3 };
2152
+ var geminiDir = (env) => env.GEMINI_CLI_HOME || path7.join(homeDir(), ".gemini");
2153
+ var settings = (env) => path7.join(geminiDir(env), "settings.json");
2154
+ var validTool5 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
2155
+ var eventsFor4 = (lean) => lean ? GEMINI_EVENTS.filter((e) => e !== "BeforeTool") : [...GEMINI_EVENTS];
1681
2156
  function geminiHooksActive(settingsFile) {
1682
2157
  let tools;
1683
2158
  try {
@@ -1689,7 +2164,7 @@ function geminiHooksActive(settingsFile) {
1689
2164
  return { active: false, note: "inactive: tools.enableHooks is false in settings.json" };
1690
2165
  let version = "";
1691
2166
  try {
1692
- version = execFileSync3("gemini", ["--version"], {
2167
+ version = execFileSync4("gemini", ["--version"], {
1693
2168
  encoding: "utf8",
1694
2169
  timeout: 4e3,
1695
2170
  stdio: ["ignore", "pipe", "ignore"]
@@ -1708,21 +2183,21 @@ function geminiHooksActive(settingsFile) {
1708
2183
  };
1709
2184
  }
1710
2185
  var geminiCli = {
1711
- name: NAME4,
2186
+ name: NAME5,
1712
2187
  events: GEMINI_EVENTS,
1713
2188
  // ~/.gemini also belongs to Antigravity; Gemini CLI is here when its settings or its chat logs exist.
1714
- detect: (env = process.env) => existsSync5(settings(env)) || existsSync5(path6.join(geminiDir(env), "tmp")),
2189
+ detect: (env = process.env) => existsSync7(settings(env)) || existsSync7(path7.join(geminiDir(env), "tmp")),
1715
2190
  configFiles: (env = process.env) => [settings(env)],
1716
2191
  install: (cmd, env = process.env, opts = {}) => {
1717
- const r2 = installClaudeShaped(settings(env), NAME4, eventsFor3(opts.lean), cmd, OPTS3);
2192
+ const r = installClaudeShaped(settings(env), NAME5, eventsFor4(opts.lean), cmd, OPTS4);
1718
2193
  const a = geminiHooksActive(settings(env));
1719
- return [a.note && !r2.skipped ? { ...r2, note: [r2.note, a.note].filter(Boolean).join("; ") } : r2];
2194
+ return [a.note && !r.skipped ? { ...r, note: [r.note, a.note].filter(Boolean).join("; ") } : r];
1720
2195
  },
1721
2196
  uninstall: (env = process.env, opts = {}) => [
1722
2197
  uninstallClaudeShaped(settings(env), GEMINI_EVENTS, { created: opts.created?.includes(settings(env)) })
1723
2198
  ],
1724
2199
  installed: (cmd, env = process.env, opts = {}) => {
1725
- const state = installedClaudeShaped(settings(env), NAME4, eventsFor3(opts.lean), cmd, OPTS3);
2200
+ const state = installedClaudeShaped(settings(env), NAME5, eventsFor4(opts.lean), cmd, OPTS4);
1726
2201
  const a = geminiHooksActive(settings(env));
1727
2202
  return [
1728
2203
  {
@@ -1736,14 +2211,15 @@ var geminiCli = {
1736
2211
  const event = input.argv[1] ?? "";
1737
2212
  let s = {};
1738
2213
  try {
1739
- s = input.stdin ? JSON.parse(input.stdin) : {};
2214
+ const j = input.stdin ? JSON.parse(input.stdin) : {};
2215
+ if (j && typeof j === "object" && !Array.isArray(j)) s = j;
1740
2216
  } catch {
1741
2217
  }
1742
2218
  const id = s.session_id;
1743
2219
  if (!id) return null;
1744
2220
  const session = { id: String(id), cwd: typeof s.cwd === "string" ? s.cwd : input.cwd };
1745
- const tool = validTool4(s.tool_name) ? s.tool_name : void 0;
1746
- const ev = (type, data) => ({ type, data: prune4(data), harnessEvent: event });
2221
+ const tool = validTool5(s.tool_name) ? s.tool_name : void 0;
2222
+ const ev = (type, data) => ({ type, data: prune5(data), harnessEvent: event });
1747
2223
  const events = [];
1748
2224
  switch (event) {
1749
2225
  case "SessionStart":
@@ -1801,9 +2277,9 @@ var geminiCli = {
1801
2277
  default:
1802
2278
  return null;
1803
2279
  }
1804
- const r2 = { session, events, hints: {} };
1805
- if (typeof s.transcript_path === "string") r2.transcript = s.transcript_path;
1806
- return r2;
2280
+ const r = { session, events, hints: {} };
2281
+ if (typeof s.transcript_path === "string") r.transcript = s.transcript_path;
2282
+ return r;
1807
2283
  },
1808
2284
  readTranscript(file, fromLine) {
1809
2285
  let text = "";
@@ -1824,7 +2300,7 @@ var geminiCli = {
1824
2300
  const raw = first ? first.content ?? first.message ?? first.text : void 0;
1825
2301
  const text = typeof raw === "string" ? raw.trim() : "";
1826
2302
  if (text) {
1827
- const line = text.split("\n").find((l) => l.trim()) ?? "";
2303
+ const line = text.split("\n")[0];
1828
2304
  out.title = line.length > 80 ? `${line.slice(0, 80).replace(/\s+\S*$/, "")}\u2026` : line;
1829
2305
  out.title_source = "first-ask";
1830
2306
  }
@@ -1838,13 +2314,13 @@ var geminiCli = {
1838
2314
  backfill: () => []
1839
2315
  // chat logs are keyed by a project hash, not a session id we can name; the first hook of a resumed session reports it
1840
2316
  };
1841
- function prune4(o) {
2317
+ function prune5(o) {
1842
2318
  for (const k of Object.keys(o)) if (o[k] === void 0) delete o[k];
1843
2319
  return o;
1844
2320
  }
1845
2321
 
1846
2322
  // ../core/src/adapters/index.ts
1847
- var ADAPTERS = [claudeCode, codex, geminiCli, antigravity];
2323
+ var ADAPTERS = [claudeCode, codex, geminiCli, antigravity, devin];
1848
2324
  var adapterByName = (name) => ADAPTERS.find((a) => a.name === name);
1849
2325
 
1850
2326
  // src/index.ts
@@ -1853,16 +2329,16 @@ function matches(expect, got, pathStr = "") {
1853
2329
  if (Array.isArray(expect)) {
1854
2330
  if (!Array.isArray(got) || got.length !== expect.length) return `${pathStr}: expected ${expect.length} items`;
1855
2331
  for (let i = 0; i < expect.length; i++) {
1856
- const r2 = matches(expect[i], got[i], `${pathStr}[${i}]`);
1857
- if (r2) return r2;
2332
+ const r = matches(expect[i], got[i], `${pathStr}[${i}]`);
2333
+ if (r) return r;
1858
2334
  }
1859
2335
  return null;
1860
2336
  }
1861
2337
  if (expect && typeof expect === "object") {
1862
2338
  if (!got || typeof got !== "object") return `${pathStr}: expected an object`;
1863
2339
  for (const [k, v] of Object.entries(expect)) {
1864
- const r2 = matches(v, got[k], `${pathStr}.${k}`);
1865
- if (r2) return r2;
2340
+ const r = matches(v, got[k], `${pathStr}.${k}`);
2341
+ if (r) return r;
1866
2342
  }
1867
2343
  return null;
1868
2344
  }
@@ -1877,51 +2353,64 @@ async function runAdapterFixtures(files) {
1877
2353
  results.push({ file, ok: false, why: `no adapter ${fx.harness}` });
1878
2354
  continue;
1879
2355
  }
1880
- const r2 = adapter.fromHook({
2356
+ const r = adapter.fromHook({
1881
2357
  argv: fx.input.argv,
1882
2358
  stdin: fx.input.stdin,
1883
2359
  env: fx.input.env ?? {},
1884
2360
  cwd: fx.input.cwd ?? "/"
1885
2361
  });
1886
- if (!r2) {
1887
- results.push({ file, ok: false, why: "fromHook returned null" });
2362
+ if (!r) {
2363
+ results.push(fx.expect.length ? { file, ok: false, why: "fromHook returned null" } : { file, ok: true });
1888
2364
  continue;
1889
2365
  }
1890
2366
  let why = matches(
1891
2367
  fx.expect.map((e) => e.type),
1892
- r2.events.map((e) => e.type),
2368
+ r.events.map((e) => e.type),
1893
2369
  "types"
1894
2370
  );
1895
2371
  if (!why)
1896
2372
  for (const [i, e] of fx.expect.entries()) {
1897
- const got = r2.events[i];
1898
- why = matches(e.harness.event, got?.harnessEvent, `[${i}].harness.event`) ?? matches(e.data, got?.data, `[${i}].data`) ?? matches({ ...e.session, seq: "*" }, r2.session, `[${i}].session`);
2373
+ const got = r.events[i];
2374
+ why = matches(e.harness.event, got.harnessEvent, `[${i}].harness.event`) ?? matches(e.data, got.data, `[${i}].data`) ?? matches({ ...e.session, seq: "*" }, r.session, `[${i}].session`);
1899
2375
  if (why) break;
1900
2376
  }
1901
2377
  results.push(why ? { file, ok: false, why } : { file, ok: true });
1902
2378
  }
1903
- const failed = results.filter((r2) => !r2.ok).length;
1904
- const table = results.map((r2) => `${r2.ok ? "ok " : "FAIL"} ${r2.file}${r2.why ? ` \u2014 ${r2.why}` : ""}`).join("\n");
2379
+ const failed = results.filter((r) => !r.ok).length;
2380
+ const table = results.map((r) => `${r.ok ? "ok " : "FAIL"} ${r.file}${r.why ? ` \u2014 ${r.why}` : ""}`).join("\n");
1905
2381
  return { results, failed, table };
1906
2382
  }
1907
2383
 
1908
2384
  // src/cli.ts
1909
- var target = process.argv[2];
1910
- if (!target) {
1911
- process.stdout.write("usage: sessionpipe-conformance <conformance/harness dir | https://receiver>\n");
1912
- process.exit(2);
1913
- }
1914
- if (/^https?:\/\//.test(target)) {
1915
- process.stdout.write("receiver scenarios land in M3\n");
1916
- process.exit(2);
1917
- }
1918
2385
  var walk = (d) => readdirSync5(d).flatMap((n) => {
1919
- const p = path7.join(d, n);
2386
+ const p = path8.join(d, n);
1920
2387
  return statSync4(p).isDirectory() ? walk(p) : p.endsWith(".json") ? [p] : [];
1921
2388
  });
1922
- var r = await runAdapterFixtures(walk(target));
1923
- process.stdout.write(`${r.table}
2389
+ async function main(argv, io = process) {
2390
+ const target = argv[0];
2391
+ if (!target) {
2392
+ io.stdout.write("usage: sessionpipe-conformance <conformance/harness dir | https://receiver>\n");
2393
+ return 2;
2394
+ }
2395
+ if (/^https?:\/\//.test(target)) {
2396
+ io.stdout.write("receiver scenarios land in M3\n");
2397
+ return 2;
2398
+ }
2399
+ const r = await runAdapterFixtures(walk(target));
2400
+ io.stdout.write(`${r.table}
1924
2401
  ${r.results.length - r.failed}/${r.results.length} passed
1925
2402
  `);
1926
- process.exit(r.failed ? 1 : 0);
2403
+ return r.failed ? 1 : 0;
2404
+ }
2405
+ function isMain() {
2406
+ try {
2407
+ return !!process.argv[1] && import.meta.url === pathToFileURL(realpathSync2(process.argv[1])).href;
2408
+ } catch {
2409
+ return false;
2410
+ }
2411
+ }
2412
+ if (isMain()) process.exit(await main(process.argv.slice(2)));
2413
+ export {
2414
+ main
2415
+ };
1927
2416
  //# sourceMappingURL=cli.js.map