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/index.js CHANGED
@@ -1,4 +1,4 @@
1
- // sessionpipe-conformance 0.7.0 · Apache-2.0 · https://sessionpipe.org
1
+ // sessionpipe-conformance 0.9.0 · Apache-2.0 · https://sessionpipe.org
2
2
 
3
3
  // src/index.ts
4
4
  import { readFileSync as readFileSync6 } from "node:fs";
@@ -18,7 +18,7 @@ import path3 from "node:path";
18
18
 
19
19
  // ../core/src/paths.ts
20
20
  import os from "node:os";
21
- var HOME = os.homedir();
21
+ var homeDir = () => os.homedir();
22
22
 
23
23
  // ../core/src/readers/files.ts
24
24
  import { closeSync, fstatSync, openSync, readdirSync, readSync, statSync } from "node:fs";
@@ -110,6 +110,19 @@ function recentFiles(root, depth, sinceMs, match) {
110
110
  return out;
111
111
  }
112
112
 
113
+ // ../core/src/adapters/codex-rollout.ts
114
+ function codexSubagent(meta) {
115
+ if (meta?.type !== "session_meta") return false;
116
+ const p = meta.payload;
117
+ return !!p?.source?.subagent;
118
+ }
119
+ function codexReplayBoundary(meta) {
120
+ if (meta?.type !== "session_meta") return void 0;
121
+ const p = meta.payload;
122
+ if (!p || !p.forked_from_id && !p.source?.subagent) return void 0;
123
+ return (record) => typeof p.id === "string" && !!p.id && record?.type === "event_msg" && record.payload?.thread_id === p.id;
124
+ }
125
+
113
126
  // ../core/src/readers/transcripts.ts
