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/cli.js CHANGED
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
- // sessionpipe-conformance 0.7.0 · Apache-2.0 · https://sessionpipe.org
2
+ // sessionpipe-conformance 0.10.0 · Apache-2.0 · https://sessionpipe.org
3
3
 
4
4
  // src/cli.ts
5
- import { readdirSync as readdirSync5, statSync as statSync4 } from "node:fs";
6
- import path7 from "node:path";
5
+ import { readdirSync as readdirSync5, realpathSync as realpathSync2, statSync as statSync4 } from "node:fs";
6
+ import path8 from "node:path";
7
+ import { pathToFileURL } from "node:url";
7
8
 
8
9
  // src/index.ts
9
10
  import { readFileSync as readFileSync6 } from "node:fs";
@@ -23,7 +24,7 @@ import path3 from "node:path";
23
24
 
24
25
  // ../core/src/paths.ts
25
26
  import os from "node:os";
26
- var HOME = os.homedir();
27
+ var homeDir = () => os.homedir();
27
28
 
28
29
  // ../core/src/readers/files.ts
29
30
  import { closeSync, fstatSync, openSync, readdirSync, readSync, statSync } from "node:fs";
@@ -72,8 +73,8 @@ function scanLines(file, visit, cap = 16 * 1024 * 1024) {
72
73
  let chunk = Buffer.concat([rest, buf.subarray(0, n)]);
73
74
  let nl = chunk.indexOf(10);
74
75
  while (nl >= 0) {
75
- const r2 = visit(chunk.subarray(0, nl).toString("utf8"));
76
- if (r2 !== null) return r2;
76
+ const r = visit(chunk.subarray(0, nl).toString("utf8"));
77
+ if (r !== null) return r;
77
78
  chunk = chunk.subarray(nl + 1);
78
79
  nl = chunk.indexOf(10);
79
80
  }
@@ -115,6 +116,19 @@ function recentFiles(root, depth, sinceMs, match) {
115
116
  return out;
116
117
  }
117
118
 
119
+ // ../core/src/adapters/codex-rollout.ts
120
+ function codexSubagent(meta) {
121
+ if (meta?.type !== "session_meta") return false;
122
+ const p = meta.payload;
123
+ return !!p?.source?.subagent;
124
+ }
125
+ function codexReplayBoundary(meta) {
126
+ if (meta?.type !== "session_meta") return void 0;
127
+ const p = meta.payload;
128
+ if (!p || !p.forked_from_id && !p.source?.subagent) return void 0;
129
+ return (record) => typeof p.id === "string" && !!p.id && record?.type === "event_msg" && record.payload?.thread_id === p.id;
130
+ }
131
+
118
132
  // ../core/src/readers/transcripts.ts
119
133
  function pairTurns(msgs, endLine) {
120
134
  const turns = [];
@@ -137,10 +151,14 @@ ${m.text}`;
137
151
  }
138
152
  if (cur?.assistant) turns.push(cur);
139
153
  const line = cur && !cur.assistant ? cur.startLine : endLine;
140
- return { turns: turns.map((t) => ({ user: t.user, assistant: t.assistant, at: t.at, endLine: t.endLine })), line };
154
+ const out = {
155
+ turns: turns.map((t) => ({ user: t.user, assistant: t.assistant, at: t.at, endLine: t.endLine })),
156
+ line
157
+ };
158
+ if (cur && !cur.assistant && cur.user) out.open = { user: cur.user, at: cur.at };
159
+ return out;
141
160
  }
142
- var isInjected = (text) => /^<[a-z_-]+>/i.test(text) && /<\/[a-z_-]+>\s*$/i.test(text) || /^<!--\s/.test(text) || /^<command-/.test(text);
143
- var endOf = (lines) => lines[lines.length - 1] === "" ? lines.length - 1 : lines.length;
161
+ var isInjected = (text) => /^<[a-z_-]+(?:\s[^>]*)?>/i.test(text) && /<\/[a-z_-]+>\s*$/i.test(text) || /^<!--\s/.test(text) || /^<command-/.test(text);
144
162
  var parse = (raw) => {
145
163
  try {
146
164
  return JSON.parse(raw);
@@ -148,6 +166,11 @@ var parse = (raw) => {
148
166
  return null;
149
167
  }
150
168
  };
169
+ var endOf = (lines) => {
170
+ const last = lines[lines.length - 1];
171
+ if (last === void 0) return 0;
172
+ return last === "" || parse(last) === null ? lines.length - 1 : lines.length;
173
+ };
151
174
  var textParts = (c, kinds) => {
152
175
  if (typeof c === "string") return c;
153
176
  if (!Array.isArray(c)) return "";
@@ -155,13 +178,21 @@ var textParts = (c, kinds) => {
155
178
  (b) => b && typeof b === "object" && kinds.includes(b.type ?? "") && typeof b.text === "string"
156
179
  ).map((b) => b.text).join("\n");
157
180
  };
181
+ var queuedPrompt = (a) => {
182
+ if (!a || typeof a !== "object") return "";
183
+ const q = a;
184
+ if (q.type !== "queued_command" || (q.commandMode ?? "prompt") !== "prompt") return "";
185
+ if (q.origin?.kind && q.origin.kind !== "human") return "";
186
+ return textParts(q.prompt, ["text"]).trim();
187
+ };
158
188
  function readClaude(lines, from) {
159
189
  const msgs = [];
160
190
  for (let i = from; i < lines.length; i++) {
161
191
  const j = lines[i] ? parse(lines[i]) : null;
162
192
  if (!j || j.isSidechain) continue;
163
193
  const origin = j.origin;
164
- if (j.type === "user" && (j.isMeta || origin?.kind && origin.kind !== "human")) continue;
194
+ if (j.type === "user" && (j.isMeta || j.isCompactSummary || j.repeatsCompactSummary || origin?.kind && origin.kind !== "human"))
195
+ continue;
165
196
  const at = Date.parse(String(j.timestamp ?? "")) || Date.now();
166
197
  const c = j.message?.content;
167
198
  if (j.type === "user") {
@@ -171,6 +202,9 @@ function readClaude(lines, from) {
171
202
  } else if (j.type === "assistant" && Array.isArray(c)) {
172
203
  const text = textParts(c, ["text"]).trim();
173
204
  if (text) msgs.push({ role: "assistant", text, at, line: i });
205
+ } else if (j.type === "attachment") {
206
+ const text = queuedPrompt(j.attachment);
207
+ if (text && !isInjected(text)) msgs.push({ role: "user", text, at, line: i });
174
208
  }
175
209
  }
176
210
  return pairTurns(msgs, endOf(lines));
@@ -180,7 +214,10 @@ function codexInjected(text) {
180
214
  }
181
215
  function readCodex(lines, from) {
182
216
  const msgs = [];
183
- for (let i = from; i < lines.length; i++) {
217
+ const boundary = codexReplayBoundary(parse(lines.find((line) => line.trim()) ?? ""));
218
+ const start = boundary ? lines.findIndex((line) => boundary(parse(line))) : 0;
219
+ if (start < 0) return pairTurns(msgs, endOf(lines));
220
+ for (let i = Math.max(from, start); i < lines.length; i++) {
184
221
  const j = lines[i] ? parse(lines[i]) : null;
185
222
  if (!j || j.type !== "response_item") continue;
186
223
  const p = j.payload;
@@ -218,7 +255,7 @@ function readAntigravity(lines, from) {
218
255
  const at = Date.parse(String(j.created_at ?? j.timestamp ?? "")) || Date.now();
219
256
  if (j.type === "USER_INPUT") {
220
257
  const m = /<USER_REQUEST>([\s\S]*?)(?:<\/USER_REQUEST>|$)/.exec(j.content);
221
- const text = (m ? m[1] ?? "" : j.content).trim();
258
+ const text = (m ? m[1] : j.content).trim();
222
259
  if (text) msgs.push({ role: "user", text, at, line: i });
223
260
  } else if (j.type === "PLANNER_RESPONSE") {
224
261
  const text = j.content.trim();
@@ -266,6 +303,10 @@ function formatJson(value, f) {
266
303
  const text = JSON.stringify(value, null, f.indent).replace(/\n/g, f.eol);
267
304
  return f.finalNewline ? text + f.eol : text;
268
305
  }
306
+ function readJson(file) {
307
+ const f = readJsonFile(file);
308
+ return f.ok ? f.value : {};
309
+ }
269
310
  function entryFor(event, harness, cmd, o) {
270
311
  const matcher = o.matcherFor(event);
271
312
  return {
@@ -335,8 +376,7 @@ function uninstallClaudeShaped(file, events, opts = {}) {
335
376
  unlinkSync(file);
336
377
  return { file, changed: true, note: "removed (sessionpipe created it)" };
337
378
  }
338
- const next = formatJson(settings2, f);
339
- if (next !== f.raw) writeFileSync(file, next);
379
+ writeFileSync(file, formatJson(settings2, f));
340
380
  return { file, changed: true };
341
381
  }
342
382
  function installedClaudeShaped(file, harness, events, cmd, o) {
@@ -363,7 +403,7 @@ var HOOK_NAME = "sessionpipe";
363
403
  var AG_EVENTS = ["PreInvocation", "PostInvocation", "PostToolUse", "Stop"];
364
404
  var AG_STALE_PRETOOL = '{"decision":"ask","reason":"sessionpipe: an outdated PreToolUse hook is still loaded. Restart this conversation (or run sessionpipe install) to drop it."}\n';
365
405
  var antigravityAnswer = (event) => event === "PreToolUse" ? AG_STALE_PRETOOL : "{}\n";
366
- var gemini = (env) => env.GEMINI_CLI_HOME || path3.join(HOME, ".gemini");
406
+ var gemini = (env) => env.GEMINI_CLI_HOME || path3.join(homeDir(), ".gemini");
367
407
  var hooksFile = (env) => path3.join(gemini(env), "config", "hooks.json");
368
408
  var projectsDir = (env) => path3.join(gemini(env), "config", "projects");
369
409
  var dataDirs = (env) => ["antigravity", "antigravity-cli", "antigravity-ide"].map((d) => path3.join(gemini(env), d));
@@ -429,7 +469,8 @@ var antigravity = {
429
469
  const event = input.argv[1] ?? "";
430
470
  let s = {};
431
471
  try {
432
- s = input.stdin ? JSON.parse(input.stdin) : {};
472
+ const j = input.stdin ? JSON.parse(input.stdin) : {};
473
+ if (j && typeof j === "object" && !Array.isArray(j)) s = j;
433
474
  } catch {
434
475
  }
435
476
  const id = s.conversationId ?? s.session_id ?? s.sessionId;
@@ -472,9 +513,9 @@ var antigravity = {
472
513
  default:
473
514
  return { session, events: [], stdout };
474
515
  }
475
- const r2 = { session, events, stdout, hints: { workspaces } };
476
- if (typeof s.transcriptPath === "string") r2.transcript = s.transcriptPath;
477
- return r2;
516
+ const r = { session, events, stdout, hints: { workspaces } };
517
+ if (typeof s.transcriptPath === "string") r.transcript = s.transcriptPath;
518
+ return r;
478
519
  },
479
520
  readTranscript(file, fromLine) {
480
521
  let text = "";
@@ -485,19 +526,16 @@ var antigravity = {
485
526
  }
486
527
  return readAntigravity(text.split("\n"), fromLine);
487
528
  },
488
- facts(session, transcript, hints, state) {
529
+ facts(session, transcript, hints) {
489
530
  if (!transcript) return {};
490
531
  return antigravityFacts(transcript, {
491
532
  conversation: session.id,
492
533
  workspaces: hints.workspaces ?? (session.cwd ? [session.cwd] : []),
493
- state,
494
534
  env: process.env
495
535
  });
496
536
  },
497
537
  backfill(sinceMs, env = process.env) {
498
- const rows = [];
499
- const noState = { get: () => ({}), save: () => {
500
- } };
538
+ const rows2 = [];
501
539
  for (const data of dataDirs(env)) {
502
540
  let ids = [];
503
541
  try {
@@ -511,13 +549,13 @@ var antigravity = {
511
549
  } catch {
512
550
  continue;
513
551
  }
514
- const facts = antigravityFacts(f, { conversation: id, workspaces: [], state: noState, env });
552
+ const facts = antigravityFacts(f, { conversation: id, workspaces: [], env });
515
553
  if (!facts.started_at || !facts.last_at) continue;
516
554
  const { started_at, last_at, first_ask: _fa, harness_version: _hv, ...rest } = facts;
517
- rows.push({ session: { id, ...rest }, started_at, last_at });
555
+ rows2.push({ session: { id, ...rest }, started_at, last_at });
518
556
  }
519
557
  }
520
- return rows;
558
+ return rows2;
521
559
  }
522
560
  };
523
561
  function prune(o) {
@@ -542,8 +580,8 @@ var agWorkspaces = (paths) => (Array.isArray(paths) ? paths : []).map(agPath).fi
542
580
  function agVariants(p) {
543
581
  const out = [p];
544
582
  try {
545
- const r2 = realpathSync.native(p);
546
- if (r2 !== p) out.push(r2);
583
+ const r = realpathSync.native(p);
584
+ if (r !== p) out.push(r);
547
585
  } catch {
548
586
  }
549
587
  return out;
@@ -566,8 +604,8 @@ function agProjects(dir) {
566
604
  continue;
567
605
  }
568
606
  const folders = [];
569
- for (const r2 of j?.projectResources?.resources ?? []) {
570
- const f = agPath(r2?.folderUri);
607
+ for (const r of j?.projectResources?.resources ?? []) {
608
+ const f = agPath(r?.folderUri ?? r?.gitFolder?.folderUri);
571
609
  if (f) folders.push(...agVariants(f));
572
610
  }
573
611
  if (folders.length) out.push({ id, folders });
@@ -621,7 +659,7 @@ function antigravityFacts(file, opts) {
621
659
  for (const j of H) {
622
660
  if (j.type !== "USER_INPUT" || typeof j.content !== "string") continue;
623
661
  const m = /<USER_REQUEST>([\s\S]*?)(?:<\/USER_REQUEST>|$)/.exec(j.content);
624
- const text = (m ? m[1] ?? "" : j.content).trim();
662
+ const text = (m ? m[1] : j.content).trim();
625
663
  const line = (text.split("\n").find((l) => l.trim()) ?? "").trim();
626
664
  if (!line || /^<[a-z_-]+>/i.test(line)) continue;
627
665
  out.title = line.length > 80 ? `${line.slice(0, 80).replace(/\s+\S*$/, "")}\u2026` : line;
@@ -669,10 +707,9 @@ function gitFacts(cwd) {
669
707
  return out;
670
708
  }
671
709
  function stripCredentials(url) {
672
- let u = url.replace(/\/\/[^/@]*@/, "//");
673
- const ssh = /^(?:ssh:\/\/)?git@([^:/]+)[:/](.+?)(?:\.git)?$/.exec(u);
674
- if (ssh) u = `${ssh[1]}/${ssh[2]}`;
675
- return u.replace(/^https?:\/\//, "").replace(/\.git$/, "");
710
+ const ssh = /^(?:ssh:\/\/)?git@([^:/]+)[:/](.+?)(?:\.git)?$/.exec(url);
711
+ if (ssh) return `${ssh[1]}/${ssh[2]}`;
712
+ return url.replace(/\/\/[^/@]*@/, "//").replace(/^https?:\/\//, "").replace(/\.git$/, "");
676
713
  }
677
714
 
678
715
  // ../core/src/adapters/claude-code.ts
@@ -703,12 +740,12 @@ var OPTS = {
703
740
  timeoutFor: (e) => e === "PermissionRequest" ? 130 : void 0
704
741
  };
705
742
  var DESKTOP_ID = /^local_[A-Za-z0-9-]{1,64}$/;
706
- function claudeDirs(env = process.env, home = env.HOME || HOME) {
743
+ function claudeDirs(env = process.env, home = env.HOME || homeDir()) {
707
744
  const out = [];
708
745
  const add = (d) => {
709
746
  if (!d) return;
710
- const r2 = path4.resolve(d);
711
- if (!out.includes(r2)) out.push(r2);
747
+ const r = path4.resolve(d);
748
+ if (!out.includes(r)) out.push(r);
712
749
  };
713
750
  add(path4.join(home, ".claude"));
714
751
  add(env.CLAUDE_CONFIG_DIR);
@@ -725,7 +762,7 @@ function claudeDirs(env = process.env, home = env.HOME || HOME) {
725
762
  var settingsIn = (dir) => path4.join(dir, "settings.json");
726
763
  var configDirOf = (transcript) => path4.dirname(path4.dirname(path4.dirname(transcript)));
727
764
  var validTool2 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
728
- var endReason = (r2) => r2 === "clear" || r2 === "logout" || r2 === "exit" ? r2 : r2 === "prompt_input_exit" ? "exit" : "other";
765
+ var endReason = (r) => r === "clear" || r === "logout" || r === "exit" ? r : r === "prompt_input_exit" ? "exit" : "other";
729
766
  var notifKind = (t) => t === "permission_prompt" ? "permission" : t === "idle_prompt" ? "idle" : t === "elicitation_dialog" ? "elicitation" : "question";
730
767
  var claudeCode = {
731
768
  name: NAME2,
@@ -744,11 +781,13 @@ var claudeCode = {
744
781
  const event = input.argv[1] ?? "";
745
782
  let s = {};
746
783
  try {
747
- s = input.stdin ? JSON.parse(input.stdin) : {};
784
+ const j = input.stdin ? JSON.parse(input.stdin) : {};
785
+ if (j && typeof j === "object" && !Array.isArray(j)) s = j;
748
786
  } catch {
749
787
  }
750
788
  const id = s.session_id ?? s.sessionId;
751
789
  if (!id) return null;
790
+ if (notClaudeCode(String(id), s)) return null;
752
791
  const session = { id: String(id), cwd: typeof s.cwd === "string" ? s.cwd : input.cwd };
753
792
  if (typeof s.model === "string") session.model = s.model;
754
793
  if (typeof s.agent_id === "string" && event.startsWith("Subagent") === false && s.agent_id)
@@ -765,14 +804,28 @@ var claudeCode = {
765
804
  case "SessionStart":
766
805
  events.push(ev("session.started", { source: pickSource(s.source ?? s.trigger) }));
767
806
  break;
768
- case "UserPromptSubmit":
807
+ case "UserPromptSubmit": {
808
+ const prompt = typeof s.prompt === "string" ? s.prompt : void 0;
769
809
  events.push(
770
810
  ev("turn.started", {
771
811
  turn_id,
772
- prompt_chars: typeof s.prompt === "string" ? [...s.prompt].length : void 0
812
+ prompt_chars: prompt === void 0 ? void 0 : [...prompt].length,
813
+ // Claude Code's hook schema documents `source`
814
+ // (user | sdk | system | loop_wakeup | schedule_wakeup | poll_event) and says a
815
+ // payload may omit it while the field rolls out; 2.1.304's payload has no such
816
+ // key at all. Forwarded verbatim, shape-guarded only: the list is the harness's
817
+ // to extend.
818
+ submitted_by: harnessWord(s.source),
819
+ // One of Claude Code's own prompt wrappers: the prompt is a block the harness
820
+ // wrote, so it is not the person's words. Absent is unknown, never "the person" —
821
+ // and `true` is never "no person caused this turn", because a Talk or
822
+ // `sessionpipe wait` delivery hands the person's own message over inside a
823
+ // <task-notification> (PROTOCOL.md §4).
824
+ injected: prompt !== void 0 && injectedBlock(prompt) ? true : void 0
773
825
  })
774
826
  );
775
827
  break;
828
+ }
776
829
  case "PreToolUse":
777
830
  if (tool) events.push(ev("tool.started", { tool, call_id, turn_id, agent_id, input: s.tool_input }));
778
831
  break;
@@ -827,7 +880,13 @@ var claudeCode = {
827
880
  );
828
881
  break;
829
882
  case "Stop":
830
- events.push(ev("turn.ended", { turn_id, reason: "stop" }));
883
+ events.push(
884
+ ev("turn.ended", {
885
+ turn_id,
886
+ reason: "stop",
887
+ background: Array.isArray(s.background_tasks) ? s.background_tasks.length : void 0
888
+ })
889
+ );
831
890
  break;
832
891
  case "StopFailure":
833
892
  events.push(
@@ -867,12 +926,15 @@ var claudeCode = {
867
926
  return null;
868
927
  }
869
928
  const hints = {};
929
+ if (event === "UserPromptSubmit" && typeof s.prompt === "string") hints.prompt = s.prompt;
870
930
  const host = input.env.CLAUDE_CODE_HOST_SESSION_ID;
871
931
  if (host && DESKTOP_ID.test(host)) hints.desktop_id = host;
932
+ const attended = input.env.CLAUDE_CODE_SESSION_ATTENDED;
933
+ if (attended === "1" || attended === "0") hints.attended = attended === "1" || !!input.env.SESSIONPIPE_CONTROL_JOB;
872
934
  if (event !== "PreToolUse" && event !== "PostToolUse") hints.claude_pid = claudePid(input.env);
873
- const r2 = { session, events, hints };
874
- if (typeof s.transcript_path === "string") r2.transcript = s.transcript_path;
875
- return r2;
935
+ const r = { session, events, hints };
936
+ if (typeof s.transcript_path === "string") r.transcript = s.transcript_path;
937
+ return r;
876
938
  },
877
939
  readTranscript(file, fromLine) {
878
940
  let text = "";
@@ -889,10 +951,11 @@ var claudeCode = {
889
951
  const cwd = out.cwd ?? session.cwd;
890
952
  Object.assign(out, gitFacts(cwd), out.branch ? { branch: out.branch } : {});
891
953
  if (!out.url && typeof hints.desktop_id === "string") out.url = `claude://code/continue/${hints.desktop_id}`;
954
+ if (typeof hints.attended === "boolean") out.attended = hints.attended || !!out.url;
892
955
  return out;
893
956
  },
894
957
  backfill(sinceMs, env = process.env) {
895
- const rows = [];
958
+ const rows2 = [];
896
959
  const files = [];
897
960
  for (const d of claudeDirs(env))
898
961
  files.push(...recentFiles(path4.join(d, "projects"), 1, sinceMs, (n) => /^[0-9a-f-]{36}\.jsonl$/.test(n)));
@@ -904,22 +967,29 @@ var claudeCode = {
904
967
  if (!facts.started_at || !facts.last_at) continue;
905
968
  const { started_at, last_at, first_ask: _fa, harness_version: _hv, ...rest } = facts;
906
969
  const cwd = rest.cwd;
907
- rows.push({
970
+ rows2.push({
908
971
  session: { id, ...rest, ...gitFacts(cwd), ...rest.branch ? { branch: rest.branch } : {} },
909
972
  started_at,
910
973
  last_at
911
974
  });
912
975
  }
913
- return rows;
976
+ return rows2;
914
977
  }
915
978
  };
916
979
  function prune2(o) {
917
980
  for (const k of Object.keys(o)) if (o[k] === void 0) delete o[k];
918
981
  return o;
919
982
  }
983
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
984
+ function notClaudeCode(id, s) {
985
+ return typeof s.transcript_path !== "string" && !UUID.test(id);
986
+ }
920
987
  function pickSource(v) {
921
988
  return typeof v === "string" && ["startup", "resume", "clear", "compact", "fork"].includes(v) ? v : "unknown";
922
989
  }
990
+ var harnessWord = (v) => typeof v === "string" && /^[A-Za-z0-9][A-Za-z0-9_.-]{0,39}$/.test(v) ? v : void 0;
991
+ var WRAPPER = /^<(?:task-notification|cross-session-message|system-reminder|local-command-stdout|command-[a-z-]+)[\s/>]/i;
992
+ var injectedBlock = (prompt) => WRAPPER.test(prompt.trim());
923
993
  function errText(v) {
924
994
  if (typeof v === "string") return v.slice(0, 2e3);
925
995
  if (v && typeof v === "object") {
@@ -943,13 +1013,13 @@ var NAME_RANK = {
943
1013
  };
944
1014
  function registryEntry(j) {
945
1015
  if (!j || typeof j !== "object") return null;
946
- const r2 = j;
947
- const source = r2.nameSource ? NAME_RANK[r2.nameSource] : void 0;
1016
+ const r = j;
1017
+ const source = r.nameSource ? NAME_RANK[r.nameSource] : void 0;
948
1018
  return {
949
- sessionId: r2.sessionId ? String(r2.sessionId) : null,
950
- name: source && typeof r2.name === "string" && r2.name.trim() ? r2.name.trim() : null,
1019
+ sessionId: r.sessionId ? String(r.sessionId) : null,
1020
+ name: source && typeof r.name === "string" && r.name.trim() ? r.name.trim() : null,
951
1021
  name_source: source ?? null,
952
- bridge: typeof r2.bridgeSessionId === "string" ? r2.bridgeSessionId : null
1022
+ bridge: typeof r.bridgeSessionId === "string" ? r.bridgeSessionId : null
953
1023
  };
954
1024
  }
955
1025
  var liveCache = /* @__PURE__ */ new Map();
@@ -1022,23 +1092,47 @@ function parentOf() {
1022
1092
  }
1023
1093
  return (pid) => table.get(pid) || 0;
1024
1094
  }
1025
- var accountCache = /* @__PURE__ */ new Map();
1026
- function claudeAccount(dir) {
1027
- const hit = accountCache.get(dir);
1028
- if (hit !== void 0) return hit;
1095
+ var EMAIL_RE = /^[^@\s]{1,64}@[^@\s]{1,189}$/;
1096
+ function claudeLogin(dir, home = homeDir()) {
1029
1097
  const files = [path4.join(dir, ".claude.json")];
1030
- if (path4.resolve(dir) === path4.join(HOME, ".claude")) files.unshift(path4.join(HOME, ".claude.json"));
1031
- let acct = null;
1098
+ if (path4.resolve(dir) === path4.join(home, ".claude")) files.unshift(path4.join(home, ".claude.json"));
1032
1099
  for (const f of files) {
1033
1100
  try {
1034
1101
  const a = JSON.parse(readFileSync3(f, "utf8")).oauthAccount;
1035
- if (a?.accountUuid) {
1036
- acct = String(a.accountUuid);
1037
- break;
1102
+ if (typeof a?.accountUuid === "string" && a.accountUuid) {
1103
+ const email = typeof a.emailAddress === "string" && EMAIL_RE.test(a.emailAddress) ? a.emailAddress : void 0;
1104
+ return { id: a.accountUuid.slice(0, 128), ...email ? { email } : {} };
1038
1105
  }
1039
1106
  } catch {
1040
1107
  }
1041
1108
  }
1109
+ return apiAccount(dir);
1110
+ }
1111
+ var ANTHROPIC_HOST = /(^|\.)anthropic\.com$/i;
1112
+ function apiAccount(dir) {
1113
+ let base;
1114
+ try {
1115
+ base = JSON.parse(readFileSync3(settingsIn(dir), "utf8")).env?.ANTHROPIC_BASE_URL;
1116
+ } catch {
1117
+ return null;
1118
+ }
1119
+ if (typeof base !== "string") return null;
1120
+ let u;
1121
+ try {
1122
+ u = new URL(base.trim());
1123
+ } catch {
1124
+ return null;
1125
+ }
1126
+ if (u.protocol !== "https:" && u.protocol !== "http:" || !u.hostname || ANTHROPIC_HOST.test(u.hostname))
1127
+ return null;
1128
+ const origin = `${u.protocol}//${u.host}`;
1129
+ return { id: `api-${createHash("sha256").update(origin).digest("hex").slice(0, 16)}`, name: u.host };
1130
+ }
1131
+ var accountCache = /* @__PURE__ */ new Map();
1132
+ function claudeAccount(dir) {
1133
+ const hit = accountCache.get(dir);
1134
+ if (hit !== void 0) return hit;
1135
+ const acct = claudeLogin(dir)?.id ?? null;
1042
1136
  accountCache.set(dir, acct);
1043
1137
  return acct;
1044
1138
  }
@@ -1258,18 +1352,13 @@ function parseArgv(raw) {
1258
1352
  function readNotify(text) {
1259
1353
  const { head, tail, at } = split(text);
1260
1354
  const m = NOTIFY_LINE.exec(head);
1261
- if (m)
1262
- return {
1263
- raw: m[1] ?? "",
1264
- argv: parseArgv(m[1] ?? ""),
1265
- index: m.index,
1266
- length: m[0].length,
1267
- misplaced: false,
1268
- other: !OURS2.test(m[1] ?? "")
1269
- };
1355
+ if (m) {
1356
+ const raw = m[1];
1357
+ return { raw, argv: parseArgv(raw), index: m.index, length: m[0].length, misplaced: false, other: !OURS2.test(raw) };
1358
+ }
1270
1359
  const k = NOTIFY_KEY.exec(head);
1271
1360
  if (k) {
1272
- const rest = head.slice(k.index).split("\n")[0] ?? "";
1361
+ const rest = head.slice(k.index).split("\n")[0];
1273
1362
  return {
1274
1363
  raw: rest.replace(NOTIFY_KEY, "").trim(),
1275
1364
  argv: null,
@@ -1280,15 +1369,11 @@ function readNotify(text) {
1280
1369
  };
1281
1370
  }
1282
1371
  const t = NOTIFY_LINE.exec(tail);
1283
- if (t && OURS2.test(t[1] ?? ""))
1284
- return {
1285
- raw: t[1] ?? "",
1286
- argv: parseArgv(t[1] ?? ""),
1287
- index: at + t.index,
1288
- length: t[0].length,
1289
- misplaced: true,
1290
- other: false
1291
- };
1372
+ if (t) {
1373
+ const raw = t[1];
1374
+ if (OURS2.test(raw))
1375
+ return { raw, argv: parseArgv(raw), index: at + t.index, length: t[0].length, misplaced: true, other: false };
1376
+ }
1292
1377
  return null;
1293
1378
  }
1294
1379
  function dropLine(text, index, length) {
@@ -1307,9 +1392,7 @@ function withNotify(text, line) {
1307
1392
  if (n?.misplaced) return insertTopLevel(dropLine(text, n.index, n.length), line);
1308
1393
  return insertTopLevel(text, line);
1309
1394
  }
1310
- function replaceNotify(text, line) {
1311
- const n = readNotify(text);
1312
- if (!n || n.misplaced || !n.length) return null;
1395
+ function replaceNotify(text, n, line) {
1313
1396
  return text.slice(0, n.index) + line + text.slice(n.index + n.length);
1314
1397
  }
1315
1398
  function withoutNotify(text) {
@@ -1350,12 +1433,12 @@ var CODEX_EVENTS = [
1350
1433
  ];
1351
1434
  var OPTS2 = { matcherFor: (e) => /Tool|Permission/.test(e) ? "" : void 0, timeout: 5 };
1352
1435
  var eventsFor2 = (lean) => lean ? CODEX_EVENTS.filter((e) => e !== "PreToolUse") : [...CODEX_EVENTS];
1353
- var codexHome = (env) => env.CODEX_HOME || path5.join(HOME, ".codex");
1436
+ var codexHome = (env) => env.CODEX_HOME || path5.join(homeDir(), ".codex");
1354
1437
  var hooksFile2 = (env) => path5.join(codexHome(env), "hooks.json");
1355
1438
  var configFile = (env) => path5.join(codexHome(env), "config.toml");
1356
1439
  var sessionsDir = (env) => path5.join(codexHome(env), "sessions");
1357
1440
  var validTool3 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
1358
- var endReason2 = (r2) => r2 === "clear" || r2 === "logout" || r2 === "exit" ? r2 : "other";
1441
+ var endReason2 = (r) => r === "clear" || r === "logout" || r === "exit" ? r : "other";
1359
1442
  function installNotify(cmd, env) {
1360
1443
  const file = configFile(env);
1361
1444
  let text = "";
@@ -1388,11 +1471,8 @@ function installNotify(cmd, env) {
1388
1471
  return { file, changed: false, note: "notify current (after Codex Computer Use)" };
1389
1472
  }
1390
1473
  if (!prev || OURS2.test(JSON.stringify(prev))) {
1391
- const next = replaceNotify(text, `notify = ${withPreviousRaw(n.raw, argv)}`);
1392
- if (next) {
1393
- write(next);
1394
- return { file, changed: true, note: "chained after Codex Computer Use (--previous-notify)" };
1395
- }
1474
+ write(replaceNotify(text, n, `notify = ${withPreviousRaw(n.raw, argv)}`));
1475
+ return { file, changed: true, note: "chained after Codex Computer Use (--previous-notify)" };
1396
1476
  }
1397
1477
  }
1398
1478
  return {
@@ -1421,8 +1501,7 @@ function uninstallNotify(env) {
1421
1501
  }
1422
1502
  const prev = previousNotify(n.argv);
1423
1503
  if (isComputerUse(n.argv) && prev && OURS2.test(JSON.stringify(prev))) {
1424
- const next = replaceNotify(text, `notify = ${stripPreviousRaw(n.raw)}`);
1425
- if (next) writeFileSync3(file, next);
1504
+ writeFileSync3(file, replaceNotify(text, n, `notify = ${stripPreviousRaw(n.raw)}`));
1426
1505
  return { file, changed: true };
1427
1506
  }
1428
1507
  return { file, changed: false };
@@ -1452,6 +1531,13 @@ function findRollout(threadId, env = process.env) {
1452
1531
  walk2(sessionsDir(env), 0);
1453
1532
  return best;
1454
1533
  }
1534
+ function rolloutMeta(file) {
1535
+ return scanLines(file, (line) => parseLines([line])[0] ?? {});
1536
+ }
1537
+ function ofAnotherThread(file, id) {
1538
+ 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);
1539
+ return !!m?.[1] && m[1].toLowerCase() !== id.toLowerCase();
1540
+ }
1455
1541
  var codex = {
1456
1542
  name: NAME3,
1457
1543
  events: CODEX_EVENTS,
@@ -1494,12 +1580,15 @@ var codex = {
1494
1580
  if (event === "notify") {
1495
1581
  const raw = input.argv.find((a) => a.startsWith("{")) ?? input.stdin;
1496
1582
  try {
1497
- s = raw ? JSON.parse(raw) : {};
1583
+ const j = raw ? JSON.parse(raw) : {};
1584
+ if (j && typeof j === "object" && !Array.isArray(j)) s = j;
1498
1585
  } catch {
1499
1586
  }
1500
1587
  const id2 = s["thread-id"] ?? s.thread_id ?? s["session-id"] ?? s.session_id;
1501
1588
  if (!id2) return null;
1502
- const r3 = {
1589
+ const file = findRollout(String(id2), input.env);
1590
+ if (!file || codexSubagent(rolloutMeta(file))) return null;
1591
+ const r2 = {
1503
1592
  session: { id: String(id2), cwd: typeof s.cwd === "string" ? s.cwd : input.cwd },
1504
1593
  events: [
1505
1594
  {
@@ -1508,18 +1597,19 @@ var codex = {
1508
1597
  harnessEvent: "notify"
1509
1598
  }
1510
1599
  ],
1511
- hints: {}
1600
+ hints: {},
1601
+ transcript: file
1512
1602
  };
1513
- const file = findRollout(String(id2), input.env);
1514
- if (file) r3.transcript = file;
1515
- return r3;
1603
+ return r2;
1516
1604
  }
1517
1605
  try {
1518
- s = input.stdin ? JSON.parse(input.stdin) : {};
1606
+ const j = input.stdin ? JSON.parse(input.stdin) : {};
1607
+ if (j && typeof j === "object" && !Array.isArray(j)) s = j;
1519
1608
  } catch {
1520
1609
  }
1521
- const id = s.session_id;
1610
+ const id = s.session_id ?? s.thread_id ?? s.conversation_id ?? s["thread-id"] ?? s["session-id"];
1522
1611
  if (!id) return null;
1612
+ if (s.transcript_path === null) return null;
1523
1613
  const session = { id: String(id), cwd: typeof s.cwd === "string" ? s.cwd : input.cwd };
1524
1614
  if (typeof s.model === "string") session.model = s.model;
1525
1615
  const turn_id = typeof s.turn_id === "string" ? s.turn_id : void 0;
@@ -1590,9 +1680,10 @@ var codex = {
1590
1680
  default:
1591
1681
  return null;
1592
1682
  }
1593
- const r2 = { session, events, hints: {} };
1594
- if (typeof s.transcript_path === "string") r2.transcript = s.transcript_path;
1595
- return r2;
1683
+ const r = { session, events, hints: {} };
1684
+ if (typeof s.transcript_path === "string" && !ofAnotherThread(s.transcript_path, String(id)))
1685
+ r.transcript = s.transcript_path;
1686
+ return r;
1596
1687
  },
1597
1688
  readTranscript(file, fromLine) {
1598
1689
  let text = "";
@@ -1609,16 +1700,17 @@ var codex = {
1609
1700
  return out;
1610
1701
  },
1611
1702
  backfill(sinceMs, env = process.env) {
1612
- const rows = [];
1703
+ const rows2 = [];
1613
1704
  for (const f of recentFiles(sessionsDir(env), 3, sinceMs, (n) => /^rollout-.*\.jsonl$/.test(n))) {
1614
1705
  const m = /([0-9a-f]{8}-[0-9a-f-]{27,})\.jsonl$/i.exec(path5.basename(f));
1615
1706
  if (!m?.[1]) continue;
1707
+ if (codexSubagent(rolloutMeta(f))) continue;
1616
1708
  const facts = codexFacts(f);
1617
1709
  if (!facts.started_at || !facts.last_at) continue;
1618
1710
  const { started_at, last_at, first_ask: _fa, harness_version: _hv, ...rest } = facts;
1619
- rows.push({ session: { id: m[1], ...rest }, started_at, last_at });
1711
+ rows2.push({ session: { id: m[1], ...rest }, started_at, last_at });
1620
1712
  }
1621
- return rows;
1713
+ return rows2;
1622
1714
  }
1623
1715
  };
1624
1716
  function prune3(o) {
@@ -1627,10 +1719,11 @@ function prune3(o) {
1627
1719
  }
1628
1720
  function codexFacts(file) {
1629
1721
  const { head, tail } = ends(file, 128 * 1024, 64 * 1024);
1630
- const H = parseLines(head);
1631
- const T = parseLines(tail);
1722
+ let H = parseLines(head);
1723
+ let T = parseLines(tail);
1632
1724
  const out = {};
1633
1725
  const meta = H.find((j) => j.type === "session_meta" && j.payload);
1726
+ const boundary = codexReplayBoundary(meta);
1634
1727
  if (meta?.payload) {
1635
1728
  const p = meta.payload;
1636
1729
  if (p.cwd) out.cwd = p.cwd;
@@ -1638,6 +1731,13 @@ function codexFacts(file) {
1638
1731
  if (p.git?.branch) out.branch = p.git.branch;
1639
1732
  if (p.git?.repository_url) out.repo = String(p.git.repository_url).replace(/\/\/[^/@]*@/, "//");
1640
1733
  }
1734
+ if (boundary) {
1735
+ const h = H.findIndex(boundary);
1736
+ const t = T.findIndex(boundary);
1737
+ H = h < 0 ? [] : H.slice(h);
1738
+ const found = h >= 0 || t >= 0 || scanLines(file, (line) => boundary(parseLines([line])[0] ?? null) ? true : null, Number.POSITIVE_INFINITY);
1739
+ T = t >= 0 ? T.slice(t) : found ? T : [];
1740
+ }
1641
1741
  const ctx = H.concat(T).reverse().find((j) => j.type === "turn_context" && j.payload?.model);
1642
1742
  if (ctx?.payload?.model) out.model = ctx.payload.model;
1643
1743
  for (const j of H) {
@@ -1645,12 +1745,12 @@ function codexFacts(file) {
1645
1745
  if (j.type !== "response_item" || !p || p.type !== "message" || p.role !== "user") continue;
1646
1746
  const text = (p.content ?? []).filter((b) => b && b.type === "input_text" && typeof b.text === "string").map((b) => b.text).join("\n").trim();
1647
1747
  if (!text || codexInjected(text)) continue;
1648
- const line = text.split("\n").find((l) => l.trim()) ?? "";
1748
+ const line = text.split("\n")[0];
1649
1749
  out.title = line.length > 80 ? `${line.slice(0, 80).replace(/\s+\S*$/, "")}\u2026` : line;
1650
1750
  out.title_source = "first-ask";
1651
1751
  break;
1652
1752
  }
1653
- const times = H.concat(T).map((j) => Date.parse(String(j.timestamp ?? ""))).filter((n) => n > 0);
1753
+ const times = (boundary && meta ? [meta, ...H, ...T] : H.concat(T)).map((j) => Date.parse(String(j.timestamp ?? ""))).filter((n) => n > 0);
1654
1754
  if (times.length) {
1655
1755
  out.started_at = Math.min(...times);
1656
1756
  out.last_at = Math.max(...times);
@@ -1658,11 +1758,432 @@ function codexFacts(file) {
1658
1758
  return out;
1659
1759
  }
1660
1760
 
1661
- // ../core/src/adapters/gemini-cli.ts
1662
- import { execFileSync as execFileSync3 } from "node:child_process";
1663
- import { existsSync as existsSync5, readFileSync as readFileSync5 } from "node:fs";
1761
+ // ../core/src/adapters/devin.ts
1762
+ import { createHash as createHash2 } from "node:crypto";
1763
+ import { existsSync as existsSync6, readlinkSync } from "node:fs";
1664
1764
  import path6 from "node:path";
1665
- var NAME4 = "gemini-cli";
1765
+
1766
+ // ../core/src/readers/sqlite.ts
1767
+ import { execFileSync as execFileSync3 } from "node:child_process";
1768
+ import { existsSync as existsSync5 } from "node:fs";
1769
+ import { createRequire } from "node:module";
1770
+ function quiet(fn) {
1771
+ const original = process.emitWarning;
1772
+ try {
1773
+ process.emitWarning = ((warning, ...rest) => {
1774
+ const kind = typeof rest[0] === "string" ? rest[0] : rest[0]?.type;
1775
+ if (kind === "ExperimentalWarning" && /sqlite/i.test(String(warning))) return;
1776
+ original.call(process, warning, ...rest);
1777
+ });
1778
+ return fn();
1779
+ } finally {
1780
+ process.emitWarning = original;
1781
+ }
1782
+ }
1783
+ var ctorCache;
1784
+ function dbCtor() {
1785
+ if (ctorCache !== void 0) return ctorCache;
1786
+ ctorCache = null;
1787
+ try {
1788
+ const req = createRequire(import.meta.url);
1789
+ const m = quiet(() => req("node:sqlite"));
1790
+ if (typeof m?.DatabaseSync === "function") ctorCache = m.DatabaseSync;
1791
+ } catch {
1792
+ }
1793
+ return ctorCache;
1794
+ }
1795
+ function plain(row) {
1796
+ if (!row || typeof row !== "object") return null;
1797
+ const out = {};
1798
+ for (const [k, v] of Object.entries(row)) {
1799
+ if (typeof v === "bigint") out[k] = Number(v);
1800
+ else if (v instanceof Uint8Array) out[k] = null;
1801
+ else out[k] = v;
1802
+ }
1803
+ return out;
1804
+ }
1805
+ var rows = (v) => {
1806
+ if (!Array.isArray(v)) return null;
1807
+ const out = [];
1808
+ for (const r of v) {
1809
+ const p = plain(r);
1810
+ if (p) out.push(p);
1811
+ }
1812
+ return out;
1813
+ };
1814
+ function viaNode(file, sql, params, o) {
1815
+ const C = dbCtor();
1816
+ if (!C) return null;
1817
+ let db;
1818
+ try {
1819
+ db = quiet(() => new C(file, { readOnly: true, timeout: o.timeoutMs }));
1820
+ } catch {
1821
+ return null;
1822
+ }
1823
+ try {
1824
+ try {
1825
+ db.exec(`PRAGMA busy_timeout = ${Math.round(o.timeoutMs)}`);
1826
+ } catch {
1827
+ }
1828
+ return quiet(() => rows(db.prepare(sql).all(...params)));
1829
+ } catch {
1830
+ return null;
1831
+ } finally {
1832
+ try {
1833
+ db.close();
1834
+ } catch {
1835
+ }
1836
+ }
1837
+ }
1838
+ var CHILD = `const { DatabaseSync } = require("node:sqlite");
1839
+ const [, file, sql, params, ms] = process.argv;
1840
+ const db = new DatabaseSync(file, { readOnly: true, timeout: Number(ms) });
1841
+ try { db.exec("PRAGMA busy_timeout = " + Number(ms)); } catch {}
1842
+ const out = db.prepare(sql).all(...JSON.parse(params));
1843
+ db.close();
1844
+ process.stdout.write(JSON.stringify(out));`;
1845
+ function viaChild(file, sql, params, o) {
1846
+ try {
1847
+ const out = execFileSync3(
1848
+ process.execPath,
1849
+ ["--experimental-sqlite", "--no-warnings", "-e", CHILD, file, sql, JSON.stringify(params), String(o.timeoutMs)],
1850
+ {
1851
+ encoding: "utf8",
1852
+ timeout: o.timeoutMs + 3e3,
1853
+ stdio: ["ignore", "pipe", "ignore"],
1854
+ maxBuffer: o.maxBuffer,
1855
+ windowsHide: true
1856
+ }
1857
+ );
1858
+ return rows(JSON.parse(out || "null"));
1859
+ } catch {
1860
+ return null;
1861
+ }
1862
+ }
1863
+ var SAFE = /^[A-Za-z0-9 ._:@/+-]{0,256}$/;
1864
+ function inlineParams(sql, params) {
1865
+ if (sql.includes("'")) return null;
1866
+ const holes = sql.split("?").length - 1;
1867
+ if (holes !== params.length) return null;
1868
+ let i = 0;
1869
+ let bad = false;
1870
+ const out = sql.replace(/\?/g, () => {
1871
+ const p = params[i++];
1872
+ if (typeof p === "number") {
1873
+ if (!Number.isFinite(p)) {
1874
+ bad = true;
1875
+ return "NULL";
1876
+ }
1877
+ return String(p);
1878
+ }
1879
+ if (typeof p !== "string" || !SAFE.test(p)) {
1880
+ bad = true;
1881
+ return "NULL";
1882
+ }
1883
+ return `'${p}'`;
1884
+ });
1885
+ return bad ? null : out;
1886
+ }
1887
+ function viaCli(file, sql, params, o) {
1888
+ const text = inlineParams(sql, params);
1889
+ if (!text) return null;
1890
+ try {
1891
+ const out = execFileSync3("sqlite3", ["-readonly", "-json", file, text], {
1892
+ encoding: "utf8",
1893
+ timeout: o.timeoutMs + 3e3,
1894
+ stdio: ["ignore", "pipe", "ignore"],
1895
+ maxBuffer: o.maxBuffer,
1896
+ windowsHide: true
1897
+ }).trim();
1898
+ if (!out) return [];
1899
+ return rows(JSON.parse(out));
1900
+ } catch {
1901
+ return null;
1902
+ }
1903
+ }
1904
+ function querySqlite(file, sql, params = [], opts = {}) {
1905
+ const o = {
1906
+ timeoutMs: opts.timeoutMs ?? 2e3,
1907
+ engine: opts.engine ?? "auto",
1908
+ maxBuffer: opts.maxBuffer ?? 32 * 1024 * 1024
1909
+ };
1910
+ if (!file) return null;
1911
+ try {
1912
+ if (!existsSync5(file)) return null;
1913
+ } catch {
1914
+ return null;
1915
+ }
1916
+ const ways = o.engine === "auto" ? [viaNode, viaChild, viaCli] : o.engine === "node" ? [viaNode] : o.engine === "child" ? [viaChild] : [viaCli];
1917
+ for (const way of ways) {
1918
+ const r = way(file, sql, params, o);
1919
+ if (r) return r;
1920
+ }
1921
+ return null;
1922
+ }
1923
+
1924
+ // ../core/src/adapters/devin.ts
1925
+ var NAME4 = "devin";
1926
+ var DEVIN_EVENTS = [
1927
+ "SessionStart",
1928
+ "UserPromptSubmit",
1929
+ "PreToolUse",
1930
+ "PostToolUse",
1931
+ "PermissionRequest",
1932
+ "Stop",
1933
+ "PostCompaction",
1934
+ "SessionEnd"
1935
+ ];
1936
+ var OPTS3 = { matcherFor: (e) => /Tool|Permission/.test(e) ? "" : void 0, timeout: 5 };
1937
+ var eventsFor3 = (lean) => lean ? DEVIN_EVENTS.filter((e) => e !== "PreToolUse") : [...DEVIN_EVENTS];
1938
+ var INSTALL_NOTE = "Devin drops a whole hooks file on one event name it doesn't know; `/hooks` in `devin` lists what loaded";
1939
+ 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";
1940
+ 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)`;
1941
+ function foreignEvent(file) {
1942
+ const hooks = readJson(file).hooks;
1943
+ if (!hooks || typeof hooks !== "object" || Array.isArray(hooks)) return void 0;
1944
+ const ok = DEVIN_EVENTS;
1945
+ return Object.keys(hooks).find((k) => !ok.includes(k));
1946
+ }
1947
+ var homeOf = (env) => env.HOME || homeDir();
1948
+ var appData = (env) => env.APPDATA || path6.join(homeOf(env), "AppData", "Roaming");
1949
+ 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");
1950
+ var configFile2 = (env) => path6.join(devinConfigDir(env), "config.json");
1951
+ 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");
1952
+ var sessionsDb = (env) => path6.join(devinDataDir(env), "sessions.db");
1953
+ var SESSION_ID = /^[A-Za-z0-9._:-]{1,128}$/;
1954
+ var validTool4 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
1955
+ var endReason3 = (r) => r === "clear" || r === "logout" || r === "exit" ? r : "other";
1956
+ var pickSource2 = (v) => typeof v === "string" && ["startup", "resume", "clear", "compact", "fork"].includes(v) ? v : "unknown";
1957
+ function errText2(v) {
1958
+ if (typeof v === "string" && v) return v.slice(0, 2e3);
1959
+ if (v && typeof v === "object") {
1960
+ const e = v;
1961
+ if (typeof e.message === "string" && e.message) return e.message.slice(0, 2e3);
1962
+ if (typeof e.error === "string" && e.error) return e.error.slice(0, 2e3);
1963
+ }
1964
+ return void 0;
1965
+ }
1966
+ var cut = (s, n) => s.length > n ? `${s.slice(0, n).replace(/\s+\S*$/, "")}\u2026` : s;
1967
+ var devin = {
1968
+ name: NAME4,
1969
+ events: DEVIN_EVENTS,
1970
+ detect: (env = process.env) => existsSync6(devinConfigDir(env)) || existsSync6(devinDataDir(env)),
1971
+ configFiles: (env = process.env) => [configFile2(env)],
1972
+ install: (cmd, env = process.env, opts = {}) => {
1973
+ const file = configFile2(env);
1974
+ const r = installClaudeShaped(file, NAME4, eventsFor3(opts.lean), cmd, OPTS3);
1975
+ if (r.skipped) return [r];
1976
+ const bad = foreignEvent(file);
1977
+ return [{ ...r, note: bad ? deadFileNote(bad) : INSTALL_NOTE }];
1978
+ },
1979
+ uninstall: (env = process.env, opts = {}) => [
1980
+ uninstallClaudeShaped(configFile2(env), DEVIN_EVENTS, { created: opts.created?.includes(configFile2(env)) })
1981
+ ],
1982
+ installed: (cmd, env = process.env, opts = {}) => {
1983
+ const file = configFile2(env);
1984
+ const state = installedClaudeShaped(file, NAME4, eventsFor3(opts.lean), cmd, OPTS3);
1985
+ const bad = state === "missing" ? void 0 : foreignEvent(file);
1986
+ return [{ file, state: bad ? "inactive" : state, note: bad ? deadFileNote(bad) : CLAUDE_NOTE }];
1987
+ },
1988
+ fromHook(input) {
1989
+ const event = input.argv[1] ?? "";
1990
+ let s = {};
1991
+ try {
1992
+ const j = input.stdin ? JSON.parse(input.stdin) : {};
1993
+ if (j && typeof j === "object" && !Array.isArray(j)) s = j;
1994
+ } catch {
1995
+ }
1996
+ const id = typeof s.session_id === "string" ? s.session_id : "";
1997
+ if (!id) return null;
1998
+ const session = { id, cwd: input.env.DEVIN_PROJECT_DIR ?? input.cwd };
1999
+ const turn_id = typeof s.prompt_id === "string" ? s.prompt_id : void 0;
2000
+ const tool = validTool4(s.tool_name) ? s.tool_name : void 0;
2001
+ const call_id = typeof s.tool_use_id === "string" ? s.tool_use_id : void 0;
2002
+ const response = s.tool_response && typeof s.tool_response === "object" && !Array.isArray(s.tool_response) ? s.tool_response : void 0;
2003
+ const permissionId = () => `perm-${createHash2("sha256").update(`${turn_id ?? ""}|${tool ?? ""}|${JSON.stringify(s.tool_input ?? null)}`).digest("hex").slice(0, 16)}`;
2004
+ const ev = (type, data) => ({ type, data: prune4(data), harnessEvent: event });
2005
+ const events = [];
2006
+ switch (event) {
2007
+ case "SessionStart":
2008
+ events.push(ev("session.started", { source: pickSource2(s.source) }));
2009
+ break;
2010
+ case "UserPromptSubmit":
2011
+ events.push(
2012
+ ev("turn.started", {
2013
+ turn_id,
2014
+ prompt_chars: typeof s.prompt === "string" ? [...s.prompt].length : void 0
2015
+ })
2016
+ );
2017
+ break;
2018
+ case "PreToolUse":
2019
+ if (tool) events.push(ev("tool.started", { tool, call_id, turn_id, input: s.tool_input }));
2020
+ break;
2021
+ case "PostToolUse":
2022
+ if (tool)
2023
+ events.push(
2024
+ ev("tool.ended", {
2025
+ tool,
2026
+ call_id,
2027
+ turn_id,
2028
+ // `ok` is required on tool.ended (PROTOCOL.md §events), so a response
2029
+ // that carries no success flag at all reads as "nothing said it failed".
2030
+ ok: response?.success !== false,
2031
+ error: errText2(response?.error),
2032
+ input: s.tool_input,
2033
+ output: response ? response.output : s.tool_response
2034
+ })
2035
+ );
2036
+ break;
2037
+ case "PermissionRequest":
2038
+ events.push(ev("attention.needed", { attention_id: call_id ?? permissionId(), kind: "permission", tool }));
2039
+ break;
2040
+ case "Stop":
2041
+ events.push(ev("turn.ended", { turn_id, reason: "stop" }));
2042
+ break;
2043
+ case "PostCompaction":
2044
+ events.push(ev("context.compacted", { trigger: "auto" }));
2045
+ break;
2046
+ case "SessionEnd":
2047
+ events.push(ev("session.ended", { reason: endReason3(s.reason) }));
2048
+ break;
2049
+ default:
2050
+ return null;
2051
+ }
2052
+ return {
2053
+ session,
2054
+ events,
2055
+ hints: {},
2056
+ transcript: transcriptRef(sessionsDb(input.env), id)
2057
+ };
2058
+ },
2059
+ readTranscript(file, fromLine) {
2060
+ const ref = splitRef(file);
2061
+ if (!ref) return { turns: [], line: fromLine };
2062
+ return readDevinChain(ref.db, ref.session, fromLine);
2063
+ },
2064
+ facts(session, transcript, _hints, _state, env = process.env) {
2065
+ const out = {};
2066
+ const version = devinVersion(env);
2067
+ if (version) out.harness_version = version;
2068
+ const account = devinAccount(env);
2069
+ if (account) out.account_id = account;
2070
+ const ref = splitRef(transcript) ?? { db: sessionsDb(env), session: session.id };
2071
+ if (!SESSION_ID.test(ref.session)) return out;
2072
+ const rows2 = querySqlite(ref.db, SESSION_SQL, [ref.session]);
2073
+ const row = rows2?.[0];
2074
+ if (!row) return out;
2075
+ Object.assign(out, sessionFacts(row));
2076
+ return out;
2077
+ },
2078
+ backfill(sinceMs, env = process.env) {
2079
+ const rows2 = querySqlite(sessionsDb(env), BACKFILL_SQL, ["text", Math.floor(sinceMs / 1e3)]);
2080
+ if (!rows2) return [];
2081
+ const out = [];
2082
+ for (const row of rows2) {
2083
+ const id = String(row.id);
2084
+ if (!SESSION_ID.test(id)) continue;
2085
+ const { started_at, last_at, ...rest } = sessionFacts(row);
2086
+ if (!started_at || !last_at) continue;
2087
+ out.push({ session: { id, ...rest }, started_at, last_at });
2088
+ }
2089
+ return out;
2090
+ }
2091
+ };
2092
+ function prune4(o) {
2093
+ for (const k of Object.keys(o)) if (o[k] === void 0) delete o[k];
2094
+ return o;
2095
+ }
2096
+ var transcriptRef = (db, session) => `${db}#${session}`;
2097
+ function splitRef(ref) {
2098
+ if (!ref) return null;
2099
+ const at = ref.lastIndexOf("#");
2100
+ if (at <= 0) return null;
2101
+ const db = ref.slice(0, at);
2102
+ const session = ref.slice(at + 1);
2103
+ return db && SESSION_ID.test(session) ? { db, session } : null;
2104
+ }
2105
+ var CHAIN_LIMIT = 4e3;
2106
+ var CHAIN_SQL = `WITH RECURSIVE chain(node_id, parent_node_id, chat_message, created_at) AS (
2107
+ SELECT node_id, parent_node_id, chat_message, created_at FROM message_nodes
2108
+ WHERE session_id = ? AND node_id = (SELECT main_chain_id FROM sessions WHERE id = ?)
2109
+ UNION ALL
2110
+ SELECT m.node_id, m.parent_node_id, m.chat_message, m.created_at FROM message_nodes m
2111
+ JOIN chain c ON m.node_id = c.parent_node_id AND m.session_id = ? AND m.node_id >= ?
2112
+ )
2113
+ SELECT node_id, chat_message, created_at FROM chain ORDER BY node_id DESC LIMIT ${CHAIN_LIMIT}`;
2114
+ var SESSION_FIELDS = `s.model, s.working_directory, s.created_at, s.last_activity_at,
2115
+ (SELECT p.content FROM prompt_history p WHERE p.session_id = s.id AND p.is_shell = 0
2116
+ ORDER BY p.timestamp ASC, p.id ASC LIMIT 1) AS first_ask`;
2117
+ var SESSION_SQL = `SELECT ${SESSION_FIELDS} FROM sessions s WHERE s.id = ? LIMIT 1`;
2118
+ var BACKFILL_SQL = `SELECT s.id, ${SESSION_FIELDS} FROM sessions s
2119
+ WHERE s.hidden = 0 AND typeof(s.id) = ? AND s.last_activity_at >= ?
2120
+ ORDER BY s.last_activity_at DESC LIMIT 500`;
2121
+ function sessionFacts(row) {
2122
+ const out = {};
2123
+ if (typeof row.model === "string" && row.model) out.model = row.model;
2124
+ if (typeof row.working_directory === "string" && path6.isAbsolute(row.working_directory))
2125
+ out.cwd = row.working_directory;
2126
+ const started = Number(row.created_at);
2127
+ const last = Number(row.last_activity_at);
2128
+ if (Number.isFinite(started) && started > 0) out.started_at = Math.round(started * 1e3);
2129
+ if (Number.isFinite(last) && last > 0) out.last_at = Math.round(last * 1e3);
2130
+ if (typeof row.first_ask === "string") {
2131
+ const line = row.first_ask.split("\n").find((l) => l.trim()) ?? "";
2132
+ if (line.trim()) {
2133
+ out.title = cut(line.trim(), 80);
2134
+ out.title_source = "first-ask";
2135
+ }
2136
+ }
2137
+ return out;
2138
+ }
2139
+ function readDevinChain(db, session, fromLine) {
2140
+ if (!SESSION_ID.test(session)) return { turns: [], line: fromLine };
2141
+ const floor = Number.isFinite(fromLine) && fromLine > 0 ? Math.floor(fromLine) : 0;
2142
+ const rows2 = querySqlite(db, CHAIN_SQL, [session, session, session, floor]);
2143
+ if (!rows2?.length) return { turns: [], line: fromLine };
2144
+ const msgs = [];
2145
+ let end = fromLine;
2146
+ for (const row of rows2) {
2147
+ const node = Number(row.node_id);
2148
+ if (!Number.isFinite(node)) continue;
2149
+ if (node + 1 > end) end = node + 1;
2150
+ if (node < fromLine) continue;
2151
+ let j;
2152
+ try {
2153
+ j = JSON.parse(String(row.chat_message ?? ""));
2154
+ } catch {
2155
+ continue;
2156
+ }
2157
+ const text = typeof j.content === "string" ? j.content.trim() : "";
2158
+ if (!text) continue;
2159
+ const secs = Number(row.created_at);
2160
+ const at = Date.parse(String(j.metadata?.created_at ?? "")) || (Number.isFinite(secs) && secs > 0 ? Math.round(secs * 1e3) : 0) || Date.now();
2161
+ if (j.role === "user" && j.metadata?.is_user_input === true) msgs.push({ role: "user", text, at, line: node });
2162
+ else if (j.role === "assistant") msgs.push({ role: "assistant", text, at, line: node });
2163
+ }
2164
+ msgs.sort((a, b) => a.line - b.line);
2165
+ return pairTurns(msgs, end);
2166
+ }
2167
+ function devinVersion(env = process.env) {
2168
+ try {
2169
+ const target = readlinkSync(path6.join(devinDataDir(env), "_versions", "current"));
2170
+ const name = path6.basename(target);
2171
+ return /^[0-9][0-9A-Za-z.+-]{0,63}$/.test(name) ? name : void 0;
2172
+ } catch {
2173
+ return void 0;
2174
+ }
2175
+ }
2176
+ function devinAccount(env = process.env) {
2177
+ const d = readJson(configFile2(env)).devin;
2178
+ const id = d && typeof d === "object" ? d.org_id : void 0;
2179
+ return typeof id === "string" && /^[A-Za-z0-9_.:-]{1,128}$/.test(id) ? id : void 0;
2180
+ }
2181
+
2182
+ // ../core/src/adapters/gemini-cli.ts
2183
+ import { execFileSync as execFileSync4 } from "node:child_process";
2184
+ import { existsSync as existsSync7, readFileSync as readFileSync5 } from "node:fs";
2185
+ import path7 from "node:path";
2186
+ var NAME5 = "gemini-cli";
1666
2187
  var GEMINI_EVENTS = [
1667
2188
  "SessionStart",
1668
2189
  "BeforeAgent",
@@ -1673,11 +2194,11 @@ var GEMINI_EVENTS = [
1673
2194
  "PreCompress",
1674
2195
  "SessionEnd"
1675
2196
  ];
1676
- var OPTS3 = { matcherFor: (e) => /Tool/.test(e) ? ".*" : void 0, timeout: 5e3 };
1677
- var geminiDir = (env) => env.GEMINI_CLI_HOME || path6.join(HOME, ".gemini");
1678
- var settings = (env) => path6.join(geminiDir(env), "settings.json");
1679
- var validTool4 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
1680
- var eventsFor3 = (lean) => lean ? GEMINI_EVENTS.filter((e) => e !== "BeforeTool") : [...GEMINI_EVENTS];
2197
+ var OPTS4 = { matcherFor: (e) => /Tool/.test(e) ? ".*" : void 0, timeout: 5e3 };
2198
+ var geminiDir = (env) => env.GEMINI_CLI_HOME || path7.join(homeDir(), ".gemini");
2199
+ var settings = (env) => path7.join(geminiDir(env), "settings.json");
2200
+ var validTool5 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
2201
+ var eventsFor4 = (lean) => lean ? GEMINI_EVENTS.filter((e) => e !== "BeforeTool") : [...GEMINI_EVENTS];
1681
2202
  function geminiHooksActive(settingsFile) {
1682
2203
  let tools;
1683
2204
  try {
@@ -1689,7 +2210,7 @@ function geminiHooksActive(settingsFile) {
1689
2210
  return { active: false, note: "inactive: tools.enableHooks is false in settings.json" };
1690
2211
  let version = "";
1691
2212
  try {
1692
- version = execFileSync3("gemini", ["--version"], {
2213
+ version = execFileSync4("gemini", ["--version"], {
1693
2214
  encoding: "utf8",
1694
2215
  timeout: 4e3,
1695
2216
  stdio: ["ignore", "pipe", "ignore"]
@@ -1708,21 +2229,21 @@ function geminiHooksActive(settingsFile) {
1708
2229
  };
1709
2230
  }
1710
2231
  var geminiCli = {
1711
- name: NAME4,
2232
+ name: NAME5,
1712
2233
  events: GEMINI_EVENTS,
1713
2234
  // ~/.gemini also belongs to Antigravity; Gemini CLI is here when its settings or its chat logs exist.
1714
- detect: (env = process.env) => existsSync5(settings(env)) || existsSync5(path6.join(geminiDir(env), "tmp")),
2235
+ detect: (env = process.env) => existsSync7(settings(env)) || existsSync7(path7.join(geminiDir(env), "tmp")),
1715
2236
  configFiles: (env = process.env) => [settings(env)],
1716
2237
  install: (cmd, env = process.env, opts = {}) => {
1717
- const r2 = installClaudeShaped(settings(env), NAME4, eventsFor3(opts.lean), cmd, OPTS3);
2238
+ const r = installClaudeShaped(settings(env), NAME5, eventsFor4(opts.lean), cmd, OPTS4);
1718
2239
  const a = geminiHooksActive(settings(env));
1719
- return [a.note && !r2.skipped ? { ...r2, note: [r2.note, a.note].filter(Boolean).join("; ") } : r2];
2240
+ return [a.note && !r.skipped ? { ...r, note: [r.note, a.note].filter(Boolean).join("; ") } : r];
1720
2241
  },
1721
2242
  uninstall: (env = process.env, opts = {}) => [
1722
2243
  uninstallClaudeShaped(settings(env), GEMINI_EVENTS, { created: opts.created?.includes(settings(env)) })
1723
2244
  ],
1724
2245
  installed: (cmd, env = process.env, opts = {}) => {
1725
- const state = installedClaudeShaped(settings(env), NAME4, eventsFor3(opts.lean), cmd, OPTS3);
2246
+ const state = installedClaudeShaped(settings(env), NAME5, eventsFor4(opts.lean), cmd, OPTS4);
1726
2247
  const a = geminiHooksActive(settings(env));
1727
2248
  return [
1728
2249
  {
@@ -1736,14 +2257,15 @@ var geminiCli = {
1736
2257
  const event = input.argv[1] ?? "";
1737
2258
  let s = {};
1738
2259
  try {
1739
- s = input.stdin ? JSON.parse(input.stdin) : {};
2260
+ const j = input.stdin ? JSON.parse(input.stdin) : {};
2261
+ if (j && typeof j === "object" && !Array.isArray(j)) s = j;
1740
2262
  } catch {
1741
2263
  }
1742
2264
  const id = s.session_id;
1743
2265
  if (!id) return null;
1744
2266
  const session = { id: String(id), cwd: typeof s.cwd === "string" ? s.cwd : input.cwd };
1745
- const tool = validTool4(s.tool_name) ? s.tool_name : void 0;
1746
- const ev = (type, data) => ({ type, data: prune4(data), harnessEvent: event });
2267
+ const tool = validTool5(s.tool_name) ? s.tool_name : void 0;
2268
+ const ev = (type, data) => ({ type, data: prune5(data), harnessEvent: event });
1747
2269
  const events = [];
1748
2270
  switch (event) {
1749
2271
  case "SessionStart":
@@ -1801,9 +2323,9 @@ var geminiCli = {
1801
2323
  default:
1802
2324
  return null;
1803
2325
  }
1804
- const r2 = { session, events, hints: {} };
1805
- if (typeof s.transcript_path === "string") r2.transcript = s.transcript_path;
1806
- return r2;
2326
+ const r = { session, events, hints: {} };
2327
+ if (typeof s.transcript_path === "string") r.transcript = s.transcript_path;
2328
+ return r;
1807
2329
  },
1808
2330
  readTranscript(file, fromLine) {
1809
2331
  let text = "";
@@ -1824,7 +2346,7 @@ var geminiCli = {
1824
2346
  const raw = first ? first.content ?? first.message ?? first.text : void 0;
1825
2347
  const text = typeof raw === "string" ? raw.trim() : "";
1826
2348
  if (text) {
1827
- const line = text.split("\n").find((l) => l.trim()) ?? "";
2349
+ const line = text.split("\n")[0];
1828
2350
  out.title = line.length > 80 ? `${line.slice(0, 80).replace(/\s+\S*$/, "")}\u2026` : line;
1829
2351
  out.title_source = "first-ask";
1830
2352
  }
@@ -1838,31 +2360,47 @@ var geminiCli = {
1838
2360
  backfill: () => []
1839
2361
  // chat logs are keyed by a project hash, not a session id we can name; the first hook of a resumed session reports it
1840
2362
  };
1841
- function prune4(o) {
2363
+ function prune5(o) {
1842
2364
  for (const k of Object.keys(o)) if (o[k] === void 0) delete o[k];
1843
2365
  return o;
1844
2366
  }
1845
2367
 
1846
2368
  // ../core/src/adapters/index.ts
1847
- var ADAPTERS = [claudeCode, codex, geminiCli, antigravity];
2369
+ var ADAPTERS = [claudeCode, codex, geminiCli, antigravity, devin];
1848
2370
  var adapterByName = (name) => ADAPTERS.find((a) => a.name === name);
1849
2371
 
2372
+ // ../core/src/launched-by.ts
2373
+ var LAUNCHED_BY_ENV = "SESSIONPIPE_LAUNCHED_BY";
2374
+ var LAUNCHED_BY = /^[a-z][a-z0-9-]{0,39}:[A-Za-z0-9._:-]{1,128}$/;
2375
+ function launchedBy(env) {
2376
+ const v = env[LAUNCHED_BY_ENV]?.trim();
2377
+ return v && LAUNCHED_BY.test(v) ? v : void 0;
2378
+ }
2379
+ function readHook(adapter, input) {
2380
+ const r = adapter.fromHook(input);
2381
+ if (r && !r.session.parent_id) {
2382
+ const launcher = launchedBy(input.env);
2383
+ if (launcher && launcher !== `${adapter.name}:${r.session.id}`) r.session.launched_by = launcher;
2384
+ }
2385
+ return r;
2386
+ }
2387
+
1850
2388
  // src/index.ts
1851
2389
  function matches(expect, got, pathStr = "") {
1852
2390
  if (expect === "*") return null;
1853
2391
  if (Array.isArray(expect)) {
1854
2392
  if (!Array.isArray(got) || got.length !== expect.length) return `${pathStr}: expected ${expect.length} items`;
1855
2393
  for (let i = 0; i < expect.length; i++) {
1856
- const r2 = matches(expect[i], got[i], `${pathStr}[${i}]`);
1857
- if (r2) return r2;
2394
+ const r = matches(expect[i], got[i], `${pathStr}[${i}]`);
2395
+ if (r) return r;
1858
2396
  }
1859
2397
  return null;
1860
2398
  }
1861
2399
  if (expect && typeof expect === "object") {
1862
2400
  if (!got || typeof got !== "object") return `${pathStr}: expected an object`;
1863
2401
  for (const [k, v] of Object.entries(expect)) {
1864
- const r2 = matches(v, got[k], `${pathStr}.${k}`);
1865
- if (r2) return r2;
2402
+ const r = matches(v, got[k], `${pathStr}.${k}`);
2403
+ if (r) return r;
1866
2404
  }
1867
2405
  return null;
1868
2406
  }
@@ -1877,51 +2415,64 @@ async function runAdapterFixtures(files) {
1877
2415
  results.push({ file, ok: false, why: `no adapter ${fx.harness}` });
1878
2416
  continue;
1879
2417
  }
1880
- const r2 = adapter.fromHook({
2418
+ const r = readHook(adapter, {
1881
2419
  argv: fx.input.argv,
1882
2420
  stdin: fx.input.stdin,
1883
2421
  env: fx.input.env ?? {},
1884
2422
  cwd: fx.input.cwd ?? "/"
1885
2423
  });
1886
- if (!r2) {
1887
- results.push({ file, ok: false, why: "fromHook returned null" });
2424
+ if (!r) {
2425
+ results.push(fx.expect.length ? { file, ok: false, why: "fromHook returned null" } : { file, ok: true });
1888
2426
  continue;
1889
2427
  }
1890
2428
  let why = matches(
1891
2429
  fx.expect.map((e) => e.type),
1892
- r2.events.map((e) => e.type),
2430
+ r.events.map((e) => e.type),
1893
2431
  "types"
1894
2432
  );
1895
2433
  if (!why)
1896
2434
  for (const [i, e] of fx.expect.entries()) {
1897
- const got = r2.events[i];
1898
- why = matches(e.harness.event, got?.harnessEvent, `[${i}].harness.event`) ?? matches(e.data, got?.data, `[${i}].data`) ?? matches({ ...e.session, seq: "*" }, r2.session, `[${i}].session`);
2435
+ const got = r.events[i];
2436
+ why = matches(e.harness.event, got.harnessEvent, `[${i}].harness.event`) ?? matches(e.data, got.data, `[${i}].data`) ?? matches({ ...e.session, seq: "*" }, r.session, `[${i}].session`);
1899
2437
  if (why) break;
1900
2438
  }
1901
2439
  results.push(why ? { file, ok: false, why } : { file, ok: true });
1902
2440
  }
1903
- const failed = results.filter((r2) => !r2.ok).length;
1904
- const table = results.map((r2) => `${r2.ok ? "ok " : "FAIL"} ${r2.file}${r2.why ? ` \u2014 ${r2.why}` : ""}`).join("\n");
2441
+ const failed = results.filter((r) => !r.ok).length;
2442
+ const table = results.map((r) => `${r.ok ? "ok " : "FAIL"} ${r.file}${r.why ? ` \u2014 ${r.why}` : ""}`).join("\n");
1905
2443
  return { results, failed, table };
1906
2444
  }
1907
2445
 
1908
2446
  // src/cli.ts
1909
- var target = process.argv[2];
1910
- if (!target) {
1911
- process.stdout.write("usage: sessionpipe-conformance <conformance/harness dir | https://receiver>\n");
1912
- process.exit(2);
1913
- }
1914
- if (/^https?:\/\//.test(target)) {
1915
- process.stdout.write("receiver scenarios land in M3\n");
1916
- process.exit(2);
1917
- }
1918
2447
  var walk = (d) => readdirSync5(d).flatMap((n) => {
1919
- const p = path7.join(d, n);
2448
+ const p = path8.join(d, n);
1920
2449
  return statSync4(p).isDirectory() ? walk(p) : p.endsWith(".json") ? [p] : [];
1921
2450
  });
1922
- var r = await runAdapterFixtures(walk(target));
1923
- process.stdout.write(`${r.table}
2451
+ async function main(argv, io = process) {
2452
+ const target = argv[0];
2453
+ if (!target) {
2454
+ io.stdout.write("usage: sessionpipe-conformance <conformance/harness dir | https://receiver>\n");
2455
+ return 2;
2456
+ }
2457
+ if (/^https?:\/\//.test(target)) {
2458
+ io.stdout.write("receiver scenarios land in M3\n");
2459
+ return 2;
2460
+ }
2461
+ const r = await runAdapterFixtures(walk(target));
2462
+ io.stdout.write(`${r.table}
1924
2463
  ${r.results.length - r.failed}/${r.results.length} passed
1925
2464
  `);
1926
- process.exit(r.failed ? 1 : 0);
2465
+ return r.failed ? 1 : 0;
2466
+ }
2467
+ function isMain() {
2468
+ try {
2469
+ return !!process.argv[1] && import.meta.url === pathToFileURL(realpathSync2(process.argv[1])).href;
2470
+ } catch {
2471
+ return false;
2472
+ }
2473
+ }
2474
+ if (isMain()) process.exit(await main(process.argv.slice(2)));
2475
+ export {
2476
+ main
2477
+ };
1927
2478
  //# sourceMappingURL=cli.js.map