sessionpipe-conformance 0.6.1 → 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.6.1 · 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,13 +202,22 @@ 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));
177
211
  }
212
+ function codexInjected(text) {
213
+ return /^<[a-z_-]+>/i.test(text) || /^# AGENTS\.md instructions\b/i.test(text);
214
+ }
178
215
  function readCodex(lines, from) {
179
216
  const msgs = [];
180
- 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++) {
181
221
  const j = lines[i] ? parse(lines[i]) : null;
182
222
  if (!j || j.type !== "response_item") continue;
183
223
  const p = j.payload;
@@ -186,7 +226,7 @@ function readCodex(lines, from) {
186
226
  const text = textParts(p.content, ["input_text", "output_text"]).trim();
187
227
  if (!text) continue;
188
228
  if (p.role === "user") {
189
- if (/^<[a-z_-]+>/i.test(text)) continue;
229
+ if (codexInjected(text)) continue;
190
230
  msgs.push({ role: "user", text, at, line: i });
191
231
  } else if (p.role === "assistant") msgs.push({ role: "assistant", text, at, line: i });
192
232
  }
@@ -215,7 +255,7 @@ function readAntigravity(lines, from) {
215
255
  const at = Date.parse(String(j.created_at ?? j.timestamp ?? "")) || Date.now();
216
256
  if (j.type === "USER_INPUT") {
217
257
  const m = /<USER_REQUEST>([\s\S]*?)(?:<\/USER_REQUEST>|$)/.exec(j.content);
218
- const text = (m ? m[1] ?? "" : j.content).trim();
258
+ const text = (m ? m[1] : j.content).trim();
219
259
  if (text) msgs.push({ role: "user", text, at, line: i });
220
260
  } else if (j.type === "PLANNER_RESPONSE") {
221
261
  const text = j.content.trim();
@@ -263,6 +303,10 @@ function formatJson(value, f) {
263
303
  const text = JSON.stringify(value, null, f.indent).replace(/\n/g, f.eol);
264
304
  return f.finalNewline ? text + f.eol : text;
265
305
  }
306
+ function readJson(file) {
307
+ const f = readJsonFile(file);
308
+ return f.ok ? f.value : {};
309
+ }
266
310
  function entryFor(event, harness, cmd, o) {
267
311
  const matcher = o.matcherFor(event);
268
312
  return {
@@ -332,8 +376,7 @@ function uninstallClaudeShaped(file, events, opts = {}) {
332
376
  unlinkSync(file);
333
377
  return { file, changed: true, note: "removed (sessionpipe created it)" };
334
378
  }
335
- const next = formatJson(settings2, f);
336
- if (next !== f.raw) writeFileSync(file, next);
379
+ writeFileSync(file, formatJson(settings2, f));
337
380
  return { file, changed: true };
338
381
  }
339
382
  function installedClaudeShaped(file, harness, events, cmd, o) {
@@ -360,7 +403,7 @@ var HOOK_NAME = "sessionpipe";
360
403
  var AG_EVENTS = ["PreInvocation", "PostInvocation", "PostToolUse", "Stop"];
361
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';
362
405
  var antigravityAnswer = (event) => event === "PreToolUse" ? AG_STALE_PRETOOL : "{}\n";
363
- var gemini = (env) => env.GEMINI_CLI_HOME || path3.join(HOME, ".gemini");
406
+ var gemini = (env) => env.GEMINI_CLI_HOME || path3.join(homeDir(), ".gemini");
364
407
  var hooksFile = (env) => path3.join(gemini(env), "config", "hooks.json");
365
408
  var projectsDir = (env) => path3.join(gemini(env), "config", "projects");
366
409
  var dataDirs = (env) => ["antigravity", "antigravity-cli", "antigravity-ide"].map((d) => path3.join(gemini(env), d));
@@ -426,7 +469,8 @@ var antigravity = {
426
469
  const event = input.argv[1] ?? "";
427
470
  let s = {};
428
471
  try {
429
- 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;
430
474
  } catch {
431
475
  }
432
476
  const id = s.conversationId ?? s.session_id ?? s.sessionId;
@@ -469,9 +513,9 @@ var antigravity = {
469
513
  default:
470
514
  return { session, events: [], stdout };
471
515
  }
472
- const r2 = { session, events, stdout, hints: { workspaces } };
473
- if (typeof s.transcriptPath === "string") r2.transcript = s.transcriptPath;
474
- return r2;
516
+ const r = { session, events, stdout, hints: { workspaces } };
517
+ if (typeof s.transcriptPath === "string") r.transcript = s.transcriptPath;
518
+ return r;
475
519
  },
476
520
  readTranscript(file, fromLine) {
477
521
  let text = "";
@@ -482,19 +526,16 @@ var antigravity = {
482
526
  }
483
527
  return readAntigravity(text.split("\n"), fromLine);
484
528
  },
485
- facts(session, transcript, hints, state) {
529
+ facts(session, transcript, hints) {
486
530
  if (!transcript) return {};
487
531
  return antigravityFacts(transcript, {
488
532
  conversation: session.id,
489
533
  workspaces: hints.workspaces ?? (session.cwd ? [session.cwd] : []),
490
- state,
491
534
  env: process.env
492
535
  });
493
536
  },
494
537
  backfill(sinceMs, env = process.env) {
495
- const rows = [];
496
- const noState = { get: () => ({}), save: () => {
497
- } };
538
+ const rows2 = [];
498
539
  for (const data of dataDirs(env)) {
499
540
  let ids = [];
500
541
  try {
@@ -508,13 +549,13 @@ var antigravity = {
508
549
  } catch {
509
550
  continue;
510
551
  }
511
- const facts = antigravityFacts(f, { conversation: id, workspaces: [], state: noState, env });
552
+ const facts = antigravityFacts(f, { conversation: id, workspaces: [], env });
512
553
  if (!facts.started_at || !facts.last_at) continue;
513
554
  const { started_at, last_at, first_ask: _fa, harness_version: _hv, ...rest } = facts;
514
- rows.push({ session: { id, ...rest }, started_at, last_at });
555
+ rows2.push({ session: { id, ...rest }, started_at, last_at });
515
556
  }
516
557
  }
517
- return rows;
558
+ return rows2;
518
559
  }
519
560
  };
520
561
  function prune(o) {
@@ -539,8 +580,8 @@ var agWorkspaces = (paths) => (Array.isArray(paths) ? paths : []).map(agPath).fi
539
580
  function agVariants(p) {
540
581
  const out = [p];
541
582
  try {
542
- const r2 = realpathSync.native(p);
543
- if (r2 !== p) out.push(r2);
583
+ const r = realpathSync.native(p);
584
+ if (r !== p) out.push(r);
544
585
  } catch {
545
586
  }
546
587
  return out;
@@ -563,8 +604,8 @@ function agProjects(dir) {
563
604
  continue;
564
605
  }
565
606
  const folders = [];
566
- for (const r2 of j?.projectResources?.resources ?? []) {
567
- const f = agPath(r2?.folderUri);
607
+ for (const r of j?.projectResources?.resources ?? []) {
608
+ const f = agPath(r?.folderUri ?? r?.gitFolder?.folderUri);
568
609
  if (f) folders.push(...agVariants(f));
569
610
  }
570
611
  if (folders.length) out.push({ id, folders });
@@ -618,7 +659,7 @@ function antigravityFacts(file, opts) {
618
659
  for (const j of H) {
619
660
  if (j.type !== "USER_INPUT" || typeof j.content !== "string") continue;
620
661
  const m = /<USER_REQUEST>([\s\S]*?)(?:<\/USER_REQUEST>|$)/.exec(j.content);
621
- const text = (m ? m[1] ?? "" : j.content).trim();
662
+ const text = (m ? m[1] : j.content).trim();
622
663
  const line = (text.split("\n").find((l) => l.trim()) ?? "").trim();
623
664
  if (!line || /^<[a-z_-]+>/i.test(line)) continue;
624
665
  out.title = line.length > 80 ? `${line.slice(0, 80).replace(/\s+\S*$/, "")}\u2026` : line;
@@ -666,10 +707,9 @@ function gitFacts(cwd) {
666
707
  return out;
667
708
  }
668
709
  function stripCredentials(url) {
669
- let u = url.replace(/\/\/[^/@]*@/, "//");
670
- const ssh = /^(?:ssh:\/\/)?git@([^:/]+)[:/](.+?)(?:\.git)?$/.exec(u);
671
- if (ssh) u = `${ssh[1]}/${ssh[2]}`;
672
- 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$/, "");
673
713
  }
674
714
 
675
715
  // ../core/src/adapters/claude-code.ts
@@ -700,12 +740,12 @@ var OPTS = {
700
740
  timeoutFor: (e) => e === "PermissionRequest" ? 130 : void 0
701
741
  };
702
742
  var DESKTOP_ID = /^local_[A-Za-z0-9-]{1,64}$/;
703
- function claudeDirs(env = process.env, home = env.HOME || HOME) {
743
+ function claudeDirs(env = process.env, home = env.HOME || homeDir()) {
704
744
  const out = [];
705
745
  const add = (d) => {
706
746
  if (!d) return;
707
- const r2 = path4.resolve(d);
708
- if (!out.includes(r2)) out.push(r2);
747
+ const r = path4.resolve(d);
748
+ if (!out.includes(r)) out.push(r);
709
749
  };
710
750
  add(path4.join(home, ".claude"));
711
751
  add(env.CLAUDE_CONFIG_DIR);
@@ -722,7 +762,7 @@ function claudeDirs(env = process.env, home = env.HOME || HOME) {
722
762
  var settingsIn = (dir) => path4.join(dir, "settings.json");
723
763
  var configDirOf = (transcript) => path4.dirname(path4.dirname(path4.dirname(transcript)));
724
764
  var validTool2 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
725
- 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";
726
766
  var notifKind = (t) => t === "permission_prompt" ? "permission" : t === "idle_prompt" ? "idle" : t === "elicitation_dialog" ? "elicitation" : "question";
727
767
  var claudeCode = {
728
768
  name: NAME2,
@@ -741,11 +781,13 @@ var claudeCode = {
741
781
  const event = input.argv[1] ?? "";
742
782
  let s = {};
743
783
  try {
744
- 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;
745
786
  } catch {
746
787
  }
747
788
  const id = s.session_id ?? s.sessionId;
748
789
  if (!id) return null;
790
+ if (notClaudeCode(String(id), s)) return null;
749
791
  const session = { id: String(id), cwd: typeof s.cwd === "string" ? s.cwd : input.cwd };
750
792
  if (typeof s.model === "string") session.model = s.model;
751
793
  if (typeof s.agent_id === "string" && event.startsWith("Subagent") === false && s.agent_id)
@@ -864,12 +906,13 @@ var claudeCode = {
864
906
  return null;
865
907
  }
866
908
  const hints = {};
909
+ if (event === "UserPromptSubmit" && typeof s.prompt === "string") hints.prompt = s.prompt;
867
910
  const host = input.env.CLAUDE_CODE_HOST_SESSION_ID;
868
911
  if (host && DESKTOP_ID.test(host)) hints.desktop_id = host;
869
912
  if (event !== "PreToolUse" && event !== "PostToolUse") hints.claude_pid = claudePid(input.env);
870
- const r2 = { session, events, hints };
871
- if (typeof s.transcript_path === "string") r2.transcript = s.transcript_path;
872
- return r2;
913
+ const r = { session, events, hints };
914
+ if (typeof s.transcript_path === "string") r.transcript = s.transcript_path;
915
+ return r;
873
916
  },
874
917
  readTranscript(file, fromLine) {
875
918
  let text = "";
@@ -889,7 +932,7 @@ var claudeCode = {
889
932
  return out;
890
933
  },
891
934
  backfill(sinceMs, env = process.env) {
892
- const rows = [];
935
+ const rows2 = [];
893
936
  const files = [];
894
937
  for (const d of claudeDirs(env))
895
938
  files.push(...recentFiles(path4.join(d, "projects"), 1, sinceMs, (n) => /^[0-9a-f-]{36}\.jsonl$/.test(n)));
@@ -901,19 +944,23 @@ var claudeCode = {
901
944
  if (!facts.started_at || !facts.last_at) continue;
902
945
  const { started_at, last_at, first_ask: _fa, harness_version: _hv, ...rest } = facts;
903
946
  const cwd = rest.cwd;
904
- rows.push({
947
+ rows2.push({
905
948
  session: { id, ...rest, ...gitFacts(cwd), ...rest.branch ? { branch: rest.branch } : {} },
906
949
  started_at,
907
950
  last_at
908
951
  });
909
952
  }
910
- return rows;
953
+ return rows2;
911
954
  }
912
955
  };
913
956
  function prune2(o) {
914
957
  for (const k of Object.keys(o)) if (o[k] === void 0) delete o[k];
915
958
  return o;
916
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
+ }
917
964
  function pickSource(v) {
918
965
  return typeof v === "string" && ["startup", "resume", "clear", "compact", "fork"].includes(v) ? v : "unknown";
919
966
  }
@@ -940,13 +987,13 @@ var NAME_RANK = {
940
987
  };
941
988
  function registryEntry(j) {
942
989
  if (!j || typeof j !== "object") return null;
943
- const r2 = j;
944
- 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;
945
992
  return {
946
- sessionId: r2.sessionId ? String(r2.sessionId) : null,
947
- 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,
948
995
  name_source: source ?? null,
949
- bridge: typeof r2.bridgeSessionId === "string" ? r2.bridgeSessionId : null
996
+ bridge: typeof r.bridgeSessionId === "string" ? r.bridgeSessionId : null
950
997
  };
951
998
  }
952
999
  var liveCache = /* @__PURE__ */ new Map();
@@ -1019,23 +1066,27 @@ function parentOf() {
1019
1066
  }
1020
1067
  return (pid) => table.get(pid) || 0;
1021
1068
  }
1022
- var accountCache = /* @__PURE__ */ new Map();
1023
- function claudeAccount(dir) {
1024
- const hit = accountCache.get(dir);
1025
- if (hit !== void 0) return hit;
1069
+ var EMAIL_RE = /^[^@\s]{1,64}@[^@\s]{1,189}$/;
1070
+ function claudeLogin(dir, home = homeDir()) {
1026
1071
  const files = [path4.join(dir, ".claude.json")];
1027
- if (path4.resolve(dir) === path4.join(HOME, ".claude")) files.unshift(path4.join(HOME, ".claude.json"));
1028
- let acct = null;
1072
+ if (path4.resolve(dir) === path4.join(home, ".claude")) files.unshift(path4.join(home, ".claude.json"));
1029
1073
  for (const f of files) {
1030
1074
  try {
1031
1075
  const a = JSON.parse(readFileSync3(f, "utf8")).oauthAccount;
1032
- if (a?.accountUuid) {
1033
- acct = String(a.accountUuid);
1034
- 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 } : {} };
1035
1079
  }
1036
1080
  } catch {
1037
1081
  }
1038
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;
1039
1090
  accountCache.set(dir, acct);
1040
1091
  return acct;
1041
1092
  }
@@ -1255,18 +1306,13 @@ function parseArgv(raw) {
1255
1306
  function readNotify(text) {
1256
1307
  const { head, tail, at } = split(text);
1257
1308
  const m = NOTIFY_LINE.exec(head);
1258
- if (m)
1259
- return {
1260
- raw: m[1] ?? "",
1261
- argv: parseArgv(m[1] ?? ""),
1262
- index: m.index,
1263
- length: m[0].length,
1264
- misplaced: false,
1265
- other: !OURS2.test(m[1] ?? "")
1266
- };
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
+ }
1267
1313
  const k = NOTIFY_KEY.exec(head);
1268
1314
  if (k) {
1269
- const rest = head.slice(k.index).split("\n")[0] ?? "";
1315
+ const rest = head.slice(k.index).split("\n")[0];
1270
1316
  return {
1271
1317
  raw: rest.replace(NOTIFY_KEY, "").trim(),
1272
1318
  argv: null,
@@ -1277,15 +1323,11 @@ function readNotify(text) {
1277
1323
  };
1278
1324
  }
1279
1325
  const t = NOTIFY_LINE.exec(tail);
1280
- if (t && OURS2.test(t[1] ?? ""))
1281
- return {
1282
- raw: t[1] ?? "",
1283
- argv: parseArgv(t[1] ?? ""),
1284
- index: at + t.index,
1285
- length: t[0].length,
1286
- misplaced: true,
1287
- other: false
1288
- };
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
+ }
1289
1331
  return null;
1290
1332
  }
1291
1333
  function dropLine(text, index, length) {
@@ -1304,9 +1346,7 @@ function withNotify(text, line) {
1304
1346
  if (n?.misplaced) return insertTopLevel(dropLine(text, n.index, n.length), line);
1305
1347
  return insertTopLevel(text, line);
1306
1348
  }
1307
- function replaceNotify(text, line) {
1308
- const n = readNotify(text);
1309
- if (!n || n.misplaced || !n.length) return null;
1349
+ function replaceNotify(text, n, line) {
1310
1350
  return text.slice(0, n.index) + line + text.slice(n.index + n.length);
1311
1351
  }
1312
1352
  function withoutNotify(text) {
@@ -1347,12 +1387,12 @@ var CODEX_EVENTS = [
1347
1387
  ];
1348
1388
  var OPTS2 = { matcherFor: (e) => /Tool|Permission/.test(e) ? "" : void 0, timeout: 5 };
1349
1389
  var eventsFor2 = (lean) => lean ? CODEX_EVENTS.filter((e) => e !== "PreToolUse") : [...CODEX_EVENTS];
1350
- var codexHome = (env) => env.CODEX_HOME || path5.join(HOME, ".codex");
1390
+ var codexHome = (env) => env.CODEX_HOME || path5.join(homeDir(), ".codex");
1351
1391
  var hooksFile2 = (env) => path5.join(codexHome(env), "hooks.json");
1352
1392
  var configFile = (env) => path5.join(codexHome(env), "config.toml");
1353
1393
  var sessionsDir = (env) => path5.join(codexHome(env), "sessions");
1354
1394
  var validTool3 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
1355
- var endReason2 = (r2) => r2 === "clear" || r2 === "logout" || r2 === "exit" ? r2 : "other";
1395
+ var endReason2 = (r) => r === "clear" || r === "logout" || r === "exit" ? r : "other";
1356
1396
  function installNotify(cmd, env) {
1357
1397
  const file = configFile(env);
1358
1398
  let text = "";
@@ -1385,11 +1425,8 @@ function installNotify(cmd, env) {
1385
1425
  return { file, changed: false, note: "notify current (after Codex Computer Use)" };
1386
1426
  }
1387
1427
  if (!prev || OURS2.test(JSON.stringify(prev))) {
1388
- const next = replaceNotify(text, `notify = ${withPreviousRaw(n.raw, argv)}`);
1389
- if (next) {
1390
- write(next);
1391
- return { file, changed: true, note: "chained after Codex Computer Use (--previous-notify)" };
1392
- }
1428
+ write(replaceNotify(text, n, `notify = ${withPreviousRaw(n.raw, argv)}`));
1429
+ return { file, changed: true, note: "chained after Codex Computer Use (--previous-notify)" };
1393
1430
  }
1394
1431
  }
1395
1432
  return {
@@ -1418,13 +1455,13 @@ function uninstallNotify(env) {
1418
1455
  }
1419
1456
  const prev = previousNotify(n.argv);
1420
1457
  if (isComputerUse(n.argv) && prev && OURS2.test(JSON.stringify(prev))) {
1421
- const next = replaceNotify(text, `notify = ${stripPreviousRaw(n.raw)}`);
1422
- if (next) writeFileSync3(file, next);
1458
+ writeFileSync3(file, replaceNotify(text, n, `notify = ${stripPreviousRaw(n.raw)}`));
1423
1459
  return { file, changed: true };
1424
1460
  }
1425
1461
  return { file, changed: false };
1426
1462
  }
1427
- function findRollout(threadId, env) {
1463
+ function findRollout(threadId, env = process.env) {
1464
+ if (!/^[A-Za-z0-9-]{8,128}$/.test(threadId)) return null;
1428
1465
  let best = null;
1429
1466
  const walk2 = (dir, depth) => {
1430
1467
  let names = [];
@@ -1435,7 +1472,7 @@ function findRollout(threadId, env) {
1435
1472
  }
1436
1473
  for (const n of names.sort().reverse()) {
1437
1474
  const p = path5.join(dir, n);
1438
- if (n.endsWith(".jsonl") && n.includes(threadId)) {
1475
+ if (n.endsWith(`-${threadId}.jsonl`)) {
1439
1476
  best = p;
1440
1477
  return;
1441
1478
  }
@@ -1448,6 +1485,13 @@ function findRollout(threadId, env) {
1448
1485
  walk2(sessionsDir(env), 0);
1449
1486
  return best;
1450
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
+ }
1451
1495
  var codex = {
1452
1496
  name: NAME3,
1453
1497
  events: CODEX_EVENTS,
@@ -1490,12 +1534,15 @@ var codex = {
1490
1534
  if (event === "notify") {
1491
1535
  const raw = input.argv.find((a) => a.startsWith("{")) ?? input.stdin;
1492
1536
  try {
1493
- s = raw ? JSON.parse(raw) : {};
1537
+ const j = raw ? JSON.parse(raw) : {};
1538
+ if (j && typeof j === "object" && !Array.isArray(j)) s = j;
1494
1539
  } catch {
1495
1540
  }
1496
1541
  const id2 = s["thread-id"] ?? s.thread_id ?? s["session-id"] ?? s.session_id;
1497
1542
  if (!id2) return null;
1498
- const r3 = {
1543
+ const file = findRollout(String(id2), input.env);
1544
+ if (!file || codexSubagent(rolloutMeta(file))) return null;
1545
+ const r2 = {
1499
1546
  session: { id: String(id2), cwd: typeof s.cwd === "string" ? s.cwd : input.cwd },
1500
1547
  events: [
1501
1548
  {
@@ -1504,18 +1551,19 @@ var codex = {
1504
1551
  harnessEvent: "notify"
1505
1552
  }
1506
1553
  ],
1507
- hints: {}
1554
+ hints: {},
1555
+ transcript: file
1508
1556
  };
1509
- const file = findRollout(String(id2), input.env);
1510
- if (file) r3.transcript = file;
1511
- return r3;
1557
+ return r2;
1512
1558
  }
1513
1559
  try {
1514
- 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;
1515
1562
  } catch {
1516
1563
  }
1517
1564
  const id = s.session_id;
1518
1565
  if (!id) return null;
1566
+ if (s.transcript_path === null) return null;
1519
1567
  const session = { id: String(id), cwd: typeof s.cwd === "string" ? s.cwd : input.cwd };
1520
1568
  if (typeof s.model === "string") session.model = s.model;
1521
1569
  const turn_id = typeof s.turn_id === "string" ? s.turn_id : void 0;
@@ -1586,9 +1634,10 @@ var codex = {
1586
1634
  default:
1587
1635
  return null;
1588
1636
  }
1589
- const r2 = { session, events, hints: {} };
1590
- if (typeof s.transcript_path === "string") r2.transcript = s.transcript_path;
1591
- 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;
1592
1641
  },
1593
1642
  readTranscript(file, fromLine) {
1594
1643
  let text = "";
@@ -1605,16 +1654,17 @@ var codex = {
1605
1654
  return out;
1606
1655
  },
1607
1656
  backfill(sinceMs, env = process.env) {
1608
- const rows = [];
1657
+ const rows2 = [];
1609
1658
  for (const f of recentFiles(sessionsDir(env), 3, sinceMs, (n) => /^rollout-.*\.jsonl$/.test(n))) {
1610
1659
  const m = /([0-9a-f]{8}-[0-9a-f-]{27,})\.jsonl$/i.exec(path5.basename(f));
1611
1660
  if (!m?.[1]) continue;
1661
+ if (codexSubagent(rolloutMeta(f))) continue;
1612
1662
  const facts = codexFacts(f);
1613
1663
  if (!facts.started_at || !facts.last_at) continue;
1614
1664
  const { started_at, last_at, first_ask: _fa, harness_version: _hv, ...rest } = facts;
1615
- rows.push({ session: { id: m[1], ...rest }, started_at, last_at });
1665
+ rows2.push({ session: { id: m[1], ...rest }, started_at, last_at });
1616
1666
  }
1617
- return rows;
1667
+ return rows2;
1618
1668
  }
1619
1669
  };
1620
1670
  function prune3(o) {
@@ -1623,10 +1673,11 @@ function prune3(o) {
1623
1673
  }
1624
1674
  function codexFacts(file) {
1625
1675
  const { head, tail } = ends(file, 128 * 1024, 64 * 1024);
1626
- const H = parseLines(head);
1627
- const T = parseLines(tail);
1676
+ let H = parseLines(head);
1677
+ let T = parseLines(tail);
1628
1678
  const out = {};
1629
1679
  const meta = H.find((j) => j.type === "session_meta" && j.payload);
1680
+ const boundary = codexReplayBoundary(meta);
1630
1681
  if (meta?.payload) {
1631
1682
  const p = meta.payload;
1632
1683
  if (p.cwd) out.cwd = p.cwd;
@@ -1634,19 +1685,26 @@ function codexFacts(file) {
1634
1685
  if (p.git?.branch) out.branch = p.git.branch;
1635
1686
  if (p.git?.repository_url) out.repo = String(p.git.repository_url).replace(/\/\/[^/@]*@/, "//");
1636
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
+ }
1637
1695
  const ctx = H.concat(T).reverse().find((j) => j.type === "turn_context" && j.payload?.model);
1638
1696
  if (ctx?.payload?.model) out.model = ctx.payload.model;
1639
1697
  for (const j of H) {
1640
1698
  const p = j.payload;
1641
1699
  if (j.type !== "response_item" || !p || p.type !== "message" || p.role !== "user") continue;
1642
1700
  const text = (p.content ?? []).filter((b) => b && b.type === "input_text" && typeof b.text === "string").map((b) => b.text).join("\n").trim();
1643
- if (!text || /^<[a-z_-]+>/i.test(text)) continue;
1644
- const line = text.split("\n").find((l) => l.trim()) ?? "";
1701
+ if (!text || codexInjected(text)) continue;
1702
+ const line = text.split("\n")[0];
1645
1703
  out.title = line.length > 80 ? `${line.slice(0, 80).replace(/\s+\S*$/, "")}\u2026` : line;
1646
1704
  out.title_source = "first-ask";
1647
1705
  break;
1648
1706
  }
1649
- 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);
1650
1708
  if (times.length) {
1651
1709
  out.started_at = Math.min(...times);
1652
1710
  out.last_at = Math.max(...times);
@@ -1654,11 +1712,432 @@ function codexFacts(file) {
1654
1712
  return out;
1655
1713
  }
1656
1714
 
1657
- // ../core/src/adapters/gemini-cli.ts
1658
- import { execFileSync as execFileSync3 } from "node:child_process";
1659
- 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";
1660
1718
  import path6 from "node:path";
1661
- 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";
1662
2141
  var GEMINI_EVENTS = [
1663
2142
  "SessionStart",
1664
2143
  "BeforeAgent",
@@ -1669,11 +2148,11 @@ var GEMINI_EVENTS = [
1669
2148
  "PreCompress",
1670
2149
  "SessionEnd"
1671
2150
  ];
1672
- var OPTS3 = { matcherFor: (e) => /Tool/.test(e) ? ".*" : void 0, timeout: 5e3 };
1673
- var geminiDir = (env) => env.GEMINI_CLI_HOME || path6.join(HOME, ".gemini");
1674
- var settings = (env) => path6.join(geminiDir(env), "settings.json");
1675
- var validTool4 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
1676
- 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];
1677
2156
  function geminiHooksActive(settingsFile) {
1678
2157
  let tools;
1679
2158
  try {
@@ -1685,7 +2164,7 @@ function geminiHooksActive(settingsFile) {
1685
2164
  return { active: false, note: "inactive: tools.enableHooks is false in settings.json" };
1686
2165
  let version = "";
1687
2166
  try {
1688
- version = execFileSync3("gemini", ["--version"], {
2167
+ version = execFileSync4("gemini", ["--version"], {
1689
2168
  encoding: "utf8",
1690
2169
  timeout: 4e3,
1691
2170
  stdio: ["ignore", "pipe", "ignore"]
@@ -1704,21 +2183,21 @@ function geminiHooksActive(settingsFile) {
1704
2183
  };
1705
2184
  }
1706
2185
  var geminiCli = {
1707
- name: NAME4,
2186
+ name: NAME5,
1708
2187
  events: GEMINI_EVENTS,
1709
2188
  // ~/.gemini also belongs to Antigravity; Gemini CLI is here when its settings or its chat logs exist.
1710
- 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")),
1711
2190
  configFiles: (env = process.env) => [settings(env)],
1712
2191
  install: (cmd, env = process.env, opts = {}) => {
1713
- const r2 = installClaudeShaped(settings(env), NAME4, eventsFor3(opts.lean), cmd, OPTS3);
2192
+ const r = installClaudeShaped(settings(env), NAME5, eventsFor4(opts.lean), cmd, OPTS4);
1714
2193
  const a = geminiHooksActive(settings(env));
1715
- 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];
1716
2195
  },
1717
2196
  uninstall: (env = process.env, opts = {}) => [
1718
2197
  uninstallClaudeShaped(settings(env), GEMINI_EVENTS, { created: opts.created?.includes(settings(env)) })
1719
2198
  ],
1720
2199
  installed: (cmd, env = process.env, opts = {}) => {
1721
- const state = installedClaudeShaped(settings(env), NAME4, eventsFor3(opts.lean), cmd, OPTS3);
2200
+ const state = installedClaudeShaped(settings(env), NAME5, eventsFor4(opts.lean), cmd, OPTS4);
1722
2201
  const a = geminiHooksActive(settings(env));
1723
2202
  return [
1724
2203
  {
@@ -1732,14 +2211,15 @@ var geminiCli = {
1732
2211
  const event = input.argv[1] ?? "";
1733
2212
  let s = {};
1734
2213
  try {
1735
- 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;
1736
2216
  } catch {
1737
2217
  }
1738
2218
  const id = s.session_id;
1739
2219
  if (!id) return null;
1740
2220
  const session = { id: String(id), cwd: typeof s.cwd === "string" ? s.cwd : input.cwd };
1741
- const tool = validTool4(s.tool_name) ? s.tool_name : void 0;
1742
- 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 });
1743
2223
  const events = [];
1744
2224
  switch (event) {
1745
2225
  case "SessionStart":
@@ -1797,9 +2277,9 @@ var geminiCli = {
1797
2277
  default:
1798
2278
  return null;
1799
2279
  }
1800
- const r2 = { session, events, hints: {} };
1801
- if (typeof s.transcript_path === "string") r2.transcript = s.transcript_path;
1802
- return r2;
2280
+ const r = { session, events, hints: {} };
2281
+ if (typeof s.transcript_path === "string") r.transcript = s.transcript_path;
2282
+ return r;
1803
2283
  },
1804
2284
  readTranscript(file, fromLine) {
1805
2285
  let text = "";
@@ -1820,7 +2300,7 @@ var geminiCli = {
1820
2300
  const raw = first ? first.content ?? first.message ?? first.text : void 0;
1821
2301
  const text = typeof raw === "string" ? raw.trim() : "";
1822
2302
  if (text) {
1823
- const line = text.split("\n").find((l) => l.trim()) ?? "";
2303
+ const line = text.split("\n")[0];
1824
2304
  out.title = line.length > 80 ? `${line.slice(0, 80).replace(/\s+\S*$/, "")}\u2026` : line;
1825
2305
  out.title_source = "first-ask";
1826
2306
  }
@@ -1834,13 +2314,13 @@ var geminiCli = {
1834
2314
  backfill: () => []
1835
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
1836
2316
  };
1837
- function prune4(o) {
2317
+ function prune5(o) {
1838
2318
  for (const k of Object.keys(o)) if (o[k] === void 0) delete o[k];
1839
2319
  return o;
1840
2320
  }
1841
2321
 
1842
2322
  // ../core/src/adapters/index.ts
1843
- var ADAPTERS = [claudeCode, codex, geminiCli, antigravity];
2323
+ var ADAPTERS = [claudeCode, codex, geminiCli, antigravity, devin];
1844
2324
  var adapterByName = (name) => ADAPTERS.find((a) => a.name === name);
1845
2325
 
1846
2326
  // src/index.ts
@@ -1849,16 +2329,16 @@ function matches(expect, got, pathStr = "") {
1849
2329
  if (Array.isArray(expect)) {
1850
2330
  if (!Array.isArray(got) || got.length !== expect.length) return `${pathStr}: expected ${expect.length} items`;
1851
2331
  for (let i = 0; i < expect.length; i++) {
1852
- const r2 = matches(expect[i], got[i], `${pathStr}[${i}]`);
1853
- if (r2) return r2;
2332
+ const r = matches(expect[i], got[i], `${pathStr}[${i}]`);
2333
+ if (r) return r;
1854
2334
  }
1855
2335
  return null;
1856
2336
  }
1857
2337
  if (expect && typeof expect === "object") {
1858
2338
  if (!got || typeof got !== "object") return `${pathStr}: expected an object`;
1859
2339
  for (const [k, v] of Object.entries(expect)) {
1860
- const r2 = matches(v, got[k], `${pathStr}.${k}`);
1861
- if (r2) return r2;
2340
+ const r = matches(v, got[k], `${pathStr}.${k}`);
2341
+ if (r) return r;
1862
2342
  }
1863
2343
  return null;
1864
2344
  }
@@ -1873,51 +2353,64 @@ async function runAdapterFixtures(files) {
1873
2353
  results.push({ file, ok: false, why: `no adapter ${fx.harness}` });
1874
2354
  continue;
1875
2355
  }
1876
- const r2 = adapter.fromHook({
2356
+ const r = adapter.fromHook({
1877
2357
  argv: fx.input.argv,
1878
2358
  stdin: fx.input.stdin,
1879
2359
  env: fx.input.env ?? {},
1880
2360
  cwd: fx.input.cwd ?? "/"
1881
2361
  });
1882
- if (!r2) {
1883
- 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 });
1884
2364
  continue;
1885
2365
  }
1886
2366
  let why = matches(
1887
2367
  fx.expect.map((e) => e.type),
1888
- r2.events.map((e) => e.type),
2368
+ r.events.map((e) => e.type),
1889
2369
  "types"
1890
2370
  );
1891
2371
  if (!why)
1892
2372
  for (const [i, e] of fx.expect.entries()) {
1893
- const got = r2.events[i];
1894
- 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`);
1895
2375
  if (why) break;
1896
2376
  }
1897
2377
  results.push(why ? { file, ok: false, why } : { file, ok: true });
1898
2378
  }
1899
- const failed = results.filter((r2) => !r2.ok).length;
1900
- 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");
1901
2381
  return { results, failed, table };
1902
2382
  }
1903
2383
 
1904
2384
  // src/cli.ts
1905
- var target = process.argv[2];
1906
- if (!target) {
1907
- process.stdout.write("usage: sessionpipe-conformance <conformance/harness dir | https://receiver>\n");
1908
- process.exit(2);
1909
- }
1910
- if (/^https?:\/\//.test(target)) {
1911
- process.stdout.write("receiver scenarios land in M3\n");
1912
- process.exit(2);
1913
- }
1914
2385
  var walk = (d) => readdirSync5(d).flatMap((n) => {
1915
- const p = path7.join(d, n);
2386
+ const p = path8.join(d, n);
1916
2387
  return statSync4(p).isDirectory() ? walk(p) : p.endsWith(".json") ? [p] : [];
1917
2388
  });
1918
- var r = await runAdapterFixtures(walk(target));
1919
- 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}
1920
2401
  ${r.results.length - r.failed}/${r.results.length} passed
1921
2402
  `);
1922
- 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
+ };
1923
2416
  //# sourceMappingURL=cli.js.map