114
127
  function pairTurns(msgs, endLine) {
115
128
  const turns = [];
@@ -132,10 +145,14 @@ ${m.text}`;
132
145
  }
133
146
  if (cur?.assistant) turns.push(cur);
134
147
  const line = cur && !cur.assistant ? cur.startLine : endLine;
135
- return { turns: turns.map((t) => ({ user: t.user, assistant: t.assistant, at: t.at, endLine: t.endLine })), line };
148
+ const out = {
149
+ turns: turns.map((t) => ({ user: t.user, assistant: t.assistant, at: t.at, endLine: t.endLine })),
150
+ line
151
+ };
152
+ if (cur && !cur.assistant && cur.user) out.open = { user: cur.user, at: cur.at };
153
+ return out;
136
154
  }
137
- var isInjected = (text) => /^<[a-z_-]+>/i.test(text) && /<\/[a-z_-]+>\s*$/i.test(text) || /^<!--\s/.test(text) || /^<command-/.test(text);
138
- var endOf = (lines) => lines[lines.length - 1] === "" ? lines.length - 1 : lines.length;
155
+ var isInjected = (text) => /^<[a-z_-]+(?:\s[^>]*)?>/i.test(text) && /<\/[a-z_-]+>\s*$/i.test(text) || /^<!--\s/.test(text) || /^<command-/.test(text);
139
156
  var parse = (raw) => {
140
157
  try {
141
158
  return JSON.parse(raw);
@@ -143,6 +160,11 @@ var parse = (raw) => {
143
160
  return null;
144
161
  }
145
162
  };
163
+ var endOf = (lines) => {
164
+ const last = lines[lines.length - 1];
165
+ if (last === void 0) return 0;
166
+ return last === "" || parse(last) === null ? lines.length - 1 : lines.length;
167
+ };
146
168
  var textParts = (c, kinds) => {
147
169
  if (typeof c === "string") return c;
148
170
  if (!Array.isArray(c)) return "";
@@ -150,13 +172,21 @@ var textParts = (c, kinds) => {
150
172
  (b) => b && typeof b === "object" && kinds.includes(b.type ?? "") && typeof b.text === "string"
151
173
  ).map((b) => b.text).join("\n");
152
174
  };
175
+ var queuedPrompt = (a) => {
176
+ if (!a || typeof a !== "object") return "";
177
+ const q = a;
178
+ if (q.type !== "queued_command" || (q.commandMode ?? "prompt") !== "prompt") return "";
179
+ if (q.origin?.kind && q.origin.kind !== "human") return "";
180
+ return textParts(q.prompt, ["text"]).trim();
181
+ };
153
182
  function readClaude(lines, from) {
154
183
  const msgs = [];
155
184
  for (let i = from; i < lines.length; i++) {
156
185
  const j = lines[i] ? parse(lines[i]) : null;
157
186
  if (!j || j.isSidechain) continue;
158
187
  const origin = j.origin;
159
- if (j.type === "user" && (j.isMeta || origin?.kind && origin.kind !== "human")) continue;
188
+ if (j.type === "user" && (j.isMeta || j.isCompactSummary || j.repeatsCompactSummary || origin?.kind && origin.kind !== "human"))
189
+ continue;
160
190
  const at = Date.parse(String(j.timestamp ?? "")) || Date.now();
161
191
  const c = j.message?.content;
162
192
  if (j.type === "user") {
@@ -166,6 +196,9 @@ function readClaude(lines, from) {
166
196
  } else if (j.type === "assistant" && Array.isArray(c)) {
167
197
  const text = textParts(c, ["text"]).trim();
168
198
  if (text) msgs.push({ role: "assistant", text, at, line: i });
199
+ } else if (j.type === "attachment") {
200
+ const text = queuedPrompt(j.attachment);
201
+ if (text && !isInjected(text)) msgs.push({ role: "user", text, at, line: i });
169
202
  }
170
203
  }
171
204
  return pairTurns(msgs, endOf(lines));
@@ -175,7 +208,10 @@ function codexInjected(text) {
175
208
  }
176
209
  function readCodex(lines, from) {
177
210
  const msgs = [];
178
- for (let i = from; i < lines.length; i++) {
211
+ const boundary = codexReplayBoundary(parse(lines.find((line) => line.trim()) ?? ""));
212
+ const start = boundary ? lines.findIndex((line) => boundary(parse(line))) : 0;
213
+ if (start < 0) return pairTurns(msgs, endOf(lines));
214
+ for (let i = Math.max(from, start); i < lines.length; i++) {
179
215
  const j = lines[i] ? parse(lines[i]) : null;
180
216
  if (!j || j.type !== "response_item") continue;
181
217
  const p = j.payload;
@@ -213,7 +249,7 @@ function readAntigravity(lines, from) {
213
249
  const at = Date.parse(String(j.created_at ?? j.timestamp ?? "")) || Date.now();
214
250
  if (j.type === "USER_INPUT") {
215
251
  const m = /<USER_REQUEST>([\s\S]*?)(?:<\/USER_REQUEST>|$)/.exec(j.content);
216
- const text = (m ? m[1] ?? "" : j.content).trim();
252
+ const text = (m ? m[1] : j.content).trim();
217
253
  if (text) msgs.push({ role: "user", text, at, line: i });
218
254
  } else if (j.type === "PLANNER_RESPONSE") {
219
255
  const text = j.content.trim();
@@ -261,6 +297,10 @@ function formatJson(value, f) {
261
297
  const text = JSON.stringify(value, null, f.indent).replace(/\n/g, f.eol);
262
298
  return f.finalNewline ? text + f.eol : text;
263
299
  }
300
+ function readJson(file) {
301
+ const f = readJsonFile(file);
302
+ return f.ok ? f.value : {};
303
+ }
264
304
  function entryFor(event, harness, cmd, o) {
265
305
  const matcher = o.matcherFor(event);
266
306
  return {
@@ -330,8 +370,7 @@ function uninstallClaudeShaped(file, events, opts = {}) {
330
370
  unlinkSync(file);
331
371
  return { file, changed: true, note: "removed (sessionpipe created it)" };
332
372
  }
333
- const next = formatJson(settings2, f);
334
- if (next !== f.raw) writeFileSync(file, next);
373
+ writeFileSync(file, formatJson(settings2, f));
335
374
  return { file, changed: true };
336
375
  }
337
376
  function installedClaudeShaped(file, harness, events, cmd, o) {
@@ -358,7 +397,7 @@ var HOOK_NAME = "sessionpipe";
358
397
  var AG_EVENTS = ["PreInvocation", "PostInvocation", "PostToolUse", "Stop"];
359
398
  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';
360
399
  var antigravityAnswer = (event) => event === "PreToolUse" ? AG_STALE_PRETOOL : "{}\n";
361
- var gemini = (env) => env.GEMINI_CLI_HOME || path3.join(HOME, ".gemini");
400
+ var gemini = (env) => env.GEMINI_CLI_HOME || path3.join(homeDir(), ".gemini");
362
401
  var hooksFile = (env) => path3.join(gemini(env), "config", "hooks.json");
363
402
  var projectsDir = (env) => path3.join(gemini(env), "config", "projects");
364
403
  var dataDirs = (env) => ["antigravity", "antigravity-cli", "antigravity-ide"].map((d) => path3.join(gemini(env), d));
@@ -424,7 +463,8 @@ var antigravity = {
424
463
  const event = input.argv[1] ?? "";
425
464
  let s = {};
426
465
  try {
427
- s = input.stdin ? JSON.parse(input.stdin) : {};
466
+ const j = input.stdin ? JSON.parse(input.stdin) : {};
467
+ if (j && typeof j === "object" && !Array.isArray(j)) s = j;
428
468
  } catch {
429
469
  }
430
470
  const id = s.conversationId ?? s.session_id ?? s.sessionId;
@@ -480,19 +520,16 @@ var antigravity = {
480
520
  }
481
521
  return readAntigravity(text.split("\n"), fromLine);
482
522
  },
483
- facts(session, transcript, hints, state) {
523
+ facts(session, transcript, hints) {
484
524
  if (!transcript) return {};
485
525
  return antigravityFacts(transcript, {
486
526
  conversation: session.id,
487
527
  workspaces: hints.workspaces ?? (session.cwd ? [session.cwd] : []),
488
- state,
489
528
  env: process.env
490
529
  });
491
530
  },
492
531
  backfill(sinceMs, env = process.env) {
493
- const rows = [];
494
- const noState = { get: () => ({}), save: () => {
495
- } };
532
+ const rows2 = [];
496
533
  for (const data of dataDirs(env)) {
497
534
  let ids = [];
498
535
  try {
@@ -506,13 +543,13 @@ var antigravity = {
506
543
  } catch {
507
544
  continue;
508
545
  }
509
- const facts = antigravityFacts(f, { conversation: id, workspaces: [], state: noState, env });
546
+ const facts = antigravityFacts(f, { conversation: id, workspaces: [], env });
510
547
  if (!facts.started_at || !facts.last_at) continue;
511
548
  const { started_at, last_at, first_ask: _fa, harness_version: _hv, ...rest } = facts;
512
- rows.push({ session: { id, ...rest }, started_at, last_at });
549
+ rows2.push({ session: { id, ...rest }, started_at, last_at });
513
550
  }
514
551
  }
515
- return rows;
552
+ return rows2;
516
553
  }
517
554
  };
518
555
  function prune(o) {
@@ -562,7 +599,7 @@ function agProjects(dir) {
562
599
  }
563
600
  const folders = [];
564
601
  for (const r of j?.projectResources?.resources ?? []) {
565
- const f = agPath(r?.folderUri);
602
+ const f = agPath(r?.folderUri ?? r?.gitFolder?.folderUri);
566
603
  if (f) folders.push(...agVariants(f));
567
604
  }
568
605
  if (folders.length) out.push({ id, folders });
@@ -616,7 +653,7 @@ function antigravityFacts(file, opts) {
616
653
  for (const j of H) {
617
654
  if (j.type !== "USER_INPUT" || typeof j.content !== "string") continue;
618
655
  const m = /<USER_REQUEST>([\s\S]*?)(?:<\/USER_REQUEST>|$)/.exec(j.content);
619
- const text = (m ? m[1] ?? "" : j.content).trim();
656
+ const text = (m ? m[1] : j.content).trim();
620
657
  const line = (text.split("\n").find((l) => l.trim()) ?? "").trim();
621
658
  if (!line || /^<[a-z_-]+>/i.test(line)) continue;
622
659
  out.title = line.length > 80 ? `${line.slice(0, 80).replace(/\s+\S*$/, "")}\u2026` : line;
@@ -664,10 +701,9 @@ function gitFacts(cwd) {
664
701
  return out;
665
702
  }
666
703
  function stripCredentials(url) {
667
- let u = url.replace(/\/\/[^/@]*@/, "//");
668
- const ssh = /^(?:ssh:\/\/)?git@([^:/]+)[:/](.+?)(?:\.git)?$/.exec(u);
669
- if (ssh) u = `${ssh[1]}/${ssh[2]}`;
670
- return u.replace(/^https?:\/\//, "").replace(/\.git$/, "");
704
+ const ssh = /^(?:ssh:\/\/)?git@([^:/]+)[:/](.+?)(?:\.git)?$/.exec(url);
705
+ if (ssh) return `${ssh[1]}/${ssh[2]}`;
706
+ return url.replace(/\/\/[^/@]*@/, "//").replace(/^https?:\/\//, "").replace(/\.git$/, "");
671
707
  }
672
708
 
673
709
  // ../core/src/adapters/claude-code.ts
@@ -698,7 +734,7 @@ var OPTS = {
698
734
  timeoutFor: (e) => e === "PermissionRequest" ? 130 : void 0
699
735
  };
700
736
  var DESKTOP_ID = /^local_[A-Za-z0-9-]{1,64}$/;
701
- function claudeDirs(env = process.env, home = env.HOME || HOME) {
737
+ function claudeDirs(env = process.env, home = env.HOME || homeDir()) {
702
738
  const out = [];
703
739
  const add = (d) => {
704
740
  if (!d) return;
@@ -739,11 +775,13 @@ var claudeCode = {
739
775
  const event = input.argv[1] ?? "";
740
776
  let s = {};
741
777
  try {
742
- s = input.stdin ? JSON.parse(input.stdin) : {};
778
+ const j = input.stdin ? JSON.parse(input.stdin) : {};
779
+ if (j && typeof j === "object" && !Array.isArray(j)) s = j;
743
780
  } catch {
744
781
  }
745
782
  const id = s.session_id ?? s.sessionId;
746
783
  if (!id) return null;
784
+ if (notClaudeCode(String(id), s)) return null;
747
785
  const session = { id: String(id), cwd: typeof s.cwd === "string" ? s.cwd : input.cwd };
748
786
  if (typeof s.model === "string") session.model = s.model;
749
787
  if (typeof s.agent_id === "string" && event.startsWith("Subagent") === false && s.agent_id)
@@ -862,6 +900,7 @@ var claudeCode = {
862
900
  return null;
863
901
  }
864
902
  const hints = {};
903
+ if (event === "UserPromptSubmit" && typeof s.prompt === "string") hints.prompt = s.prompt;
865
904
  const host = input.env.CLAUDE_CODE_HOST_SESSION_ID;
866
905
  if (host && DESKTOP_ID.test(host)) hints.desktop_id = host;
867
906
  if (event !== "PreToolUse" && event !== "PostToolUse") hints.claude_pid = claudePid(input.env);
@@ -887,7 +926,7 @@ var claudeCode = {
887
926
  return out;
888
927
  },
889
928
  backfill(sinceMs, env = process.env) {
890
- const rows = [];
929
+ const rows2 = [];
891
930
  const files = [];
892
931
  for (const d of claudeDirs(env))
893
932
  files.push(...recentFiles(path4.join(d, "projects"), 1, sinceMs, (n) => /^[0-9a-f-]{36}\.jsonl$/.test(n)));
@@ -899,19 +938,23 @@ var claudeCode = {
899
938
  if (!facts.started_at || !facts.last_at) continue;
900
939
  const { started_at, last_at, first_ask: _fa, harness_version: _hv, ...rest } = facts;
901
940
  const cwd = rest.cwd;
902
- rows.push({
941
+ rows2.push({
903
942
  session: { id, ...rest, ...gitFacts(cwd), ...rest.branch ? { branch: rest.branch } : {} },
904
943
  started_at,
905
944
  last_at
906
945
  });
907
946
  }
908
- return rows;
947
+ return rows2;
909
948
  }
910
949
  };
911
950
  function prune2(o) {
912
951
  for (const k of Object.keys(o)) if (o[k] === void 0) delete o[k];
913
952
  return o;
914
953
  }
954
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
955
+ function notClaudeCode(id, s) {
956
+ return typeof s.transcript_path !== "string" && !UUID.test(id);
957
+ }
915
958
  function pickSource(v) {
916
959
  return typeof v === "string" && ["startup", "resume", "clear", "compact", "fork"].includes(v) ? v : "unknown";
917
960
  }
@@ -1017,23 +1060,27 @@ function parentOf() {
1017
1060
  }
1018
1061
  return (pid) => table.get(pid) || 0;
1019
1062
  }
1020
- var accountCache = /* @__PURE__ */ new Map();
1021
- function claudeAccount(dir) {
1022
- const hit = accountCache.get(dir);
1023
- if (hit !== void 0) return hit;
1063
+ var EMAIL_RE = /^[^@\s]{1,64}@[^@\s]{1,189}$/;
1064
+ function claudeLogin(dir, home = homeDir()) {
1024
1065
  const files = [path4.join(dir, ".claude.json")];
1025
- if (path4.resolve(dir) === path4.join(HOME, ".claude")) files.unshift(path4.join(HOME, ".claude.json"));
1026
- let acct = null;
1066
+ if (path4.resolve(dir) === path4.join(home, ".claude")) files.unshift(path4.join(home, ".claude.json"));
1027
1067
  for (const f of files) {
1028
1068
  try {
1029
1069
  const a = JSON.parse(readFileSync3(f, "utf8")).oauthAccount;
1030
- if (a?.accountUuid) {
1031
- acct = String(a.accountUuid);
1032
- break;
1070
+ if (typeof a?.accountUuid === "string" && a.accountUuid) {
1071
+ const email = typeof a.emailAddress === "string" && EMAIL_RE.test(a.emailAddress) ? a.emailAddress : void 0;
1072
+ return { id: a.accountUuid.slice(0, 128), ...email ? { email } : {} };
1033
1073
  }
1034
1074
  } catch {
1035
1075
  }
1036
1076
  }
1077
+ return null;
1078
+ }
1079
+ var accountCache = /* @__PURE__ */ new Map();
1080
+ function claudeAccount(dir) {
1081
+ const hit = accountCache.get(dir);
1082
+ if (hit !== void 0) return hit;
1083
+ const acct = claudeLogin(dir)?.id ?? null;
1037
1084
  accountCache.set(dir, acct);
1038
1085
  return acct;
1039
1086
  }
@@ -1253,18 +1300,13 @@ function parseArgv(raw) {
1253
1300
  function readNotify(text) {
1254
1301
  const { head, tail, at } = split(text);
1255
1302
  const m = NOTIFY_LINE.exec(head);
1256
- if (m)
1257
- return {
1258
- raw: m[1] ?? "",
1259
- argv: parseArgv(m[1] ?? ""),
1260
- index: m.index,
1261
- length: m[0].length,
1262
- misplaced: false,
1263
- other: !OURS2.test(m[1] ?? "")
1264
- };
1303
+ if (m) {
1304
+ const raw = m[1];
1305
+ return { raw, argv: parseArgv(raw), index: m.index, length: m[0].length, misplaced: false, other: !OURS2.test(raw) };
1306
+ }
1265
1307
  const k = NOTIFY_KEY.exec(head);
1266
1308
  if (k) {
1267
- const rest = head.slice(k.index).split("\n")[0] ?? "";
1309
+ const rest = head.slice(k.index).split("\n")[0];
1268
1310
  return {
1269
1311
  raw: rest.replace(NOTIFY_KEY, "").trim(),
1270
1312
  argv: null,
@@ -1275,15 +1317,11 @@ function readNotify(text) {
1275
1317
  };
1276
1318
  }
1277
1319
  const t = NOTIFY_LINE.exec(tail);
1278
- if (t && OURS2.test(t[1] ?? ""))
1279
- return {
1280
- raw: t[1] ?? "",
1281
- argv: parseArgv(t[1] ?? ""),
1282
- index: at + t.index,
1283
- length: t[0].length,
1284
- misplaced: true,
1285
- other: false
1286
- };
1320
+ if (t) {
1321
+ const raw = t[1];
1322
+ if (OURS2.test(raw))
1323
+ return { raw, argv: parseArgv(raw), index: at + t.index, length: t[0].length, misplaced: true, other: false };
1324
+ }
1287
1325
  return null;
1288
1326
  }
1289
1327
  function dropLine(text, index, length) {
@@ -1302,9 +1340,7 @@ function withNotify(text, line) {
1302
1340
  if (n?.misplaced) return insertTopLevel(dropLine(text, n.index, n.length), line);
1303
1341
  return insertTopLevel(text, line);
1304
1342
  }
1305
- function replaceNotify(text, line) {
1306
- const n = readNotify(text);
1307
- if (!n || n.misplaced || !n.length) return null;
1343
+ function replaceNotify(text, n, line) {
1308
1344
  return text.slice(0, n.index) + line + text.slice(n.index + n.length);
1309
1345
  }
1310
1346
  function withoutNotify(text) {
@@ -1345,7 +1381,7 @@ var CODEX_EVENTS = [
1345
1381
  ];
1346
1382
  var OPTS2 = { matcherFor: (e) => /Tool|Permission/.test(e) ? "" : void 0, timeout: 5 };
1347
1383
  var eventsFor2 = (lean) => lean ? CODEX_EVENTS.filter((e) => e !== "PreToolUse") : [...CODEX_EVENTS];
1348
- var codexHome = (env) => env.CODEX_HOME || path5.join(HOME, ".codex");
1384
+ var codexHome = (env) => env.CODEX_HOME || path5.join(homeDir(), ".codex");
1349
1385
  var hooksFile2 = (env) => path5.join(codexHome(env), "hooks.json");
1350
1386
  var configFile = (env) => path5.join(codexHome(env), "config.toml");
1351
1387
  var sessionsDir = (env) => path5.join(codexHome(env), "sessions");
@@ -1383,11 +1419,8 @@ function installNotify(cmd, env) {
1383
1419
  return { file, changed: false, note: "notify current (after Codex Computer Use)" };
1384
1420
  }
1385
1421
  if (!prev || OURS2.test(JSON.stringify(prev))) {
1386
- const next = replaceNotify(text, `notify = ${withPreviousRaw(n.raw, argv)}`);
1387
- if (next) {
1388
- write(next);
1389
- return { file, changed: true, note: "chained after Codex Computer Use (--previous-notify)" };
1390
- }
1422
+ write(replaceNotify(text, n, `notify = ${withPreviousRaw(n.raw, argv)}`));
1423
+ return { file, changed: true, note: "chained after Codex Computer Use (--previous-notify)" };
1391
1424
  }
1392
1425
  }
1393
1426
  return {
@@ -1416,8 +1449,7 @@ function uninstallNotify(env) {
1416
1449
  }
1417
1450
  const prev = previousNotify(n.argv);
1418
1451
  if (isComputerUse(n.argv) && prev && OURS2.test(JSON.stringify(prev))) {
1419
- const next = replaceNotify(text, `notify = ${stripPreviousRaw(n.raw)}`);
1420
- if (next) writeFileSync3(file, next);
1452
+ writeFileSync3(file, replaceNotify(text, n, `notify = ${stripPreviousRaw(n.raw)}`));
1421
1453
  return { file, changed: true };
1422
1454
  }
1423
1455
  return { file, changed: false };
@@ -1447,6 +1479,13 @@ function findRollout(threadId, env = process.env) {
1447
1479
  walk(sessionsDir(env), 0);
1448
1480
  return best;
1449
1481
  }
1482
+ function rolloutMeta(file) {
1483
+ return scanLines(file, (line) => parseLines([line])[0] ?? {});
1484
+ }
1485
+ function ofAnotherThread(file, id) {
1486
+ 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);
1487
+ return !!m?.[1] && m[1].toLowerCase() !== id.toLowerCase();
1488
+ }
1450
1489
  var codex = {
1451
1490
  name: NAME3,
1452
1491
  events: CODEX_EVENTS,
@@ -1489,11 +1528,14 @@ var codex = {
1489
1528
  if (event === "notify") {
1490
1529
  const raw = input.argv.find((a) => a.startsWith("{")) ?? input.stdin;
1491
1530
  try {
1492
- s = raw ? JSON.parse(raw) : {};
1531
+ const j = raw ? JSON.parse(raw) : {};
1532
+ if (j && typeof j === "object" && !Array.isArray(j)) s = j;
1493
1533
  } catch {
1494
1534
  }
1495
1535
  const id2 = s["thread-id"] ?? s.thread_id ?? s["session-id"] ?? s.session_id;
1496
1536
  if (!id2) return null;
1537
+ const file = findRollout(String(id2), input.env);
1538
+ if (!file || codexSubagent(rolloutMeta(file))) return null;
1497
1539
  const r2 = {
1498
1540
  session: { id: String(id2), cwd: typeof s.cwd === "string" ? s.cwd : input.cwd },
1499
1541
  events: [
@@ -1503,18 +1545,19 @@ var codex = {
1503
1545
  harnessEvent: "notify"
1504
1546
  }
1505
1547
  ],
1506
- hints: {}
1548
+ hints: {},
1549
+ transcript: file
1507
1550
  };
1508
- const file = findRollout(String(id2), input.env);
1509
- if (file) r2.transcript = file;
1510
1551
  return r2;
1511
1552
  }
1512
1553
  try {
1513
- s = input.stdin ? JSON.parse(input.stdin) : {};
1554
+ const j = input.stdin ? JSON.parse(input.stdin) : {};
1555
+ if (j && typeof j === "object" && !Array.isArray(j)) s = j;
1514
1556
  } catch {
1515
1557
  }
1516
1558
  const id = s.session_id;
1517
1559
  if (!id) return null;
1560
+ if (s.transcript_path === null) return null;
1518
1561
  const session = { id: String(id), cwd: typeof s.cwd === "string" ? s.cwd : input.cwd };
1519
1562
  if (typeof s.model === "string") session.model = s.model;
1520
1563
  const turn_id = typeof s.turn_id === "string" ? s.turn_id : void 0;
@@ -1586,7 +1629,8 @@ var codex = {
1586
1629
  return null;
1587
1630
  }
1588
1631
  const r = { session, events, hints: {} };
1589
- if (typeof s.transcript_path === "string") r.transcript = s.transcript_path;
1632
+ if (typeof s.transcript_path === "string" && !ofAnotherThread(s.transcript_path, String(id)))
1633
+ r.transcript = s.transcript_path;
1590
1634
  return r;
1591
1635
  },
1592
1636
  readTranscript(file, fromLine) {
@@ -1604,16 +1648,17 @@ var codex = {
1604
1648
  return out;
1605
1649
  },
1606
1650
  backfill(sinceMs, env = process.env) {
1607
- const rows = [];
1651
+ const rows2 = [];
1608
1652
  for (const f of recentFiles(sessionsDir(env), 3, sinceMs, (n) => /^rollout-.*\.jsonl$/.test(n))) {
1609
1653
  const m = /([0-9a-f]{8}-[0-9a-f-]{27,})\.jsonl$/i.exec(path5.basename(f));
1610
1654
  if (!m?.[1]) continue;
1655
+ if (codexSubagent(rolloutMeta(f))) continue;
1611
1656
  const facts = codexFacts(f);
1612
1657
  if (!facts.started_at || !facts.last_at) continue;
1613
1658
  const { started_at, last_at, first_ask: _fa, harness_version: _hv, ...rest } = facts;
1614
- rows.push({ session: { id: m[1], ...rest }, started_at, last_at });
1659
+ rows2.push({ session: { id: m[1], ...rest }, started_at, last_at });
1615
1660
  }
1616
- return rows;
1661
+ return rows2;
1617
1662
  }
1618
1663
  };
1619
1664
  function prune3(o) {
@@ -1622,10 +1667,11 @@ function prune3(o) {
1622
1667
  }
1623
1668
  function codexFacts(file) {
1624
1669
  const { head, tail } = ends(file, 128 * 1024, 64 * 1024);
1625
- const H = parseLines(head);
1626
- const T = parseLines(tail);
1670
+ let H = parseLines(head);
1671
+ let T = parseLines(tail);
1627
1672
  const out = {};
1628
1673
  const meta = H.find((j) => j.type === "session_meta" && j.payload);
1674
+ const boundary = codexReplayBoundary(meta);
1629
1675
  if (meta?.payload) {
1630
1676
  const p = meta.payload;
1631
1677
  if (p.cwd) out.cwd = p.cwd;
@@ -1633,6 +1679,13 @@ function codexFacts(file) {
1633
1679
  if (p.git?.branch) out.branch = p.git.branch;
1634
1680
  if (p.git?.repository_url) out.repo = String(p.git.repository_url).replace(/\/\/[^/@]*@/, "//");
1635
1681
  }
1682
+ if (boundary) {
1683
+ const h = H.findIndex(boundary);
1684
+ const t = T.findIndex(boundary);
1685
+ H = h < 0 ? [] : H.slice(h);
1686
+ const found = h >= 0 || t >= 0 || scanLines(file, (line) => boundary(parseLines([line])[0] ?? null) ? true : null, Number.POSITIVE_INFINITY);
1687
+ T = t >= 0 ? T.slice(t) : found ? T : [];
1688
+ }
1636
1689
  const ctx = H.concat(T).reverse().find((j) => j.type === "turn_context" && j.payload?.model);
1637
1690
  if (ctx?.payload?.model) out.model = ctx.payload.model;
1638
1691
  for (const j of H) {
@@ -1640,12 +1693,12 @@ function codexFacts(file) {
1640
1693
  if (j.type !== "response_item" || !p || p.type !== "message" || p.role !== "user") continue;
1641
1694
  const text = (p.content ?? []).filter((b) => b && b.type === "input_text" && typeof b.text === "string").map((b) => b.text).join("\n").trim();
1642
1695
  if (!text || codexInjected(text)) continue;
1643
- const line = text.split("\n").find((l) => l.trim()) ?? "";
1696
+ const line = text.split("\n")[0];
1644
1697
  out.title = line.length > 80 ? `${line.slice(0, 80).replace(/\s+\S*$/, "")}\u2026` : line;
1645
1698
  out.title_source = "first-ask";
1646
1699
  break;
1647
1700
  }
1648
- const times = H.concat(T).map((j) => Date.parse(String(j.timestamp ?? ""))).filter((n) => n > 0);
1701
+ const times = (boundary && meta ? [meta, ...H, ...T] : H.concat(T)).map((j) => Date.parse(String(j.timestamp ?? ""))).filter((n) => n > 0);
1649
1702
  if (times.length) {
1650
1703
  out.started_at = Math.min(...times);
1651
1704
  out.last_at = Math.max(...times);
@@ -1653,11 +1706,432 @@ function codexFacts(file) {
1653
1706
  return out;
1654
1707
  }
1655
1708
 
1656
- // ../core/src/adapters/gemini-cli.ts
1657
- import { execFileSync as execFileSync3 } from "node:child_process";
1658
- import { existsSync as existsSync5, readFileSync as readFileSync5 } from "node:fs";
1709
+ // ../core/src/adapters/devin.ts
1710
+ import { createHash as createHash2 } from "node:crypto";
1711
+ import { existsSync as existsSync6, readlinkSync } from "node:fs";
1659
1712
  import path6 from "node:path";
1660
- var NAME4 = "gemini-cli";
1713
+
1714
+ // ../core/src/readers/sqlite.ts
1715
+ import { execFileSync as execFileSync3 } from "node:child_process";
1716
+ import { existsSync as existsSync5 } from "node:fs";
1717
+ import { createRequire } from "node:module";
1718
+ function quiet(fn) {
1719
+ const original = process.emitWarning;
1720
+ try {
1721
+ process.emitWarning = ((warning, ...rest) => {
1722
+ const kind = typeof rest[0] === "string" ? rest[0] : rest[0]?.type;
1723
+ if (kind === "ExperimentalWarning" && /sqlite/i.test(String(warning))) return;
1724
+ original.call(process, warning, ...rest);
1725
+ });
1726
+ return fn();
1727
+ } finally {
1728
+ process.emitWarning = original;
1729
+ }
1730
+ }
1731
+ var ctorCache;
1732
+ function dbCtor() {
1733
+ if (ctorCache !== void 0) return ctorCache;
1734
+ ctorCache = null;
1735
+ try {
1736
+ const req = createRequire(import.meta.url);
1737
+ const m = quiet(() => req("node:sqlite"));
1738
+ if (typeof m?.DatabaseSync === "function") ctorCache = m.DatabaseSync;
1739
+ } catch {
1740
+ }
1741
+ return ctorCache;
1742
+ }
1743
+ function plain(row) {
1744
+ if (!row || typeof row !== "object") return null;
1745
+ const out = {};
1746
+ for (const [k, v] of Object.entries(row)) {
1747
+ if (typeof v === "bigint") out[k] = Number(v);
1748
+ else if (v instanceof Uint8Array) out[k] = null;
1749
+ else out[k] = v;
1750
+ }
1751
+ return out;
1752
+ }
1753
+ var rows = (v) => {
1754
+ if (!Array.isArray(v)) return null;
1755
+ const out = [];
1756
+ for (const r of v) {
1757
+ const p = plain(r);
1758
+ if (p) out.push(p);
1759
+ }
1760
+ return out;
1761
+ };
1762
+ function viaNode(file, sql, params, o) {
1763
+ const C = dbCtor();
1764
+ if (!C) return null;
1765
+ let db;
1766
+ try {
1767
+ db = quiet(() => new C(file, { readOnly: true, timeout: o.timeoutMs }));
1768
+ } catch {
1769
+ return null;
1770
+ }
1771
+ try {
1772
+ try {
1773
+ db.exec(`PRAGMA busy_timeout = ${Math.round(o.timeoutMs)}`);
1774
+ } catch {
1775
+ }
1776
+ return quiet(() => rows(db.prepare(sql).all(...params)));
1777
+ } catch {
1778
+ return null;
1779
+ } finally {
1780
+ try {
1781
+ db.close();
1782
+ } catch {
1783
+ }
1784
+ }
1785
+ }
1786
+ var CHILD = `const { DatabaseSync } = require("node:sqlite");
1787
+ const [, file, sql, params, ms] = process.argv;
1788
+ const db = new DatabaseSync(file, { readOnly: true, timeout: Number(ms) });
1789
+ try { db.exec("PRAGMA busy_timeout = " + Number(ms)); } catch {}
1790
+ const out = db.prepare(sql).all(...JSON.parse(params));
1791
+ db.close();
1792
+ process.stdout.write(JSON.stringify(out));`;
1793
+ function viaChild(file, sql, params, o) {
1794
+ try {
1795
+ const out = execFileSync3(
1796
+ process.execPath,
1797
+ ["--experimental-sqlite", "--no-warnings", "-e", CHILD, file, sql, JSON.stringify(params), String(o.timeoutMs)],
1798
+ {
1799
+ encoding: "utf8",
1800
+ timeout: o.timeoutMs + 3e3,
1801
+ stdio: ["ignore", "pipe", "ignore"],
1802
+ maxBuffer: o.maxBuffer,
1803
+ windowsHide: true
1804
+ }
1805
+ );
1806
+ return rows(JSON.parse(out || "null"));
1807
+ } catch {
1808
+ return null;
1809
+ }
1810
+ }
1811
+ var SAFE = /^[A-Za-z0-9 ._:@/+-]{0,256}$/;
1812
+ function inlineParams(sql, params) {
1813
+ if (sql.includes("'")) return null;
1814
+ const holes = sql.split("?").length - 1;
1815
+ if (holes !== params.length) return null;
1816
+ let i = 0;
1817
+ let bad = false;
1818
+ const out = sql.replace(/\?/g, () => {
1819
+ const p = params[i++];
1820
+ if (typeof p === "number") {
1821
+ if (!Number.isFinite(p)) {
1822
+ bad = true;
1823
+ return "NULL";
1824
+ }
1825
+ return String(p);
1826
+ }
1827
+ if (typeof p !== "string" || !SAFE.test(p)) {
1828
+ bad = true;
1829
+ return "NULL";
1830
+ }
1831
+ return `'${p}'`;
1832
+ });
1833
+ return bad ? null : out;
1834
+ }
1835
+ function viaCli(file, sql, params, o) {
1836
+ const text = inlineParams(sql, params);
1837
+ if (!text) return null;
1838
+ try {
1839
+ const out = execFileSync3("sqlite3", ["-readonly", "-json", file, text], {
1840
+ encoding: "utf8",
1841
+ timeout: o.timeoutMs + 3e3,
1842
+ stdio: ["ignore", "pipe", "ignore"],
1843
+ maxBuffer: o.maxBuffer,
1844
+ windowsHide: true
1845
+ }).trim();
1846
+ if (!out) return [];
1847
+ return rows(JSON.parse(out));
1848
+ } catch {
1849
+ return null;
1850
+ }
1851
+ }
1852
+ function querySqlite(file, sql, params = [], opts = {}) {
1853
+ const o = {
1854
+ timeoutMs: opts.timeoutMs ?? 2e3,
1855
+ engine: opts.engine ?? "auto",
1856
+ maxBuffer: opts.maxBuffer ?? 32 * 1024 * 1024
1857
+ };
1858
+ if (!file) return null;
1859
+ try {
1860
+ if (!existsSync5(file)) return null;
1861
+ } catch {
1862
+ return null;
1863
+ }
1864
+ const ways = o.engine === "auto" ? [viaNode, viaChild, viaCli] : o.engine === "node" ? [viaNode] : o.engine === "child" ? [viaChild] : [viaCli];
1865
+ for (const way of ways) {
1866
+ const r = way(file, sql, params, o);
1867
+ if (r) return r;
1868
+ }
1869
+ return null;
1870
+ }
1871
+
1872
+ // ../core/src/adapters/devin.ts
1873
+ var NAME4 = "devin";
1874
+ var DEVIN_EVENTS = [
1875
+ "SessionStart",
1876
+ "UserPromptSubmit",
1877
+ "PreToolUse",
1878
+ "PostToolUse",
1879
+ "PermissionRequest",
1880
+ "Stop",
1881
+ "PostCompaction",
1882
+ "SessionEnd"
1883
+ ];
1884
+ var OPTS3 = { matcherFor: (e) => /Tool|Permission/.test(e) ? "" : void 0, timeout: 5 };
1885
+ var eventsFor3 = (lean) => lean ? DEVIN_EVENTS.filter((e) => e !== "PreToolUse") : [...DEVIN_EVENTS];
1886
+ var INSTALL_NOTE = "Devin drops a whole hooks file on one event name it doesn't know; `/hooks` in `devin` lists what loaded";
1887
+ 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";
1888
+ 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)`;
1889
+ function foreignEvent(file) {
1890
+ const hooks = readJson(file).hooks;
1891
+ if (!hooks || typeof hooks !== "object" || Array.isArray(hooks)) return void 0;
1892
+ const ok = DEVIN_EVENTS;
1893
+ return Object.keys(hooks).find((k) => !ok.includes(k));
1894
+ }
1895
+ var homeOf = (env) => env.HOME || homeDir();
1896
+ var appData = (env) => env.APPDATA || path6.join(homeOf(env), "AppData", "Roaming");
1897
+ 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");
1898
+ var configFile2 = (env) => path6.join(devinConfigDir(env), "config.json");
1899
+ 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");
1900
+ var sessionsDb = (env) => path6.join(devinDataDir(env), "sessions.db");
1901
+ var SESSION_ID = /^[A-Za-z0-9._:-]{1,128}$/;
1902
+ var validTool4 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
1903
+ var endReason3 = (r) => r === "clear" || r === "logout" || r === "exit" ? r : "other";
1904
+ var pickSource2 = (v) => typeof v === "string" && ["startup", "resume", "clear", "compact", "fork"].includes(v) ? v : "unknown";
1905
+ function errText2(v) {
1906
+ if (typeof v === "string" && v) return v.slice(0, 2e3);
1907
+ if (v && typeof v === "object") {
1908
+ const e = v;
1909
+ if (typeof e.message === "string" && e.message) return e.message.slice(0, 2e3);
1910
+ if (typeof e.error === "string" && e.error) return e.error.slice(0, 2e3);
1911
+ }
1912
+ return void 0;
1913
+ }
1914
+ var cut = (s, n) => s.length > n ? `${s.slice(0, n).replace(/\s+\S*$/, "")}\u2026` : s;
1915
+ var devin = {
1916
+ name: NAME4,
1917
+ events: DEVIN_EVENTS,
1918
+ detect: (env = process.env) => existsSync6(devinConfigDir(env)) || existsSync6(devinDataDir(env)),
1919
+ configFiles: (env = process.env) => [configFile2(env)],
1920
+ install: (cmd, env = process.env, opts = {}) => {
1921
+ const file = configFile2(env);
1922
+ const r = installClaudeShaped(file, NAME4, eventsFor3(opts.lean), cmd, OPTS3);
1923
+ if (r.skipped) return [r];
1924
+ const bad = foreignEvent(file);
1925
+ return [{ ...r, note: bad ? deadFileNote(bad) : INSTALL_NOTE }];
1926
+ },
1927
+ uninstall: (env = process.env, opts = {}) => [
1928
+ uninstallClaudeShaped(configFile2(env), DEVIN_EVENTS, { created: opts.created?.includes(configFile2(env)) })
1929
+ ],
1930
+ installed: (cmd, env = process.env, opts = {}) => {
1931
+ const file = configFile2(env);
1932
+ const state = installedClaudeShaped(file, NAME4, eventsFor3(opts.lean), cmd, OPTS3);
1933
+ const bad = state === "missing" ? void 0 : foreignEvent(file);
1934
+ return [{ file, state: bad ? "inactive" : state, note: bad ? deadFileNote(bad) : CLAUDE_NOTE }];
1935
+ },
1936
+ fromHook(input) {
1937
+ const event = input.argv[1] ?? "";
1938
+ let s = {};
1939
+ try {
1940
+ const j = input.stdin ? JSON.parse(input.stdin) : {};
1941
+ if (j && typeof j === "object" && !Array.isArray(j)) s = j;
1942
+ } catch {
1943
+ }
1944
+ const id = typeof s.session_id === "string" ? s.session_id : "";
1945
+ if (!id) return null;
1946
+ const session = { id, cwd: input.env.DEVIN_PROJECT_DIR ?? input.cwd };
1947
+ const turn_id = typeof s.prompt_id === "string" ? s.prompt_id : void 0;
1948
+ const tool = validTool4(s.tool_name) ? s.tool_name : void 0;
1949
+ const call_id = typeof s.tool_use_id === "string" ? s.tool_use_id : void 0;
1950
+ const response = s.tool_response && typeof s.tool_response === "object" && !Array.isArray(s.tool_response) ? s.tool_response : void 0;
1951
+ const permissionId = () => `perm-${createHash2("sha256").update(`${turn_id ?? ""}|${tool ?? ""}|${JSON.stringify(s.tool_input ?? null)}`).digest("hex").slice(0, 16)}`;
1952
+ const ev = (type, data) => ({ type, data: prune4(data), harnessEvent: event });
1953
+ const events = [];
1954
+ switch (event) {
1955
+ case "SessionStart":
1956
+ events.push(ev("session.started", { source: pickSource2(s.source) }));
1957
+ break;
1958
+ case "UserPromptSubmit":
1959
+ events.push(
1960
+ ev("turn.started", {
1961
+ turn_id,
1962
+ prompt_chars: typeof s.prompt === "string" ? [...s.prompt].length : void 0
1963
+ })
1964
+ );
1965
+ break;
1966
+ case "PreToolUse":
1967
+ if (tool) events.push(ev("tool.started", { tool, call_id, turn_id, input: s.tool_input }));
1968
+ break;
1969
+ case "PostToolUse":
1970
+ if (tool)
1971
+ events.push(
1972
+ ev("tool.ended", {
1973
+ tool,
1974
+ call_id,
1975
+ turn_id,
1976
+ // `ok` is required on tool.ended (PROTOCOL.md §events), so a response
1977
+ // that carries no success flag at all reads as "nothing said it failed".
1978
+ ok: response?.success !== false,
1979
+ error: errText2(response?.error),
1980
+ input: s.tool_input,
1981
+ output: response ? response.output : s.tool_response
1982
+ })
1983
+ );
1984
+ break;
1985
+ case "PermissionRequest":
1986
+ events.push(ev("attention.needed", { attention_id: call_id ?? permissionId(), kind: "permission", tool }));
1987
+ break;
1988
+ case "Stop":
1989
+ events.push(ev("turn.ended", { turn_id, reason: "stop" }));
1990
+ break;
1991
+ case "PostCompaction":
1992
+ events.push(ev("context.compacted", { trigger: "auto" }));
1993
+ break;
1994
+ case "SessionEnd":
1995
+ events.push(ev("session.ended", { reason: endReason3(s.reason) }));
1996
+ break;
1997
+ default:
1998
+ return null;
1999
+ }
2000
+ return {
2001
+ session,
2002
+ events,
2003
+ hints: {},
2004
+ transcript: transcriptRef(sessionsDb(input.env), id)
2005
+ };
2006
+ },
2007
+ readTranscript(file, fromLine) {
2008
+ const ref = splitRef(file);
2009
+ if (!ref) return { turns: [], line: fromLine };
2010
+ return readDevinChain(ref.db, ref.session, fromLine);
2011
+ },
2012
+ facts(session, transcript, _hints, _state, env = process.env) {
2013
+ const out = {};
2014
+ const version = devinVersion(env);
2015
+ if (version) out.harness_version = version;
2016
+ const account = devinAccount(env);
2017
+ if (account) out.account_id = account;
2018
+ const ref = splitRef(transcript) ?? { db: sessionsDb(env), session: session.id };
2019
+ if (!SESSION_ID.test(ref.session)) return out;
2020
+ const rows2 = querySqlite(ref.db, SESSION_SQL, [ref.session]);
2021
+ const row = rows2?.[0];
2022
+ if (!row) return out;
2023
+ Object.assign(out, sessionFacts(row));
2024
+ return out;
2025
+ },
2026
+ backfill(sinceMs, env = process.env) {
2027
+ const rows2 = querySqlite(sessionsDb(env), BACKFILL_SQL, ["text", Math.floor(sinceMs / 1e3)]);
2028
+ if (!rows2) return [];
2029
+ const out = [];
2030
+ for (const row of rows2) {
2031
+ const id = String(row.id);
2032
+ if (!SESSION_ID.test(id)) continue;
2033
+ const { started_at, last_at, ...rest } = sessionFacts(row);
2034
+ if (!started_at || !last_at) continue;
2035
+ out.push({ session: { id, ...rest }, started_at, last_at });
2036
+ }
2037
+ return out;
2038
+ }
2039
+ };
2040
+ function prune4(o) {
2041
+ for (const k of Object.keys(o)) if (o[k] === void 0) delete o[k];
2042
+ return o;
2043
+ }
2044
+ var transcriptRef = (db, session) => `${db}#${session}`;
2045
+ function splitRef(ref) {
2046
+ if (!ref) return null;
2047
+ const at = ref.lastIndexOf("#");
2048
+ if (at <= 0) return null;
2049
+ const db = ref.slice(0, at);
2050
+ const session = ref.slice(at + 1);
2051
+ return db && SESSION_ID.test(session) ? { db, session } : null;
2052
+ }
2053
+ var CHAIN_LIMIT = 4e3;
2054
+ var CHAIN_SQL = `WITH RECURSIVE chain(node_id, parent_node_id, chat_message, created_at) AS (
2055
+ SELECT node_id, parent_node_id, chat_message, created_at FROM message_nodes
2056
+ WHERE session_id = ? AND node_id = (SELECT main_chain_id FROM sessions WHERE id = ?)
2057
+ UNION ALL
2058
+ SELECT m.node_id, m.parent_node_id, m.chat_message, m.created_at FROM message_nodes m
2059
+ JOIN chain c ON m.node_id = c.parent_node_id AND m.session_id = ? AND m.node_id >= ?
2060
+ )
2061
+ SELECT node_id, chat_message, created_at FROM chain ORDER BY node_id DESC LIMIT ${CHAIN_LIMIT}`;
2062
+ var SESSION_FIELDS = `s.model, s.working_directory, s.created_at, s.last_activity_at,
2063
+ (SELECT p.content FROM prompt_history p WHERE p.session_id = s.id AND p.is_shell = 0
2064
+ ORDER BY p.timestamp ASC, p.id ASC LIMIT 1) AS first_ask`;
2065
+ var SESSION_SQL = `SELECT ${SESSION_FIELDS} FROM sessions s WHERE s.id = ? LIMIT 1`;
2066
+ var BACKFILL_SQL = `SELECT s.id, ${SESSION_FIELDS} FROM sessions s
2067
+ WHERE s.hidden = 0 AND typeof(s.id) = ? AND s.last_activity_at >= ?
2068
+ ORDER BY s.last_activity_at DESC LIMIT 500`;
2069
+ function sessionFacts(row) {
2070
+ const out = {};
2071
+ if (typeof row.model === "string" && row.model) out.model = row.model;
2072
+ if (typeof row.working_directory === "string" && path6.isAbsolute(row.working_directory))
2073
+ out.cwd = row.working_directory;
2074
+ const started = Number(row.created_at);
2075
+ const last = Number(row.last_activity_at);
2076
+ if (Number.isFinite(started) && started > 0) out.started_at = Math.round(started * 1e3);
2077
+ if (Number.isFinite(last) && last > 0) out.last_at = Math.round(last * 1e3);
2078
+ if (typeof row.first_ask === "string") {
2079
+ const line = row.first_ask.split("\n").find((l) => l.trim()) ?? "";
2080
+ if (line.trim()) {
2081
+ out.title = cut(line.trim(), 80);
2082
+ out.title_source = "first-ask";
2083
+ }
2084
+ }
2085
+ return out;
2086
+ }
2087
+ function readDevinChain(db, session, fromLine) {
2088
+ if (!SESSION_ID.test(session)) return { turns: [], line: fromLine };
2089
+ const floor = Number.isFinite(fromLine) && fromLine > 0 ? Math.floor(fromLine) : 0;
2090
+ const rows2 = querySqlite(db, CHAIN_SQL, [session, session, session, floor]);
2091
+ if (!rows2?.length) return { turns: [], line: fromLine };
2092
+ const msgs = [];
2093
+ let end = fromLine;
2094
+ for (const row of rows2) {
2095
+ const node = Number(row.node_id);
2096
+ if (!Number.isFinite(node)) continue;
2097
+ if (node + 1 > end) end = node + 1;
2098
+ if (node < fromLine) continue;
2099
+ let j;
2100
+ try {
2101
+ j = JSON.parse(String(row.chat_message ?? ""));
2102
+ } catch {
2103
+ continue;
2104
+ }
2105
+ const text = typeof j.content === "string" ? j.content.trim() : "";
2106
+ if (!text) continue;
2107
+ const secs = Number(row.created_at);
2108
+ const at = Date.parse(String(j.metadata?.created_at ?? "")) || (Number.isFinite(secs) && secs > 0 ? Math.round(secs * 1e3) : 0) || Date.now();
2109
+ if (j.role === "user" && j.metadata?.is_user_input === true) msgs.push({ role: "user", text, at, line: node });
2110
+ else if (j.role === "assistant") msgs.push({ role: "assistant", text, at, line: node });
2111
+ }
2112
+ msgs.sort((a, b) => a.line - b.line);
2113
+ return pairTurns(msgs, end);
2114
+ }
2115
+ function devinVersion(env = process.env) {
2116
+ try {
2117
+ const target = readlinkSync(path6.join(devinDataDir(env), "_versions", "current"));
2118
+ const name = path6.basename(target);
2119
+ return /^[0-9][0-9A-Za-z.+-]{0,63}$/.test(name) ? name : void 0;
2120
+ } catch {
2121
+ return void 0;
2122
+ }
2123
+ }
2124
+ function devinAccount(env = process.env) {
2125
+ const d = readJson(configFile2(env)).devin;
2126
+ const id = d && typeof d === "object" ? d.org_id : void 0;
2127
+ return typeof id === "string" && /^[A-Za-z0-9_.:-]{1,128}$/.test(id) ? id : void 0;
2128
+ }
2129
+
2130
+ // ../core/src/adapters/gemini-cli.ts
2131
+ import { execFileSync as execFileSync4 } from "node:child_process";
2132
+ import { existsSync as existsSync7, readFileSync as readFileSync5 } from "node:fs";
2133
+ import path7 from "node:path";
2134
+ var NAME5 = "gemini-cli";
1661
2135
  var GEMINI_EVENTS = [
1662
2136
  "SessionStart",
1663
2137
  "BeforeAgent",
@@ -1668,11 +2142,11 @@ var GEMINI_EVENTS = [
1668
2142
  "PreCompress",
1669
2143
  "SessionEnd"
1670
2144
  ];
1671
- var OPTS3 = { matcherFor: (e) => /Tool/.test(e) ? ".*" : void 0, timeout: 5e3 };
1672
- var geminiDir = (env) => env.GEMINI_CLI_HOME || path6.join(HOME, ".gemini");
1673
- var settings = (env) => path6.join(geminiDir(env), "settings.json");
1674
- var validTool4 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
1675
- var eventsFor3 = (lean) => lean ? GEMINI_EVENTS.filter((e) => e !== "BeforeTool") : [...GEMINI_EVENTS];
2145
+ var OPTS4 = { matcherFor: (e) => /Tool/.test(e) ? ".*" : void 0, timeout: 5e3 };
2146
+ var geminiDir = (env) => env.GEMINI_CLI_HOME || path7.join(homeDir(), ".gemini");
2147
+ var settings = (env) => path7.join(geminiDir(env), "settings.json");
2148
+ var validTool5 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
2149
+ var eventsFor4 = (lean) => lean ? GEMINI_EVENTS.filter((e) => e !== "BeforeTool") : [...GEMINI_EVENTS];
1676
2150
  function geminiHooksActive(settingsFile) {
1677
2151
  let tools;
1678
2152
  try {
@@ -1684,7 +2158,7 @@ function geminiHooksActive(settingsFile) {
1684
2158
  return { active: false, note: "inactive: tools.enableHooks is false in settings.json" };
1685
2159
  let version = "";
1686
2160
  try {
1687
- version = execFileSync3("gemini", ["--version"], {
2161
+ version = execFileSync4("gemini", ["--version"], {
1688
2162
  encoding: "utf8",
1689
2163
  timeout: 4e3,
1690
2164
  stdio: ["ignore", "pipe", "ignore"]
@@ -1703,13 +2177,13 @@ function geminiHooksActive(settingsFile) {
1703
2177
  };
1704
2178
  }
1705
2179
  var geminiCli = {
1706
- name: NAME4,
2180
+ name: NAME5,
1707
2181
  events: GEMINI_EVENTS,
1708
2182
  // ~/.gemini also belongs to Antigravity; Gemini CLI is here when its settings or its chat logs exist.
1709
- detect: (env = process.env) => existsSync5(settings(env)) || existsSync5(path6.join(geminiDir(env), "tmp")),
2183
+ detect: (env = process.env) => existsSync7(settings(env)) || existsSync7(path7.join(geminiDir(env), "tmp")),
1710
2184
  configFiles: (env = process.env) => [settings(env)],
1711
2185
  install: (cmd, env = process.env, opts = {}) => {
1712
- const r = installClaudeShaped(settings(env), NAME4, eventsFor3(opts.lean), cmd, OPTS3);
2186
+ const r = installClaudeShaped(settings(env), NAME5, eventsFor4(opts.lean), cmd, OPTS4);
1713
2187
  const a = geminiHooksActive(settings(env));
1714
2188
  return [a.note && !r.skipped ? { ...r, note: [r.note, a.note].filter(Boolean).join("; ") } : r];
1715
2189
  },
@@ -1717,7 +2191,7 @@ var geminiCli = {
1717
2191
  uninstallClaudeShaped(settings(env), GEMINI_EVENTS, { created: opts.created?.includes(settings(env)) })
1718
2192
  ],
1719
2193
  installed: (cmd, env = process.env, opts = {}) => {
1720
- const state = installedClaudeShaped(settings(env), NAME4, eventsFor3(opts.lean), cmd, OPTS3);
2194
+ const state = installedClaudeShaped(settings(env), NAME5, eventsFor4(opts.lean), cmd, OPTS4);
1721
2195
  const a = geminiHooksActive(settings(env));
1722
2196
  return [
1723
2197
  {
@@ -1731,14 +2205,15 @@ var geminiCli = {
1731
2205
  const event = input.argv[1] ?? "";
1732
2206
  let s = {};
1733
2207
  try {
1734
- s = input.stdin ? JSON.parse(input.stdin) : {};
2208
+ const j = input.stdin ? JSON.parse(input.stdin) : {};
2209
+ if (j && typeof j === "object" && !Array.isArray(j)) s = j;
1735
2210
  } catch {
1736
2211
  }
1737
2212
  const id = s.session_id;
1738
2213
  if (!id) return null;
1739
2214
  const session = { id: String(id), cwd: typeof s.cwd === "string" ? s.cwd : input.cwd };
1740
- const tool = validTool4(s.tool_name) ? s.tool_name : void 0;
1741
- const ev = (type, data) => ({ type, data: prune4(data), harnessEvent: event });
2215
+ const tool = validTool5(s.tool_name) ? s.tool_name : void 0;
2216
+ const ev = (type, data) => ({ type, data: prune5(data), harnessEvent: event });
1742
2217
  const events = [];
1743
2218
  switch (event) {
1744
2219
  case "SessionStart":
@@ -1819,7 +2294,7 @@ var geminiCli = {
1819
2294
  const raw = first ? first.content ?? first.message ?? first.text : void 0;
1820
2295
  const text = typeof raw === "string" ? raw.trim() : "";
1821
2296
  if (text) {
1822
- const line = text.split("\n").find((l) => l.trim()) ?? "";
2297
+ const line = text.split("\n")[0];
1823
2298
  out.title = line.length > 80 ? `${line.slice(0, 80).replace(/\s+\S*$/, "")}\u2026` : line;
1824
2299
  out.title_source = "first-ask";
1825
2300
  }
@@ -1833,13 +2308,13 @@ var geminiCli = {
1833
2308
  backfill: () => []
1834
2309
  // chat logs are keyed by a project hash, not a session id we can name; the first hook of a resumed session reports it
1835
2310
  };
1836
- function prune4(o) {
2311
+ function prune5(o) {
1837
2312
  for (const k of Object.keys(o)) if (o[k] === void 0) delete o[k];
1838
2313
  return o;
1839
2314
  }
1840
2315
 
1841
2316
  // ../core/src/adapters/index.ts
1842
- var ADAPTERS = [claudeCode, codex, geminiCli, antigravity];
2317
+ var ADAPTERS = [claudeCode, codex, geminiCli, antigravity, devin];
1843
2318
  var adapterByName = (name) => ADAPTERS.find((a) => a.name === name);
1844
2319
 
1845
2320
  // src/index.ts
@@ -1879,7 +2354,7 @@ async function runAdapterFixtures(files) {
1879
2354
  cwd: fx.input.cwd ?? "/"
1880
2355
  });
1881
2356
  if (!r) {
1882
- results.push({ file, ok: false, why: "fromHook returned null" });
2357
+ results.push(fx.expect.length ? { file, ok: false, why: "fromHook returned null" } : { file, ok: true });
1883
2358
  continue;
1884
2359
  }
1885
2360
  let why = matches(
@@ -1890,7 +2365,7 @@ async function runAdapterFixtures(files) {
1890
2365
  if (!why)
1891
2366
  for (const [i, e] of fx.expect.entries()) {
1892
2367
  const got = r.events[i];
1893
- 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`);
2368
+ 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`);
1894
2369
  if (why) break;
1895
2370
  }
1896
2371
  results.push(why ? { file, ok: false, why } : { file, ok: true });