sessionpipe 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // sessionpipe 0.7.0 · Apache-2.0 · https://sessionpipe.org
2
+ // sessionpipe 0.9.0 · Apache-2.0 · https://sessionpipe.org
3
3
  var __defProp = Object.defineProperty;
4
4
  var __export = (target, all) => {
5
5
  for (var name in all)
@@ -7,10 +7,19 @@ var __export = (target, all) => {
7
7
  };
8
8
 
9
9
  // src/cli.ts
10
- import { execFileSync as execFileSync7, spawnSync as spawnSync2 } from "node:child_process";
11
- import { mkdtempSync, readdirSync as readdirSync8, readFileSync as readFileSync15, realpathSync as realpathSync6, rmSync as rmSync2, statSync as statSync10, unlinkSync as unlinkSync7 } from "node:fs";
10
+ import { execFileSync as execFileSync8, spawnSync as spawnSync2 } from "node:child_process";
11
+ import {
12
+ existsSync as existsSync17,
13
+ mkdtempSync,
14
+ readdirSync as readdirSync8,
15
+ readFileSync as readFileSync16,
16
+ realpathSync as realpathSync7,
17
+ rmSync as rmSync3,
18
+ statSync as statSync11,
19
+ unlinkSync as unlinkSync7
20
+ } from "node:fs";
12
21
  import os11 from "node:os";
13
- import path27 from "node:path";
22
+ import path29 from "node:path";
14
23
  import { fileURLToPath } from "node:url";
15
24
 
16
25
  // ../core/src/adapters/antigravity.ts
@@ -29,20 +38,20 @@ import path4 from "node:path";
29
38
  // ../core/src/paths.ts
30
39
  import os from "node:os";
31
40
  import path from "node:path";
32
- var HOME = os.homedir();
41
+ var homeDir = () => os.homedir();
33
42
  function configFile(env = process.env) {
34
43
  if (env.SESSIONPIPE_CONFIG) return path.resolve(env.SESSIONPIPE_CONFIG);
35
44
  if (process.platform === "win32")
36
- return path.join(env.APPDATA || path.join(HOME, "AppData", "Roaming"), "sessionpipe", "config.json");
37
- return path.join(env.XDG_CONFIG_HOME || path.join(HOME, ".config"), "sessionpipe", "config.json");
45
+ return path.join(env.APPDATA || path.join(homeDir(), "AppData", "Roaming"), "sessionpipe", "config.json");
46
+ return path.join(env.XDG_CONFIG_HOME || path.join(homeDir(), ".config"), "sessionpipe", "config.json");
38
47
  }
39
48
  function stateDir(env = process.env) {
40
49
  if (env.SESSIONPIPE_STATE) return path.resolve(env.SESSIONPIPE_STATE);
41
50
  if (process.platform === "win32")
42
- return path.join(env.LOCALAPPDATA || path.join(HOME, "AppData", "Local"), "sessionpipe");
43
- return path.join(env.XDG_STATE_HOME || path.join(HOME, ".local", "state"), "sessionpipe");
51
+ return path.join(env.LOCALAPPDATA || path.join(homeDir(), "AppData", "Local"), "sessionpipe");
52
+ return path.join(env.XDG_STATE_HOME || path.join(homeDir(), ".local", "state"), "sessionpipe");
44
53
  }
45
- function tilde(p, home = HOME) {
54
+ function tilde(p, home = homeDir()) {
46
55
  if (!p) return p;
47
56
  if (p === home) return "~";
48
57
  return p.startsWith(home + path.sep) || p.startsWith(`${home}/`) ? `~${p.slice(home.length)}` : p;
@@ -139,6 +148,19 @@ function recentFiles(root, depth, sinceMs, match) {
139
148
  return out2;
140
149
  }
141
150
 
151
+ // ../core/src/adapters/codex-rollout.ts
152
+ function codexSubagent(meta) {
153
+ if (meta?.type !== "session_meta") return false;
154
+ const p = meta.payload;
155
+ return !!p?.source?.subagent;
156
+ }
157
+ function codexReplayBoundary(meta) {
158
+ if (meta?.type !== "session_meta") return void 0;
159
+ const p = meta.payload;
160
+ if (!p || !p.forked_from_id && !p.source?.subagent) return void 0;
161
+ return (record) => typeof p.id === "string" && !!p.id && record?.type === "event_msg" && record.payload?.thread_id === p.id;
162
+ }
163
+
142
164
  // ../core/src/readers/transcripts.ts
143
165
  function pairTurns(msgs, endLine) {
144
166
  const turns = [];
@@ -161,10 +183,14 @@ ${m.text}`;
161
183
  }
162
184
  if (cur?.assistant) turns.push(cur);
163
185
  const line = cur && !cur.assistant ? cur.startLine : endLine;
164
- return { turns: turns.map((t) => ({ user: t.user, assistant: t.assistant, at: t.at, endLine: t.endLine })), line };
186
+ const out2 = {
187
+ turns: turns.map((t) => ({ user: t.user, assistant: t.assistant, at: t.at, endLine: t.endLine })),
188
+ line
189
+ };
190
+ if (cur && !cur.assistant && cur.user) out2.open = { user: cur.user, at: cur.at };
191
+ return out2;
165
192
  }
166
- var isInjected = (text) => /^<[a-z_-]+>/i.test(text) && /<\/[a-z_-]+>\s*$/i.test(text) || /^<!--\s/.test(text) || /^<command-/.test(text);
167
- var endOf = (lines) => lines[lines.length - 1] === "" ? lines.length - 1 : lines.length;
193
+ var isInjected = (text) => /^<[a-z_-]+(?:\s[^>]*)?>/i.test(text) && /<\/[a-z_-]+>\s*$/i.test(text) || /^<!--\s/.test(text) || /^<command-/.test(text);
168
194
  var parse = (raw) => {
169
195
  try {
170
196
  return JSON.parse(raw);
@@ -172,6 +198,11 @@ var parse = (raw) => {
172
198
  return null;
173
199
  }
174
200
  };
201
+ var endOf = (lines) => {
202
+ const last = lines[lines.length - 1];
203
+ if (last === void 0) return 0;
204
+ return last === "" || parse(last) === null ? lines.length - 1 : lines.length;
205
+ };
175
206
  var textParts = (c, kinds) => {
176
207
  if (typeof c === "string") return c;
177
208
  if (!Array.isArray(c)) return "";
@@ -179,13 +210,21 @@ var textParts = (c, kinds) => {
179
210
  (b) => b && typeof b === "object" && kinds.includes(b.type ?? "") && typeof b.text === "string"
180
211
  ).map((b) => b.text).join("\n");
181
212
  };
213
+ var queuedPrompt = (a) => {
214
+ if (!a || typeof a !== "object") return "";
215
+ const q = a;
216
+ if (q.type !== "queued_command" || (q.commandMode ?? "prompt") !== "prompt") return "";
217
+ if (q.origin?.kind && q.origin.kind !== "human") return "";
218
+ return textParts(q.prompt, ["text"]).trim();
219
+ };
182
220
  function readClaude(lines, from) {
183
221
  const msgs = [];
184
222
  for (let i = from; i < lines.length; i++) {
185
223
  const j = lines[i] ? parse(lines[i]) : null;
186
224
  if (!j || j.isSidechain) continue;
187
225
  const origin = j.origin;
188
- if (j.type === "user" && (j.isMeta || origin?.kind && origin.kind !== "human")) continue;
226
+ if (j.type === "user" && (j.isMeta || j.isCompactSummary || j.repeatsCompactSummary || origin?.kind && origin.kind !== "human"))
227
+ continue;
189
228
  const at = Date.parse(String(j.timestamp ?? "")) || Date.now();
190
229
  const c = j.message?.content;
191
230
  if (j.type === "user") {
@@ -195,6 +234,9 @@ function readClaude(lines, from) {
195
234
  } else if (j.type === "assistant" && Array.isArray(c)) {
196
235
  const text = textParts(c, ["text"]).trim();
197
236
  if (text) msgs.push({ role: "assistant", text, at, line: i });
237
+ } else if (j.type === "attachment") {
238
+ const text = queuedPrompt(j.attachment);
239
+ if (text && !isInjected(text)) msgs.push({ role: "user", text, at, line: i });
198
240
  }
199
241
  }
200
242
  return pairTurns(msgs, endOf(lines));
@@ -204,7 +246,10 @@ function codexInjected(text) {
204
246
  }
205
247
  function readCodex(lines, from) {
206
248
  const msgs = [];
207
- for (let i = from; i < lines.length; i++) {
249
+ const boundary = codexReplayBoundary(parse(lines.find((line) => line.trim()) ?? ""));
250
+ const start = boundary ? lines.findIndex((line) => boundary(parse(line))) : 0;
251
+ if (start < 0) return pairTurns(msgs, endOf(lines));
252
+ for (let i = Math.max(from, start); i < lines.length; i++) {
208
253
  const j = lines[i] ? parse(lines[i]) : null;
209
254
  if (!j || j.type !== "response_item") continue;
210
255
  const p = j.payload;
@@ -242,7 +287,7 @@ function readAntigravity(lines, from) {
242
287
  const at = Date.parse(String(j.created_at ?? j.timestamp ?? "")) || Date.now();
243
288
  if (j.type === "USER_INPUT") {
244
289
  const m = /<USER_REQUEST>([\s\S]*?)(?:<\/USER_REQUEST>|$)/.exec(j.content);
245
- const text = (m ? m[1] ?? "" : j.content).trim();
290
+ const text = (m ? m[1] : j.content).trim();
246
291
  if (text) msgs.push({ role: "user", text, at, line: i });
247
292
  } else if (j.type === "PLANNER_RESPONSE") {
248
293
  const text = j.content.trim();
@@ -290,14 +335,18 @@ function formatJson(value, f) {
290
335
  const text = JSON.stringify(value, null, f.indent).replace(/\n/g, f.eol);
291
336
  return f.finalNewline ? text + f.eol : text;
292
337
  }
293
- function entryFor(event, harness, cmd2, o) {
338
+ function readJson(file) {
339
+ const f = readJsonFile(file);
340
+ return f.ok ? f.value : {};
341
+ }
342
+ function entryFor(event, harness, cmd, o) {
294
343
  const matcher = o.matcherFor(event);
295
344
  return {
296
345
  ...matcher !== void 0 ? { matcher } : {},
297
- hooks: [{ type: "command", command: cmd2([harness, event]).command, timeout: o.timeoutFor?.(event) ?? o.timeout }]
346
+ hooks: [{ type: "command", command: cmd([harness, event]).command, timeout: o.timeoutFor?.(event) ?? o.timeout }]
298
347
  };
299
348
  }
300
- function installClaudeShaped(file, harness, events, cmd2, o) {
349
+ function installClaudeShaped(file, harness, events, cmd, o) {
301
350
  const f = readJsonFile(file);
302
351
  if (!f.ok) return { file, changed: false, skipped: true, note: `${f.reason}; not touched` };
303
352
  const settings2 = f.value;
@@ -316,7 +365,7 @@ function installClaudeShaped(file, harness, events, cmd2, o) {
316
365
  }
317
366
  for (const ev of events) {
318
367
  const list = Array.isArray(hooks[ev]) ? hooks[ev] : [];
319
- const entry2 = entryFor(ev, harness, cmd2, o);
368
+ const entry2 = entryFor(ev, harness, cmd, o);
320
369
  const mine = list.filter((h) => isOurs(h));
321
370
  if (mine.length === 1 && JSON.stringify(mine[0]) === JSON.stringify(entry2)) continue;
322
371
  hooks[ev] = list.filter((h) => !isOurs(h)).concat([entry2]);
@@ -359,11 +408,10 @@ function uninstallClaudeShaped(file, events, opts = {}) {
359
408
  unlinkSync(file);
360
409
  return { file, changed: true, note: "removed (sessionpipe created it)" };
361
410
  }
362
- const next = formatJson(settings2, f);
363
- if (next !== f.raw) writeFileSync(file, next);
411
+ writeFileSync(file, formatJson(settings2, f));
364
412
  return { file, changed: true };
365
413
  }
366
- function installedClaudeShaped(file, harness, events, cmd2, o) {
414
+ function installedClaudeShaped(file, harness, events, cmd, o) {
367
415
  const f = readJsonFile(file);
368
416
  const hooks = f.ok ? f.value.hooks ?? {} : {};
369
417
  let present = 0;
@@ -372,7 +420,7 @@ function installedClaudeShaped(file, harness, events, cmd2, o) {
372
420
  const list = Array.isArray(hooks[ev]) ? hooks[ev] : [];
373
421
  const mine = list.filter((h) => isOurs(h));
374
422
  if (mine.length) present++;
375
- if (mine.length === 1 && JSON.stringify(mine[0]) === JSON.stringify(entryFor(ev, harness, cmd2, o))) current++;
423
+ if (mine.length === 1 && JSON.stringify(mine[0]) === JSON.stringify(entryFor(ev, harness, cmd, o))) current++;
376
424
  }
377
425
  for (const ev of Object.keys(hooks))
378
426
  if (!events.includes(ev) && (Array.isArray(hooks[ev]) ? hooks[ev] : []).some((h) => isOurs(h)))
@@ -387,14 +435,14 @@ var HOOK_NAME = "sessionpipe";
387
435
  var AG_EVENTS = ["PreInvocation", "PostInvocation", "PostToolUse", "Stop"];
388
436
  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';
389
437
  var antigravityAnswer = (event) => event === "PreToolUse" ? AG_STALE_PRETOOL : "{}\n";
390
- var gemini = (env) => env.GEMINI_CLI_HOME || path4.join(HOME, ".gemini");
438
+ var gemini = (env) => env.GEMINI_CLI_HOME || path4.join(homeDir(), ".gemini");
391
439
  var hooksFile = (env) => path4.join(gemini(env), "config", "hooks.json");
392
440
  var projectsDir = (env) => path4.join(gemini(env), "config", "projects");
393
441
  var dataDirs = (env) => ["antigravity", "antigravity-cli", "antigravity-ide"].map((d) => path4.join(gemini(env), d));
394
442
  var ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
395
443
  var validTool = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
396
- function entry(cmd2) {
397
- const handler = (e) => ({ type: "command", command: cmd2([NAME, e]).command, timeout: 5 });
444
+ function entry(cmd) {
445
+ const handler = (e) => ({ type: "command", command: cmd([NAME, e]).command, timeout: 5 });
398
446
  const out2 = { enabled: true };
399
447
  for (const e of AG_EVENTS) out2[e] = /Tool/.test(e) ? [{ matcher: "*", hooks: [handler(e)] }] : [handler(e)];
400
448
  return out2;
@@ -404,12 +452,12 @@ var antigravity = {
404
452
  events: AG_EVENTS,
405
453
  detect: (env = process.env) => existsSync2(path4.join(gemini(env), "config")) || dataDirs(env).some((d) => existsSync2(d)),
406
454
  configFiles: (env = process.env) => [hooksFile(env)],
407
- install(cmd2, env = process.env) {
455
+ install(cmd, env = process.env) {
408
456
  const file = hooksFile(env);
409
457
  const f = readJsonFile(file);
410
458
  if (!f.ok) return [{ file, changed: false, skipped: true, note: `${f.reason}; not touched` }];
411
459
  const hooks = f.value;
412
- const e = entry(cmd2);
460
+ const e = entry(cmd);
413
461
  if (JSON.stringify(hooks[HOOK_NAME]) === JSON.stringify(e)) return [{ file, changed: false }];
414
462
  hooks[HOOK_NAME] = e;
415
463
  mkdirSync2(path4.dirname(file), { recursive: true });
@@ -438,14 +486,14 @@ var antigravity = {
438
486
  writeFileSync2(file, formatJson(hooks, f));
439
487
  return [{ file, changed: true }];
440
488
  },
441
- installed(cmd2, env = process.env) {
489
+ installed(cmd, env = process.env) {
442
490
  const file = hooksFile(env);
443
491
  const f = readJsonFile(file);
444
492
  const have = f.ok ? f.value[HOOK_NAME] : void 0;
445
493
  return [
446
494
  {
447
495
  file,
448
- state: !have ? "missing" : JSON.stringify(have) === JSON.stringify(entry(cmd2)) ? "current" : "stale"
496
+ state: !have ? "missing" : JSON.stringify(have) === JSON.stringify(entry(cmd)) ? "current" : "stale"
449
497
  }
450
498
  ];
451
499
  },
@@ -453,7 +501,8 @@ var antigravity = {
453
501
  const event = input.argv[1] ?? "";
454
502
  let s = {};
455
503
  try {
456
- s = input.stdin ? JSON.parse(input.stdin) : {};
504
+ const j = input.stdin ? JSON.parse(input.stdin) : {};
505
+ if (j && typeof j === "object" && !Array.isArray(j)) s = j;
457
506
  } catch {
458
507
  }
459
508
  const id = s.conversationId ?? s.session_id ?? s.sessionId;
@@ -509,19 +558,16 @@ var antigravity = {
509
558
  }
510
559
  return readAntigravity(text.split("\n"), fromLine);
511
560
  },
512
- facts(session, transcript, hints, state2) {
561
+ facts(session, transcript, hints) {
513
562
  if (!transcript) return {};
514
563
  return antigravityFacts(transcript, {
515
564
  conversation: session.id,
516
565
  workspaces: hints.workspaces ?? (session.cwd ? [session.cwd] : []),
517
- state: state2,
518
566
  env: process.env
519
567
  });
520
568
  },
521
569
  backfill(sinceMs, env = process.env) {
522
- const rows = [];
523
- const noState = { get: () => ({}), save: () => {
524
- } };
570
+ const rows2 = [];
525
571
  for (const data of dataDirs(env)) {
526
572
  let ids = [];
527
573
  try {
@@ -535,13 +581,13 @@ var antigravity = {
535
581
  } catch {
536
582
  continue;
537
583
  }
538
- const facts = antigravityFacts(f, { conversation: id, workspaces: [], state: noState, env });
584
+ const facts = antigravityFacts(f, { conversation: id, workspaces: [], env });
539
585
  if (!facts.started_at || !facts.last_at) continue;
540
586
  const { started_at, last_at, first_ask: _fa, harness_version: _hv, ...rest } = facts;
541
- rows.push({ session: { id, ...rest }, started_at, last_at });
587
+ rows2.push({ session: { id, ...rest }, started_at, last_at });
542
588
  }
543
589
  }
544
- return rows;
590
+ return rows2;
545
591
  }
546
592
  };
547
593
  function prune(o) {
@@ -591,7 +637,7 @@ function agProjects(dir) {
591
637
  }
592
638
  const folders = [];
593
639
  for (const r of j?.projectResources?.resources ?? []) {
594
- const f = agPath(r?.folderUri);
640
+ const f = agPath(r?.folderUri ?? r?.gitFolder?.folderUri);
595
641
  if (f) folders.push(...agVariants(f));
596
642
  }
597
643
  if (folders.length) out2.push({ id, folders });
@@ -645,7 +691,7 @@ function antigravityFacts(file, opts) {
645
691
  for (const j of H) {
646
692
  if (j.type !== "USER_INPUT" || typeof j.content !== "string") continue;
647
693
  const m = /<USER_REQUEST>([\s\S]*?)(?:<\/USER_REQUEST>|$)/.exec(j.content);
648
- const text = (m ? m[1] ?? "" : j.content).trim();
694
+ const text = (m ? m[1] : j.content).trim();
649
695
  const line = (text.split("\n").find((l) => l.trim()) ?? "").trim();
650
696
  if (!line || /^<[a-z_-]+>/i.test(line)) continue;
651
697
  out2.title = line.length > 80 ? `${line.slice(0, 80).replace(/\s+\S*$/, "")}\u2026` : line;
@@ -744,7 +790,7 @@ function runHeadless(bin, args2, o) {
744
790
  if (o.onLine) {
745
791
  const chunk = partial + b.toString("utf8");
746
792
  const lines = chunk.split("\n");
747
- partial = lines.pop() ?? "";
793
+ partial = lines.pop();
748
794
  for (const line of lines) {
749
795
  if (!line.trim()) continue;
750
796
  try {
@@ -893,7 +939,7 @@ function agyError(stderr) {
893
939
  for (const k of ["message", "error", "detail"]) if (typeof j[k] === "string" && j[k]) return j[k];
894
940
  } catch {
895
941
  }
896
- return raw.split("\n")[0]?.slice(0, 300) ?? "";
942
+ return raw.split("\n")[0].slice(0, 300);
897
943
  }
898
944
  var AgyReading = class {
899
945
  session = null;
@@ -970,10 +1016,9 @@ function gitFacts(cwd) {
970
1016
  return out2;
971
1017
  }
972
1018
  function stripCredentials(url) {
973
- let u = url.replace(/\/\/[^/@]*@/, "//");
974
- const ssh = /^(?:ssh:\/\/)?git@([^:/]+)[:/](.+?)(?:\.git)?$/.exec(u);
975
- if (ssh) u = `${ssh[1]}/${ssh[2]}`;
976
- return u.replace(/^https?:\/\//, "").replace(/\.git$/, "");
1019
+ const ssh = /^(?:ssh:\/\/)?git@([^:/]+)[:/](.+?)(?:\.git)?$/.exec(url);
1020
+ if (ssh) return `${ssh[1]}/${ssh[2]}`;
1021
+ return url.replace(/\/\/[^/@]*@/, "//").replace(/^https?:\/\//, "").replace(/\.git$/, "");
977
1022
  }
978
1023
 
979
1024
  // ../core/src/adapters/claude-code.ts
@@ -1004,7 +1049,7 @@ var OPTS = {
1004
1049
  timeoutFor: (e) => e === "PermissionRequest" ? 130 : void 0
1005
1050
  };
1006
1051
  var DESKTOP_ID = /^local_[A-Za-z0-9-]{1,64}$/;
1007
- function claudeDirs(env = process.env, home = env.HOME || HOME) {
1052
+ function claudeDirs(env = process.env, home = env.HOME || homeDir()) {
1008
1053
  const out2 = [];
1009
1054
  const add = (d) => {
1010
1055
  if (!d) return;
@@ -1033,23 +1078,25 @@ var claudeCode = {
1033
1078
  events: CLAUDE_EVENTS,
1034
1079
  detect: (env = process.env) => claudeDirs(env).some((d) => existsSync3(d)),
1035
1080
  configFiles: (env = process.env) => claudeDirs(env).map(settingsIn),
1036
- install: (cmd2, env = process.env, opts = {}) => claudeDirs(env).map((d) => installClaudeShaped(settingsIn(d), NAME2, eventsFor(opts.lean), cmd2, OPTS)),
1081
+ install: (cmd, env = process.env, opts = {}) => claudeDirs(env).map((d) => installClaudeShaped(settingsIn(d), NAME2, eventsFor(opts.lean), cmd, OPTS)),
1037
1082
  uninstall: (env = process.env, opts = {}) => claudeDirs(env).map(
1038
1083
  (d) => uninstallClaudeShaped(settingsIn(d), CLAUDE_EVENTS, { created: opts.created?.includes(settingsIn(d)) })
1039
1084
  ),
1040
- installed: (cmd2, env = process.env, opts = {}) => claudeDirs(env).map((d) => ({
1085
+ installed: (cmd, env = process.env, opts = {}) => claudeDirs(env).map((d) => ({
1041
1086
  file: settingsIn(d),
1042
- state: installedClaudeShaped(settingsIn(d), NAME2, eventsFor(opts.lean), cmd2, OPTS)
1087
+ state: installedClaudeShaped(settingsIn(d), NAME2, eventsFor(opts.lean), cmd, OPTS)
1043
1088
  })),
1044
1089
  fromHook(input) {
1045
1090
  const event = input.argv[1] ?? "";
1046
1091
  let s = {};
1047
1092
  try {
1048
- s = input.stdin ? JSON.parse(input.stdin) : {};
1093
+ const j = input.stdin ? JSON.parse(input.stdin) : {};
1094
+ if (j && typeof j === "object" && !Array.isArray(j)) s = j;
1049
1095
  } catch {
1050
1096
  }
1051
1097
  const id = s.session_id ?? s.sessionId;
1052
1098
  if (!id) return null;
1099
+ if (notClaudeCode(String(id), s)) return null;
1053
1100
  const session = { id: String(id), cwd: typeof s.cwd === "string" ? s.cwd : input.cwd };
1054
1101
  if (typeof s.model === "string") session.model = s.model;
1055
1102
  if (typeof s.agent_id === "string" && event.startsWith("Subagent") === false && s.agent_id)
@@ -1168,6 +1215,7 @@ var claudeCode = {
1168
1215
  return null;
1169
1216
  }
1170
1217
  const hints = {};
1218
+ if (event === "UserPromptSubmit" && typeof s.prompt === "string") hints.prompt = s.prompt;
1171
1219
  const host = input.env.CLAUDE_CODE_HOST_SESSION_ID;
1172
1220
  if (host && DESKTOP_ID.test(host)) hints.desktop_id = host;
1173
1221
  if (event !== "PreToolUse" && event !== "PostToolUse") hints.claude_pid = claudePid(input.env);
@@ -1193,7 +1241,7 @@ var claudeCode = {
1193
1241
  return out2;
1194
1242
  },
1195
1243
  backfill(sinceMs, env = process.env) {
1196
- const rows = [];
1244
+ const rows2 = [];
1197
1245
  const files = [];
1198
1246
  for (const d of claudeDirs(env))
1199
1247
  files.push(...recentFiles(path7.join(d, "projects"), 1, sinceMs, (n) => /^[0-9a-f-]{36}\.jsonl$/.test(n)));
@@ -1205,19 +1253,23 @@ var claudeCode = {
1205
1253
  if (!facts.started_at || !facts.last_at) continue;
1206
1254
  const { started_at, last_at, first_ask: _fa, harness_version: _hv, ...rest } = facts;
1207
1255
  const cwd = rest.cwd;
1208
- rows.push({
1256
+ rows2.push({
1209
1257
  session: { id, ...rest, ...gitFacts(cwd), ...rest.branch ? { branch: rest.branch } : {} },
1210
1258
  started_at,
1211
1259
  last_at
1212
1260
  });
1213
1261
  }
1214
- return rows;
1262
+ return rows2;
1215
1263
  }
1216
1264
  };
1217
1265
  function prune2(o) {
1218
1266
  for (const k of Object.keys(o)) if (o[k] === void 0) delete o[k];
1219
1267
  return o;
1220
1268
  }
1269
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1270
+ function notClaudeCode(id, s) {
1271
+ return typeof s.transcript_path !== "string" && !UUID.test(id);
1272
+ }
1221
1273
  function pickSource(v) {
1222
1274
  return typeof v === "string" && ["startup", "resume", "clear", "compact", "fork"].includes(v) ? v : "unknown";
1223
1275
  }
@@ -1323,26 +1375,41 @@ function parentOf() {
1323
1375
  }
1324
1376
  return (pid) => table.get(pid) || 0;
1325
1377
  }
1326
- var accountCache = /* @__PURE__ */ new Map();
1327
- function claudeAccount(dir) {
1328
- const hit = accountCache.get(dir);
1329
- if (hit !== void 0) return hit;
1378
+ var EMAIL_RE = /^[^@\s]{1,64}@[^@\s]{1,189}$/;
1379
+ function claudeLogin(dir, home = homeDir()) {
1330
1380
  const files = [path7.join(dir, ".claude.json")];
1331
- if (path7.resolve(dir) === path7.join(HOME, ".claude")) files.unshift(path7.join(HOME, ".claude.json"));
1332
- let acct = null;
1381
+ if (path7.resolve(dir) === path7.join(home, ".claude")) files.unshift(path7.join(home, ".claude.json"));
1333
1382
  for (const f of files) {
1334
1383
  try {
1335
1384
  const a = JSON.parse(readFileSync3(f, "utf8")).oauthAccount;
1336
- if (a?.accountUuid) {
1337
- acct = String(a.accountUuid);
1338
- break;
1385
+ if (typeof a?.accountUuid === "string" && a.accountUuid) {
1386
+ const email = typeof a.emailAddress === "string" && EMAIL_RE.test(a.emailAddress) ? a.emailAddress : void 0;
1387
+ return { id: a.accountUuid.slice(0, 128), ...email ? { email } : {} };
1339
1388
  }
1340
1389
  } catch {
1341
1390
  }
1342
1391
  }
1392
+ return null;
1393
+ }
1394
+ var accountCache = /* @__PURE__ */ new Map();
1395
+ function claudeAccount(dir) {
1396
+ const hit = accountCache.get(dir);
1397
+ if (hit !== void 0) return hit;
1398
+ const acct = claudeLogin(dir)?.id ?? null;
1343
1399
  accountCache.set(dir, acct);
1344
1400
  return acct;
1345
1401
  }
1402
+ function claudeAccounts(dirs, home = homeDir()) {
1403
+ const out2 = [];
1404
+ for (const d of dirs) {
1405
+ const l = claudeLogin(d, home);
1406
+ if (!l) continue;
1407
+ const seen = out2.find((a) => a.id === l.id);
1408
+ if (!seen) out2.push({ harness: NAME2, ...l });
1409
+ else if (!seen.email && l.email) seen.email = l.email;
1410
+ }
1411
+ return out2;
1412
+ }
1346
1413
  var LINK_RE = /^https:\/\/claude\.ai\/code\/session_[A-Za-z0-9]+$/;
1347
1414
  function transcriptLink(records) {
1348
1415
  for (let i = records.length - 1; i >= 0; i--) {
@@ -1498,7 +1565,8 @@ var claude_code_control_exports = {};
1498
1565
  __export(claude_code_control_exports, {
1499
1566
  JOB_TIMEOUT_MS: () => JOB_TIMEOUT_MS,
1500
1567
  claudeEnv: () => claudeEnv,
1501
- claudeLogin: () => claudeLogin,
1568
+ claudeLogin: () => claudeLogin2,
1569
+ cmpVersion: () => cmpVersion,
1502
1570
  detectCaps: () => detectCaps,
1503
1571
  findClaude: () => findClaude,
1504
1572
  findTranscript: () => findTranscript,
@@ -1624,7 +1692,7 @@ function parseAuthStatus(stdout) {
1624
1692
  return { loggedIn: null };
1625
1693
  }
1626
1694
  }
1627
- function claudeLogin(bin, configDir, env = process.env) {
1695
+ function claudeLogin2(bin, configDir, env = process.env) {
1628
1696
  return new Promise((resolve) => {
1629
1697
  execFile(
1630
1698
  bin,
@@ -1875,18 +1943,13 @@ function parseArgv(raw) {
1875
1943
  function readNotify(text) {
1876
1944
  const { head, tail: tail2, at } = split(text);
1877
1945
  const m = NOTIFY_LINE.exec(head);
1878
- if (m)
1879
- return {
1880
- raw: m[1] ?? "",
1881
- argv: parseArgv(m[1] ?? ""),
1882
- index: m.index,
1883
- length: m[0].length,
1884
- misplaced: false,
1885
- other: !OURS2.test(m[1] ?? "")
1886
- };
1946
+ if (m) {
1947
+ const raw = m[1];
1948
+ return { raw, argv: parseArgv(raw), index: m.index, length: m[0].length, misplaced: false, other: !OURS2.test(raw) };
1949
+ }
1887
1950
  const k = NOTIFY_KEY.exec(head);
1888
1951
  if (k) {
1889
- const rest = head.slice(k.index).split("\n")[0] ?? "";
1952
+ const rest = head.slice(k.index).split("\n")[0];
1890
1953
  return {
1891
1954
  raw: rest.replace(NOTIFY_KEY, "").trim(),
1892
1955
  argv: null,
@@ -1897,15 +1960,11 @@ function readNotify(text) {
1897
1960
  };
1898
1961
  }
1899
1962
  const t = NOTIFY_LINE.exec(tail2);
1900
- if (t && OURS2.test(t[1] ?? ""))
1901
- return {
1902
- raw: t[1] ?? "",
1903
- argv: parseArgv(t[1] ?? ""),
1904
- index: at + t.index,
1905
- length: t[0].length,
1906
- misplaced: true,
1907
- other: false
1908
- };
1963
+ if (t) {
1964
+ const raw = t[1];
1965
+ if (OURS2.test(raw))
1966
+ return { raw, argv: parseArgv(raw), index: at + t.index, length: t[0].length, misplaced: true, other: false };
1967
+ }
1909
1968
  return null;
1910
1969
  }
1911
1970
  function dropLine(text, index, length) {
@@ -1924,9 +1983,7 @@ function withNotify(text, line) {
1924
1983
  if (n?.misplaced) return insertTopLevel(dropLine(text, n.index, n.length), line);
1925
1984
  return insertTopLevel(text, line);
1926
1985
  }
1927
- function replaceNotify(text, line) {
1928
- const n = readNotify(text);
1929
- if (!n || n.misplaced || !n.length) return null;
1986
+ function replaceNotify(text, n, line) {
1930
1987
  return text.slice(0, n.index) + line + text.slice(n.index + n.length);
1931
1988
  }
1932
1989
  function withoutNotify(text) {
@@ -1967,20 +2024,20 @@ var CODEX_EVENTS = [
1967
2024
  ];
1968
2025
  var OPTS2 = { matcherFor: (e) => /Tool|Permission/.test(e) ? "" : void 0, timeout: 5 };
1969
2026
  var eventsFor2 = (lean) => lean ? CODEX_EVENTS.filter((e) => e !== "PreToolUse") : [...CODEX_EVENTS];
1970
- var codexHome = (env) => env.CODEX_HOME || path9.join(HOME, ".codex");
2027
+ var codexHome = (env) => env.CODEX_HOME || path9.join(homeDir(), ".codex");
1971
2028
  var hooksFile2 = (env) => path9.join(codexHome(env), "hooks.json");
1972
2029
  var configFile2 = (env) => path9.join(codexHome(env), "config.toml");
1973
2030
  var sessionsDir = (env) => path9.join(codexHome(env), "sessions");
1974
2031
  var validTool3 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
1975
2032
  var endReason2 = (r) => r === "clear" || r === "logout" || r === "exit" ? r : "other";
1976
- function installNotify(cmd2, env) {
2033
+ function installNotify(cmd, env) {
1977
2034
  const file = configFile2(env);
1978
2035
  let text = "";
1979
2036
  try {
1980
2037
  text = readFileSync5(file, "utf8");
1981
2038
  } catch {
1982
2039
  }
1983
- const argv = cmd2([NAME3, "notify"]).argv;
2040
+ const argv = cmd([NAME3, "notify"]).argv;
1984
2041
  const line = `notify = ${JSON.stringify(argv)}`;
1985
2042
  const n = readNotify(text);
1986
2043
  const write = (next) => {
@@ -2005,11 +2062,8 @@ function installNotify(cmd2, env) {
2005
2062
  return { file, changed: false, note: "notify current (after Codex Computer Use)" };
2006
2063
  }
2007
2064
  if (!prev || OURS2.test(JSON.stringify(prev))) {
2008
- const next = replaceNotify(text, `notify = ${withPreviousRaw(n.raw, argv)}`);
2009
- if (next) {
2010
- write(next);
2011
- return { file, changed: true, note: "chained after Codex Computer Use (--previous-notify)" };
2012
- }
2065
+ write(replaceNotify(text, n, `notify = ${withPreviousRaw(n.raw, argv)}`));
2066
+ return { file, changed: true, note: "chained after Codex Computer Use (--previous-notify)" };
2013
2067
  }
2014
2068
  }
2015
2069
  return {
@@ -2038,8 +2092,7 @@ function uninstallNotify(env) {
2038
2092
  }
2039
2093
  const prev = previousNotify(n.argv);
2040
2094
  if (isComputerUse(n.argv) && prev && OURS2.test(JSON.stringify(prev))) {
2041
- const next = replaceNotify(text, `notify = ${stripPreviousRaw(n.raw)}`);
2042
- if (next) writeFileSync3(file, next);
2095
+ writeFileSync3(file, replaceNotify(text, n, `notify = ${stripPreviousRaw(n.raw)}`));
2043
2096
  return { file, changed: true };
2044
2097
  }
2045
2098
  return { file, changed: false };
@@ -2069,24 +2122,31 @@ function findRollout(threadId, env = process.env) {
2069
2122
  walk(sessionsDir(env), 0);
2070
2123
  return best;
2071
2124
  }
2125
+ function rolloutMeta(file) {
2126
+ return scanLines(file, (line) => parseLines([line])[0] ?? {});
2127
+ }
2128
+ function ofAnotherThread(file, id) {
2129
+ 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);
2130
+ return !!m?.[1] && m[1].toLowerCase() !== id.toLowerCase();
2131
+ }
2072
2132
  var codex = {
2073
2133
  name: NAME3,
2074
2134
  events: CODEX_EVENTS,
2075
2135
  detect: (env = process.env) => existsSync4(codexHome(env)),
2076
2136
  configFiles: (env = process.env) => [hooksFile2(env), configFile2(env)],
2077
- install: (cmd2, env = process.env, opts = {}) => [
2078
- installClaudeShaped(hooksFile2(env), NAME3, eventsFor2(opts.lean), cmd2, OPTS2),
2079
- installNotify(cmd2, env)
2137
+ install: (cmd, env = process.env, opts = {}) => [
2138
+ installClaudeShaped(hooksFile2(env), NAME3, eventsFor2(opts.lean), cmd, OPTS2),
2139
+ installNotify(cmd, env)
2080
2140
  ],
2081
2141
  uninstall: (env = process.env, opts = {}) => [
2082
2142
  uninstallClaudeShaped(hooksFile2(env), CODEX_EVENTS, { created: opts.created?.includes(hooksFile2(env)) }),
2083
2143
  uninstallNotify(env)
2084
2144
  ],
2085
- installed: (cmd2, env = process.env, opts = {}) => {
2145
+ installed: (cmd, env = process.env, opts = {}) => {
2086
2146
  const out2 = [
2087
2147
  {
2088
2148
  file: hooksFile2(env),
2089
- state: installedClaudeShaped(hooksFile2(env), NAME3, eventsFor2(opts.lean), cmd2, OPTS2),
2149
+ state: installedClaudeShaped(hooksFile2(env), NAME3, eventsFor2(opts.lean), cmd, OPTS2),
2090
2150
  note: "Codex runs non-managed hooks only after the review in `codex` (/hooks); until then nothing is reported unless the notify fallback is ours"
2091
2151
  }
2092
2152
  ];
@@ -2096,7 +2156,7 @@ var codex = {
2096
2156
  } catch {
2097
2157
  }
2098
2158
  const n = readNotify(text);
2099
- const argv = cmd2([NAME3, "notify"]).argv;
2159
+ const argv = cmd([NAME3, "notify"]).argv;
2100
2160
  const prev = n ? previousNotify(n.argv) : null;
2101
2161
  const ours = n && (!n.other || prev && JSON.stringify(prev) === JSON.stringify(argv));
2102
2162
  out2.push({
@@ -2111,11 +2171,14 @@ var codex = {
2111
2171
  if (event === "notify") {
2112
2172
  const raw = input.argv.find((a) => a.startsWith("{")) ?? input.stdin;
2113
2173
  try {
2114
- s = raw ? JSON.parse(raw) : {};
2174
+ const j = raw ? JSON.parse(raw) : {};
2175
+ if (j && typeof j === "object" && !Array.isArray(j)) s = j;
2115
2176
  } catch {
2116
2177
  }
2117
2178
  const id2 = s["thread-id"] ?? s.thread_id ?? s["session-id"] ?? s.session_id;
2118
2179
  if (!id2) return null;
2180
+ const file = findRollout(String(id2), input.env);
2181
+ if (!file || codexSubagent(rolloutMeta(file))) return null;
2119
2182
  const r2 = {
2120
2183
  session: { id: String(id2), cwd: typeof s.cwd === "string" ? s.cwd : input.cwd },
2121
2184
  events: [
@@ -2125,18 +2188,19 @@ var codex = {
2125
2188
  harnessEvent: "notify"
2126
2189
  }
2127
2190
  ],
2128
- hints: {}
2191
+ hints: {},
2192
+ transcript: file
2129
2193
  };
2130
- const file = findRollout(String(id2), input.env);
2131
- if (file) r2.transcript = file;
2132
2194
  return r2;
2133
2195
  }
2134
2196
  try {
2135
- s = input.stdin ? JSON.parse(input.stdin) : {};
2197
+ const j = input.stdin ? JSON.parse(input.stdin) : {};
2198
+ if (j && typeof j === "object" && !Array.isArray(j)) s = j;
2136
2199
  } catch {
2137
2200
  }
2138
2201
  const id = s.session_id;
2139
2202
  if (!id) return null;
2203
+ if (s.transcript_path === null) return null;
2140
2204
  const session = { id: String(id), cwd: typeof s.cwd === "string" ? s.cwd : input.cwd };
2141
2205
  if (typeof s.model === "string") session.model = s.model;
2142
2206
  const turn_id = typeof s.turn_id === "string" ? s.turn_id : void 0;
@@ -2208,7 +2272,8 @@ var codex = {
2208
2272
  return null;
2209
2273
  }
2210
2274
  const r = { session, events, hints: {} };
2211
- if (typeof s.transcript_path === "string") r.transcript = s.transcript_path;
2275
+ if (typeof s.transcript_path === "string" && !ofAnotherThread(s.transcript_path, String(id)))
2276
+ r.transcript = s.transcript_path;
2212
2277
  return r;
2213
2278
  },
2214
2279
  readTranscript(file, fromLine) {
@@ -2226,16 +2291,17 @@ var codex = {
2226
2291
  return out2;
2227
2292
  },
2228
2293
  backfill(sinceMs, env = process.env) {
2229
- const rows = [];
2294
+ const rows2 = [];
2230
2295
  for (const f of recentFiles(sessionsDir(env), 3, sinceMs, (n) => /^rollout-.*\.jsonl$/.test(n))) {
2231
2296
  const m = /([0-9a-f]{8}-[0-9a-f-]{27,})\.jsonl$/i.exec(path9.basename(f));
2232
2297
  if (!m?.[1]) continue;
2298
+ if (codexSubagent(rolloutMeta(f))) continue;
2233
2299
  const facts = codexFacts(f);
2234
2300
  if (!facts.started_at || !facts.last_at) continue;
2235
2301
  const { started_at, last_at, first_ask: _fa, harness_version: _hv, ...rest } = facts;
2236
- rows.push({ session: { id: m[1], ...rest }, started_at, last_at });
2302
+ rows2.push({ session: { id: m[1], ...rest }, started_at, last_at });
2237
2303
  }
2238
- return rows;
2304
+ return rows2;
2239
2305
  }
2240
2306
  };
2241
2307
  function prune3(o) {
@@ -2244,10 +2310,11 @@ function prune3(o) {
2244
2310
  }
2245
2311
  function codexFacts(file) {
2246
2312
  const { head, tail: tail2 } = ends(file, 128 * 1024, 64 * 1024);
2247
- const H = parseLines(head);
2248
- const T = parseLines(tail2);
2313
+ let H = parseLines(head);
2314
+ let T = parseLines(tail2);
2249
2315
  const out2 = {};
2250
2316
  const meta = H.find((j) => j.type === "session_meta" && j.payload);
2317
+ const boundary = codexReplayBoundary(meta);
2251
2318
  if (meta?.payload) {
2252
2319
  const p = meta.payload;
2253
2320
  if (p.cwd) out2.cwd = p.cwd;
@@ -2255,6 +2322,13 @@ function codexFacts(file) {
2255
2322
  if (p.git?.branch) out2.branch = p.git.branch;
2256
2323
  if (p.git?.repository_url) out2.repo = String(p.git.repository_url).replace(/\/\/[^/@]*@/, "//");
2257
2324
  }
2325
+ if (boundary) {
2326
+ const h = H.findIndex(boundary);
2327
+ const t = T.findIndex(boundary);
2328
+ H = h < 0 ? [] : H.slice(h);
2329
+ const found = h >= 0 || t >= 0 || scanLines(file, (line) => boundary(parseLines([line])[0] ?? null) ? true : null, Number.POSITIVE_INFINITY);
2330
+ T = t >= 0 ? T.slice(t) : found ? T : [];
2331
+ }
2258
2332
  const ctx = H.concat(T).reverse().find((j) => j.type === "turn_context" && j.payload?.model);
2259
2333
  if (ctx?.payload?.model) out2.model = ctx.payload.model;
2260
2334
  for (const j of H) {
@@ -2262,12 +2336,12 @@ function codexFacts(file) {
2262
2336
  if (j.type !== "response_item" || !p || p.type !== "message" || p.role !== "user") continue;
2263
2337
  const text = (p.content ?? []).filter((b) => b && b.type === "input_text" && typeof b.text === "string").map((b) => b.text).join("\n").trim();
2264
2338
  if (!text || codexInjected(text)) continue;
2265
- const line = text.split("\n").find((l) => l.trim()) ?? "";
2339
+ const line = text.split("\n")[0];
2266
2340
  out2.title = line.length > 80 ? `${line.slice(0, 80).replace(/\s+\S*$/, "")}\u2026` : line;
2267
2341
  out2.title_source = "first-ask";
2268
2342
  break;
2269
2343
  }
2270
- const times = H.concat(T).map((j) => Date.parse(String(j.timestamp ?? ""))).filter((n) => n > 0);
2344
+ const times = (boundary && meta ? [meta, ...H, ...T] : H.concat(T)).map((j) => Date.parse(String(j.timestamp ?? ""))).filter((n) => n > 0);
2271
2345
  if (times.length) {
2272
2346
  out2.started_at = Math.min(...times);
2273
2347
  out2.last_at = Math.max(...times);
@@ -2409,11 +2483,432 @@ function codexStderr(stderr) {
2409
2483
  }
2410
2484
  var reading2 = () => new CodexReading();
2411
2485
 
2412
- // ../core/src/adapters/gemini-cli.ts
2413
- import { execFileSync as execFileSync4 } from "node:child_process";
2414
- import { existsSync as existsSync5, readFileSync as readFileSync6 } from "node:fs";
2486
+ // ../core/src/adapters/devin.ts
2487
+ import { createHash as createHash2 } from "node:crypto";
2488
+ import { existsSync as existsSync6, readlinkSync } from "node:fs";
2415
2489
  import path11 from "node:path";
2416
- var NAME4 = "gemini-cli";
2490
+
2491
+ // ../core/src/readers/sqlite.ts
2492
+ import { execFileSync as execFileSync4 } from "node:child_process";
2493
+ import { existsSync as existsSync5 } from "node:fs";
2494
+ import { createRequire } from "node:module";
2495
+ function quiet(fn) {
2496
+ const original = process.emitWarning;
2497
+ try {
2498
+ process.emitWarning = ((warning, ...rest) => {
2499
+ const kind = typeof rest[0] === "string" ? rest[0] : rest[0]?.type;
2500
+ if (kind === "ExperimentalWarning" && /sqlite/i.test(String(warning))) return;
2501
+ original.call(process, warning, ...rest);
2502
+ });
2503
+ return fn();
2504
+ } finally {
2505
+ process.emitWarning = original;
2506
+ }
2507
+ }
2508
+ var ctorCache;
2509
+ function dbCtor() {
2510
+ if (ctorCache !== void 0) return ctorCache;
2511
+ ctorCache = null;
2512
+ try {
2513
+ const req = createRequire(import.meta.url);
2514
+ const m = quiet(() => req("node:sqlite"));
2515
+ if (typeof m?.DatabaseSync === "function") ctorCache = m.DatabaseSync;
2516
+ } catch {
2517
+ }
2518
+ return ctorCache;
2519
+ }
2520
+ function plain(row) {
2521
+ if (!row || typeof row !== "object") return null;
2522
+ const out2 = {};
2523
+ for (const [k, v] of Object.entries(row)) {
2524
+ if (typeof v === "bigint") out2[k] = Number(v);
2525
+ else if (v instanceof Uint8Array) out2[k] = null;
2526
+ else out2[k] = v;
2527
+ }
2528
+ return out2;
2529
+ }
2530
+ var rows = (v) => {
2531
+ if (!Array.isArray(v)) return null;
2532
+ const out2 = [];
2533
+ for (const r of v) {
2534
+ const p = plain(r);
2535
+ if (p) out2.push(p);
2536
+ }
2537
+ return out2;
2538
+ };
2539
+ function viaNode(file, sql, params, o) {
2540
+ const C = dbCtor();
2541
+ if (!C) return null;
2542
+ let db;
2543
+ try {
2544
+ db = quiet(() => new C(file, { readOnly: true, timeout: o.timeoutMs }));
2545
+ } catch {
2546
+ return null;
2547
+ }
2548
+ try {
2549
+ try {
2550
+ db.exec(`PRAGMA busy_timeout = ${Math.round(o.timeoutMs)}`);
2551
+ } catch {
2552
+ }
2553
+ return quiet(() => rows(db.prepare(sql).all(...params)));
2554
+ } catch {
2555
+ return null;
2556
+ } finally {
2557
+ try {
2558
+ db.close();
2559
+ } catch {
2560
+ }
2561
+ }
2562
+ }
2563
+ var CHILD = `const { DatabaseSync } = require("node:sqlite");
2564
+ const [, file, sql, params, ms] = process.argv;
2565
+ const db = new DatabaseSync(file, { readOnly: true, timeout: Number(ms) });
2566
+ try { db.exec("PRAGMA busy_timeout = " + Number(ms)); } catch {}
2567
+ const out = db.prepare(sql).all(...JSON.parse(params));
2568
+ db.close();
2569
+ process.stdout.write(JSON.stringify(out));`;
2570
+ function viaChild(file, sql, params, o) {
2571
+ try {
2572
+ const out2 = execFileSync4(
2573
+ process.execPath,
2574
+ ["--experimental-sqlite", "--no-warnings", "-e", CHILD, file, sql, JSON.stringify(params), String(o.timeoutMs)],
2575
+ {
2576
+ encoding: "utf8",
2577
+ timeout: o.timeoutMs + 3e3,
2578
+ stdio: ["ignore", "pipe", "ignore"],
2579
+ maxBuffer: o.maxBuffer,
2580
+ windowsHide: true
2581
+ }
2582
+ );
2583
+ return rows(JSON.parse(out2 || "null"));
2584
+ } catch {
2585
+ return null;
2586
+ }
2587
+ }
2588
+ var SAFE = /^[A-Za-z0-9 ._:@/+-]{0,256}$/;
2589
+ function inlineParams(sql, params) {
2590
+ if (sql.includes("'")) return null;
2591
+ const holes = sql.split("?").length - 1;
2592
+ if (holes !== params.length) return null;
2593
+ let i = 0;
2594
+ let bad2 = false;
2595
+ const out2 = sql.replace(/\?/g, () => {
2596
+ const p = params[i++];
2597
+ if (typeof p === "number") {
2598
+ if (!Number.isFinite(p)) {
2599
+ bad2 = true;
2600
+ return "NULL";
2601
+ }
2602
+ return String(p);
2603
+ }
2604
+ if (typeof p !== "string" || !SAFE.test(p)) {
2605
+ bad2 = true;
2606
+ return "NULL";
2607
+ }
2608
+ return `'${p}'`;
2609
+ });
2610
+ return bad2 ? null : out2;
2611
+ }
2612
+ function viaCli(file, sql, params, o) {
2613
+ const text = inlineParams(sql, params);
2614
+ if (!text) return null;
2615
+ try {
2616
+ const out2 = execFileSync4("sqlite3", ["-readonly", "-json", file, text], {
2617
+ encoding: "utf8",
2618
+ timeout: o.timeoutMs + 3e3,
2619
+ stdio: ["ignore", "pipe", "ignore"],
2620
+ maxBuffer: o.maxBuffer,
2621
+ windowsHide: true
2622
+ }).trim();
2623
+ if (!out2) return [];
2624
+ return rows(JSON.parse(out2));
2625
+ } catch {
2626
+ return null;
2627
+ }
2628
+ }
2629
+ function querySqlite(file, sql, params = [], opts = {}) {
2630
+ const o = {
2631
+ timeoutMs: opts.timeoutMs ?? 2e3,
2632
+ engine: opts.engine ?? "auto",
2633
+ maxBuffer: opts.maxBuffer ?? 32 * 1024 * 1024
2634
+ };
2635
+ if (!file) return null;
2636
+ try {
2637
+ if (!existsSync5(file)) return null;
2638
+ } catch {
2639
+ return null;
2640
+ }
2641
+ const ways = o.engine === "auto" ? [viaNode, viaChild, viaCli] : o.engine === "node" ? [viaNode] : o.engine === "child" ? [viaChild] : [viaCli];
2642
+ for (const way of ways) {
2643
+ const r = way(file, sql, params, o);
2644
+ if (r) return r;
2645
+ }
2646
+ return null;
2647
+ }
2648
+
2649
+ // ../core/src/adapters/devin.ts
2650
+ var NAME4 = "devin";
2651
+ var DEVIN_EVENTS = [
2652
+ "SessionStart",
2653
+ "UserPromptSubmit",
2654
+ "PreToolUse",
2655
+ "PostToolUse",
2656
+ "PermissionRequest",
2657
+ "Stop",
2658
+ "PostCompaction",
2659
+ "SessionEnd"
2660
+ ];
2661
+ var OPTS3 = { matcherFor: (e) => /Tool|Permission/.test(e) ? "" : void 0, timeout: 5 };
2662
+ var eventsFor3 = (lean) => lean ? DEVIN_EVENTS.filter((e) => e !== "PreToolUse") : [...DEVIN_EVENTS];
2663
+ var INSTALL_NOTE = "Devin drops a whole hooks file on one event name it doesn't know; `/hooks` in `devin` lists what loaded";
2664
+ 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";
2665
+ var deadFileNote = (bad2) => `Devin loads NO hooks from this file: "${bad2}" is not one of its event names and it rejects the file whole \u2014 remove that key (\`/hooks\` in \`devin\` lists what loaded)`;
2666
+ function foreignEvent(file) {
2667
+ const hooks = readJson(file).hooks;
2668
+ if (!hooks || typeof hooks !== "object" || Array.isArray(hooks)) return void 0;
2669
+ const ok = DEVIN_EVENTS;
2670
+ return Object.keys(hooks).find((k) => !ok.includes(k));
2671
+ }
2672
+ var homeOf = (env) => env.HOME || homeDir();
2673
+ var appData = (env) => env.APPDATA || path11.join(homeOf(env), "AppData", "Roaming");
2674
+ var devinConfigDir = (env = process.env) => process.platform === "win32" ? path11.join(appData(env), "devin") : path11.join(env.XDG_CONFIG_HOME || path11.join(homeOf(env), ".config"), "devin");
2675
+ var configFile3 = (env) => path11.join(devinConfigDir(env), "config.json");
2676
+ var devinDataDir = (env = process.env) => process.platform === "win32" ? path11.join(appData(env), "devin", "cli") : path11.join(env.XDG_DATA_HOME || path11.join(homeOf(env), ".local", "share"), "devin", "cli");
2677
+ var sessionsDb = (env) => path11.join(devinDataDir(env), "sessions.db");
2678
+ var SESSION_ID = /^[A-Za-z0-9._:-]{1,128}$/;
2679
+ var validTool4 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
2680
+ var endReason3 = (r) => r === "clear" || r === "logout" || r === "exit" ? r : "other";
2681
+ var pickSource2 = (v) => typeof v === "string" && ["startup", "resume", "clear", "compact", "fork"].includes(v) ? v : "unknown";
2682
+ function errText2(v) {
2683
+ if (typeof v === "string" && v) return v.slice(0, 2e3);
2684
+ if (v && typeof v === "object") {
2685
+ const e = v;
2686
+ if (typeof e.message === "string" && e.message) return e.message.slice(0, 2e3);
2687
+ if (typeof e.error === "string" && e.error) return e.error.slice(0, 2e3);
2688
+ }
2689
+ return void 0;
2690
+ }
2691
+ var cut = (s, n) => s.length > n ? `${s.slice(0, n).replace(/\s+\S*$/, "")}\u2026` : s;
2692
+ var devin = {
2693
+ name: NAME4,
2694
+ events: DEVIN_EVENTS,
2695
+ detect: (env = process.env) => existsSync6(devinConfigDir(env)) || existsSync6(devinDataDir(env)),
2696
+ configFiles: (env = process.env) => [configFile3(env)],
2697
+ install: (cmd, env = process.env, opts = {}) => {
2698
+ const file = configFile3(env);
2699
+ const r = installClaudeShaped(file, NAME4, eventsFor3(opts.lean), cmd, OPTS3);
2700
+ if (r.skipped) return [r];
2701
+ const bad2 = foreignEvent(file);
2702
+ return [{ ...r, note: bad2 ? deadFileNote(bad2) : INSTALL_NOTE }];
2703
+ },
2704
+ uninstall: (env = process.env, opts = {}) => [
2705
+ uninstallClaudeShaped(configFile3(env), DEVIN_EVENTS, { created: opts.created?.includes(configFile3(env)) })
2706
+ ],
2707
+ installed: (cmd, env = process.env, opts = {}) => {
2708
+ const file = configFile3(env);
2709
+ const state2 = installedClaudeShaped(file, NAME4, eventsFor3(opts.lean), cmd, OPTS3);
2710
+ const bad2 = state2 === "missing" ? void 0 : foreignEvent(file);
2711
+ return [{ file, state: bad2 ? "inactive" : state2, note: bad2 ? deadFileNote(bad2) : CLAUDE_NOTE }];
2712
+ },
2713
+ fromHook(input) {
2714
+ const event = input.argv[1] ?? "";
2715
+ let s = {};
2716
+ try {
2717
+ const j = input.stdin ? JSON.parse(input.stdin) : {};
2718
+ if (j && typeof j === "object" && !Array.isArray(j)) s = j;
2719
+ } catch {
2720
+ }
2721
+ const id = typeof s.session_id === "string" ? s.session_id : "";
2722
+ if (!id) return null;
2723
+ const session = { id, cwd: input.env.DEVIN_PROJECT_DIR ?? input.cwd };
2724
+ const turn_id = typeof s.prompt_id === "string" ? s.prompt_id : void 0;
2725
+ const tool = validTool4(s.tool_name) ? s.tool_name : void 0;
2726
+ const call_id = typeof s.tool_use_id === "string" ? s.tool_use_id : void 0;
2727
+ const response = s.tool_response && typeof s.tool_response === "object" && !Array.isArray(s.tool_response) ? s.tool_response : void 0;
2728
+ const permissionId = () => `perm-${createHash2("sha256").update(`${turn_id ?? ""}|${tool ?? ""}|${JSON.stringify(s.tool_input ?? null)}`).digest("hex").slice(0, 16)}`;
2729
+ const ev = (type, data) => ({ type, data: prune4(data), harnessEvent: event });
2730
+ const events = [];
2731
+ switch (event) {
2732
+ case "SessionStart":
2733
+ events.push(ev("session.started", { source: pickSource2(s.source) }));
2734
+ break;
2735
+ case "UserPromptSubmit":
2736
+ events.push(
2737
+ ev("turn.started", {
2738
+ turn_id,
2739
+ prompt_chars: typeof s.prompt === "string" ? [...s.prompt].length : void 0
2740
+ })
2741
+ );
2742
+ break;
2743
+ case "PreToolUse":
2744
+ if (tool) events.push(ev("tool.started", { tool, call_id, turn_id, input: s.tool_input }));
2745
+ break;
2746
+ case "PostToolUse":
2747
+ if (tool)
2748
+ events.push(
2749
+ ev("tool.ended", {
2750
+ tool,
2751
+ call_id,
2752
+ turn_id,
2753
+ // `ok` is required on tool.ended (PROTOCOL.md §events), so a response
2754
+ // that carries no success flag at all reads as "nothing said it failed".
2755
+ ok: response?.success !== false,
2756
+ error: errText2(response?.error),
2757
+ input: s.tool_input,
2758
+ output: response ? response.output : s.tool_response
2759
+ })
2760
+ );
2761
+ break;
2762
+ case "PermissionRequest":
2763
+ events.push(ev("attention.needed", { attention_id: call_id ?? permissionId(), kind: "permission", tool }));
2764
+ break;
2765
+ case "Stop":
2766
+ events.push(ev("turn.ended", { turn_id, reason: "stop" }));
2767
+ break;
2768
+ case "PostCompaction":
2769
+ events.push(ev("context.compacted", { trigger: "auto" }));
2770
+ break;
2771
+ case "SessionEnd":
2772
+ events.push(ev("session.ended", { reason: endReason3(s.reason) }));
2773
+ break;
2774
+ default:
2775
+ return null;
2776
+ }
2777
+ return {
2778
+ session,
2779
+ events,
2780
+ hints: {},
2781
+ transcript: transcriptRef(sessionsDb(input.env), id)
2782
+ };
2783
+ },
2784
+ readTranscript(file, fromLine) {
2785
+ const ref = splitRef(file);
2786
+ if (!ref) return { turns: [], line: fromLine };
2787
+ return readDevinChain(ref.db, ref.session, fromLine);
2788
+ },
2789
+ facts(session, transcript, _hints, _state, env = process.env) {
2790
+ const out2 = {};
2791
+ const version = devinVersion(env);
2792
+ if (version) out2.harness_version = version;
2793
+ const account = devinAccount(env);
2794
+ if (account) out2.account_id = account;
2795
+ const ref = splitRef(transcript) ?? { db: sessionsDb(env), session: session.id };
2796
+ if (!SESSION_ID.test(ref.session)) return out2;
2797
+ const rows2 = querySqlite(ref.db, SESSION_SQL, [ref.session]);
2798
+ const row = rows2?.[0];
2799
+ if (!row) return out2;
2800
+ Object.assign(out2, sessionFacts(row));
2801
+ return out2;
2802
+ },
2803
+ backfill(sinceMs, env = process.env) {
2804
+ const rows2 = querySqlite(sessionsDb(env), BACKFILL_SQL, ["text", Math.floor(sinceMs / 1e3)]);
2805
+ if (!rows2) return [];
2806
+ const out2 = [];
2807
+ for (const row of rows2) {
2808
+ const id = String(row.id);
2809
+ if (!SESSION_ID.test(id)) continue;
2810
+ const { started_at, last_at, ...rest } = sessionFacts(row);
2811
+ if (!started_at || !last_at) continue;
2812
+ out2.push({ session: { id, ...rest }, started_at, last_at });
2813
+ }
2814
+ return out2;
2815
+ }
2816
+ };
2817
+ function prune4(o) {
2818
+ for (const k of Object.keys(o)) if (o[k] === void 0) delete o[k];
2819
+ return o;
2820
+ }
2821
+ var transcriptRef = (db, session) => `${db}#${session}`;
2822
+ function splitRef(ref) {
2823
+ if (!ref) return null;
2824
+ const at = ref.lastIndexOf("#");
2825
+ if (at <= 0) return null;
2826
+ const db = ref.slice(0, at);
2827
+ const session = ref.slice(at + 1);
2828
+ return db && SESSION_ID.test(session) ? { db, session } : null;
2829
+ }
2830
+ var CHAIN_LIMIT = 4e3;
2831
+ var CHAIN_SQL = `WITH RECURSIVE chain(node_id, parent_node_id, chat_message, created_at) AS (
2832
+ SELECT node_id, parent_node_id, chat_message, created_at FROM message_nodes
2833
+ WHERE session_id = ? AND node_id = (SELECT main_chain_id FROM sessions WHERE id = ?)
2834
+ UNION ALL
2835
+ SELECT m.node_id, m.parent_node_id, m.chat_message, m.created_at FROM message_nodes m
2836
+ JOIN chain c ON m.node_id = c.parent_node_id AND m.session_id = ? AND m.node_id >= ?
2837
+ )
2838
+ SELECT node_id, chat_message, created_at FROM chain ORDER BY node_id DESC LIMIT ${CHAIN_LIMIT}`;
2839
+ var SESSION_FIELDS = `s.model, s.working_directory, s.created_at, s.last_activity_at,
2840
+ (SELECT p.content FROM prompt_history p WHERE p.session_id = s.id AND p.is_shell = 0
2841
+ ORDER BY p.timestamp ASC, p.id ASC LIMIT 1) AS first_ask`;
2842
+ var SESSION_SQL = `SELECT ${SESSION_FIELDS} FROM sessions s WHERE s.id = ? LIMIT 1`;
2843
+ var BACKFILL_SQL = `SELECT s.id, ${SESSION_FIELDS} FROM sessions s
2844
+ WHERE s.hidden = 0 AND typeof(s.id) = ? AND s.last_activity_at >= ?
2845
+ ORDER BY s.last_activity_at DESC LIMIT 500`;
2846
+ function sessionFacts(row) {
2847
+ const out2 = {};
2848
+ if (typeof row.model === "string" && row.model) out2.model = row.model;
2849
+ if (typeof row.working_directory === "string" && path11.isAbsolute(row.working_directory))
2850
+ out2.cwd = row.working_directory;
2851
+ const started = Number(row.created_at);
2852
+ const last = Number(row.last_activity_at);
2853
+ if (Number.isFinite(started) && started > 0) out2.started_at = Math.round(started * 1e3);
2854
+ if (Number.isFinite(last) && last > 0) out2.last_at = Math.round(last * 1e3);
2855
+ if (typeof row.first_ask === "string") {
2856
+ const line = row.first_ask.split("\n").find((l) => l.trim()) ?? "";
2857
+ if (line.trim()) {
2858
+ out2.title = cut(line.trim(), 80);
2859
+ out2.title_source = "first-ask";
2860
+ }
2861
+ }
2862
+ return out2;
2863
+ }
2864
+ function readDevinChain(db, session, fromLine) {
2865
+ if (!SESSION_ID.test(session)) return { turns: [], line: fromLine };
2866
+ const floor = Number.isFinite(fromLine) && fromLine > 0 ? Math.floor(fromLine) : 0;
2867
+ const rows2 = querySqlite(db, CHAIN_SQL, [session, session, session, floor]);
2868
+ if (!rows2?.length) return { turns: [], line: fromLine };
2869
+ const msgs = [];
2870
+ let end = fromLine;
2871
+ for (const row of rows2) {
2872
+ const node = Number(row.node_id);
2873
+ if (!Number.isFinite(node)) continue;
2874
+ if (node + 1 > end) end = node + 1;
2875
+ if (node < fromLine) continue;
2876
+ let j;
2877
+ try {
2878
+ j = JSON.parse(String(row.chat_message ?? ""));
2879
+ } catch {
2880
+ continue;
2881
+ }
2882
+ const text = typeof j.content === "string" ? j.content.trim() : "";
2883
+ if (!text) continue;
2884
+ const secs = Number(row.created_at);
2885
+ const at = Date.parse(String(j.metadata?.created_at ?? "")) || (Number.isFinite(secs) && secs > 0 ? Math.round(secs * 1e3) : 0) || Date.now();
2886
+ if (j.role === "user" && j.metadata?.is_user_input === true) msgs.push({ role: "user", text, at, line: node });
2887
+ else if (j.role === "assistant") msgs.push({ role: "assistant", text, at, line: node });
2888
+ }
2889
+ msgs.sort((a, b) => a.line - b.line);
2890
+ return pairTurns(msgs, end);
2891
+ }
2892
+ function devinVersion(env = process.env) {
2893
+ try {
2894
+ const target = readlinkSync(path11.join(devinDataDir(env), "_versions", "current"));
2895
+ const name = path11.basename(target);
2896
+ return /^[0-9][0-9A-Za-z.+-]{0,63}$/.test(name) ? name : void 0;
2897
+ } catch {
2898
+ return void 0;
2899
+ }
2900
+ }
2901
+ function devinAccount(env = process.env) {
2902
+ const d = readJson(configFile3(env)).devin;
2903
+ const id = d && typeof d === "object" ? d.org_id : void 0;
2904
+ return typeof id === "string" && /^[A-Za-z0-9_.:-]{1,128}$/.test(id) ? id : void 0;
2905
+ }
2906
+
2907
+ // ../core/src/adapters/gemini-cli.ts
2908
+ import { execFileSync as execFileSync5 } from "node:child_process";
2909
+ import { existsSync as existsSync7, readFileSync as readFileSync6 } from "node:fs";
2910
+ import path12 from "node:path";
2911
+ var NAME5 = "gemini-cli";
2417
2912
  var GEMINI_EVENTS = [
2418
2913
  "SessionStart",
2419
2914
  "BeforeAgent",
@@ -2424,11 +2919,11 @@ var GEMINI_EVENTS = [
2424
2919
  "PreCompress",
2425
2920
  "SessionEnd"
2426
2921
  ];
2427
- var OPTS3 = { matcherFor: (e) => /Tool/.test(e) ? ".*" : void 0, timeout: 5e3 };
2428
- var geminiDir = (env) => env.GEMINI_CLI_HOME || path11.join(HOME, ".gemini");
2429
- var settings = (env) => path11.join(geminiDir(env), "settings.json");
2430
- var validTool4 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
2431
- var eventsFor3 = (lean) => lean ? GEMINI_EVENTS.filter((e) => e !== "BeforeTool") : [...GEMINI_EVENTS];
2922
+ var OPTS4 = { matcherFor: (e) => /Tool/.test(e) ? ".*" : void 0, timeout: 5e3 };
2923
+ var geminiDir = (env) => env.GEMINI_CLI_HOME || path12.join(homeDir(), ".gemini");
2924
+ var settings = (env) => path12.join(geminiDir(env), "settings.json");
2925
+ var validTool5 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
2926
+ var eventsFor4 = (lean) => lean ? GEMINI_EVENTS.filter((e) => e !== "BeforeTool") : [...GEMINI_EVENTS];
2432
2927
  function geminiHooksActive(settingsFile) {
2433
2928
  let tools;
2434
2929
  try {
@@ -2440,7 +2935,7 @@ function geminiHooksActive(settingsFile) {
2440
2935
  return { active: false, note: "inactive: tools.enableHooks is false in settings.json" };
2441
2936
  let version = "";
2442
2937
  try {
2443
- version = execFileSync4("gemini", ["--version"], {
2938
+ version = execFileSync5("gemini", ["--version"], {
2444
2939
  encoding: "utf8",
2445
2940
  timeout: 4e3,
2446
2941
  stdio: ["ignore", "pipe", "ignore"]
@@ -2459,21 +2954,21 @@ function geminiHooksActive(settingsFile) {
2459
2954
  };
2460
2955
  }
2461
2956
  var geminiCli = {
2462
- name: NAME4,
2957
+ name: NAME5,
2463
2958
  events: GEMINI_EVENTS,
2464
2959
  // ~/.gemini also belongs to Antigravity; Gemini CLI is here when its settings or its chat logs exist.
2465
- detect: (env = process.env) => existsSync5(settings(env)) || existsSync5(path11.join(geminiDir(env), "tmp")),
2960
+ detect: (env = process.env) => existsSync7(settings(env)) || existsSync7(path12.join(geminiDir(env), "tmp")),
2466
2961
  configFiles: (env = process.env) => [settings(env)],
2467
- install: (cmd2, env = process.env, opts = {}) => {
2468
- const r = installClaudeShaped(settings(env), NAME4, eventsFor3(opts.lean), cmd2, OPTS3);
2962
+ install: (cmd, env = process.env, opts = {}) => {
2963
+ const r = installClaudeShaped(settings(env), NAME5, eventsFor4(opts.lean), cmd, OPTS4);
2469
2964
  const a = geminiHooksActive(settings(env));
2470
2965
  return [a.note && !r.skipped ? { ...r, note: [r.note, a.note].filter(Boolean).join("; ") } : r];
2471
2966
  },
2472
2967
  uninstall: (env = process.env, opts = {}) => [
2473
2968
  uninstallClaudeShaped(settings(env), GEMINI_EVENTS, { created: opts.created?.includes(settings(env)) })
2474
2969
  ],
2475
- installed: (cmd2, env = process.env, opts = {}) => {
2476
- const state2 = installedClaudeShaped(settings(env), NAME4, eventsFor3(opts.lean), cmd2, OPTS3);
2970
+ installed: (cmd, env = process.env, opts = {}) => {
2971
+ const state2 = installedClaudeShaped(settings(env), NAME5, eventsFor4(opts.lean), cmd, OPTS4);
2477
2972
  const a = geminiHooksActive(settings(env));
2478
2973
  return [
2479
2974
  {
@@ -2487,14 +2982,15 @@ var geminiCli = {
2487
2982
  const event = input.argv[1] ?? "";
2488
2983
  let s = {};
2489
2984
  try {
2490
- s = input.stdin ? JSON.parse(input.stdin) : {};
2985
+ const j = input.stdin ? JSON.parse(input.stdin) : {};
2986
+ if (j && typeof j === "object" && !Array.isArray(j)) s = j;
2491
2987
  } catch {
2492
2988
  }
2493
2989
  const id = s.session_id;
2494
2990
  if (!id) return null;
2495
2991
  const session = { id: String(id), cwd: typeof s.cwd === "string" ? s.cwd : input.cwd };
2496
- const tool = validTool4(s.tool_name) ? s.tool_name : void 0;
2497
- const ev = (type, data) => ({ type, data: prune4(data), harnessEvent: event });
2992
+ const tool = validTool5(s.tool_name) ? s.tool_name : void 0;
2993
+ const ev = (type, data) => ({ type, data: prune5(data), harnessEvent: event });
2498
2994
  const events = [];
2499
2995
  switch (event) {
2500
2996
  case "SessionStart":
@@ -2575,7 +3071,7 @@ var geminiCli = {
2575
3071
  const raw = first ? first.content ?? first.message ?? first.text : void 0;
2576
3072
  const text = typeof raw === "string" ? raw.trim() : "";
2577
3073
  if (text) {
2578
- const line = text.split("\n").find((l) => l.trim()) ?? "";
3074
+ const line = text.split("\n")[0];
2579
3075
  out2.title = line.length > 80 ? `${line.slice(0, 80).replace(/\s+\S*$/, "")}\u2026` : line;
2580
3076
  out2.title_source = "first-ask";
2581
3077
  }
@@ -2589,13 +3085,13 @@ var geminiCli = {
2589
3085
  backfill: () => []
2590
3086
  // chat logs are keyed by a project hash, not a session id we can name; the first hook of a resumed session reports it
2591
3087
  };
2592
- function prune4(o) {
3088
+ function prune5(o) {
2593
3089
  for (const k of Object.keys(o)) if (o[k] === void 0) delete o[k];
2594
3090
  return o;
2595
3091
  }
2596
3092
 
2597
3093
  // ../core/src/adapters/index.ts
2598
- var ADAPTERS = [claudeCode, codex, geminiCli, antigravity];
3094
+ var ADAPTERS = [claudeCode, codex, geminiCli, antigravity, devin];
2599
3095
  var adapterByName = (name) => ADAPTERS.find((a) => a.name === name);
2600
3096
  function drivableHarnesses(env = process.env) {
2601
3097
  return [
@@ -2606,8 +3102,8 @@ function drivableHarnesses(env = process.env) {
2606
3102
  }
2607
3103
 
2608
3104
  // ../core/src/config.ts
2609
- import { chmodSync, existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
2610
- import path12 from "node:path";
3105
+ import { chmodSync, existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
3106
+ import path13 from "node:path";
2611
3107
  var DEFAULT_CONFIG = { sinks: [], harnesses: {}, update_check: true, keep_days: 30 };
2612
3108
  function readConfig(file = configFile()) {
2613
3109
  try {
@@ -2623,10 +3119,10 @@ function readConfig(file = configFile()) {
2623
3119
  }
2624
3120
  }
2625
3121
  function writeConfig(c, file = configFile()) {
2626
- mkdirSync4(path12.dirname(file), { recursive: true, mode: 448 });
3122
+ mkdirSync4(path13.dirname(file), { recursive: true, mode: 448 });
2627
3123
  writeFileSync4(file, `${JSON.stringify(c, null, 2)}
2628
3124
  `, { mode: 384 });
2629
- if (process.platform !== "win32" && existsSync6(file)) {
3125
+ if (process.platform !== "win32" && existsSync8(file)) {
2630
3126
  try {
2631
3127
  chmodSync(file, 384);
2632
3128
  } catch {
@@ -2655,7 +3151,7 @@ function encodeTime(t) {
2655
3151
  function ulid(now = Date.now()) {
2656
3152
  if (now === lastTime) {
2657
3153
  for (let i = lastRand.length - 1; i >= 0; i--) {
2658
- const v = (lastRand[i] ?? 0) + 1;
3154
+ const v = lastRand[i] + 1;
2659
3155
  if (v < 32) {
2660
3156
  lastRand[i] = v;
2661
3157
  break;
@@ -2664,8 +3160,7 @@ function ulid(now = Date.now()) {
2664
3160
  }
2665
3161
  } else {
2666
3162
  lastTime = now;
2667
- const b = randomBytes(16);
2668
- lastRand = Array.from({ length: 16 }, (_, i) => (b[i] ?? 0) % 32);
3163
+ lastRand = Array.from(randomBytes(16), (b) => b % 32);
2669
3164
  }
2670
3165
  return encodeTime(now) + lastRand.map((v) => ALPHABET[v]).join("");
2671
3166
  }
@@ -2692,7 +3187,7 @@ function makeEvent(a, ctx) {
2692
3187
  import {
2693
3188
  appendFileSync,
2694
3189
  closeSync as closeSync3,
2695
- existsSync as existsSync7,
3190
+ existsSync as existsSync9,
2696
3191
  mkdirSync as mkdirSync5,
2697
3192
  openSync as openSync3,
2698
3193
  readdirSync as readdirSync6,
@@ -2703,42 +3198,45 @@ import {
2703
3198
  unlinkSync as unlinkSync3,
2704
3199
  writeFileSync as writeFileSync5
2705
3200
  } from "node:fs";
2706
- import path13 from "node:path";
3201
+ import path14 from "node:path";
3202
+ var READ_CHUNK = 1024 * 1024;
3203
+ var READ_MAX = 8 * 1024 * 1024;
2707
3204
  var Outbox = class {
2708
3205
  constructor(root) {
2709
3206
  this.root = root;
2710
3207
  }
2711
3208
  root;
2712
3209
  dir(harness) {
2713
- return path13.join(this.root, "outbox", safeId(harness));
3210
+ return path14.join(this.root, "outbox", safeId(harness));
2714
3211
  }
2715
3212
  file(ref) {
2716
- return path13.join(this.dir(ref.harness), `${safeId(ref.session)}.jsonl`);
3213
+ return path14.join(this.dir(ref.harness), `${safeId(ref.session)}.jsonl`);
2717
3214
  }
2718
3215
  seqFile(ref) {
2719
- return path13.join(this.dir(ref.harness), `${safeId(ref.session)}.seq`);
3216
+ return path14.join(this.dir(ref.harness), `${safeId(ref.session)}.seq`);
2720
3217
  }
2721
3218
  /** The next seq for a session, persisted (survives a hibernating machine and a
2722
3219
  * restart; an in-memory counter would restart at 0 and collide). */
2723
3220
  nextSeq(ref) {
2724
3221
  const f = this.seqFile(ref);
2725
- let n = 0;
3222
+ let n = -1;
2726
3223
  try {
2727
- n = Number(readFileSync8(f, "utf8")) || 0;
3224
+ const t = readFileSync8(f, "utf8").trim();
3225
+ if (/^\d+$/.test(t)) n = Number(t);
2728
3226
  } catch {
2729
- n = this.lastSeqInFile(ref) + 1;
2730
3227
  }
2731
- mkdirSync5(path13.dirname(f), { recursive: true, mode: 448 });
3228
+ if (n < 0) n = this.lastSeqInFile(ref) + 1;
3229
+ mkdirSync5(path14.dirname(f), { recursive: true, mode: 448 });
2732
3230
  writeFileSync5(f, String(n + 1), { mode: 384 });
2733
3231
  return n;
2734
3232
  }
2735
3233
  lastSeqInFile(ref) {
2736
3234
  const f = this.file(ref);
2737
- if (!existsSync7(f)) return -1;
3235
+ if (!existsSync9(f)) return -1;
2738
3236
  const lines = readFileSync8(f, "utf8").trim().split("\n");
2739
3237
  for (let i = lines.length - 1; i >= 0; i--) {
2740
3238
  try {
2741
- const j = JSON.parse(lines[i] ?? "");
3239
+ const j = JSON.parse(lines[i]);
2742
3240
  if (typeof j.session?.seq === "number") return j.session.seq;
2743
3241
  } catch {
2744
3242
  }
@@ -2754,10 +3252,10 @@ var Outbox = class {
2754
3252
  /** Every session file, newest first. */
2755
3253
  sessions() {
2756
3254
  const out2 = [];
2757
- const base = path13.join(this.root, "outbox");
2758
- if (!existsSync7(base)) return [];
3255
+ const base = path14.join(this.root, "outbox");
3256
+ if (!existsSync9(base)) return [];
2759
3257
  for (const h of readdirSync6(base)) {
2760
- const d = path13.join(base, h);
3258
+ const d = path14.join(base, h);
2761
3259
  let names = [];
2762
3260
  try {
2763
3261
  names = readdirSync6(d).filter((n) => n.endsWith(".jsonl"));
@@ -2766,7 +3264,7 @@ var Outbox = class {
2766
3264
  }
2767
3265
  for (const n of names) {
2768
3266
  try {
2769
- out2.push({ ref: { harness: h, session: n.slice(0, -6) }, mtime: statSync6(path13.join(d, n)).mtimeMs });
3267
+ out2.push({ ref: { harness: h, session: n.slice(0, -6) }, mtime: statSync6(path14.join(d, n)).mtimeMs });
2770
3268
  } catch {
2771
3269
  }
2772
3270
  }
@@ -2774,31 +3272,43 @@ var Outbox = class {
2774
3272
  return out2.sort((a, b) => b.mtime - a.mtime).map((x) => x.ref);
2775
3273
  }
2776
3274
  /** Read events after byte offset `from`; returns them and the new offset. Lines
2777
- * are read whole; a partial trailing line (a write in progress) is left. */
3275
+ * are read whole; a partial trailing line (a write in progress) is left. A read
3276
+ * takes READ_CHUNK bytes at a time, not the rest of the file: a session file grows
3277
+ * to tens of MB and a drain takes it in batches of 50. It reads on until the batch
3278
+ * is full or the file ends, so lines that parse to nothing never stop a drain; one
3279
+ * line longer than the chunk gets a larger one, up to READ_MAX. */
2778
3280
  read(ref, from, maxEvents = 500) {
2779
3281
  const f = this.file(ref);
2780
- if (!existsSync7(f)) return { events: [], offset: from };
3282
+ if (!existsSync9(f)) return { events: [], offset: from };
2781
3283
  const size = statSync6(f).size;
2782
3284
  if (from >= size) return { events: [], offset: from };
2783
3285
  const fd = openSync3(f, "r");
2784
3286
  try {
2785
- const buf = Buffer.alloc(Math.min(size - from, 8 * 1024 * 1024));
2786
- const n = readSync3(fd, buf, 0, buf.length, from);
2787
- const text = buf.subarray(0, n).toString("utf8");
2788
3287
  const events = [];
2789
3288
  let offset = from;
2790
- let pos = 0;
2791
- while (events.length < maxEvents) {
2792
- const nl = text.indexOf("\n", pos);
2793
- if (nl < 0) break;
2794
- const line = text.slice(pos, nl);
2795
- pos = nl + 1;
2796
- offset = from + Buffer.byteLength(text.slice(0, pos), "utf8");
2797
- if (!line.trim()) continue;
2798
- try {
2799
- events.push(JSON.parse(line));
2800
- } catch {
3289
+ let chunk = READ_CHUNK;
3290
+ while (events.length < maxEvents && offset < size) {
3291
+ const buf = Buffer.alloc(Math.min(size - offset, chunk));
3292
+ const n = readSync3(fd, buf, 0, buf.length, offset);
3293
+ const text = buf.subarray(0, n).toString("utf8");
3294
+ let pos = 0;
3295
+ let used = 0;
3296
+ while (events.length < maxEvents) {
3297
+ const nl = text.indexOf("\n", pos);
3298
+ if (nl < 0) break;
3299
+ const line = text.slice(pos, nl);
3300
+ pos = nl + 1;
3301
+ used += Buffer.byteLength(line, "utf8") + 1;
3302
+ if (!line.trim()) continue;
3303
+ try {
3304
+ events.push(JSON.parse(line));
3305
+ } catch {
3306
+ }
2801
3307
  }
3308
+ offset += used;
3309
+ if (used) continue;
3310
+ if (offset + n >= size || chunk >= READ_MAX) break;
3311
+ chunk *= 2;
2802
3312
  }
2803
3313
  return { events, offset };
2804
3314
  } finally {
@@ -2842,7 +3352,7 @@ var Cursors = class {
2842
3352
  root;
2843
3353
  sink;
2844
3354
  file() {
2845
- return path13.join(this.root, "cursors", `${safeId(this.sink)}.json`);
3355
+ return path14.join(this.root, "cursors", `${safeId(this.sink)}.json`);
2846
3356
  }
2847
3357
  read() {
2848
3358
  try {
@@ -2853,7 +3363,7 @@ var Cursors = class {
2853
3363
  }
2854
3364
  write(c) {
2855
3365
  const f = this.file();
2856
- mkdirSync5(path13.dirname(f), { recursive: true, mode: 448 });
3366
+ mkdirSync5(path14.dirname(f), { recursive: true, mode: 448 });
2857
3367
  const tmp = `${f}.${process.pid}.tmp`;
2858
3368
  writeFileSync5(tmp, JSON.stringify(c), { mode: 384 });
2859
3369
  renameSync(tmp, f);
@@ -2863,11 +3373,14 @@ var Cursors = class {
2863
3373
  }
2864
3374
  };
2865
3375
  function takeLock(root, ref, staleMs = 5 * 6e4) {
2866
- const dir = path13.join(root, "locks");
3376
+ return takeNamedLock(root, `${safeId(ref.harness)}-${safeId(ref.session)}`, staleMs);
3377
+ }
3378
+ function takeNamedLock(root, name, staleMs = 5 * 6e4) {
3379
+ const dir = path14.join(root, "locks");
2867
3380
  mkdirSync5(dir, { recursive: true, mode: 448 });
2868
- const f = path13.join(dir, `${safeId(ref.harness)}-${safeId(ref.session)}.lock`);
3381
+ const f = path14.join(dir, `${name}.lock`);
2869
3382
  try {
2870
- if (Date.now() - statSync6(f).mtimeMs > staleMs) unlinkSync3(f);
3383
+ if (Date.now() - statSync6(f).mtimeMs > staleMs || !processAlive(Number(readFileSync8(f, "utf8")))) unlinkSync3(f);
2871
3384
  else return null;
2872
3385
  } catch {
2873
3386
  }
@@ -2883,9 +3396,18 @@ function takeLock(root, ref, staleMs = 5 * 6e4) {
2883
3396
  }
2884
3397
  };
2885
3398
  }
3399
+ function processAlive(pid) {
3400
+ if (!Number.isInteger(pid) || pid <= 0) return true;
3401
+ try {
3402
+ process.kill(pid, 0);
3403
+ return true;
3404
+ } catch (e) {
3405
+ return e.code === "EPERM";
3406
+ }
3407
+ }
2886
3408
 
2887
3409
  // ../core/src/privacy/pii.ts
2888
- import { createHash as createHash2 } from "node:crypto";
3410
+ import { createHash as createHash3 } from "node:crypto";
2889
3411
  var RULESET_PII = "pii@1";
2890
3412
  var EMAIL = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g;
2891
3413
  var PHONE = /(?<![\w.])(?:\+\d{1,3}[ .-]?)?(?:\(\d{2,4}\)[ .-]?)?\d{2,4}(?:[ .-]?\d{2,4}){1,3}(?!\w)/g;
@@ -2907,7 +3429,7 @@ function reducePii(text, home) {
2907
3429
  return out2;
2908
3430
  }
2909
3431
  function hashMachine(name) {
2910
- return createHash2("sha256").update(name).digest("hex").slice(0, 8);
3432
+ return createHash3("sha256").update(name).digest("hex").slice(0, 8);
2911
3433
  }
2912
3434
  function reducePiiDeep(v, home) {
2913
3435
  if (typeof v === "string") return reducePii(v, home);
@@ -2996,12 +3518,12 @@ function redactAssignments(text) {
2996
3518
  let last = 0;
2997
3519
  NAME_SEP.lastIndex = 0;
2998
3520
  for (let m = NAME_SEP.exec(text); m; m = NAME_SEP.exec(text)) {
2999
- const name = m[1] ?? "";
3521
+ const name = m[1];
3000
3522
  if (!isSecretName(name)) continue;
3001
3523
  VALUE.lastIndex = NAME_SEP.lastIndex;
3002
3524
  const v = VALUE.exec(text);
3003
3525
  if (!v) continue;
3004
- const value = v[1] ?? v[2] ?? v[3] ?? v[4] ?? "";
3526
+ const value = v[1] ?? v[2] ?? v[3] ?? v[4];
3005
3527
  NAME_SEP.lastIndex = VALUE.lastIndex;
3006
3528
  if (isPlaceholder(value) || value.includes(REDACTED)) continue;
3007
3529
  if (/tokens?$/i.test(name) && /^\d+(?:[.,]\d+)*[km]?$/i.test(value)) continue;
@@ -3057,7 +3579,7 @@ function redactSecrets(text) {
3057
3579
  let out2 = text;
3058
3580
  for (const re of TOKEN_PATTERNS) out2 = out2.replace(re, REDACTED);
3059
3581
  out2 = out2.replace(URL_CREDENTIALS, (_m, head, _pw, at) => `${head}${REDACTED}${at}`);
3060
- out2 = out2.replace(AUTH_HEADER, (m, head, v) => v.includes(REDACTED) ? m : `${head}${REDACTED}`);
3582
+ out2 = out2.replace(AUTH_HEADER, (_m, head) => `${head}${REDACTED}`);
3061
3583
  out2 = redactAssignments(out2);
3062
3584
  out2 = out2.replace(
3063
3585
  PROSE,
@@ -3086,6 +3608,7 @@ var TYPE_TIER = {
3086
3608
  "turn.started": 0,
3087
3609
  "turn.ended": 0,
3088
3610
  "turn.transcript": 2,
3611
+ "turn.progress": 2,
3089
3612
  "tool.started": 1,
3090
3613
  "tool.ended": 1,
3091
3614
  "files.changed": 1,
@@ -3105,6 +3628,7 @@ var DATA_ALLOW = {
3105
3628
  "turn.started": { 0: ["turn_id"], 1: ["prompt_chars"] },
3106
3629
  "turn.ended": { 0: ["turn_id", "reason", "ms"], 1: ["error"] },
3107
3630
  "turn.transcript": { 2: ["turn", "user", "assistant", "at"] },
3631
+ "turn.progress": { 2: ["turn", "user", "assistant", "at"] },
3108
3632
  "tool.started": { 1: ["tool", "call_id", "turn_id", "agent_id"], 3: ["input"] },
3109
3633
  "tool.ended": { 1: ["tool", "call_id", "turn_id", "agent_id", "ms", "ok", "error"], 3: ["input", "output"] },
3110
3634
  "files.changed": { 1: ["paths"] },
@@ -3145,10 +3669,12 @@ function cap(v, max) {
3145
3669
  }
3146
3670
  return v;
3147
3671
  }
3672
+ function capTail(v, max) {
3673
+ return v.length > max ? `\u2026${v.slice(v.length - max + 1)}` : v;
3674
+ }
3148
3675
  function filterEvent(event, tier, pii, opts = {}) {
3149
3676
  const minTier = TYPE_TIER[event.type];
3150
3677
  if (minTier !== void 0 && minTier > tier) return null;
3151
- if (event.type === "native" && tier < 3) return null;
3152
3678
  const session = {};
3153
3679
  for (const k of SESSION_ALLOW)
3154
3680
  if (event.session[k] !== void 0)
@@ -3158,7 +3684,8 @@ function filterEvent(event, tier, pii, opts = {}) {
3158
3684
  if (keys === null) data = { ...event.data };
3159
3685
  else for (const k of keys) if (event.data[k] !== void 0) data[k] = event.data[k];
3160
3686
  if (typeof data.user === "string") data.user = cap(data.user, MAX_SIDE);
3161
- if (typeof data.assistant === "string") data.assistant = cap(data.assistant, MAX_SIDE);
3687
+ if (typeof data.assistant === "string")
3688
+ data.assistant = event.type === "turn.progress" ? capTail(data.assistant, MAX_SIDE) : cap(data.assistant, MAX_SIDE);
3162
3689
  if (data.input !== void 0) data.input = cap(data.input, MAX_IO);
3163
3690
  if (data.output !== void 0) data.output = cap(data.output, MAX_IO);
3164
3691
  if (typeof data.message === "string") data.message = cap(data.message, 200);
@@ -3179,9 +3706,15 @@ function filterEvent(event, tier, pii, opts = {}) {
3179
3706
  return out2;
3180
3707
  }
3181
3708
 
3709
+ // ../core/src/short-id.ts
3710
+ import { createHash as createHash4 } from "node:crypto";
3711
+ function shortId(harness, id) {
3712
+ return createHash4("sha256").update(`sessionpipe.session:${harness}:${id}`).digest("hex").slice(0, 8);
3713
+ }
3714
+
3182
3715
  // ../core/src/sinks/file.ts
3183
3716
  import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync6 } from "node:fs";
3184
- import path14 from "node:path";
3717
+ import path15 from "node:path";
3185
3718
  var FileSink = class {
3186
3719
  constructor(name, file, tier, pii) {
3187
3720
  this.name = name;
@@ -3195,7 +3728,7 @@ var FileSink = class {
3195
3728
  pii;
3196
3729
  async send(events) {
3197
3730
  try {
3198
- mkdirSync6(path14.dirname(this.file), { recursive: true });
3731
+ mkdirSync6(path15.dirname(this.file), { recursive: true });
3199
3732
  appendFileSync2(this.file, `${events.map((e) => JSON.stringify(e)).join("\n")}
3200
3733
  `);
3201
3734
  return { ok: true, acknowledged: events.length, accepted: events.length, duplicates: 0, rejected: [] };
@@ -3219,29 +3752,39 @@ var StdoutSink = class {
3219
3752
  pii;
3220
3753
  write;
3221
3754
  async send(events) {
3222
- for (const e of events) this.write(JSON.stringify(e));
3223
- return { ok: true, acknowledged: events.length, accepted: events.length, duplicates: 0, rejected: [] };
3755
+ try {
3756
+ for (const e of events) this.write(JSON.stringify(e));
3757
+ return { ok: true, acknowledged: events.length, accepted: events.length, duplicates: 0, rejected: [] };
3758
+ } catch (e) {
3759
+ return { ok: false, acknowledged: 0, error: String(e.message ?? e), action: "retry" };
3760
+ }
3224
3761
  }
3225
3762
  };
3226
3763
 
3227
3764
  // src/connect.ts
3228
- import path15 from "node:path";
3765
+ import path16 from "node:path";
3229
3766
  function isBroad(dir, home) {
3230
- const d = path15.resolve(dir);
3231
- const h = path15.resolve(home);
3232
- if (d === path15.parse(d).root) return true;
3233
- return d === h || h.startsWith(d.endsWith(path15.sep) ? d : d + path15.sep);
3767
+ const d = path16.resolve(dir);
3768
+ const h = path16.resolve(home);
3769
+ if (d === path16.parse(d).root) return true;
3770
+ return d === h || h.startsWith(
3771
+ /* v8 ignore next */
3772
+ d.endsWith(path16.sep) ? d : d + path16.sep
3773
+ );
3234
3774
  }
3235
- var inside = (child, parent) => child !== parent && child.startsWith(parent.endsWith(path15.sep) ? parent : parent + path15.sep);
3775
+ var inside = (child, parent) => child !== parent && child.startsWith(
3776
+ /* v8 ignore next */
3777
+ parent.endsWith(path16.sep) ? parent : parent + path16.sep
3778
+ );
3236
3779
  function connectFolders(o) {
3237
- if (o.flags.length) return { folders: o.flags.map((d) => path15.resolve(d)), from: "flags" };
3780
+ if (o.flags.length) return { folders: o.flags.map((d) => path16.resolve(d)), from: "flags" };
3238
3781
  if (o.existing.length) return { folders: [], from: "existing" };
3239
- if (!isBroad(o.cwd, o.home)) return { folders: [path15.resolve(o.cwd)], from: "here" };
3782
+ if (!isBroad(o.cwd, o.home)) return { folders: [path16.resolve(o.cwd)], from: "here" };
3240
3783
  const exists = o.exists ?? (() => true);
3241
3784
  const picked = [];
3242
3785
  for (const raw of o.recent) {
3243
- if (!raw || !path15.isAbsolute(raw)) continue;
3244
- const d = path15.resolve(raw);
3786
+ if (!raw || !path16.isAbsolute(raw)) continue;
3787
+ const d = path16.resolve(raw);
3245
3788
  if (isBroad(d, o.home) || picked.includes(d) || !exists(d)) continue;
3246
3789
  if (picked.some((p) => inside(d, p))) continue;
3247
3790
  for (let i = picked.length - 1; i >= 0; i--) if (inside(picked[i], d)) picked.splice(i, 1);
@@ -3258,42 +3801,136 @@ function connectTier(flag2, current) {
3258
3801
 
3259
3802
  // src/control/autoupdate.ts
3260
3803
  import { spawn as spawn3 } from "node:child_process";
3261
- import { readFileSync as readFileSync12 } from "node:fs";
3262
- import path22 from "node:path";
3804
+ import { readFileSync as readFileSync14 } from "node:fs";
3805
+ import path24 from "node:path";
3263
3806
 
3264
3807
  // src/update.ts
3265
3808
  import { spawn as spawn2 } from "node:child_process";
3266
- import { accessSync as accessSync2, constants as constants2, existsSync as existsSync9, realpathSync as realpathSync3 } from "node:fs";
3267
- import path20 from "node:path";
3809
+ import { accessSync as accessSync3, constants as constants3, existsSync as existsSync11, realpathSync as realpathSync3 } from "node:fs";
3810
+ import path22 from "node:path";
3268
3811
 
3269
3812
  // src/runtime.ts
3270
- import { execFileSync as execFileSync5 } from "node:child_process";
3271
- import { realpathSync as realpathSync2 } from "node:fs";
3272
- import os7 from "node:os";
3273
- import path19 from "node:path";
3813
+ import { execFileSync as execFileSync6 } from "node:child_process";
3814
+ import { readFileSync as readFileSync12, realpathSync as realpathSync2 } from "node:fs";
3815
+ import os8 from "node:os";
3816
+ import path21 from "node:path";
3817
+
3818
+ // src/launcher.ts
3819
+ import { accessSync as accessSync2, chmodSync as chmodSync2, constants as constants2, mkdirSync as mkdirSync7, readFileSync as readFileSync9, renameSync as renameSync2, rmSync, writeFileSync as writeFileSync6 } from "node:fs";
3820
+ import os5 from "node:os";
3821
+ import path17 from "node:path";
3822
+ var LAUNCHER = "sessionpipe-hook";
3823
+ var COPIED = ["worker.js", "hook.js"];
3824
+ function dataDir(env = process.env) {
3825
+ if (env.SESSIONPIPE_DATA) return path17.resolve(env.SESSIONPIPE_DATA);
3826
+ return path17.join(env.XDG_DATA_HOME || path17.join(os5.homedir(), ".local", "share"), "sessionpipe");
3827
+ }
3828
+ var launcherPath = (env = process.env) => path17.join(dataDir(env), LAUNCHER);
3829
+ var usesLauncher = (platform = process.platform) => platform !== "win32";
3830
+ var sq = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
3831
+ function launcherScript(node, hook, version) {
3832
+ return `#!/bin/sh
3833
+ # sessionpipe ${version} hook launcher, written by \`sessionpipe install\`. Harnesses run
3834
+ # this; it runs hook.js beside it with the node sessionpipe was installed with, or with
3835
+ # any node on PATH once that one is gone (an nvm uninstall, a Homebrew upgrade).
3836
+ n=${sq(node)}
3837
+ [ -x "$n" ] || n=$(command -v node) || { [ "$1" = antigravity ] && if [ "$2" = PreToolUse ]; then echo ${sq(AG_STALE_PRETOOL.trim())}; else echo '{}'; fi; exit 0; }
3838
+ exec "$n" ${sq(hook)} "$@"
3839
+ `;
3840
+ }
3841
+ function writeIfChanged(file, body, mode) {
3842
+ try {
3843
+ const cur = readFileSync9(file);
3844
+ if (cur.equals(Buffer.isBuffer(body) ? body : Buffer.from(body))) {
3845
+ chmodSync2(file, mode);
3846
+ return false;
3847
+ }
3848
+ } catch {
3849
+ }
3850
+ const tmp = `${file}.${process.pid}.tmp`;
3851
+ writeFileSync6(tmp, body, { mode });
3852
+ renameSync2(tmp, file);
3853
+ chmodSync2(file, mode);
3854
+ return true;
3855
+ }
3856
+ function installLauncher(o) {
3857
+ const dir = dataDir(o.env);
3858
+ mkdirSync7(dir, { recursive: true, mode: 493 });
3859
+ let changed = false;
3860
+ for (const f of COPIED)
3861
+ changed = writeIfChanged(path17.join(dir, f), readFileSync9(path17.join(o.distDir, f)), 420) || changed;
3862
+ const launcher = path17.join(dir, LAUNCHER);
3863
+ changed = writeIfChanged(launcher, launcherScript(o.node, path17.join(dir, "hook.js"), o.version), 493) || changed;
3864
+ return { launcher, changed };
3865
+ }
3866
+ function removeLauncher(env = process.env) {
3867
+ for (const f of [LAUNCHER, ...COPIED]) rmSync(path17.join(dataDir(env), f), { force: true });
3868
+ }
3869
+ function launcherState(version, env = process.env) {
3870
+ const file = launcherPath(env);
3871
+ let text = "";
3872
+ try {
3873
+ text = readFileSync9(file, "utf8");
3874
+ } catch {
3875
+ return {
3876
+ file,
3877
+ present: false,
3878
+ node: null,
3879
+ nodeOk: false,
3880
+ version: null,
3881
+ problems: ["the hook launcher is missing; run `sessionpipe install` again"]
3882
+ };
3883
+ }
3884
+ const node = /^n='((?:[^']|'\\'')*)'$/m.exec(text)?.[1]?.replace(/'\\''/g, "'") ?? null;
3885
+ const v = /^# sessionpipe (\S+) hook launcher/m.exec(text)?.[1] ?? null;
3886
+ let nodeOk = false;
3887
+ try {
3888
+ if (node) {
3889
+ accessSync2(node, constants2.X_OK);
3890
+ nodeOk = true;
3891
+ }
3892
+ } catch {
3893
+ }
3894
+ const problems = [];
3895
+ if (!nodeOk)
3896
+ problems.push(
3897
+ `the node the hook launcher names (${node ?? "?"}) is gone; it falls back to the node on PATH. \`sessionpipe install\` points it at this one.`
3898
+ );
3899
+ for (const f of COPIED)
3900
+ try {
3901
+ accessSync2(path17.join(path17.dirname(file), f), constants2.R_OK);
3902
+ } catch {
3903
+ problems.push(`${f} is missing beside the hook launcher; run \`sessionpipe install\` again`);
3904
+ }
3905
+ if (v && v !== version)
3906
+ problems.push(
3907
+ `the hooks run sessionpipe ${v}, this is ${version}; run \`sessionpipe install\` to bring them up to date`
3908
+ );
3909
+ return { file, present: true, node, nodeOk, version: v, problems };
3910
+ }
3274
3911
 
3275
3912
  // src/run.ts
3276
3913
  import {
3277
3914
  appendFileSync as appendFileSync3,
3278
- existsSync as existsSync8,
3279
- mkdirSync as mkdirSync8,
3915
+ existsSync as existsSync10,
3916
+ mkdirSync as mkdirSync9,
3280
3917
  readdirSync as readdirSync7,
3281
- readFileSync as readFileSync10,
3282
- renameSync as renameSync3,
3918
+ readFileSync as readFileSync11,
3919
+ renameSync as renameSync4,
3283
3920
  statSync as statSync7,
3284
3921
  unlinkSync as unlinkSync4,
3285
- writeFileSync as writeFileSync7
3922
+ writeFileSync as writeFileSync8
3286
3923
  } from "node:fs";
3287
- import os6 from "node:os";
3288
- import path18 from "node:path";
3924
+ import os7 from "node:os";
3925
+ import path20 from "node:path";
3289
3926
 
3290
3927
  // src/control/aliases.ts
3291
- import { chmodSync as chmodSync2, mkdirSync as mkdirSync7, readFileSync as readFileSync9, renameSync as renameSync2, writeFileSync as writeFileSync6 } from "node:fs";
3292
- import path16 from "node:path";
3293
- var aliasFile = (controlDir) => path16.join(controlDir, "aliases.json");
3928
+ import { chmodSync as chmodSync3, mkdirSync as mkdirSync8, readFileSync as readFileSync10, renameSync as renameSync3, writeFileSync as writeFileSync7 } from "node:fs";
3929
+ import path18 from "node:path";
3930
+ var aliasFile = (controlDir) => path18.join(controlDir, "aliases.json");
3294
3931
  function readAliases(file) {
3295
3932
  try {
3296
- const j = JSON.parse(readFileSync9(file, "utf8"));
3933
+ const j = JSON.parse(readFileSync10(file, "utf8"));
3297
3934
  if (!j || typeof j !== "object" || Array.isArray(j)) return {};
3298
3935
  const out2 = {};
3299
3936
  for (const [k, v] of Object.entries(j)) {
@@ -3307,13 +3944,13 @@ function readAliases(file) {
3307
3944
  }
3308
3945
  }
3309
3946
  function writeAliases(file, all) {
3310
- mkdirSync7(path16.dirname(file), { recursive: true, mode: 448 });
3947
+ mkdirSync8(path18.dirname(file), { recursive: true, mode: 448 });
3311
3948
  const tmp = `${file}.${process.pid}.tmp`;
3312
- writeFileSync6(tmp, `${JSON.stringify(all, null, 2)}
3949
+ writeFileSync7(tmp, `${JSON.stringify(all, null, 2)}
3313
3950
  `, { mode: 384 });
3314
- renameSync2(tmp, file);
3951
+ renameSync3(tmp, file);
3315
3952
  try {
3316
- chmodSync2(file, 384);
3953
+ chmodSync3(file, 384);
3317
3954
  } catch {
3318
3955
  }
3319
3956
  }
@@ -3325,12 +3962,12 @@ function receiverIdOf(all, harness, id) {
3325
3962
 
3326
3963
  // src/control/local.ts
3327
3964
  import net from "node:net";
3328
- import os5 from "node:os";
3329
- import path17 from "node:path";
3965
+ import os6 from "node:os";
3966
+ import path19 from "node:path";
3330
3967
  function socketPath(state2, env = process.env) {
3331
3968
  if (env.SESSIONPIPE_SOCKET) return env.SESSIONPIPE_SOCKET;
3332
- if (process.platform === "win32") return `\\\\.\\pipe\\sessionpipe-control-${os5.userInfo().username}`;
3333
- return path17.join(state2, "control.sock");
3969
+ if (process.platform === "win32") return `\\\\.\\pipe\\sessionpipe-control-${os6.userInfo().username}`;
3970
+ return path19.join(state2, "control.sock");
3334
3971
  }
3335
3972
  function ask(sock, req, timeoutMs) {
3336
3973
  return new Promise((resolve) => {
@@ -3363,10 +4000,17 @@ function ask(sock, req, timeoutMs) {
3363
4000
  }
3364
4001
 
3365
4002
  // src/http-sink.ts
3366
- import { createHash as createHash3, createHmac } from "node:crypto";
4003
+ import { createHash as createHash5, createHmac } from "node:crypto";
3367
4004
  var TIMEOUT_MS = 15e3;
3368
4005
  function webhookId(events) {
3369
- return `msg_${createHash3("sha256").update(events.map((e) => e.id).join(",")).digest("base64url").slice(0, 32)}`;
4006
+ return `msg_${createHash5("sha256").update(events.map((e) => e.id).join(",")).digest("base64url").slice(0, 32)}`;
4007
+ }
4008
+ function retryAfterMs(header, now = Date.now()) {
4009
+ if (header === null || header.trim() === "") return 6e4;
4010
+ const secs = Number(header);
4011
+ if (Number.isFinite(secs)) return Math.max(0, secs) * 1e3;
4012
+ const at = Date.parse(header);
4013
+ return Number.isFinite(at) ? Math.max(0, at - now) : 6e4;
3370
4014
  }
3371
4015
  var HttpSink = class {
3372
4016
  constructor(cfg, tier) {
@@ -3439,7 +4083,7 @@ var HttpSink = class {
3439
4083
  status: 429,
3440
4084
  error: err,
3441
4085
  action: "retry",
3442
- retryAfterMs: Number(r.headers.get("retry-after") ?? 60) * 1e3
4086
+ retryAfterMs: retryAfterMs(r.headers.get("retry-after"))
3443
4087
  };
3444
4088
  return { ok: false, acknowledged: 0, status: r.status, error: err, action: "retry" };
3445
4089
  } catch (e) {
@@ -3452,10 +4096,10 @@ var HttpSink = class {
3452
4096
 
3453
4097
  // src/secrets.ts
3454
4098
  import { spawnSync } from "node:child_process";
3455
- import { createHash as createHash4, randomBytes as randomBytes2 } from "node:crypto";
4099
+ import { createHash as createHash6, randomBytes as randomBytes2 } from "node:crypto";
3456
4100
  var SERVICE = "sessionpipe";
3457
- var defaultRunner = (cmd2, args2, input) => {
3458
- const r = spawnSync(cmd2, args2, {
4101
+ var defaultRunner = (cmd, args2, input) => {
4102
+ const r = spawnSync(cmd, args2, {
3459
4103
  input: input ?? "",
3460
4104
  encoding: "utf8",
3461
4105
  timeout: 8e3,
@@ -3464,7 +4108,7 @@ var defaultRunner = (cmd2, args2, input) => {
3464
4108
  return { status: r.error ? null : r.status, stdout: r.stdout ?? "" };
3465
4109
  };
3466
4110
  var sinkAccount = (name, what = "token") => {
3467
- const scope = process.env.SESSIONPIPE_CONFIG ? `@${createHash4("sha256").update(configFile()).digest("hex").slice(0, 8)}` : "";
4111
+ const scope = process.env.SESSIONPIPE_CONFIG ? `@${createHash6("sha256").update(configFile()).digest("hex").slice(0, 8)}` : "";
3468
4112
  return `sink:${name}:${what}${scope}`;
3469
4113
  };
3470
4114
  var controlAccount = (machine) => `control:${machine}`;
@@ -3566,7 +4210,7 @@ function dropSinkSecrets(s, run = defaultRunner) {
3566
4210
  // src/run.ts
3567
4211
  async function tellDaemon(state2, job, session, cwd, transcript) {
3568
4212
  const sock = socketPath(state2);
3569
- if (process.platform !== "win32" && !existsSync8(sock)) return;
4213
+ if (process.platform !== "win32" && !existsSync10(sock)) return;
3570
4214
  await ask(
3571
4215
  sock,
3572
4216
  {
@@ -3579,20 +4223,27 @@ async function tellDaemon(state2, job, session, cwd, transcript) {
3579
4223
  300
3580
4224
  );
3581
4225
  }
3582
- var VERSION = true ? "0.7.0" : "0.0.0";
4226
+ var VERSION = true ? "0.9.0" : "0.0.0";
3583
4227
  function enqueueJob(state2, job) {
3584
4228
  const jobs = jobsDir(state2);
3585
- mkdirSync8(jobs, { recursive: true, mode: 448 });
3586
- const f = path18.join(jobs, `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}.json`);
3587
- writeFileSync7(f, JSON.stringify(job), { mode: 384 });
4229
+ mkdirSync9(jobs, { recursive: true, mode: 448 });
4230
+ const f = path20.join(jobs, `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}.json`);
4231
+ writeFileSync8(f, JSON.stringify(job), { mode: 384 });
3588
4232
  return f;
3589
4233
  }
4234
+ function releaseJob(claimed) {
4235
+ if (!claimed) return;
4236
+ try {
4237
+ unlinkSync4(claimed);
4238
+ } catch {
4239
+ }
4240
+ }
3590
4241
  function log(state2, line) {
3591
4242
  try {
3592
- mkdirSync8(state2, { recursive: true, mode: 448 });
3593
- const f = path18.join(state2, "log");
4243
+ mkdirSync9(state2, { recursive: true, mode: 448 });
4244
+ const f = path20.join(state2, "log");
3594
4245
  try {
3595
- if (statSync7(f).size > 1024 * 1024) renameSync3(f, `${f}.1`);
4246
+ if (statSync7(f).size > 1024 * 1024) renameSync4(f, `${f}.1`);
3596
4247
  } catch {
3597
4248
  }
3598
4249
  appendFileSync3(f, `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
@@ -3601,11 +4252,11 @@ function log(state2, line) {
3601
4252
  }
3602
4253
  }
3603
4254
  function factsState(state2) {
3604
- const file = (h, s) => path18.join(state2, "facts", safeId(h), `${safeId(s)}.json`);
4255
+ const file = (h, s) => path20.join(state2, "facts", safeId(h), `${safeId(s)}.json`);
3605
4256
  return {
3606
4257
  get(h, s) {
3607
4258
  try {
3608
- return JSON.parse(readFileSync10(file(h, s), "utf8"));
4259
+ return JSON.parse(readFileSync11(file(h, s), "utf8"));
3609
4260
  } catch {
3610
4261
  return {};
3611
4262
  }
@@ -3614,8 +4265,8 @@ function factsState(state2) {
3614
4265
  const cur = this.get(h, s);
3615
4266
  const next = { ...cur, ...patch };
3616
4267
  if (JSON.stringify(next) === JSON.stringify(cur)) return;
3617
- mkdirSync8(path18.dirname(file(h, s)), { recursive: true, mode: 448 });
3618
- writeFileSync7(file(h, s), JSON.stringify(next), { mode: 384 });
4268
+ mkdirSync9(path20.dirname(file(h, s)), { recursive: true, mode: 448 });
4269
+ writeFileSync8(file(h, s), JSON.stringify(next), { mode: 384 });
3619
4270
  }
3620
4271
  };
3621
4272
  }
@@ -3631,44 +4282,80 @@ function buildSinks(sinks, o = {}) {
3631
4282
  const tier = Math.min(s.tier, s.max_tier ?? 3);
3632
4283
  if (s.url === "stdout") out2.push(new StdoutSink(s.name, tier, !!s.pii));
3633
4284
  else if (s.url.startsWith("file:"))
3634
- out2.push(new FileSink(s.name, s.url.slice(5).replace(/^~(?=\/|$)/, os6.homedir()), tier, !!s.pii));
4285
+ out2.push(new FileSink(s.name, s.url.slice(5).replace(/^~(?=\/|$)/, os7.homedir()), tier, !!s.pii));
3635
4286
  else if (/^https?:\/\//.test(s.url)) out2.push(new HttpSink(s, tier));
3636
4287
  }
3637
4288
  return out2;
3638
4289
  }
4290
+ var PROGRESS_MS = 2e4;
4291
+ function openTurn(read, o) {
4292
+ const last = read.turns[read.turns.length - 1];
4293
+ const fresh = (user, at) => ({
4294
+ turn: o.base + read.turns.length,
4295
+ user: user.slice(0, MAX_TURN_SIDE),
4296
+ assistant: "",
4297
+ at
4298
+ });
4299
+ if (read.open) return fresh(read.open.user, read.open.at);
4300
+ const prompt = o.prompt?.trim();
4301
+ if (prompt && !isInjected(prompt) && !(last && last.user.includes(prompt.slice(0, 200)))) return fresh(prompt, o.at);
4302
+ if (!last) return null;
4303
+ return {
4304
+ turn: o.base + read.turns.length - 1,
4305
+ user: last.user.slice(0, MAX_TURN_SIDE),
4306
+ assistant: capTail(last.assistant, MAX_TURN_SIDE),
4307
+ at: last.at
4308
+ };
4309
+ }
4310
+ var MAX_TURN_SIDE = 2e4;
3639
4311
  async function runJob(job, opts = {}) {
3640
4312
  const state2 = opts.state ?? stateDir();
3641
4313
  const cfg = readConfig();
3642
4314
  const adapter = adapterByName(job.harness);
3643
- if (!adapter) return { events: [], delivered: {} };
4315
+ if (!adapter) {
4316
+ log(state2, `${job.harness} ${job.event}: no adapter for harness "${job.harness}"; event dropped`);
4317
+ return { events: [], delivered: {} };
4318
+ }
3644
4319
  const r = adapter.fromHook({ argv: job.argv, stdin: job.stdin, env: { ...process.env, ...job.env }, cwd: job.cwd });
3645
- if (!r || !r.session.id) return { events: [], delivered: {} };
3646
- const id = receiverIdOf(readAliases(aliasFile(path18.join(state2, "control"))), job.harness, r.session.id) ?? r.session.id;
3647
- await tellDaemon(state2, job, id, r.session.cwd, r.transcript);
4320
+ if (!r || !r.session.id) {
4321
+ log(state2, `${job.harness} ${job.event}: the ${job.harness} adapter did not claim this payload; event dropped`);
4322
+ return { events: [], delivered: {} };
4323
+ }
4324
+ const id = receiverIdOf(readAliases(aliasFile(path20.join(state2, "control"))), job.harness, r.session.id) ?? r.session.id;
4325
+ const claudeEnd = job.harness === "claude-code" && r.events.some((e) => e.type === "session.ended");
4326
+ if (!claudeEnd) await tellDaemon(state2, job, id, r.session.cwd, r.transcript);
3648
4327
  const outbox = new Outbox(state2);
3649
4328
  const ref = { harness: job.harness, session: id };
3650
4329
  let release = takeLock(state2, ref);
3651
- for (let wait = 50; !release && wait <= 3200; wait *= 2) {
3652
- await new Promise((res) => setTimeout(res, wait));
4330
+ const lockWait = opts.lockWaitMs ?? LOCK_WAIT_MS;
4331
+ for (const until = Date.now() + lockWait; !release && Date.now() < until; ) {
4332
+ await new Promise((res) => setTimeout(res, 50));
3653
4333
  release = takeLock(state2, ref);
3654
4334
  }
3655
4335
  if (!release) {
3656
- log(state2, `${job.harness} ${job.event}: session ${id} locked for 6 s, re-queued`);
3657
- enqueueJob(state2, job);
3658
- return { events: [], delivered: {} };
4336
+ log(state2, `${job.harness} ${job.event}: session ${id} locked for ${lockWait / 1e3} s, re-queued`);
4337
+ const requeued = enqueueJob(state2, { ...job, tries: (job.tries ?? 0) + 1 });
4338
+ releaseJob(opts.claimed);
4339
+ return { events: [], delivered: {}, requeued };
3659
4340
  }
4341
+ const events = [];
3660
4342
  try {
3661
4343
  const facts = factsState(state2);
3662
4344
  const mem = facts.get(job.harness, id);
4345
+ if (claudeEnd && !existsSync10(outbox.file(ref)) && Object.keys(mem).length === 0 && !(r.transcript && existsSync10(r.transcript))) {
4346
+ releaseJob(opts.claimed);
4347
+ return { events: [], delivered: {} };
4348
+ }
4349
+ if (claudeEnd) await tellDaemon(state2, job, id, r.session.cwd, r.transcript);
3663
4350
  const heavy = !/^(PreToolUse|PostToolUse|PostToolUseFailure|BeforeTool|AfterTool|PostInvocation)$/.test(job.event);
3664
4351
  let learned = {};
3665
4352
  if (heavy) {
3666
- const f = adapter.facts(r.session, r.transcript, r.hints ?? {}, facts);
4353
+ const f = adapter.facts(r.session, r.transcript, r.hints ?? {}, facts, { ...process.env, ...job.env });
3667
4354
  const { started_at: _s, last_at: _l, first_ask: _fa, harness_version, ...rest } = f;
3668
4355
  learned = rest;
3669
4356
  if (harness_version) facts.save(job.harness, id, { harness_version });
3670
4357
  facts.save(job.harness, id, { session: { ...mem.session, ...learned } });
3671
- if (job.harness === "antigravity" && r.session.cwd && path18.isAbsolute(r.session.cwd))
4358
+ if (CWD_FROM_HOOKS.has(job.harness) && r.session.cwd && path20.isAbsolute(r.session.cwd))
3672
4359
  facts.save(job.harness, id, { cwd: r.session.cwd });
3673
4360
  }
3674
4361
  const known = mem.session ?? {};
@@ -3681,12 +4368,11 @@ async function runJob(job, opts = {}) {
3681
4368
  ...learned.branch ? { branch: learned.branch } : {},
3682
4369
  id,
3683
4370
  seq: 0,
3684
- machine: machineName(cfg, os6.hostname()),
4371
+ machine: machineName(cfg, os7.hostname()),
3685
4372
  ...cwdRaw ? { cwd: tilde(cwdRaw) } : {},
3686
4373
  ...r.session.model ? { model: r.session.model } : {}
3687
4374
  };
3688
4375
  const version = facts.get(job.harness, id).harness_version ?? void 0;
3689
- const events = [];
3690
4376
  for (const a of r.events) {
3691
4377
  const e = makeEvent(a, {
3692
4378
  harness: { name: job.harness, ...version ? { version } : {} },
@@ -3696,6 +4382,37 @@ async function runJob(job, opts = {}) {
3696
4382
  events.push(e);
3697
4383
  }
3698
4384
  const wantsTranscript = cfg.sinks.some((s) => !s.paused && Math.min(s.tier, s.max_tier ?? 3) >= 2);
4385
+ const ending = r.events.some((e) => e.type === "turn.ended" || e.type === "session.ended");
4386
+ const starting = r.events.some((e) => e.type === "turn.started");
4387
+ const startedId = r.events.find((e) => e.type === "turn.started")?.data.turn_id;
4388
+ const absorbed = starting && typeof startedId === "string" && facts.get(job.harness, id).open_turn === startedId;
4389
+ if (wantsTranscript && r.transcript && !ending && (starting || r.events.some((e) => e.type === "tool.ended"))) {
4390
+ const cur = facts.get(job.harness, id);
4391
+ const at = opts.now ?? job.at;
4392
+ if (starting || at - (Number(cur.progress_at) || 0) >= PROGRESS_MS) {
4393
+ const p = openTurn(adapter.readTranscript(r.transcript, Number(cur.transcript_line) || 0), {
4394
+ base: Number(cur.transcript_turns) || 0,
4395
+ prompt: !absorbed && typeof r.hints?.prompt === "string" ? r.hints.prompt : void 0,
4396
+ at
4397
+ });
4398
+ const sig = p && `${p.turn}:${p.user.length}:${p.assistant.length}`;
4399
+ if (p && (starting || sig !== cur.progress_sig)) {
4400
+ events.push(
4401
+ makeEvent(
4402
+ { type: "turn.progress", harnessEvent: job.event, data: { ...p, at: new Date(p.at).toISOString() } },
4403
+ {
4404
+ harness: { name: job.harness, ...version ? { version } : {} },
4405
+ session: { ...session, seq: outbox.nextSeq(ref) },
4406
+ now: at
4407
+ }
4408
+ )
4409
+ );
4410
+ facts.save(job.harness, id, { progress_at: at, progress_sig: sig });
4411
+ }
4412
+ }
4413
+ }
4414
+ if (starting && typeof startedId === "string" && !absorbed) facts.save(job.harness, id, { open_turn: startedId });
4415
+ if (ending) facts.save(job.harness, id, { open_turn: null });
3699
4416
  if (wantsTranscript && r.transcript && r.events.some((e) => e.type === "turn.ended" || e.type === "session.ended")) {
3700
4417
  const cur = facts.get(job.harness, id);
3701
4418
  const fromLine = Number(cur.transcript_line) || 0;
@@ -3723,17 +4440,80 @@ async function runJob(job, opts = {}) {
3723
4440
  ref,
3724
4441
  events.map((e) => ({ ...e, session: redactDeep(e.session), data: redactDeep(e.data) }))
3725
4442
  );
3726
- log(state2, `${job.harness} ${job.event}: ${events.map((e) => e.type).join(",") || "nothing"} (${id.slice(0, 8)})`);
3727
- const delivered = await flush(state2, cfg.sinks, outbox, ref);
3728
- if (Math.random() < 0.02) outbox.prune(cfg.keep_days ?? 30);
3729
- return { events, delivered };
4443
+ releaseJob(opts.claimed);
4444
+ log(
4445
+ state2,
4446
+ `${job.harness} ${job.event}: ${events.map((e) => e.type).join(",") || "nothing"} (${shortId(job.harness, id)})`
4447
+ );
3730
4448
  } finally {
3731
4449
  release();
3732
4450
  }
4451
+ const delivered = await drain(state2, cfg.sinks, outbox, ref) ?? {};
4452
+ if (Math.random() < 0.02) outbox.prune(cfg.keep_days ?? 30);
4453
+ return { events, delivered };
4454
+ }
4455
+ async function drain(state2, sinkConfigs, outbox, first) {
4456
+ const more = path20.join(state2, "locks", "flush.more");
4457
+ let release = takeNamedLock(state2, "flush");
4458
+ if (!release) {
4459
+ try {
4460
+ writeFileSync8(more, "", { mode: 384 });
4461
+ } catch {
4462
+ }
4463
+ release = takeNamedLock(state2, "flush");
4464
+ if (!release) return null;
4465
+ }
4466
+ const delivered = {};
4467
+ for (; ; ) {
4468
+ try {
4469
+ unlinkSync4(more);
4470
+ } catch {
4471
+ }
4472
+ try {
4473
+ for (const [k, v] of Object.entries(await flush(state2, sinkConfigs, outbox, first)))
4474
+ delivered[k] = (delivered[k] ?? 0) + v;
4475
+ } finally {
4476
+ release();
4477
+ }
4478
+ if (!existsSync10(more)) return delivered;
4479
+ release = takeNamedLock(state2, "flush");
4480
+ if (!release) return delivered;
4481
+ }
3733
4482
  }
4483
+ var BACKOFF_MS = [2e3, 1e4, 6e4, 5 * 6e4, 30 * 6e4];
4484
+ var backoffFile = (state2, sink2) => path20.join(state2, "backoff", `${safeId(sink2)}.json`);
4485
+ function readBackoff(state2, sink2) {
4486
+ try {
4487
+ const b = JSON.parse(readFileSync11(backoffFile(state2, sink2), "utf8"));
4488
+ return typeof b.until === "number" ? b : null;
4489
+ } catch {
4490
+ return null;
4491
+ }
4492
+ }
4493
+ function noteFailure(state2, sink2, res, prev) {
4494
+ const failures = (prev?.failures ?? 0) + 1;
4495
+ const ladder = BACKOFF_MS[Math.min(failures, BACKOFF_MS.length) - 1];
4496
+ const wait = Number.isFinite(res.retryAfterMs) ? res.retryAfterMs : ladder;
4497
+ const b = { until: Date.now() + wait, failures, why: `${res.status ?? ""} ${res.error ?? ""}`.trim() };
4498
+ const f = backoffFile(state2, sink2);
4499
+ try {
4500
+ mkdirSync9(path20.dirname(f), { recursive: true, mode: 448 });
4501
+ writeFileSync8(f, JSON.stringify(b), { mode: 384 });
4502
+ } catch {
4503
+ }
4504
+ return b;
4505
+ }
4506
+ function clearBackoff(state2, sink2) {
4507
+ try {
4508
+ unlinkSync4(backoffFile(state2, sink2));
4509
+ } catch {
4510
+ }
4511
+ }
4512
+ var LOCK_WAIT_MS = 6e3;
4513
+ var CWD_FROM_HOOKS = /* @__PURE__ */ new Set(["antigravity", "devin"]);
3734
4514
  var BATCH = 50;
3735
4515
  var MAX_PER_RUN = 500;
3736
- async function flush(state2, sinkConfigs, outbox, first) {
4516
+ async function flush(state2, sinkConfigs, outbox, first, o = {}) {
3737
4517
  const delivered = {};
3738
4518
  const sinks = buildSinks(sinkConfigs, {
3739
4519
  locked: (name) => log(state2, `sink ${name}: its token is in a keychain this process can't read; left for later`)
@@ -3743,13 +4523,39 @@ async function flush(state2, sinkConfigs, outbox, first) {
3743
4523
  const k = `${first.harness}/${safeId(first.session)}`;
3744
4524
  refs.sort((a, b) => `${a.harness}/${a.session}` === k ? -1 : `${b.harness}/${b.session}` === k ? 1 : 0);
3745
4525
  }
3746
- for (const sink2 of sinks) {
4526
+ const drainSink = async (sink2) => {
4527
+ let backoff = readBackoff(state2, sink2.name);
4528
+ if (backoff && !o.force && backoff.until > Date.now()) {
4529
+ delivered[sink2.name] = 0;
4530
+ return;
4531
+ }
3747
4532
  const cursors = new Cursors(state2, sink2.name);
3748
4533
  const c = cursors.read();
3749
4534
  let sent = 0;
3750
4535
  let stalled = false;
3751
4536
  let batch = [];
3752
4537
  let pending = {};
4538
+ const deliver = async (events) => {
4539
+ const res = await sink2.send(events);
4540
+ if (res.ok || res.action !== "split") return res;
4541
+ if (events.length === 1) {
4542
+ const e = events[0];
4543
+ log(state2, `sink ${sink2.name}: 413 ${res.error ?? ""} \u2192 event ${e.id} (${e.type}) is too large alone, dropped`);
4544
+ return { ok: true, acknowledged: 0, accepted: 0, duplicates: 0, rejected: [{ id: e.id, reason: "too_large" }] };
4545
+ }
4546
+ const half = Math.ceil(events.length / 2);
4547
+ const a = await deliver(events.slice(0, half));
4548
+ if (!a.ok) return a;
4549
+ const b = await deliver(events.slice(half));
4550
+ if (!b.ok) return b;
4551
+ return {
4552
+ ok: true,
4553
+ acknowledged: a.acknowledged + b.acknowledged,
4554
+ accepted: (a.accepted ?? 0) + (b.accepted ?? 0),
4555
+ duplicates: (a.duplicates ?? 0) + (b.duplicates ?? 0),
4556
+ rejected: [...a.rejected ?? [], ...b.rejected ?? []]
4557
+ };
4558
+ };
3753
4559
  const send = async () => {
3754
4560
  if (!batch.length) {
3755
4561
  Object.assign(c, pending);
@@ -3757,15 +4563,24 @@ async function flush(state2, sinkConfigs, outbox, first) {
3757
4563
  cursors.write(c);
3758
4564
  return true;
3759
4565
  }
3760
- const res = await sink2.send(batch);
4566
+ const res = await deliver(batch);
3761
4567
  if (!res.ok) {
3762
- log(state2, `sink ${sink2.name}: ${res.status ?? ""} ${res.error ?? ""} \u2192 ${res.action ?? "retry"}`);
4568
+ const action = res.action ?? "retry";
4569
+ let wait = "";
4570
+ if (action === "retry") {
4571
+ backoff = noteFailure(state2, sink2.name, res, backoff);
4572
+ wait = `, waits ${Math.round((backoff.until - Date.now()) / 1e3)} s`;
4573
+ }
4574
+ log(state2, `sink ${sink2.name}: ${res.status ?? ""} ${res.error ?? ""} \u2192 ${action}${wait}`);
3763
4575
  if (res.action === "pause" || res.action === "lower-tier") applySinkVerdict(sink2.name, res);
3764
4576
  if (res.action !== "drop") {
3765
4577
  batch = [];
3766
4578
  pending = {};
3767
4579
  return false;
3768
4580
  }
4581
+ } else if (backoff) {
4582
+ clearBackoff(state2, sink2.name);
4583
+ backoff = null;
3769
4584
  }
3770
4585
  sent += batch.length;
3771
4586
  Object.assign(c, pending);
@@ -3782,7 +4597,7 @@ async function flush(state2, sinkConfigs, outbox, first) {
3782
4597
  const { events, offset: next } = outbox.read(ref, offset, room);
3783
4598
  if (!events.length) break;
3784
4599
  for (const e of events) {
3785
- const f = filterEvent(e, sink2.tier, sink2.pii, { home: os6.homedir() });
4600
+ const f = filterEvent(e, sink2.tier, sink2.pii, { home: os7.homedir() });
3786
4601
  if (f) batch.push(f);
3787
4602
  }
3788
4603
  offset = next;
@@ -3800,7 +4615,8 @@ async function flush(state2, sinkConfigs, outbox, first) {
3800
4615
  if (!await send()) stalled = true;
3801
4616
  }
3802
4617
  delivered[sink2.name] = sent;
3803
- }
4618
+ };
4619
+ await Promise.all(sinks.map(drainSink));
3804
4620
  return delivered;
3805
4621
  }
3806
4622
  function applySinkVerdict(name, res) {
@@ -3814,7 +4630,7 @@ function applySinkVerdict(name, res) {
3814
4630
  } catch {
3815
4631
  }
3816
4632
  }
3817
- var jobsDir = (state2) => path18.join(state2, "jobs");
4633
+ var jobsDir = (state2) => path20.join(state2, "jobs");
3818
4634
 
3819
4635
  // src/runtime.ts
3820
4636
  function stableNode(exe) {
@@ -3837,26 +4653,59 @@ function stableNode(exe) {
3837
4653
  function viaNpx(distDir2) {
3838
4654
  return /[\\/]_npx[\\/]/.test(distDir2) || /[\\/]\.npm[\\/]/.test(distDir2);
3839
4655
  }
3840
- function rerunGlobally(argv, out2) {
4656
+ function installLocation() {
4657
+ let prefix = "";
4658
+ try {
4659
+ prefix = execFileSync6("npm", ["prefix", "-g"], {
4660
+ encoding: "utf8",
4661
+ timeout: 15e3,
4662
+ stdio: ["ignore", "pipe", "ignore"]
4663
+ }).trim();
4664
+ } catch {
4665
+ }
4666
+ return {
4667
+ prefix,
4668
+ target: /\/Cellar\/node(?:@\d+)?\/[^/]+/.test(prefix) ? path21.join(os8.homedir(), ".local") : prefix
4669
+ };
4670
+ }
4671
+ function assertSetupVersion(allowDowngrade, target) {
4672
+ if (allowDowngrade) return;
4673
+ const check = (installed2, where) => {
4674
+ if (installed2 && isNewer(installed2, VERSION, true))
4675
+ throw new Error(
4676
+ `refusing to downgrade sessionpipe: ${installed2} is installed at ${where}; this command would install ${VERSION}. Use the installed newer CLI, or pass --allow-downgrade to force ${VERSION}.`
4677
+ );
4678
+ };
4679
+ const hooks = launcherState(VERSION);
4680
+ check(hooks.version, hooks.file);
4681
+ const prefix = target ?? installLocation().target;
4682
+ if (!prefix) return;
4683
+ const pkg = path21.join(
4684
+ prefix,
4685
+ ...process.platform === "win32" ? [] : ["lib"],
4686
+ "node_modules",
4687
+ "sessionpipe",
4688
+ "package.json"
4689
+ );
4690
+ let installed;
4691
+ try {
4692
+ installed = JSON.parse(readFileSync12(pkg, "utf8")).version;
4693
+ } catch {
4694
+ }
4695
+ if (typeof installed === "string") check(installed, pkg);
4696
+ }
4697
+ function rerunGlobally(argv, out2, setup) {
4698
+ const { prefix, target } = installLocation();
4699
+ if (setup) assertSetupVersion(setup.allowDowngrade, target);
3841
4700
  out2(` Installing sessionpipe@${VERSION} globally, so hooks and services use a persistent copy\u2026`);
3842
4701
  let bin;
3843
4702
  try {
3844
- let prefix = "";
3845
- try {
3846
- prefix = execFileSync5("npm", ["prefix", "-g"], {
3847
- encoding: "utf8",
3848
- timeout: 15e3,
3849
- stdio: ["ignore", "pipe", "ignore"]
3850
- }).trim();
3851
- } catch {
3852
- }
3853
- const target = /\/Cellar\/node(?:@\d+)?\/[^/]+/.test(prefix) ? path19.join(os7.homedir(), ".local") : prefix;
3854
- execFileSync5("npm", ["install", "-g", `sessionpipe@${VERSION}`, ...target ? ["--prefix", target] : []], {
4703
+ execFileSync6("npm", ["install", "-g", `sessionpipe@${VERSION}`, ...target ? ["--prefix", target] : []], {
3855
4704
  stdio: ["ignore", "ignore", "inherit"]
3856
4705
  });
3857
- bin = process.platform === "win32" ? path19.join(target, "sessionpipe.cmd") : target ? path19.join(target, "bin", "sessionpipe") : "sessionpipe";
4706
+ bin = process.platform === "win32" ? path21.join(target, "sessionpipe.cmd") : target ? path21.join(target, "bin", "sessionpipe") : "sessionpipe";
3858
4707
  if (target && target !== prefix)
3859
- out2(` Installed into ${target}; make sure ${path19.join(target, "bin")} is on your PATH.`);
4708
+ out2(` Installed into ${target}; make sure ${path21.join(target, "bin")} is on your PATH.`);
3860
4709
  } catch {
3861
4710
  out2(
3862
4711
  " ! couldn't install globally (`npm install -g sessionpipe` failed). Run it yourself, then retry this command."
@@ -3864,7 +4713,7 @@ function rerunGlobally(argv, out2) {
3864
4713
  process.exit(1);
3865
4714
  }
3866
4715
  try {
3867
- execFileSync5(bin, argv, { stdio: "inherit", shell: process.platform === "win32" });
4716
+ execFileSync6(bin, argv, { stdio: "inherit", shell: process.platform === "win32" });
3868
4717
  } catch (e) {
3869
4718
  process.exit(typeof e.status === "number" ? e.status : 1);
3870
4719
  }
@@ -3888,9 +4737,9 @@ function compareVersions(a, b) {
3888
4737
  if (y.pre === null) return -1;
3889
4738
  return x.pre.localeCompare(y.pre, "en", { numeric: true });
3890
4739
  }
3891
- function isNewer(a, b) {
4740
+ function isNewer(a, b, includePrereleases = false) {
3892
4741
  const x = parse2(a);
3893
- if (!x || x.pre !== null) return false;
4742
+ if (!x || !includePrereleases && x.pre !== null) return false;
3894
4743
  const c = compareVersions(a, b);
3895
4744
  return c !== null && c > 0;
3896
4745
  }
@@ -3912,21 +4761,21 @@ function globalPrefix(distDir2, platform = process.platform) {
3912
4761
  if (viaNpx(distDir2)) return null;
3913
4762
  const prefix = prefixOf(distDir2, platform);
3914
4763
  if (!prefix) return null;
3915
- const bin = platform === "win32" ? path20.join(prefix, "sessionpipe.cmd") : path20.join(prefix, "bin", "sessionpipe");
3916
- return existsSync9(bin) ? prefix : null;
4764
+ const bin = platform === "win32" ? path22.join(prefix, "sessionpipe.cmd") : path22.join(prefix, "bin", "sessionpipe");
4765
+ return existsSync11(bin) ? prefix : null;
3917
4766
  }
3918
4767
  function unwritable(prefix, platform = process.platform) {
3919
- const dir = platform === "win32" ? path20.join(prefix, "node_modules") : path20.join(prefix, "lib", "node_modules");
4768
+ const dir = platform === "win32" ? path22.join(prefix, "node_modules") : path22.join(prefix, "lib", "node_modules");
3920
4769
  try {
3921
- accessSync2(dir, constants2.W_OK);
4770
+ accessSync3(dir, constants3.W_OK);
3922
4771
  return null;
3923
4772
  } catch {
3924
4773
  return `${dir} isn't writable by this user (installed with sudo?)`;
3925
4774
  }
3926
4775
  }
3927
4776
  function npmArgv(node, platform = process.platform) {
3928
- const dir = path20.dirname(node);
3929
- const candidates = platform === "win32" ? [path20.join(dir, "node_modules", "npm", "bin", "npm-cli.js")] : [path20.join(dir, "npm"), path20.join(dir, "..", "lib", "node_modules", "npm", "bin", "npm-cli.js")];
4777
+ const dir = path22.dirname(node);
4778
+ const candidates = platform === "win32" ? [path22.join(dir, "node_modules", "npm", "bin", "npm-cli.js")] : [path22.join(dir, "npm"), path22.join(dir, "..", "lib", "node_modules", "npm", "bin", "npm-cli.js")];
3930
4779
  for (const c of candidates) {
3931
4780
  try {
3932
4781
  const real = realpathSync3(c);
@@ -3937,17 +4786,17 @@ function npmArgv(node, platform = process.platform) {
3937
4786
  return [platform === "win32" ? "npm.cmd" : "npm"];
3938
4787
  }
3939
4788
  var execCapture = (argv, o) => new Promise((resolve) => {
3940
- const [cmd2, ...rest] = argv;
4789
+ const [cmd, ...rest] = argv;
3941
4790
  let stdout = "";
3942
4791
  let stderr = "";
3943
4792
  const keep = (s, b) => (s + b.toString("utf8")).slice(-16e3);
3944
4793
  let child;
3945
4794
  try {
3946
- child = spawn2(cmd2, rest, {
4795
+ child = spawn2(cmd, rest, {
3947
4796
  env: o.env,
3948
4797
  stdio: ["ignore", "pipe", "pipe"],
3949
4798
  windowsHide: true,
3950
- shell: process.platform === "win32" && cmd2.endsWith(".cmd")
4799
+ shell: process.platform === "win32" && cmd.endsWith(".cmd")
3951
4800
  });
3952
4801
  } catch (e) {
3953
4802
  resolve({ code: null, stdout, stderr, error: e.message });
@@ -3971,7 +4820,7 @@ var execCapture = (argv, o) => new Promise((resolve) => {
3971
4820
  });
3972
4821
  var lastLine = (r) => (r.error || r.stderr.trim().split("\n").filter(Boolean).slice(-2).join(" ") || `exit ${r.code}`).slice(0, 300);
3973
4822
  async function installedVersion(o) {
3974
- const r = await (o.exec ?? execCapture)([o.node, path20.join(o.distDir, "cli.js"), "--version"], {
4823
+ const r = await (o.exec ?? execCapture)([o.node, path22.join(o.distDir, "cli.js"), "--version"], {
3975
4824
  env: o.env ?? process.env,
3976
4825
  timeoutMs: 3e4
3977
4826
  });
@@ -3983,7 +4832,7 @@ async function installVersion(version, o) {
3983
4832
  const platform = o.platform ?? process.platform;
3984
4833
  const exec = o.exec ?? execCapture;
3985
4834
  const base = o.env ?? process.env;
3986
- const env = { ...base, PATH: [path20.dirname(o.node), base.PATH].filter(Boolean).join(path20.delimiter) };
4835
+ const env = { ...base, PATH: [path22.dirname(o.node), base.PATH].filter(Boolean).join(path22.delimiter) };
3987
4836
  const prefix = globalPrefix(o.distDir, platform);
3988
4837
  if (!prefix)
3989
4838
  return { ok: false, ms: Date.now() - t0, stage: "prefix", error: "this copy isn't a global npm install" };
@@ -4014,7 +4863,7 @@ async function installVersion(version, o) {
4014
4863
  let hooks = "no harness set up here; nothing to re-arm";
4015
4864
  if (o.harnesses.length) {
4016
4865
  const h = await exec(
4017
- [o.node, path20.join(o.distDir, "cli.js"), "install", "--backfill", "0", ...o.harnesses.map((n) => `--${n}`)],
4866
+ [o.node, path22.join(o.distDir, "cli.js"), "install", "--backfill", "0", ...o.harnesses.map((n) => `--${n}`)],
4018
4867
  { env, timeoutMs: 2 * 6e4 }
4019
4868
  );
4020
4869
  hooks = h.code === 0 ? `hooks re-armed for ${o.harnesses.join(", ")}` : `re-arming the hooks failed (${lastLine(h)}); \`sessionpipe install\` does it`;
@@ -4025,17 +4874,17 @@ async function installVersion(version, o) {
4025
4874
  // src/control/store.ts
4026
4875
  import {
4027
4876
  appendFileSync as appendFileSync4,
4028
- chmodSync as chmodSync3,
4877
+ chmodSync as chmodSync4,
4029
4878
  closeSync as closeSync4,
4030
- existsSync as existsSync10,
4879
+ existsSync as existsSync12,
4031
4880
  fsyncSync,
4032
- mkdirSync as mkdirSync9,
4881
+ mkdirSync as mkdirSync10,
4033
4882
  openSync as openSync4,
4034
- readFileSync as readFileSync11,
4035
- renameSync as renameSync4,
4036
- writeFileSync as writeFileSync8
4883
+ readFileSync as readFileSync13,
4884
+ renameSync as renameSync5,
4885
+ writeFileSync as writeFileSync9
4037
4886
  } from "node:fs";
4038
- import path21 from "node:path";
4887
+ import path23 from "node:path";
4039
4888
 
4040
4889
  // ../core/src/control/verify.ts
4041
4890
  var GRANT_MAX_MS = 24 * 36e5;
@@ -4047,6 +4896,11 @@ var CMD_MAX = 48e3;
4047
4896
  var TEXT_MAX = 2e4;
4048
4897
  var NOTE_MAX = 2e3;
4049
4898
  var GRANT_STR_MAX = 2e3;
4899
+ var FILES_MAX = 10;
4900
+ var FILE_SIZE_MAX = 26214400;
4901
+ var FILES_TOTAL_MAX = 52428800;
4902
+ var FILE_NAME_MAX = 200;
4903
+ var FILE_TYPE_MAX = 100;
4050
4904
  var COMMAND_TYPE = "sessionpipe.control";
4051
4905
  var GRANT_TYPE = "sessionpipe.grant";
4052
4906
  var CONTROL_KINDS = ["prompt", "permission.answer", "cancel", "start", "key.add"];
@@ -4059,6 +4913,9 @@ var HARNESS_RE = /^[a-z][a-z0-9-]{0,39}$/;
4059
4913
  var SESSION_ID_RE = /^[A-Za-z0-9._:-]{1,128}$/;
4060
4914
  var UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
4061
4915
  var B64URL_RE = /^[A-Za-z0-9_-]*$/;
4916
+ var SHA256_RE = /^[A-Za-z0-9_-]{43}$/;
4917
+ var MEDIA_TYPE_RE = /^[a-z0-9][a-z0-9!#$&^_.+-]*\/[a-z0-9][a-z0-9!#$&^_.+-]*$/i;
4918
+ var BAD_NAME_CHAR_RE = /[/\\\x00-\x1f\x7f]/;
4062
4919
  var UP = 1;
4063
4920
  var UV = 4;
4064
4921
  var enc = new TextEncoder();
@@ -4129,7 +4986,7 @@ async function importKey(key) {
4129
4986
  const k = await crypto.subtle.importKey("spki", u8(spki), { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, false, [
4130
4987
  "verify"
4131
4988
  ]);
4132
- const bits = k.algorithm.modulusLength ?? 0;
4989
+ const bits = k.algorithm.modulusLength;
4133
4990
  return bits >= 2048 ? k : null;
4134
4991
  }
4135
4992
  } catch {
@@ -4248,7 +5105,11 @@ function parseCommand(str) {
4248
5105
  if (!text()) return bad("bad_fields", `a new session needs text, at most ${TEXT_MAX} characters`);
4249
5106
  if (typeof c.cwd !== "string" || !isAbsolute(c.cwd) || c.cwd.length > 1024)
4250
5107
  return bad("bad_fields", "a new session needs an absolute folder");
4251
- if (has2("for") || has2("decision")) return bad("bad_fields", "a start carries text and cwd");
5108
+ if (has2("for") || has2("decision")) return bad("bad_fields", "a start carries text, cwd and files");
5109
+ if (has2("files")) {
5110
+ const why = filesProblem(c.files);
5111
+ if (why) return bad("bad_fields", why);
5112
+ }
4252
5113
  break;
4253
5114
  case "permission.answer":
4254
5115
  if (typeof c.for !== "string" || !c.for || c.for.length > 128) return bad("bad_fields", "which attention?");
@@ -4273,10 +5134,42 @@ function parseCommand(str) {
4273
5134
  }
4274
5135
  }
4275
5136
  if (c.kind !== "key.add" && (has2("key") || has2("name"))) return bad("bad_fields", "only a key.add carries a key");
5137
+ if (c.kind !== "start" && has2("files")) return bad("bad_fields", "only a start carries files");
4276
5138
  if (has2("note") && (typeof c.note !== "string" || c.note.length > NOTE_MAX))
4277
5139
  return bad("bad_fields", `a note is text, at most ${NOTE_MAX} characters`);
4278
5140
  return { ok: true, cmd: c };
4279
5141
  }
5142
+ function filesProblem(files) {
5143
+ if (!Array.isArray(files) || files.length < 1 || files.length > FILES_MAX)
5144
+ return `files is a list of 1 to ${FILES_MAX} files`;
5145
+ const seen = /* @__PURE__ */ new Set();
5146
+ let total = 0;
5147
+ for (const f of files) {
5148
+ if (!f || typeof f !== "object" || Array.isArray(f)) return "each file is an object";
5149
+ const { name, type, size, sha256: hash } = f;
5150
+ const why = fileNameProblem(name);
5151
+ if (why) return why;
5152
+ const key = name.normalize("NFC").toLowerCase();
5153
+ if (seen.has(key)) return "two files have the same name";
5154
+ seen.add(key);
5155
+ if (typeof type !== "string" || type.length > FILE_TYPE_MAX || !MEDIA_TYPE_RE.test(type))
5156
+ return "a file's type isn't a media type";
5157
+ if (!isNum(size) || !Number.isInteger(size) || size < 1 || size > FILE_SIZE_MAX)
5158
+ return `a file is 1 byte to ${FILE_SIZE_MAX / 1048576} MiB`;
5159
+ total += size;
5160
+ if (typeof hash !== "string" || !SHA256_RE.test(hash)) return "a file's sha256 isn't 43 base64url characters";
5161
+ }
5162
+ if (total > FILES_TOTAL_MAX) return `the files add up to more than ${FILES_TOTAL_MAX / 1048576} MiB`;
5163
+ return null;
5164
+ }
5165
+ function fileNameProblem(name) {
5166
+ if (typeof name !== "string") return "a file has no name";
5167
+ const n = name.normalize("NFC");
5168
+ if (n.length < 1 || n.length > FILE_NAME_MAX) return `a file name is 1 to ${FILE_NAME_MAX} characters`;
5169
+ if (BAD_NAME_CHAR_RE.test(n)) return "a file name has a slash, a backslash or a control character";
5170
+ if (n.startsWith(".")) return "a file name can't start with a dot";
5171
+ return null;
5172
+ }
4280
5173
  function isAbsolute(p) {
4281
5174
  return p.startsWith("/") || /^[A-Za-z]:[\\/]/.test(p);
4282
5175
  }
@@ -4298,7 +5191,7 @@ function parseGrant(str, rpId) {
4298
5191
  if (g.exp - g.iat > GRANT_MAX_MS + SKEW_MS) return { ok: false, why: "the grant lasts longer than a day" };
4299
5192
  return { ok: true, grant: g };
4300
5193
  }
4301
- async function verifyCsig(cmd2, csig, pub) {
5194
+ async function verifyCsig(cmd, csig, pub) {
4302
5195
  const sig = fromB64url(csig);
4303
5196
  const spki = fromB64url(pub);
4304
5197
  if (sig?.length !== 64 || !spki) return false;
@@ -4306,7 +5199,7 @@ async function verifyCsig(cmd2, csig, pub) {
4306
5199
  const key = await crypto.subtle.importKey("spki", u8(spki), { name: "ECDSA", namedCurve: "P-256" }, false, [
4307
5200
  "verify"
4308
5201
  ]);
4309
- return await crypto.subtle.verify({ name: "ECDSA", hash: "SHA-256" }, key, u8(sig), u8(enc.encode(cmd2)));
5202
+ return await crypto.subtle.verify({ name: "ECDSA", hash: "SHA-256" }, key, u8(sig), u8(enc.encode(cmd)));
4310
5203
  } catch {
4311
5204
  return false;
4312
5205
  }
@@ -4371,22 +5264,22 @@ async function checkGrantPath(s, c, o) {
4371
5264
  }
4372
5265
 
4373
5266
  // src/control/store.ts
4374
- var controlFile = (env = process.env) => path21.join(path21.dirname(configFile(env)), "control.json");
4375
- var controlState = (env = process.env) => path21.join(stateDir(env), "control");
5267
+ var controlFile = (env = process.env) => path23.join(path23.dirname(configFile(env)), "control.json");
5268
+ var controlState = (env = process.env) => path23.join(stateDir(env), "control");
4376
5269
  function writePrivate(file, value) {
4377
- mkdirSync9(path21.dirname(file), { recursive: true, mode: 448 });
5270
+ mkdirSync10(path23.dirname(file), { recursive: true, mode: 448 });
4378
5271
  const tmp = `${file}.${process.pid}.tmp`;
4379
- writeFileSync8(tmp, `${JSON.stringify(value, null, 2)}
5272
+ writeFileSync9(tmp, `${JSON.stringify(value, null, 2)}
4380
5273
  `, { mode: 384 });
4381
- renameSync4(tmp, file);
5274
+ renameSync5(tmp, file);
4382
5275
  try {
4383
- chmodSync3(file, 384);
5276
+ chmodSync4(file, 384);
4384
5277
  } catch {
4385
5278
  }
4386
5279
  }
4387
5280
  function readControl(env = process.env, run = defaultRunner) {
4388
5281
  try {
4389
- const j = JSON.parse(readFileSync11(controlFile(env), "utf8"));
5282
+ const j = JSON.parse(readFileSync13(controlFile(env), "utf8"));
4390
5283
  if (!j || !Array.isArray(j.receivers)) return null;
4391
5284
  return {
4392
5285
  name: j.name || "machine",
@@ -4403,7 +5296,7 @@ function readControl(env = process.env, run = defaultRunner) {
4403
5296
  }
4404
5297
  function pairedCount(env = process.env) {
4405
5298
  try {
4406
- const j = JSON.parse(readFileSync11(controlFile(env), "utf8"));
5299
+ const j = JSON.parse(readFileSync13(controlFile(env), "utf8"));
4407
5300
  return Array.isArray(j.receivers) ? j.receivers.length : 0;
4408
5301
  } catch {
4409
5302
  return 0;
@@ -4453,15 +5346,15 @@ var NonceStore = class {
4453
5346
  this.now = now;
4454
5347
  let raw = "";
4455
5348
  try {
4456
- raw = readFileSync11(file, "utf8");
5349
+ raw = readFileSync13(file, "utf8");
4457
5350
  } catch {
4458
5351
  }
4459
- const cut = this.now() - NONCE_TTL_MS;
5352
+ const cut2 = this.now() - NONCE_TTL_MS;
4460
5353
  for (const line of raw.split("\n")) {
4461
5354
  if (!line) continue;
4462
5355
  try {
4463
5356
  const { n, t } = JSON.parse(line);
4464
- if (typeof n === "string" && t > cut) this.seenAt.set(n, t);
5357
+ if (typeof n === "string" && t > cut2) this.seenAt.set(n, t);
4465
5358
  } catch {
4466
5359
  }
4467
5360
  }
@@ -4474,7 +5367,7 @@ var NonceStore = class {
4474
5367
  const t = this.seenAt.get(nonce);
4475
5368
  if (t !== void 0 && t > this.now() - NONCE_TTL_MS) return true;
4476
5369
  const at = this.now();
4477
- mkdirSync9(path21.dirname(this.file), { recursive: true, mode: 448 });
5370
+ mkdirSync10(path23.dirname(this.file), { recursive: true, mode: 448 });
4478
5371
  const fd = openSync4(this.file, "a", 384);
4479
5372
  try {
4480
5373
  appendFileSync4(fd, `${JSON.stringify({ n: nonce, t: at })}
@@ -4488,14 +5381,14 @@ var NonceStore = class {
4488
5381
  };
4489
5382
  /** Rewrite the file with only the live window (on start, and daily). */
4490
5383
  compact() {
4491
- const cut = this.now() - NONCE_TTL_MS;
4492
- for (const [n, t] of this.seenAt) if (t <= cut) this.seenAt.delete(n);
4493
- if (!existsSync10(this.file) && !this.seenAt.size) return;
5384
+ const cut2 = this.now() - NONCE_TTL_MS;
5385
+ for (const [n, t] of this.seenAt) if (t <= cut2) this.seenAt.delete(n);
5386
+ if (!existsSync12(this.file) && !this.seenAt.size) return;
4494
5387
  const body = [...this.seenAt].map(([n, t]) => JSON.stringify({ n, t })).join("\n");
4495
- mkdirSync9(path21.dirname(this.file), { recursive: true, mode: 448 });
4496
- writeFileSync8(`${this.file}.tmp`, body ? `${body}
5388
+ mkdirSync10(path23.dirname(this.file), { recursive: true, mode: 448 });
5389
+ writeFileSync9(`${this.file}.tmp`, body ? `${body}
4497
5390
  ` : "", { mode: 384 });
4498
- renameSync4(`${this.file}.tmp`, this.file);
5391
+ renameSync5(`${this.file}.tmp`, this.file);
4499
5392
  }
4500
5393
  get size() {
4501
5394
  return this.seenAt.size;
@@ -4508,27 +5401,27 @@ var AckOutbox = class {
4508
5401
  file;
4509
5402
  all() {
4510
5403
  try {
4511
- return readFileSync11(this.file, "utf8").split("\n").filter(Boolean).map((l) => JSON.parse(l));
5404
+ return readFileSync13(this.file, "utf8").split("\n").filter(Boolean).map((l) => JSON.parse(l));
4512
5405
  } catch {
4513
5406
  return [];
4514
5407
  }
4515
5408
  }
4516
5409
  add(a) {
4517
- mkdirSync9(path21.dirname(this.file), { recursive: true, mode: 448 });
5410
+ mkdirSync10(path23.dirname(this.file), { recursive: true, mode: 448 });
4518
5411
  appendFileSync4(this.file, `${JSON.stringify(a)}
4519
5412
  `, { mode: 384 });
4520
5413
  }
4521
5414
  keep(left) {
4522
- mkdirSync9(path21.dirname(this.file), { recursive: true, mode: 448 });
4523
- writeFileSync8(`${this.file}.tmp`, left.map((a) => JSON.stringify(a)).join("\n") + (left.length ? "\n" : ""), {
5415
+ mkdirSync10(path23.dirname(this.file), { recursive: true, mode: 448 });
5416
+ writeFileSync9(`${this.file}.tmp`, left.map((a) => JSON.stringify(a)).join("\n") + (left.length ? "\n" : ""), {
4524
5417
  mode: 384
4525
5418
  });
4526
- renameSync4(`${this.file}.tmp`, this.file);
5419
+ renameSync5(`${this.file}.tmp`, this.file);
4527
5420
  }
4528
5421
  };
4529
5422
  function readState(file) {
4530
5423
  try {
4531
- const j = JSON.parse(readFileSync11(file, "utf8"));
5424
+ const j = JSON.parse(readFileSync13(file, "utf8"));
4532
5425
  return { copies: j.copies && typeof j.copies === "object" ? j.copies : {} };
4533
5426
  } catch {
4534
5427
  return { copies: {} };
@@ -4541,10 +5434,10 @@ var UPDATE_EXIT = 75;
4541
5434
  var DAY_MS = 24 * 36e5;
4542
5435
  var LABEL3 = "org.sessionpipe.control";
4543
5436
  var UNIT = "sessionpipe-control.service";
4544
- var updateFile = (env = process.env) => path22.join(controlState(env), "update.json");
5437
+ var updateFile = (env = process.env) => path24.join(controlState(env), "update.json");
4545
5438
  function readUpdate(file) {
4546
5439
  try {
4547
- const j = JSON.parse(readFileSync12(file, "utf8"));
5440
+ const j = JSON.parse(readFileSync14(file, "utf8"));
4548
5441
  return j && typeof j === "object" ? j : {};
4549
5442
  } catch {
4550
5443
  return {};
@@ -4607,7 +5500,7 @@ function restartVia(o) {
4607
5500
  if (o.platform === "linux") {
4608
5501
  let cg = "";
4609
5502
  try {
4610
- cg = (o.cgroup ?? (() => readFileSync12("/proc/self/cgroup", "utf8")))();
5503
+ cg = (o.cgroup ?? (() => readFileSync14("/proc/self/cgroup", "utf8")))();
4611
5504
  } catch {
4612
5505
  }
4613
5506
  const inUnit = cg.split("\n").some((l) => l.endsWith(`/${UNIT}`) || l.includes(`/${UNIT}/`));
@@ -4837,14 +5730,112 @@ function supervise(o) {
4837
5730
  }
4838
5731
 
4839
5732
  // src/control/cli.ts
4840
- import { existsSync as existsSync13, realpathSync as realpathSync5, unwatchFile, watchFile } from "node:fs";
4841
- import os9 from "node:os";
4842
- import path25 from "node:path";
5733
+ import { existsSync as existsSync16, realpathSync as realpathSync6, statSync as statSync10, unwatchFile, watchFile } from "node:fs";
5734
+ import os10 from "node:os";
5735
+ import path28 from "node:path";
4843
5736
 
4844
5737
  // src/control/daemon.ts
4845
- import { existsSync as existsSync11, mkdirSync as mkdirSync10, readFileSync as readFileSync13, realpathSync as realpathSync4, statSync as statSync9, unlinkSync as unlinkSync5 } from "node:fs";
5738
+ import { chmodSync as chmodSync5, existsSync as existsSync14, mkdirSync as mkdirSync12, readFileSync as readFileSync15, realpathSync as realpathSync5, statSync as statSync9, unlinkSync as unlinkSync5 } from "node:fs";
4846
5739
  import net2 from "node:net";
4847
- import path23 from "node:path";
5740
+ import path26 from "node:path";
5741
+
5742
+ // src/control/files.ts
5743
+ import { createHash as createHash7 } from "node:crypto";
5744
+ import { existsSync as existsSync13, lstatSync, mkdirSync as mkdirSync11, realpathSync as realpathSync4, rmSync as rmSync2, writeFileSync as writeFileSync10 } from "node:fs";
5745
+ import path25 from "node:path";
5746
+ var FETCH_TIMEOUT_MS = 5 * 6e4;
5747
+ var FileProblem = class extends Error {
5748
+ };
5749
+ var b64url2 = (b) => b.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
5750
+ var reason = (e) => (e instanceof Error ? e.message : String(e)).slice(0, 160);
5751
+ async function fetchFiles(files, o) {
5752
+ const out2 = [];
5753
+ for (const file of files) {
5754
+ let bytes;
5755
+ try {
5756
+ bytes = await fetchOne(file, o);
5757
+ } catch (e) {
5758
+ if (e instanceof FileProblem) throw e;
5759
+ throw new FileProblem(`couldn't download ${JSON.stringify(file.name)}: ${reason(e)}`);
5760
+ }
5761
+ out2.push({ file, name: file.name.normalize("NFC"), bytes });
5762
+ }
5763
+ return out2;
5764
+ }
5765
+ async function fetchOne(f, o) {
5766
+ const label = JSON.stringify(f.name);
5767
+ const ctl = new AbortController();
5768
+ const t = setTimeout(() => ctl.abort(), o.timeoutMs ?? FETCH_TIMEOUT_MS);
5769
+ try {
5770
+ const res = await o.fetch(`${o.control.replace(/\/$/, "")}/files/${f.sha256}`, {
5771
+ headers: { authorization: `Bearer ${o.token}` },
5772
+ signal: ctl.signal
5773
+ });
5774
+ if (res.status === 404) throw new FileProblem(`${label} isn't on the receiver (never uploaded, or expired)`);
5775
+ if (res.status !== 200) throw new FileProblem(`couldn't download ${label}: HTTP ${res.status}`);
5776
+ if (Number(res.headers.get("content-length")) > f.size) throw tooLong(label, f);
5777
+ const buf = new Uint8Array(f.size);
5778
+ let got = 0;
5779
+ const reader = res.body.getReader();
5780
+ for (; ; ) {
5781
+ const { done, value } = await reader.read();
5782
+ if (done) break;
5783
+ if (got + value.length > f.size) throw tooLong(label, f);
5784
+ buf.set(value, got);
5785
+ got += value.length;
5786
+ }
5787
+ if (got !== f.size) throw new FileProblem(`${label} is ${got} bytes, not its signed ${f.size}`);
5788
+ const hash = b64url2(createHash7("sha256").update(buf).digest());
5789
+ if (hash !== f.sha256) throw new FileProblem(`${label} doesn't match its signed hash: it was changed`);
5790
+ return buf;
5791
+ } finally {
5792
+ clearTimeout(t);
5793
+ ctl.abort();
5794
+ }
5795
+ }
5796
+ var tooLong = (label, f) => new FileProblem(`${label} is larger than its signed size (${f.size} bytes)`);
5797
+ function realDir(p, why) {
5798
+ mkdirSync11(p, { recursive: true, mode: 448 });
5799
+ if (lstatSync(p).isSymbolicLink()) throw new FileProblem(`${why} is a symlink`);
5800
+ }
5801
+ function writeFiles(cwd, session, files) {
5802
+ const top = path25.join(cwd, ".sessionpipe");
5803
+ const parent = path25.join(top, "files");
5804
+ const dir = path25.join(parent, session);
5805
+ let made = false;
5806
+ try {
5807
+ realDir(top, ".sessionpipe");
5808
+ const ignore = path25.join(top, ".gitignore");
5809
+ if (!existsSync13(ignore)) writeFileSync10(ignore, "*\n", { flag: "wx", mode: 384 });
5810
+ realDir(parent, ".sessionpipe/files");
5811
+ if (existsSync13(dir)) throw new FileProblem(`.sessionpipe/files/${session} already exists`);
5812
+ mkdirSync11(dir, { mode: 448 });
5813
+ made = true;
5814
+ if (realpathSync4.native(dir) !== dir) throw new FileProblem(".sessionpipe leads outside the session's folder");
5815
+ const rel = [];
5816
+ for (const f of files) {
5817
+ writeFileSync10(path25.join(dir, f.name), f.bytes, { flag: "wx", mode: 384 });
5818
+ rel.push(`.sessionpipe/files/${session}/${f.name}`);
5819
+ }
5820
+ return rel;
5821
+ } catch (e) {
5822
+ if (made) rmSync2(dir, { recursive: true, force: true });
5823
+ if (e instanceof FileProblem) throw e;
5824
+ throw new FileProblem(`couldn't save the files: ${reason(e)}`);
5825
+ }
5826
+ }
5827
+ function humanSize(n) {
5828
+ if (n < 1024) return `${n} B`;
5829
+ if (n < 1048576) return `${Math.round(n / 1024)} KB`;
5830
+ return `${(n / 1048576).toFixed(1)} MB`;
5831
+ }
5832
+ function withFiles(text, files, rel) {
5833
+ const lines = files.map((f, i) => `- ${rel[i]} (${f.file.type}, ${humanSize(f.file.size)})`);
5834
+ return `${text}
5835
+
5836
+ Attached files (saved in this folder):
5837
+ ${lines.join("\n")}`;
5838
+ }
4848
5839
 
4849
5840
  // src/control/reply.ts
4850
5841
  import { closeSync as closeSync5, openSync as openSync5, readSync as readSync4, statSync as statSync8 } from "node:fs";
@@ -4891,9 +5882,9 @@ async function awaitAnswer(file, from, o) {
4891
5882
  const n = readSync4(fd, buf, 0, buf.length, offset);
4892
5883
  offset += n;
4893
5884
  const chunk = partial + buf.subarray(0, n).toString("utf8");
4894
- const cut = chunk.lastIndexOf("\n");
4895
- partial = cut < 0 ? chunk : chunk.slice(cut + 1);
4896
- if (cut >= 0) lines.push(...chunk.slice(0, cut).split("\n").filter(Boolean));
5885
+ const cut2 = chunk.lastIndexOf("\n");
5886
+ partial = cut2 < 0 ? chunk : chunk.slice(cut2 + 1);
5887
+ if (cut2 >= 0) lines.push(...chunk.slice(0, cut2).split("\n").filter(Boolean));
4897
5888
  } finally {
4898
5889
  closeSync5(fd);
4899
5890
  }
@@ -4926,7 +5917,7 @@ function kindPath(kind, harness) {
4926
5917
  case "prompt":
4927
5918
  return "prompt";
4928
5919
  case "permission.answer":
4929
- return harness === "gemini-cli" ? { unsupported: "Gemini CLI has no permission answer channel" } : "permission";
5920
+ return harness === "gemini-cli" ? { unsupported: "Gemini CLI has no permission answer channel" } : harness === "devin" ? { unsupported: "Devin has no permission answer channel" } : "permission";
4930
5921
  case "cancel":
4931
5922
  return "cancel";
4932
5923
  case "start":
@@ -5046,16 +6037,16 @@ var {
5046
6037
  recentlyWritten: recentlyWritten2,
5047
6038
  sessionFolder: sessionFolder2
5048
6039
  } = claude_code_control_exports;
5049
- function startNamed(cmd2) {
5050
- if (typeof cmd2 !== "string" || cmd2.length > 64 * 1024 || !cmd2.includes('"start"')) return null;
6040
+ function startNamed(cmd) {
6041
+ if (typeof cmd !== "string" || cmd.length > 64 * 1024 || !cmd.includes('"start"')) return null;
5051
6042
  try {
5052
- const c = JSON.parse(cmd2);
6043
+ const c = JSON.parse(cmd);
5053
6044
  return c?.kind === "start" && typeof c.session === "string" ? c.session : null;
5054
6045
  } catch {
5055
6046
  return null;
5056
6047
  }
5057
6048
  }
5058
- var VERSION2 = true ? "0.7.0" : "0.0.0";
6049
+ var VERSION2 = true ? "0.9.0" : "0.0.0";
5059
6050
  var MAX_REPLY = 2e4;
5060
6051
  var HELLO_EVERY_MS = 10 * 6e4;
5061
6052
  var TURN_FALLBACK_MS = 30 * 6e4;
@@ -5091,9 +6082,9 @@ var ControlDaemon = class {
5091
6082
  this.dir = dir;
5092
6083
  this.sock = sock;
5093
6084
  this.deps = deps;
5094
- this.nonces = new NonceStore(path23.join(dir, "nonces.jsonl"), deps.now);
5095
- this.acks = new AckOutbox(path23.join(dir, "acks.jsonl"));
5096
- this.state = readState(path23.join(dir, "state.json"));
6085
+ this.nonces = new NonceStore(path26.join(dir, "nonces.jsonl"), deps.now);
6086
+ this.acks = new AckOutbox(path26.join(dir, "acks.jsonl"));
6087
+ this.state = readState(path26.join(dir, "state.json"));
5097
6088
  this.aliases = readAliases(aliasFile(dir));
5098
6089
  }
5099
6090
  cfg;
@@ -5149,7 +6140,7 @@ var ControlDaemon = class {
5149
6140
  // --- lifecycle ------------------------------------------------------------------
5150
6141
  async start() {
5151
6142
  this.running = true;
5152
- mkdirSync10(this.dir, { recursive: true, mode: 448 });
6143
+ mkdirSync12(this.dir, { recursive: true, mode: 448 });
5153
6144
  await this.listen();
5154
6145
  for (const r of this.cfg.receivers) void this.pollLoop(r);
5155
6146
  this.helloTimer = setInterval(() => void this.helloAll(), HELLO_EVERY_MS);
@@ -5173,6 +6164,7 @@ var ControlDaemon = class {
5173
6164
  this.running = false;
5174
6165
  this.paused?.go();
5175
6166
  for (const a of this.aborts) a.abort();
6167
+ for (const st of this.polling.values()) st.wake?.();
5176
6168
  if (this.helloTimer) clearInterval(this.helloTimer);
5177
6169
  if (this.helloSoon) clearTimeout(this.helloSoon);
5178
6170
  if (this.ownTimer) clearInterval(this.ownTimer);
@@ -5242,7 +6234,12 @@ var ControlDaemon = class {
5242
6234
  try {
5243
6235
  await this.listen();
5244
6236
  } catch (e) {
5245
- this.deps.log(`couldn't listen again: ${String(e?.message || e)}`);
6237
+ this.deps.log(
6238
+ `couldn't listen again: ${String(
6239
+ /* v8 ignore next -- listen() throws Errors (net, DaemonRunning) */
6240
+ e?.message || e
6241
+ )}`
6242
+ );
5246
6243
  }
5247
6244
  return;
5248
6245
  }
@@ -5252,8 +6249,10 @@ var ControlDaemon = class {
5252
6249
  }
5253
6250
  reload(cfg) {
5254
6251
  const known = new Set(this.cfg.receivers.map((r) => r.control + r.machine));
6252
+ const told = JSON.stringify([this.cfg.folders, this.cfg.mode, this.cfg.name]);
5255
6253
  this.cfg = cfg;
5256
6254
  for (const r of cfg.receivers) if (!known.has(r.control + r.machine)) void this.pollLoop(r);
6255
+ if (this.running && JSON.stringify([cfg.folders, cfg.mode, cfg.name]) !== told) void this.helloAll();
5257
6256
  }
5258
6257
  status() {
5259
6258
  return {
@@ -5268,6 +6267,7 @@ var ControlDaemon = class {
5268
6267
  keys: r.keys.length,
5269
6268
  polling: !!p?.on,
5270
6269
  last_poll: p?.last ?? null,
6270
+ last_poll_age_ms: p ? Math.max(0, this.deps.now() - (p.last ?? p.started)) : 0,
5271
6271
  last_error: p?.error ?? null
5272
6272
  };
5273
6273
  }),
@@ -5290,7 +6290,7 @@ var ControlDaemon = class {
5290
6290
  if (other?.op === "status") throw new DaemonRunning(other.status ?? {});
5291
6291
  if (process.platform !== "win32") {
5292
6292
  try {
5293
- if (existsSync11(this.sock)) unlinkSync5(this.sock);
6293
+ if (existsSync14(this.sock)) unlinkSync5(this.sock);
5294
6294
  } catch {
5295
6295
  }
5296
6296
  }
@@ -5299,7 +6299,10 @@ var ControlDaemon = class {
5299
6299
  this.server.once("error", reject);
5300
6300
  this.server.listen(this.sock, () => resolve());
5301
6301
  });
5302
- if (process.platform !== "win32") this.sockId = socketId(this.sock);
6302
+ if (process.platform !== "win32") {
6303
+ chmodSync5(this.sock, 384);
6304
+ this.sockId = socketId(this.sock);
6305
+ }
5303
6306
  }
5304
6307
  onConn(s) {
5305
6308
  let buf = "";
@@ -5441,8 +6444,30 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
5441
6444
  // --- receivers ------------------------------------------------------------------
5442
6445
  async pollLoop(first) {
5443
6446
  const key = first.control + first.machine;
5444
- const st = { last: null, error: null, on: true };
6447
+ const st = {
6448
+ started: this.deps.now(),
6449
+ last: null,
6450
+ error: null,
6451
+ on: true,
6452
+ unauthorized: false,
6453
+ authBackoff: 0,
6454
+ wake: null
6455
+ };
5445
6456
  this.polling.set(key, st);
6457
+ const retry = (ms) => new Promise((resolve) => {
6458
+ st.on = false;
6459
+ if (!this.running) {
6460
+ resolve();
6461
+ return;
6462
+ }
6463
+ const timer = setTimeout(() => st.wake?.(), ms);
6464
+ timer.unref();
6465
+ st.wake = () => {
6466
+ clearTimeout(timer);
6467
+ st.wake = null;
6468
+ resolve();
6469
+ };
6470
+ });
5446
6471
  let backoff = 0;
5447
6472
  let downSince = 0;
5448
6473
  const steps = [1e3, 2e3, 5e3, 1e4, 15e3];
@@ -5451,6 +6476,7 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
5451
6476
  const r = this.cfg.receivers.find((x) => x.control + x.machine === key);
5452
6477
  if (!this.running || !r) break;
5453
6478
  if (this.paused) {
6479
+ st.on = false;
5454
6480
  await this.paused.until;
5455
6481
  continue;
5456
6482
  }
@@ -5463,11 +6489,12 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
5463
6489
  if (!st.error?.startsWith("can't read"))
5464
6490
  this.deps.log(`${r.url}: can't read this machine's token from ${r.token_in ?? "its store"}; retrying`);
5465
6491
  st.error = `can't read this machine's token from ${r.token_in ?? "its store"} (locked?); retrying every 30 s`;
5466
- await new Promise((res) => setTimeout(res, 3e4));
6492
+ await retry(3e4);
5467
6493
  continue;
5468
6494
  }
5469
6495
  }
5470
6496
  const ctl = new AbortController();
6497
+ st.on = true;
5471
6498
  this.aborts.add(ctl);
5472
6499
  const t = setTimeout(() => ctl.abort(), 35e3);
5473
6500
  try {
@@ -5476,15 +6503,21 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
5476
6503
  u.searchParams.set("wait", "25");
5477
6504
  const asked = this.deps.now();
5478
6505
  const res = await this.deps.fetch(u, { headers: { authorization: `Bearer ${r.token}` }, signal: ctl.signal });
5479
- st.last = this.deps.now();
5480
- if (downSince && res.status < 500) {
6506
+ if (res.status !== 200) clearTimeout(t);
6507
+ st.unauthorized = res.status === 401;
6508
+ if (downSince && (res.status === 200 || res.status === 204)) {
5481
6509
  this.deps.log(`${r.url}: back after ${Math.round((asked - downSince) / 1e3)} s (${backoff} failed tries)`);
5482
6510
  downSince = 0;
5483
6511
  }
5484
6512
  if (res.status === 401) {
5485
6513
  st.error = "the receiver no longer knows this machine (401); run `sessionpipe control pair` again";
5486
6514
  this.deps.log(`${r.url}: ${st.error}`);
5487
- break;
6515
+ downSince ||= this.deps.now();
6516
+ backoff++;
6517
+ const delay = Math.min(6e4 * 2 ** st.authBackoff, 30 * 6e4);
6518
+ st.authBackoff = Math.min(st.authBackoff + 1, 5);
6519
+ await retry(delay);
6520
+ continue;
5488
6521
  }
5489
6522
  if (res.status === 200) {
5490
6523
  let fresh = 0;
@@ -5495,29 +6528,47 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
5495
6528
  for (const m of body.messages ?? []) {
5496
6529
  if (m && !this.inflight.has(m.id)) fresh++;
5497
6530
  void this.handle(cur, m).catch(
5498
- (e) => this.deps.log(`handle ${String(m?.id).slice(-8)}: ${String(e?.message || e)}`)
6531
+ (e) => this.deps.log(
6532
+ `handle ${String(m?.id).slice(-8)}: ${String(
6533
+ /* v8 ignore next -- handle() rejects with an Error (the ack file) */
6534
+ e?.message || e
6535
+ )}`
6536
+ )
5499
6537
  );
5500
6538
  }
5501
6539
  } finally {
5502
6540
  this.receiving--;
5503
6541
  }
5504
- if (!fresh) await sleep2(2e3);
6542
+ clearTimeout(t);
6543
+ st.last = this.deps.now();
5505
6544
  st.error = null;
6545
+ st.authBackoff = 0;
5506
6546
  backoff = 0;
6547
+ if (!fresh) await sleep2(2e3);
5507
6548
  } else if (res.status === 204) {
6549
+ st.last = this.deps.now();
5508
6550
  st.error = null;
6551
+ st.authBackoff = 0;
5509
6552
  backoff = 0;
5510
6553
  } else {
5511
6554
  st.error = `HTTP ${res.status}`;
5512
6555
  downSince ||= this.deps.now();
5513
- await sleep2(pause(backoff++));
6556
+ let delay = pause(backoff++);
6557
+ if (res.status === 503) {
6558
+ const after = res.headers.get("retry-after");
6559
+ const ms = after && /^\d+$/.test(after) ? Number(after) * 1e3 : Date.parse(after ?? "") - this.deps.now();
6560
+ if (Number.isFinite(ms)) delay = Math.max(delay, ms);
6561
+ }
6562
+ await retry(Math.min(delay, 2147483647));
5514
6563
  }
5515
6564
  } catch (e) {
5516
6565
  if (!this.running) break;
5517
6566
  if (this.paused) continue;
5518
6567
  st.error = String(e?.message || e).slice(0, 120);
6568
+ st.unauthorized = false;
5519
6569
  downSince ||= this.deps.now();
5520
- await sleep2(pause(backoff++));
6570
+ clearTimeout(t);
6571
+ await retry(pause(backoff++));
5521
6572
  } finally {
5522
6573
  clearTimeout(t);
5523
6574
  this.aborts.delete(ctl);
@@ -5561,6 +6612,7 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
5561
6612
  const waiting = [...this.live].filter(([, l]) => l.waiter).map(([k]) => k);
5562
6613
  const modes = ["waiter", "turn", "resume", "fork", ...this.deps.sdk ? ["sdk"] : []];
5563
6614
  const harnesses = this.harnesses();
6615
+ const accounts = this.accounts();
5564
6616
  for (const r of this.cfg.receivers) {
5565
6617
  const body = {
5566
6618
  name: this.cfg.name,
@@ -5570,10 +6622,20 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
5570
6622
  version: VERSION2,
5571
6623
  waiting,
5572
6624
  folders: this.cfg.folders,
5573
- mode: this.cfg.mode
6625
+ mode: this.cfg.mode,
6626
+ ...accounts.length ? { accounts } : {},
6627
+ // This daemon takes `files` on a start (an older one would start without them).
6628
+ files: true
5574
6629
  };
5575
- await this.post(r, "/hello", body).catch(() => {
5576
- });
6630
+ const res = await this.post(r, "/hello", body).catch(() => null);
6631
+ const st = this.polling.get(r.control + r.machine);
6632
+ if (res?.ok && st?.unauthorized) {
6633
+ st.unauthorized = false;
6634
+ st.authBackoff = 0;
6635
+ st.error = null;
6636
+ this.deps.log(`${r.url}: hello accepted; retrying control poll now`);
6637
+ st.wake?.();
6638
+ }
5577
6639
  }
5578
6640
  }
5579
6641
  async post(r, sub, body) {
@@ -5674,30 +6736,30 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
5674
6736
  this.deps.log(`refused ${m.id.slice(-8)}: ${v.code} \u2014 ${v.why}`);
5675
6737
  return this.finish(r, m.id, { outcome: "refused", code: v.code, detail: v.why });
5676
6738
  }
5677
- const cmd2 = v.cmd;
5678
- if (cmd2.kind === "key.add") return this.addKey(r, m.id, cmd2, sent);
5679
- const ref = parseSessionRef(cmd2.session);
5680
- const kp = kindPath(cmd2.kind, ref.harness);
6739
+ const cmd = v.cmd;
6740
+ if (cmd.kind === "key.add") return this.addKey(r, m.id, cmd, sent);
6741
+ const ref = parseSessionRef(cmd.session);
6742
+ const kp = kindPath(cmd.kind, ref.harness);
5681
6743
  if (typeof kp === "object") return this.finish(r, m.id, { outcome: "unsupported", detail: kp.unsupported });
5682
6744
  try {
5683
- if (kp === "start") return await this.startSession(r, m.id, cmd2, sent);
5684
- let place = this.place(cmd2.session);
5685
- const starting = kp === "prompt" ? this.startsPending.get(cmd2.session) : void 0;
6745
+ if (kp === "start") return await this.startSession(r, m.id, cmd, sent);
6746
+ let place = this.place(cmd.session);
6747
+ const starting = kp === "prompt" ? this.startsPending.get(cmd.session) : void 0;
5686
6748
  if ("refused" in place && (starting || this.ownRuns.has(ref.id))) {
5687
6749
  this.finish(r, m.id, { outcome: "taken" });
5688
6750
  await starting;
5689
6751
  await this.serial(ref.id, async () => {
5690
6752
  });
5691
- place = this.place(cmd2.session);
6753
+ place = this.place(cmd.session);
5692
6754
  }
5693
6755
  if ("refused" in place)
5694
6756
  return this.finish(r, m.id, { outcome: "refused", code: place.refused, detail: place.why });
5695
- if (kp === "permission") return this.permission(r, m.id, cmd2, sent);
5696
- if (kp === "cancel") return await this.cancel(r, m.id, cmd2, sent);
5697
- return await this.serial(`deliver:${cmd2.session}`, async () => {
5698
- const now = this.place(cmd2.session);
6757
+ if (kp === "permission") return this.permission(r, m.id, cmd, sent);
6758
+ if (kp === "cancel") return await this.cancel(r, m.id, cmd, sent);
6759
+ return await this.serial(`deliver:${cmd.session}`, async () => {
6760
+ const now = this.place(cmd.session);
5699
6761
  if ("refused" in now) return this.finish(r, m.id, { outcome: "refused", code: now.refused, detail: now.why });
5700
- return this.prompt(r, m.id, cmd2, now, sent);
6762
+ return this.prompt(r, m.id, cmd, now, sent);
5701
6763
  });
5702
6764
  } catch (e) {
5703
6765
  this.finish(r, m.id, { outcome: "failed", detail: String(e?.message || e) });
@@ -5712,7 +6774,8 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
5712
6774
  if (ref.harness === "claude-code") {
5713
6775
  const target = this.state.copies[ref.id] ?? ref.id;
5714
6776
  const t = findTranscript2(target, this.deps.claudeDirs()) ?? findTranscript2(ref.id, this.deps.claudeDirs());
5715
- if (!t) return { refused: "no_session", why: `no Claude Code session ${ref.id.slice(0, 8)} on this machine` };
6777
+ if (!t)
6778
+ return { refused: "no_session", why: `no Claude Code session ${shortId(ref.harness, ref.id)} on this machine` };
5716
6779
  const folder = sessionFolder2(t);
5717
6780
  const ok2 = folder && this.allowed(folder);
5718
6781
  if (!ok2) return { refused: "folder", why: "that session's folder isn't one this machine allows" };
@@ -5733,13 +6796,14 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
5733
6796
  let file = null;
5734
6797
  if (ref.harness === "codex") {
5735
6798
  file = isUuid2(target) ? codex_control_exports.findRollout(target, this.deps.env ?? process.env) : null;
5736
- if (!a && !file) return { refused: "no_session", why: `no Codex session ${ref.id.slice(0, 8)} on this machine` };
6799
+ if (!a && !file)
6800
+ return { refused: "no_session", why: `no Codex session ${shortId(ref.harness, ref.id)} on this machine` };
5737
6801
  cwd ||= file ? codex_control_exports.codexFacts(file).cwd : void 0;
5738
6802
  } else cwd ||= this.live.get(session)?.cwd || this.factsCwd(ref.harness, ref.id);
5739
6803
  if (!cwd)
5740
6804
  return {
5741
6805
  refused: "no_session",
5742
- why: `no folder known for ${ref.harness === "codex" ? "Codex" : "Antigravity"} session ${ref.id.slice(0, 8)}`
6806
+ why: `no folder known for ${ref.harness === "codex" ? "Codex" : "Antigravity"} session ${shortId(ref.harness, ref.id)}`
5743
6807
  };
5744
6808
  const ok = this.allowed(cwd);
5745
6809
  if (!ok) return { refused: "folder", why: "that session's folder isn't one this machine allows" };
@@ -5748,9 +6812,9 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
5748
6812
  /** The folder the worker recorded for a session from its hooks (run.ts, facts). */
5749
6813
  factsCwd(harness, id) {
5750
6814
  try {
5751
- const f = path23.join(path23.dirname(this.dir), "facts", safeId(harness), `${safeId(id)}.json`);
5752
- const cwd = JSON.parse(readFileSync13(f, "utf8")).cwd;
5753
- return typeof cwd === "string" && path23.isAbsolute(cwd) ? cwd : void 0;
6815
+ const f = path26.join(path26.dirname(this.dir), "facts", safeId(harness), `${safeId(id)}.json`);
6816
+ const cwd = JSON.parse(readFileSync15(f, "utf8")).cwd;
6817
+ return typeof cwd === "string" && path26.isAbsolute(cwd) ? cwd : void 0;
5754
6818
  } catch {
5755
6819
  return void 0;
5756
6820
  }
@@ -5769,7 +6833,10 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
5769
6833
  writeAliases(aliasFile(this.dir), this.aliases);
5770
6834
  } catch (e) {
5771
6835
  this.deps.log(
5772
- `couldn't record ${harness}:${id.slice(0, 8)} \u2192 ${real.slice(0, 8)}: ${String(e?.message || e)}`
6836
+ `couldn't record ${harness}:${id.slice(0, 8)} \u2192 ${real.slice(0, 8)}: ${String(
6837
+ /* v8 ignore next -- writeAliases throws fs Errors */
6838
+ e?.message || e
6839
+ )}`
5773
6840
  );
5774
6841
  }
5775
6842
  }
@@ -5802,6 +6869,14 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
5802
6869
  };
5803
6870
  return null;
5804
6871
  }
6872
+ /** Who is signed in here, for hello. A bad .claude.json names nobody, never fails a hello. */
6873
+ accounts() {
6874
+ try {
6875
+ return this.deps.accounts ? this.deps.accounts() : claudeAccounts(this.deps.claudeDirs());
6876
+ } catch {
6877
+ return [];
6878
+ }
6879
+ }
5805
6880
  /** The harnesses this machine can run a message in, for hello. */
5806
6881
  harnesses() {
5807
6882
  if (this.deps.harnesses) return this.deps.harnesses();
@@ -5812,10 +6887,10 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
5812
6887
  ];
5813
6888
  }
5814
6889
  allowed(dir) {
5815
- if (!path23.isAbsolute(dir)) return null;
6890
+ if (!path26.isAbsolute(dir)) return null;
5816
6891
  let r;
5817
6892
  try {
5818
- r = realpathSync4.native(dir);
6893
+ r = realpathSync5.native(dir);
5819
6894
  if (!statSync9(r).isDirectory()) return null;
5820
6895
  } catch {
5821
6896
  return null;
@@ -5823,18 +6898,18 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
5823
6898
  for (const f of this.cfg.folders) {
5824
6899
  let rf = f;
5825
6900
  try {
5826
- rf = realpathSync4.native(f);
6901
+ rf = realpathSync5.native(f);
5827
6902
  } catch {
5828
6903
  }
5829
- if (r === rf || r.startsWith(rf.endsWith(path23.sep) ? rf : rf + path23.sep)) return r;
6904
+ if (r === rf || r.startsWith(rf.endsWith(path26.sep) ? rf : rf + path26.sep)) return r;
5830
6905
  }
5831
6906
  return null;
5832
6907
  }
5833
- permission(r, id, cmd2, sent) {
5834
- const l = this.live.get(cmd2.session);
5835
- const hook = l?.attentions.get(cmd2.for);
6908
+ permission(r, id, cmd, sent) {
6909
+ const l = this.live.get(cmd.session);
6910
+ const hook = l?.attentions.get(cmd.for);
5836
6911
  if (!hook) {
5837
- if (cmd2.decision === "allow")
6912
+ if (cmd.decision === "allow")
5838
6913
  return this.finish(r, id, {
5839
6914
  outcome: "refused",
5840
6915
  code: "no_attention",
@@ -5843,17 +6918,17 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
5843
6918
  this.finish(r, id, { outcome: "expired", detail: "the prompt was already answered or closed" });
5844
6919
  return;
5845
6920
  }
5846
- l.attentions.delete(cmd2.for);
6921
+ l.attentions.delete(cmd.for);
5847
6922
  this.reply(hook, {
5848
6923
  op: "decision",
5849
- behavior: cmd2.decision,
5850
- ...cmd2.note ? { message: cmd2.note } : {},
6924
+ behavior: cmd.decision,
6925
+ ...cmd.note ? { message: cmd.note } : {},
5851
6926
  id
5852
6927
  });
5853
6928
  this.finish(r, id, { outcome: "delivered", mode: "turn" }, sent);
5854
6929
  }
5855
- async cancel(r, id, cmd2, sent) {
5856
- const ref = parseSessionRef(cmd2.session);
6930
+ async cancel(r, id, cmd, sent) {
6931
+ const ref = parseSessionRef(cmd.session);
5857
6932
  const held = this.sdkSessions.get(ref.id);
5858
6933
  if (!held)
5859
6934
  return this.finish(r, id, {
@@ -5880,7 +6955,10 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
5880
6955
  };
5881
6956
  }
5882
6957
  const transcript = place.transcript;
5883
- const claude = ref.harness === "claude-code" ? this.deps.claude() : null;
6958
+ const claude = ref.harness === "claude-code" ? this.deps.claude() : (
6959
+ /* v8 ignore next -- every other harness returned above */
6960
+ null
6961
+ );
5884
6962
  const target = this.state.copies[ref.id] ?? ref.id;
5885
6963
  const t = transcript;
5886
6964
  const live = !this.ownRuns.has(target) && !!t && (liveProcess2(target, t.configDir) || recentlyWritten2(t.file, this.lastEnd.get(target), this.deps.now()));
@@ -5895,25 +6973,25 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
5895
6973
  api: false
5896
6974
  };
5897
6975
  }
5898
- async prompt(r, id, cmd2, place, sent) {
5899
- const route = routePrompt(this.view(cmd2.session, place));
6976
+ async prompt(r, id, cmd, place, sent) {
6977
+ const route = routePrompt(this.view(cmd.session, place));
5900
6978
  if ("unsupported" in route) return this.finish(r, id, { outcome: "unsupported", detail: route.unsupported });
5901
6979
  if ("refused" in route) return this.finish(r, id, { outcome: "refused", code: route.refused });
5902
- const ref = parseSessionRef(cmd2.session);
5903
- const text = cmd2.text;
6980
+ const ref = parseSessionRef(cmd.session);
6981
+ const text = cmd.text;
5904
6982
  switch (route.mode) {
5905
6983
  case "waiter": {
5906
- const l = this.live.get(cmd2.session);
6984
+ const l = this.live.get(cmd.session);
5907
6985
  const w = l.waiter;
5908
6986
  l.waiter = null;
5909
- const from = this.answerFrom(cmd2.session);
6987
+ const from = this.answerFrom(cmd.session);
5910
6988
  this.reply(w, { op: "message", text: frame(text, r.url), id });
5911
6989
  this.scheduleHello();
5912
- return this.followAnswer(r, id, "waiter", cmd2.session, from, sent);
6990
+ return this.followAnswer(r, id, "waiter", cmd.session, from, sent);
5913
6991
  }
5914
6992
  case "turn": {
5915
- const l = this.liveOf(cmd2.session);
5916
- const p = { receiver: r, msgId: id, text, session: cmd2.session, queuedAt: sent };
6993
+ const l = this.liveOf(cmd.session);
6994
+ const p = { receiver: r, msgId: id, text, session: cmd.session, queuedAt: sent };
5917
6995
  l.turnQueue.push(p);
5918
6996
  this.finish(r, id, { outcome: "taken" });
5919
6997
  const t = setTimeout(() => {
@@ -5938,7 +7016,8 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
5938
7016
  }
5939
7017
  case "resume":
5940
7018
  case "fork":
5941
- return this.headless(r, id, cmd2, place, route.mode, sent);
7019
+ return this.headless(r, id, cmd, place, route.mode, sent);
7020
+ /* v8 ignore next -- no harness has an input API wired yet (view() says api: false) */
5942
7021
  case "api":
5943
7022
  return this.finish(r, id, { outcome: "unsupported", detail: "no input API wired for this harness" });
5944
7023
  }
@@ -5949,10 +7028,10 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
5949
7028
  l.midTurn = false;
5950
7029
  const place = this.place(p.session);
5951
7030
  if ("refused" in place) return this.finish(p.receiver, p.msgId, { outcome: "refused", code: place.refused });
5952
- const cmd2 = { kind: "prompt", session: p.session, text: p.text };
7031
+ const cmd = { kind: "prompt", session: p.session, text: p.text };
5953
7032
  this.inflight.delete(p.msgId);
5954
7033
  this.inflight.add(p.msgId);
5955
- await this.prompt(p.receiver, p.msgId, cmd2, place, p.queuedAt).catch(
7034
+ await this.prompt(p.receiver, p.msgId, cmd, place, p.queuedAt).catch(
5956
7035
  (e) => this.finish(p.receiver, p.msgId, { outcome: "failed", detail: String(e?.message || e) })
5957
7036
  );
5958
7037
  }
@@ -5994,14 +7073,19 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
5994
7073
  const login = drv.login;
5995
7074
  if (login && !await this.signedIn(`${drv.cmd}:`, () => login(bin))) {
5996
7075
  this.deps.log(`${drv.cmd} isn't signed in as the daemon sees it`);
5997
- return { failed: drv.loginHint ?? `${drv.label} isn't signed in on this machine` };
7076
+ return {
7077
+ failed: (
7078
+ /* v8 ignore next -- only Codex has a login check, and it has a hint */
7079
+ drv.loginHint ?? `${drv.label} isn't signed in on this machine`
7080
+ )
7081
+ };
5998
7082
  }
5999
7083
  return { bin };
6000
7084
  }
6001
7085
  /** A Codex or Antigravity session's next message: a headless turn resumed under the
6002
7086
  * harness's own id, after any run of the daemon's own that holds it. */
6003
- async resumeDriven(r, id, cmd2, place, sent) {
6004
- const ref = parseSessionRef(cmd2.session);
7087
+ async resumeDriven(r, id, cmd, place, sent) {
7088
+ const ref = parseSessionRef(cmd.session);
6005
7089
  const drv = this.driver(ref.harness);
6006
7090
  const ready = await this.drivenProblem(drv);
6007
7091
  if ("failed" in ready) return this.finish(r, id, { outcome: "failed", detail: ready.failed });
@@ -6010,7 +7094,7 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
6010
7094
  const target = this.aliasOf(ref.harness, ref.id)?.id ?? place.target ?? ref.id;
6011
7095
  const args2 = drv.args({
6012
7096
  mode: this.cfg.mode,
6013
- prompt: frame(cmd2.text, r.url),
7097
+ prompt: frame(cmd.text, r.url),
6014
7098
  cwd: place.folder,
6015
7099
  resume: target
6016
7100
  });
@@ -6062,8 +7146,8 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
6062
7146
  (${drv.label} didn't find that session on this machine, so this ran in a new one; later messages continue there.)` : "";
6063
7147
  this.finish(r, id, { outcome: "delivered", mode: "resume", reply: out2.reply + moved }, sent);
6064
7148
  }
6065
- async headless(r, id, cmd2, place, mode, sent) {
6066
- if (parseSessionRef(cmd2.session)?.harness !== "claude-code") return this.resumeDriven(r, id, cmd2, place, sent);
7149
+ async headless(r, id, cmd, place, mode, sent) {
7150
+ if (parseSessionRef(cmd.session)?.harness !== "claude-code") return this.resumeDriven(r, id, cmd, place, sent);
6067
7151
  const claude = this.deps.claude();
6068
7152
  if (!claude)
6069
7153
  return this.finish(r, id, { outcome: "failed", detail: "Claude Code isn't installed on this machine" });
@@ -6071,7 +7155,7 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
6071
7155
  if ("error" in mf) return this.finish(r, id, { outcome: "refused", code: "mode", detail: mf.error });
6072
7156
  const signedOut = await this.loginProblem(place.transcript?.configDir);
6073
7157
  if (signedOut) return this.finish(r, id, { outcome: "failed", detail: signedOut });
6074
- const ref = parseSessionRef(cmd2.session);
7158
+ const ref = parseSessionRef(cmd.session);
6075
7159
  this.finish(r, id, { outcome: "taken" });
6076
7160
  await this.serial(ref.id, async () => {
6077
7161
  const target = this.state.copies[ref.id] ?? ref.id;
@@ -6083,7 +7167,7 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
6083
7167
  ...mf.args,
6084
7168
  ...allow.length ? [`--allowedTools=${allow.join(",")}`] : [],
6085
7169
  ...resume,
6086
- frame(cmd2.text, r.url)
7170
+ frame(cmd.text, r.url)
6087
7171
  ];
6088
7172
  this.deps.log(`${id.slice(-8)}: claude -p ${[...mf.args, ...resume].join(" ")} in ${place.folder}`);
6089
7173
  const stream = this.answerStream(r, id);
@@ -6105,7 +7189,7 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
6105
7189
  const ran = isUuid2(out2.session) ? out2.session : target;
6106
7190
  if (mode === "fork" && ran !== target) {
6107
7191
  this.state.copies[ref.id] = ran;
6108
- writeState(path23.join(this.dir, "state.json"), this.state);
7192
+ writeState(path26.join(this.dir, "state.json"), this.state);
6109
7193
  this.lastEnd.set(ran, this.deps.now());
6110
7194
  }
6111
7195
  this.finish(
@@ -6134,9 +7218,9 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
6134
7218
  const text = typeof j.result === "string" && j.result.trim() ? j.result : "(Claude Code finished without a written reply.)";
6135
7219
  return { reply: text + tail2, ...typeof j.session_id === "string" ? { session: j.session_id } : {} };
6136
7220
  }
6137
- addKey(r, id, cmd2, sent) {
6138
- const k = cmd2.key;
6139
- const name = typeof cmd2.name === "string" ? cmd2.name.trim().slice(0, 80) : void 0;
7221
+ addKey(r, id, cmd, sent) {
7222
+ const k = cmd.key;
7223
+ const name = typeof cmd.name === "string" ? cmd.name.trim().slice(0, 80) : void 0;
6140
7224
  const rec = this.cfg.receivers.find((x) => x.control === r.control && x.machine === r.machine) ?? r;
6141
7225
  if (rec.keys.some((x) => x.id === k.id))
6142
7226
  return this.finish(r, id, { outcome: "delivered", detail: "that device was already trusted" }, sent);
@@ -6153,7 +7237,10 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
6153
7237
  rec.keys = rec.keys.filter((x) => x.id !== k.id);
6154
7238
  return this.finish(r, id, {
6155
7239
  outcome: "failed",
6156
- detail: `couldn't save the key: ${String(e?.message || e)}`
7240
+ detail: `couldn't save the key: ${String(
7241
+ /* v8 ignore next -- writeControl throws fs Errors */
7242
+ e?.message || e
7243
+ )}`
6157
7244
  });
6158
7245
  }
6159
7246
  this.deps.log(
@@ -6184,7 +7271,16 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
6184
7271
  }) : null;
6185
7272
  stream.close();
6186
7273
  this.finish(r, id, { outcome: "delivered", mode, ...answer ? { reply: answer } : {} }, sent);
6187
- })().catch((e) => this.finish(r, id, { outcome: "delivered", mode, detail: String(e?.message || e) }));
7274
+ })().catch(
7275
+ (e) => this.finish(r, id, {
7276
+ outcome: "delivered",
7277
+ mode,
7278
+ detail: String(
7279
+ /* v8 ignore next -- awaitAnswer throws fs Errors */
7280
+ e?.message || e
7281
+ )
7282
+ })
7283
+ );
6188
7284
  }
6189
7285
  /** The answer so far, sent as a `progress` ack: whole, with a rising seq, at most
6190
7286
  * one in flight. Fire-and-forget — the final ack carries everything. */
@@ -6238,7 +7334,7 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
6238
7334
  this.procWaiters.shift()?.();
6239
7335
  }
6240
7336
  }
6241
- async startSession(r, id, cmd2, sent) {
7337
+ async startSession(r, id, cmd, sent) {
6242
7338
  const lim = this.limits();
6243
7339
  const now = this.deps.now();
6244
7340
  this.startTimes = this.startTimes.filter((t) => now - t < 36e5);
@@ -6249,23 +7345,23 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
6249
7345
  }
6250
7346
  this.startsRunning++;
6251
7347
  this.startTimes.push(now);
6252
- const sid = parseSessionRef(cmd2.session).id;
7348
+ const sid = parseSessionRef(cmd.session).id;
6253
7349
  try {
6254
- await this.serial(sid, () => this.owning(sid, () => this.startOne(r, id, cmd2, sent)));
7350
+ await this.serial(sid, () => this.owning(sid, () => this.startOne(r, id, cmd, sent)));
6255
7351
  } finally {
6256
7352
  this.startsRunning--;
6257
7353
  }
6258
7354
  }
6259
- async startOne(r, id, cmd2, sent) {
6260
- const ref = parseSessionRef(cmd2.session);
6261
- const cwd = this.allowed(cmd2.cwd);
7355
+ async startOne(r, id, cmd, sent) {
7356
+ const ref = parseSessionRef(cmd.session);
7357
+ const cwd = this.allowed(cmd.cwd);
6262
7358
  if (!cwd)
6263
7359
  return this.finish(r, id, {
6264
7360
  outcome: "refused",
6265
7361
  code: "folder",
6266
7362
  detail: "that folder isn't one this machine allows"
6267
7363
  });
6268
- if (ref.harness !== "claude-code") return this.startDriven(r, id, cmd2, ref, cwd, sent);
7364
+ if (ref.harness !== "claude-code") return this.startDriven(r, id, cmd, ref, cwd, sent);
6269
7365
  if (findTranscript2(ref.id, this.deps.claudeDirs()) || this.sdkSessions.has(ref.id))
6270
7366
  return this.finish(r, id, {
6271
7367
  outcome: "refused",
@@ -6275,12 +7371,14 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
6275
7371
  const signedOut = await this.loginProblem(this.deps.env?.CLAUDE_CONFIG_DIR);
6276
7372
  if (signedOut) return this.finish(r, id, { outcome: "failed", detail: signedOut });
6277
7373
  this.finish(r, id, { outcome: "taken" });
7374
+ const text = await this.attached(r, id, cmd, ref, cwd);
7375
+ if (text === null) return;
6278
7376
  if (this.deps.sdk) {
6279
7377
  const sdk = this.deps.sdk;
6280
7378
  const out3 = await this.slot(async () => {
6281
7379
  const s = sdk.start({ session: ref.id, cwd, mode: this.cfg.mode, allowedTools: this.allowedFor(r) });
6282
7380
  this.sdkSessions.set(ref.id, s);
6283
- return s.send(frameStart(cmd2.text, r.url));
7381
+ return s.send(frameStart(text, r.url));
6284
7382
  });
6285
7383
  return this.finish(
6286
7384
  r,
@@ -6305,7 +7403,7 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
6305
7403
  ...this.allowedFor(r).length ? [`--allowedTools=${this.allowedFor(r).join(",")}`] : [],
6306
7404
  "--session-id",
6307
7405
  ref.id,
6308
- frameStart(cmd2.text, r.url)
7406
+ frameStart(text, r.url)
6309
7407
  ],
6310
7408
  {
6311
7409
  cwd,
@@ -6323,10 +7421,10 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
6323
7421
  }
6324
7422
  /** A new Codex or Antigravity session: the harness picks its id, and the start's id
6325
7423
  * becomes its name (aliases.ts) as soon as the run says what it picked. */
6326
- async startDriven(r, id, cmd2, ref, cwd, sent) {
7424
+ async startDriven(r, id, cmd, ref, cwd, sent) {
6327
7425
  const drv = this.driver(ref.harness);
6328
7426
  const env = this.deps.env ?? process.env;
6329
- if (this.aliasOf(ref.harness, ref.id) || this.live.get(cmd2.session)?.cwd || ref.harness === "codex" && isUuid2(ref.id) && codex_control_exports.findRollout(ref.id, env))
7427
+ if (this.aliasOf(ref.harness, ref.id) || this.live.get(cmd.session)?.cwd || ref.harness === "codex" && isUuid2(ref.id) && codex_control_exports.findRollout(ref.id, env))
6330
7428
  return this.finish(r, id, {
6331
7429
  outcome: "refused",
6332
7430
  code: "bad_session",
@@ -6335,10 +7433,38 @@ ${IN_PLACE_ANSWER}`, id: p.msgId });
6335
7433
  const ready = await this.drivenProblem(drv);
6336
7434
  if ("failed" in ready) return this.finish(r, id, { outcome: "failed", detail: ready.failed });
6337
7435
  this.finish(r, id, { outcome: "taken" });
6338
- const args2 = drv.args({ mode: this.cfg.mode, prompt: frameStart(cmd2.text, r.url), cwd });
6339
- this.deps.log(`${id.slice(-8)}: ${drv.cmd} new session for ${ref.id.slice(0, 8)} (${this.cfg.mode}) in ${cwd}`);
7436
+ const text = await this.attached(r, id, cmd, ref, cwd);
7437
+ if (text === null) return;
7438
+ const args2 = drv.args({ mode: this.cfg.mode, prompt: frameStart(text, r.url), cwd });
7439
+ this.deps.log(
7440
+ `${id.slice(-8)}: ${drv.cmd} new session for ${shortId(ref.harness, ref.id)} (${this.cfg.mode}) in ${cwd}`
7441
+ );
6340
7442
  await this.drive(r, id, ref, drv, ready.bin, args2, cwd, sent, null);
6341
7443
  }
7444
+ /** A start's text, with its files (CONTROL.md §6) fetched, checked and written under
7445
+ * `cwd` first — every check in §5 passed and the nonce is recorded by now, so the
7446
+ * `taken` ack is out and the download may take its time. Null when a file failed:
7447
+ * the command is acked `failed` with code `file`, nothing is kept, no session starts. */
7448
+ async attached(r, id, cmd, ref, cwd) {
7449
+ const text = cmd.text;
7450
+ if (!cmd.files) return text;
7451
+ try {
7452
+ const got = await fetchFiles(cmd.files, {
7453
+ control: r.control,
7454
+ token: r.token,
7455
+ fetch: this.deps.fetch,
7456
+ timeoutMs: this.deps.fileTimeoutMs
7457
+ });
7458
+ const rel = writeFiles(cwd, ref.id, got);
7459
+ this.deps.log(`${id.slice(-8)}: ${got.length} file(s) saved in ${cwd}/.sessionpipe/files/${ref.id}`);
7460
+ return withFiles(text, got, rel);
7461
+ } catch (e) {
7462
+ const detail = e.message;
7463
+ this.deps.log(`${id.slice(-8)}: file \u2014 ${detail}`);
7464
+ this.finish(r, id, { outcome: "failed", code: "file", detail });
7465
+ return null;
7466
+ }
7467
+ }
6342
7468
  closeIdleSdk() {
6343
7469
  const idle = this.deps.sdkIdleMs ?? 10 * 6e4;
6344
7470
  for (const [id, s] of this.sdkSessions)
@@ -6362,12 +7488,13 @@ function socketId(p) {
6362
7488
  }
6363
7489
 
6364
7490
  // src/control/pair.ts
6365
- import { execFileSync as execFileSync6 } from "node:child_process";
7491
+ import { execFileSync as execFileSync7 } from "node:child_process";
6366
7492
  import { randomBytes as randomBytes3 } from "node:crypto";
6367
- import { existsSync as existsSync12, mkdirSync as mkdirSync11, unlinkSync as unlinkSync6, writeFileSync as writeFileSync9 } from "node:fs";
6368
- import os8 from "node:os";
6369
- import path24 from "node:path";
6370
- var VERSION3 = true ? "0.7.0" : "0.0.0";
7493
+ import { existsSync as existsSync15, mkdirSync as mkdirSync13, unlinkSync as unlinkSync6, writeFileSync as writeFileSync11 } from "node:fs";
7494
+ import os9 from "node:os";
7495
+ import path27 from "node:path";
7496
+ var VERSION3 = true ? "0.9.0" : "0.0.0";
7497
+ var RENEW_FOR_MS = 60 * 6e4;
6371
7498
  async function json(r) {
6372
7499
  try {
6373
7500
  return await r.json();
@@ -6402,55 +7529,89 @@ async function pair(o) {
6402
7529
  const cfg = readControl(o.env) ?? { name: o.name, folders: [], mode: o.mode, receivers: [] };
6403
7530
  cfg.name = o.name || cfg.name;
6404
7531
  cfg.mode = o.mode;
6405
- cfg.folders = [.../* @__PURE__ */ new Set([...cfg.folders, ...o.folders.map((d) => path24.resolve(d))])];
7532
+ cfg.folders = [.../* @__PURE__ */ new Set([...cfg.folders, ...o.folders.map((d) => path27.resolve(d))])];
6406
7533
  const existing = cfg.receivers.find((r) => r.control === control);
7534
+ let accounts = o.accounts;
7535
+ if (!accounts)
7536
+ try {
7537
+ const e = o.env ?? process.env;
7538
+ accounts = claudeAccounts(claudeDirs(e), e.HOME);
7539
+ } catch {
7540
+ accounts = [];
7541
+ }
6407
7542
  const auth = { authorization: `Bearer ${o.token ?? pollKey}`, "content-type": "application/json" };
6408
- const started = await f(`${control}/pair`, {
6409
- method: "POST",
6410
- headers: o.token ? auth : { "content-type": "application/json" },
6411
- body: JSON.stringify({
6412
- ...pollKey ? { poll_key: pollKey } : {},
6413
- ...existing ? { machine: existing.machine } : {},
6414
- name: cfg.name,
6415
- harnesses: o.harnesses ?? drivableHarnesses(o.env ?? process.env),
6416
- modes: ["waiter", "turn", "resume", "fork"],
6417
- version: VERSION3,
6418
- folders: cfg.folders,
6419
- mode: cfg.mode
6420
- })
6421
- });
6422
- const s = await json(started);
6423
- if (started.status !== 201 && started.status !== 200)
6424
- throw new Error(
6425
- `the receiver refused to start pairing (HTTP ${started.status}${s.reason ? `: ${String(s.reason)}` : ""})`
6426
- );
6427
- const machine = String(s.machine);
6428
- const code = String(s.code);
6429
- o.out("");
6430
- const digits = String(s.check);
6431
- const spaced = /^\d{6}$/.test(digits) ? `${digits.slice(0, 3)} ${digits.slice(3)}` : digits;
7543
+ const hello = {
7544
+ ...pollKey ? { poll_key: pollKey } : {},
7545
+ name: cfg.name,
7546
+ harnesses: o.harnesses ?? drivableHarnesses(o.env ?? process.env),
7547
+ modes: ["waiter", "turn", "resume", "fork"],
7548
+ version: VERSION3,
7549
+ folders: cfg.folders,
7550
+ mode: cfg.mode,
7551
+ ...accounts.length ? { accounts } : {},
7552
+ // This daemon takes `files` on a start (an older one would start without them).
7553
+ files: true
7554
+ };
7555
+ const start = async (machine) => {
7556
+ const started = await f(`${control}/pair`, {
7557
+ method: "POST",
7558
+ headers: o.token ? auth : { "content-type": "application/json" },
7559
+ body: JSON.stringify({ ...hello, ...machine ? { machine } : {} })
7560
+ });
7561
+ const s = await json(started);
7562
+ if (started.status !== 201 && started.status !== 200)
7563
+ throw new Error(
7564
+ `the receiver refused to start pairing (HTTP ${started.status}${s.reason ? `: ${String(s.reason)}` : ""})`
7565
+ );
7566
+ return {
7567
+ machine: String(s.machine),
7568
+ code: String(s.code),
7569
+ url: String(s.url),
7570
+ check: String(s.check),
7571
+ until: Date.parse(String(s.expires_at)) || Date.now() + 10 * 6e4
7572
+ };
7573
+ };
6432
7574
  const host = new URL(o.url).host;
6433
- o.out(` On any phone or computer where you're signed in to ${host} (it doesn't have to be this one),`);
6434
- if (pollKey) {
6435
- o.out(` open ${String(s.url)} and enter: ${spaced}`);
6436
- o.out(" then approve with that device's passkey (Touch ID, Face ID, a security key).");
6437
- o.out(" Pairing several machines? Run this on each one: every machine shows its own digits.");
6438
- } else {
6439
- o.out(" open this link and approve with that device's passkey (Touch ID, Face ID, a security key):");
6440
- o.out(` ${String(s.url)}`);
6441
- o.out(` The page shows these digits too: ${digits}`);
6442
- }
6443
- o.out("");
6444
- const until = Date.parse(String(s.expires_at)) || Date.now() + 10 * 6e4;
7575
+ const announce = (p) => {
7576
+ const spaced = /^\d{6}$/.test(p.check) ? `${p.check.slice(0, 3)} ${p.check.slice(3)}` : p.check;
7577
+ o.out("");
7578
+ o.out(` On any phone or computer where you're signed in to ${host} (it doesn't have to be this one),`);
7579
+ if (pollKey) {
7580
+ o.out(` open ${p.url} and enter: ${spaced}`);
7581
+ o.out(" then approve with that device's passkey (Touch ID, Face ID, a security key).");
7582
+ o.out(" Pairing several machines? Run this on each one: every machine shows its own digits.");
7583
+ } else {
7584
+ o.out(" open this link and approve with that device's passkey (Touch ID, Face ID, a security key):");
7585
+ o.out(` ${p.url}`);
7586
+ o.out(` The page shows these digits too: ${p.check}`);
7587
+ }
7588
+ const mins = Math.max(1, Math.round((p.until - Date.now()) / 6e4));
7589
+ o.out(` This code is good for ${mins} minute${mins === 1 ? "" : "s"}; if it runs out, a fresh one appears here.`);
7590
+ o.out("");
7591
+ };
7592
+ const renewUntil = Date.now() + (o.renewFor ?? RENEW_FOR_MS);
7593
+ let cur = await start(existing?.machine);
7594
+ announce(cur);
6445
7595
  for (; ; ) {
6446
- if (Date.now() > until) throw new Error("the pairing link expired; run `sessionpipe control pair` again");
6447
7596
  await sleep3(2e3);
6448
- const r = await f(`${control}/pair?code=${encodeURIComponent(code)}`, { headers: auth }).catch(() => null);
6449
- if (!r) continue;
6450
- if (r.status === 404) throw new Error("the receiver lost the pairing; run `sessionpipe control pair` again");
6451
- const p = await json(r);
6452
- if (p.status === "expired") throw new Error("the pairing link expired; run `sessionpipe control pair` again");
6453
- if (p.status !== "paired") continue;
7597
+ let p = {};
7598
+ let expired = Date.now() > cur.until;
7599
+ if (!expired) {
7600
+ const r = await f(`${control}/pair?code=${encodeURIComponent(cur.code)}`, { headers: auth }).catch(() => null);
7601
+ if (!r) continue;
7602
+ if (r.status === 404) throw new Error("the receiver lost the pairing; run `sessionpipe control pair` again");
7603
+ p = await json(r);
7604
+ if (p.status === "expired") expired = true;
7605
+ else if (p.status !== "paired") continue;
7606
+ }
7607
+ if (expired) {
7608
+ if (Date.now() >= renewUntil) throw new Error("the pairing link expired; run `sessionpipe control pair` again");
7609
+ cur = await start(o.token ? cur.machine : existing?.machine);
7610
+ o.out(" Nobody approved that code before it expired. Here is a fresh one; the old digits no longer work.");
7611
+ announce(cur);
7612
+ continue;
7613
+ }
7614
+ const { machine, code } = cur;
6454
7615
  const key = p.key;
6455
7616
  const proof = p.proof;
6456
7617
  const v = await verifyEnrollment({ machine, code, key, proof, rpId });
@@ -6470,6 +7631,9 @@ async function pair(o) {
6470
7631
  if (!existing) cfg.receivers.push(rec);
6471
7632
  writeControl(cfg, o.env);
6472
7633
  o.out(` \u2713 Paired with ${rec.url} as ${machine} \xB7 ${rec.keys.length} key(s) trusted on this machine`);
7634
+ const named = accounts.flatMap((a) => a.email ? [a.email] : []);
7635
+ if (named.length)
7636
+ o.out(` Its session board names your Claude Code account${named.length > 1 ? "s" : ""}: ${named.join(", ")}`);
6473
7637
  if (typeof p.account === "string")
6474
7638
  o.out(` Approved by ${p.account}. If that isn't you, run \`sessionpipe control off\` now.`);
6475
7639
  if (typeof p.sink_token === "string" && o.addSink) o.addSink(p.sink_token);
@@ -6505,7 +7669,7 @@ function removeKey(id, env) {
6505
7669
  var LABEL4 = "org.sessionpipe.control";
6506
7670
  var UNIT2 = "sessionpipe-control.service";
6507
7671
  function serviceFiles(o) {
6508
- const home = o.home ?? os8.homedir();
7672
+ const home = o.home ?? os9.homedir();
6509
7673
  const xml = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
6510
7674
  const plist = `<?xml version="1.0" encoding="UTF-8"?>
6511
7675
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
@@ -6518,8 +7682,8 @@ function serviceFiles(o) {
6518
7682
  <key>RunAtLoad</key><true/>
6519
7683
  <key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict>
6520
7684
  <key>ProcessType</key><string>Background</string>
6521
- <key>StandardOutPath</key><string>${xml(path24.join(o.logDir, "daemon.log"))}</string>
6522
- <key>StandardErrorPath</key><string>${xml(path24.join(o.logDir, "daemon.log"))}</string>
7685
+ <key>StandardOutPath</key><string>${xml(path27.join(o.logDir, "daemon.log"))}</string>
7686
+ <key>StandardErrorPath</key><string>${xml(path27.join(o.logDir, "daemon.log"))}</string>
6523
7687
  </dict>
6524
7688
  </plist>
6525
7689
  `;
@@ -6539,11 +7703,11 @@ RestartSec=5
6539
7703
  WantedBy=default.target
6540
7704
  `;
6541
7705
  return {
6542
- plist: { file: path24.join(home, "Library", "LaunchAgents", `${LABEL4}.plist`), body: plist },
6543
- unit: { file: path24.join(home, ".config", "systemd", "user", UNIT2), body: unit }
7706
+ plist: { file: path27.join(home, "Library", "LaunchAgents", `${LABEL4}.plist`), body: plist },
7707
+ unit: { file: path27.join(home, ".config", "systemd", "user", UNIT2), body: unit }
6544
7708
  };
6545
7709
  }
6546
- var execQuiet = (cmd2, args2) => execFileSync6(cmd2, args2, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 15e3 });
7710
+ var execQuiet = (cmd, args2) => execFileSync7(cmd, args2, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 15e3 });
6547
7711
  function ensureLinger(user, exec = execQuiet) {
6548
7712
  const lingering = () => {
6549
7713
  try {
@@ -6565,13 +7729,13 @@ function ensureLinger(user, exec = execQuiet) {
6565
7729
  }
6566
7730
  function installService(o) {
6567
7731
  const logDir = controlState(o.env);
6568
- mkdirSync11(logDir, { recursive: true, mode: 448 });
7732
+ mkdirSync13(logDir, { recursive: true, mode: 448 });
6569
7733
  const f = serviceFiles({ node: o.node, cli: o.cli, logDir });
6570
7734
  const run = o.exec ?? execQuiet;
6571
7735
  const platform = o.platform ?? process.platform;
6572
7736
  if (platform === "darwin") {
6573
- mkdirSync11(path24.dirname(f.plist.file), { recursive: true });
6574
- writeFileSync9(f.plist.file, f.plist.body);
7737
+ mkdirSync13(path27.dirname(f.plist.file), { recursive: true });
7738
+ writeFileSync11(f.plist.file, f.plist.body);
6575
7739
  const uid = String(process.getuid?.() ?? "");
6576
7740
  for (const d of ["gui", "user"])
6577
7741
  try {
@@ -6580,40 +7744,40 @@ function installService(o) {
6580
7744
  }
6581
7745
  try {
6582
7746
  run("launchctl", ["bootstrap", `gui/${uid}`, f.plist.file]);
6583
- return `launchd agent ${LABEL4} (log: ${path24.join(logDir, "daemon.log")})`;
7747
+ return `launchd agent ${LABEL4} (log: ${path27.join(logDir, "daemon.log")})`;
6584
7748
  } catch {
6585
7749
  }
6586
7750
  run("launchctl", ["bootstrap", `user/${uid}`, f.plist.file]);
6587
- return `launchd agent ${LABEL4} in the background domain, since nobody is logged in at this Mac's screen; after a restart it starts again when someone logs in there (log: ${path24.join(logDir, "daemon.log")})`;
7751
+ return `launchd agent ${LABEL4} in the background domain, since nobody is logged in at this Mac's screen; after a restart it starts again when someone logs in there (log: ${path27.join(logDir, "daemon.log")})`;
6588
7752
  }
6589
7753
  if (platform === "linux") {
6590
- mkdirSync11(path24.dirname(f.unit.file), { recursive: true });
6591
- writeFileSync9(f.unit.file, f.unit.body);
7754
+ mkdirSync13(path27.dirname(f.unit.file), { recursive: true });
7755
+ writeFileSync11(f.unit.file, f.unit.body);
6592
7756
  try {
6593
7757
  run("systemctl", ["--user", "daemon-reload"]);
6594
7758
  run("systemctl", ["--user", "enable", "--now", UNIT2]);
6595
7759
  } catch {
6596
7760
  return `wrote ${f.unit.file}, but systemd --user isn't reachable here; run \`sessionpipe control run\` yourself (tmux, screen)`;
6597
7761
  }
6598
- const linger = ensureLinger(os8.userInfo().username, run);
7762
+ const linger = ensureLinger(os9.userInfo().username, run);
6599
7763
  return `systemd user unit ${UNIT2}; ${linger.note} (journalctl --user -u ${UNIT2})`;
6600
7764
  }
6601
7765
  return "no service manager for this platform yet; run `sessionpipe control run` yourself";
6602
7766
  }
6603
7767
  function uninstallService(env) {
6604
7768
  const f = serviceFiles({ node: "", cli: "", logDir: controlState(env) });
6605
- const run = (cmd2, args2) => {
7769
+ const run = (cmd, args2) => {
6606
7770
  try {
6607
- execFileSync6(cmd2, args2, { stdio: "ignore", timeout: 15e3 });
7771
+ execFileSync7(cmd, args2, { stdio: "ignore", timeout: 15e3 });
6608
7772
  } catch {
6609
7773
  }
6610
7774
  };
6611
- if (process.platform === "darwin" && existsSync12(f.plist.file)) {
7775
+ if (process.platform === "darwin" && existsSync15(f.plist.file)) {
6612
7776
  run("launchctl", ["bootout", `gui/${process.getuid?.() ?? ""}/${LABEL4}`]);
6613
7777
  run("launchctl", ["bootout", `user/${process.getuid?.() ?? ""}/${LABEL4}`]);
6614
7778
  unlinkSync6(f.plist.file);
6615
7779
  }
6616
- if (process.platform === "linux" && existsSync12(f.unit.file)) {
7780
+ if (process.platform === "linux" && existsSync15(f.unit.file)) {
6617
7781
  run("systemctl", ["--user", "disable", "--now", UNIT2]);
6618
7782
  unlinkSync6(f.unit.file);
6619
7783
  run("systemctl", ["--user", "daemon-reload"]);
@@ -6733,9 +7897,10 @@ ${RE_ARM}`;
6733
7897
  }
6734
7898
 
6735
7899
  // src/control/cli.ts
6736
- var { claudeLogin: claudeLogin2, detectCaps: detectCaps2, findClaude: findClaude2, runClaude: runClaude2 } = claude_code_control_exports;
7900
+ var { claudeLogin: claudeLogin3, detectCaps: detectCaps2, findClaude: findClaude2, runClaude: runClaude2 } = claude_code_control_exports;
6737
7901
  var CONTROL_HELP = ` sessionpipe control pair <receiver> [--folder DIR]\u2026 [--mode safe|auto] [--tier 0-3] [--token T] [--name N] [--no-service]
6738
7902
  sessionpipe control mode auto|safe (how sessions run when you message them)
7903
+ sessionpipe control folders [add|remove DIR\u2026] (where sessions you message may run)
6739
7904
  sessionpipe control status | keys [remove <id>] | off [<receiver>] | run
6740
7905
  sessionpipe wait [--session <harness>:<id>] (a session runs this in the background)`;
6741
7906
  var MODE_TEXT = ` auto Claude Code's auto mode: the session reads, edits and runs commands, and Claude Code's
@@ -6765,6 +7930,42 @@ async function askMode(out2) {
6765
7930
  rl.close();
6766
7931
  }
6767
7932
  }
7933
+ function changeFolders(folders, op, dirs, home = os10.homedir()) {
7934
+ const real = (d) => {
7935
+ try {
7936
+ return realpathSync6.native(d);
7937
+ } catch {
7938
+ return path28.resolve(d);
7939
+ }
7940
+ };
7941
+ const next = [...folders];
7942
+ const said = [];
7943
+ for (const d of dirs) {
7944
+ const abs = path28.resolve(d);
7945
+ const at = next.findIndex((f) => real(f) === real(abs));
7946
+ if (op === "remove") {
7947
+ if (at < 0) said.push(` \xB7 ${abs} wasn't allowed`);
7948
+ else said.push(` \u2713 No longer allowed: ${next.splice(at, 1)[0]}`);
7949
+ continue;
7950
+ }
7951
+ let dir = false;
7952
+ try {
7953
+ dir = statSync10(abs).isDirectory();
7954
+ } catch {
7955
+ }
7956
+ if (!dir) said.push(` \u2717 ${abs} isn't a folder on this machine`);
7957
+ else if (isBroad(abs, home))
7958
+ said.push(
7959
+ ` \u2717 Not allowing all of ${abs}: name a project folder inside it, so a message can't run anywhere you own`
7960
+ );
7961
+ else if (at >= 0) said.push(` \xB7 ${abs} is already allowed`);
7962
+ else {
7963
+ next.push(abs);
7964
+ said.push(` \u2713 Allowed ${abs}`);
7965
+ }
7966
+ }
7967
+ return { folders: next, said };
7968
+ }
6768
7969
  async function controlMain(argv, out2, distDir2) {
6769
7970
  const flag2 = (n) => {
6770
7971
  const i = argv.indexOf(n);
@@ -6789,7 +7990,7 @@ ${CONTROL_HELP}`);
6789
7990
  const folders = flags("--folder");
6790
7991
  const existing = readControl();
6791
7992
  if (!folders.length && !existing?.folders.length) {
6792
- if (!isBroad(process.cwd(), os9.homedir())) folders.push(process.cwd());
7993
+ if (!isBroad(process.cwd(), os10.homedir())) folders.push(process.cwd());
6793
7994
  else
6794
7995
  out2(
6795
7996
  ` ! Not allowing all of ${process.cwd()}: sessions you message may run only in folders you name. Pass --folder <project>, or run this from inside one.`
@@ -6811,7 +8012,7 @@ ${CONTROL_HELP}`);
6811
8012
  },
6812
8013
  folders,
6813
8014
  mode,
6814
- name: flag2("--name") ?? existing?.name ?? cfg.machine ?? os9.hostname(),
8015
+ name: flag2("--name") ?? existing?.name ?? machineName(cfg, os10.hostname()),
6815
8016
  out: out2
6816
8017
  });
6817
8018
  const c = readControl();
@@ -6819,9 +8020,9 @@ ${CONTROL_HELP}`);
6819
8020
  out2(
6820
8021
  ` Mode: ${c.mode} (${c.mode === "safe" ? "nothing that needs approval runs unattended" : "Claude Code's auto mode"})`
6821
8022
  );
6822
- let cli = path25.join(distDir2, "cli.js");
8023
+ let cli = path28.join(distDir2, "cli.js");
6823
8024
  try {
6824
- cli = realpathSync5(cli);
8025
+ cli = realpathSync6(cli);
6825
8026
  } catch {
6826
8027
  }
6827
8028
  if (argv.includes("--no-service"))
@@ -6845,6 +8046,28 @@ ${MODE_TEXT}`);
6845
8046
  ` \u2713 Sessions you message now run in ${want} mode${want === "auto" ? " (Claude Code's auto mode decides what's safe)" : " (only what each folder's settings already allow)"}.`
6846
8047
  );
6847
8048
  }
8049
+ case "folders": {
8050
+ const op = argv[2];
8051
+ const usage = " usage: sessionpipe control folders [add|remove <folder>\u2026]";
8052
+ if (op !== void 0 && op !== "add" && op !== "remove") return out2(usage);
8053
+ const c = readControl();
8054
+ if (!c) return out2(" control isn't set up on this machine: run `sessionpipe control pair <receiver>` first");
8055
+ if (op) {
8056
+ const dirs = argv.slice(3);
8057
+ if (!dirs.length) return out2(usage);
8058
+ const r = changeFolders(c.folders, op, dirs);
8059
+ for (const l of r.said) out2(l);
8060
+ if (r.folders.join("\n") !== c.folders.join("\n")) {
8061
+ c.folders = r.folders;
8062
+ writeControl(c);
8063
+ out2(" The control daemon uses the new list within seconds.");
8064
+ }
8065
+ }
8066
+ return out2(
8067
+ c.folders.length ? ` Sessions you message may run in:
8068
+ ${c.folders.map((f) => ` ${f}`).join("\n")}` : " No folder is allowed: a message to a session here is refused until you add one."
8069
+ );
8070
+ }
6848
8071
  case "keys": {
6849
8072
  const c = readControl();
6850
8073
  if (argv[2] === "remove" && argv[3]) {
@@ -6890,7 +8113,7 @@ async function runDaemon(out2, distDir2) {
6890
8113
  const claudeNow = () => {
6891
8114
  const bin = findClaude2();
6892
8115
  if (!bin) return null;
6893
- const sig = `${bin}:${existsSync13(bin) ? realpathSync5(bin) : ""}`;
8116
+ const sig = `${bin}:${existsSync16(bin) ? realpathSync6(bin) : ""}`;
6894
8117
  if (claude?.sig !== sig) claude = { bin, caps: detectCaps2(bin), sig };
6895
8118
  return claude;
6896
8119
  };
@@ -6910,7 +8133,7 @@ async function runDaemon(out2, distDir2) {
6910
8133
  sdk,
6911
8134
  login: async (configDir) => {
6912
8135
  const c = claudeNow();
6913
- return c ? claudeLogin2(c.bin, configDir) : { loggedIn: null };
8136
+ return c ? claudeLogin3(c.bin, configDir) : { loggedIn: null };
6914
8137
  },
6915
8138
  token: (r) => r.token_in ? secretGet(r.token_in, controlAccount(r.machine)) : null,
6916
8139
  onRestart: () => updater?.restartWhenIdle() ?? false,
@@ -6925,12 +8148,12 @@ async function runDaemon(out2, distDir2) {
6925
8148
  log2(`${e.message}; this one exits`);
6926
8149
  process.exit(0);
6927
8150
  }
6928
- const drain = setInterval(() => {
6929
- const sinks = readConfig().sinks.filter((s) => s.token_in && !s.paused);
6930
- if (sinks.length) void flush(stateDir(), sinks, new Outbox(stateDir())).catch(() => {
8151
+ const drainTimer = setInterval(() => {
8152
+ const sinks = readConfig().sinks.filter((s) => !s.paused);
8153
+ if (sinks.length) void drain(stateDir(), sinks, new Outbox(stateDir())).catch(() => {
6931
8154
  });
6932
8155
  }, 6e4);
6933
- drain.unref();
8156
+ drainTimer.unref();
6934
8157
  watchFile(controlFile(), { interval: 5e3 }, () => {
6935
8158
  const next = readControl();
6936
8159
  if (next) d.reload(next);
@@ -6948,7 +8171,7 @@ async function runDaemon(out2, distDir2) {
6948
8171
  const restart = async (_from, to) => {
6949
8172
  stopping = true;
6950
8173
  updater?.stop();
6951
- clearInterval(drain);
8174
+ clearInterval(drainTimer);
6952
8175
  unwatchFile(controlFile());
6953
8176
  for (const sig of signals) process.off(sig, bye);
6954
8177
  await d.stop();
@@ -6957,9 +8180,9 @@ async function runDaemon(out2, distDir2) {
6957
8180
  log2(`exiting ${UPDATE_EXIT} so ${via === "supervisor" ? "the supervising sessionpipe" : via} starts ${to}`);
6958
8181
  process.exit(UPDATE_EXIT);
6959
8182
  }
6960
- let cli = path25.join(distDir2, "cli.js");
8183
+ let cli = path28.join(distDir2, "cli.js");
6961
8184
  try {
6962
- cli = realpathSync5(cli);
8185
+ cli = realpathSync6(cli);
6963
8186
  } catch {
6964
8187
  }
6965
8188
  log2(`no service manager restarts this daemon: pid ${process.pid} now runs ${to} as its child`);
@@ -7001,126 +8224,96 @@ function adoptSinkToken(url, token, tier, file) {
7001
8224
  return same ? "filled" : "added";
7002
8225
  }
7003
8226
 
7004
- // src/launcher.ts
7005
- import { accessSync as accessSync3, chmodSync as chmodSync4, constants as constants3, mkdirSync as mkdirSync12, readFileSync as readFileSync14, renameSync as renameSync5, rmSync, writeFileSync as writeFileSync10 } from "node:fs";
7006
- import os10 from "node:os";
7007
- import path26 from "node:path";
7008
- var LAUNCHER = "sessionpipe-hook";
7009
- var COPIED = ["worker.js", "hook.js"];
7010
- function dataDir(env = process.env) {
7011
- if (env.SESSIONPIPE_DATA) return path26.resolve(env.SESSIONPIPE_DATA);
7012
- return path26.join(env.XDG_DATA_HOME || path26.join(os10.homedir(), ".local", "share"), "sessionpipe");
7013
- }
7014
- var launcherPath = (env = process.env) => path26.join(dataDir(env), LAUNCHER);
7015
- var usesLauncher = (platform = process.platform) => platform !== "win32";
7016
- var sq = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
7017
- function launcherScript(node, hook, version) {
7018
- return `#!/bin/sh
7019
- # sessionpipe ${version} hook launcher, written by \`sessionpipe install\`. Harnesses run
7020
- # this; it runs hook.js beside it with the node sessionpipe was installed with, or with
7021
- # any node on PATH once that one is gone (an nvm uninstall, a Homebrew upgrade).
7022
- n=${sq(node)}
7023
- [ -x "$n" ] || n=$(command -v node) || { [ "$1" = antigravity ] && if [ "$2" = PreToolUse ]; then echo ${sq(AG_STALE_PRETOOL.trim())}; else echo '{}'; fi; exit 0; }
7024
- exec "$n" ${sq(hook)} "$@"
7025
- `;
7026
- }
7027
- function writeIfChanged(file, body, mode) {
7028
- try {
7029
- const cur = readFileSync14(file);
7030
- if (cur.equals(Buffer.isBuffer(body) ? body : Buffer.from(body))) {
7031
- chmodSync4(file, mode);
7032
- return false;
7033
- }
7034
- } catch {
7035
- }
7036
- const tmp = `${file}.${process.pid}.tmp`;
7037
- writeFileSync10(tmp, body, { mode });
7038
- renameSync5(tmp, file);
7039
- chmodSync4(file, mode);
7040
- return true;
7041
- }
7042
- function installLauncher(o) {
7043
- const dir = dataDir(o.env);
7044
- mkdirSync12(dir, { recursive: true, mode: 493 });
7045
- let changed = false;
7046
- for (const f of COPIED)
7047
- changed = writeIfChanged(path26.join(dir, f), readFileSync14(path26.join(o.distDir, f)), 420) || changed;
7048
- const launcher = path26.join(dir, LAUNCHER);
7049
- changed = writeIfChanged(launcher, launcherScript(o.node, path26.join(dir, "hook.js"), o.version), 493) || changed;
7050
- return { launcher, changed };
7051
- }
7052
- function removeLauncher(env = process.env) {
7053
- for (const f of [LAUNCHER, ...COPIED]) rmSync(path26.join(dataDir(env), f), { force: true });
8227
+ // src/help.ts
8228
+ function usageTable(harnesses) {
8229
+ return {
8230
+ connect: "connect <receiver> [--machine NAME] [--rename-machine] [--allow-downgrade] [--tier 0-3] [--mode safe|auto] [--folder DIR]\u2026 [--no-service]",
8231
+ install: `install [${harnesses.map((h) => `--${h}`).join(" ")}] [--machine NAME] [--rename-machine] [--allow-downgrade] [--backfill DAYS] [--sink URL --tier N]`,
8232
+ sink: "sink add|list|remove|test (use --help after a subcommand for its options)",
8233
+ "sink add": "sink add <url|file:PATH|stdout> [--tier 0-3] [--token T] [--secret S] [--pii] [--name N]",
8234
+ "sink list": "sink list",
8235
+ "sink remove": "sink remove <name>",
8236
+ "sink test": "sink test <name>",
8237
+ secrets: "secrets [move keychain|secret-service|file]",
8238
+ "secrets move": "secrets move keychain|secret-service|file",
8239
+ status: "status [--json]",
8240
+ doctor: "doctor [--json]",
8241
+ tail: "tail [--session ID] [--tier 0-3] [--harness NAME] [--all] [--json]",
8242
+ backfill: `backfill [--days 30] [${harnesses.map((h) => `--${h}`).join(" ")}]`,
8243
+ forget: "forget <harness> <session>",
8244
+ replay: "replay [--sink NAME] [--from-start]",
8245
+ update: "update [off|on] (install the latest now; off/on: the daemon's daily update)",
8246
+ "update off": "update off",
8247
+ "update on": "update on",
8248
+ control: "control pair|mode|status|keys|off|run (use --help after a subcommand for its options)",
8249
+ "control pair": "control pair <receiver> [--folder DIR]\u2026 [--mode safe|auto] [--tier 0-3] [--token T] [--name N] [--no-service]",
8250
+ "control mode": "control mode auto|safe",
8251
+ "control status": "control status",
8252
+ "control keys": "control keys [remove <id>]",
8253
+ "control keys remove": "control keys remove <id>",
8254
+ "control off": "control off [<receiver>]",
8255
+ "control run": "control run",
8256
+ wait: "wait [--session <harness>:<id>]",
8257
+ worker: "worker <job-file> (internal: process one queued job)",
8258
+ hook: "hook (internal: harnesses run dist/hook.js directly)",
8259
+ version: "version (print the installed version)",
8260
+ "--version": "--version (print the installed version)",
8261
+ "-v": "-v (print the installed version)"
8262
+ };
7054
8263
  }
7055
- function launcherState(version, env = process.env) {
7056
- const file = launcherPath(env);
7057
- let text = "";
7058
- try {
7059
- text = readFileSync14(file, "utf8");
7060
- } catch {
7061
- return {
7062
- file,
7063
- present: false,
7064
- node: null,
7065
- nodeOk: false,
7066
- version: null,
7067
- problems: ["the hook launcher is missing; run `sessionpipe install` again"]
7068
- };
7069
- }
7070
- const node = /^n='((?:[^']|'\\'')*)'$/m.exec(text)?.[1]?.replace(/'\\''/g, "'") ?? null;
7071
- const v = /^# sessionpipe (\S+) hook launcher/m.exec(text)?.[1] ?? null;
7072
- let nodeOk = false;
7073
- try {
7074
- if (node) {
7075
- accessSync3(node, constants3.X_OK);
7076
- nodeOk = true;
7077
- }
7078
- } catch {
8264
+ function commandHelp(args2, harnesses) {
8265
+ const usage = usageTable(harnesses);
8266
+ for (let n = Math.min(args2.length, 3); n > 0; n--) {
8267
+ const line = usage[args2.slice(0, n).join(" ")];
8268
+ if (line) return `Usage: sessionpipe ${line}
8269
+
8270
+ -h, --help Show help without running the command.`;
7079
8271
  }
7080
- const problems = [];
7081
- if (!nodeOk)
7082
- problems.push(
7083
- `the node the hook launcher names (${node ?? "?"}) is gone; it falls back to the node on PATH. \`sessionpipe install\` points it at this one.`
7084
- );
7085
- for (const f of COPIED)
7086
- try {
7087
- accessSync3(path26.join(path26.dirname(file), f), constants3.R_OK);
7088
- } catch {
7089
- problems.push(`${f} is missing beside the hook launcher; run \`sessionpipe install\` again`);
7090
- }
7091
- if (v && v !== version)
7092
- problems.push(
7093
- `the hooks run sessionpipe ${v}, this is ${version}; run \`sessionpipe install\` to bring them up to date`
7094
- );
7095
- return { file, present: true, node, nodeOk, version: v, problems };
8272
+ return void 0;
7096
8273
  }
7097
8274
 
7098
8275
  // src/cli.ts
7099
- process.stdout.on("error", (e) => {
7100
- if (e.code === "EPIPE") process.exit(0);
7101
- });
7102
- var args = process.argv.slice(2);
7103
- var cmd = args[0] ?? "help";
8276
+ var print = (s = "") => {
8277
+ process.stdout.write(`${s}
8278
+ `);
8279
+ };
8280
+ var args = [];
8281
+ var out = print;
8282
+ var state = "";
7104
8283
  var flag = (name) => {
7105
8284
  const i = args.indexOf(name);
7106
8285
  return i >= 0 ? args[i + 1] : void 0;
7107
8286
  };
7108
8287
  var has = (name) => args.includes(name);
7109
- var out = (s = "") => {
7110
- process.stdout.write(`${s}
8288
+ var distDir = path29.dirname(fileURLToPath(import.meta.url));
8289
+ async function main(argv, io = { out: print, err: (s) => void process.stderr.write(s), exit: (c) => process.exit(c) }) {
8290
+ args = argv;
8291
+ out = io.out;
8292
+ state = stateDir();
8293
+ try {
8294
+ await command(args[0] ?? "help");
8295
+ } catch (e) {
8296
+ io.err(`sessionpipe: ${e.message}
7111
8297
  `);
7112
- };
7113
- var state = stateDir();
7114
- var distDir = path27.dirname(fileURLToPath(import.meta.url));
8298
+ io.exit(1);
8299
+ }
8300
+ }
8301
+ function isEntrypoint(argv1, url) {
8302
+ try {
8303
+ return !!argv1 && realpathSync7(argv1) === fileURLToPath(url);
8304
+ } catch {
8305
+ return false;
8306
+ }
8307
+ }
7115
8308
  function hookCommand() {
7116
8309
  if (usesLauncher()) {
7117
8310
  const l = launcherPath();
7118
8311
  return (a) => ({ argv: [l, ...a], command: `${JSON.stringify(l)} ${a.join(" ")}` });
7119
8312
  }
7120
8313
  const node = stableNode(process.execPath);
7121
- let script = path27.join(distDir, "hook.js");
8314
+ let script = path29.join(distDir, "hook.js");
7122
8315
  try {
7123
- script = realpathSync6(script);
8316
+ script = realpathSync7(script);
7124
8317
  } catch {
7125
8318
  }
7126
8319
  return (a) => ({
@@ -7133,7 +8326,14 @@ function selectedAdapters() {
7133
8326
  if (named.length) return named;
7134
8327
  return ADAPTERS.filter((a) => a.detect());
7135
8328
  }
7136
- async function main() {
8329
+ async function command(cmd) {
8330
+ if (has("--help") || has("-h")) {
8331
+ const usage = commandHelp(
8332
+ args,
8333
+ ADAPTERS.map((a) => a.name)
8334
+ );
8335
+ return usage ? out(usage) : help();
8336
+ }
7137
8337
  switch (cmd) {
7138
8338
  case "connect":
7139
8339
  return connect();
@@ -7180,14 +8380,15 @@ async function main() {
7180
8380
  function help() {
7181
8381
  out(`sessionpipe ${VERSION} \u2014 an open protocol for what your coding agents are doing
7182
8382
 
7183
- sessionpipe connect <receiver> [--machine NAME] [--tier 0-3] [--mode safe|auto] [--folder DIR]\u2026
8383
+ Usage:
8384
+ sessionpipe connect <receiver> [--machine NAME] [--rename-machine] [--allow-downgrade] [--tier 0-3] [--mode safe|auto] [--folder DIR]\u2026
7184
8385
  (everything, in one go: hooks, a sink, signed messages, keys)
7185
- sessionpipe install [--claude-code --codex --gemini-cli --antigravity] [--machine NAME] [--backfill DAYS] [--sink URL --tier N]
7186
- sessionpipe uninstall [--keep-state]
8386
+ sessionpipe install [--claude-code --codex --gemini-cli --antigravity --devin] [--machine NAME] [--rename-machine] [--allow-downgrade] [--backfill DAYS] [--sink URL --tier N]
8387
+ sessionpipe uninstall [--claude-code --codex --gemini-cli --antigravity --devin | --all] [--keep-state]
7187
8388
  sessionpipe sink add <url|file:PATH|stdout> [--tier 0-3] [--token T] [--pii] [--name N]
7188
8389
  sessionpipe sink list | remove <name> | test <name>
7189
8390
  sessionpipe secrets [move keychain|secret-service|file] (where the machine's keys live)
7190
- sessionpipe status | doctor [--json]
8391
+ sessionpipe status [--json] | doctor [--json]
7191
8392
  sessionpipe tail [--session ID] [--tier N] [--harness NAME]
7192
8393
  sessionpipe backfill [--days 30] | forget <harness> <session> | replay --sink NAME
7193
8394
  sessionpipe update [off|on] (install the latest now; off/on: the daemon's daily update)
@@ -7195,17 +8396,41 @@ ${CONTROL_HELP}
7195
8396
 
7196
8397
  Docs: https://sessionpipe.org \xB7 nothing leaves this machine until you add a sink.`);
7197
8398
  }
7198
- async function install() {
7199
- if (viaNpx(distDir)) return rerunGlobally(args, out);
8399
+ function setupConfig() {
7200
8400
  const cfg = readConfig();
7201
- const machine = flag("--machine");
7202
- if (machine) cfg.machine = machine.slice(0, 80);
8401
+ const requested = flag("--machine");
8402
+ if ((has("--machine") || has("--rename-machine")) && (!requested || requested.startsWith("-")))
8403
+ throw new Error("--machine NAME is required when naming or renaming a machine");
8404
+ const machine = requested?.slice(0, 80);
8405
+ const control = readControl();
8406
+ const hooksName = existsSync17(configFile()) ? machineName(cfg, os11.hostname()) : void 0;
8407
+ const pairedName = control && hooksName && machineName({}, control.name) === hooksName ? hooksName : control?.name;
8408
+ const current = cfg.machine || pairedName || hooksName;
8409
+ if (current && machine && machine !== current && !has("--rename-machine")) {
8410
+ out(
8411
+ ` Keeping configured machine name ${current}; ignoring --machine ${machine}. Use --rename-machine with --machine NAME for an explicit rename.`
8412
+ );
8413
+ cfg.machine = current;
8414
+ } else {
8415
+ const name = machine || current || machineName(cfg, os11.hostname());
8416
+ if (control && control.name !== name && (machine || name === pairedName)) {
8417
+ writeControl({ ...control, name });
8418
+ out(` Renamed the control daemon ${control.name} \u2192 ${name}; its next hello tells each receiver.`);
8419
+ }
8420
+ cfg.machine = name;
8421
+ }
8422
+ return cfg;
8423
+ }
8424
+ async function install() {
8425
+ const cfg = setupConfig();
8426
+ if (viaNpx(distDir)) return rerunGlobally(args, out, { allowDowngrade: has("--allow-downgrade") });
8427
+ assertSetupVersion(has("--allow-downgrade"));
7203
8428
  const sinkUrl = flag("--sink");
7204
8429
  if (sinkUrl) await sinkAdd(sinkUrl, cfg);
7205
8430
  const adapters = selectedAdapters();
7206
8431
  if (!adapters.length) {
7207
8432
  out(
7208
- " No supported harness found on this machine (Claude Code, Codex, Gemini CLI, Antigravity). Pass --<harness> to force one."
8433
+ " No supported harness found on this machine (Claude Code, Codex, Gemini CLI, Antigravity, Devin). Pass --<harness> to force one."
7209
8434
  );
7210
8435
  return;
7211
8436
  }
@@ -7253,6 +8478,10 @@ function installHarnesses(adapters, cfg, lean) {
7253
8478
  cfg.harnesses[a.name] = { enabled: true, ...created.size ? { created: [...created] } : {} };
7254
8479
  if (a.name === "gemini-cli")
7255
8480
  out(" note: Gemini CLI's own telemetry defaults logPrompts on; that is Google's setting, not sessionpipe's.");
8481
+ if (a.name === "devin" && !reports.some((r) => r.skipped))
8482
+ out(
8483
+ " note: Devin ignores a hooks file whole if it holds one event name it doesn't know; run `/hooks` in `devin` to see what loaded."
8484
+ );
7256
8485
  if (a.name === "codex" && !reports.some((r) => r.skipped))
7257
8486
  out(
7258
8487
  " note: Codex runs non-managed hooks only after you approve them: start `codex` and accept the hook review. The notify fallback reports turn ends meanwhile."
@@ -7263,14 +8492,13 @@ async function connect() {
7263
8492
  const raw = args[1];
7264
8493
  if (!raw || raw.startsWith("-"))
7265
8494
  return out(
7266
- "usage: sessionpipe connect <receiver> [--machine NAME] [--tier 0-3] [--mode safe|auto] [--folder DIR]\u2026 [--no-service]"
8495
+ "usage: sessionpipe connect <receiver> [--machine NAME] [--rename-machine] [--allow-downgrade] [--tier 0-3] [--mode safe|auto] [--folder DIR]\u2026 [--no-service]"
7267
8496
  );
7268
- if (viaNpx(distDir)) return rerunGlobally(args, out);
8497
+ const cfg = setupConfig();
8498
+ if (viaNpx(distDir)) return rerunGlobally(args, out, { allowDowngrade: has("--allow-downgrade") });
8499
+ assertSetupVersion(has("--allow-downgrade"));
7269
8500
  const url = (/^https?:\/\//.test(raw) ? raw : `https://${raw}`).replace(/\/$/, "");
7270
8501
  const host = new URL(url).host;
7271
- const cfg = readConfig();
7272
- const machine = flag("--machine");
7273
- if (machine) cfg.machine = machine.slice(0, 80);
7274
8502
  const name = machineName(cfg, os11.hostname());
7275
8503
  const before = cfg.sinks.find((x) => x.url.replace(/\/$/, "") === url);
7276
8504
  const tier = connectTier(flag("--tier"), before?.tier);
@@ -7278,19 +8506,19 @@ async function connect() {
7278
8506
  const adapters = ADAPTERS.filter((a) => a.detect());
7279
8507
  if (!adapters.length)
7280
8508
  out(
7281
- " ! No coding agent here yet (Claude Code, Codex, Gemini CLI, Antigravity): run `sessionpipe install` once you have one."
8509
+ " ! No coding agent here yet (Claude Code, Codex, Gemini CLI, Antigravity, Devin): run `sessionpipe install` once you have one."
7282
8510
  );
7283
8511
  else installHarnesses(adapters, cfg, tier < 1);
7284
8512
  writeConfig(cfg);
7285
8513
  await reportClaudeAccounts();
7286
- const rows = collectRows(adapters, 30);
8514
+ const rows2 = collectRows(adapters, 30);
7287
8515
  const control = readControl();
7288
8516
  const paired = control?.receivers.find((r) => r.url === url);
7289
8517
  const sinkNow = readConfig().sinks.find((x) => x.url.replace(/\/$/, "") === url);
7290
8518
  if (paired && sinkNow && (sinkNow.token || sinkNow.token_in)) {
7291
8519
  out(` \u2713 Already paired with ${host} as ${paired.machine}: nothing to approve.`);
7292
8520
  } else {
7293
- const recent = rows.slice().sort((a, b) => b.row.last_at - a.row.last_at).map((r) => r.row.session.cwd ?? "");
8521
+ const recent = rows2.slice().sort((a, b) => b.row.last_at - a.row.last_at).map((r) => r.row.session.cwd ?? "");
7294
8522
  const choice = connectFolders({
7295
8523
  cwd: process.cwd(),
7296
8524
  home: os11.homedir(),
@@ -7299,7 +8527,7 @@ async function connect() {
7299
8527
  recent,
7300
8528
  exists: (d) => {
7301
8529
  try {
7302
- return statSync10(d).isDirectory();
8530
+ return statSync11(d).isDirectory();
7303
8531
  } catch {
7304
8532
  return false;
7305
8533
  }
@@ -7352,9 +8580,9 @@ async function connect() {
7352
8580
  out(` \u2713 Keys: in ${storeLabel(store)}${kept.length ? ` (left in the file: ${kept.join(", ")})` : ""}`);
7353
8581
  } else
7354
8582
  out(
7355
- ` Keys: in ${tilde(path27.dirname(configFile()))} at 0600 \u2014 no unlocked keychain in this session${process.env.SSH_CONNECTION ? " (an ssh shell)" : ""}`
8583
+ ` Keys: in ${tilde(path29.dirname(configFile()))} at 0600 \u2014 no unlocked keychain in this session${process.env.SSH_CONNECTION ? " (an ssh shell)" : ""}`
7356
8584
  );
7357
- if (rows.length) queueBackfill(rows, 30, readConfig());
8585
+ if (rows2.length) queueBackfill(rows2, 30, readConfig());
7358
8586
  out("");
7359
8587
  out(` Done: ${name} reports to ${host} and takes messages you sign on a device it trusts.`);
7360
8588
  out(" Run this again any time; it only fixes what's missing. `sessionpipe doctor` checks everything.");
@@ -7408,9 +8636,37 @@ function secrets() {
7408
8636
  if (!cfg.sinks.length && !readControl()?.receivers.length) out(" no keys on this machine");
7409
8637
  out(` best store from this session: ${storeLabel(bestStore())}`);
7410
8638
  }
8639
+ var AWAY_FROM_HOME = {
8640
+ "claude-code": "CLAUDE_CONFIG_DIR",
8641
+ codex: "CODEX_HOME",
8642
+ "gemini-cli": "GEMINI_CLI_HOME",
8643
+ antigravity: "GEMINI_CLI_HOME",
8644
+ devin: "XDG_CONFIG_HOME"
8645
+ };
8646
+ function unisolated() {
8647
+ if (!process.env.SESSIONPIPE_CONFIG && !process.env.SESSIONPIPE_STATE) return [];
8648
+ return ADAPTERS.filter((a) => {
8649
+ const v = AWAY_FROM_HOME[a.name];
8650
+ return v === null || typeof v === "string" && !process.env[v];
8651
+ }).map((a) => a.name);
8652
+ }
7411
8653
  function uninstall() {
8654
+ const named = ADAPTERS.filter((a) => has(`--${a.name}`));
8655
+ const all = !named.length;
8656
+ if (all && !has("--all")) {
8657
+ const exposed = unisolated();
8658
+ if (exposed.length) {
8659
+ out(
8660
+ ` ! Refusing an uninstall of every harness: SESSIONPIPE_CONFIG/SESSIONPIPE_STATE point away from this machine, but ${exposed.join(", ")} still resolve from ${tilde(os11.homedir())}.`
8661
+ );
8662
+ out(` Nothing was changed. Name what to remove (${exposed.map((n) => `--${n}`).join(" ")}),`);
8663
+ out(" pass --all if you do mean every harness on this machine, or unset SESSIONPIPE_CONFIG/SESSIONPIPE_STATE.");
8664
+ process.exitCode = 2;
8665
+ return;
8666
+ }
8667
+ }
7412
8668
  const cfg = readConfig();
7413
- for (const a of ADAPTERS) {
8669
+ for (const a of all ? ADAPTERS : named) {
7414
8670
  const created = cfg.harnesses[a.name]?.created ?? [];
7415
8671
  for (const r of a.uninstall(process.env, { created }))
7416
8672
  if (r.changed) out(` \u2713 ${a.name}: ${r.note ?? `hooks removed from ${tilde(r.file)}`}`);
@@ -7418,7 +8674,11 @@ function uninstall() {
7418
8674
  delete cfg.harnesses[a.name];
7419
8675
  }
7420
8676
  writeConfig(cfg);
7421
- if (usesLauncher()) removeLauncher();
8677
+ if (all && usesLauncher()) removeLauncher();
8678
+ if (!all) {
8679
+ out(` Still installed: ${Object.keys(cfg.harnesses).join(", ") || "nothing"}.`);
8680
+ return;
8681
+ }
7422
8682
  if (!has("--keep-state"))
7423
8683
  out(
7424
8684
  ` Outbox and state kept under ${tilde(state)} (delete it yourself, or pass nothing: it prunes after ${readConfig().keep_days ?? 30} days).`
@@ -7518,23 +8778,23 @@ var TIER_TEXT = {
7518
8778
  };
7519
8779
  function collectRows(adapters, days) {
7520
8780
  const since = Date.now() - days * 864e5;
7521
- const rows = [];
8781
+ const rows2 = [];
7522
8782
  for (const a of adapters) {
7523
8783
  try {
7524
- for (const row of a.backfill(since)) rows.push({ harness: a.name, row });
8784
+ for (const row of a.backfill(since)) rows2.push({ harness: a.name, row });
7525
8785
  } catch {
7526
8786
  }
7527
8787
  }
7528
- return rows;
8788
+ return rows2;
7529
8789
  }
7530
8790
  function backfillRows(adapters, days, cfg) {
7531
8791
  queueBackfill(collectRows(adapters, days), days, cfg);
7532
8792
  }
7533
- function queueBackfill(rows, days, cfg) {
8793
+ function queueBackfill(rows2, days, cfg) {
7534
8794
  const outbox = new Outbox(state);
7535
8795
  let n = 0;
7536
8796
  const machine = machineName(cfg, os11.hostname());
7537
- for (const { harness, row } of rows) {
8797
+ for (const { harness, row } of rows2) {
7538
8798
  const ref = { harness, session: row.session.id };
7539
8799
  const facts = factsState(state).get(harness, row.session.id);
7540
8800
  if (facts.backfilled) continue;
@@ -7560,7 +8820,7 @@ function queueBackfill(rows, days, cfg) {
7560
8820
  n++;
7561
8821
  }
7562
8822
  out(` \u2713 Backfill: ${n} session${n === 1 ? "" : "s"} from the last ${days} days queued as session.backfill`);
7563
- void flush(state, cfg.sinks, outbox);
8823
+ void drain(state, cfg.sinks, outbox);
7564
8824
  }
7565
8825
  function backfill() {
7566
8826
  backfillRows(selectedAdapters(), Number(flag("--days") ?? 30), readConfig());
@@ -7594,21 +8854,65 @@ async function replay() {
7594
8854
  for (const s of sinks) {
7595
8855
  if (has("--from-start")) new Cursors(state, s.name).write({});
7596
8856
  }
7597
- const d = await flush(state, sinks, new Outbox(state));
8857
+ const d = await flush(state, sinks, new Outbox(state), void 0, { force: true });
7598
8858
  for (const [k, v] of Object.entries(d)) out(` ${k}: ${v} event${v === 1 ? "" : "s"} delivered`);
7599
8859
  }
7600
- function status() {
8860
+ async function controlHealth() {
8861
+ if (!pairedCount()) return null;
8862
+ const reply = await ask(socketPath(state), { op: "status" }, 1500);
8863
+ if (reply?.op !== "status") return { running: false, receivers: [] };
8864
+ const status2 = reply.status;
8865
+ return {
8866
+ running: true,
8867
+ receivers: status2.receivers.map((r) => ({
8868
+ url: r.url,
8869
+ polling: r.polling,
8870
+ last_poll: r.last_poll,
8871
+ last_error: r.last_error,
8872
+ // An older daemon has no age field. Its last_poll still exposes a loop
8873
+ // that stopped long ago, and polling=false catches the terminal 401.
8874
+ last_poll_age_ms: r.last_poll_age_ms ?? (r.last_poll === null ? null : Math.max(0, Date.now() - r.last_poll))
8875
+ }))
8876
+ };
8877
+ }
8878
+ function controlLines(control) {
8879
+ if (!control) {
8880
+ out(" control: not paired");
8881
+ return;
8882
+ }
8883
+ if (!control.running) {
8884
+ out(" control: daemon not running or not responding; polling: no");
8885
+ return;
8886
+ }
8887
+ for (const r of control.receivers) {
8888
+ const age = r.last_poll_age_ms === null ? "unknown" : `${Math.floor(r.last_poll_age_ms / 6e4)} minutes`;
8889
+ out(
8890
+ ` control: ${r.url} \xB7 polling: ${r.polling ? "yes" : "no"} \xB7 last successful poll: ${r.last_poll === null ? `never (${age} since start)` : `${age} ago`} \xB7 last error: ${r.last_error ?? "none"}`
8891
+ );
8892
+ }
8893
+ }
8894
+ async function status() {
7601
8895
  const cfg = readConfig();
7602
8896
  const hc = hookCommand();
7603
- out(` machine: ${machineName(cfg, os11.hostname())} sessionpipe ${VERSION} state ${tilde(state)}`);
8897
+ const json2 = has("--json");
8898
+ const harnesses = {};
8899
+ const sinks = [];
8900
+ if (!json2) out(` machine: ${machineName(cfg, os11.hostname())} sessionpipe ${VERSION} state ${tilde(state)}`);
7604
8901
  for (const a of ADAPTERS) {
7605
8902
  if (!a.detect() && !cfg.harnesses[a.name]) continue;
7606
- for (const r of a.installed(hc, process.env, { lean: isLean(cfg) }))
7607
- out(
7608
- ` ${a.name.padEnd(12)} ${tilde(r.file).padEnd(44)} ${r.state}${r.state !== "current" && r.note ? ` \u2014 ${r.note}` : ""}`
7609
- );
8903
+ const installed = a.installed(hc, process.env, { lean: isLean(cfg) });
8904
+ harnesses[a.name] = installed.map((r) => ({
8905
+ file: tilde(r.file),
8906
+ state: r.state,
8907
+ ...r.note ? { note: r.note } : {}
8908
+ }));
8909
+ if (!json2)
8910
+ for (const r of installed)
8911
+ out(
8912
+ ` ${a.name.padEnd(12)} ${tilde(r.file).padEnd(44)} ${r.state}${r.state !== "current" && r.note ? ` \u2014 ${r.note}` : ""}`
8913
+ );
7610
8914
  }
7611
- if (!cfg.sinks.length) out(" sinks: none (events stay in the local outbox)");
8915
+ if (!json2 && !cfg.sinks.length) out(" sinks: none (events stay in the local outbox)");
7612
8916
  for (const s of cfg.sinks) {
7613
8917
  const c = new Cursors(state, s.name).read();
7614
8918
  const outbox = new Outbox(state);
@@ -7616,34 +8920,67 @@ function status() {
7616
8920
  for (const ref of outbox.sessions()) {
7617
8921
  const f = outbox.file(ref);
7618
8922
  try {
7619
- pending += Math.max(0, statSync10(f).size - (c[`${ref.harness}/${ref.session}`] ?? 0)) > 0 ? 1 : 0;
8923
+ pending += Math.max(0, statSync11(f).size - (c[`${ref.harness}/${ref.session}`] ?? 0)) > 0 ? 1 : 0;
7620
8924
  } catch {
7621
8925
  }
7622
8926
  }
7623
- out(
7624
- ` sink ${s.name.padEnd(16)} tier ${Math.min(s.tier, s.max_tier ?? 3)} ${s.paused ? `PAUSED ${s.paused}` : pending ? `${pending} session file(s) with undelivered events` : "up to date"}`
7625
- );
8927
+ sinks.push({
8928
+ name: s.name,
8929
+ url: s.url,
8930
+ tier: s.tier,
8931
+ max_tier: s.max_tier ?? null,
8932
+ pii: !!s.pii,
8933
+ control: !!s.control,
8934
+ paused: s.paused ?? null,
8935
+ pending_sessions: pending
8936
+ });
8937
+ if (!json2)
8938
+ out(
8939
+ ` sink ${s.name.padEnd(16)} tier ${Math.min(s.tier, s.max_tier ?? 3)} ${s.paused ? `PAUSED ${s.paused}` : pending ? `${pending} session file(s) with undelivered events` : "up to date"}`
8940
+ );
7626
8941
  }
7627
- out(` update: ${updateLine(updateState(cfg), Date.now())}`);
8942
+ const control = await controlHealth();
8943
+ if (!json2) controlLines(control);
8944
+ const update2 = updateState(cfg);
8945
+ if (!json2) out(` update: ${updateLine(update2, Date.now())}`);
7628
8946
  const t = timing();
7629
- if (t)
8947
+ if (!json2 && t)
7630
8948
  out(
7631
8949
  ` hook: in-process p50 ${t.p50} ms over ${t.n} runs (\`sessionpipe doctor\` measures the wall clock the harness waits)`
7632
8950
  );
7633
- const jobs = jobsDir(state);
8951
+ let jobs = 0;
7634
8952
  try {
7635
- const n = readdirSync8(jobs).length;
7636
- if (n) out(` jobs: ${n} waiting`);
8953
+ jobs = readdirSync8(jobsDir(state)).filter((n) => n.endsWith(".json")).length;
7637
8954
  } catch {
7638
8955
  }
8956
+ if (json2)
8957
+ out(
8958
+ JSON.stringify(
8959
+ {
8960
+ version: VERSION,
8961
+ machine: machineName(cfg, os11.hostname()),
8962
+ state,
8963
+ config: configFile(),
8964
+ harnesses,
8965
+ sinks,
8966
+ timing: t,
8967
+ control,
8968
+ update: update2,
8969
+ jobs
8970
+ },
8971
+ null,
8972
+ 2
8973
+ )
8974
+ );
8975
+ else if (jobs) out(` jobs: ${jobs} waiting`);
7639
8976
  }
7640
8977
  function hookWall(runs = 8) {
7641
- const hook = path27.join(distDir, "hook.js");
7642
- const tmp = mkdtempSync(path27.join(os11.tmpdir(), "sessionpipe-doctor-"));
8978
+ const hook = path29.join(distDir, "hook.js");
8979
+ const tmp = mkdtempSync(path29.join(os11.tmpdir(), "sessionpipe-doctor-"));
7643
8980
  const env = {
7644
8981
  ...process.env,
7645
8982
  SESSIONPIPE_STATE: tmp,
7646
- SESSIONPIPE_CONFIG: path27.join(tmp, "config.json"),
8983
+ SESSIONPIPE_CONFIG: path29.join(tmp, "config.json"),
7647
8984
  SESSIONPIPE_NO_WORKER: "1"
7648
8985
  };
7649
8986
  const input = JSON.stringify({
@@ -7663,18 +9000,24 @@ function hookWall(runs = 8) {
7663
9000
  } catch {
7664
9001
  return null;
7665
9002
  } finally {
7666
- rmSync2(tmp, { recursive: true, force: true });
9003
+ rmSync3(tmp, { recursive: true, force: true });
7667
9004
  }
7668
9005
  times.sort((a, b) => a - b);
7669
- const q = (p) => Math.round(times[Math.min(times.length - 1, Math.floor(p * times.length))] ?? 0);
9006
+ const q = (p) => Math.round(
9007
+ /* v8 ignore next */
9008
+ times[Math.min(times.length - 1, Math.floor(p * times.length))] ?? 0
9009
+ );
7670
9010
  return { n: times.length, p50: q(0.5), p95: q(0.95) };
7671
9011
  }
7672
9012
  function timing() {
7673
9013
  try {
7674
- const lines = readFileSync15(path27.join(state, "timing.jsonl"), "utf8").trim().split("\n").slice(-100);
9014
+ const lines = readFileSync16(path29.join(state, "timing.jsonl"), "utf8").trim().split("\n").slice(-100);
7675
9015
  const ms = lines.map((l) => JSON.parse(l).ms).filter((x) => x >= 0).sort((a, b) => a - b);
7676
9016
  if (!ms.length) return null;
7677
- const q = (p) => Math.round(ms[Math.min(ms.length - 1, Math.floor(p * ms.length))] ?? 0);
9017
+ const q = (p) => Math.round(
9018
+ /* v8 ignore next */
9019
+ ms[Math.min(ms.length - 1, Math.floor(p * ms.length))] ?? 0
9020
+ );
7678
9021
  return { n: ms.length, p50: q(0.5), p95: q(0.95) };
7679
9022
  } catch {
7680
9023
  return null;
@@ -7698,6 +9041,18 @@ async function doctor() {
7698
9041
  warnings: []
7699
9042
  };
7700
9043
  const warnings = report.warnings;
9044
+ const health = await controlHealth();
9045
+ report.control = health;
9046
+ if (health && !health.running)
9047
+ warnings.push("the control daemon is not running or not responding; polling is stopped");
9048
+ for (const r of health?.receivers ?? []) {
9049
+ if (!r.polling || r.last_poll_age_ms !== null && r.last_poll_age_ms >= 6e4) {
9050
+ const age = r.last_poll_age_ms === null ? "never polled" : `not polled for ${Math.floor(r.last_poll_age_ms / 6e4)} minutes`;
9051
+ warnings.push(
9052
+ `${r.url}: the daemon has ${age}: ${r.last_error ?? (r.polling ? "no successful poll response" : "polling stopped")}`
9053
+ );
9054
+ }
9055
+ }
7701
9056
  const upd = report.update;
7702
9057
  if (upd.failed)
7703
9058
  warnings.push(
@@ -7735,7 +9090,7 @@ async function doctor() {
7735
9090
  let linger = null;
7736
9091
  try {
7737
9092
  linger = /^Linger=yes/m.test(
7738
- execFileSync7("loginctl", ["show-user", os11.userInfo().username, "--property=Linger"], {
9093
+ execFileSync8("loginctl", ["show-user", os11.userInfo().username, "--property=Linger"], {
7739
9094
  encoding: "utf8",
7740
9095
  timeout: 5e3,
7741
9096
  stdio: ["ignore", "pipe", "ignore"]
@@ -7803,6 +9158,7 @@ async function doctor() {
7803
9158
  out(
7804
9159
  ` sink ${s.name.padEnd(16)} tier ${s.tier}${s.max_tier !== null ? ` (receiver max ${s.max_tier})` : ""}${s.paused ? ` PAUSED ${s.paused}` : ""}`
7805
9160
  );
9161
+ controlLines(health);
7806
9162
  const lr = report.launcher;
7807
9163
  if (lr)
7808
9164
  out(` hooks run ${lr.file} \u2192 node ${lr.node ? tilde(lr.node) : "?"}${lr.node_ok ? "" : " (gone: PATH's node)"}`);
@@ -7858,6 +9214,9 @@ async function tail() {
7858
9214
  case "turn.transcript":
7859
9215
  detail = `#${d.turn} "${String(d.user).slice(0, 50).replace(/\s+/g, " ")}"`;
7860
9216
  break;
9217
+ case "turn.progress":
9218
+ detail = `#${d.turn} ${String(d.assistant ?? "").length} chars so far`;
9219
+ break;
7861
9220
  case "session.backfill":
7862
9221
  detail = `${e.session.title ?? ""}`;
7863
9222
  break;
@@ -7869,7 +9228,7 @@ async function tail() {
7869
9228
  out(`sessionpipe tail \xB7 tier ${tier} \xB7 ${tilde(state)}/outbox (Ctrl-C to stop)`);
7870
9229
  for (const ref of outbox.sessions()) {
7871
9230
  try {
7872
- offsets.set(`${ref.harness}/${ref.session}`, has("--all") ? 0 : statSync10(outbox.file(ref)).size);
9231
+ offsets.set(`${ref.harness}/${ref.session}`, has("--all") ? 0 : statSync11(outbox.file(ref)).size);
7873
9232
  } catch {
7874
9233
  }
7875
9234
  }
@@ -7892,7 +9251,7 @@ async function tail() {
7892
9251
  async function workerFromArgv() {
7893
9252
  const f = args[1];
7894
9253
  if (!f) return;
7895
- const job = JSON.parse(readFileSync15(f, "utf8"));
9254
+ const job = JSON.parse(readFileSync16(f, "utf8"));
7896
9255
  try {
7897
9256
  unlinkSync7(f);
7898
9257
  } catch {
@@ -7954,9 +9313,14 @@ async function update() {
7954
9313
  ` The control daemon still runs ${running} and can't restart itself: ${process.platform === "darwin" ? `\`launchctl kickstart -k gui/${process.getuid?.() ?? "$(id -u)"}/org.sessionpipe.control\`` : process.platform === "linux" ? "`systemctl --user restart sessionpipe-control.service`" : "stop `sessionpipe control run` and start it again"} moves it (a run in progress stops).`
7955
9314
  );
7956
9315
  }
7957
- main().catch((e) => {
7958
- process.stderr.write(`sessionpipe: ${e.message}
7959
- `);
7960
- process.exit(1);
7961
- });
9316
+ if (isEntrypoint(process.argv[1], import.meta.url)) {
9317
+ process.stdout.on("error", (e) => {
9318
+ if (e.code === "EPIPE") process.exit(0);
9319
+ });
9320
+ void main(process.argv.slice(2));
9321
+ }
9322
+ export {
9323
+ isEntrypoint,
9324
+ main
9325
+ };
7962
9326
  //# sourceMappingURL=cli.js.map