sessionpipe 0.10.0 → 0.12.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.
Files changed (45) hide show
  1. package/README.md +74 -0
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +5165 -2399
  4. package/dist/cli.js.map +4 -4
  5. package/dist/control/cli.d.ts +4 -0
  6. package/dist/control/cli.d.ts.map +1 -1
  7. package/dist/control/daemon.d.ts +24 -0
  8. package/dist/control/daemon.d.ts.map +1 -1
  9. package/dist/control/local.d.ts +24 -0
  10. package/dist/control/local.d.ts.map +1 -1
  11. package/dist/control/pool.d.ts +49 -0
  12. package/dist/control/pool.d.ts.map +1 -0
  13. package/dist/control/route.d.ts +1 -1
  14. package/dist/control/route.d.ts.map +1 -1
  15. package/dist/help.d.ts.map +1 -1
  16. package/dist/hook.d.ts.map +1 -1
  17. package/dist/hook.js +38 -2
  18. package/dist/hook.js.map +2 -2
  19. package/dist/index.js +1135 -880
  20. package/dist/index.js.map +4 -4
  21. package/dist/pool/cli.d.ts +39 -0
  22. package/dist/pool/cli.d.ts.map +1 -0
  23. package/dist/pool/credentials.d.ts +44 -0
  24. package/dist/pool/credentials.d.ts.map +1 -0
  25. package/dist/pool/import.d.ts +23 -0
  26. package/dist/pool/import.d.ts.map +1 -0
  27. package/dist/pool/index.d.ts +138 -0
  28. package/dist/pool/index.d.ts.map +1 -0
  29. package/dist/pool/install.d.ts +46 -0
  30. package/dist/pool/install.d.ts.map +1 -0
  31. package/dist/pool/login.d.ts +27 -0
  32. package/dist/pool/login.d.ts.map +1 -0
  33. package/dist/pool/policy.d.ts +100 -0
  34. package/dist/pool/policy.d.ts.map +1 -0
  35. package/dist/pool/server.d.ts +76 -0
  36. package/dist/pool/server.d.ts.map +1 -0
  37. package/dist/pool/store.d.ts +89 -0
  38. package/dist/pool/store.d.ts.map +1 -0
  39. package/dist/pool/types.d.ts +92 -0
  40. package/dist/pool/types.d.ts.map +1 -0
  41. package/dist/run.d.ts +14 -1
  42. package/dist/run.d.ts.map +1 -1
  43. package/dist/worker.js +1143 -888
  44. package/dist/worker.js.map +4 -4
  45. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- // sessionpipe 0.10.0 · Apache-2.0 · https://sessionpipe.org
1
+ // sessionpipe 0.12.0 · Apache-2.0 · https://sessionpipe.org
2
2
 
3
3
  // src/http-sink.ts
4
4
  import { createHash, createHmac } from "node:crypto";
@@ -101,27 +101,14 @@ import {
101
101
  existsSync as existsSync10,
102
102
  mkdirSync as mkdirSync8,
103
103
  readdirSync as readdirSync6,
104
- readFileSync as readFileSync9,
104
+ readFileSync as readFileSync10,
105
105
  renameSync as renameSync3,
106
- statSync as statSync5,
106
+ statSync as statSync6,
107
107
  unlinkSync as unlinkSync4,
108
- writeFileSync as writeFileSync7
108
+ writeFileSync as writeFileSync8
109
109
  } from "node:fs";
110
110
  import os3 from "node:os";
111
- import path14 from "node:path";
112
-
113
- // ../core/src/adapters/antigravity.ts
114
- import {
115
- existsSync as existsSync2,
116
- mkdirSync as mkdirSync2,
117
- readdirSync as readdirSync2,
118
- readFileSync as readFileSync2,
119
- realpathSync,
120
- statSync as statSync2,
121
- unlinkSync as unlinkSync2,
122
- writeFileSync as writeFileSync2
123
- } from "node:fs";
124
- import path4 from "node:path";
111
+ import path15 from "node:path";
125
112
 
126
113
  // ../core/src/paths.ts
127
114
  import os from "node:os";
@@ -146,6 +133,13 @@ function tilde(p, home = homeDir()) {
146
133
  }
147
134
  var safeId = (s) => String(s).replace(/[^A-Za-z0-9_.-]/g, "_").slice(0, 160);
148
135
 
136
+ // ../core/src/adapters/claude-code.ts
137
+ import { execFileSync as execFileSync2 } from "node:child_process";
138
+ import { createHash as createHash2 } from "node:crypto";
139
+ import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
140
+ import { userInfo } from "node:os";
141
+ import path4 from "node:path";
142
+
149
143
  // ../core/src/readers/files.ts
150
144
  import { closeSync, fstatSync, openSync, readdirSync, readSync, statSync } from "node:fs";
151
145
  import path2 from "node:path";
@@ -236,6 +230,38 @@ function recentFiles(root, depth, sinceMs, match) {
236
230
  return out;
237
231
  }
238
232
 
