sessionpipe-conformance 0.7.0 → 0.10.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.10.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)
@@ -760,14 +798,28 @@ var claudeCode = {
760
798
  case "SessionStart":
761
799
  events.push(ev("session.started", { source: pickSource(s.source ?? s.trigger) }));
762
800
  break;
763
- case "UserPromptSubmit":
801
+ case "UserPromptSubmit": {
802
+ const prompt = typeof s.prompt === "string" ? s.prompt : void 0;
764
803
  events.push(
765
804
  ev("turn.started", {
766
805
  turn_id,
767
- prompt_chars: typeof s.prompt === "string" ? [...s.prompt].length : void 0
806
+ prompt_chars: prompt === void 0 ? void 0 : [...prompt].length,
807
+ // Claude Code's hook schema documents `source`
808
+ // (user | sdk | system | loop_wakeup | schedule_wakeup | poll_event) and says a
809
+ // payload may omit it while the field rolls out; 2.1.304's payload has no such
810
+ // key at all. Forwarded verbatim, shape-guarded only: the list is the harness's
811
+ // to extend.
812
+ submitted_by: harnessWord(s.source),
813
+ // One of Claude Code's own prompt wrappers: the prompt is a block the harness
814
+ // wrote, so it is not the person's words. Absent is unknown, never "the person" —
815
+ // and `true` is never "no person caused this turn", because a Talk or
816
+ // `sessionpipe wait` delivery hands the person's own message over inside a
817
+ // <task-notification> (PROTOCOL.md §4).
818
+ injected: prompt !== void 0 && injectedBlock(prompt) ? true : void 0
768
819
  })
769
820
  );
770
821
  break;
822
+ }
771
823
  case "PreToolUse":
772
824
  if (tool) events.push(ev("tool.started", { tool, call_id, turn_id, agent_id, input: s.tool_input }));
773
825
  break;
@@ -822,7 +874,13 @@ var claudeCode = {
822
874
  );
823
875
  break;
824
876
  case "Stop":
825
- events.push(ev("turn.ended", { turn_id, reason: "stop" }));
877
+ events.push(
878
+ ev("turn.ended", {
879
+ turn_id,
880
+ reason: "stop",
881
+ background: Array.isArray(s.background_tasks) ? s.background_tasks.length : void 0
882
+ })
883
+ );
826
884
  break;
827
885
  case "StopFailure":
828
886
  events.push(
@@ -862,8 +920,11 @@ var claudeCode = {
862
920
  return null;
863
921
  }
864
922
  const hints = {};
923
+ if (event === "UserPromptSubmit" && typeof s.prompt === "string") hints.prompt = s.prompt;
865
924
  const host = input.env.CLAUDE_CODE_HOST_SESSION_ID;
866
925
  if (host && DESKTOP_ID.test(host)) hints.desktop_id = host;
926
+ const attended = input.env.CLAUDE_CODE_SESSION_ATTENDED;
927
+ if (attended === "1" || attended === "0") hints.attended = attended === "1" || !!input.env.SESSIONPIPE_CONTROL_JOB;
867
928
  if (event !== "PreToolUse" && event !== "PostToolUse") hints.claude_pid = claudePid(input.env);
868
929
  const r = { session, events, hints };
869
930
  if (typeof s.transcript_path === "string") r.transcript = s.transcript_path;
@@ -884,10 +945,11 @@ var claudeCode = {
884
945
  const cwd = out.cwd ?? session.cwd;
885
946
  Object.assign(out, gitFacts(cwd), out.branch ? { branch: out.branch } : {});
886
947
  if (!out.url && typeof hints.desktop_id === "string") out.url = `claude://code/continue/${hints.desktop_id}`;
948
+ if (typeof hints.attended === "boolean") out.attended = hints.attended || !!out.url;
887
949
  return out;
888
950
  },
889
951
  backfill(sinceMs, env = process.env) {
890
- const rows = [];
952
+ const rows2 = [];
891
953
  const files = [];
892
954
  for (const d of claudeDirs(env))
893
955
  files.push(...recentFiles(path4.join(d, "projects"), 1, sinceMs, (n) => /^[0-9a-f-]{36}\.jsonl$/.test(n)));
@@ -899,22 +961,29 @@ var claudeCode = {
899
961
  if (!facts.started_at || !facts.last_at) continue;
900
962
  const { started_at, last_at, first_ask: _fa, harness_version: _hv, ...rest } = facts;
901
963
  const cwd = rest.cwd;
902
- rows.push({
964
+ rows2.push({
903
965
  session: { id, ...rest, ...gitFacts(cwd), ...rest.branch ? { branch: rest.branch } : {} },
904
966
  started_at,
905
967
  last_at
906
968
  });
907
969
  }
908
- return rows;
970
+ return rows2;
909
971
  }
910
972
  };
911
973
  function prune2(o) {
912
974
  for (const k of Object.keys(o)) if (o[k] === void 0) delete o[k];
913
975
  return o;
914
976
  }
977
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
978
+ function notClaudeCode(id, s) {
979
+ return typeof s.transcript_path !== "string" && !UUID.test(id);
980
+ }
915
981
  function pickSource(v) {
916
982
  return typeof v === "string" && ["startup", "resume", "clear", "compact", "fork"].includes(v) ? v : "unknown";
917
983
  }
984
+ var harnessWord = (v) => typeof v === "string" && /^[A-Za-z0-9][A-Za-z0-9_.-]{0,39}$/.test(v) ? v : void 0;
985
+ var WRAPPER = /^<(?:task-notification|cross-session-message|system-reminder|local-command-stdout|command-[a-z-]+)[\s/>]/i;
986
+ var injectedBlock = (prompt) => WRAPPER.test(prompt.trim());
918
987
  function errText(v) {
919
988
  if (typeof v === "string") return v.slice(0, 2e3);
920
989
  if (v && typeof v === "object") {
@@ -1017,23 +1086,47 @@ function parentOf() {
1017
1086
  }
1018
1087
  return (pid) => table.get(pid) || 0;
1019
1088
  }
1020
- var accountCache = /* @__PURE__ */ new Map();
1021
- function claudeAccount(dir) {
1022
- const hit = accountCache.get(dir);
1023
- if (hit !== void 0) return hit;
1089
+ var EMAIL_RE = /^[^@\s]{1,64}@[^@\s]{1,189}$/;
1090
+ function claudeLogin(dir, home = homeDir()) {
1024
1091
  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;
1092
+ if (path4.resolve(dir) === path4.join(home, ".claude")) files.unshift(path4.join(home, ".claude.json"));
1027
1093
  for (const f of files) {
1028
1094
  try {
1029
1095
  const a = JSON.parse(readFileSync3(f, "utf8")).oauthAccount;
1030
- if (a?.accountUuid) {
1031
- acct = String(a.accountUuid);
1032
- break;
1096
+ if (typeof a?.accountUuid === "string" && a.accountUuid) {
1097
+ const email = typeof a.emailAddress === "string" && EMAIL_RE.test(a.emailAddress) ? a.emailAddress : void 0;
1098
+ return { id: a.accountUuid.slice(0, 128), ...email ? { email } : {} };
1033
1099
  }
1034
1100
  } catch {
1035
1101
  }
1036
1102
  }
1103
+ return apiAccount(dir);
1104
+ }
1105
+ var ANTHROPIC_HOST = /(^|\.)anthropic\.com$/i;
1106
+ function apiAccount(dir) {
1107
+ let base;
1108
+ try {
1109
+ base = JSON.parse(readFileSync3(settingsIn(dir), "utf8")).env?.ANTHROPIC_BASE_URL;
1110
+ } catch {
1111
+ return null;
1112
+ }
1113
+ if (typeof base !== "string") return null;
1114
+ let u;
1115
+ try {
1116
+ u = new URL(base.trim());
1117
+ } catch {
1118
+ return null;
1119
+ }
1120
+ if (u.protocol !== "https:" && u.protocol !== "http:" || !u.hostname || ANTHROPIC_HOST.test(u.hostname))
1121
+ return null;
1122
+ const origin = `${u.protocol}//${u.host}`;
1123
+ return { id: `api-${createHash("sha256").update(origin).digest("hex").slice(0, 16)}`, name: u.host };
1124
+ }
1125
+ var accountCache = /* @__PURE__ */ new Map();
1126
+ function claudeAccount(dir) {
1127
+ const hit = accountCache.get(dir);
1128
+ if (hit !== void 0) return hit;
1129
+ const acct = claudeLogin(dir)?.id ?? null;
1037
1130
  accountCache.set(dir, acct);
1038
1131
  return acct;
1039
1132
  }
@@ -1253,18 +1346,13 @@ function parseArgv(raw) {
1253
1346
  function readNotify(text) {
1254
1347
  const { head, tail, at } = split(text);
1255
1348
  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
- };
1349
+ if (m) {
1350
+ const raw = m[1];
1351
+ return { raw, argv: parseArgv(raw), index: m.index, length: m[0].length, misplaced: false, other: !OURS2.test(raw) };
1352
+ }
1265
1353
  const k = NOTIFY_KEY.exec(head);
1266
1354
  if (k) {
1267
- const rest = head.slice(k.index).split("\n")[0] ?? "";
1355
+ const rest = head.slice(k.index).split("\n")[0];
1268
1356
  return {
1269
1357
  raw: rest.replace(NOTIFY_KEY, "").trim(),
1270
1358
  argv: null,
@@ -1275,15 +1363,11 @@ function readNotify(text) {
1275
1363
  };
1276
1364
  }
1277
1365
  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
- };
1366
+ if (t) {
1367
+ const raw = t[1];
1368
+ if (OURS2.test(raw))
1369
+ return { raw, argv: parseArgv(raw), index: at + t.index, length: t[0].length, misplaced: true, other: false };
1370
+ }
1287
1371
  return null;
1288
1372
  }
1289
1373
  function dropLine(text, index, length) {
@@ -1302,9 +1386,7 @@ function withNotify(text, line) {
1302
1386
  if (n?.misplaced) return insertTopLevel(dropLine(text, n.index, n.length), line);
1303
1387
  return insertTopLevel(text, line);
1304
1388
  }
1305
- function replaceNotify(text, line) {
1306
- const n = readNotify(text);
1307
- if (!n || n.misplaced || !n.length) return null;
1389
+ function replaceNotify(text, n, line) {
1308
1390
  return text.slice(0, n.index) + line + text.slice(n.index + n.length);
1309
1391
  }
1310
1392
  function withoutNotify(text) {
@@ -1345,7 +1427,7 @@ var CODEX_EVENTS = [
1345
1427
  ];
1346
1428
  var OPTS2 = { matcherFor: (e) => /Tool|Permission/.test(e) ? "" : void 0, timeout: 5 };
1347
1429
  var eventsFor2 = (lean) => lean ? CODEX_EVENTS.filter((e) => e !== "PreToolUse") : [...CODEX_EVENTS];
1348
- var codexHome = (env) => env.CODEX_HOME || path5.join(HOME, ".codex");
1430
+ var codexHome = (env) => env.CODEX_HOME || path5.join(homeDir(), ".codex");
1349
1431
  var hooksFile2 = (env) => path5.join(codexHome(env), "hooks.json");
1350
1432
  var configFile = (env) => path5.join(codexHome(env), "config.toml");
1351
1433
  var sessionsDir = (env) => path5.join(codexHome(env), "sessions");
@@ -1383,11 +1465,8 @@ function installNotify(cmd, env) {
1383
1465
  return { file, changed: false, note: "notify current (after Codex Computer Use)" };
1384
1466
  }
1385
1467
  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
- }
1468
+ write(replaceNotify(text, n, `notify = ${withPreviousRaw(n.raw, argv)}`));
1469
+ return { file, changed: true, note: "chained after Codex Computer Use (--previous-notify)" };
1391
1470
  }
1392
1471
  }
1393
1472
  return {
@@ -1416,8 +1495,7 @@ function uninstallNotify(env) {
1416
1495
  }
1417
1496
  const prev = previousNotify(n.argv);
1418
1497
  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);
1498
+ writeFileSync3(file, replaceNotify(text, n, `notify = ${stripPreviousRaw(n.raw)}`));
1421
1499
  return { file, changed: true };
1422
1500
  }
1423
1501
  return { file, changed: false };
@@ -1447,6 +1525,13 @@ function findRollout(threadId, env = process.env) {
1447
1525
  walk(sessionsDir(env), 0);
1448
1526
  return best;
1449
1527
  }
1528
+ function rolloutMeta(file) {
1529
+ return scanLines(file, (line) => parseLines([line])[0] ?? {});
1530
+ }
1531
+ function ofAnotherThread(file, id) {
1532
+ 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);
1533
+ return !!m?.[1] && m[1].toLowerCase() !== id.toLowerCase();
1534
+ }
1450
1535
  var codex = {
1451
1536
  name: NAME3,
1452
1537
  events: CODEX_EVENTS,
@@ -1489,11 +1574,14 @@ var codex = {
1489
1574
  if (event === "notify") {
1490
1575
  const raw = input.argv.find((a) => a.startsWith("{")) ?? input.stdin;
1491
1576
  try {
1492
- s = raw ? JSON.parse(raw) : {};
1577
+ const j = raw ? JSON.parse(raw) : {};
1578
+ if (j && typeof j === "object" && !Array.isArray(j)) s = j;
1493
1579
  } catch {
1494
1580
  }
1495
1581
  const id2 = s["thread-id"] ?? s.thread_id ?? s["session-id"] ?? s.session_id;
1496
1582
  if (!id2) return null;
1583
+ const file = findRollout(String(id2), input.env);
1584
+ if (!file || codexSubagent(rolloutMeta(file))) return null;
1497
1585
  const r2 = {
1498
1586
  session: { id: String(id2), cwd: typeof s.cwd === "string" ? s.cwd : input.cwd },
1499
1587
  events: [
@@ -1503,18 +1591,19 @@ var codex = {
1503
1591
  harnessEvent: "notify"
1504
1592
  }
1505
1593
  ],
1506
- hints: {}
1594
+ hints: {},
1595
+ transcript: file
1507
1596
  };
1508
- const file = findRollout(String(id2), input.env);
1509
- if (file) r2.transcript = file;
1510
1597
  return r2;
1511
1598
  }
1512
1599
  try {
1513
- s = input.stdin ? JSON.parse(input.stdin) : {};
1600
+ const j = input.stdin ? JSON.parse(input.stdin) : {};
1601
+ if (j && typeof j === "object" && !Array.isArray(j)) s = j;
1514
1602
  } catch {
1515
1603
  }
1516
- const id = s.session_id;
1604
+ const id = s.session_id ?? s.thread_id ?? s.conversation_id ?? s["thread-id"] ?? s["session-id"];
1517
1605
  if (!id) return null;
1606
+ if (s.transcript_path === null) return null;
1518
1607
  const session = { id: String(id), cwd: typeof s.cwd === "string" ? s.cwd : input.cwd };
1519
1608
  if (typeof s.model === "string") session.model = s.model;
1520
1609
  const turn_id = typeof s.turn_id === "string" ? s.turn_id : void 0;
@@ -1586,7 +1675,8 @@ var codex = {
1586
1675
  return null;
1587
1676
  }
1588
1677
  const r = { session, events, hints: {} };
1589
- if (typeof s.transcript_path === "string") r.transcript = s.transcript_path;
1678
+ if (typeof s.transcript_path === "string" && !ofAnotherThread(s.transcript_path, String(id)))
1679
+ r.transcript = s.transcript_path;
1590
1680
  return r;
1591
1681
  },
1592
1682
  readTranscript(file, fromLine) {
@@ -1604,16 +1694,17 @@ var codex = {
1604
1694
  return out;
1605
1695
  },
1606
1696
  backfill(sinceMs, env = process.env) {
1607
- const rows = [];
1697
+ const rows2 = [];
1608
1698
  for (const f of recentFiles(sessionsDir(env), 3, sinceMs, (n) => /^rollout-.*\.jsonl$/.test(n))) {
1609
1699
  const m = /([0-9a-f]{8}-[0-9a-f-]{27,})\.jsonl$/i.exec(path5.basename(f));
1610
1700
  if (!m?.[1]) continue;
1701
+ if (codexSubagent(rolloutMeta(f))) continue;
1611
1702
  const facts = codexFacts(f);
1612
1703
  if (!facts.started_at || !facts.last_at) continue;
1613
1704
  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 });
1705
+ rows2.push({ session: { id: m[1], ...rest }, started_at, last_at });
1615
1706
  }
1616
- return rows;
1707
+ return rows2;
1617
1708
  }
1618
1709
  };
1619
1710
  function prune3(o) {
@@ -1622,10 +1713,11 @@ function prune3(o) {
1622
1713
  }
1623
1714
  function codexFacts(file) {
1624
1715
  const { head, tail } = ends(file, 128 * 1024, 64 * 1024);
1625
- const H = parseLines(head);
1626
- const T = parseLines(tail);
1716
+ let H = parseLines(head);
1717
+ let T = parseLines(tail);
1627
1718
  const out = {};
1628
1719
  const meta = H.find((j) => j.type === "session_meta" && j.payload);
1720
+ const boundary = codexReplayBoundary(meta);
1629
1721
  if (meta?.payload) {
1630
1722
  const p = meta.payload;
1631
1723
  if (p.cwd) out.cwd = p.cwd;
@@ -1633,6 +1725,13 @@ function codexFacts(file) {
1633
1725
  if (p.git?.branch) out.branch = p.git.branch;
1634
1726
  if (p.git?.repository_url) out.repo = String(p.git.repository_url).replace(/\/\/[^/@]*@/, "//");
1635
1727
  }
1728
+ if (boundary) {
1729
+ const h = H.findIndex(boundary);
1730
+ const t = T.findIndex(boundary);
1731
+ H = h < 0 ? [] : H.slice(h);
1732
+ const found = h >= 0 || t >= 0 || scanLines(file, (line) => boundary(parseLines([line])[0] ?? null) ? true : null, Number.POSITIVE_INFINITY);
1733
+ T = t >= 0 ? T.slice(t) : found ? T : [];
1734
+ }
1636
1735
  const ctx = H.concat(T).reverse().find((j) => j.type === "turn_context" && j.payload?.model);
1637
1736
  if (ctx?.payload?.model) out.model = ctx.payload.model;
1638
1737
  for (const j of H) {
@@ -1640,12 +1739,12 @@ function codexFacts(file) {
1640
1739
  if (j.type !== "response_item" || !p || p.type !== "message" || p.role !== "user") continue;
1641
1740
  const text = (p.content ?? []).filter((b) => b && b.type === "input_text" && typeof b.text === "string").map((b) => b.text).join("\n").trim();
1642
1741
  if (!text || codexInjected(text)) continue;
1643
- const line = text.split("\n").find((l) => l.trim()) ?? "";
1742
+ const line = text.split("\n")[0];
1644
1743
  out.title = line.length > 80 ? `${line.slice(0, 80).replace(/\s+\S*$/, "")}\u2026` : line;
1645
1744
  out.title_source = "first-ask";
1646
1745
  break;
1647
1746
  }
1648
- const times = H.concat(T).map((j) => Date.parse(String(j.timestamp ?? ""))).filter((n) => n > 0);
1747
+ const times = (boundary && meta ? [meta, ...H, ...T] : H.concat(T)).map((j) => Date.parse(String(j.timestamp ?? ""))).filter((n) => n > 0);
1649
1748
  if (times.length) {
1650
1749
  out.started_at = Math.min(...times);
1651
1750
  out.last_at = Math.max(...times);
@@ -1653,11 +1752,432 @@ function codexFacts(file) {
1653
1752
  return out;
1654
1753
  }
1655
1754
 
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";
1755
+ // ../core/src/adapters/devin.ts
1756
+ import { createHash as createHash2 } from "node:crypto";
1757
+ import { existsSync as existsSync6, readlinkSync } from "node:fs";
1659
1758
  import path6 from "node:path";
1660
- var NAME4 = "gemini-cli";
1759
+
1760
+ // ../core/src/readers/sqlite.ts
1761
+ import { execFileSync as execFileSync3 } from "node:child_process";
1762
+ import { existsSync as existsSync5 } from "node:fs";
1763
+ import { createRequire } from "node:module";
1764
+ function quiet(fn) {
1765
+ const original = process.emitWarning;
1766
+ try {
1767
+ process.emitWarning = ((warning, ...rest) => {
1768
+ const kind = typeof rest[0] === "string" ? rest[0] : rest[0]?.type;
1769
+ if (kind === "ExperimentalWarning" && /sqlite/i.test(String(warning))) return;
1770
+ original.call(process, warning, ...rest);
1771
+ });
1772
+ return fn();
1773
+ } finally {
1774
+ process.emitWarning = original;
1775
+ }
1776
+ }
1777
+ var ctorCache;
1778
+ function dbCtor() {
1779
+ if (ctorCache !== void 0) return ctorCache;
1780
+ ctorCache = null;
1781
+ try {
1782
+ const req = createRequire(import.meta.url);
1783
+ const m = quiet(() => req("node:sqlite"));
1784
+ if (typeof m?.DatabaseSync === "function") ctorCache = m.DatabaseSync;
1785
+ } catch {
1786
+ }
1787
+ return ctorCache;
1788
+ }
1789
+ function plain(row) {
1790
+ if (!row || typeof row !== "object") return null;
1791
+ const out = {};
1792
+ for (const [k, v] of Object.entries(row)) {
1793
+ if (typeof v === "bigint") out[k] = Number(v);
1794
+ else if (v instanceof Uint8Array) out[k] = null;
1795
+ else out[k] = v;
1796
+ }
1797
+ return out;
1798
+ }
1799
+ var rows = (v) => {
1800
+ if (!Array.isArray(v)) return null;
1801
+ const out = [];
1802
+ for (const r of v) {
1803
+ const p = plain(r);
1804
+ if (p) out.push(p);
1805
+ }
1806
+ return out;
1807
+ };
1808
+ function viaNode(file, sql, params, o) {
1809
+ const C = dbCtor();
1810
+ if (!C) return null;
1811
+ let db;
1812
+ try {
1813
+ db = quiet(() => new C(file, { readOnly: true, timeout: o.timeoutMs }));
1814
+ } catch {
1815
+ return null;
1816
+ }
1817
+ try {
1818
+ try {
1819
+ db.exec(`PRAGMA busy_timeout = ${Math.round(o.timeoutMs)}`);
1820
+ } catch {
1821
+ }
1822
+ return quiet(() => rows(db.prepare(sql).all(...params)));
1823
+ } catch {
1824
+ return null;
1825
+ } finally {
1826
+ try {
1827
+ db.close();
1828
+ } catch {
1829
+ }
1830
+ }
1831
+ }
1832
+ var CHILD = `const { DatabaseSync } = require("node:sqlite");
1833
+ const [, file, sql, params, ms] = process.argv;
1834
+ const db = new DatabaseSync(file, { readOnly: true, timeout: Number(ms) });
1835
+ try { db.exec("PRAGMA busy_timeout = " + Number(ms)); } catch {}
1836
+ const out = db.prepare(sql).all(...JSON.parse(params));
1837
+ db.close();
1838
+ process.stdout.write(JSON.stringify(out));`;
1839
+ function viaChild(file, sql, params, o) {
1840
+ try {
1841
+ const out = execFileSync3(
1842
+ process.execPath,
1843
+ ["--experimental-sqlite", "--no-warnings", "-e", CHILD, file, sql, JSON.stringify(params), String(o.timeoutMs)],
1844
+ {
1845
+ encoding: "utf8",
1846
+ timeout: o.timeoutMs + 3e3,
1847
+ stdio: ["ignore", "pipe", "ignore"],
1848
+ maxBuffer: o.maxBuffer,
1849
+ windowsHide: true
1850
+ }
1851
+ );
1852
+ return rows(JSON.parse(out || "null"));
1853
+ } catch {
1854
+ return null;
1855
+ }
1856
+ }
1857
+ var SAFE = /^[A-Za-z0-9 ._:@/+-]{0,256}$/;
1858
+ function inlineParams(sql, params) {
1859
+ if (sql.includes("'")) return null;
1860
+ const holes = sql.split("?").length - 1;
1861
+ if (holes !== params.length) return null;
1862
+ let i = 0;
1863
+ let bad = false;
1864
+ const out = sql.replace(/\?/g, () => {
1865
+ const p = params[i++];
1866
+ if (typeof p === "number") {
1867
+ if (!Number.isFinite(p)) {
1868
+ bad = true;
1869
+ return "NULL";
1870
+ }
1871
+ return String(p);
1872
+ }
1873
+ if (typeof p !== "string" || !SAFE.test(p)) {
1874
+ bad = true;
1875
+ return "NULL";
1876
+ }
1877
+ return `'${p}'`;
1878
+ });
1879
+ return bad ? null : out;
1880
+ }
1881
+ function viaCli(file, sql, params, o) {
1882
+ const text = inlineParams(sql, params);
1883
+ if (!text) return null;
1884
+ try {
1885
+ const out = execFileSync3("sqlite3", ["-readonly", "-json", file, text], {
1886
+ encoding: "utf8",
1887
+ timeout: o.timeoutMs + 3e3,
1888
+ stdio: ["ignore", "pipe", "ignore"],
1889
+ maxBuffer: o.maxBuffer,
1890
+ windowsHide: true
1891
+ }).trim();
1892
+ if (!out) return [];
1893
+ return rows(JSON.parse(out));
1894
+ } catch {
1895
+ return null;
1896
+ }
1897
+ }
1898
+ function querySqlite(file, sql, params = [], opts = {}) {
1899
+ const o = {
1900
+ timeoutMs: opts.timeoutMs ?? 2e3,
1901
+ engine: opts.engine ?? "auto",
1902
+ maxBuffer: opts.maxBuffer ?? 32 * 1024 * 1024
1903
+ };
1904
+ if (!file) return null;
1905
+ try {
1906
+ if (!existsSync5(file)) return null;
1907
+ } catch {
1908
+ return null;
1909
+ }
1910
+ const ways = o.engine === "auto" ? [viaNode, viaChild, viaCli] : o.engine === "node" ? [viaNode] : o.engine === "child" ? [viaChild] : [viaCli];
1911
+ for (const way of ways) {
1912
+ const r = way(file, sql, params, o);
1913
+ if (r) return r;
1914
+ }
1915
+ return null;
1916
+ }
1917
+
1918
+ // ../core/src/adapters/devin.ts
1919
+ var NAME4 = "devin";
1920
+ var DEVIN_EVENTS = [
1921
+ "SessionStart",
1922
+ "UserPromptSubmit",
1923
+ "PreToolUse",
1924
+ "PostToolUse",
1925
+ "PermissionRequest",
1926
+ "Stop",
1927
+ "PostCompaction",
1928
+ "SessionEnd"
1929
+ ];
1930
+ var OPTS3 = { matcherFor: (e) => /Tool|Permission/.test(e) ? "" : void 0, timeout: 5 };
1931
+ var eventsFor3 = (lean) => lean ? DEVIN_EVENTS.filter((e) => e !== "PreToolUse") : [...DEVIN_EVENTS];
1932
+ var INSTALL_NOTE = "Devin drops a whole hooks file on one event name it doesn't know; `/hooks` in `devin` lists what loaded";
1933
+ 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";
1934
+ 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)`;
1935
+ function foreignEvent(file) {
1936
+ const hooks = readJson(file).hooks;
1937
+ if (!hooks || typeof hooks !== "object" || Array.isArray(hooks)) return void 0;
1938
+ const ok = DEVIN_EVENTS;
1939
+ return Object.keys(hooks).find((k) => !ok.includes(k));
1940
+ }
1941
+ var homeOf = (env) => env.HOME || homeDir();
1942
+ var appData = (env) => env.APPDATA || path6.join(homeOf(env), "AppData", "Roaming");
1943
+ 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");
1944
+ var configFile2 = (env) => path6.join(devinConfigDir(env), "config.json");
1945
+ 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");
1946
+ var sessionsDb = (env) => path6.join(devinDataDir(env), "sessions.db");
1947
+ var SESSION_ID = /^[A-Za-z0-9._:-]{1,128}$/;
1948
+ var validTool4 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
1949
+ var endReason3 = (r) => r === "clear" || r === "logout" || r === "exit" ? r : "other";
1950
+ var pickSource2 = (v) => typeof v === "string" && ["startup", "resume", "clear", "compact", "fork"].includes(v) ? v : "unknown";
1951
+ function errText2(v) {
1952
+ if (typeof v === "string" && v) return v.slice(0, 2e3);
1953
+ if (v && typeof v === "object") {
1954
+ const e = v;
1955
+ if (typeof e.message === "string" && e.message) return e.message.slice(0, 2e3);
1956
+ if (typeof e.error === "string" && e.error) return e.error.slice(0, 2e3);
1957
+ }
1958
+ return void 0;
1959
+ }
1960
+ var cut = (s, n) => s.length > n ? `${s.slice(0, n).replace(/\s+\S*$/, "")}\u2026` : s;
1961
+ var devin = {
1962
+ name: NAME4,
1963
+ events: DEVIN_EVENTS,
1964
+ detect: (env = process.env) => existsSync6(devinConfigDir(env)) || existsSync6(devinDataDir(env)),
1965
+ configFiles: (env = process.env) => [configFile2(env)],
1966
+ install: (cmd, env = process.env, opts = {}) => {
1967
+ const file = configFile2(env);
1968
+ const r = installClaudeShaped(file, NAME4, eventsFor3(opts.lean), cmd, OPTS3);
1969
+ if (r.skipped) return [r];
1970
+ const bad = foreignEvent(file);
1971
+ return [{ ...r, note: bad ? deadFileNote(bad) : INSTALL_NOTE }];
1972
+ },
1973
+ uninstall: (env = process.env, opts = {}) => [
1974
+ uninstallClaudeShaped(configFile2(env), DEVIN_EVENTS, { created: opts.created?.includes(configFile2(env)) })
1975
+ ],
1976
+ installed: (cmd, env = process.env, opts = {}) => {
1977
+ const file = configFile2(env);
1978
+ const state = installedClaudeShaped(file, NAME4, eventsFor3(opts.lean), cmd, OPTS3);
1979
+ const bad = state === "missing" ? void 0 : foreignEvent(file);
1980
+ return [{ file, state: bad ? "inactive" : state, note: bad ? deadFileNote(bad) : CLAUDE_NOTE }];
1981
+ },
1982
+ fromHook(input) {
1983
+ const event = input.argv[1] ?? "";
1984
+ let s = {};
1985
+ try {
1986
+ const j = input.stdin ? JSON.parse(input.stdin) : {};
1987
+ if (j && typeof j === "object" && !Array.isArray(j)) s = j;
1988
+ } catch {
1989
+ }
1990
+ const id = typeof s.session_id === "string" ? s.session_id : "";
1991
+ if (!id) return null;
1992
+ const session = { id, cwd: input.env.DEVIN_PROJECT_DIR ?? input.cwd };
1993
+ const turn_id = typeof s.prompt_id === "string" ? s.prompt_id : void 0;
1994
+ const tool = validTool4(s.tool_name) ? s.tool_name : void 0;
1995
+ const call_id = typeof s.tool_use_id === "string" ? s.tool_use_id : void 0;
1996
+ const response = s.tool_response && typeof s.tool_response === "object" && !Array.isArray(s.tool_response) ? s.tool_response : void 0;
1997
+ const permissionId = () => `perm-${createHash2("sha256").update(`${turn_id ?? ""}|${tool ?? ""}|${JSON.stringify(s.tool_input ?? null)}`).digest("hex").slice(0, 16)}`;
1998
+ const ev = (type, data) => ({ type, data: prune4(data), harnessEvent: event });
1999
+ const events = [];
2000
+ switch (event) {
2001
+ case "SessionStart":
2002
+ events.push(ev("session.started", { source: pickSource2(s.source) }));
2003
+ break;
2004
+ case "UserPromptSubmit":
2005
+ events.push(
2006
+ ev("turn.started", {
2007
+ turn_id,
2008
+ prompt_chars: typeof s.prompt === "string" ? [...s.prompt].length : void 0
2009
+ })
2010
+ );
2011
+ break;
2012
+ case "PreToolUse":
2013
+ if (tool) events.push(ev("tool.started", { tool, call_id, turn_id, input: s.tool_input }));
2014
+ break;
2015
+ case "PostToolUse":
2016
+ if (tool)
2017
+ events.push(
2018
+ ev("tool.ended", {
2019
+ tool,
2020
+ call_id,
2021
+ turn_id,
2022
+ // `ok` is required on tool.ended (PROTOCOL.md §events), so a response
2023
+ // that carries no success flag at all reads as "nothing said it failed".
2024
+ ok: response?.success !== false,
2025
+ error: errText2(response?.error),
2026
+ input: s.tool_input,
2027
+ output: response ? response.output : s.tool_response
2028
+ })
2029
+ );
2030
+ break;
2031
+ case "PermissionRequest":
2032
+ events.push(ev("attention.needed", { attention_id: call_id ?? permissionId(), kind: "permission", tool }));
2033
+ break;
2034
+ case "Stop":
2035
+ events.push(ev("turn.ended", { turn_id, reason: "stop" }));
2036
+ break;
2037
+ case "PostCompaction":
2038
+ events.push(ev("context.compacted", { trigger: "auto" }));
2039
+ break;
2040
+ case "SessionEnd":
2041
+ events.push(ev("session.ended", { reason: endReason3(s.reason) }));
2042
+ break;
2043
+ default:
2044
+ return null;
2045
+ }
2046
+ return {
2047
+ session,
2048
+ events,
2049
+ hints: {},
2050
+ transcript: transcriptRef(sessionsDb(input.env), id)
2051
+ };
2052
+ },
2053
+ readTranscript(file, fromLine) {
2054
+ const ref = splitRef(file);
2055
+ if (!ref) return { turns: [], line: fromLine };
2056
+ return readDevinChain(ref.db, ref.session, fromLine);
2057
+ },
2058
+ facts(session, transcript, _hints, _state, env = process.env) {
2059
+ const out = {};
2060
+ const version = devinVersion(env);
2061
+ if (version) out.harness_version = version;
2062
+ const account = devinAccount(env);
2063
+ if (account) out.account_id = account;
2064
+ const ref = splitRef(transcript) ?? { db: sessionsDb(env), session: session.id };
2065
+ if (!SESSION_ID.test(ref.session)) return out;
2066
+ const rows2 = querySqlite(ref.db, SESSION_SQL, [ref.session]);
2067
+ const row = rows2?.[0];
2068
+ if (!row) return out;
2069
+ Object.assign(out, sessionFacts(row));
2070
+ return out;
2071
+ },
2072
+ backfill(sinceMs, env = process.env) {
2073
+ const rows2 = querySqlite(sessionsDb(env), BACKFILL_SQL, ["text", Math.floor(sinceMs / 1e3)]);
2074
+ if (!rows2) return [];
2075
+ const out = [];
2076
+ for (const row of rows2) {
2077
+ const id = String(row.id);
2078
+ if (!SESSION_ID.test(id)) continue;
2079
+ const { started_at, last_at, ...rest } = sessionFacts(row);
2080
+ if (!started_at || !last_at) continue;
2081
+ out.push({ session: { id, ...rest }, started_at, last_at });
2082
+ }
2083
+ return out;
2084
+ }
2085
+ };
2086
+ function prune4(o) {
2087
+ for (const k of Object.keys(o)) if (o[k] === void 0) delete o[k];
2088
+ return o;
2089
+ }
2090
+ var transcriptRef = (db, session) => `${db}#${session}`;
2091
+ function splitRef(ref) {
2092
+ if (!ref) return null;
2093
+ const at = ref.lastIndexOf("#");
2094
+ if (at <= 0) return null;
2095
+ const db = ref.slice(0, at);
2096
+ const session = ref.slice(at + 1);
2097
+ return db && SESSION_ID.test(session) ? { db, session } : null;
2098
+ }
2099
+ var CHAIN_LIMIT = 4e3;
2100
+ var CHAIN_SQL = `WITH RECURSIVE chain(node_id, parent_node_id, chat_message, created_at) AS (
2101
+ SELECT node_id, parent_node_id, chat_message, created_at FROM message_nodes
2102
+ WHERE session_id = ? AND node_id = (SELECT main_chain_id FROM sessions WHERE id = ?)
2103
+ UNION ALL
2104
+ SELECT m.node_id, m.parent_node_id, m.chat_message, m.created_at FROM message_nodes m
2105
+ JOIN chain c ON m.node_id = c.parent_node_id AND m.session_id = ? AND m.node_id >= ?
2106
+ )
2107
+ SELECT node_id, chat_message, created_at FROM chain ORDER BY node_id DESC LIMIT ${CHAIN_LIMIT}`;
2108
+ var SESSION_FIELDS = `s.model, s.working_directory, s.created_at, s.last_activity_at,
2109
+ (SELECT p.content FROM prompt_history p WHERE p.session_id = s.id AND p.is_shell = 0
2110
+ ORDER BY p.timestamp ASC, p.id ASC LIMIT 1) AS first_ask`;
2111
+ var SESSION_SQL = `SELECT ${SESSION_FIELDS} FROM sessions s WHERE s.id = ? LIMIT 1`;
2112
+ var BACKFILL_SQL = `SELECT s.id, ${SESSION_FIELDS} FROM sessions s
2113
+ WHERE s.hidden = 0 AND typeof(s.id) = ? AND s.last_activity_at >= ?
2114
+ ORDER BY s.last_activity_at DESC LIMIT 500`;
2115
+ function sessionFacts(row) {
2116
+ const out = {};
2117
+ if (typeof row.model === "string" && row.model) out.model = row.model;
2118
+ if (typeof row.working_directory === "string" && path6.isAbsolute(row.working_directory))
2119
+ out.cwd = row.working_directory;
2120
+ const started = Number(row.created_at);
2121
+ const last = Number(row.last_activity_at);
2122
+ if (Number.isFinite(started) && started > 0) out.started_at = Math.round(started * 1e3);
2123
+ if (Number.isFinite(last) && last > 0) out.last_at = Math.round(last * 1e3);
2124
+ if (typeof row.first_ask === "string") {
2125
+ const line = row.first_ask.split("\n").find((l) => l.trim()) ?? "";
2126
+ if (line.trim()) {
2127
+ out.title = cut(line.trim(), 80);
2128
+ out.title_source = "first-ask";
2129
+ }
2130
+ }
2131
+ return out;
2132
+ }
2133
+ function readDevinChain(db, session, fromLine) {
2134
+ if (!SESSION_ID.test(session)) return { turns: [], line: fromLine };
2135
+ const floor = Number.isFinite(fromLine) && fromLine > 0 ? Math.floor(fromLine) : 0;
2136
+ const rows2 = querySqlite(db, CHAIN_SQL, [session, session, session, floor]);
2137
+ if (!rows2?.length) return { turns: [], line: fromLine };
2138
+ const msgs = [];
2139
+ let end = fromLine;
2140
+ for (const row of rows2) {
2141
+ const node = Number(row.node_id);
2142
+ if (!Number.isFinite(node)) continue;
2143
+ if (node + 1 > end) end = node + 1;
2144
+ if (node < fromLine) continue;
2145
+ let j;
2146
+ try {
2147
+ j = JSON.parse(String(row.chat_message ?? ""));
2148
+ } catch {
2149
+ continue;
2150
+ }
2151
+ const text = typeof j.content === "string" ? j.content.trim() : "";
2152
+ if (!text) continue;
2153
+ const secs = Number(row.created_at);
2154
+ const at = Date.parse(String(j.metadata?.created_at ?? "")) || (Number.isFinite(secs) && secs > 0 ? Math.round(secs * 1e3) : 0) || Date.now();
2155
+ if (j.role === "user" && j.metadata?.is_user_input === true) msgs.push({ role: "user", text, at, line: node });
2156
+ else if (j.role === "assistant") msgs.push({ role: "assistant", text, at, line: node });
2157
+ }
2158
+ msgs.sort((a, b) => a.line - b.line);
2159
+ return pairTurns(msgs, end);
2160
+ }
2161
+ function devinVersion(env = process.env) {
2162
+ try {
2163
+ const target = readlinkSync(path6.join(devinDataDir(env), "_versions", "current"));
2164
+ const name = path6.basename(target);
2165
+ return /^[0-9][0-9A-Za-z.+-]{0,63}$/.test(name) ? name : void 0;
2166
+ } catch {
2167
+ return void 0;
2168
+ }
2169
+ }
2170
+ function devinAccount(env = process.env) {
2171
+ const d = readJson(configFile2(env)).devin;
2172
+ const id = d && typeof d === "object" ? d.org_id : void 0;
2173
+ return typeof id === "string" && /^[A-Za-z0-9_.:-]{1,128}$/.test(id) ? id : void 0;
2174
+ }
2175
+
2176
+ // ../core/src/adapters/gemini-cli.ts
2177
+ import { execFileSync as execFileSync4 } from "node:child_process";
2178
+ import { existsSync as existsSync7, readFileSync as readFileSync5 } from "node:fs";
2179
+ import path7 from "node:path";
2180
+ var NAME5 = "gemini-cli";
1661
2181
  var GEMINI_EVENTS = [
1662
2182
  "SessionStart",
1663
2183
  "BeforeAgent",
@@ -1668,11 +2188,11 @@ var GEMINI_EVENTS = [
1668
2188
  "PreCompress",
1669
2189
  "SessionEnd"
1670
2190
  ];
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];
2191
+ var OPTS4 = { matcherFor: (e) => /Tool/.test(e) ? ".*" : void 0, timeout: 5e3 };
2192
+ var geminiDir = (env) => env.GEMINI_CLI_HOME || path7.join(homeDir(), ".gemini");
2193
+ var settings = (env) => path7.join(geminiDir(env), "settings.json");
2194
+ var validTool5 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
2195
+ var eventsFor4 = (lean) => lean ? GEMINI_EVENTS.filter((e) => e !== "BeforeTool") : [...GEMINI_EVENTS];
1676
2196
  function geminiHooksActive(settingsFile) {
1677
2197
  let tools;
1678
2198
  try {
@@ -1684,7 +2204,7 @@ function geminiHooksActive(settingsFile) {
1684
2204
  return { active: false, note: "inactive: tools.enableHooks is false in settings.json" };
1685
2205
  let version = "";
1686
2206
  try {
1687
- version = execFileSync3("gemini", ["--version"], {
2207
+ version = execFileSync4("gemini", ["--version"], {
1688
2208
  encoding: "utf8",
1689
2209
  timeout: 4e3,
1690
2210
  stdio: ["ignore", "pipe", "ignore"]
@@ -1703,13 +2223,13 @@ function geminiHooksActive(settingsFile) {
1703
2223
  };
1704
2224
  }
1705
2225
  var geminiCli = {
1706
- name: NAME4,
2226
+ name: NAME5,
1707
2227
  events: GEMINI_EVENTS,
1708
2228
  // ~/.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")),
2229
+ detect: (env = process.env) => existsSync7(settings(env)) || existsSync7(path7.join(geminiDir(env), "tmp")),
1710
2230
  configFiles: (env = process.env) => [settings(env)],
1711
2231
  install: (cmd, env = process.env, opts = {}) => {
1712
- const r = installClaudeShaped(settings(env), NAME4, eventsFor3(opts.lean), cmd, OPTS3);
2232
+ const r = installClaudeShaped(settings(env), NAME5, eventsFor4(opts.lean), cmd, OPTS4);
1713
2233
  const a = geminiHooksActive(settings(env));
1714
2234
  return [a.note && !r.skipped ? { ...r, note: [r.note, a.note].filter(Boolean).join("; ") } : r];
1715
2235
  },
@@ -1717,7 +2237,7 @@ var geminiCli = {
1717
2237
  uninstallClaudeShaped(settings(env), GEMINI_EVENTS, { created: opts.created?.includes(settings(env)) })
1718
2238
  ],
1719
2239
  installed: (cmd, env = process.env, opts = {}) => {
1720
- const state = installedClaudeShaped(settings(env), NAME4, eventsFor3(opts.lean), cmd, OPTS3);
2240
+ const state = installedClaudeShaped(settings(env), NAME5, eventsFor4(opts.lean), cmd, OPTS4);
1721
2241
  const a = geminiHooksActive(settings(env));
1722
2242
  return [
1723
2243
  {
@@ -1731,14 +2251,15 @@ var geminiCli = {
1731
2251
  const event = input.argv[1] ?? "";
1732
2252
  let s = {};
1733
2253
  try {
1734
- s = input.stdin ? JSON.parse(input.stdin) : {};
2254
+ const j = input.stdin ? JSON.parse(input.stdin) : {};
2255
+ if (j && typeof j === "object" && !Array.isArray(j)) s = j;
1735
2256
  } catch {
1736
2257
  }
1737
2258
  const id = s.session_id;
1738
2259
  if (!id) return null;
1739
2260
  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 });
2261
+ const tool = validTool5(s.tool_name) ? s.tool_name : void 0;
2262
+ const ev = (type, data) => ({ type, data: prune5(data), harnessEvent: event });
1742
2263
  const events = [];
1743
2264
  switch (event) {
1744
2265
  case "SessionStart":
@@ -1819,7 +2340,7 @@ var geminiCli = {
1819
2340
  const raw = first ? first.content ?? first.message ?? first.text : void 0;
1820
2341
  const text = typeof raw === "string" ? raw.trim() : "";
1821
2342
  if (text) {
1822
- const line = text.split("\n").find((l) => l.trim()) ?? "";
2343
+ const line = text.split("\n")[0];
1823
2344
  out.title = line.length > 80 ? `${line.slice(0, 80).replace(/\s+\S*$/, "")}\u2026` : line;
1824
2345
  out.title_source = "first-ask";
1825
2346
  }
@@ -1833,15 +2354,31 @@ var geminiCli = {
1833
2354
  backfill: () => []
1834
2355
  // 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
2356
  };
1836
- function prune4(o) {
2357
+ function prune5(o) {
1837
2358
  for (const k of Object.keys(o)) if (o[k] === void 0) delete o[k];
1838
2359
  return o;
1839
2360
  }
1840
2361
 
1841
2362
  // ../core/src/adapters/index.ts
1842
- var ADAPTERS = [claudeCode, codex, geminiCli, antigravity];
2363
+ var ADAPTERS = [claudeCode, codex, geminiCli, antigravity, devin];
1843
2364
  var adapterByName = (name) => ADAPTERS.find((a) => a.name === name);
1844
2365
 
2366
+ // ../core/src/launched-by.ts
2367
+ var LAUNCHED_BY_ENV = "SESSIONPIPE_LAUNCHED_BY";
2368
+ var LAUNCHED_BY = /^[a-z][a-z0-9-]{0,39}:[A-Za-z0-9._:-]{1,128}$/;
2369
+ function launchedBy(env) {
2370
+ const v = env[LAUNCHED_BY_ENV]?.trim();
2371
+ return v && LAUNCHED_BY.test(v) ? v : void 0;
2372
+ }
2373
+ function readHook(adapter, input) {
2374
+ const r = adapter.fromHook(input);
2375
+ if (r && !r.session.parent_id) {
2376
+ const launcher = launchedBy(input.env);
2377
+ if (launcher && launcher !== `${adapter.name}:${r.session.id}`) r.session.launched_by = launcher;
2378
+ }
2379
+ return r;
2380
+ }
2381
+
1845
2382
  // src/index.ts
1846
2383
  function matches(expect, got, pathStr = "") {
1847
2384
  if (expect === "*") return null;
@@ -1872,14 +2409,14 @@ async function runAdapterFixtures(files) {
1872
2409
  results.push({ file, ok: false, why: `no adapter ${fx.harness}` });
1873
2410
  continue;
1874
2411
  }
1875
- const r = adapter.fromHook({
2412
+ const r = readHook(adapter, {
1876
2413
  argv: fx.input.argv,
1877
2414
  stdin: fx.input.stdin,
1878
2415
  env: fx.input.env ?? {},
1879
2416
  cwd: fx.input.cwd ?? "/"
1880
2417
  });
1881
2418
  if (!r) {
1882
- results.push({ file, ok: false, why: "fromHook returned null" });
2419
+ results.push(fx.expect.length ? { file, ok: false, why: "fromHook returned null" } : { file, ok: true });
1883
2420
  continue;
1884
2421
  }
1885
2422
  let why = matches(
@@ -1890,7 +2427,7 @@ async function runAdapterFixtures(files) {
1890
2427
  if (!why)
1891
2428
  for (const [i, e] of fx.expect.entries()) {
1892
2429
  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`);
2430
+ 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
2431
  if (why) break;
1895
2432
  }
1896
2433
  results.push(why ? { file, ok: false, why } : { file, ok: true });