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/index.js CHANGED
@@ -1,4 +1,4 @@
1
- // sessionpipe-conformance 0.6.1 · 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,13 +196,22 @@ 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));
172
205
  }
206
+ function codexInjected(text) {
207
+ return /^<[a-z_-]+>/i.test(text) || /^# AGENTS\.md instructions\b/i.test(text);
208
+ }
173
209
  function readCodex(lines, from) {
174
210
  const msgs = [];
175
- 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++) {
176
215
  const j = lines[i] ? parse(lines[i]) : null;
177
216
  if (!j || j.type !== "response_item") continue;
178
217
  const p = j.payload;
@@ -181,7 +220,7 @@ function readCodex(lines, from) {
181
220
  const text = textParts(p.content, ["input_text", "output_text"]).trim();
182
221
  if (!text) continue;
183
222
  if (p.role === "user") {
184
- if (/^<[a-z_-]+>/i.test(text)) continue;
223
+ if (codexInjected(text)) continue;
185
224
  msgs.push({ role: "user", text, at, line: i });
186
225
  } else if (p.role === "assistant") msgs.push({ role: "assistant", text, at, line: i });
187
226
  }
@@ -210,7 +249,7 @@ function readAntigravity(lines, from) {
210
249
  const at = Date.parse(String(j.created_at ?? j.timestamp ?? "")) || Date.now();
211
250
  if (j.type === "USER_INPUT") {
212
251
  const m = /<USER_REQUEST>([\s\S]*?)(?:<\/USER_REQUEST>|$)/.exec(j.content);
213
- const text = (m ? m[1] ?? "" : j.content).trim();
252
+ const text = (m ? m[1] : j.content).trim();
214
253
  if (text) msgs.push({ role: "user", text, at, line: i });
215
254
  } else if (j.type === "PLANNER_RESPONSE") {
216
255
  const text = j.content.trim();
@@ -258,6 +297,10 @@ function formatJson(value, f) {
258
297
  const text = JSON.stringify(value, null, f.indent).replace(/\n/g, f.eol);
259
298
  return f.finalNewline ? text + f.eol : text;
260
299
  }
300
+ function readJson(file) {
301
+ const f = readJsonFile(file);
302
+ return f.ok ? f.value : {};
303
+ }
261
304
  function entryFor(event, harness, cmd, o) {
262
305
  const matcher = o.matcherFor(event);
263
306
  return {
@@ -327,8 +370,7 @@ function uninstallClaudeShaped(file, events, opts = {}) {
327
370
  unlinkSync(file);
328
371
  return { file, changed: true, note: "removed (sessionpipe created it)" };
329
372
  }
330
- const next = formatJson(settings2, f);
331
- if (next !== f.raw) writeFileSync(file, next);
373
+ writeFileSync(file, formatJson(settings2, f));
332
374
  return { file, changed: true };
333
375
  }
334
376
  function installedClaudeShaped(file, harness, events, cmd, o) {
@@ -355,7 +397,7 @@ var HOOK_NAME = "sessionpipe";
355
397
  var AG_EVENTS = ["PreInvocation", "PostInvocation", "PostToolUse", "Stop"];
356
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';
357
399
  var antigravityAnswer = (event) => event === "PreToolUse" ? AG_STALE_PRETOOL : "{}\n";
358
- var gemini = (env) => env.GEMINI_CLI_HOME || path3.join(HOME, ".gemini");
400
+ var gemini = (env) => env.GEMINI_CLI_HOME || path3.join(homeDir(), ".gemini");
359
401
  var hooksFile = (env) => path3.join(gemini(env), "config", "hooks.json");
360
402
  var projectsDir = (env) => path3.join(gemini(env), "config", "projects");
361
403
  var dataDirs = (env) => ["antigravity", "antigravity-cli", "antigravity-ide"].map((d) => path3.join(gemini(env), d));
@@ -421,7 +463,8 @@ var antigravity = {
421
463
  const event = input.argv[1] ?? "";
422
464
  let s = {};
423
465
  try {
424
- 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;
425
468
  } catch {
426
469
  }
427
470
  const id = s.conversationId ?? s.session_id ?? s.sessionId;
@@ -477,19 +520,16 @@ var antigravity = {
477
520
  }
478
521
  return readAntigravity(text.split("\n"), fromLine);
479
522
  },
480
- facts(session, transcript, hints, state) {
523
+ facts(session, transcript, hints) {
481
524
  if (!transcript) return {};
482
525
  return antigravityFacts(transcript, {
483
526
  conversation: session.id,
484
527
  workspaces: hints.workspaces ?? (session.cwd ? [session.cwd] : []),
485
- state,
486
528
  env: process.env
487
529
  });
488
530
  },
489
531
  backfill(sinceMs, env = process.env) {
490
- const rows = [];
491
- const noState = { get: () => ({}), save: () => {
492
- } };
532
+ const rows2 = [];
493
533
  for (const data of dataDirs(env)) {
494
534
  let ids = [];
495
535
  try {
@@ -503,13 +543,13 @@ var antigravity = {
503
543
  } catch {
504
544
  continue;
505
545
  }
506
- const facts = antigravityFacts(f, { conversation: id, workspaces: [], state: noState, env });
546
+ const facts = antigravityFacts(f, { conversation: id, workspaces: [], env });
507
547
  if (!facts.started_at || !facts.last_at) continue;
508
548
  const { started_at, last_at, first_ask: _fa, harness_version: _hv, ...rest } = facts;
509
- rows.push({ session: { id, ...rest }, started_at, last_at });
549
+ rows2.push({ session: { id, ...rest }, started_at, last_at });
510
550
  }
511
551
  }
512
- return rows;
552
+ return rows2;
513
553
  }
514
554
  };
515
555
  function prune(o) {
@@ -559,7 +599,7 @@ function agProjects(dir) {
559
599
  }
560
600
  const folders = [];
561
601
  for (const r of j?.projectResources?.resources ?? []) {
562
- const f = agPath(r?.folderUri);
602
+ const f = agPath(r?.folderUri ?? r?.gitFolder?.folderUri);
563
603
  if (f) folders.push(...agVariants(f));
564
604
  }
565
605
  if (folders.length) out.push({ id, folders });
@@ -613,7 +653,7 @@ function antigravityFacts(file, opts) {
613
653
  for (const j of H) {
614
654
  if (j.type !== "USER_INPUT" || typeof j.content !== "string") continue;
615
655
  const m = /<USER_REQUEST>([\s\S]*?)(?:<\/USER_REQUEST>|$)/.exec(j.content);
616
- const text = (m ? m[1] ?? "" : j.content).trim();
656
+ const text = (m ? m[1] : j.content).trim();
617
657
  const line = (text.split("\n").find((l) => l.trim()) ?? "").trim();
618
658
  if (!line || /^<[a-z_-]+>/i.test(line)) continue;
619
659
  out.title = line.length > 80 ? `${line.slice(0, 80).replace(/\s+\S*$/, "")}\u2026` : line;
@@ -661,10 +701,9 @@ function gitFacts(cwd) {
661
701
  return out;
662
702
  }
663
703
  function stripCredentials(url) {
664
- let u = url.replace(/\/\/[^/@]*@/, "//");
665
- const ssh = /^(?:ssh:\/\/)?git@([^:/]+)[:/](.+?)(?:\.git)?$/.exec(u);
666
- if (ssh) u = `${ssh[1]}/${ssh[2]}`;
667
- 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$/, "");
668
707
  }
669
708
 
670
709
  // ../core/src/adapters/claude-code.ts
@@ -695,7 +734,7 @@ var OPTS = {
695
734
  timeoutFor: (e) => e === "PermissionRequest" ? 130 : void 0
696
735
  };
697
736
  var DESKTOP_ID = /^local_[A-Za-z0-9-]{1,64}$/;
698
- function claudeDirs(env = process.env, home = env.HOME || HOME) {
737
+ function claudeDirs(env = process.env, home = env.HOME || homeDir()) {
699
738
  const out = [];
700
739
  const add = (d) => {
701
740
  if (!d) return;
@@ -736,11 +775,13 @@ var claudeCode = {
736
775
  const event = input.argv[1] ?? "";
737
776
  let s = {};
738
777
  try {
739
- 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;
740
780
  } catch {
741
781
  }
742
782
  const id = s.session_id ?? s.sessionId;
743
783
  if (!id) return null;
784
+ if (notClaudeCode(String(id), s)) return null;
744
785
  const session = { id: String(id), cwd: typeof s.cwd === "string" ? s.cwd : input.cwd };
745
786
  if (typeof s.model === "string") session.model = s.model;
746
787
  if (typeof s.agent_id === "string" && event.startsWith("Subagent") === false && s.agent_id)
@@ -859,6 +900,7 @@ var claudeCode = {
859
900
  return null;
860
901
  }
861
902
  const hints = {};
903
+ if (event === "UserPromptSubmit" && typeof s.prompt === "string") hints.prompt = s.prompt;
862
904
  const host = input.env.CLAUDE_CODE_HOST_SESSION_ID;
863
905
  if (host && DESKTOP_ID.test(host)) hints.desktop_id = host;
864
906
  if (event !== "PreToolUse" && event !== "PostToolUse") hints.claude_pid = claudePid(input.env);
@@ -884,7 +926,7 @@ var claudeCode = {
884
926
  return out;
885
927
  },
886
928
  backfill(sinceMs, env = process.env) {
887
- const rows = [];
929
+ const rows2 = [];
888
930
  const files = [];
889
931
  for (const d of claudeDirs(env))
890
932
  files.push(...recentFiles(path4.join(d, "projects"), 1, sinceMs, (n) => /^[0-9a-f-]{36}\.jsonl$/.test(n)));
@@ -896,19 +938,23 @@ var claudeCode = {
896
938
  if (!facts.started_at || !facts.last_at) continue;
897
939
  const { started_at, last_at, first_ask: _fa, harness_version: _hv, ...rest } = facts;
898
940
  const cwd = rest.cwd;
899
- rows.push({
941
+ rows2.push({
900
942
  session: { id, ...rest, ...gitFacts(cwd), ...rest.branch ? { branch: rest.branch } : {} },
901
943
  started_at,
902
944
  last_at
903
945
  });
904
946
  }
905
- return rows;
947
+ return rows2;
906
948
  }
907
949
  };
908
950
  function prune2(o) {
909
951
  for (const k of Object.keys(o)) if (o[k] === void 0) delete o[k];
910
952
  return o;
911
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
+ }
912
958
  function pickSource(v) {
913
959
  return typeof v === "string" && ["startup", "resume", "clear", "compact", "fork"].includes(v) ? v : "unknown";
914
960
  }
@@ -1014,23 +1060,27 @@ function parentOf() {
1014
1060
  }
1015
1061
  return (pid) => table.get(pid) || 0;
1016
1062
  }
1017
- var accountCache = /* @__PURE__ */ new Map();
1018
- function claudeAccount(dir) {
1019
- const hit = accountCache.get(dir);
1020
- if (hit !== void 0) return hit;
1063
+ var EMAIL_RE = /^[^@\s]{1,64}@[^@\s]{1,189}$/;
1064
+ function claudeLogin(dir, home = homeDir()) {
1021
1065
  const files = [path4.join(dir, ".claude.json")];
1022
- if (path4.resolve(dir) === path4.join(HOME, ".claude")) files.unshift(path4.join(HOME, ".claude.json"));
1023
- let acct = null;
1066
+ if (path4.resolve(dir) === path4.join(home, ".claude")) files.unshift(path4.join(home, ".claude.json"));
1024
1067
  for (const f of files) {
1025
1068
  try {
1026
1069
  const a = JSON.parse(readFileSync3(f, "utf8")).oauthAccount;
1027
- if (a?.accountUuid) {
1028
- acct = String(a.accountUuid);
1029
- 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 } : {} };
1030
1073
  }
1031
1074
  } catch {
1032
1075
  }
1033
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;
1034
1084
  accountCache.set(dir, acct);
1035
1085
  return acct;
1036
1086
  }
@@ -1250,18 +1300,13 @@ function parseArgv(raw) {
1250
1300
  function readNotify(text) {
1251
1301
  const { head, tail, at } = split(text);
1252
1302
  const m = NOTIFY_LINE.exec(head);
1253
- if (m)
1254
- return {
1255
- raw: m[1] ?? "",
1256
- argv: parseArgv(m[1] ?? ""),
1257
- index: m.index,
1258
- length: m[0].length,
1259
- misplaced: false,
1260
- other: !OURS2.test(m[1] ?? "")
1261
- };
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
+ }
1262
1307
  const k = NOTIFY_KEY.exec(head);
1263
1308
  if (k) {
1264
- const rest = head.slice(k.index).split("\n")[0] ?? "";
1309
+ const rest = head.slice(k.index).split("\n")[0];
1265
1310
  return {
1266
1311
  raw: rest.replace(NOTIFY_KEY, "").trim(),
1267
1312
  argv: null,
@@ -1272,15 +1317,11 @@ function readNotify(text) {
1272
1317
  };
1273
1318
  }
1274
1319
  const t = NOTIFY_LINE.exec(tail);
1275
- if (t && OURS2.test(t[1] ?? ""))
1276
- return {
1277
- raw: t[1] ?? "",
1278
- argv: parseArgv(t[1] ?? ""),
1279
- index: at + t.index,
1280
- length: t[0].length,
1281
- misplaced: true,
1282
- other: false
1283
- };
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
+ }
1284
1325
  return null;
1285
1326
  }
1286
1327
  function dropLine(text, index, length) {
@@ -1299,9 +1340,7 @@ function withNotify(text, line) {
1299
1340
  if (n?.misplaced) return insertTopLevel(dropLine(text, n.index, n.length), line);
1300
1341
  return insertTopLevel(text, line);
1301
1342
  }
1302
- function replaceNotify(text, line) {
1303
- const n = readNotify(text);
1304
- if (!n || n.misplaced || !n.length) return null;
1343
+ function replaceNotify(text, n, line) {
1305
1344
  return text.slice(0, n.index) + line + text.slice(n.index + n.length);
1306
1345
  }
1307
1346
  function withoutNotify(text) {
@@ -1342,7 +1381,7 @@ var CODEX_EVENTS = [
1342
1381
  ];
1343
1382
  var OPTS2 = { matcherFor: (e) => /Tool|Permission/.test(e) ? "" : void 0, timeout: 5 };
1344
1383
  var eventsFor2 = (lean) => lean ? CODEX_EVENTS.filter((e) => e !== "PreToolUse") : [...CODEX_EVENTS];
1345
- var codexHome = (env) => env.CODEX_HOME || path5.join(HOME, ".codex");
1384
+ var codexHome = (env) => env.CODEX_HOME || path5.join(homeDir(), ".codex");
1346
1385
  var hooksFile2 = (env) => path5.join(codexHome(env), "hooks.json");
1347
1386
  var configFile = (env) => path5.join(codexHome(env), "config.toml");
1348
1387
  var sessionsDir = (env) => path5.join(codexHome(env), "sessions");
@@ -1380,11 +1419,8 @@ function installNotify(cmd, env) {
1380
1419
  return { file, changed: false, note: "notify current (after Codex Computer Use)" };
1381
1420
  }
1382
1421
  if (!prev || OURS2.test(JSON.stringify(prev))) {
1383
- const next = replaceNotify(text, `notify = ${withPreviousRaw(n.raw, argv)}`);
1384
- if (next) {
1385
- write(next);
1386
- return { file, changed: true, note: "chained after Codex Computer Use (--previous-notify)" };
1387
- }
1422
+ write(replaceNotify(text, n, `notify = ${withPreviousRaw(n.raw, argv)}`));
1423
+ return { file, changed: true, note: "chained after Codex Computer Use (--previous-notify)" };
1388
1424
  }
1389
1425
  }
1390
1426
  return {
@@ -1413,13 +1449,13 @@ function uninstallNotify(env) {
1413
1449
  }
1414
1450
  const prev = previousNotify(n.argv);
1415
1451
  if (isComputerUse(n.argv) && prev && OURS2.test(JSON.stringify(prev))) {
1416
- const next = replaceNotify(text, `notify = ${stripPreviousRaw(n.raw)}`);
1417
- if (next) writeFileSync3(file, next);
1452
+ writeFileSync3(file, replaceNotify(text, n, `notify = ${stripPreviousRaw(n.raw)}`));
1418
1453
  return { file, changed: true };
1419
1454
  }
1420
1455
  return { file, changed: false };
1421
1456
  }
1422
- function findRollout(threadId, env) {
1457
+ function findRollout(threadId, env = process.env) {
1458
+ if (!/^[A-Za-z0-9-]{8,128}$/.test(threadId)) return null;
1423
1459
  let best = null;
1424
1460
  const walk = (dir, depth) => {
1425
1461
  let names = [];
@@ -1430,7 +1466,7 @@ function findRollout(threadId, env) {
1430
1466
  }
1431
1467
  for (const n of names.sort().reverse()) {
1432
1468
  const p = path5.join(dir, n);
1433
- if (n.endsWith(".jsonl") && n.includes(threadId)) {
1469
+ if (n.endsWith(`-${threadId}.jsonl`)) {
1434
1470
  best = p;
1435
1471
  return;
1436
1472
  }
@@ -1443,6 +1479,13 @@ function findRollout(threadId, env) {
1443
1479
  walk(sessionsDir(env), 0);
1444
1480
  return best;
1445
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
+ }
1446
1489
  var codex = {
1447
1490
  name: NAME3,
1448
1491
  events: CODEX_EVENTS,
@@ -1485,11 +1528,14 @@ var codex = {
1485
1528
  if (event === "notify") {
1486
1529
  const raw = input.argv.find((a) => a.startsWith("{")) ?? input.stdin;
1487
1530
  try {
1488
- s = raw ? JSON.parse(raw) : {};
1531
+ const j = raw ? JSON.parse(raw) : {};
1532
+ if (j && typeof j === "object" && !Array.isArray(j)) s = j;
1489
1533
  } catch {
1490
1534
  }
1491
1535
  const id2 = s["thread-id"] ?? s.thread_id ?? s["session-id"] ?? s.session_id;
1492
1536
  if (!id2) return null;
1537
+ const file = findRollout(String(id2), input.env);
1538
+ if (!file || codexSubagent(rolloutMeta(file))) return null;
1493
1539
  const r2 = {
1494
1540
  session: { id: String(id2), cwd: typeof s.cwd === "string" ? s.cwd : input.cwd },
1495
1541
  events: [
@@ -1499,18 +1545,19 @@ var codex = {
1499
1545
  harnessEvent: "notify"
1500
1546
  }
1501
1547
  ],
1502
- hints: {}
1548
+ hints: {},
1549
+ transcript: file
1503
1550
  };
1504
- const file = findRollout(String(id2), input.env);
1505
- if (file) r2.transcript = file;
1506
1551
  return r2;
1507
1552
  }
1508
1553
  try {
1509
- 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;
1510
1556
  } catch {
1511
1557
  }
1512
1558
  const id = s.session_id;
1513
1559
  if (!id) return null;
1560
+ if (s.transcript_path === null) return null;
1514
1561
  const session = { id: String(id), cwd: typeof s.cwd === "string" ? s.cwd : input.cwd };
1515
1562
  if (typeof s.model === "string") session.model = s.model;
1516
1563
  const turn_id = typeof s.turn_id === "string" ? s.turn_id : void 0;
@@ -1582,7 +1629,8 @@ var codex = {
1582
1629
  return null;
1583
1630
  }
1584
1631
  const r = { session, events, hints: {} };
1585
- 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;
1586
1634
  return r;
1587
1635
  },
1588
1636
  readTranscript(file, fromLine) {
@@ -1600,16 +1648,17 @@ var codex = {
1600
1648
  return out;
1601
1649
  },
1602
1650
  backfill(sinceMs, env = process.env) {
1603
- const rows = [];
1651
+ const rows2 = [];
1604
1652
  for (const f of recentFiles(sessionsDir(env), 3, sinceMs, (n) => /^rollout-.*\.jsonl$/.test(n))) {
1605
1653
  const m = /([0-9a-f]{8}-[0-9a-f-]{27,})\.jsonl$/i.exec(path5.basename(f));
1606
1654
  if (!m?.[1]) continue;
1655
+ if (codexSubagent(rolloutMeta(f))) continue;
1607
1656
  const facts = codexFacts(f);
1608
1657
  if (!facts.started_at || !facts.last_at) continue;
1609
1658
  const { started_at, last_at, first_ask: _fa, harness_version: _hv, ...rest } = facts;
1610
- rows.push({ session: { id: m[1], ...rest }, started_at, last_at });
1659
+ rows2.push({ session: { id: m[1], ...rest }, started_at, last_at });
1611
1660
  }
1612
- return rows;
1661
+ return rows2;
1613
1662
  }
1614
1663
  };
1615
1664
  function prune3(o) {
@@ -1618,10 +1667,11 @@ function prune3(o) {
1618
1667
  }
1619
1668
  function codexFacts(file) {
1620
1669
  const { head, tail } = ends(file, 128 * 1024, 64 * 1024);
1621
- const H = parseLines(head);
1622
- const T = parseLines(tail);
1670
+ let H = parseLines(head);
1671
+ let T = parseLines(tail);
1623
1672
  const out = {};
1624
1673
  const meta = H.find((j) => j.type === "session_meta" && j.payload);
1674
+ const boundary = codexReplayBoundary(meta);
1625
1675
  if (meta?.payload) {
1626
1676
  const p = meta.payload;
1627
1677
  if (p.cwd) out.cwd = p.cwd;
@@ -1629,19 +1679,26 @@ function codexFacts(file) {
1629
1679
  if (p.git?.branch) out.branch = p.git.branch;
1630
1680
  if (p.git?.repository_url) out.repo = String(p.git.repository_url).replace(/\/\/[^/@]*@/, "//");
1631
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
+ }
1632
1689
  const ctx = H.concat(T).reverse().find((j) => j.type === "turn_context" && j.payload?.model);
1633
1690
  if (ctx?.payload?.model) out.model = ctx.payload.model;
1634
1691
  for (const j of H) {
1635
1692
  const p = j.payload;
1636
1693
  if (j.type !== "response_item" || !p || p.type !== "message" || p.role !== "user") continue;
1637
1694
  const text = (p.content ?? []).filter((b) => b && b.type === "input_text" && typeof b.text === "string").map((b) => b.text).join("\n").trim();
1638
- if (!text || /^<[a-z_-]+>/i.test(text)) continue;
1639
- const line = text.split("\n").find((l) => l.trim()) ?? "";
1695
+ if (!text || codexInjected(text)) continue;
1696
+ const line = text.split("\n")[0];
1640
1697
  out.title = line.length > 80 ? `${line.slice(0, 80).replace(/\s+\S*$/, "")}\u2026` : line;
1641
1698
  out.title_source = "first-ask";
1642
1699
  break;
1643
1700
  }
1644
- 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);
1645
1702
  if (times.length) {
1646
1703
  out.started_at = Math.min(...times);
1647
1704
  out.last_at = Math.max(...times);
@@ -1649,11 +1706,432 @@ function codexFacts(file) {
1649
1706
  return out;
1650
1707
  }
1651
1708
 
1652
- // ../core/src/adapters/gemini-cli.ts
1653
- import { execFileSync as execFileSync3 } from "node:child_process";
1654
- 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";
1655
1712
  import path6 from "node:path";
1656
- 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";
1657
2135
  var GEMINI_EVENTS = [
1658
2136
  "SessionStart",
1659
2137
  "BeforeAgent",
@@ -1664,11 +2142,11 @@ var GEMINI_EVENTS = [
1664
2142
  "PreCompress",
1665
2143
  "SessionEnd"
1666
2144
  ];
1667
- var OPTS3 = { matcherFor: (e) => /Tool/.test(e) ? ".*" : void 0, timeout: 5e3 };
1668
- var geminiDir = (env) => env.GEMINI_CLI_HOME || path6.join(HOME, ".gemini");
1669
- var settings = (env) => path6.join(geminiDir(env), "settings.json");
1670
- var validTool4 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
1671
- 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];
1672
2150
  function geminiHooksActive(settingsFile) {
1673
2151
  let tools;
1674
2152
  try {
@@ -1680,7 +2158,7 @@ function geminiHooksActive(settingsFile) {
1680
2158
  return { active: false, note: "inactive: tools.enableHooks is false in settings.json" };
1681
2159
  let version = "";
1682
2160
  try {
1683
- version = execFileSync3("gemini", ["--version"], {
2161
+ version = execFileSync4("gemini", ["--version"], {
1684
2162
  encoding: "utf8",
1685
2163
  timeout: 4e3,
1686
2164
  stdio: ["ignore", "pipe", "ignore"]
@@ -1699,13 +2177,13 @@ function geminiHooksActive(settingsFile) {
1699
2177
  };
1700
2178
  }
1701
2179
  var geminiCli = {
1702
- name: NAME4,
2180
+ name: NAME5,
1703
2181
  events: GEMINI_EVENTS,
1704
2182
  // ~/.gemini also belongs to Antigravity; Gemini CLI is here when its settings or its chat logs exist.
1705
- 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")),
1706
2184
  configFiles: (env = process.env) => [settings(env)],
1707
2185
  install: (cmd, env = process.env, opts = {}) => {
1708
- const r = installClaudeShaped(settings(env), NAME4, eventsFor3(opts.lean), cmd, OPTS3);
2186
+ const r = installClaudeShaped(settings(env), NAME5, eventsFor4(opts.lean), cmd, OPTS4);
1709
2187
  const a = geminiHooksActive(settings(env));
1710
2188
  return [a.note && !r.skipped ? { ...r, note: [r.note, a.note].filter(Boolean).join("; ") } : r];
1711
2189
  },
@@ -1713,7 +2191,7 @@ var geminiCli = {
1713
2191
  uninstallClaudeShaped(settings(env), GEMINI_EVENTS, { created: opts.created?.includes(settings(env)) })
1714
2192
  ],
1715
2193
  installed: (cmd, env = process.env, opts = {}) => {
1716
- const state = installedClaudeShaped(settings(env), NAME4, eventsFor3(opts.lean), cmd, OPTS3);
2194
+ const state = installedClaudeShaped(settings(env), NAME5, eventsFor4(opts.lean), cmd, OPTS4);
1717
2195
  const a = geminiHooksActive(settings(env));
1718
2196
  return [
1719
2197
  {
@@ -1727,14 +2205,15 @@ var geminiCli = {
1727
2205
  const event = input.argv[1] ?? "";
1728
2206
  let s = {};
1729
2207
  try {
1730
- 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;
1731
2210
  } catch {
1732
2211
  }
1733
2212
  const id = s.session_id;
1734
2213
  if (!id) return null;
1735
2214
  const session = { id: String(id), cwd: typeof s.cwd === "string" ? s.cwd : input.cwd };
1736
- const tool = validTool4(s.tool_name) ? s.tool_name : void 0;
1737
- 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 });
1738
2217
  const events = [];
1739
2218
  switch (event) {
1740
2219
  case "SessionStart":
@@ -1815,7 +2294,7 @@ var geminiCli = {
1815
2294
  const raw = first ? first.content ?? first.message ?? first.text : void 0;
1816
2295
  const text = typeof raw === "string" ? raw.trim() : "";
1817
2296
  if (text) {
1818
- const line = text.split("\n").find((l) => l.trim()) ?? "";
2297
+ const line = text.split("\n")[0];
1819
2298
  out.title = line.length > 80 ? `${line.slice(0, 80).replace(/\s+\S*$/, "")}\u2026` : line;
1820
2299
  out.title_source = "first-ask";
1821
2300
  }
@@ -1829,13 +2308,13 @@ var geminiCli = {
1829
2308
  backfill: () => []
1830
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
1831
2310
  };
1832
- function prune4(o) {
2311
+ function prune5(o) {
1833
2312
  for (const k of Object.keys(o)) if (o[k] === void 0) delete o[k];
1834
2313
  return o;
1835
2314
  }
1836
2315
 
1837
2316
  // ../core/src/adapters/index.ts
1838
- var ADAPTERS = [claudeCode, codex, geminiCli, antigravity];
2317
+ var ADAPTERS = [claudeCode, codex, geminiCli, antigravity, devin];
1839
2318
  var adapterByName = (name) => ADAPTERS.find((a) => a.name === name);
1840
2319
 
1841
2320
  // src/index.ts
@@ -1875,7 +2354,7 @@ async function runAdapterFixtures(files) {
1875
2354
  cwd: fx.input.cwd ?? "/"
1876
2355
  });
1877
2356
  if (!r) {
1878
- 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 });
1879
2358
  continue;
1880
2359
  }
1881
2360
  let why = matches(
@@ -1886,7 +2365,7 @@ async function runAdapterFixtures(files) {
1886
2365
  if (!why)
1887
2366
  for (const [i, e] of fx.expect.entries()) {
1888
2367
  const got = r.events[i];
1889
- 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`);
1890
2369
  if (why) break;
1891
2370
  }
1892
2371
  results.push(why ? { file, ok: false, why } : { file, ok: true });