233
+ // ../core/src/readers/git.ts
234
+ import { execFileSync } from "node:child_process";
235
+ var cache = /* @__PURE__ */ new Map();
236
+ function gitFacts(cwd) {
237
+ if (!cwd) return {};
238
+ const hit = cache.get(cwd);
239
+ if (hit) return hit;
240
+ const run = (args) => {
241
+ try {
242
+ return execFileSync("git", ["-C", cwd, ...args], {
243
+ encoding: "utf8",
244
+ timeout: 1500,
245
+ stdio: ["ignore", "pipe", "ignore"]
246
+ }).trim();
247
+ } catch {
248
+ return "";
249
+ }
250
+ };
251
+ const out = {};
252
+ const repo = run(["remote", "get-url", "origin"]);
253
+ if (repo) out.repo = stripCredentials(repo);
254
+ const branch = run(["rev-parse", "--abbrev-ref", "HEAD"]);
255
+ if (branch && branch !== "HEAD") out.branch = branch;
256
+ cache.set(cwd, out);
257
+ return out;
258
+ }
259
+ function stripCredentials(url) {
260
+ const ssh = /^(?:ssh:\/\/)?git@([^:/]+)[:/](.+?)(?:\.git)?$/.exec(url);
261
+ if (ssh) return `${ssh[1]}/${ssh[2]}`;
262
+ return url.replace(/\/\/[^/@]*@/, "//").replace(/^https?:\/\//, "").replace(/\.git$/, "");
263
+ }
264
+
239
265
  // ../core/src/adapters/codex-rollout.ts
240
266
  function codexSubagent(meta) {
241
267
  if (meta?.type !== "session_meta") return false;
@@ -395,8 +421,10 @@ function readJsonFile(file) {
395
421
  try {
396
422
  raw = readFileSync(file, "utf8");
397
423
  } catch {
398
- return { ok: true, existed: false, value: {}, raw: "", indent: " ", eol: "\n", finalNewline: true };
424
+ return { ok: true, existed: false, value: {}, raw: "", indent: " ", eol: "\n", finalNewline: true, bom: false };
399
425
  }
426
+ const bom = raw.charCodeAt(0) === 65279;
427
+ if (bom) raw = raw.slice(1);
400
428
  let value;
401
429
  try {
402
430
  value = JSON.parse(raw);
@@ -416,12 +444,13 @@ function readJsonFile(file) {
416
444
  raw,
417
445
  indent,
418
446
  eol,
419
- finalNewline: /\r?\n$/.test(raw)
447
+ finalNewline: /\r?\n$/.test(raw),
448
+ bom
420
449
  };
421
450
  }
422
451
  function formatJson(value, f) {
423
452
  const text = JSON.stringify(value, null, f.indent).replace(/\n/g, f.eol);
424
- return f.finalNewline ? text + f.eol : text;
453
+ return (f.bom ? "\uFEFF" : "") + (f.finalNewline ? text + f.eol : text);
425
454
  }
426
455
  function readJson(file) {
427
456
  const f = readJsonFile(file);
@@ -517,74 +546,79 @@ function installedClaudeShaped(file, harness, events, cmd, o) {
517
546
  return current === events.length ? "current" : "stale";
518
547
  }
519
548
 
520
- // ../core/src/adapters/antigravity.ts
521
- var NAME = "antigravity";
522
- var HOOK_NAME = "sessionpipe";
523
- var AG_EVENTS = ["PreInvocation", "PostInvocation", "PostToolUse", "Stop"];
524
- 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';
525
- var antigravityAnswer = (event) => event === "PreToolUse" ? AG_STALE_PRETOOL : "{}\n";
526
- var gemini = (env) => env.GEMINI_CLI_HOME || path4.join(homeDir(), ".gemini");
527
- var hooksFile = (env) => path4.join(gemini(env), "config", "hooks.json");
528
- var projectsDir = (env) => path4.join(gemini(env), "config", "projects");
529
- var dataDirs = (env) => ["antigravity", "antigravity-cli", "antigravity-ide"].map((d) => path4.join(gemini(env), d));
530
- var ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
531
- var validTool = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
532
- function entry(cmd) {
533
- const handler = (e) => ({ type: "command", command: cmd([NAME, e]).command, timeout: 5 });
534
- const out = { enabled: true };
535
- for (const e of AG_EVENTS) out[e] = /Tool/.test(e) ? [{ matcher: "*", hooks: [handler(e)] }] : [handler(e)];
536
- return out;
549
+ // ../core/src/adapters/claude-code.ts
550
+ var NAME = "claude-code";
551
+ var CLAUDE_EVENTS = [
552
+ "SessionStart",
553
+ "UserPromptSubmit",
554
+ "PreToolUse",
555
+ "PostToolUse",
556
+ "PostToolUseFailure",
557
+ "PermissionRequest",
558
+ "Notification",
559
+ "Stop",
560
+ "SubagentStart",
561
+ "SubagentStop",
562
+ "PreCompact",
563
+ "PostCompact",
564
+ "SessionEnd",
565
+ "StopFailure"
566
+ ];
567
+ var LEAN_SKIP = /* @__PURE__ */ new Set(["PreToolUse", "PostToolUseFailure"]);
568
+ var eventsFor = (lean) => lean ? CLAUDE_EVENTS.filter((e) => !LEAN_SKIP.has(e)) : [...CLAUDE_EVENTS];
569
+ var OPTS = {
570
+ matcherFor: (e) => /Tool|Permission/.test(e) ? "" : void 0,
571
+ timeout: 5,
572
+ // The hook waits up to 125 s for a verified control answer, and only when control
573
+ // is paired on this machine; everywhere else it exits in milliseconds as before.
574
+ timeoutFor: (e) => e === "PermissionRequest" ? 130 : void 0
575
+ };
576
+ var DESKTOP_ID = /^local_[A-Za-z0-9-]{1,64}$/;
577
+ function claudeDirs(env = process.env, home = env.HOME || homeDir()) {
578
+ const out = [];
579
+ const add = (d) => {
580
+ if (!d) return;
581
+ const r = path4.resolve(d);
582
+ if (!out.includes(r)) out.push(r);
583
+ };
584
+ add(path4.join(home, ".claude"));
585
+ add(env.CLAUDE_CONFIG_DIR);
586
+ try {
587
+ for (const e of readdirSync2(home, { withFileTypes: true })) {
588
+ if (!e.isDirectory() || !/^\.claude[-_.]/.test(e.name)) continue;
589
+ const d = path4.join(home, e.name);
590
+ if (existsSync2(path4.join(d, "projects")) || existsSync2(path4.join(d, "settings.json"))) add(d);
591
+ }
592
+ } catch {
593
+ }
594
+ return out.filter((d, i) => i === 0 || existsSync2(d));
537
595
  }
538
- var antigravity = {
596
+ var settingsIn = (dir) => path4.join(dir, "settings.json");
597
+ var configDirOf = (transcript) => path4.dirname(path4.dirname(path4.dirname(transcript)));
598
+ var validTool = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
599
+ var endReason = (r) => r === "clear" || r === "logout" || r === "exit" ? r : r === "prompt_input_exit" ? "exit" : "other";
600
+ var notifKind = (t) => t === "permission_prompt" ? "permission" : t === "idle_prompt" ? "idle" : t === "elicitation_dialog" ? "elicitation" : "question";
601
+ var claudeCode = {
539
602
  name: NAME,
540
- events: AG_EVENTS,
541
- detect: (env = process.env) => existsSync2(path4.join(gemini(env), "config")) || dataDirs(env).some((d) => existsSync2(d)),
542
- configFiles: (env = process.env) => [hooksFile(env)],
543
- install(cmd, env = process.env) {
544
- const file = hooksFile(env);
545
- const f = readJsonFile(file);
546
- if (!f.ok) return [{ file, changed: false, skipped: true, note: `${f.reason}; not touched` }];
547
- const hooks = f.value;
548
- const e = entry(cmd);
549
- if (JSON.stringify(hooks[HOOK_NAME]) === JSON.stringify(e)) return [{ file, changed: false }];
550
- hooks[HOOK_NAME] = e;
551
- mkdirSync2(path4.dirname(file), { recursive: true });
552
- writeFileSync2(file, formatJson(hooks, f));
553
- return [
554
- {
555
- file,
556
- changed: true,
557
- created: !f.existed,
558
- note: "hooks.json is read when a conversation starts; open ones report after a restart"
559
- }
560
- ];
561
- },
562
- uninstall(env = process.env, opts = {}) {
563
- const file = hooksFile(env);
564
- if (!existsSync2(file)) return [{ file, changed: false }];
565
- const f = readJsonFile(file);
566
- if (!f.ok) return [{ file, changed: false, skipped: true, note: `${f.reason}; not touched` }];
567
- const hooks = f.value;
568
- if (!(HOOK_NAME in hooks)) return [{ file, changed: false }];
569
- delete hooks[HOOK_NAME];
570
- if (opts.created?.includes(file) && !Object.keys(hooks).length) {
571
- unlinkSync2(file);
572
- return [{ file, changed: true, note: "removed (sessionpipe created it)" }];
573
- }
574
- writeFileSync2(file, formatJson(hooks, f));
575
- return [{ file, changed: true }];
576
- },
577
- installed(cmd, env = process.env) {
578
- const file = hooksFile(env);
579
- const f = readJsonFile(file);
580
- const have = f.ok ? f.value[HOOK_NAME] : void 0;
581
- return [
582
- {
583
- file,
584
- state: !have ? "missing" : JSON.stringify(have) === JSON.stringify(entry(cmd)) ? "current" : "stale"
585
- }
586
- ];
587
- },
603
+ events: CLAUDE_EVENTS,
604
+ detect: (env = process.env) => claudeDirs(env).some((d) => existsSync2(d)),
605
+ configFiles: (env = process.env) => claudeDirs(env).map(settingsIn),
606
+ install: (cmd, env = process.env, opts = {}) => claudeDirs(env).flatMap((d) => {
607
+ const hooks = installClaudeShaped(settingsIn(d), NAME, eventsFor(opts.lean), cmd, OPTS);
608
+ const line = opts.statusLine === void 0 ? null : statusLine(settingsIn(d), opts.statusLine, cmd);
609
+ return line ? [hooks, line] : [hooks];
610
+ }),
611
+ uninstall: (env = process.env, opts = {}) => claudeDirs(env).map((d) => {
612
+ const line = statusLine(settingsIn(d), false);
613
+ const hooks = uninstallClaudeShaped(settingsIn(d), CLAUDE_EVENTS, {
614
+ created: opts.created?.includes(settingsIn(d))
615
+ });
616
+ return line && !hooks.changed ? line : hooks;
617
+ }),
618
+ installed: (cmd, env = process.env, opts = {}) => claudeDirs(env).map((d) => ({
619
+ file: settingsIn(d),
620
+ state: installedClaudeShaped(settingsIn(d), NAME, eventsFor(opts.lean), cmd, OPTS)
621
+ })),
588
622
  fromHook(input) {
589
623
  const event = input.argv[1] ?? "";
590
624
  let s = {};
@@ -593,49 +627,157 @@ var antigravity = {
593
627
  if (j && typeof j === "object" && !Array.isArray(j)) s = j;
594
628
  } catch {
595
629
  }
596
- const id = s.conversationId ?? s.session_id ?? s.sessionId;
597
- const stdout = antigravityAnswer(event);
598
- if (!id) return { session: { id: "" }, events: [], stdout };
599
- const workspaces = agWorkspaces(s.workspacePaths);
600
- const session = { id: String(id), cwd: workspaces[0] ?? input.cwd };
601
- if (typeof s.modelName === "string" && s.modelName !== "auto") session.model = s.modelName;
602
- const toolCall = s.toolCall;
603
- const tool = validTool(toolCall?.name) ? toolCall?.name : validTool(s.toolName) ? s.toolName : void 0;
630
+ const id = s.session_id ?? s.sessionId;
631
+ if (!id) return null;
632
+ if (notClaudeCode(String(id), s)) return null;
633
+ const session = { id: String(id), cwd: typeof s.cwd === "string" ? s.cwd : input.cwd };
634
+ if (typeof s.model === "string") session.model = s.model;
635
+ if (typeof s.agent_id === "string" && event.startsWith("Subagent") === false && s.agent_id)
636
+ session.parent_id = null;
637
+ const turn_id = typeof s.prompt_id === "string" ? s.prompt_id : void 0;
638
+ const tool = validTool(s.tool_name) ? s.tool_name : void 0;
639
+ const call_id = typeof s.tool_use_id === "string" ? s.tool_use_id : void 0;
640
+ const ms = typeof s.duration_ms === "number" ? Math.round(s.duration_ms) : void 0;
641
+ const agent_id = typeof s.agent_id === "string" ? s.agent_id : void 0;
642
+ const permissionId = () => `perm-${createHash2("sha256").update(`${turn_id ?? ""}|${tool ?? ""}|${JSON.stringify(s.tool_input ?? null)}`).digest("hex").slice(0, 16)}`;
604
643
  const ev = (type, data) => ({ type, data: prune(data), harnessEvent: event });
605
644
  const events = [];
606
645
  switch (event) {
607
- case "PreInvocation":
608
- if (Number(s.invocationNum) === 0 || s.invocationNum === void 0) {
609
- events.push(ev("session.started", { source: "unknown" }));
610
- events.push(ev("turn.started", {}));
611
- } else events.push(ev("session.heartbeat", {}));
646
+ case "SessionStart":
647
+ events.push(ev("session.started", { source: pickSource(s.source ?? s.trigger) }));
612
648
  break;
613
- case "PostInvocation":
649
+ case "UserPromptSubmit": {
650
+ const prompt = typeof s.prompt === "string" ? s.prompt : void 0;
651
+ events.push(
652
+ ev("turn.started", {
653
+ turn_id,
654
+ prompt_chars: prompt === void 0 ? void 0 : [...prompt].length,
655
+ // Claude Code's hook schema documents `source`
656
+ // (user | sdk | system | loop_wakeup | schedule_wakeup | poll_event) and says a
657
+ // payload may omit it while the field rolls out; 2.1.304's payload has no such
658
+ // key at all. Forwarded verbatim, shape-guarded only: the list is the harness's
659
+ // to extend.
660
+ submitted_by: harnessWord(s.source),
661
+ // One of Claude Code's own prompt wrappers: the prompt is a block the harness
662
+ // wrote, so it is not the person's words. Absent is unknown, never "the person" —
663
+ // and `true` is never "no person caused this turn", because a Talk or
664
+ // `sessionpipe wait` delivery hands the person's own message over inside a
665
+ // <task-notification> (PROTOCOL.md §4).
666
+ injected: prompt !== void 0 && injectedBlock(prompt) ? true : void 0
667
+ })
668
+ );
614
669
  break;
670
+ }
615
671
  case "PreToolUse":
616
- if (tool) events.push(ev("tool.started", { tool, input: toolCall?.args }));
672
+ if (tool) events.push(ev("tool.started", { tool, call_id, turn_id, agent_id, input: s.tool_input }));
617
673
  break;
618
674
  case "PostToolUse":
619
- if (!tool) events.push(ev("session.heartbeat", {}));
620
- else
675
+ if (tool)
621
676
  events.push(
622
677
  ev("tool.ended", {
623
678
  tool,
624
- ok: !s.error,
625
- error: typeof s.error === "string" ? s.error.slice(0, 2e3) : void 0,
626
- input: toolCall?.args
679
+ call_id,
680
+ turn_id,
681
+ agent_id,
682
+ ms,
683
+ ok: true,
684
+ input: s.tool_input,
685
+ output: s.tool_response
686
+ })
687
+ );
688
+ break;
689
+ case "PostToolUseFailure":
690
+ if (tool)
691
+ events.push(
692
+ ev("tool.ended", {
693
+ tool,
694
+ call_id,
695
+ turn_id,
696
+ agent_id,
697
+ ms,
698
+ ok: false,
699
+ error: errText(s.error ?? s.tool_response),
700
+ input: s.tool_input,
701
+ output: s.tool_response
627
702
  })
628
703
  );
629
704
  break;
705
+ case "PermissionRequest":
706
+ events.push(
707
+ ev("attention.needed", {
708
+ attention_id: call_id ?? permissionId(),
709
+ kind: "permission",
710
+ tool,
711
+ message: tool ? `${tool}${describe(s.tool_input)}` : void 0
712
+ })
713
+ );
714
+ break;
715
+ case "Notification":
716
+ events.push(
717
+ ev("attention.needed", {
718
+ attention_id: `notif-${Date.now()}`,
719
+ kind: notifKind(s.notification_type),
720
+ message: typeof s.message === "string" ? s.message.slice(0, 200) : void 0
721
+ })
722
+ );
723
+ break;
630
724
  case "Stop":
631
- events.push(ev("turn.ended", { reason: s.error ? "error" : "stop" }));
725
+ events.push(ev("turn.ended", { turn_id, reason: "stop", background: backgroundCount(s.background_tasks) }));
632
726
  break;
633
- default:
634
- return { session, events: [], stdout };
635
- }
636
- const r = { session, events, stdout, hints: { workspaces } };
637
- if (typeof s.transcriptPath === "string") r.transcript = s.transcriptPath;
638
- return r;
727
+ case "StopFailure":
728
+ events.push(
729
+ ev("turn.ended", {
730
+ turn_id,
731
+ reason: "error",
732
+ error: typeof s.error === "string" ? s.error.slice(0, 200) : void 0
733
+ })
734
+ );
735
+ break;
736
+ case "SubagentStart":
737
+ events.push(
738
+ ev("subagent.started", {
739
+ agent_id: String(s.agent_id ?? ""),
740
+ agent_type: typeof s.agent_type === "string" ? s.agent_type : void 0
741
+ })
742
+ );
743
+ break;
744
+ case "SubagentStop":
745
+ events.push(
746
+ ev("subagent.ended", {
747
+ agent_id: String(s.agent_id ?? ""),
748
+ agent_type: typeof s.agent_type === "string" ? s.agent_type : void 0,
749
+ background: backgroundCount(s.background_tasks, String(s.agent_id ?? ""))
750
+ })
751
+ );
752
+ break;
753
+ case "PreCompact":
754
+ events.push(ev("context.compacted", { trigger: s.trigger === "manual" ? "manual" : "auto" }));
755
+ break;
756
+ case "PostCompact":
757
+ break;
758
+ // PreCompact already said it; a second event would double-count
759
+ case "SessionEnd":
760
+ events.push(ev("session.ended", { reason: endReason(s.reason) }));
761
+ break;
762
+ case "StatusLine": {
763
+ const limits = claudeLimits(s.rate_limits);
764
+ if (!limits) return null;
765
+ events.push(ev("limits.observed", limits));
766
+ break;
767
+ }
768
+ default:
769
+ return null;
770
+ }
771
+ const hints = {};
772
+ if (event === "UserPromptSubmit" && typeof s.prompt === "string") hints.prompt = s.prompt;
773
+ const host = input.env.CLAUDE_CODE_HOST_SESSION_ID;
774
+ if (host && DESKTOP_ID.test(host)) hints.desktop_id = host;
775
+ const attended = input.env.CLAUDE_CODE_SESSION_ATTENDED;
776
+ if (attended === "1" || attended === "0") hints.attended = attended === "1" || !!input.env.SESSIONPIPE_CONTROL_JOB;
777
+ if (event !== "PreToolUse" && event !== "PostToolUse") hints.claude_pid = claudePid(input.env);
778
+ const r = { session, events, hints };
779
+ if (typeof s.transcript_path === "string") r.transcript = s.transcript_path;
780
+ return r;
639
781
  },
640
782
  readTranscript(file, fromLine) {
641
783
  let text = "";
@@ -644,765 +786,811 @@ var antigravity = {
644
786
  } catch {
645
787
  return { turns: [], line: fromLine };
646
788
  }
647
- return readAntigravity(text.split("\n"), fromLine);
789
+ return readClaude(text.split("\n"), fromLine);
648
790
  },
649
- facts(session, transcript, hints) {
650
- if (!transcript) return {};
651
- return antigravityFacts(transcript, {
652
- conversation: session.id,
653
- workspaces: hints.workspaces ?? (session.cwd ? [session.cwd] : []),
654
- env: process.env
655
- });
791
+ facts(session, transcript, hints, state) {
792
+ const out = {};
793
+ if (transcript) Object.assign(out, claudeFacts(transcript, session.id, hints, state));
794
+ const cwd = out.cwd ?? session.cwd;
795
+ Object.assign(out, gitFacts(cwd), out.branch ? { branch: out.branch } : {});
796
+ if (!out.url && typeof hints.desktop_id === "string") out.url = `claude://code/continue/${hints.desktop_id}`;
797
+ if (typeof hints.attended === "boolean") out.attended = hints.attended || !!out.url;
798
+ return out;
656
799
  },
800
+ remoteFacts: (session, local, transcript, state, env = process.env) => claudeRemoteFacts(session.id, local, transcript, state, env),
657
801
  backfill(sinceMs, env = process.env) {
658
802
  const rows2 = [];
659
- for (const data of dataDirs(env)) {
660
- let ids = [];
661
- try {
662
- ids = readdirSync2(path4.join(data, "brain")).filter((n) => /^[0-9a-f-]{36}$/i.test(n));
663
- } catch {
664
- }
665
- for (const id of ids) {
666
- const f = path4.join(data, "brain", id, ".system_generated", "logs", "transcript.jsonl");
667
- try {
668
- if (statSync2(f).mtimeMs < sinceMs) continue;
669
- } catch {
670
- continue;
671
- }
672
- const facts = antigravityFacts(f, { conversation: id, workspaces: [], env });
673
- if (!facts.started_at || !facts.last_at) continue;
674
- const { started_at, last_at, first_ask: _fa, harness_version: _hv, ...rest } = facts;
675
- rows2.push({ session: { id, ...rest }, started_at, last_at });
676
- }
803
+ const files = [];
804
+ for (const d of claudeDirs(env))
805
+ files.push(...recentFiles(path4.join(d, "projects"), 1, sinceMs, (n) => /^[0-9a-f-]{36}\.jsonl$/.test(n)));
806
+ const noState = { get: () => ({}), save: () => {
807
+ } };
808
+ for (const f of files) {
809
+ const id = path4.basename(f, ".jsonl");
810
+ const facts = claudeFacts(f, id, {}, noState);
811
+ if (!facts.started_at || !facts.last_at) continue;
812
+ const { started_at, last_at, first_ask: _fa, harness_version: _hv, ...rest } = facts;
813
+ const cwd = rest.cwd;
814
+ rows2.push({
815
+ session: { id, ...rest, ...gitFacts(cwd), ...rest.branch ? { branch: rest.branch } : {} },
816
+ started_at,
817
+ last_at,
818
+ transcript: f
819
+ });
677
820
  }
678
821
  return rows2;
679
822
  }
680
823
  };
824
+ var WAITER_RE = /(?:^|[\s"'`/\\])sessionpipe(?:\.\w+)?["']?\s+wait(?:\s|$)/;
825
+ function backgroundCount(tasks, ending) {
826
+ if (!Array.isArray(tasks)) return void 0;
827
+ return tasks.filter((t) => {
828
+ const task = t;
829
+ if (ending !== void 0 && task?.id === ending) return false;
830
+ return !(typeof task?.command === "string" && WAITER_RE.test(task.command));
831
+ }).length;
832
+ }
833
+ var LINE_REFRESH_S = 60;
834
+ function statusLine(file, want, cmd) {
835
+ const f = readJsonFile(file);
836
+ if (!f.ok) return null;
837
+ const cur = f.value.statusLine;
838
+ const ours = typeof cur?.command === "string" && OURS.test(cur.command);
839
+ const command = cmd?.([NAME, "StatusLine"]).command;
840
+ if (want && cur && !ours)
841
+ return { file, changed: false, skipped: true, note: "a status line of your own is set; limits are not read" };
842
+ if (want ? ours && cur?.command === command && cur?.refreshInterval === LINE_REFRESH_S : !ours) return null;
843
+ if (want) f.value.statusLine = { type: "command", command, refreshInterval: LINE_REFRESH_S };
844
+ else delete f.value.statusLine;
845
+ writeFileSync2(file, formatJson(f.value, f));
846
+ return { file, changed: true, note: want ? "status line written (account limits)" : "status line removed" };
847
+ }
848
+ var epochIso = (t) => typeof t === "number" && t > 0 && t < 1e11 ? new Date(t * 1e3).toISOString() : void 0;
849
+ var WINDOW_MINUTES = { five_hour: 300, seven_day: 10080 };
850
+ function claudeLimits(rl) {
851
+ if (!rl || typeof rl !== "object" || Array.isArray(rl)) return null;
852
+ const windows = [];
853
+ for (const [id, w] of Object.entries(rl)) {
854
+ if (!/^[a-z0-9_]{1,64}$/.test(id) || typeof w?.used_percentage !== "number" || w.used_percentage < 0) continue;
855
+ windows.push(
856
+ prune({
857
+ id,
858
+ used_percent: w.used_percentage,
859
+ window_minutes: WINDOW_MINUTES[id],
860
+ resets_at: epochIso(w.resets_at)
861
+ })
862
+ );
863
+ }
864
+ return windows.length ? { windows: windows.slice(0, 8) } : null;
865
+ }
681
866
  function prune(o) {
682
867
  for (const k of Object.keys(o)) if (o[k] === void 0) delete o[k];
683
868
  return o;
684
869
  }
685
- function agPath(p) {
686
- if (typeof p !== "string" || !p) return null;
687
- let s = p;
688
- if (/^file:/i.test(s)) {
689
- try {
690
- s = decodeURIComponent(new URL(s).pathname);
691
- } catch {
692
- return null;
693
- }
694
- if (process.platform === "win32" && /^\/[A-Za-z]:/.test(s)) s = s.slice(1);
695
- } else if (/^[a-z][a-z0-9+.-]*:\/\//i.test(s)) return null;
696
- if (!path4.isAbsolute(s)) return null;
697
- return path4.resolve(s);
870
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
871
+ function notClaudeCode(id, s) {
872
+ return typeof s.transcript_path !== "string" && !UUID.test(id);
698
873
  }
699
- var agWorkspaces = (paths) => (Array.isArray(paths) ? paths : []).map(agPath).filter((p) => !!p).filter((p, i, a) => a.indexOf(p) === i);
700
- function agVariants(p) {
701
- const out = [p];
702
- try {
703
- const r = realpathSync.native(p);
704
- if (r !== p) out.push(r);
705
- } catch {
874
+ function pickSource(v) {
875
+ return typeof v === "string" && ["startup", "resume", "clear", "compact", "fork"].includes(v) ? v : "unknown";
876
+ }
877
+ var harnessWord = (v) => typeof v === "string" && /^[A-Za-z0-9][A-Za-z0-9_.-]{0,39}$/.test(v) ? v : void 0;
878
+ var WRAPPER = /^<(?:task-notification|cross-session-message|system-reminder|local-command-stdout|command-[a-z-]+)[\s/>]/i;
879
+ var injectedBlock = (prompt) => WRAPPER.test(prompt.trim());
880
+ function errText(v) {
881
+ if (typeof v === "string") return v.slice(0, 2e3);
882
+ if (v && typeof v === "object") {
883
+ const e = v;
884
+ if (typeof e.error === "string") return e.error.slice(0, 2e3);
885
+ if (typeof e.message === "string") return e.message.slice(0, 2e3);
706
886
  }
707
- return out;
887
+ return void 0;
708
888
  }
709
- function agProjects(dir) {
710
- const out = [];
889
+ function describe(input) {
890
+ if (!input || typeof input !== "object") return "";
891
+ const i = input;
892
+ const v = i.command ?? i.file_path ?? i.path ?? i.url ?? i.pattern;
893
+ return typeof v === "string" ? ` ${v.slice(0, 120)}` : "";
894
+ }
895
+ var NAME_RANK = {
896
+ user: "custom",
897
+ auto: "harness",
898
+ hook: "harness",
899
+ peer: "harness"
900
+ };
901
+ function registryEntry(j) {
902
+ if (!j || typeof j !== "object") return null;
903
+ const r = j;
904
+ const source = r.nameSource ? NAME_RANK[r.nameSource] : void 0;
905
+ return {
906
+ sessionId: r.sessionId ? String(r.sessionId) : null,
907
+ name: source && typeof r.name === "string" && r.name.trim() ? r.name.trim() : null,
908
+ name_source: source ?? null,
909
+ bridge: typeof r.bridgeSessionId === "string" ? r.bridgeSessionId : null
910
+ };
911
+ }
912
+ var liveCache = /* @__PURE__ */ new Map();
913
+ function claudeLive(dir) {
914
+ const hit = liveCache.get(dir);
915
+ if (hit) return hit;
916
+ const map = /* @__PURE__ */ new Map();
711
917
  let names = [];
712
918
  try {
713
- names = readdirSync2(dir).filter((n) => n.endsWith(".json"));
919
+ names = readdirSync2(path4.join(dir, "sessions")).filter((n) => n.endsWith(".json"));
714
920
  } catch {
715
- return out;
716
921
  }
717
922
  for (const n of names) {
718
- const id = n.slice(0, -5);
719
- if (!ID_RE.test(id)) continue;
720
- let j;
721
923
  try {
722
- j = JSON.parse(readFileSync2(path4.join(dir, n), "utf8"));
924
+ const e = registryEntry(JSON.parse(readFileSync2(path4.join(dir, "sessions", n), "utf8")));
925
+ if (e?.sessionId) map.set(e.sessionId, e);
723
926
  } catch {
724
- continue;
725
927
  }
726
- const folders = [];
727
- for (const r of j?.projectResources?.resources ?? []) {
728
- const f = agPath(r?.folderUri ?? r?.gitFolder?.folderUri);
729
- if (f) folders.push(...agVariants(f));
730
- }
731
- if (folders.length) out.push({ id, folders });
732
928
  }
733
- return out;
929
+ liveCache.set(dir, map);
930
+ return map;
734
931
  }
735
- function agMatchProject(projects, workspaces) {
736
- const ws = [];
737
- for (const w of workspaces) {
738
- const p = agPath(w);
739
- if (p) ws.push(...agVariants(p));
932
+ function claudeLiveByPid(dir, pid) {
933
+ if (!pid) return null;
934
+ try {
935
+ return registryEntry(JSON.parse(readFileSync2(path4.join(dir, "sessions", `${pid}.json`), "utf8")));
936
+ } catch {
937
+ return null;
740
938
  }
741
- let best = null;
742
- let bestLen = -1;
743
- for (const pr of projects)
744
- for (const f of pr.folders) {
745
- const within = ws.some((w) => w === f || w.startsWith(f.endsWith(path4.sep) ? f : f + path4.sep));
746
- if (within && f.length > bestLen) {
747
- best = pr.id;
748
- bestLen = f.length;
939
+ }
940
+ function claudePid(env = process.env) {
941
+ try {
942
+ const dirs = claudeDirs(env);
943
+ const has = (pid2) => dirs.some((d) => existsSync2(path4.join(d, "sessions", `${pid2}.json`)));
944
+ let pid = process.ppid;
945
+ if (has(pid)) return pid;
946
+ const parent = parentOf();
947
+ for (let hop = 0; hop < 6 && pid > 1; hop++) {
948
+ pid = parent(pid);
949
+ if (pid > 1 && has(pid)) return pid;
950
+ }
951
+ } catch {
952
+ }
953
+ return null;
954
+ }
955
+ function parentOf() {
956
+ if (process.platform === "linux") {
957
+ return (pid) => {
958
+ try {
959
+ const st = readFileSync2(`/proc/${pid}/stat`, "utf8");
960
+ return Number(st.slice(st.lastIndexOf(")") + 2).split(" ")[1]) || 0;
961
+ } catch {
962
+ return 0;
749
963
  }
964
+ };
965
+ }
966
+ const table = /* @__PURE__ */ new Map();
967
+ try {
968
+ const out = execFileSync2("ps", ["-axo", "pid=,ppid="], {
969
+ encoding: "utf8",
970
+ timeout: 2e3,
971
+ maxBuffer: 8 * 1024 * 1024,
972
+ stdio: ["ignore", "pipe", "ignore"]
973
+ });
974
+ for (const l of out.split("\n")) {
975
+ const m = /^\s*(\d+)\s+(\d+)/.exec(l);
976
+ if (m) table.set(Number(m[1]), Number(m[2]));
750
977
  }
751
- return best;
978
+ } catch {
979
+ }
980
+ return (pid) => table.get(pid) || 0;
752
981
  }
753
- function antigravityRemoteUrl(transcript, opts) {
754
- const i = transcript.indexOf(`${path4.sep}brain${path4.sep}`);
755
- const dirs = (i > 0 ? [transcript.slice(0, i)] : []).concat(dataDirs(opts.env));
756
- for (const d of dirs) {
982
+ var EMAIL_RE = /^[^@\s]{1,64}@[^@\s]{1,189}$/;
983
+ function claudeLogin(dir, home = homeDir()) {
984
+ const files = [path4.join(dir, ".claude.json")];
985
+ if (path4.resolve(dir) === path4.join(home, ".claude")) files.unshift(path4.join(home, ".claude.json"));
986
+ for (const f of files) {
757
987
  try {
758
- const m = /installation_uuid:\s*"([0-9a-f-]{36})"/i.exec(
759
- readFileSync2(path4.join(d, "antigravity_state.pbtxt"), "utf8")
760
- );
761
- if (!m) continue;
762
- const base = `https://antigravity.google.com/r/${m[1]}-v2`;
763
- const conv = opts.conversation ?? "";
764
- if (!ID_RE.test(conv) || !opts.workspaces?.length) return base;
765
- const project = agMatchProject(agProjects(opts.projectsDir ?? projectsDir(opts.env)), opts.workspaces);
766
- return project ? `${base}?p=${encodeURIComponent(`c/${conv}?section=${project}`)}` : base;
988
+ const a = JSON.parse(readFileSync2(f, "utf8")).oauthAccount;
989
+ if (typeof a?.accountUuid === "string" && a.accountUuid) {
990
+ const email = typeof a.emailAddress === "string" && EMAIL_RE.test(a.emailAddress) ? a.emailAddress : void 0;
991
+ return { id: a.accountUuid.slice(0, 128), ...email ? { email } : {} };
992
+ }
767
993
  } catch {
768
994
  }
769
995
  }
770
- return null;
996
+ return apiAccount(dir);
771
997
  }
772
- function antigravityFacts(file, opts) {
773
- const { head, tail } = ends(file, 128 * 1024, 64 * 1024);
774
- const H = parseLines(head);
775
- const T = parseLines(tail);
776
- const out = {};
777
- const url = antigravityRemoteUrl(file, opts);
778
- if (url) out.url = url;
779
- for (const j of H) {
780
- if (j.type !== "USER_INPUT" || typeof j.content !== "string") continue;
781
- const m = /<USER_REQUEST>([\s\S]*?)(?:<\/USER_REQUEST>|$)/.exec(j.content);
782
- const text = (m ? m[1] : j.content).trim();
783
- const line = (text.split("\n").find((l) => l.trim()) ?? "").trim();
784
- if (!line || /^<[a-z_-]+>/i.test(line)) continue;
785
- out.title = line.length > 80 ? `${line.slice(0, 80).replace(/\s+\S*$/, "")}\u2026` : line;
786
- out.title_source = "first-ask";
787
- break;
788
- }
789
- const times = H.concat(T).map((j) => Date.parse(String(j.created_at ?? j.timestamp ?? ""))).filter((n) => n > 0);
790
- if (times.length) {
791
- out.started_at = Math.min(...times);
792
- out.last_at = Math.max(...times);
793
- }
794
- return out;
795
- }
796
-
797
- // ../core/src/adapters/claude-code.ts
798
- import { execFileSync as execFileSync2 } from "node:child_process";
799
- import { createHash as createHash2 } from "node:crypto";
800
- import { existsSync as existsSync3, readdirSync as readdirSync3, readFileSync as readFileSync3, statSync as statSync3 } from "node:fs";
801
- import path5 from "node:path";
802
-
803
- // ../core/src/readers/git.ts
804
- import { execFileSync } from "node:child_process";
805
- var cache = /* @__PURE__ */ new Map();
806
- function gitFacts(cwd) {
807
- if (!cwd) return {};
808
- const hit = cache.get(cwd);
809
- if (hit) return hit;
810
- const run = (args) => {
811
- try {
812
- return execFileSync("git", ["-C", cwd, ...args], {
813
- encoding: "utf8",
814
- timeout: 1500,
815
- stdio: ["ignore", "pipe", "ignore"]
816
- }).trim();
817
- } catch {
818
- return "";
819
- }
820
- };
821
- const out = {};
822
- const repo = run(["remote", "get-url", "origin"]);
823
- if (repo) out.repo = stripCredentials(repo);
824
- const branch = run(["rev-parse", "--abbrev-ref", "HEAD"]);
825
- if (branch && branch !== "HEAD") out.branch = branch;
826
- cache.set(cwd, out);
827
- return out;
828
- }
829
- function stripCredentials(url) {
830
- const ssh = /^(?:ssh:\/\/)?git@([^:/]+)[:/](.+?)(?:\.git)?$/.exec(url);
831
- if (ssh) return `${ssh[1]}/${ssh[2]}`;
832
- return url.replace(/\/\/[^/@]*@/, "//").replace(/^https?:\/\//, "").replace(/\.git$/, "");
833
- }
834
-
835
- // ../core/src/adapters/claude-code.ts
836
- var NAME2 = "claude-code";
837
- var CLAUDE_EVENTS = [
838
- "SessionStart",
839
- "UserPromptSubmit",
840
- "PreToolUse",
841
- "PostToolUse",
842
- "PostToolUseFailure",
843
- "PermissionRequest",
844
- "Notification",
845
- "Stop",
846
- "SubagentStart",
847
- "SubagentStop",
848
- "PreCompact",
849
- "PostCompact",
850
- "SessionEnd",
851
- "StopFailure"
852
- ];
853
- var LEAN_SKIP = /* @__PURE__ */ new Set(["PreToolUse", "PostToolUseFailure"]);
854
- var eventsFor = (lean) => lean ? CLAUDE_EVENTS.filter((e) => !LEAN_SKIP.has(e)) : [...CLAUDE_EVENTS];
855
- var OPTS = {
856
- matcherFor: (e) => /Tool|Permission/.test(e) ? "" : void 0,
857
- timeout: 5,
858
- // The hook waits up to 125 s for a verified control answer, and only when control
859
- // is paired on this machine; everywhere else it exits in milliseconds as before.
860
- timeoutFor: (e) => e === "PermissionRequest" ? 130 : void 0
861
- };
862
- var DESKTOP_ID = /^local_[A-Za-z0-9-]{1,64}$/;
863
- function claudeDirs(env = process.env, home = env.HOME || homeDir()) {
864
- const out = [];
865
- const add = (d) => {
866
- if (!d) return;
867
- const r = path5.resolve(d);
868
- if (!out.includes(r)) out.push(r);
869
- };
870
- add(path5.join(home, ".claude"));
871
- add(env.CLAUDE_CONFIG_DIR);
998
+ var ANTHROPIC_HOST = /(^|\.)anthropic\.com$/i;
999
+ function apiAccount(dir) {
1000
+ const base = readJson(settingsIn(dir)).env?.ANTHROPIC_BASE_URL;
1001
+ if (typeof base !== "string") return null;
1002
+ let u;
872
1003
  try {
873
- for (const e of readdirSync3(home, { withFileTypes: true })) {
874
- if (!e.isDirectory() || !/^\.claude[-_.]/.test(e.name)) continue;
875
- const d = path5.join(home, e.name);
876
- if (existsSync3(path5.join(d, "projects")) || existsSync3(path5.join(d, "settings.json"))) add(d);
877
- }
1004
+ u = new URL(base.trim());
878
1005
  } catch {
1006
+ return null;
879
1007
  }
880
- return out.filter((d, i) => i === 0 || existsSync3(d));
1008
+ if (u.protocol !== "https:" && u.protocol !== "http:" || !u.hostname || ANTHROPIC_HOST.test(u.hostname))
1009
+ return null;
1010
+ const origin = `${u.protocol}//${u.host}`;
1011
+ return { id: `api-${createHash2("sha256").update(origin).digest("hex").slice(0, 16)}`, name: u.host };
881
1012
  }
882
- var settingsIn = (dir) => path5.join(dir, "settings.json");
883
- var configDirOf = (transcript) => path5.dirname(path5.dirname(path5.dirname(transcript)));
884
- var validTool2 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
885
- var endReason = (r) => r === "clear" || r === "logout" || r === "exit" ? r : r === "prompt_input_exit" ? "exit" : "other";
886
- var notifKind = (t) => t === "permission_prompt" ? "permission" : t === "idle_prompt" ? "idle" : t === "elicitation_dialog" ? "elicitation" : "question";
887
- var claudeCode = {
888
- name: NAME2,
889
- events: CLAUDE_EVENTS,
890
- detect: (env = process.env) => claudeDirs(env).some((d) => existsSync3(d)),
891
- configFiles: (env = process.env) => claudeDirs(env).map(settingsIn),
892
- install: (cmd, env = process.env, opts = {}) => claudeDirs(env).map((d) => installClaudeShaped(settingsIn(d), NAME2, eventsFor(opts.lean), cmd, OPTS)),
893
- uninstall: (env = process.env, opts = {}) => claudeDirs(env).map(
894
- (d) => uninstallClaudeShaped(settingsIn(d), CLAUDE_EVENTS, { created: opts.created?.includes(settingsIn(d)) })
895
- ),
896
- installed: (cmd, env = process.env, opts = {}) => claudeDirs(env).map((d) => ({
897
- file: settingsIn(d),
898
- state: installedClaudeShaped(settingsIn(d), NAME2, eventsFor(opts.lean), cmd, OPTS)
899
- })),
900
- fromHook(input) {
901
- const event = input.argv[1] ?? "";
902
- let s = {};
903
- try {
904
- const j = input.stdin ? JSON.parse(input.stdin) : {};
905
- if (j && typeof j === "object" && !Array.isArray(j)) s = j;
906
- } catch {
907
- }
908
- const id = s.session_id ?? s.sessionId;
909
- if (!id) return null;
910
- if (notClaudeCode(String(id), s)) return null;
911
- const session = { id: String(id), cwd: typeof s.cwd === "string" ? s.cwd : input.cwd };
912
- if (typeof s.model === "string") session.model = s.model;
913
- if (typeof s.agent_id === "string" && event.startsWith("Subagent") === false && s.agent_id)
914
- session.parent_id = null;
915
- const turn_id = typeof s.prompt_id === "string" ? s.prompt_id : void 0;
916
- const tool = validTool2(s.tool_name) ? s.tool_name : void 0;
917
- const call_id = typeof s.tool_use_id === "string" ? s.tool_use_id : void 0;
918
- const ms = typeof s.duration_ms === "number" ? Math.round(s.duration_ms) : void 0;
919
- const agent_id = typeof s.agent_id === "string" ? s.agent_id : void 0;
920
- const permissionId = () => `perm-${createHash2("sha256").update(`${turn_id ?? ""}|${tool ?? ""}|${JSON.stringify(s.tool_input ?? null)}`).digest("hex").slice(0, 16)}`;
921
- const ev = (type, data) => ({ type, data: prune2(data), harnessEvent: event });
922
- const events = [];
923
- switch (event) {
924
- case "SessionStart":
925
- events.push(ev("session.started", { source: pickSource(s.source ?? s.trigger) }));
926
- break;
927
- case "UserPromptSubmit": {
928
- const prompt = typeof s.prompt === "string" ? s.prompt : void 0;
929
- events.push(
930
- ev("turn.started", {
931
- turn_id,
932
- prompt_chars: prompt === void 0 ? void 0 : [...prompt].length,
933
- // Claude Code's hook schema documents `source`
934
- // (user | sdk | system | loop_wakeup | schedule_wakeup | poll_event) and says a
935
- // payload may omit it while the field rolls out; 2.1.304's payload has no such
936
- // key at all. Forwarded verbatim, shape-guarded only: the list is the harness's
937
- // to extend.
938
- submitted_by: harnessWord(s.source),
939
- // One of Claude Code's own prompt wrappers: the prompt is a block the harness
940
- // wrote, so it is not the person's words. Absent is unknown, never "the person" —
941
- // and `true` is never "no person caused this turn", because a Talk or
942
- // `sessionpipe wait` delivery hands the person's own message over inside a
943
- // <task-notification> (PROTOCOL.md §4).
944
- injected: prompt !== void 0 && injectedBlock(prompt) ? true : void 0
945
- })
946
- );
947
- break;
948
- }
949
- case "PreToolUse":
950
- if (tool) events.push(ev("tool.started", { tool, call_id, turn_id, agent_id, input: s.tool_input }));
951
- break;
952
- case "PostToolUse":
953
- if (tool)
954
- events.push(
955
- ev("tool.ended", {
956
- tool,
957
- call_id,
958
- turn_id,
959
- agent_id,
960
- ms,
961
- ok: true,
962
- input: s.tool_input,
963
- output: s.tool_response
964
- })
965
- );
966
- break;
967
- case "PostToolUseFailure":
968
- if (tool)
969
- events.push(
970
- ev("tool.ended", {
971
- tool,
972
- call_id,
973
- turn_id,
974
- agent_id,
975
- ms,
976
- ok: false,
977
- error: errText(s.error ?? s.tool_response),
978
- input: s.tool_input,
979
- output: s.tool_response
980
- })
981
- );
982
- break;
983
- case "PermissionRequest":
984
- events.push(
985
- ev("attention.needed", {
986
- attention_id: call_id ?? permissionId(),
987
- kind: "permission",
988
- tool,
989
- message: tool ? `${tool}${describe(s.tool_input)}` : void 0
990
- })
991
- );
992
- break;
993
- case "Notification":
994
- events.push(
995
- ev("attention.needed", {
996
- attention_id: `notif-${Date.now()}`,
997
- kind: notifKind(s.notification_type),
998
- message: typeof s.message === "string" ? s.message.slice(0, 200) : void 0
999
- })
1000
- );
1001
- break;
1002
- case "Stop":
1003
- events.push(
1004
- ev("turn.ended", {
1005
- turn_id,
1006
- reason: "stop",
1007
- background: Array.isArray(s.background_tasks) ? s.background_tasks.length : void 0
1008
- })
1009
- );
1010
- break;
1011
- case "StopFailure":
1012
- events.push(
1013
- ev("turn.ended", {
1014
- turn_id,
1015
- reason: "error",
1016
- error: typeof s.error === "string" ? s.error.slice(0, 200) : void 0
1017
- })
1018
- );
1019
- break;
1020
- case "SubagentStart":
1021
- events.push(
1022
- ev("subagent.started", {
1023
- agent_id: String(s.agent_id ?? ""),
1024
- agent_type: typeof s.agent_type === "string" ? s.agent_type : void 0
1025
- })
1026
- );
1027
- break;
1028
- case "SubagentStop":
1029
- events.push(
1030
- ev("subagent.ended", {
1031
- agent_id: String(s.agent_id ?? ""),
1032
- agent_type: typeof s.agent_type === "string" ? s.agent_type : void 0
1033
- })
1034
- );
1035
- break;
1036
- case "PreCompact":
1037
- events.push(ev("context.compacted", { trigger: s.trigger === "manual" ? "manual" : "auto" }));
1038
- break;
1039
- case "PostCompact":
1040
- break;
1041
- // PreCompact already said it; a second event would double-count
1042
- case "SessionEnd":
1043
- events.push(ev("session.ended", { reason: endReason(s.reason) }));
1044
- break;
1045
- default:
1046
- return null;
1047
- }
1048
- const hints = {};
1049
- if (event === "UserPromptSubmit" && typeof s.prompt === "string") hints.prompt = s.prompt;
1050
- const host = input.env.CLAUDE_CODE_HOST_SESSION_ID;
1051
- if (host && DESKTOP_ID.test(host)) hints.desktop_id = host;
1052
- const attended = input.env.CLAUDE_CODE_SESSION_ATTENDED;
1053
- if (attended === "1" || attended === "0") hints.attended = attended === "1" || !!input.env.SESSIONPIPE_CONTROL_JOB;
1054
- if (event !== "PreToolUse" && event !== "PostToolUse") hints.claude_pid = claudePid(input.env);
1055
- const r = { session, events, hints };
1056
- if (typeof s.transcript_path === "string") r.transcript = s.transcript_path;
1057
- return r;
1058
- },
1059
- readTranscript(file, fromLine) {
1060
- let text = "";
1061
- try {
1062
- text = readFileSync3(file, "utf8");
1063
- } catch {
1064
- return { turns: [], line: fromLine };
1065
- }
1066
- return readClaude(text.split("\n"), fromLine);
1067
- },
1068
- facts(session, transcript, hints, state) {
1069
- const out = {};
1070
- if (transcript) Object.assign(out, claudeFacts(transcript, session.id, hints, state));
1071
- const cwd = out.cwd ?? session.cwd;
1072
- Object.assign(out, gitFacts(cwd), out.branch ? { branch: out.branch } : {});
1073
- if (!out.url && typeof hints.desktop_id === "string") out.url = `claude://code/continue/${hints.desktop_id}`;
1074
- if (typeof hints.attended === "boolean") out.attended = hints.attended || !!out.url;
1075
- return out;
1076
- },
1077
- backfill(sinceMs, env = process.env) {
1078
- const rows2 = [];
1079
- const files = [];
1080
- for (const d of claudeDirs(env))
1081
- files.push(...recentFiles(path5.join(d, "projects"), 1, sinceMs, (n) => /^[0-9a-f-]{36}\.jsonl$/.test(n)));
1082
- const noState = { get: () => ({}), save: () => {
1083
- } };
1084
- for (const f of files) {
1085
- const id = path5.basename(f, ".jsonl");
1086
- const facts = claudeFacts(f, id, {}, noState);
1087
- if (!facts.started_at || !facts.last_at) continue;
1088
- const { started_at, last_at, first_ask: _fa, harness_version: _hv, ...rest } = facts;
1089
- const cwd = rest.cwd;
1090
- rows2.push({
1091
- session: { id, ...rest, ...gitFacts(cwd), ...rest.branch ? { branch: rest.branch } : {} },
1092
- started_at,
1093
- last_at
1094
- });
1013
+ var accountCache = /* @__PURE__ */ new Map();
1014
+ function claudeAccount(dir) {
1015
+ const hit = accountCache.get(dir);
1016
+ if (hit !== void 0) return hit;
1017
+ const acct = claudeLogin(dir)?.id ?? null;
1018
+ accountCache.set(dir, acct);
1019
+ return acct;
1020
+ }
1021
+ var LINK_RE = /^https:\/\/claude\.ai\/code\/session_[A-Za-z0-9]+$/;
1022
+ function transcriptLink(records) {
1023
+ for (let i = records.length - 1; i >= 0; i--) {
1024
+ const j = records[i];
1025
+ if (j.type === "system" && j.subtype === "bridge_status" && typeof j.url === "string" && LINK_RE.test(j.url))
1026
+ return j.url;
1027
+ if (j.type === "bridge-session" && typeof j.bridgeSessionId === "string") {
1028
+ const m = /^(?:cse|session)_([A-Za-z0-9]+)$/.exec(j.bridgeSessionId);
1029
+ if (m) return `https://claude.ai/code/session_${m[1]}`;
1095
1030
  }
1096
- return rows2;
1097
1031
  }
1098
- };
1099
- function prune2(o) {
1100
- for (const k of Object.keys(o)) if (o[k] === void 0) delete o[k];
1101
- return o;
1032
+ return null;
1102
1033
  }
1103
- var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1104
- function notClaudeCode(id, s) {
1105
- return typeof s.transcript_path !== "string" && !UUID.test(id);
1034
+ function appTitle(text) {
1035
+ const t = String(text).replace(/\s+/g, " ").trim();
1036
+ return t.length > 50 ? `${t.slice(0, 50)}...` : t;
1106
1037
  }
1107
- function pickSource(v) {
1108
- return typeof v === "string" && ["startup", "resume", "clear", "compact", "fork"].includes(v) ? v : "unknown";
1038
+ function askText(j) {
1039
+ if (!j || j.type !== "user" || j.isMeta || j.isSidechain) return null;
1040
+ const origin = j.origin;
1041
+ if (origin?.kind && origin.kind !== "human") return null;
1042
+ const c = j.message?.content;
1043
+ const text = (typeof c === "string" ? c : Array.isArray(c) ? c.filter((b) => b && b.type === "text" && typeof b.text === "string").map((b) => b.text).join("\n") : "").trim();
1044
+ if (!text || isInjected(text)) return null;
1045
+ return text;
1109
1046
  }
1110
- var harnessWord = (v) => typeof v === "string" && /^[A-Za-z0-9][A-Za-z0-9_.-]{0,39}$/.test(v) ? v : void 0;
1111
- var WRAPPER = /^<(?:task-notification|cross-session-message|system-reminder|local-command-stdout|command-[a-z-]+)[\s/>]/i;
1112
- var injectedBlock = (prompt) => WRAPPER.test(prompt.trim());
1113
- function errText(v) {
1114
- if (typeof v === "string") return v.slice(0, 2e3);
1115
- if (v && typeof v === "object") {
1116
- const e = v;
1117
- if (typeof e.error === "string") return e.error.slice(0, 2e3);
1118
- if (typeof e.message === "string") return e.message.slice(0, 2e3);
1047
+ function firstAsk(records) {
1048
+ for (const j of records) {
1049
+ const t = askText(j);
1050
+ if (t) return appTitle(t);
1119
1051
  }
1120
- return void 0;
1052
+ return null;
1121
1053
  }
1122
- function describe(input) {
1123
- if (!input || typeof input !== "object") return "";
1124
- const i = input;
1125
- const v = i.command ?? i.file_path ?? i.path ?? i.url ?? i.pattern;
1126
- return typeof v === "string" ? ` ${v.slice(0, 120)}` : "";
1054
+ function scanTitles(file, from, found) {
1055
+ const out = { custom: found.custom ?? null, ai: found.ai ?? null, upto: from };
1056
+ let size = 0;
1057
+ try {
1058
+ size = statSync2(file).size;
1059
+ } catch {
1060
+ return out;
1061
+ }
1062
+ if (size < from) {
1063
+ out.upto = 0;
1064
+ out.custom = out.ai = null;
1065
+ from = 0;
1066
+ }
1067
+ let pos = 0;
1068
+ scanLines(
1069
+ file,
1070
+ (l) => {
1071
+ pos += Buffer.byteLength(l, "utf8") + 1;
1072
+ if (pos <= from || l.indexOf('-title"') < 0) return null;
1073
+ try {
1074
+ const j = JSON.parse(l);
1075
+ if (j.type === "custom-title" && j.customTitle) out.custom = j.customTitle;
1076
+ if (j.type === "ai-title" && j.aiTitle) out.ai = j.aiTitle;
1077
+ } catch {
1078
+ }
1079
+ return null;
1080
+ },
1081
+ 512 * 1024 * 1024
1082
+ );
1083
+ out.upto = size;
1084
+ return out;
1127
1085
  }
1128
- var NAME_RANK = {
1129
- user: "custom",
1130
- auto: "harness",
1131
- hook: "harness",
1132
- peer: "harness"
1133
- };
1134
- function registryEntry(j) {
1135
- if (!j || typeof j !== "object") return null;
1136
- const r = j;
1137
- const source = r.nameSource ? NAME_RANK[r.nameSource] : void 0;
1138
- return {
1139
- sessionId: r.sessionId ? String(r.sessionId) : null,
1140
- name: source && typeof r.name === "string" && r.name.trim() ? r.name.trim() : null,
1141
- name_source: source ?? null,
1142
- bridge: typeof r.bridgeSessionId === "string" ? r.bridgeSessionId : null
1143
- };
1086
+ function claudeFacts(file, sessionId, hints, state) {
1087
+ const { head, tail } = ends(file, 64 * 1024, 256 * 1024);
1088
+ const H = parseLines(head);
1089
+ const T = parseLines(tail);
1090
+ const out = {};
1091
+ const id = sessionId || path4.basename(file, ".jsonl");
1092
+ const mem = state.get(NAME, id);
1093
+ let custom = null;
1094
+ let ai = null;
1095
+ for (const j of H.concat(T)) {
1096
+ if (j.type === "custom-title" && typeof j.customTitle === "string") custom = j.customTitle;
1097
+ if (j.type === "ai-title" && typeof j.aiTitle === "string") ai = j.aiTitle;
1098
+ }
1099
+ let size = 0;
1100
+ try {
1101
+ size = statSync2(file).size;
1102
+ } catch {
1103
+ }
1104
+ if (!custom && !ai && size > 64 * 1024 + 256 * 1024) {
1105
+ const s = scanTitles(file, Number(mem.scan_upto) || 0, {
1106
+ custom: mem.scan_custom,
1107
+ ai: mem.scan_ai
1108
+ });
1109
+ custom = s.custom;
1110
+ ai = s.ai;
1111
+ state.save(NAME, id, { scan_upto: s.upto, scan_custom: s.custom, scan_ai: s.ai });
1112
+ }
1113
+ const dir = configDirOf(file);
1114
+ const live = claudeLive(dir).get(id) ?? claudeLiveByPid(dir, hints.claude_pid);
1115
+ if (live?.name) state.save(NAME, id, { name: live.name, name_source: live.name_source });
1116
+ const bridge = live?.bridge && /^session_[A-Za-z0-9]+$/.test(live.bridge) ? `https://claude.ai/code/${live.bridge}` : null;
1117
+ const url = bridge ?? transcriptLink(T) ?? transcriptLink(H) ?? (typeof mem.url === "string" ? mem.url : null);
1118
+ if (url) {
1119
+ out.url = url;
1120
+ state.save(NAME, id, { url });
1121
+ }
1122
+ const name = live?.name ? { title: live.name, source: live.name_source } : typeof mem.name === "string" ? { title: mem.name, source: mem.name_source } : null;
1123
+ const pick = name && name.source === "custom" && name || custom && { title: custom, source: "custom" } || name || ai && { title: ai, source: "harness" };
1124
+ let ask2 = typeof mem.first_ask === "string" ? mem.first_ask : firstAsk(H);
1125
+ if (!ask2 && size > 64 * 1024 && !mem.first_ask_none) {
1126
+ ask2 = scanLines(file, (l) => {
1127
+ try {
1128
+ const t = askText(JSON.parse(l));
1129
+ return t ? appTitle(t) : null;
1130
+ } catch {
1131
+ return null;
1132
+ }
1133
+ });
1134
+ if (!ask2 && size > 16 * 1024 * 1024) state.save(NAME, id, { first_ask_none: true });
1135
+ }
1136
+ if (ask2) {
1137
+ out.first_ask = ask2;
1138
+ if (!mem.first_ask) state.save(NAME, id, { first_ask: ask2 });
1139
+ }
1140
+ if (pick) {
1141
+ out.title = String(pick.title).slice(0, 120);
1142
+ out.title_source = pick.source;
1143
+ } else if (ask2) {
1144
+ out.title = ask2;
1145
+ out.title_source = "first-ask";
1146
+ }
1147
+ const acct = claudeAccount(dir);
1148
+ if (acct) out.account_id = acct;
1149
+ for (let i = T.length - 1; i >= 0; i--) {
1150
+ const m = T[i].message?.model;
1151
+ if (T[i]?.type === "assistant" && m && m !== "<synthetic>") {
1152
+ out.model = m;
1153
+ break;
1154
+ }
1155
+ }
1156
+ const meta = [...T].reverse().find((j) => j.version) ?? H.find((j) => j.version);
1157
+ if (meta?.version) out.harness_version = String(meta.version);
1158
+ const times = H.concat(T).map((j) => Date.parse(String(j.timestamp ?? ""))).filter((n) => n > 0);
1159
+ if (times.length) {
1160
+ out.started_at = Math.min(...times);
1161
+ out.last_at = Math.max(...times);
1162
+ }
1163
+ const withCwd = H.find((j) => typeof j.cwd === "string");
1164
+ if (withCwd) {
1165
+ out.cwd = withCwd.cwd;
1166
+ if (typeof withCwd.gitBranch === "string" && withCwd.gitBranch !== "HEAD") out.branch = withCwd.gitBranch;
1167
+ }
1168
+ return out;
1144
1169
  }
1145
- var liveCache = /* @__PURE__ */ new Map();
1146
- function claudeLive(dir) {
1147
- const hit = liveCache.get(dir);
1148
- if (hit) return hit;
1149
- const map = /* @__PURE__ */ new Map();
1150
- let names = [];
1170
+ var REMOTE_RETRY_MS = 6e4;
1171
+ var REMOTE_REFRESH_MS = 30 * 6e4;
1172
+ var CLAUDE_API = "https://api.anthropic.com";
1173
+ var KEYCHAIN = "Claude Code-credentials";
1174
+ var REMOTE_TIMEOUT_MS = 3e3;
1175
+ var KEYCHAIN_TIMEOUT_MS = 2e3;
1176
+ var REMOTE_MAX_BYTES = 1 << 20;
1177
+ var cleanName = (s) => s.replace(/[\p{Cc}\u061C\u200E\u200F\u202A-\u202E\u2066-\u2069]/gu, " ").replace(/\s+/g, " ").trim();
1178
+ async function claudeRemoteFacts(id, local, transcript, state, env, now = Date.now()) {
1179
+ if (!transcript || !local.url || !LINK_RE.test(local.url) || local.title_source === "custom") return {};
1180
+ const mem = state.get(NAME, id);
1181
+ let title = typeof mem.remote_title === "string" ? mem.remote_title : null;
1182
+ if (now - (Number(mem.remote_at) || 0) >= (title ? REMOTE_REFRESH_MS : REMOTE_RETRY_MS)) {
1183
+ state.save(NAME, id, { remote_at: now });
1184
+ const token = claudeAiToken(configDirOf(transcript), env, now);
1185
+ const got = token ? await claudeAiTitle(local.url.slice(local.url.lastIndexOf("/") + 1), token) : void 0;
1186
+ if (got !== void 0) {
1187
+ title = got?.slice(0, 200) ?? null;
1188
+ state.save(NAME, id, { remote_title: title });
1189
+ }
1190
+ }
1191
+ return title ? { title: title.slice(0, 120), title_source: "harness" } : {};
1192
+ }
1193
+ function keychainServices(dir, env) {
1194
+ const named = (p) => `${KEYCHAIN}-${createHash2("sha256").update(p.normalize("NFC")).digest("hex").slice(0, 8)}`;
1195
+ const out = /* @__PURE__ */ new Set();
1196
+ if (env.CLAUDE_CONFIG_DIR && path4.resolve(env.CLAUDE_CONFIG_DIR) === dir) out.add(named(env.CLAUDE_CONFIG_DIR));
1197
+ if (dir === path4.join(env.HOME || homeDir(), ".claude")) out.add(KEYCHAIN);
1198
+ out.add(named(dir));
1199
+ return [...out];
1200
+ }
1201
+ var tokenCache = /* @__PURE__ */ new Map();
1202
+ function claudeAiToken(dir, env, now) {
1203
+ const hit = tokenCache.get(dir);
1204
+ if (hit && hit.expires > now + 6e4) return hit.token;
1205
+ const raw = [];
1206
+ if (process.platform === "darwin")
1207
+ for (const service of keychainServices(dir, env)) {
1208
+ try {
1209
+ const user = env.USER || userInfo().username;
1210
+ const account = /^[\w.-]+$/.test(user) ? user : "claude-code-user";
1211
+ raw.push(
1212
+ execFileSync2("security", ["find-generic-password", "-a", account, "-w", "-s", service], {
1213
+ encoding: "utf8",
1214
+ timeout: KEYCHAIN_TIMEOUT_MS,
1215
+ stdio: ["ignore", "pipe", "ignore"]
1216
+ })
1217
+ );
1218
+ break;
1219
+ } catch {
1220
+ }
1221
+ }
1151
1222
  try {
1152
- names = readdirSync3(path5.join(dir, "sessions")).filter((n) => n.endsWith(".json"));
1223
+ raw.push(readFileSync2(path4.join(dir, ".credentials.json"), "utf8"));
1153
1224
  } catch {
1154
1225
  }
1155
- for (const n of names) {
1226
+ for (const r of raw) {
1156
1227
  try {
1157
- const e = registryEntry(JSON.parse(readFileSync3(path5.join(dir, "sessions", n), "utf8")));
1158
- if (e?.sessionId) map.set(e.sessionId, e);
1228
+ const t = r.trim();
1229
+ const o = JSON.parse(/^(?:[0-9a-f]{2})+$/i.test(t) ? Buffer.from(t, "hex").toString("utf8") : t).claudeAiOauth;
1230
+ if (typeof o?.accessToken !== "string" || !o.accessToken) continue;
1231
+ const expires = typeof o.expiresAt === "number" ? o.expiresAt : Number.POSITIVE_INFINITY;
1232
+ if (expires <= now + 6e4) continue;
1233
+ tokenCache.set(dir, { token: o.accessToken, expires });
1234
+ return o.accessToken;
1159
1235
  } catch {
1160
1236
  }
1161
1237
  }
1162
- liveCache.set(dir, map);
1163
- return map;
1164
- }
1165
- function claudeLiveByPid(dir, pid) {
1166
- if (!pid) return null;
1167
- try {
1168
- return registryEntry(JSON.parse(readFileSync3(path5.join(dir, "sessions", `${pid}.json`), "utf8")));
1169
- } catch {
1170
- return null;
1171
- }
1238
+ return null;
1172
1239
  }
1173
- function claudePid(env = process.env) {
1240
+ async function claudeAiTitle(bridge, token) {
1174
1241
  try {
1175
- const dirs = claudeDirs(env);
1176
- const has = (pid2) => dirs.some((d) => existsSync3(path5.join(d, "sessions", `${pid2}.json`)));
1177
- let pid = process.ppid;
1178
- if (has(pid)) return pid;
1179
- const parent = parentOf();
1180
- for (let hop = 0; hop < 6 && pid > 1; hop++) {
1181
- pid = parent(pid);
1182
- if (pid > 1 && has(pid)) return pid;
1183
- }
1242
+ const r = await fetch(`${CLAUDE_API}/v1/code/sessions/${bridge}`, {
1243
+ headers: { Authorization: `Bearer ${token}`, "anthropic-version": "2023-06-01", "User-Agent": "sessionpipe" },
1244
+ redirect: "error",
1245
+ signal: AbortSignal.timeout(REMOTE_TIMEOUT_MS)
1246
+ });
1247
+ if (r.status !== 200 || Number(r.headers.get("content-length")) > REMOTE_MAX_BYTES) return void 0;
1248
+ const text = await r.text();
1249
+ if (text.length > REMOTE_MAX_BYTES) return void 0;
1250
+ const j = JSON.parse(text);
1251
+ const s = j?.response_shape ?? j?.session ?? j;
1252
+ const bare = (x) => x.replace(/^(?:session|cse)_/, "");
1253
+ if (typeof s?.id !== "string" || bare(s.id) !== bare(bridge)) return void 0;
1254
+ return typeof s.title === "string" && cleanName(s.title) || null;
1184
1255
  } catch {
1256
+ return void 0;
1185
1257
  }
1186
- return null;
1187
1258
  }
1188
- function parentOf() {
1189
- if (process.platform === "linux") {
1190
- return (pid) => {
1191
- try {
1192
- const st = readFileSync3(`/proc/${pid}/stat`, "utf8");
1193
- return Number(st.slice(st.lastIndexOf(")") + 2).split(" ")[1]) || 0;
1194
- } catch {
1195
- return 0;
1196
- }
1197
- };
1198
- }
1199
- const table = /* @__PURE__ */ new Map();
1259
+
1260
+ // ../core/src/adapters/headless.ts
1261
+ import { accessSync, constants, readFileSync as readFileSync3, statSync as statSync3 } from "node:fs";
1262
+ import path5 from "node:path";
1263
+ var JOB_TIMEOUT_MS = 30 * 6e4;
1264
+ var OUT_CAP = 1024 * 1024;
1265
+ function isExe(p) {
1200
1266
  try {
1201
- const out = execFileSync2("ps", ["-axo", "pid=,ppid="], {
1202
- encoding: "utf8",
1203
- timeout: 2e3,
1204
- maxBuffer: 8 * 1024 * 1024,
1205
- stdio: ["ignore", "pipe", "ignore"]
1206
- });
1207
- for (const l of out.split("\n")) {
1208
- const m = /^\s*(\d+)\s+(\d+)/.exec(l);
1209
- if (m) table.set(Number(m[1]), Number(m[2]));
1210
- }
1267
+ if (!statSync3(p).isFile()) return false;
1268
+ accessSync(p, constants.X_OK);
1269
+ return true;
1211
1270
  } catch {
1271
+ return false;
1212
1272
  }
1213
- return (pid) => table.get(pid) || 0;
1214
1273
  }
1215
- var EMAIL_RE = /^[^@\s]{1,64}@[^@\s]{1,189}$/;
1216
- function claudeLogin(dir, home = homeDir()) {
1217
- const files = [path5.join(dir, ".claude.json")];
1218
- if (path5.resolve(dir) === path5.join(home, ".claude")) files.unshift(path5.join(home, ".claude.json"));
1219
- for (const f of files) {
1220
- try {
1221
- const a = JSON.parse(readFileSync3(f, "utf8")).oauthAccount;
1222
- if (typeof a?.accountUuid === "string" && a.accountUuid) {
1223
- const email = typeof a.emailAddress === "string" && EMAIL_RE.test(a.emailAddress) ? a.emailAddress : void 0;
1224
- return { id: a.accountUuid.slice(0, 128), ...email ? { email } : {} };
1225
- }
1226
- } catch {
1227
- }
1274
+ function findBinary(o) {
1275
+ if (o.override && isExe(o.override)) return o.override;
1276
+ for (const d of String(o.env?.PATH || "").split(path5.delimiter)) {
1277
+ if (!d || !path5.isAbsolute(d)) continue;
1278
+ for (const n of o.names) if (isExe(path5.join(d, n))) return path5.join(d, n);
1228
1279
  }
1229
- return apiAccount(dir);
1280
+ for (const d of o.dirs) for (const n of o.names) if (isExe(path5.join(d, n))) return path5.join(d, n);
1281
+ return null;
1230
1282
  }
1231
- var ANTHROPIC_HOST = /(^|\.)anthropic\.com$/i;
1232
- function apiAccount(dir) {
1233
- let base;
1283
+ function shimTarget(shim) {
1284
+ let text = "";
1234
1285
  try {
1235
- base = JSON.parse(readFileSync3(settingsIn(dir), "utf8")).env?.ANTHROPIC_BASE_URL;
1286
+ text = readFileSync3(shim, "utf8");
1236
1287
  } catch {
1237
1288
  return null;
1238
1289
  }
1239
- if (typeof base !== "string") return null;
1240
- let u;
1290
+ const m = /"%(?:~dp0|dp0%)\\([^"]+)"\s+%\*/.exec(text);
1291
+ if (!m) return null;
1292
+ const target = path5.resolve(path5.dirname(shim), m[1].split("\\").join(path5.sep));
1241
1293
  try {
1242
- u = new URL(base.trim());
1294
+ return statSync3(target).isFile() ? target : null;
1243
1295
  } catch {
1244
1296
  return null;
1245
1297
  }
1246
- if (u.protocol !== "https:" && u.protocol !== "http:" || !u.hostname || ANTHROPIC_HOST.test(u.hostname))
1247
- return null;
1248
- const origin = `${u.protocol}//${u.host}`;
1249
- return { id: `api-${createHash2("sha256").update(origin).digest("hex").slice(0, 16)}`, name: u.host };
1250
1298
  }
1251
- var accountCache = /* @__PURE__ */ new Map();
1252
- function claudeAccount(dir) {
1253
- const hit = accountCache.get(dir);
1254
- if (hit !== void 0) return hit;
1255
- const acct = claudeLogin(dir)?.id ?? null;
1256
- accountCache.set(dir, acct);
1257
- return acct;
1299
+ function spawnable(bin, args) {
1300
+ if (!/\.(cmd|bat)$/i.test(bin)) return { file: bin, args };
1301
+ const target = shimTarget(bin);
1302
+ return target ? { file: process.execPath, args: [target, ...args] } : null;
1258
1303
  }
1259
- var LINK_RE = /^https:\/\/claude\.ai\/code\/session_[A-Za-z0-9]+$/;
1260
- function transcriptLink(records) {
1261
- for (let i = records.length - 1; i >= 0; i--) {
1262
- const j = records[i];
1263
- if (j.type === "system" && j.subtype === "bridge_status" && typeof j.url === "string" && LINK_RE.test(j.url))
1264
- return j.url;
1265
- if (j.type === "bridge-session" && typeof j.bridgeSessionId === "string") {
1266
- const m = /^(?:cse|session)_([A-Za-z0-9]+)$/.exec(j.bridgeSessionId);
1267
- if (m) return `https://claude.ai/code/session_${m[1]}`;
1304
+ function probe(bin, args) {
1305
+ const s = spawnable(bin, args);
1306
+ return s ? { ...s, shell: false } : { file: [`"${bin}"`, ...args].join(" "), args: [], shell: true };
1307
+ }
1308
+
1309
+ // ../core/src/adapters/antigravity.ts
1310
+ import {
1311
+ existsSync as existsSync3,
1312
+ mkdirSync as mkdirSync2,
1313
+ readdirSync as readdirSync3,
1314
+ readFileSync as readFileSync4,
1315
+ realpathSync,
1316
+ statSync as statSync4,
1317
+ unlinkSync as unlinkSync2,
1318
+ writeFileSync as writeFileSync3
1319
+ } from "node:fs";
1320
+ import path6 from "node:path";
1321
+ var NAME2 = "antigravity";
1322
+ var HOOK_NAME = "sessionpipe";
1323
+ var AG_EVENTS = ["PreInvocation", "PostInvocation", "PostToolUse", "Stop"];
1324
+ 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';
1325
+ var antigravityAnswer = (event) => event === "PreToolUse" ? AG_STALE_PRETOOL : "{}\n";
1326
+ var gemini = (env) => env.GEMINI_CLI_HOME || path6.join(homeDir(), ".gemini");
1327
+ var hooksFile = (env) => path6.join(gemini(env), "config", "hooks.json");
1328
+ var projectsDir = (env) => path6.join(gemini(env), "config", "projects");
1329
+ var dataDirs = (env) => ["antigravity", "antigravity-cli", "antigravity-ide"].map((d) => path6.join(gemini(env), d));
1330
+ var ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
1331
+ var validTool2 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
1332
+ function entry(cmd) {
1333
+ const handler = (e) => ({ type: "command", command: cmd([NAME2, e]).command, timeout: 5 });
1334
+ const out = { enabled: true };
1335
+ for (const e of AG_EVENTS) out[e] = /Tool/.test(e) ? [{ matcher: "*", hooks: [handler(e)] }] : [handler(e)];
1336
+ return out;
1337
+ }
1338
+ var antigravity = {
1339
+ name: NAME2,
1340
+ events: AG_EVENTS,
1341
+ detect: (env = process.env) => existsSync3(path6.join(gemini(env), "config")) || dataDirs(env).some((d) => existsSync3(d)),
1342
+ configFiles: (env = process.env) => [hooksFile(env)],
1343
+ install(cmd, env = process.env) {
1344
+ const file = hooksFile(env);
1345
+ const f = readJsonFile(file);
1346
+ if (!f.ok) return [{ file, changed: false, skipped: true, note: `${f.reason}; not touched` }];
1347
+ const hooks = f.value;
1348
+ const e = entry(cmd);
1349
+ if (JSON.stringify(hooks[HOOK_NAME]) === JSON.stringify(e)) return [{ file, changed: false }];
1350
+ hooks[HOOK_NAME] = e;
1351
+ mkdirSync2(path6.dirname(file), { recursive: true });
1352
+ writeFileSync3(file, formatJson(hooks, f));
1353
+ return [
1354
+ {
1355
+ file,
1356
+ changed: true,
1357
+ created: !f.existed,
1358
+ note: "hooks.json is read when a conversation starts; open ones report after a restart"
1359
+ }
1360
+ ];
1361
+ },
1362
+ uninstall(env = process.env, opts = {}) {
1363
+ const file = hooksFile(env);
1364
+ if (!existsSync3(file)) return [{ file, changed: false }];
1365
+ const f = readJsonFile(file);
1366
+ if (!f.ok) return [{ file, changed: false, skipped: true, note: `${f.reason}; not touched` }];
1367
+ const hooks = f.value;
1368
+ if (!(HOOK_NAME in hooks)) return [{ file, changed: false }];
1369
+ delete hooks[HOOK_NAME];
1370
+ if (opts.created?.includes(file) && !Object.keys(hooks).length) {
1371
+ unlinkSync2(file);
1372
+ return [{ file, changed: true, note: "removed (sessionpipe created it)" }];
1373
+ }
1374
+ writeFileSync3(file, formatJson(hooks, f));
1375
+ return [{ file, changed: true }];
1376
+ },
1377
+ installed(cmd, env = process.env) {
1378
+ const file = hooksFile(env);
1379
+ const f = readJsonFile(file);
1380
+ const have = f.ok ? f.value[HOOK_NAME] : void 0;
1381
+ return [
1382
+ {
1383
+ file,
1384
+ state: !have ? "missing" : JSON.stringify(have) === JSON.stringify(entry(cmd)) ? "current" : "stale"
1385
+ }
1386
+ ];
1387
+ },
1388
+ fromHook(input) {
1389
+ const event = input.argv[1] ?? "";
1390
+ let s = {};
1391
+ try {
1392
+ const j = input.stdin ? JSON.parse(input.stdin) : {};
1393
+ if (j && typeof j === "object" && !Array.isArray(j)) s = j;
1394
+ } catch {
1395
+ }
1396
+ const id = s.conversationId ?? s.session_id ?? s.sessionId;
1397
+ const stdout = antigravityAnswer(event);
1398
+ if (!id) return { session: { id: "" }, events: [], stdout };
1399
+ const workspaces = agWorkspaces(s.workspacePaths);
1400
+ const session = { id: String(id), cwd: workspaces[0] ?? input.cwd };
1401
+ if (typeof s.modelName === "string" && s.modelName !== "auto") session.model = s.modelName;
1402
+ const toolCall = s.toolCall;
1403
+ const tool = validTool2(toolCall?.name) ? toolCall?.name : validTool2(s.toolName) ? s.toolName : void 0;
1404
+ const ev = (type, data) => ({ type, data: prune2(data), harnessEvent: event });
1405
+ const events = [];
1406
+ switch (event) {
1407
+ case "PreInvocation":
1408
+ if (Number(s.invocationNum) === 0 || s.invocationNum === void 0) {
1409
+ events.push(ev("session.started", { source: "unknown" }));
1410
+ events.push(ev("turn.started", {}));
1411
+ } else events.push(ev("session.heartbeat", {}));
1412
+ break;
1413
+ case "PostInvocation":
1414
+ break;
1415
+ case "PreToolUse":
1416
+ if (tool) events.push(ev("tool.started", { tool, input: toolCall?.args }));
1417
+ break;
1418
+ case "PostToolUse":
1419
+ if (!tool) events.push(ev("session.heartbeat", {}));
1420
+ else
1421
+ events.push(
1422
+ ev("tool.ended", {
1423
+ tool,
1424
+ ok: !s.error,
1425
+ error: typeof s.error === "string" ? s.error.slice(0, 2e3) : void 0,
1426
+ input: toolCall?.args
1427
+ })
1428
+ );
1429
+ break;
1430
+ case "Stop":
1431
+ events.push(ev("turn.ended", { reason: s.error ? "error" : "stop" }));
1432
+ break;
1433
+ default:
1434
+ return { session, events: [], stdout };
1435
+ }
1436
+ const r = { session, events, stdout, hints: { workspaces } };
1437
+ if (typeof s.transcriptPath === "string") r.transcript = s.transcriptPath;
1438
+ return r;
1439
+ },
1440
+ readTranscript(file, fromLine) {
1441
+ let text = "";
1442
+ try {
1443
+ text = readFileSync4(file, "utf8");
1444
+ } catch {
1445
+ return { turns: [], line: fromLine };
1446
+ }
1447
+ return readAntigravity(text.split("\n"), fromLine);
1448
+ },
1449
+ facts(session, transcript, hints) {
1450
+ if (!transcript) return {};
1451
+ return antigravityFacts(transcript, {
1452
+ conversation: session.id,
1453
+ workspaces: hints.workspaces ?? (session.cwd ? [session.cwd] : []),
1454
+ env: process.env
1455
+ });
1456
+ },
1457
+ backfill(sinceMs, env = process.env) {
1458
+ const rows2 = [];
1459
+ for (const data of dataDirs(env)) {
1460
+ let ids = [];
1461
+ try {
1462
+ ids = readdirSync3(path6.join(data, "brain")).filter((n) => /^[0-9a-f-]{36}$/i.test(n));
1463
+ } catch {
1464
+ }
1465
+ for (const id of ids) {
1466
+ const f = path6.join(data, "brain", id, ".system_generated", "logs", "transcript.jsonl");
1467
+ try {
1468
+ if (statSync4(f).mtimeMs < sinceMs) continue;
1469
+ } catch {
1470
+ continue;
1471
+ }
1472
+ const facts = antigravityFacts(f, { conversation: id, workspaces: [], env });
1473
+ if (!facts.started_at || !facts.last_at) continue;
1474
+ const { started_at, last_at, first_ask: _fa, harness_version: _hv, ...rest } = facts;
1475
+ rows2.push({ session: { id, ...rest }, started_at, last_at });
1476
+ }
1268
1477
  }
1478
+ return rows2;
1269
1479
  }
1270
- return null;
1271
- }
1272
- function appTitle(text) {
1273
- const t = String(text).replace(/\s+/g, " ").trim();
1274
- return t.length > 50 ? `${t.slice(0, 50)}...` : t;
1275
- }
1276
- function askText(j) {
1277
- if (!j || j.type !== "user" || j.isMeta || j.isSidechain) return null;
1278
- const origin = j.origin;
1279
- if (origin?.kind && origin.kind !== "human") return null;
1280
- const c = j.message?.content;
1281
- const text = (typeof c === "string" ? c : Array.isArray(c) ? c.filter((b) => b && b.type === "text" && typeof b.text === "string").map((b) => b.text).join("\n") : "").trim();
1282
- if (!text || isInjected(text)) return null;
1283
- return text;
1480
+ };
1481
+ function prune2(o) {
1482
+ for (const k of Object.keys(o)) if (o[k] === void 0) delete o[k];
1483
+ return o;
1284
1484
  }
1285
- function firstAsk(records) {
1286
- for (const j of records) {
1287
- const t = askText(j);
1288
- if (t) return appTitle(t);
1289
- }
1290
- return null;
1485
+ function agPath(p) {
1486
+ if (typeof p !== "string" || !p) return null;
1487
+ let s = p;
1488
+ if (/^file:/i.test(s)) {
1489
+ try {
1490
+ s = decodeURIComponent(new URL(s).pathname);
1491
+ } catch {
1492
+ return null;
1493
+ }
1494
+ if (process.platform === "win32" && /^\/[A-Za-z]:/.test(s)) s = s.slice(1);
1495
+ } else if (/^[a-z][a-z0-9+.-]*:\/\//i.test(s)) return null;
1496
+ if (!path6.isAbsolute(s)) return null;
1497
+ return path6.resolve(s);
1291
1498
  }
1292
- function scanTitles(file, from, found) {
1293
- const out = { custom: found.custom ?? null, ai: found.ai ?? null, upto: from };
1294
- let size = 0;
1499
+ var agWorkspaces = (paths) => (Array.isArray(paths) ? paths : []).map(agPath).filter((p) => !!p).filter((p, i, a) => a.indexOf(p) === i);
1500
+ function agVariants(p) {
1501
+ const out = [p];
1295
1502
  try {
1296
- size = statSync3(file).size;
1503
+ const r = realpathSync.native(p);
1504
+ if (r !== p) out.push(r);
1297
1505
  } catch {
1298
- return out;
1299
- }
1300
- if (size < from) {
1301
- out.upto = 0;
1302
- out.custom = out.ai = null;
1303
- from = 0;
1304
1506
  }
1305
- let pos = 0;
1306
- scanLines(
1307
- file,
1308
- (l) => {
1309
- pos += Buffer.byteLength(l, "utf8") + 1;
1310
- if (pos <= from || l.indexOf('-title"') < 0) return null;
1311
- try {
1312
- const j = JSON.parse(l);
1313
- if (j.type === "custom-title" && j.customTitle) out.custom = j.customTitle;
1314
- if (j.type === "ai-title" && j.aiTitle) out.ai = j.aiTitle;
1315
- } catch {
1316
- }
1317
- return null;
1318
- },
1319
- 512 * 1024 * 1024
1320
- );
1321
- out.upto = size;
1322
1507
  return out;
1323
1508
  }
1324
- function claudeFacts(file, sessionId, hints, state) {
1325
- const { head, tail } = ends(file, 64 * 1024, 256 * 1024);
1326
- const H = parseLines(head);
1327
- const T = parseLines(tail);
1328
- const out = {};
1329
- const id = sessionId || path5.basename(file, ".jsonl");
1330
- const mem = state.get(NAME2, id);
1331
- let custom = null;
1332
- let ai = null;
1333
- for (const j of H.concat(T)) {
1334
- if (j.type === "custom-title" && typeof j.customTitle === "string") custom = j.customTitle;
1335
- if (j.type === "ai-title" && typeof j.aiTitle === "string") ai = j.aiTitle;
1336
- }
1337
- let size = 0;
1509
+ function agProjects(dir) {
1510
+ const out = [];
1511
+ let names = [];
1338
1512
  try {
1339
- size = statSync3(file).size;
1513
+ names = readdirSync3(dir).filter((n) => n.endsWith(".json"));
1340
1514
  } catch {
1515
+ return out;
1341
1516
  }
1342
- if (!custom && !ai && size > 64 * 1024 + 256 * 1024) {
1343
- const s = scanTitles(file, Number(mem.scan_upto) || 0, {
1344
- custom: mem.scan_custom,
1345
- ai: mem.scan_ai
1346
- });
1347
- custom = s.custom;
1348
- ai = s.ai;
1349
- state.save(NAME2, id, { scan_upto: s.upto, scan_custom: s.custom, scan_ai: s.ai });
1517
+ for (const n of names) {
1518
+ const id = n.slice(0, -5);
1519
+ if (!ID_RE.test(id)) continue;
1520
+ let j;
1521
+ try {
1522
+ j = JSON.parse(readFileSync4(path6.join(dir, n), "utf8"));
1523
+ } catch {
1524
+ continue;
1525
+ }
1526
+ const folders = [];
1527
+ for (const r of j?.projectResources?.resources ?? []) {
1528
+ const f = agPath(r?.folderUri ?? r?.gitFolder?.folderUri);
1529
+ if (f) folders.push(...agVariants(f));
1530
+ }
1531
+ if (folders.length) out.push({ id, folders });
1350
1532
  }
1351
- const dir = configDirOf(file);
1352
- const live = claudeLive(dir).get(id) ?? claudeLiveByPid(dir, hints.claude_pid);
1353
- if (live?.name) state.save(NAME2, id, { name: live.name, name_source: live.name_source });
1354
- const bridge = live?.bridge && /^session_[A-Za-z0-9]+$/.test(live.bridge) ? `https://claude.ai/code/${live.bridge}` : null;
1355
- const url = bridge ?? transcriptLink(T) ?? transcriptLink(H) ?? (typeof mem.url === "string" ? mem.url : null);
1356
- if (url) {
1357
- out.url = url;
1358
- state.save(NAME2, id, { url });
1533
+ return out;
1534
+ }
1535
+ function agMatchProject(projects, workspaces) {
1536
+ const ws = [];
1537
+ for (const w of workspaces) {
1538
+ const p = agPath(w);
1539
+ if (p) ws.push(...agVariants(p));
1359
1540
  }
1360
- const name = live?.name ? { title: live.name, source: live.name_source } : typeof mem.name === "string" ? { title: mem.name, source: mem.name_source } : null;
1361
- const pick = name && name.source === "custom" && name || custom && { title: custom, source: "custom" } || name || ai && { title: ai, source: "harness" };
1362
- let ask2 = typeof mem.first_ask === "string" ? mem.first_ask : firstAsk(H);
1363
- if (!ask2 && size > 64 * 1024 && !mem.first_ask_none) {
1364
- ask2 = scanLines(file, (l) => {
1365
- try {
1366
- const t = askText(JSON.parse(l));
1367
- return t ? appTitle(t) : null;
1368
- } catch {
1369
- return null;
1541
+ let best = null;
1542
+ let bestLen = -1;
1543
+ for (const pr of projects)
1544
+ for (const f of pr.folders) {
1545
+ const within = ws.some((w) => w === f || w.startsWith(f.endsWith(path6.sep) ? f : f + path6.sep));
1546
+ if (within && f.length > bestLen) {
1547
+ best = pr.id;
1548
+ bestLen = f.length;
1370
1549
  }
1371
- });
1372
- if (!ask2 && size > 16 * 1024 * 1024) state.save(NAME2, id, { first_ask_none: true });
1373
- }
1374
- if (ask2) {
1375
- out.first_ask = ask2;
1376
- if (!mem.first_ask) state.save(NAME2, id, { first_ask: ask2 });
1550
+ }
1551
+ return best;
1552
+ }
1553
+ function antigravityRemoteUrl(transcript, opts) {
1554
+ const i = transcript.indexOf(`${path6.sep}brain${path6.sep}`);
1555
+ const dirs = (i > 0 ? [transcript.slice(0, i)] : []).concat(dataDirs(opts.env));
1556
+ for (const d of dirs) {
1557
+ try {
1558
+ const m = /installation_uuid:\s*"([0-9a-f-]{36})"/i.exec(
1559
+ readFileSync4(path6.join(d, "antigravity_state.pbtxt"), "utf8")
1560
+ );
1561
+ if (!m) continue;
1562
+ const base = `https://antigravity.google.com/r/${m[1]}-v2`;
1563
+ const conv = opts.conversation ?? "";
1564
+ if (!ID_RE.test(conv) || !opts.workspaces?.length) return base;
1565
+ const project = agMatchProject(agProjects(opts.projectsDir ?? projectsDir(opts.env)), opts.workspaces);
1566
+ return project ? `${base}?p=${encodeURIComponent(`c/${conv}?section=${project}`)}` : base;
1567
+ } catch {
1568
+ }
1377
1569
  }
1378
- if (pick) {
1379
- out.title = String(pick.title).slice(0, 120);
1380
- out.title_source = pick.source;
1381
- } else if (ask2) {
1382
- out.title = ask2;
1570
+ return null;
1571
+ }
1572
+ function antigravityFacts(file, opts) {
1573
+ const { head, tail } = ends(file, 128 * 1024, 64 * 1024);
1574
+ const H = parseLines(head);
1575
+ const T = parseLines(tail);
1576
+ const out = {};
1577
+ const url = antigravityRemoteUrl(file, opts);
1578
+ if (url) out.url = url;
1579
+ for (const j of H) {
1580
+ if (j.type !== "USER_INPUT" || typeof j.content !== "string") continue;
1581
+ const m = /<USER_REQUEST>([\s\S]*?)(?:<\/USER_REQUEST>|$)/.exec(j.content);
1582
+ const text = (m ? m[1] : j.content).trim();
1583
+ const line = (text.split("\n").find((l) => l.trim()) ?? "").trim();
1584
+ if (!line || /^<[a-z_-]+>/i.test(line)) continue;
1585
+ out.title = line.length > 80 ? `${line.slice(0, 80).replace(/\s+\S*$/, "")}\u2026` : line;
1383
1586
  out.title_source = "first-ask";
1587
+ break;
1384
1588
  }
1385
- const acct = claudeAccount(dir);
1386
- if (acct) out.account_id = acct;
1387
- for (let i = T.length - 1; i >= 0; i--) {
1388
- const m = T[i].message?.model;
1389
- if (T[i]?.type === "assistant" && m && m !== "<synthetic>") {
1390
- out.model = m;
1391
- break;
1392
- }
1393
- }
1394
- const meta = [...T].reverse().find((j) => j.version) ?? H.find((j) => j.version);
1395
- if (meta?.version) out.harness_version = String(meta.version);
1396
- const times = H.concat(T).map((j) => Date.parse(String(j.timestamp ?? ""))).filter((n) => n > 0);
1589
+ const times = H.concat(T).map((j) => Date.parse(String(j.created_at ?? j.timestamp ?? ""))).filter((n) => n > 0);
1397
1590
  if (times.length) {
1398
1591
  out.started_at = Math.min(...times);
1399
1592
  out.last_at = Math.max(...times);
1400
1593
  }
1401
- const withCwd = H.find((j) => typeof j.cwd === "string");
1402
- if (withCwd) {
1403
- out.cwd = withCwd.cwd;
1404
- if (typeof withCwd.gitBranch === "string" && withCwd.gitBranch !== "HEAD") out.branch = withCwd.gitBranch;
1405
- }
1406
1594
  return out;
1407
1595
  }
1408
1596
 
@@ -1415,15 +1603,24 @@ var NONCE_TTL_MS = 25 * 36e5;
1415
1603
  var MODEL_RE = /^[A-Za-z0-9][A-Za-z0-9._:/@[\]-]*$/;
1416
1604
  var START_PERMISSIONS = ["read", "safe", "edits", "auto"];
1417
1605
  var levelRank = (l) => START_PERMISSIONS.indexOf(l);
1606
+ var FIELDS = {
1607
+ prompt: ["text", "files"],
1608
+ "permission.answer": ["for", "decision"],
1609
+ cancel: [],
1610
+ start: ["text", "cwd", "files", "permission", "model"],
1611
+ "key.add": ["key", "name"],
1612
+ pool: ["pool_action", "pool_account", "pool_order", "pool_code"]
1613
+ };
1614
+ var POOL_FIELDS = FIELDS.pool;
1418
1615
  var enc = new TextEncoder();
1419
1616
  var dec = new TextDecoder();
1420
1617
 
1421
1618
  // ../core/src/adapters/codex.ts
1422
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
1423
- import path6 from "node:path";
1619
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "node:fs";
1620
+ import path7 from "node:path";
1424
1621
 
1425
1622
  // ../core/src/adapters/codex-config.ts
1426
- var NOTIFY_LINE = /^[ \t]*notify[ \t]*=[ \t]*(\[.*\])[ \t]*(?:#.*)?$/m;
1623
+ var NOTIFY_LINE = /^[ \t]*notify[ \t]*=[ \t]*(\[.*\])[ \t]*(?:#.*)?(?=\r?$)/m;
1427
1624
  var NOTIFY_KEY = /^[ \t]*notify[ \t]*=/m;
1428
1625
  var OURS2 = /(?:hook\.js|sessionpipe-hook)",\s*"codex",\s*"notify"/;
1429
1626
  function scan(line, st) {
@@ -1513,10 +1710,11 @@ function dropLine(text, index, length) {
1513
1710
  }
1514
1711
  function insertTopLevel(text, line) {
1515
1712
  const { head, tail } = split(text);
1516
- if (!tail) return (head.trim() ? head.replace(/\s*$/, "\n") : "") + line + "\n";
1517
- return `${(head.trim() ? head.replace(/\s*$/, "\n") : "") + line}
1518
-
1519
- ${tail}`;
1713
+ const body = head.replace(/\s*$/, "");
1714
+ const eol = (body.includes("\n") ? body : text).includes("\r\n") ? "\r\n" : "\n";
1715
+ const top = head.trim() ? head.replace(/\s*$/, eol) : "";
1716
+ if (!tail) return top + line + eol;
1717
+ return `${top + line}${eol}${eol}${tail}`;
1520
1718
  }
1521
1719
  function withNotify(text, line) {
1522
1720
  const n = readNotify(text);
@@ -1565,25 +1763,25 @@ var CODEX_EVENTS = [
1565
1763
  ];
1566
1764
  var OPTS2 = { matcherFor: (e) => /Tool|Permission/.test(e) ? "" : void 0, timeout: 5 };
1567
1765
  var eventsFor2 = (lean) => lean ? CODEX_EVENTS.filter((e) => e !== "PreToolUse") : [...CODEX_EVENTS];
1568
- var codexHome = (env) => env.CODEX_HOME || path6.join(homeDir(), ".codex");
1569
- var hooksFile2 = (env) => path6.join(codexHome(env), "hooks.json");
1570
- var configFile2 = (env) => path6.join(codexHome(env), "config.toml");
1571
- var sessionsDir = (env) => path6.join(codexHome(env), "sessions");
1766
+ var codexHome = (env) => env.CODEX_HOME || path7.join(homeDir(), ".codex");
1767
+ var hooksFile2 = (env) => path7.join(codexHome(env), "hooks.json");
1768
+ var configFile2 = (env) => path7.join(codexHome(env), "config.toml");
1769
+ var sessionsDir = (env) => path7.join(codexHome(env), "sessions");
1572
1770
  var validTool3 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
1573
1771
  var endReason2 = (r) => r === "clear" || r === "logout" || r === "exit" ? r : "other";
1574
1772
  function installNotify(cmd, env) {
1575
1773
  const file = configFile2(env);
1576
1774
  let text = "";
1577
1775
  try {
1578
- text = readFileSync4(file, "utf8");
1776
+ text = readFileSync5(file, "utf8");
1579
1777
  } catch {
1580
1778
  }
1581
1779
  const argv = cmd([NAME3, "notify"]).argv;
1582
1780
  const line = `notify = ${JSON.stringify(argv)}`;
1583
1781
  const n = readNotify(text);
1584
1782
  const write = (next) => {
1585
- mkdirSync3(path6.dirname(file), { recursive: true });
1586
- writeFileSync3(file, next);
1783
+ mkdirSync3(path7.dirname(file), { recursive: true });
1784
+ writeFileSync4(file, next);
1587
1785
  };
1588
1786
  if (n && !n.other) {
1589
1787
  if (!n.misplaced && JSON.stringify(n.argv) === JSON.stringify(argv))
@@ -1621,19 +1819,19 @@ function uninstallNotify(env) {
1621
1819
  const file = configFile2(env);
1622
1820
  let text;
1623
1821
  try {
1624
- text = readFileSync4(file, "utf8");
1822
+ text = readFileSync5(file, "utf8");
1625
1823
  } catch {
1626
1824
  return { file, changed: false };
1627
1825
  }
1628
1826
  const n = readNotify(text);
1629
1827
  if (!n) return { file, changed: false };
1630
1828
  if (!n.other) {
1631
- writeFileSync3(file, withoutNotify(text));
1829
+ writeFileSync4(file, withoutNotify(text));
1632
1830
  return { file, changed: true };
1633
1831
  }
1634
1832
  const prev = previousNotify(n.argv);
1635
1833
  if (isComputerUse(n.argv) && prev && OURS2.test(JSON.stringify(prev))) {
1636
- writeFileSync3(file, replaceNotify(text, n, `notify = ${stripPreviousRaw(n.raw)}`));
1834
+ writeFileSync4(file, replaceNotify(text, n, `notify = ${stripPreviousRaw(n.raw)}`));
1637
1835
  return { file, changed: true };
1638
1836
  }
1639
1837
  return { file, changed: false };
@@ -1649,7 +1847,7 @@ function findRollout(threadId, env = process.env) {
1649
1847
  return;
1650
1848
  }
1651
1849
  for (const n of names.sort().reverse()) {
1652
- const p = path6.join(dir, n);
1850
+ const p = path7.join(dir, n);
1653
1851
  if (n.endsWith(`-${threadId}.jsonl`)) {
1654
1852
  best = p;
1655
1853
  return;
@@ -1693,7 +1891,7 @@ var codex = {
1693
1891
  ];
1694
1892
  let text = "";
1695
1893
  try {
1696
- text = readFileSync4(configFile2(env), "utf8");
1894
+ text = readFileSync5(configFile2(env), "utf8");
1697
1895
  } catch {
1698
1896
  }
1699
1897
  const n = readNotify(text);
@@ -1820,7 +2018,7 @@ var codex = {
1820
2018
  readTranscript(file, fromLine) {
1821
2019
  let text = "";
1822
2020
  try {
1823
- text = readFileSync4(file, "utf8");
2021
+ text = readFileSync5(file, "utf8");
1824
2022
  } catch {
1825
2023
  return { turns: [], line: fromLine };
1826
2024
  }
@@ -1834,7 +2032,7 @@ var codex = {
1834
2032
  backfill(sinceMs, env = process.env) {
1835
2033
  const rows2 = [];
1836
2034
  for (const f of recentFiles(sessionsDir(env), 3, sinceMs, (n) => /^rollout-.*\.jsonl$/.test(n))) {
1837
- const m = /([0-9a-f]{8}-[0-9a-f-]{27,})\.jsonl$/i.exec(path6.basename(f));
2035
+ const m = /([0-9a-f]{8}-[0-9a-f-]{27,})\.jsonl$/i.exec(path7.basename(f));
1838
2036
  if (!m?.[1]) continue;
1839
2037
  if (codexSubagent(rolloutMeta(f))) continue;
1840
2038
  const facts = codexFacts(f);
@@ -1843,12 +2041,40 @@ var codex = {
1843
2041
  rows2.push({ session: { id: m[1], ...rest }, started_at, last_at });
1844
2042
  }
1845
2043
  return rows2;
1846
- }
2044
+ },
2045
+ limits: (transcript) => codexLimits(transcript)
1847
2046
  };
2047
+ var epochIso2 = (t) => typeof t === "number" && t > 0 && t < 1e11 ? new Date(t * 1e3).toISOString() : void 0;
1848
2048
  function prune3(o) {
1849
2049
  for (const k of Object.keys(o)) if (o[k] === void 0) delete o[k];
1850
2050
  return o;
1851
2051
  }
2052
+ function codexLimits(file) {
2053
+ const { tail } = ends(file, 0, 256 * 1024);
2054
+ for (const r of parseLines(tail).reverse()) {
2055
+ const p = r.payload;
2056
+ if (r.type !== "event_msg" || p?.type !== "token_count") continue;
2057
+ if (!p.rate_limits || typeof p.rate_limits !== "object") continue;
2058
+ const rl = p.rate_limits;
2059
+ if (rl.limit_id != null && rl.limit_id !== "codex") continue;
2060
+ const windows = [];
2061
+ for (const id of ["primary", "secondary"]) {
2062
+ const w = rl[id];
2063
+ if (typeof w?.used_percent !== "number" || w.used_percent < 0) continue;
2064
+ windows.push(
2065
+ prune3({
2066
+ id,
2067
+ used_percent: w.used_percent,
2068
+ window_minutes: Number.isInteger(w.window_minutes) && Number(w.window_minutes) > 0 ? Number(w.window_minutes) : void 0,
2069
+ resets_at: epochIso2(w.resets_at)
2070
+ })
2071
+ );
2072
+ }
2073
+ if (!windows.length) return null;
2074
+ return prune3({ windows, plan: typeof rl.plan_type === "string" ? rl.plan_type.slice(0, 64) : void 0 });
2075
+ }
2076
+ return null;
2077
+ }
1852
2078
  function codexFacts(file) {
1853
2079
  const { head, tail } = ends(file, 128 * 1024, 64 * 1024);
1854
2080
  let H = parseLines(head);
@@ -1893,7 +2119,7 @@ function codexFacts(file) {
1893
2119
  // ../core/src/adapters/devin.ts
1894
2120
  import { createHash as createHash3 } from "node:crypto";
1895
2121
  import { existsSync as existsSync6, readlinkSync } from "node:fs";
1896
- import path7 from "node:path";
2122
+ import path8 from "node:path";
1897
2123
 
1898
2124
  // ../core/src/readers/sqlite.ts
1899
2125
  import { execFileSync as execFileSync3 } from "node:child_process";
@@ -2077,11 +2303,11 @@ function foreignEvent(file) {
2077
2303
  return Object.keys(hooks).find((k) => !ok.includes(k));
2078
2304
  }
2079
2305
  var homeOf = (env) => env.HOME || homeDir();
2080
- var appData = (env) => env.APPDATA || path7.join(homeOf(env), "AppData", "Roaming");
2081
- var devinConfigDir = (env = process.env) => process.platform === "win32" ? path7.join(appData(env), "devin") : path7.join(env.XDG_CONFIG_HOME || path7.join(homeOf(env), ".config"), "devin");
2082
- var configFile3 = (env) => path7.join(devinConfigDir(env), "config.json");
2083
- var devinDataDir = (env = process.env) => process.platform === "win32" ? path7.join(appData(env), "devin", "cli") : path7.join(env.XDG_DATA_HOME || path7.join(homeOf(env), ".local", "share"), "devin", "cli");
2084
- var sessionsDb = (env) => path7.join(devinDataDir(env), "sessions.db");
2306
+ var appData = (env) => env.APPDATA || path8.join(homeOf(env), "AppData", "Roaming");
2307
+ var devinConfigDir = (env = process.env) => process.platform === "win32" ? path8.join(appData(env), "devin") : path8.join(env.XDG_CONFIG_HOME || path8.join(homeOf(env), ".config"), "devin");
2308
+ var configFile3 = (env) => path8.join(devinConfigDir(env), "config.json");
2309
+ var devinDataDir = (env = process.env) => process.platform === "win32" ? path8.join(appData(env), "devin", "cli") : path8.join(env.XDG_DATA_HOME || path8.join(homeOf(env), ".local", "share"), "devin", "cli");
2310
+ var sessionsDb = (env) => path8.join(devinDataDir(env), "sessions.db");
2085
2311
  var SESSION_ID = /^[A-Za-z0-9._:-]{1,128}$/;
2086
2312
  var validTool4 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
2087
2313
  var endReason3 = (r) => r === "clear" || r === "logout" || r === "exit" ? r : "other";
@@ -2253,7 +2479,7 @@ var BACKFILL_SQL = `SELECT s.id, ${SESSION_FIELDS} FROM sessions s
2253
2479
  function sessionFacts(row) {
2254
2480
  const out = {};
2255
2481
  if (typeof row.model === "string" && row.model) out.model = row.model;
2256
- if (typeof row.working_directory === "string" && path7.isAbsolute(row.working_directory))
2482
+ if (typeof row.working_directory === "string" && path8.isAbsolute(row.working_directory))
2257
2483
  out.cwd = row.working_directory;
2258
2484
  const started = Number(row.created_at);
2259
2485
  const last = Number(row.last_activity_at);
@@ -2298,8 +2524,8 @@ function readDevinChain(db, session, fromLine) {
2298
2524
  }
2299
2525
  function devinVersion(env = process.env) {
2300
2526
  try {
2301
- const target = readlinkSync(path7.join(devinDataDir(env), "_versions", "current"));
2302
- const name = path7.basename(target);
2527
+ const target = readlinkSync(path8.join(devinDataDir(env), "_versions", "current"));
2528
+ const name = path8.basename(target);
2303
2529
  return /^[0-9][0-9A-Za-z.+-]{0,63}$/.test(name) ? name : void 0;
2304
2530
  } catch {
2305
2531
  return void 0;
@@ -2313,8 +2539,8 @@ function devinAccount(env = process.env) {
2313
2539
 
2314
2540
  // ../core/src/adapters/gemini-cli.ts
2315
2541
  import { execFileSync as execFileSync4 } from "node:child_process";
2316
- import { existsSync as existsSync7, readFileSync as readFileSync5 } from "node:fs";
2317
- import path8 from "node:path";
2542
+ import { existsSync as existsSync7, readFileSync as readFileSync6 } from "node:fs";
2543
+ import path9 from "node:path";
2318
2544
  var NAME5 = "gemini-cli";
2319
2545
  var GEMINI_EVENTS = [
2320
2546
  "SessionStart",
@@ -2327,26 +2553,31 @@ var GEMINI_EVENTS = [
2327
2553
  "SessionEnd"
2328
2554
  ];
2329
2555
  var OPTS4 = { matcherFor: (e) => /Tool/.test(e) ? ".*" : void 0, timeout: 5e3 };
2330
- var geminiDir = (env) => env.GEMINI_CLI_HOME || path8.join(homeDir(), ".gemini");
2331
- var settings = (env) => path8.join(geminiDir(env), "settings.json");
2556
+ var geminiDir = (env) => env.GEMINI_CLI_HOME || path9.join(homeDir(), ".gemini");
2557
+ var settings = (env) => path9.join(geminiDir(env), "settings.json");
2332
2558
  var validTool5 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
2333
2559
  var eventsFor4 = (lean) => lean ? GEMINI_EVENTS.filter((e) => e !== "BeforeTool") : [...GEMINI_EVENTS];
2560
+ function geminiVersion(platform = process.platform, env = process.env) {
2561
+ const win = platform === "win32";
2562
+ const bin = win ? findBinary({ names: ["gemini.exe", "gemini.cmd"], dirs: [path9.dirname(process.execPath)], env }) : "gemini";
2563
+ if (!bin) throw new Error("no gemini on PATH");
2564
+ const c = probe(bin, ["--version"]);
2565
+ return execFileSync4(c.file, c.args, {
2566
+ encoding: "utf8",
2567
+ timeout: 4e3,
2568
+ stdio: ["ignore", "pipe", "ignore"],
2569
+ env,
2570
+ shell: c.shell
2571
+ }).trim();
2572
+ }
2334
2573
  function geminiHooksActive(settingsFile) {
2335
- let tools;
2336
- try {
2337
- tools = JSON.parse(readFileSync5(settingsFile, "utf8")).tools;
2338
- } catch {
2339
- }
2574
+ const tools = readJson(settingsFile).tools;
2340
2575
  if (tools?.enableHooks === true) return { active: true };
2341
2576
  if (tools?.enableHooks === false)
2342
2577
  return { active: false, note: "inactive: tools.enableHooks is false in settings.json" };
2343
2578
  let version = "";
2344
2579
  try {
2345
- version = execFileSync4("gemini", ["--version"], {
2346
- encoding: "utf8",
2347
- timeout: 4e3,
2348
- stdio: ["ignore", "pipe", "ignore"]
2349
- }).trim();
2580
+ version = geminiVersion();
2350
2581
  } catch {
2351
2582
  return {
2352
2583
  active: true,
@@ -2364,7 +2595,7 @@ var geminiCli = {
2364
2595
  name: NAME5,
2365
2596
  events: GEMINI_EVENTS,
2366
2597
  // ~/.gemini also belongs to Antigravity; Gemini CLI is here when its settings or its chat logs exist.
2367
- detect: (env = process.env) => existsSync7(settings(env)) || existsSync7(path8.join(geminiDir(env), "tmp")),
2598
+ detect: (env = process.env) => existsSync7(settings(env)) || existsSync7(path9.join(geminiDir(env), "tmp")),
2368
2599
  configFiles: (env = process.env) => [settings(env)],
2369
2600
  install: (cmd, env = process.env, opts = {}) => {
2370
2601
  const r = installClaudeShaped(settings(env), NAME5, eventsFor4(opts.lean), cmd, OPTS4);
@@ -2462,7 +2693,7 @@ var geminiCli = {
2462
2693
  readTranscript(file, fromLine) {
2463
2694
  let text = "";
2464
2695
  try {
2465
- text = readFileSync5(file, "utf8");
2696
+ text = readFileSync6(file, "utf8");
2466
2697
  } catch {
2467
2698
  return { turns: [], line: fromLine };
2468
2699
  }
@@ -2502,12 +2733,12 @@ var ADAPTERS = [claudeCode, codex, geminiCli, antigravity, devin];
2502
2733
  var adapterByName = (name) => ADAPTERS.find((a) => a.name === name);
2503
2734
 
2504
2735
  // ../core/src/config.ts
2505
- import { chmodSync, existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "node:fs";
2506
- import path9 from "node:path";
2736
+ import { chmodSync, existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "node:fs";
2737
+ import path10 from "node:path";
2507
2738
  var DEFAULT_CONFIG = { sinks: [], harnesses: {}, update_check: true, keep_days: 30 };
2508
2739
  function readConfig(file = configFile()) {
2509
2740
  try {
2510
- const j = JSON.parse(readFileSync6(file, "utf8"));
2741
+ const j = JSON.parse(readFileSync7(file, "utf8"));
2511
2742
  return {
2512
2743
  ...DEFAULT_CONFIG,
2513
2744
  ...j,
@@ -2519,8 +2750,8 @@ function readConfig(file = configFile()) {
2519
2750
  }
2520
2751
  }
2521
2752
  function writeConfig(c, file = configFile()) {
2522
- mkdirSync4(path9.dirname(file), { recursive: true, mode: 448 });
2523
- writeFileSync4(file, `${JSON.stringify(c, null, 2)}
2753
+ mkdirSync4(path10.dirname(file), { recursive: true, mode: 448 });
2754
+ writeFileSync5(file, `${JSON.stringify(c, null, 2)}
2524
2755
  `, { mode: 384 });
2525
2756
  if (process.platform !== "win32" && existsSync8(file)) {
2526
2757
  try {
@@ -2607,14 +2838,14 @@ import {
2607
2838
  mkdirSync as mkdirSync5,
2608
2839
  openSync as openSync2,
2609
2840
  readdirSync as readdirSync5,
2610
- readFileSync as readFileSync7,
2841
+ readFileSync as readFileSync8,
2611
2842
  readSync as readSync2,
2612
2843
  renameSync,
2613
- statSync as statSync4,
2844
+ statSync as statSync5,
2614
2845
  unlinkSync as unlinkSync3,
2615
- writeFileSync as writeFileSync5
2846
+ writeFileSync as writeFileSync6
2616
2847
  } from "node:fs";
2617
- import path10 from "node:path";
2848
+ import path11 from "node:path";
2618
2849
  var READ_CHUNK = 1024 * 1024;
2619
2850
  var READ_MAX = 8 * 1024 * 1024;
2620
2851
  var Outbox = class {
@@ -2623,13 +2854,13 @@ var Outbox = class {
2623
2854
  }
2624
2855
  root;
2625
2856
  dir(harness) {
2626
- return path10.join(this.root, "outbox", safeId(harness));
2857
+ return path11.join(this.root, "outbox", safeId(harness));
2627
2858
  }
2628
2859
  file(ref) {
2629
- return path10.join(this.dir(ref.harness), `${safeId(ref.session)}.jsonl`);
2860
+ return path11.join(this.dir(ref.harness), `${safeId(ref.session)}.jsonl`);
2630
2861
  }
2631
2862
  seqFile(ref) {
2632
- return path10.join(this.dir(ref.harness), `${safeId(ref.session)}.seq`);
2863
+ return path11.join(this.dir(ref.harness), `${safeId(ref.session)}.seq`);
2633
2864
  }
2634
2865
  /** The next seq for a session, persisted (survives a hibernating machine and a
2635
2866
  * restart; an in-memory counter would restart at 0 and collide). */
@@ -2637,19 +2868,19 @@ var Outbox = class {
2637
2868
  const f = this.seqFile(ref);
2638
2869
  let n = -1;
2639
2870
  try {
2640
- const t = readFileSync7(f, "utf8").trim();
2871
+ const t = readFileSync8(f, "utf8").trim();
2641
2872
  if (/^\d+$/.test(t)) n = Number(t);
2642
2873
  } catch {
2643
2874
  }
2644
2875
  if (n < 0) n = this.lastSeqInFile(ref) + 1;
2645
- mkdirSync5(path10.dirname(f), { recursive: true, mode: 448 });
2646
- writeFileSync5(f, String(n + 1), { mode: 384 });
2876
+ mkdirSync5(path11.dirname(f), { recursive: true, mode: 448 });
2877
+ writeFileSync6(f, String(n + 1), { mode: 384 });
2647
2878
  return n;
2648
2879
  }
2649
2880
  lastSeqInFile(ref) {
2650
2881
  const f = this.file(ref);
2651
2882
  if (!existsSync9(f)) return -1;
2652
- const lines = readFileSync7(f, "utf8").trim().split("\n");
2883
+ const lines = readFileSync8(f, "utf8").trim().split("\n");
2653
2884
  for (let i = lines.length - 1; i >= 0; i--) {
2654
2885
  try {
2655
2886
  const j = JSON.parse(lines[i]);
@@ -2668,10 +2899,10 @@ var Outbox = class {
2668
2899
  /** Every session file, newest first. */
2669
2900
  sessions() {
2670
2901
  const out = [];
2671
- const base = path10.join(this.root, "outbox");
2902
+ const base = path11.join(this.root, "outbox");
2672
2903
  if (!existsSync9(base)) return [];
2673
2904
  for (const h of readdirSync5(base)) {
2674
- const d = path10.join(base, h);
2905
+ const d = path11.join(base, h);
2675
2906
  let names = [];
2676
2907
  try {
2677
2908
  names = readdirSync5(d).filter((n) => n.endsWith(".jsonl"));
@@ -2680,7 +2911,7 @@ var Outbox = class {
2680
2911
  }
2681
2912
  for (const n of names) {
2682
2913
  try {
2683
- out.push({ ref: { harness: h, session: n.slice(0, -6) }, mtime: statSync4(path10.join(d, n)).mtimeMs });
2914
+ out.push({ ref: { harness: h, session: n.slice(0, -6) }, mtime: statSync5(path11.join(d, n)).mtimeMs });
2684
2915
  } catch {
2685
2916
  }
2686
2917
  }
@@ -2696,7 +2927,7 @@ var Outbox = class {
2696
2927
  read(ref, from, maxEvents = 500) {
2697
2928
  const f = this.file(ref);
2698
2929
  if (!existsSync9(f)) return { events: [], offset: from };
2699
- const size = statSync4(f).size;
2930
+ const size = statSync5(f).size;
2700
2931
  if (from >= size) return { events: [], offset: from };
2701
2932
  const fd = openSync2(f, "r");
2702
2933
  try {
@@ -2738,7 +2969,7 @@ var Outbox = class {
2738
2969
  for (const ref of this.sessions()) {
2739
2970
  const f = this.file(ref);
2740
2971
  try {
2741
- if (statSync4(f).mtimeMs < cutoff) {
2972
+ if (statSync5(f).mtimeMs < cutoff) {
2742
2973
  unlinkSync3(f);
2743
2974
  try {
2744
2975
  unlinkSync3(this.seqFile(ref));
@@ -2768,20 +2999,20 @@ var Cursors = class {
2768
2999
  root;
2769
3000
  sink;
2770
3001
  file() {
2771
- return path10.join(this.root, "cursors", `${safeId(this.sink)}.json`);
3002
+ return path11.join(this.root, "cursors", `${safeId(this.sink)}.json`);
2772
3003
  }
2773
3004
  read() {
2774
3005
  try {
2775
- return JSON.parse(readFileSync7(this.file(), "utf8"));
3006
+ return JSON.parse(readFileSync8(this.file(), "utf8"));
2776
3007
  } catch {
2777
3008
  return {};
2778
3009
  }
2779
3010
  }
2780
3011
  write(c) {
2781
3012
  const f = this.file();
2782
- mkdirSync5(path10.dirname(f), { recursive: true, mode: 448 });
3013
+ mkdirSync5(path11.dirname(f), { recursive: true, mode: 448 });
2783
3014
  const tmp = `${f}.${process.pid}.tmp`;
2784
- writeFileSync5(tmp, JSON.stringify(c), { mode: 384 });
3015
+ writeFileSync6(tmp, JSON.stringify(c), { mode: 384 });
2785
3016
  renameSync(tmp, f);
2786
3017
  }
2787
3018
  key(ref) {
@@ -2792,16 +3023,16 @@ function takeLock(root, ref, staleMs = 5 * 6e4) {
2792
3023
  return takeNamedLock(root, `${safeId(ref.harness)}-${safeId(ref.session)}`, staleMs);
2793
3024
  }
2794
3025
  function takeNamedLock(root, name, staleMs = 5 * 6e4) {
2795
- const dir = path10.join(root, "locks");
3026
+ const dir = path11.join(root, "locks");
2796
3027
  mkdirSync5(dir, { recursive: true, mode: 448 });
2797
- const f = path10.join(dir, `${name}.lock`);
3028
+ const f = path11.join(dir, `${name}.lock`);
2798
3029
  try {
2799
- if (Date.now() - statSync4(f).mtimeMs > staleMs || !processAlive(Number(readFileSync7(f, "utf8")))) unlinkSync3(f);
3030
+ if (Date.now() - statSync5(f).mtimeMs > staleMs || !processAlive(Number(readFileSync8(f, "utf8")))) unlinkSync3(f);
2800
3031
  else return null;
2801
3032
  } catch {
2802
3033
  }
2803
3034
  try {
2804
- writeFileSync5(f, String(process.pid), { flag: "wx" });
3035
+ writeFileSync6(f, String(process.pid), { flag: "wx" });
2805
3036
  } catch {
2806
3037
  return null;
2807
3038
  }
@@ -3033,6 +3264,7 @@ var TYPE_TIER = {
3033
3264
  "subagent.started": 1,
3034
3265
  "subagent.ended": 1,
3035
3266
  "context.compacted": 1,
3267
+ "limits.observed": 1,
3036
3268
  native: 3
3037
3269
  };
3038
3270
  var DATA_ALLOW = {
@@ -3052,7 +3284,8 @@ var DATA_ALLOW = {
3052
3284
  "attention.cleared": { 0: ["attention_id", "how"] },
3053
3285
  "subagent.started": { 1: ["agent_id", "agent_type"] },
3054
3286
  "subagent.ended": { 1: ["agent_id", "agent_type"] },
3055
- "context.compacted": { 1: ["trigger"] }
3287
+ "context.compacted": { 1: ["trigger"] },
3288
+ "limits.observed": { 1: ["windows", "plan", "account_id"] }
3056
3289
  };
3057
3290
  var SESSION_ALLOW = [
3058
3291
  "id",
@@ -3117,7 +3350,9 @@ function filterEvent(event, tier, pii, opts = {}) {
3117
3350
  const s = reducePiiDeep(out.session, opts.home);
3118
3351
  if (typeof s.machine === "string") s.machine = hashMachine(s.machine);
3119
3352
  delete s.account_id;
3120
- out = { ...out, session: s, data: reducePiiDeep(out.data, opts.home) };
3353
+ const d = reducePiiDeep(out.data, opts.home);
3354
+ delete d.account_id;
3355
+ out = { ...out, session: s, data: d };
3121
3356
  rulesets.push(RULESET_PII);
3122
3357
  }
3123
3358
  out.privacy = { rulesets, pii };
@@ -3132,7 +3367,7 @@ function shortId(harness, id) {
3132
3367
 
3133
3368
  // ../core/src/sinks/file.ts
3134
3369
  import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync6 } from "node:fs";
3135
- import path11 from "node:path";
3370
+ import path12 from "node:path";
3136
3371
  var FileSink = class {
3137
3372
  constructor(name, file, tier, pii) {
3138
3373
  this.name = name;
@@ -3146,7 +3381,7 @@ var FileSink = class {
3146
3381
  pii;
3147
3382
  async send(events) {
3148
3383
  try {
3149
- mkdirSync6(path11.dirname(this.file), { recursive: true });
3384
+ mkdirSync6(path12.dirname(this.file), { recursive: true });
3150
3385
  appendFileSync2(this.file, `${events.map((e) => JSON.stringify(e)).join("\n")}
3151
3386
  `);
3152
3387
  return { ok: true, acknowledged: events.length, accepted: events.length, duplicates: 0, rejected: [] };
@@ -3180,8 +3415,8 @@ var StdoutSink = class {
3180
3415
  };
3181
3416
 
3182
3417
  // src/control/aliases.ts
3183
- import { chmodSync as chmodSync2, mkdirSync as mkdirSync7, readFileSync as readFileSync8, renameSync as renameSync2, writeFileSync as writeFileSync6 } from "node:fs";
3184
- import path12 from "node:path";
3418
+ import { chmodSync as chmodSync2, mkdirSync as mkdirSync7, readFileSync as readFileSync9, renameSync as renameSync2, writeFileSync as writeFileSync7 } from "node:fs";
3419
+ import path13 from "node:path";
3185
3420
 
3186
3421
  // src/secrets.ts
3187
3422
  import { spawnSync } from "node:child_process";
@@ -3225,10 +3460,10 @@ function startOptionsOf(v) {
3225
3460
  }
3226
3461
 
3227
3462
  // src/control/aliases.ts
3228
- var aliasFile = (controlDir) => path12.join(controlDir, "aliases.json");
3463
+ var aliasFile = (controlDir) => path13.join(controlDir, "aliases.json");
3229
3464
  function readAliases(file) {
3230
3465
  try {
3231
- const j = JSON.parse(readFileSync8(file, "utf8"));
3466
+ const j = JSON.parse(readFileSync9(file, "utf8"));
3232
3467
  if (!j || typeof j !== "object" || Array.isArray(j)) return {};
3233
3468
  const out = {};
3234
3469
  for (const [k, v] of Object.entries(j)) {
@@ -3250,11 +3485,11 @@ function receiverIdOf(all, harness, id) {
3250
3485
  // src/control/local.ts
3251
3486
  import net from "node:net";
3252
3487
  import os2 from "node:os";
3253
- import path13 from "node:path";
3488
+ import path14 from "node:path";
3254
3489
  function socketPath(state, env = process.env) {
3255
3490
  if (env.SESSIONPIPE_SOCKET) return env.SESSIONPIPE_SOCKET;
3256
3491
  if (process.platform === "win32") return `\\\\.\\pipe\\sessionpipe-control-${os2.userInfo().username}`;
3257
- return path13.join(state, "control.sock");
3492
+ return path14.join(state, "control.sock");
3258
3493
  }
3259
3494
  function ask(sock, req, timeoutMs) {
3260
3495
  return new Promise((resolve) => {
@@ -3305,8 +3540,8 @@ async function tellDaemon(state, job, session, cwd, transcript) {
3305
3540
  function enqueueJob(state, job) {
3306
3541
  const jobs = jobsDir(state);
3307
3542
  mkdirSync8(jobs, { recursive: true, mode: 448 });
3308
- const f = path14.join(jobs, `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}.json`);
3309
- writeFileSync7(f, JSON.stringify(job), { mode: 384 });
3543
+ const f = path15.join(jobs, `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}.json`);
3544
+ writeFileSync8(f, JSON.stringify(job), { mode: 384 });
3310
3545
  return f;
3311
3546
  }
3312
3547
  function releaseJob(claimed) {
@@ -3319,9 +3554,9 @@ function releaseJob(claimed) {
3319
3554
  function log(state, line) {
3320
3555
  try {
3321
3556
  mkdirSync8(state, { recursive: true, mode: 448 });
3322
- const f = path14.join(state, "log");
3557
+ const f = path15.join(state, "log");
3323
3558
  try {
3324
- if (statSync5(f).size > 1024 * 1024) renameSync3(f, `${f}.1`);
3559
+ if (statSync6(f).size > 1024 * 1024) renameSync3(f, `${f}.1`);
3325
3560
  } catch {
3326
3561
  }
3327
3562
  appendFileSync3(f, `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
@@ -3330,11 +3565,11 @@ function log(state, line) {
3330
3565
  }
3331
3566
  }
3332
3567
  function factsState(state) {
3333
- const file = (h, s) => path14.join(state, "facts", safeId(h), `${safeId(s)}.json`);
3568
+ const file = (h, s) => path15.join(state, "facts", safeId(h), `${safeId(s)}.json`);
3334
3569
  return {
3335
3570
  get(h, s) {
3336
3571
  try {
3337
- return JSON.parse(readFileSync9(file(h, s), "utf8"));
3572
+ return JSON.parse(readFileSync10(file(h, s), "utf8"));
3338
3573
  } catch {
3339
3574
  return {};
3340
3575
  }
@@ -3343,8 +3578,8 @@ function factsState(state) {
3343
3578
  const cur = this.get(h, s);
3344
3579
  const next = { ...cur, ...patch };
3345
3580
  if (JSON.stringify(next) === JSON.stringify(cur)) return;
3346
- mkdirSync8(path14.dirname(file(h, s)), { recursive: true, mode: 448 });
3347
- writeFileSync7(file(h, s), JSON.stringify(next), { mode: 384 });
3581
+ mkdirSync8(path15.dirname(file(h, s)), { recursive: true, mode: 448 });
3582
+ writeFileSync8(file(h, s), JSON.stringify(next), { mode: 384 });
3348
3583
  }
3349
3584
  };
3350
3585
  }
@@ -3416,7 +3651,7 @@ async function runJob(job, opts = {}) {
3416
3651
  );
3417
3652
  return { events: [], delivered: {} };
3418
3653
  }
3419
- const id = receiverIdOf(readAliases(aliasFile(path14.join(state, "control"))), job.harness, r.session.id) ?? r.session.id;
3654
+ const id = receiverIdOf(readAliases(aliasFile(path15.join(state, "control"))), job.harness, r.session.id) ?? r.session.id;
3420
3655
  const claudeEnd = job.harness === "claude-code" && r.events.some((e) => e.type === "session.ended");
3421
3656
  if (!claudeEnd) await tellDaemon(state, job, id, r.session.cwd, r.transcript);
3422
3657
  const outbox = new Outbox(state);
@@ -3445,12 +3680,15 @@ async function runJob(job, opts = {}) {
3445
3680
  const heavy = !/^(PreToolUse|PostToolUse|PostToolUseFailure|BeforeTool|AfterTool|PostInvocation)$/.test(job.event);
3446
3681
  let learned = {};
3447
3682
  if (heavy) {
3448
- const f = adapter.facts(r.session, r.transcript, r.hints ?? {}, facts, { ...process.env, ...job.env });
3683
+ const hookEnv = { ...process.env, ...job.env };
3684
+ const f = adapter.facts(r.session, r.transcript, r.hints ?? {}, facts, hookEnv);
3685
+ if (cfg.remote_titles && adapter.remoteFacts)
3686
+ Object.assign(f, await adapter.remoteFacts(r.session, f, r.transcript, facts, hookEnv));
3449
3687
  const { started_at: _s, last_at: _l, first_ask: _fa, harness_version, ...rest } = f;
3450
3688
  learned = rest;
3451
3689
  if (harness_version) facts.save(job.harness, id, { harness_version });
3452
3690
  facts.save(job.harness, id, { session: { ...mem.session, ...learned } });
3453
- if (CWD_FROM_HOOKS.has(job.harness) && r.session.cwd && path14.isAbsolute(r.session.cwd))
3691
+ if (CWD_FROM_HOOKS.has(job.harness) && r.session.cwd && path15.isAbsolute(r.session.cwd))
3454
3692
  facts.save(job.harness, id, { cwd: r.session.cwd });
3455
3693
  }
3456
3694
  const known = mem.session ?? {};
@@ -3532,6 +3770,23 @@ async function runJob(job, opts = {}) {
3532
3770
  }
3533
3771
  if (read.turns.length) facts.save(job.harness, id, { transcript_line: read.line, transcript_turns: turnNo });
3534
3772
  }
3773
+ if (adapter.limits && r.transcript && ending && cfg.sinks.some((s) => !s.paused && Math.min(s.tier, s.max_tier ?? 3) >= 1)) {
3774
+ const limits = adapter.limits(r.transcript);
3775
+ const sig = limits && JSON.stringify(limits);
3776
+ if (limits && sig !== facts.get(job.harness, id).limits_sig) {
3777
+ events.push(
3778
+ makeEvent(
3779
+ { type: "limits.observed", harnessEvent: job.event, data: limits },
3780
+ {
3781
+ harness: { name: job.harness, ...version ? { version } : {} },
3782
+ session: { ...session, seq: outbox.nextSeq(ref) },
3783
+ now: opts.now ?? job.at
3784
+ }
3785
+ )
3786
+ );
3787
+ facts.save(job.harness, id, { limits_sig: sig });
3788
+ }
3789
+ }
3535
3790
  outbox.append(
3536
3791
  ref,
3537
3792
  events.map((e) => ({ ...e, session: redactDeep(e.session), data: redactDeep(e.data) }))
@@ -3549,11 +3804,11 @@ async function runJob(job, opts = {}) {
3549
3804
  return { events, delivered };
3550
3805
  }
3551
3806
  async function drain(state, sinkConfigs, outbox, first) {
3552
- const more = path14.join(state, "locks", "flush.more");
3807
+ const more = path15.join(state, "locks", "flush.more");
3553
3808
  let release = takeNamedLock(state, "flush");
3554
3809
  if (!release) {
3555
3810
  try {
3556
- writeFileSync7(more, "", { mode: 384 });
3811
+ writeFileSync8(more, "", { mode: 384 });
3557
3812
  } catch {
3558
3813
  }
3559
3814
  release = takeNamedLock(state, "flush");
@@ -3577,10 +3832,10 @@ async function drain(state, sinkConfigs, outbox, first) {
3577
3832
  }
3578
3833
  }
3579
3834
  var BACKOFF_MS = [2e3, 1e4, 6e4, 5 * 6e4, 30 * 6e4];
3580
- var backoffFile = (state, sink) => path14.join(state, "backoff", `${safeId(sink)}.json`);
3835
+ var backoffFile = (state, sink) => path15.join(state, "backoff", `${safeId(sink)}.json`);
3581
3836
  function readBackoff(state, sink) {
3582
3837
  try {
3583
- const b = JSON.parse(readFileSync9(backoffFile(state, sink), "utf8"));
3838
+ const b = JSON.parse(readFileSync10(backoffFile(state, sink), "utf8"));
3584
3839
  return typeof b.until === "number" ? b : null;
3585
3840
  } catch {
3586
3841
  return null;
@@ -3593,8 +3848,8 @@ function noteFailure(state, sink, res, prev) {
3593
3848
  const b = { until: Date.now() + wait, failures, why: `${res.status ?? ""} ${res.error ?? ""}`.trim() };
3594
3849
  const f = backoffFile(state, sink);
3595
3850
  try {
3596
- mkdirSync8(path14.dirname(f), { recursive: true, mode: 448 });
3597
- writeFileSync7(f, JSON.stringify(b), { mode: 384 });
3851
+ mkdirSync8(path15.dirname(f), { recursive: true, mode: 448 });
3852
+ writeFileSync8(f, JSON.stringify(b), { mode: 384 });
3598
3853
  } catch {
3599
3854
  }
3600
3855
  return b;
@@ -3726,7 +3981,7 @@ function applySinkVerdict(name, res) {
3726
3981
  } catch {
3727
3982
  }
3728
3983
  }
3729
- var jobsDir = (state) => path14.join(state, "jobs");
3984
+ var jobsDir = (state) => path15.join(state, "jobs");
3730
3985
  export {
3731
3986
  HttpSink,
3732
3987
  buildSinks,