sessionpipe 0.10.0 → 0.11.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 +5085 -2360
  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 +37 -1
  18. package/dist/hook.js.map +2 -2
  19. package/dist/index.js +1101 -875
  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 +1096 -870
  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.11.0 · Apache-2.0 · https://sessionpipe.org
2
2
 
3
3
  // src/http-sink.ts
4
4
  import { createHash, createHmac } from "node:crypto";
@@ -103,25 +103,12 @@ import {
103
103
  readdirSync as readdirSync6,
104
104
  readFileSync as readFileSync9,
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,816 +627,941 @@ 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
627
686
  })
628
687
  );
629
688
  break;
630
- case "Stop":
631
- events.push(ev("turn.ended", { reason: s.error ? "error" : "stop" }));
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
702
+ })
703
+ );
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;
724
+ case "Stop":
725
+ events.push(
726
+ ev("turn.ended", {
727
+ turn_id,
728
+ reason: "stop",
729
+ background: Array.isArray(s.background_tasks) ? s.background_tasks.length : void 0
730
+ })
731
+ );
732
+ break;
733
+ case "StopFailure":
734
+ events.push(
735
+ ev("turn.ended", {
736
+ turn_id,
737
+ reason: "error",
738
+ error: typeof s.error === "string" ? s.error.slice(0, 200) : void 0
739
+ })
740
+ );
741
+ break;
742
+ case "SubagentStart":
743
+ events.push(
744
+ ev("subagent.started", {
745
+ agent_id: String(s.agent_id ?? ""),
746
+ agent_type: typeof s.agent_type === "string" ? s.agent_type : void 0
747
+ })
748
+ );
749
+ break;
750
+ case "SubagentStop":
751
+ events.push(
752
+ ev("subagent.ended", {
753
+ agent_id: String(s.agent_id ?? ""),
754
+ agent_type: typeof s.agent_type === "string" ? s.agent_type : void 0
755
+ })
756
+ );
757
+ break;
758
+ case "PreCompact":
759
+ events.push(ev("context.compacted", { trigger: s.trigger === "manual" ? "manual" : "auto" }));
760
+ break;
761
+ case "PostCompact":
762
+ break;
763
+ // PreCompact already said it; a second event would double-count
764
+ case "SessionEnd":
765
+ events.push(ev("session.ended", { reason: endReason(s.reason) }));
766
+ break;
767
+ case "StatusLine": {
768
+ const limits = claudeLimits(s.rate_limits);
769
+ if (!limits) return null;
770
+ events.push(ev("limits.observed", limits));
632
771
  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;
639
- },
640
- readTranscript(file, fromLine) {
641
- let text = "";
642
- try {
643
- text = readFileSync2(file, "utf8");
644
- } catch {
645
- return { turns: [], line: fromLine };
646
- }
647
- return readAntigravity(text.split("\n"), fromLine);
648
- },
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
- });
656
- },
657
- backfill(sinceMs, env = process.env) {
658
- 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
772
  }
773
+ default:
774
+ return null;
775
+ }
776
+ const hints = {};
777
+ if (event === "UserPromptSubmit" && typeof s.prompt === "string") hints.prompt = s.prompt;
778
+ const host = input.env.CLAUDE_CODE_HOST_SESSION_ID;
779
+ if (host && DESKTOP_ID.test(host)) hints.desktop_id = host;
780
+ const attended = input.env.CLAUDE_CODE_SESSION_ATTENDED;
781
+ if (attended === "1" || attended === "0") hints.attended = attended === "1" || !!input.env.SESSIONPIPE_CONTROL_JOB;
782
+ if (event !== "PreToolUse" && event !== "PostToolUse") hints.claude_pid = claudePid(input.env);
783
+ const r = { session, events, hints };
784
+ if (typeof s.transcript_path === "string") r.transcript = s.transcript_path;
785
+ return r;
786
+ },
787
+ readTranscript(file, fromLine) {
788
+ let text = "";
789
+ try {
790
+ text = readFileSync2(file, "utf8");
791
+ } catch {
792
+ return { turns: [], line: fromLine };
793
+ }
794
+ return readClaude(text.split("\n"), fromLine);
795
+ },
796
+ facts(session, transcript, hints, state) {
797
+ const out = {};
798
+ if (transcript) Object.assign(out, claudeFacts(transcript, session.id, hints, state));
799
+ const cwd = out.cwd ?? session.cwd;
800
+ Object.assign(out, gitFacts(cwd), out.branch ? { branch: out.branch } : {});
801
+ if (!out.url && typeof hints.desktop_id === "string") out.url = `claude://code/continue/${hints.desktop_id}`;
802
+ if (typeof hints.attended === "boolean") out.attended = hints.attended || !!out.url;
803
+ return out;
804
+ },
805
+ remoteFacts: (session, local, transcript, state, env = process.env) => claudeRemoteFacts(session.id, local, transcript, state, env),
806
+ backfill(sinceMs, env = process.env) {
807
+ const rows2 = [];
808
+ const files = [];
809
+ for (const d of claudeDirs(env))
810
+ files.push(...recentFiles(path4.join(d, "projects"), 1, sinceMs, (n) => /^[0-9a-f-]{36}\.jsonl$/.test(n)));
811
+ const noState = { get: () => ({}), save: () => {
812
+ } };
813
+ for (const f of files) {
814
+ const id = path4.basename(f, ".jsonl");
815
+ const facts = claudeFacts(f, id, {}, noState);
816
+ if (!facts.started_at || !facts.last_at) continue;
817
+ const { started_at, last_at, first_ask: _fa, harness_version: _hv, ...rest } = facts;
818
+ const cwd = rest.cwd;
819
+ rows2.push({
820
+ session: { id, ...rest, ...gitFacts(cwd), ...rest.branch ? { branch: rest.branch } : {} },
821
+ started_at,
822
+ last_at,
823
+ transcript: f
824
+ });
677
825
  }
678
826
  return rows2;
679
827
  }
680
828
  };
829
+ var LINE_REFRESH_S = 60;
830
+ function statusLine(file, want, cmd) {
831
+ const f = readJsonFile(file);
832
+ if (!f.ok) return null;
833
+ const cur = f.value.statusLine;
834
+ const ours = typeof cur?.command === "string" && OURS.test(cur.command);
835
+ const command = cmd?.([NAME, "StatusLine"]).command;
836
+ if (want && cur && !ours)
837
+ return { file, changed: false, skipped: true, note: "a status line of your own is set; limits are not read" };
838
+ if (want ? ours && cur?.command === command && cur?.refreshInterval === LINE_REFRESH_S : !ours) return null;
839
+ if (want) f.value.statusLine = { type: "command", command, refreshInterval: LINE_REFRESH_S };
840
+ else delete f.value.statusLine;
841
+ writeFileSync2(file, formatJson(f.value, f));
842
+ return { file, changed: true, note: want ? "status line written (account limits)" : "status line removed" };
843
+ }
844
+ var epochIso = (t) => typeof t === "number" && t > 0 && t < 1e11 ? new Date(t * 1e3).toISOString() : void 0;
845
+ var WINDOW_MINUTES = { five_hour: 300, seven_day: 10080 };
846
+ function claudeLimits(rl) {
847
+ if (!rl || typeof rl !== "object" || Array.isArray(rl)) return null;
848
+ const windows = [];
849
+ for (const [id, w] of Object.entries(rl)) {
850
+ if (!/^[a-z0-9_]{1,64}$/.test(id) || typeof w?.used_percentage !== "number" || w.used_percentage < 0) continue;
851
+ windows.push(
852
+ prune({
853
+ id,
854
+ used_percent: w.used_percentage,
855
+ window_minutes: WINDOW_MINUTES[id],
856
+ resets_at: epochIso(w.resets_at)
857
+ })
858
+ );
859
+ }
860
+ return windows.length ? { windows: windows.slice(0, 8) } : null;
861
+ }
681
862
  function prune(o) {
682
863
  for (const k of Object.keys(o)) if (o[k] === void 0) delete o[k];
683
864
  return o;
684
865
  }
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);
866
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
867
+ function notClaudeCode(id, s) {
868
+ return typeof s.transcript_path !== "string" && !UUID.test(id);
698
869
  }
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 {
870
+ function pickSource(v) {
871
+ return typeof v === "string" && ["startup", "resume", "clear", "compact", "fork"].includes(v) ? v : "unknown";
872
+ }
873
+ var harnessWord = (v) => typeof v === "string" && /^[A-Za-z0-9][A-Za-z0-9_.-]{0,39}$/.test(v) ? v : void 0;
874
+ var WRAPPER = /^<(?:task-notification|cross-session-message|system-reminder|local-command-stdout|command-[a-z-]+)[\s/>]/i;
875
+ var injectedBlock = (prompt) => WRAPPER.test(prompt.trim());
876
+ function errText(v) {
877
+ if (typeof v === "string") return v.slice(0, 2e3);
878
+ if (v && typeof v === "object") {
879
+ const e = v;
880
+ if (typeof e.error === "string") return e.error.slice(0, 2e3);
881
+ if (typeof e.message === "string") return e.message.slice(0, 2e3);
706
882
  }
707
- return out;
883
+ return void 0;
708
884
  }
709
- function agProjects(dir) {
710
- const out = [];
885
+ function describe(input) {
886
+ if (!input || typeof input !== "object") return "";
887
+ const i = input;
888
+ const v = i.command ?? i.file_path ?? i.path ?? i.url ?? i.pattern;
889
+ return typeof v === "string" ? ` ${v.slice(0, 120)}` : "";
890
+ }
891
+ var NAME_RANK = {
892
+ user: "custom",
893
+ auto: "harness",
894
+ hook: "harness",
895
+ peer: "harness"
896
+ };
897
+ function registryEntry(j) {
898
+ if (!j || typeof j !== "object") return null;
899
+ const r = j;
900
+ const source = r.nameSource ? NAME_RANK[r.nameSource] : void 0;
901
+ return {
902
+ sessionId: r.sessionId ? String(r.sessionId) : null,
903
+ name: source && typeof r.name === "string" && r.name.trim() ? r.name.trim() : null,
904
+ name_source: source ?? null,
905
+ bridge: typeof r.bridgeSessionId === "string" ? r.bridgeSessionId : null
906
+ };
907
+ }
908
+ var liveCache = /* @__PURE__ */ new Map();
909
+ function claudeLive(dir) {
910
+ const hit = liveCache.get(dir);
911
+ if (hit) return hit;
912
+ const map = /* @__PURE__ */ new Map();
711
913
  let names = [];
712
914
  try {
713
- names = readdirSync2(dir).filter((n) => n.endsWith(".json"));
915
+ names = readdirSync2(path4.join(dir, "sessions")).filter((n) => n.endsWith(".json"));
714
916
  } catch {
715
- return out;
716
917
  }
717
918
  for (const n of names) {
718
- const id = n.slice(0, -5);
719
- if (!ID_RE.test(id)) continue;
720
- let j;
721
919
  try {
722
- j = JSON.parse(readFileSync2(path4.join(dir, n), "utf8"));
920
+ const e = registryEntry(JSON.parse(readFileSync2(path4.join(dir, "sessions", n), "utf8")));
921
+ if (e?.sessionId) map.set(e.sessionId, e);
723
922
  } catch {
724
- continue;
725
923
  }
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
924
  }
733
- return out;
925
+ liveCache.set(dir, map);
926
+ return map;
734
927
  }
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));
928
+ function claudeLiveByPid(dir, pid) {
929
+ if (!pid) return null;
930
+ try {
931
+ return registryEntry(JSON.parse(readFileSync2(path4.join(dir, "sessions", `${pid}.json`), "utf8")));
932
+ } catch {
933
+ return null;
740
934
  }
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;
749
- }
750
- }
751
- return best;
752
935
  }
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) {
757
- 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;
767
- } catch {
936
+ function claudePid(env = process.env) {
937
+ try {
938
+ const dirs = claudeDirs(env);
939
+ const has = (pid2) => dirs.some((d) => existsSync2(path4.join(d, "sessions", `${pid2}.json`)));
940
+ let pid = process.ppid;
941
+ if (has(pid)) return pid;
942
+ const parent = parentOf();
943
+ for (let hop = 0; hop < 6 && pid > 1; hop++) {
944
+ pid = parent(pid);
945
+ if (pid > 1 && has(pid)) return pid;
768
946
  }
947
+ } catch {
769
948
  }
770
949
  return null;
771
950
  }
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;
951
+ function parentOf() {
952
+ if (process.platform === "linux") {
953
+ return (pid) => {
954
+ try {
955
+ const st = readFileSync2(`/proc/${pid}/stat`, "utf8");
956
+ return Number(st.slice(st.lastIndexOf(")") + 2).split(" ")[1]) || 0;
957
+ } catch {
958
+ return 0;
959
+ }
960
+ };
788
961
  }
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);
962
+ const table = /* @__PURE__ */ new Map();
963
+ try {
964
+ const out = execFileSync2("ps", ["-axo", "pid=,ppid="], {
965
+ encoding: "utf8",
966
+ timeout: 2e3,
967
+ maxBuffer: 8 * 1024 * 1024,
968
+ stdio: ["ignore", "pipe", "ignore"]
969
+ });
970
+ for (const l of out.split("\n")) {
971
+ const m = /^\s*(\d+)\s+(\d+)/.exec(l);
972
+ if (m) table.set(Number(m[1]), Number(m[2]));
973
+ }
974
+ } catch {
793
975
  }
794
- return out;
976
+ return (pid) => table.get(pid) || 0;
795
977
  }
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) => {
978
+ var EMAIL_RE = /^[^@\s]{1,64}@[^@\s]{1,189}$/;
979
+ function claudeLogin(dir, home = homeDir()) {
980
+ const files = [path4.join(dir, ".claude.json")];
981
+ if (path4.resolve(dir) === path4.join(home, ".claude")) files.unshift(path4.join(home, ".claude.json"));
982
+ for (const f of files) {
811
983
  try {
812
- return execFileSync("git", ["-C", cwd, ...args], {
813
- encoding: "utf8",
814
- timeout: 1500,
815
- stdio: ["ignore", "pipe", "ignore"]
816
- }).trim();
984
+ const a = JSON.parse(readFileSync2(f, "utf8")).oauthAccount;
985
+ if (typeof a?.accountUuid === "string" && a.accountUuid) {
986
+ const email = typeof a.emailAddress === "string" && EMAIL_RE.test(a.emailAddress) ? a.emailAddress : void 0;
987
+ return { id: a.accountUuid.slice(0, 128), ...email ? { email } : {} };
988
+ }
817
989
  } catch {
818
- return "";
819
990
  }
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$/, "");
991
+ }
992
+ return apiAccount(dir);
833
993
  }
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);
994
+ var ANTHROPIC_HOST = /(^|\.)anthropic\.com$/i;
995
+ function apiAccount(dir) {
996
+ const base = readJson(settingsIn(dir)).env?.ANTHROPIC_BASE_URL;
997
+ if (typeof base !== "string") return null;
998
+ let u;
872
999
  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
- }
1000
+ u = new URL(base.trim());
878
1001
  } catch {
1002
+ return null;
879
1003
  }
880
- return out.filter((d, i) => i === 0 || existsSync3(d));
1004
+ if (u.protocol !== "https:" && u.protocol !== "http:" || !u.hostname || ANTHROPIC_HOST.test(u.hostname))
1005
+ return null;
1006
+ const origin = `${u.protocol}//${u.host}`;
1007
+ return { id: `api-${createHash2("sha256").update(origin).digest("hex").slice(0, 16)}`, name: u.host };
881
1008
  }
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
- });
1009
+ var accountCache = /* @__PURE__ */ new Map();
1010
+ function claudeAccount(dir) {
1011
+ const hit = accountCache.get(dir);
1012
+ if (hit !== void 0) return hit;
1013
+ const acct = claudeLogin(dir)?.id ?? null;
1014
+ accountCache.set(dir, acct);
1015
+ return acct;
1016
+ }
1017
+ var LINK_RE = /^https:\/\/claude\.ai\/code\/session_[A-Za-z0-9]+$/;
1018
+ function transcriptLink(records) {
1019
+ for (let i = records.length - 1; i >= 0; i--) {
1020
+ const j = records[i];
1021
+ if (j.type === "system" && j.subtype === "bridge_status" && typeof j.url === "string" && LINK_RE.test(j.url))
1022
+ return j.url;
1023
+ if (j.type === "bridge-session" && typeof j.bridgeSessionId === "string") {
1024
+ const m = /^(?:cse|session)_([A-Za-z0-9]+)$/.exec(j.bridgeSessionId);
1025
+ if (m) return `https://claude.ai/code/session_${m[1]}`;
1095
1026
  }
1096
- return rows2;
1097
1027
  }
1098
- };
1099
- function prune2(o) {
1100
- for (const k of Object.keys(o)) if (o[k] === void 0) delete o[k];
1101
- return o;
1028
+ return null;
1102
1029
  }
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);
1030
+ function appTitle(text) {
1031
+ const t = String(text).replace(/\s+/g, " ").trim();
1032
+ return t.length > 50 ? `${t.slice(0, 50)}...` : t;
1106
1033
  }
1107
- function pickSource(v) {
1108
- return typeof v === "string" && ["startup", "resume", "clear", "compact", "fork"].includes(v) ? v : "unknown";
1034
+ function askText(j) {
1035
+ if (!j || j.type !== "user" || j.isMeta || j.isSidechain) return null;
1036
+ const origin = j.origin;
1037
+ if (origin?.kind && origin.kind !== "human") return null;
1038
+ const c = j.message?.content;
1039
+ 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();
1040
+ if (!text || isInjected(text)) return null;
1041
+ return text;
1109
1042
  }
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);
1043
+ function firstAsk(records) {
1044
+ for (const j of records) {
1045
+ const t = askText(j);
1046
+ if (t) return appTitle(t);
1119
1047
  }
1120
- return void 0;
1121
- }
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)}` : "";
1048
+ return null;
1127
1049
  }
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
- };
1050
+ function scanTitles(file, from, found) {
1051
+ const out = { custom: found.custom ?? null, ai: found.ai ?? null, upto: from };
1052
+ let size = 0;
1053
+ try {
1054
+ size = statSync2(file).size;
1055
+ } catch {
1056
+ return out;
1057
+ }
1058
+ if (size < from) {
1059
+ out.upto = 0;
1060
+ out.custom = out.ai = null;
1061
+ from = 0;
1062
+ }
1063
+ let pos = 0;
1064
+ scanLines(
1065
+ file,
1066
+ (l) => {
1067
+ pos += Buffer.byteLength(l, "utf8") + 1;
1068
+ if (pos <= from || l.indexOf('-title"') < 0) return null;
1069
+ try {
1070
+ const j = JSON.parse(l);
1071
+ if (j.type === "custom-title" && j.customTitle) out.custom = j.customTitle;
1072
+ if (j.type === "ai-title" && j.aiTitle) out.ai = j.aiTitle;
1073
+ } catch {
1074
+ }
1075
+ return null;
1076
+ },
1077
+ 512 * 1024 * 1024
1078
+ );
1079
+ out.upto = size;
1080
+ return out;
1144
1081
  }
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 = [];
1082
+ function claudeFacts(file, sessionId, hints, state) {
1083
+ const { head, tail } = ends(file, 64 * 1024, 256 * 1024);
1084
+ const H = parseLines(head);
1085
+ const T = parseLines(tail);
1086
+ const out = {};
1087
+ const id = sessionId || path4.basename(file, ".jsonl");
1088
+ const mem = state.get(NAME, id);
1089
+ let custom = null;
1090
+ let ai = null;
1091
+ for (const j of H.concat(T)) {
1092
+ if (j.type === "custom-title" && typeof j.customTitle === "string") custom = j.customTitle;
1093
+ if (j.type === "ai-title" && typeof j.aiTitle === "string") ai = j.aiTitle;
1094
+ }
1095
+ let size = 0;
1151
1096
  try {
1152
- names = readdirSync3(path5.join(dir, "sessions")).filter((n) => n.endsWith(".json"));
1097
+ size = statSync2(file).size;
1153
1098
  } catch {
1154
1099
  }
1155
- for (const n of names) {
1156
- try {
1157
- const e = registryEntry(JSON.parse(readFileSync3(path5.join(dir, "sessions", n), "utf8")));
1158
- if (e?.sessionId) map.set(e.sessionId, e);
1159
- } catch {
1100
+ if (!custom && !ai && size > 64 * 1024 + 256 * 1024) {
1101
+ const s = scanTitles(file, Number(mem.scan_upto) || 0, {
1102
+ custom: mem.scan_custom,
1103
+ ai: mem.scan_ai
1104
+ });
1105
+ custom = s.custom;
1106
+ ai = s.ai;
1107
+ state.save(NAME, id, { scan_upto: s.upto, scan_custom: s.custom, scan_ai: s.ai });
1108
+ }
1109
+ const dir = configDirOf(file);
1110
+ const live = claudeLive(dir).get(id) ?? claudeLiveByPid(dir, hints.claude_pid);
1111
+ if (live?.name) state.save(NAME, id, { name: live.name, name_source: live.name_source });
1112
+ const bridge = live?.bridge && /^session_[A-Za-z0-9]+$/.test(live.bridge) ? `https://claude.ai/code/${live.bridge}` : null;
1113
+ const url = bridge ?? transcriptLink(T) ?? transcriptLink(H) ?? (typeof mem.url === "string" ? mem.url : null);
1114
+ if (url) {
1115
+ out.url = url;
1116
+ state.save(NAME, id, { url });
1117
+ }
1118
+ const name = live?.name ? { title: live.name, source: live.name_source } : typeof mem.name === "string" ? { title: mem.name, source: mem.name_source } : null;
1119
+ const pick = name && name.source === "custom" && name || custom && { title: custom, source: "custom" } || name || ai && { title: ai, source: "harness" };
1120
+ let ask2 = typeof mem.first_ask === "string" ? mem.first_ask : firstAsk(H);
1121
+ if (!ask2 && size > 64 * 1024 && !mem.first_ask_none) {
1122
+ ask2 = scanLines(file, (l) => {
1123
+ try {
1124
+ const t = askText(JSON.parse(l));
1125
+ return t ? appTitle(t) : null;
1126
+ } catch {
1127
+ return null;
1128
+ }
1129
+ });
1130
+ if (!ask2 && size > 16 * 1024 * 1024) state.save(NAME, id, { first_ask_none: true });
1131
+ }
1132
+ if (ask2) {
1133
+ out.first_ask = ask2;
1134
+ if (!mem.first_ask) state.save(NAME, id, { first_ask: ask2 });
1135
+ }
1136
+ if (pick) {
1137
+ out.title = String(pick.title).slice(0, 120);
1138
+ out.title_source = pick.source;
1139
+ } else if (ask2) {
1140
+ out.title = ask2;
1141
+ out.title_source = "first-ask";
1142
+ }
1143
+ const acct = claudeAccount(dir);
1144
+ if (acct) out.account_id = acct;
1145
+ for (let i = T.length - 1; i >= 0; i--) {
1146
+ const m = T[i].message?.model;
1147
+ if (T[i]?.type === "assistant" && m && m !== "<synthetic>") {
1148
+ out.model = m;
1149
+ break;
1160
1150
  }
1161
1151
  }
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;
1152
+ const meta = [...T].reverse().find((j) => j.version) ?? H.find((j) => j.version);
1153
+ if (meta?.version) out.harness_version = String(meta.version);
1154
+ const times = H.concat(T).map((j) => Date.parse(String(j.timestamp ?? ""))).filter((n) => n > 0);
1155
+ if (times.length) {
1156
+ out.started_at = Math.min(...times);
1157
+ out.last_at = Math.max(...times);
1171
1158
  }
1172
- }
1173
- function claudePid(env = process.env) {
1174
- 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
- }
1184
- } catch {
1159
+ const withCwd = H.find((j) => typeof j.cwd === "string");
1160
+ if (withCwd) {
1161
+ out.cwd = withCwd.cwd;
1162
+ if (typeof withCwd.gitBranch === "string" && withCwd.gitBranch !== "HEAD") out.branch = withCwd.gitBranch;
1185
1163
  }
1186
- return null;
1164
+ return out;
1187
1165
  }
1188
- function parentOf() {
1189
- if (process.platform === "linux") {
1190
- return (pid) => {
1166
+ var REMOTE_RETRY_MS = 6e4;
1167
+ var REMOTE_REFRESH_MS = 30 * 6e4;
1168
+ var CLAUDE_API = "https://api.anthropic.com";
1169
+ var KEYCHAIN = "Claude Code-credentials";
1170
+ var REMOTE_TIMEOUT_MS = 3e3;
1171
+ var KEYCHAIN_TIMEOUT_MS = 2e3;
1172
+ var REMOTE_MAX_BYTES = 1 << 20;
1173
+ var cleanName = (s) => s.replace(/[\p{Cc}\u061C\u200E\u200F\u202A-\u202E\u2066-\u2069]/gu, " ").replace(/\s+/g, " ").trim();
1174
+ async function claudeRemoteFacts(id, local, transcript, state, env, now = Date.now()) {
1175
+ if (!transcript || !local.url || !LINK_RE.test(local.url) || local.title_source === "custom") return {};
1176
+ const mem = state.get(NAME, id);
1177
+ let title = typeof mem.remote_title === "string" ? mem.remote_title : null;
1178
+ if (now - (Number(mem.remote_at) || 0) >= (title ? REMOTE_REFRESH_MS : REMOTE_RETRY_MS)) {
1179
+ state.save(NAME, id, { remote_at: now });
1180
+ const token = claudeAiToken(configDirOf(transcript), env, now);
1181
+ const got = token ? await claudeAiTitle(local.url.slice(local.url.lastIndexOf("/") + 1), token) : void 0;
1182
+ if (got !== void 0) {
1183
+ title = got?.slice(0, 200) ?? null;
1184
+ state.save(NAME, id, { remote_title: title });
1185
+ }
1186
+ }
1187
+ return title ? { title: title.slice(0, 120), title_source: "harness" } : {};
1188
+ }
1189
+ function keychainServices(dir, env) {
1190
+ const named = (p) => `${KEYCHAIN}-${createHash2("sha256").update(p.normalize("NFC")).digest("hex").slice(0, 8)}`;
1191
+ const out = /* @__PURE__ */ new Set();
1192
+ if (env.CLAUDE_CONFIG_DIR && path4.resolve(env.CLAUDE_CONFIG_DIR) === dir) out.add(named(env.CLAUDE_CONFIG_DIR));
1193
+ if (dir === path4.join(env.HOME || homeDir(), ".claude")) out.add(KEYCHAIN);
1194
+ out.add(named(dir));
1195
+ return [...out];
1196
+ }
1197
+ var tokenCache = /* @__PURE__ */ new Map();
1198
+ function claudeAiToken(dir, env, now) {
1199
+ const hit = tokenCache.get(dir);
1200
+ if (hit && hit.expires > now + 6e4) return hit.token;
1201
+ const raw = [];
1202
+ if (process.platform === "darwin")
1203
+ for (const service of keychainServices(dir, env)) {
1191
1204
  try {
1192
- const st = readFileSync3(`/proc/${pid}/stat`, "utf8");
1193
- return Number(st.slice(st.lastIndexOf(")") + 2).split(" ")[1]) || 0;
1205
+ const user = env.USER || userInfo().username;
1206
+ const account = /^[\w.-]+$/.test(user) ? user : "claude-code-user";
1207
+ raw.push(
1208
+ execFileSync2("security", ["find-generic-password", "-a", account, "-w", "-s", service], {
1209
+ encoding: "utf8",
1210
+ timeout: KEYCHAIN_TIMEOUT_MS,
1211
+ stdio: ["ignore", "pipe", "ignore"]
1212
+ })
1213
+ );
1214
+ break;
1194
1215
  } catch {
1195
- return 0;
1196
1216
  }
1197
- };
1198
- }
1199
- const table = /* @__PURE__ */ new Map();
1200
- 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
1217
  }
1218
+ try {
1219
+ raw.push(readFileSync2(path4.join(dir, ".credentials.json"), "utf8"));
1211
1220
  } catch {
1212
1221
  }
1213
- return (pid) => table.get(pid) || 0;
1214
- }
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) {
1222
+ for (const r of raw) {
1220
1223
  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
- }
1224
+ const t = r.trim();
1225
+ const o = JSON.parse(/^(?:[0-9a-f]{2})+$/i.test(t) ? Buffer.from(t, "hex").toString("utf8") : t).claudeAiOauth;
1226
+ if (typeof o?.accessToken !== "string" || !o.accessToken) continue;
1227
+ const expires = typeof o.expiresAt === "number" ? o.expiresAt : Number.POSITIVE_INFINITY;
1228
+ if (expires <= now + 6e4) continue;
1229
+ tokenCache.set(dir, { token: o.accessToken, expires });
1230
+ return o.accessToken;
1226
1231
  } catch {
1227
1232
  }
1228
1233
  }
1229
- return apiAccount(dir);
1234
+ return null;
1230
1235
  }
1231
- var ANTHROPIC_HOST = /(^|\.)anthropic\.com$/i;
1232
- function apiAccount(dir) {
1233
- let base;
1236
+ async function claudeAiTitle(bridge, token) {
1234
1237
  try {
1235
- base = JSON.parse(readFileSync3(settingsIn(dir), "utf8")).env?.ANTHROPIC_BASE_URL;
1238
+ const r = await fetch(`${CLAUDE_API}/v1/code/sessions/${bridge}`, {
1239
+ headers: { Authorization: `Bearer ${token}`, "anthropic-version": "2023-06-01", "User-Agent": "sessionpipe" },
1240
+ redirect: "error",
1241
+ signal: AbortSignal.timeout(REMOTE_TIMEOUT_MS)
1242
+ });
1243
+ if (r.status !== 200 || Number(r.headers.get("content-length")) > REMOTE_MAX_BYTES) return void 0;
1244
+ const text = await r.text();
1245
+ if (text.length > REMOTE_MAX_BYTES) return void 0;
1246
+ const j = JSON.parse(text);
1247
+ const s = j?.response_shape ?? j?.session ?? j;
1248
+ const bare = (x) => x.replace(/^(?:session|cse)_/, "");
1249
+ if (typeof s?.id !== "string" || bare(s.id) !== bare(bridge)) return void 0;
1250
+ return typeof s.title === "string" && cleanName(s.title) || null;
1236
1251
  } catch {
1237
- return null;
1252
+ return void 0;
1238
1253
  }
1239
- if (typeof base !== "string") return null;
1240
- let u;
1254
+ }
1255
+
1256
+ // ../core/src/adapters/headless.ts
1257
+ import { accessSync, constants, statSync as statSync3 } from "node:fs";
1258
+ import path5 from "node:path";
1259
+ var JOB_TIMEOUT_MS = 30 * 6e4;
1260
+ var OUT_CAP = 1024 * 1024;
1261
+ function isExe(p) {
1241
1262
  try {
1242
- u = new URL(base.trim());
1263
+ if (!statSync3(p).isFile()) return false;
1264
+ accessSync(p, constants.X_OK);
1265
+ return true;
1243
1266
  } catch {
1244
- return null;
1267
+ return false;
1245
1268
  }
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
1269
  }
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;
1270
+ function findBinary(o) {
1271
+ if (o.override && isExe(o.override)) return o.override;
1272
+ for (const d of String(o.env?.PATH || "").split(path5.delimiter)) {
1273
+ if (!d || !path5.isAbsolute(d)) continue;
1274
+ for (const n of o.names) if (isExe(path5.join(d, n))) return path5.join(d, n);
1275
+ }
1276
+ for (const d of o.dirs) for (const n of o.names) if (isExe(path5.join(d, n))) return path5.join(d, n);
1277
+ return null;
1258
1278
  }
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]}`;
1279
+
1280
+ // ../core/src/adapters/antigravity.ts
1281
+ import {
1282
+ existsSync as existsSync3,
1283
+ mkdirSync as mkdirSync2,
1284
+ readdirSync as readdirSync3,
1285
+ readFileSync as readFileSync3,
1286
+ realpathSync,
1287
+ statSync as statSync4,
1288
+ unlinkSync as unlinkSync2,
1289
+ writeFileSync as writeFileSync3
1290
+ } from "node:fs";
1291
+ import path6 from "node:path";
1292
+ var NAME2 = "antigravity";
1293
+ var HOOK_NAME = "sessionpipe";
1294
+ var AG_EVENTS = ["PreInvocation", "PostInvocation", "PostToolUse", "Stop"];
1295
+ 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';
1296
+ var antigravityAnswer = (event) => event === "PreToolUse" ? AG_STALE_PRETOOL : "{}\n";
1297
+ var gemini = (env) => env.GEMINI_CLI_HOME || path6.join(homeDir(), ".gemini");
1298
+ var hooksFile = (env) => path6.join(gemini(env), "config", "hooks.json");
1299
+ var projectsDir = (env) => path6.join(gemini(env), "config", "projects");
1300
+ var dataDirs = (env) => ["antigravity", "antigravity-cli", "antigravity-ide"].map((d) => path6.join(gemini(env), d));
1301
+ var ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
1302
+ var validTool2 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
1303
+ function entry(cmd) {
1304
+ const handler = (e) => ({ type: "command", command: cmd([NAME2, e]).command, timeout: 5 });
1305
+ const out = { enabled: true };
1306
+ for (const e of AG_EVENTS) out[e] = /Tool/.test(e) ? [{ matcher: "*", hooks: [handler(e)] }] : [handler(e)];
1307
+ return out;
1308
+ }
1309
+ var antigravity = {
1310
+ name: NAME2,
1311
+ events: AG_EVENTS,
1312
+ detect: (env = process.env) => existsSync3(path6.join(gemini(env), "config")) || dataDirs(env).some((d) => existsSync3(d)),
1313
+ configFiles: (env = process.env) => [hooksFile(env)],
1314
+ install(cmd, env = process.env) {
1315
+ const file = hooksFile(env);
1316
+ const f = readJsonFile(file);
1317
+ if (!f.ok) return [{ file, changed: false, skipped: true, note: `${f.reason}; not touched` }];
1318
+ const hooks = f.value;
1319
+ const e = entry(cmd);
1320
+ if (JSON.stringify(hooks[HOOK_NAME]) === JSON.stringify(e)) return [{ file, changed: false }];
1321
+ hooks[HOOK_NAME] = e;
1322
+ mkdirSync2(path6.dirname(file), { recursive: true });
1323
+ writeFileSync3(file, formatJson(hooks, f));
1324
+ return [
1325
+ {
1326
+ file,
1327
+ changed: true,
1328
+ created: !f.existed,
1329
+ note: "hooks.json is read when a conversation starts; open ones report after a restart"
1330
+ }
1331
+ ];
1332
+ },
1333
+ uninstall(env = process.env, opts = {}) {
1334
+ const file = hooksFile(env);
1335
+ if (!existsSync3(file)) return [{ file, changed: false }];
1336
+ const f = readJsonFile(file);
1337
+ if (!f.ok) return [{ file, changed: false, skipped: true, note: `${f.reason}; not touched` }];
1338
+ const hooks = f.value;
1339
+ if (!(HOOK_NAME in hooks)) return [{ file, changed: false }];
1340
+ delete hooks[HOOK_NAME];
1341
+ if (opts.created?.includes(file) && !Object.keys(hooks).length) {
1342
+ unlinkSync2(file);
1343
+ return [{ file, changed: true, note: "removed (sessionpipe created it)" }];
1344
+ }
1345
+ writeFileSync3(file, formatJson(hooks, f));
1346
+ return [{ file, changed: true }];
1347
+ },
1348
+ installed(cmd, env = process.env) {
1349
+ const file = hooksFile(env);
1350
+ const f = readJsonFile(file);
1351
+ const have = f.ok ? f.value[HOOK_NAME] : void 0;
1352
+ return [
1353
+ {
1354
+ file,
1355
+ state: !have ? "missing" : JSON.stringify(have) === JSON.stringify(entry(cmd)) ? "current" : "stale"
1356
+ }
1357
+ ];
1358
+ },
1359
+ fromHook(input) {
1360
+ const event = input.argv[1] ?? "";
1361
+ let s = {};
1362
+ try {
1363
+ const j = input.stdin ? JSON.parse(input.stdin) : {};
1364
+ if (j && typeof j === "object" && !Array.isArray(j)) s = j;
1365
+ } catch {
1366
+ }
1367
+ const id = s.conversationId ?? s.session_id ?? s.sessionId;
1368
+ const stdout = antigravityAnswer(event);
1369
+ if (!id) return { session: { id: "" }, events: [], stdout };
1370
+ const workspaces = agWorkspaces(s.workspacePaths);
1371
+ const session = { id: String(id), cwd: workspaces[0] ?? input.cwd };
1372
+ if (typeof s.modelName === "string" && s.modelName !== "auto") session.model = s.modelName;
1373
+ const toolCall = s.toolCall;
1374
+ const tool = validTool2(toolCall?.name) ? toolCall?.name : validTool2(s.toolName) ? s.toolName : void 0;
1375
+ const ev = (type, data) => ({ type, data: prune2(data), harnessEvent: event });
1376
+ const events = [];
1377
+ switch (event) {
1378
+ case "PreInvocation":
1379
+ if (Number(s.invocationNum) === 0 || s.invocationNum === void 0) {
1380
+ events.push(ev("session.started", { source: "unknown" }));
1381
+ events.push(ev("turn.started", {}));
1382
+ } else events.push(ev("session.heartbeat", {}));
1383
+ break;
1384
+ case "PostInvocation":
1385
+ break;
1386
+ case "PreToolUse":
1387
+ if (tool) events.push(ev("tool.started", { tool, input: toolCall?.args }));
1388
+ break;
1389
+ case "PostToolUse":
1390
+ if (!tool) events.push(ev("session.heartbeat", {}));
1391
+ else
1392
+ events.push(
1393
+ ev("tool.ended", {
1394
+ tool,
1395
+ ok: !s.error,
1396
+ error: typeof s.error === "string" ? s.error.slice(0, 2e3) : void 0,
1397
+ input: toolCall?.args
1398
+ })
1399
+ );
1400
+ break;
1401
+ case "Stop":
1402
+ events.push(ev("turn.ended", { reason: s.error ? "error" : "stop" }));
1403
+ break;
1404
+ default:
1405
+ return { session, events: [], stdout };
1406
+ }
1407
+ const r = { session, events, stdout, hints: { workspaces } };
1408
+ if (typeof s.transcriptPath === "string") r.transcript = s.transcriptPath;
1409
+ return r;
1410
+ },
1411
+ readTranscript(file, fromLine) {
1412
+ let text = "";
1413
+ try {
1414
+ text = readFileSync3(file, "utf8");
1415
+ } catch {
1416
+ return { turns: [], line: fromLine };
1417
+ }
1418
+ return readAntigravity(text.split("\n"), fromLine);
1419
+ },
1420
+ facts(session, transcript, hints) {
1421
+ if (!transcript) return {};
1422
+ return antigravityFacts(transcript, {
1423
+ conversation: session.id,
1424
+ workspaces: hints.workspaces ?? (session.cwd ? [session.cwd] : []),
1425
+ env: process.env
1426
+ });
1427
+ },
1428
+ backfill(sinceMs, env = process.env) {
1429
+ const rows2 = [];
1430
+ for (const data of dataDirs(env)) {
1431
+ let ids = [];
1432
+ try {
1433
+ ids = readdirSync3(path6.join(data, "brain")).filter((n) => /^[0-9a-f-]{36}$/i.test(n));
1434
+ } catch {
1435
+ }
1436
+ for (const id of ids) {
1437
+ const f = path6.join(data, "brain", id, ".system_generated", "logs", "transcript.jsonl");
1438
+ try {
1439
+ if (statSync4(f).mtimeMs < sinceMs) continue;
1440
+ } catch {
1441
+ continue;
1442
+ }
1443
+ const facts = antigravityFacts(f, { conversation: id, workspaces: [], env });
1444
+ if (!facts.started_at || !facts.last_at) continue;
1445
+ const { started_at, last_at, first_ask: _fa, harness_version: _hv, ...rest } = facts;
1446
+ rows2.push({ session: { id, ...rest }, started_at, last_at });
1447
+ }
1268
1448
  }
1449
+ return rows2;
1269
1450
  }
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;
1451
+ };
1452
+ function prune2(o) {
1453
+ for (const k of Object.keys(o)) if (o[k] === void 0) delete o[k];
1454
+ return o;
1284
1455
  }
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;
1456
+ function agPath(p) {
1457
+ if (typeof p !== "string" || !p) return null;
1458
+ let s = p;
1459
+ if (/^file:/i.test(s)) {
1460
+ try {
1461
+ s = decodeURIComponent(new URL(s).pathname);
1462
+ } catch {
1463
+ return null;
1464
+ }
1465
+ if (process.platform === "win32" && /^\/[A-Za-z]:/.test(s)) s = s.slice(1);
1466
+ } else if (/^[a-z][a-z0-9+.-]*:\/\//i.test(s)) return null;
1467
+ if (!path6.isAbsolute(s)) return null;
1468
+ return path6.resolve(s);
1291
1469
  }
1292
- function scanTitles(file, from, found) {
1293
- const out = { custom: found.custom ?? null, ai: found.ai ?? null, upto: from };
1294
- let size = 0;
1470
+ var agWorkspaces = (paths) => (Array.isArray(paths) ? paths : []).map(agPath).filter((p) => !!p).filter((p, i, a) => a.indexOf(p) === i);
1471
+ function agVariants(p) {
1472
+ const out = [p];
1295
1473
  try {
1296
- size = statSync3(file).size;
1474
+ const r = realpathSync.native(p);
1475
+ if (r !== p) out.push(r);
1297
1476
  } catch {
1298
- return out;
1299
- }
1300
- if (size < from) {
1301
- out.upto = 0;
1302
- out.custom = out.ai = null;
1303
- from = 0;
1304
1477
  }
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
1478
  return out;
1323
1479
  }
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;
1480
+ function agProjects(dir) {
1481
+ const out = [];
1482
+ let names = [];
1338
1483
  try {
1339
- size = statSync3(file).size;
1484
+ names = readdirSync3(dir).filter((n) => n.endsWith(".json"));
1340
1485
  } catch {
1486
+ return out;
1341
1487
  }
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 });
1488
+ for (const n of names) {
1489
+ const id = n.slice(0, -5);
1490
+ if (!ID_RE.test(id)) continue;
1491
+ let j;
1492
+ try {
1493
+ j = JSON.parse(readFileSync3(path6.join(dir, n), "utf8"));
1494
+ } catch {
1495
+ continue;
1496
+ }
1497
+ const folders = [];
1498
+ for (const r of j?.projectResources?.resources ?? []) {
1499
+ const f = agPath(r?.folderUri ?? r?.gitFolder?.folderUri);
1500
+ if (f) folders.push(...agVariants(f));
1501
+ }
1502
+ if (folders.length) out.push({ id, folders });
1350
1503
  }
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 });
1504
+ return out;
1505
+ }
1506
+ function agMatchProject(projects, workspaces) {
1507
+ const ws = [];
1508
+ for (const w of workspaces) {
1509
+ const p = agPath(w);
1510
+ if (p) ws.push(...agVariants(p));
1359
1511
  }
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;
1512
+ let best = null;
1513
+ let bestLen = -1;
1514
+ for (const pr of projects)
1515
+ for (const f of pr.folders) {
1516
+ const within = ws.some((w) => w === f || w.startsWith(f.endsWith(path6.sep) ? f : f + path6.sep));
1517
+ if (within && f.length > bestLen) {
1518
+ best = pr.id;
1519
+ bestLen = f.length;
1370
1520
  }
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 });
1521
+ }
1522
+ return best;
1523
+ }
1524
+ function antigravityRemoteUrl(transcript, opts) {
1525
+ const i = transcript.indexOf(`${path6.sep}brain${path6.sep}`);
1526
+ const dirs = (i > 0 ? [transcript.slice(0, i)] : []).concat(dataDirs(opts.env));
1527
+ for (const d of dirs) {
1528
+ try {
1529
+ const m = /installation_uuid:\s*"([0-9a-f-]{36})"/i.exec(
1530
+ readFileSync3(path6.join(d, "antigravity_state.pbtxt"), "utf8")
1531
+ );
1532
+ if (!m) continue;
1533
+ const base = `https://antigravity.google.com/r/${m[1]}-v2`;
1534
+ const conv = opts.conversation ?? "";
1535
+ if (!ID_RE.test(conv) || !opts.workspaces?.length) return base;
1536
+ const project = agMatchProject(agProjects(opts.projectsDir ?? projectsDir(opts.env)), opts.workspaces);
1537
+ return project ? `${base}?p=${encodeURIComponent(`c/${conv}?section=${project}`)}` : base;
1538
+ } catch {
1539
+ }
1377
1540
  }
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;
1541
+ return null;
1542
+ }
1543
+ function antigravityFacts(file, opts) {
1544
+ const { head, tail } = ends(file, 128 * 1024, 64 * 1024);
1545
+ const H = parseLines(head);
1546
+ const T = parseLines(tail);
1547
+ const out = {};
1548
+ const url = antigravityRemoteUrl(file, opts);
1549
+ if (url) out.url = url;
1550
+ for (const j of H) {
1551
+ if (j.type !== "USER_INPUT" || typeof j.content !== "string") continue;
1552
+ const m = /<USER_REQUEST>([\s\S]*?)(?:<\/USER_REQUEST>|$)/.exec(j.content);
1553
+ const text = (m ? m[1] : j.content).trim();
1554
+ const line = (text.split("\n").find((l) => l.trim()) ?? "").trim();
1555
+ if (!line || /^<[a-z_-]+>/i.test(line)) continue;
1556
+ out.title = line.length > 80 ? `${line.slice(0, 80).replace(/\s+\S*$/, "")}\u2026` : line;
1383
1557
  out.title_source = "first-ask";
1558
+ break;
1384
1559
  }
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);
1560
+ const times = H.concat(T).map((j) => Date.parse(String(j.created_at ?? j.timestamp ?? ""))).filter((n) => n > 0);
1397
1561
  if (times.length) {
1398
1562
  out.started_at = Math.min(...times);
1399
1563
  out.last_at = Math.max(...times);
1400
1564
  }
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
1565
  return out;
1407
1566
  }
1408
1567
 
@@ -1415,15 +1574,24 @@ var NONCE_TTL_MS = 25 * 36e5;
1415
1574
  var MODEL_RE = /^[A-Za-z0-9][A-Za-z0-9._:/@[\]-]*$/;
1416
1575
  var START_PERMISSIONS = ["read", "safe", "edits", "auto"];
1417
1576
  var levelRank = (l) => START_PERMISSIONS.indexOf(l);
1577
+ var FIELDS = {
1578
+ prompt: ["text", "files"],
1579
+ "permission.answer": ["for", "decision"],
1580
+ cancel: [],
1581
+ start: ["text", "cwd", "files", "permission", "model"],
1582
+ "key.add": ["key", "name"],
1583
+ pool: ["pool_action", "pool_account", "pool_order", "pool_code"]
1584
+ };
1585
+ var POOL_FIELDS = FIELDS.pool;
1418
1586
  var enc = new TextEncoder();
1419
1587
  var dec = new TextDecoder();
1420
1588
 
1421
1589
  // ../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";
1590
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
1591
+ import path7 from "node:path";
1424
1592
 
1425
1593
  // ../core/src/adapters/codex-config.ts
1426
- var NOTIFY_LINE = /^[ \t]*notify[ \t]*=[ \t]*(\[.*\])[ \t]*(?:#.*)?$/m;
1594
+ var NOTIFY_LINE = /^[ \t]*notify[ \t]*=[ \t]*(\[.*\])[ \t]*(?:#.*)?(?=\r?$)/m;
1427
1595
  var NOTIFY_KEY = /^[ \t]*notify[ \t]*=/m;
1428
1596
  var OURS2 = /(?:hook\.js|sessionpipe-hook)",\s*"codex",\s*"notify"/;
1429
1597
  function scan(line, st) {
@@ -1513,10 +1681,11 @@ function dropLine(text, index, length) {
1513
1681
  }
1514
1682
  function insertTopLevel(text, line) {
1515
1683
  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}`;
1684
+ const body = head.replace(/\s*$/, "");
1685
+ const eol = (body.includes("\n") ? body : text).includes("\r\n") ? "\r\n" : "\n";
1686
+ const top = head.trim() ? head.replace(/\s*$/, eol) : "";
1687
+ if (!tail) return top + line + eol;
1688
+ return `${top + line}${eol}${eol}${tail}`;
1520
1689
  }
1521
1690
  function withNotify(text, line) {
1522
1691
  const n = readNotify(text);
@@ -1565,10 +1734,10 @@ var CODEX_EVENTS = [
1565
1734
  ];
1566
1735
  var OPTS2 = { matcherFor: (e) => /Tool|Permission/.test(e) ? "" : void 0, timeout: 5 };
1567
1736
  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");
1737
+ var codexHome = (env) => env.CODEX_HOME || path7.join(homeDir(), ".codex");
1738
+ var hooksFile2 = (env) => path7.join(codexHome(env), "hooks.json");
1739
+ var configFile2 = (env) => path7.join(codexHome(env), "config.toml");
1740
+ var sessionsDir = (env) => path7.join(codexHome(env), "sessions");
1572
1741
  var validTool3 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
1573
1742
  var endReason2 = (r) => r === "clear" || r === "logout" || r === "exit" ? r : "other";
1574
1743
  function installNotify(cmd, env) {
@@ -1582,8 +1751,8 @@ function installNotify(cmd, env) {
1582
1751
  const line = `notify = ${JSON.stringify(argv)}`;
1583
1752
  const n = readNotify(text);
1584
1753
  const write = (next) => {
1585
- mkdirSync3(path6.dirname(file), { recursive: true });
1586
- writeFileSync3(file, next);
1754
+ mkdirSync3(path7.dirname(file), { recursive: true });
1755
+ writeFileSync4(file, next);
1587
1756
  };
1588
1757
  if (n && !n.other) {
1589
1758
  if (!n.misplaced && JSON.stringify(n.argv) === JSON.stringify(argv))
@@ -1628,12 +1797,12 @@ function uninstallNotify(env) {
1628
1797
  const n = readNotify(text);
1629
1798
  if (!n) return { file, changed: false };
1630
1799
  if (!n.other) {
1631
- writeFileSync3(file, withoutNotify(text));
1800
+ writeFileSync4(file, withoutNotify(text));
1632
1801
  return { file, changed: true };
1633
1802
  }
1634
1803
  const prev = previousNotify(n.argv);
1635
1804
  if (isComputerUse(n.argv) && prev && OURS2.test(JSON.stringify(prev))) {
1636
- writeFileSync3(file, replaceNotify(text, n, `notify = ${stripPreviousRaw(n.raw)}`));
1805
+ writeFileSync4(file, replaceNotify(text, n, `notify = ${stripPreviousRaw(n.raw)}`));
1637
1806
  return { file, changed: true };
1638
1807
  }
1639
1808
  return { file, changed: false };
@@ -1649,7 +1818,7 @@ function findRollout(threadId, env = process.env) {
1649
1818
  return;
1650
1819
  }
1651
1820
  for (const n of names.sort().reverse()) {
1652
- const p = path6.join(dir, n);
1821
+ const p = path7.join(dir, n);
1653
1822
  if (n.endsWith(`-${threadId}.jsonl`)) {
1654
1823
  best = p;
1655
1824
  return;
@@ -1834,7 +2003,7 @@ var codex = {
1834
2003
  backfill(sinceMs, env = process.env) {
1835
2004
  const rows2 = [];
1836
2005
  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));
2006
+ const m = /([0-9a-f]{8}-[0-9a-f-]{27,})\.jsonl$/i.exec(path7.basename(f));
1838
2007
  if (!m?.[1]) continue;
1839
2008
  if (codexSubagent(rolloutMeta(f))) continue;
1840
2009
  const facts = codexFacts(f);
@@ -1843,12 +2012,40 @@ var codex = {
1843
2012
  rows2.push({ session: { id: m[1], ...rest }, started_at, last_at });
1844
2013
  }
1845
2014
  return rows2;
1846
- }
2015
+ },
2016
+ limits: (transcript) => codexLimits(transcript)
1847
2017
  };
2018
+ var epochIso2 = (t) => typeof t === "number" && t > 0 && t < 1e11 ? new Date(t * 1e3).toISOString() : void 0;
1848
2019
  function prune3(o) {
1849
2020
  for (const k of Object.keys(o)) if (o[k] === void 0) delete o[k];
1850
2021
  return o;
1851
2022
  }
2023
+ function codexLimits(file) {
2024
+ const { tail } = ends(file, 0, 256 * 1024);
2025
+ for (const r of parseLines(tail).reverse()) {
2026
+ const p = r.payload;
2027
+ if (r.type !== "event_msg" || p?.type !== "token_count") continue;
2028
+ if (!p.rate_limits || typeof p.rate_limits !== "object") continue;
2029
+ const rl = p.rate_limits;
2030
+ if (rl.limit_id != null && rl.limit_id !== "codex") continue;
2031
+ const windows = [];
2032
+ for (const id of ["primary", "secondary"]) {
2033
+ const w = rl[id];
2034
+ if (typeof w?.used_percent !== "number" || w.used_percent < 0) continue;
2035
+ windows.push(
2036
+ prune3({
2037
+ id,
2038
+ used_percent: w.used_percent,
2039
+ window_minutes: Number.isInteger(w.window_minutes) && Number(w.window_minutes) > 0 ? Number(w.window_minutes) : void 0,
2040
+ resets_at: epochIso2(w.resets_at)
2041
+ })
2042
+ );
2043
+ }
2044
+ if (!windows.length) return null;
2045
+ return prune3({ windows, plan: typeof rl.plan_type === "string" ? rl.plan_type.slice(0, 64) : void 0 });
2046
+ }
2047
+ return null;
2048
+ }
1852
2049
  function codexFacts(file) {
1853
2050
  const { head, tail } = ends(file, 128 * 1024, 64 * 1024);
1854
2051
  let H = parseLines(head);
@@ -1893,7 +2090,7 @@ function codexFacts(file) {
1893
2090
  // ../core/src/adapters/devin.ts
1894
2091
  import { createHash as createHash3 } from "node:crypto";
1895
2092
  import { existsSync as existsSync6, readlinkSync } from "node:fs";
1896
- import path7 from "node:path";
2093
+ import path8 from "node:path";
1897
2094
 
1898
2095
  // ../core/src/readers/sqlite.ts
1899
2096
  import { execFileSync as execFileSync3 } from "node:child_process";
@@ -2077,11 +2274,11 @@ function foreignEvent(file) {
2077
2274
  return Object.keys(hooks).find((k) => !ok.includes(k));
2078
2275
  }
2079
2276
  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");
2277
+ var appData = (env) => env.APPDATA || path8.join(homeOf(env), "AppData", "Roaming");
2278
+ 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");
2279
+ var configFile3 = (env) => path8.join(devinConfigDir(env), "config.json");
2280
+ 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");
2281
+ var sessionsDb = (env) => path8.join(devinDataDir(env), "sessions.db");
2085
2282
  var SESSION_ID = /^[A-Za-z0-9._:-]{1,128}$/;
2086
2283
  var validTool4 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
2087
2284
  var endReason3 = (r) => r === "clear" || r === "logout" || r === "exit" ? r : "other";
@@ -2253,7 +2450,7 @@ var BACKFILL_SQL = `SELECT s.id, ${SESSION_FIELDS} FROM sessions s
2253
2450
  function sessionFacts(row) {
2254
2451
  const out = {};
2255
2452
  if (typeof row.model === "string" && row.model) out.model = row.model;
2256
- if (typeof row.working_directory === "string" && path7.isAbsolute(row.working_directory))
2453
+ if (typeof row.working_directory === "string" && path8.isAbsolute(row.working_directory))
2257
2454
  out.cwd = row.working_directory;
2258
2455
  const started = Number(row.created_at);
2259
2456
  const last = Number(row.last_activity_at);
@@ -2298,8 +2495,8 @@ function readDevinChain(db, session, fromLine) {
2298
2495
  }
2299
2496
  function devinVersion(env = process.env) {
2300
2497
  try {
2301
- const target = readlinkSync(path7.join(devinDataDir(env), "_versions", "current"));
2302
- const name = path7.basename(target);
2498
+ const target = readlinkSync(path8.join(devinDataDir(env), "_versions", "current"));
2499
+ const name = path8.basename(target);
2303
2500
  return /^[0-9][0-9A-Za-z.+-]{0,63}$/.test(name) ? name : void 0;
2304
2501
  } catch {
2305
2502
  return void 0;
@@ -2314,7 +2511,7 @@ function devinAccount(env = process.env) {
2314
2511
  // ../core/src/adapters/gemini-cli.ts
2315
2512
  import { execFileSync as execFileSync4 } from "node:child_process";
2316
2513
  import { existsSync as existsSync7, readFileSync as readFileSync5 } from "node:fs";
2317
- import path8 from "node:path";
2514
+ import path9 from "node:path";
2318
2515
  var NAME5 = "gemini-cli";
2319
2516
  var GEMINI_EVENTS = [
2320
2517
  "SessionStart",
@@ -2327,26 +2524,31 @@ var GEMINI_EVENTS = [
2327
2524
  "SessionEnd"
2328
2525
  ];
2329
2526
  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");
2527
+ var geminiDir = (env) => env.GEMINI_CLI_HOME || path9.join(homeDir(), ".gemini");
2528
+ var settings = (env) => path9.join(geminiDir(env), "settings.json");
2332
2529
  var validTool5 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
2333
2530
  var eventsFor4 = (lean) => lean ? GEMINI_EVENTS.filter((e) => e !== "BeforeTool") : [...GEMINI_EVENTS];
2531
+ function geminiVersion(platform = process.platform, env = process.env) {
2532
+ const win = platform === "win32";
2533
+ const bin = win ? findBinary({ names: ["gemini.exe", "gemini.cmd"], dirs: [path9.dirname(process.execPath)], env }) : "gemini";
2534
+ if (!bin) throw new Error("no gemini on PATH");
2535
+ const o = {
2536
+ encoding: "utf8",
2537
+ timeout: 4e3,
2538
+ stdio: ["ignore", "pipe", "ignore"],
2539
+ env
2540
+ };
2541
+ const out = /\.cmd$/i.test(bin) ? execFileSync4(`"${bin}" --version`, { ...o, shell: true }) : execFileSync4(bin, ["--version"], o);
2542
+ return out.trim();
2543
+ }
2334
2544
  function geminiHooksActive(settingsFile) {
2335
- let tools;
2336
- try {
2337
- tools = JSON.parse(readFileSync5(settingsFile, "utf8")).tools;
2338
- } catch {
2339
- }
2545
+ const tools = readJson(settingsFile).tools;
2340
2546
  if (tools?.enableHooks === true) return { active: true };
2341
2547
  if (tools?.enableHooks === false)
2342
2548
  return { active: false, note: "inactive: tools.enableHooks is false in settings.json" };
2343
2549
  let version = "";
2344
2550
  try {
2345
- version = execFileSync4("gemini", ["--version"], {
2346
- encoding: "utf8",
2347
- timeout: 4e3,
2348
- stdio: ["ignore", "pipe", "ignore"]
2349
- }).trim();
2551
+ version = geminiVersion();
2350
2552
  } catch {
2351
2553
  return {
2352
2554
  active: true,
@@ -2364,7 +2566,7 @@ var geminiCli = {
2364
2566
  name: NAME5,
2365
2567
  events: GEMINI_EVENTS,
2366
2568
  // ~/.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")),
2569
+ detect: (env = process.env) => existsSync7(settings(env)) || existsSync7(path9.join(geminiDir(env), "tmp")),
2368
2570
  configFiles: (env = process.env) => [settings(env)],
2369
2571
  install: (cmd, env = process.env, opts = {}) => {
2370
2572
  const r = installClaudeShaped(settings(env), NAME5, eventsFor4(opts.lean), cmd, OPTS4);
@@ -2502,8 +2704,8 @@ var ADAPTERS = [claudeCode, codex, geminiCli, antigravity, devin];
2502
2704
  var adapterByName = (name) => ADAPTERS.find((a) => a.name === name);
2503
2705
 
2504
2706
  // ../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";
2707
+ import { chmodSync, existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "node:fs";
2708
+ import path10 from "node:path";
2507
2709
  var DEFAULT_CONFIG = { sinks: [], harnesses: {}, update_check: true, keep_days: 30 };
2508
2710
  function readConfig(file = configFile()) {
2509
2711
  try {
@@ -2519,8 +2721,8 @@ function readConfig(file = configFile()) {
2519
2721
  }
2520
2722
  }
2521
2723
  function writeConfig(c, file = configFile()) {
2522
- mkdirSync4(path9.dirname(file), { recursive: true, mode: 448 });
2523
- writeFileSync4(file, `${JSON.stringify(c, null, 2)}
2724
+ mkdirSync4(path10.dirname(file), { recursive: true, mode: 448 });
2725
+ writeFileSync5(file, `${JSON.stringify(c, null, 2)}
2524
2726
  `, { mode: 384 });
2525
2727
  if (process.platform !== "win32" && existsSync8(file)) {
2526
2728
  try {
@@ -2610,11 +2812,11 @@ import {
2610
2812
  readFileSync as readFileSync7,
2611
2813
  readSync as readSync2,
2612
2814
  renameSync,
2613
- statSync as statSync4,
2815
+ statSync as statSync5,
2614
2816
  unlinkSync as unlinkSync3,
2615
- writeFileSync as writeFileSync5
2817
+ writeFileSync as writeFileSync6
2616
2818
  } from "node:fs";
2617
- import path10 from "node:path";
2819
+ import path11 from "node:path";
2618
2820
  var READ_CHUNK = 1024 * 1024;
2619
2821
  var READ_MAX = 8 * 1024 * 1024;
2620
2822
  var Outbox = class {
@@ -2623,13 +2825,13 @@ var Outbox = class {
2623
2825
  }
2624
2826
  root;
2625
2827
  dir(harness) {
2626
- return path10.join(this.root, "outbox", safeId(harness));
2828
+ return path11.join(this.root, "outbox", safeId(harness));
2627
2829
  }
2628
2830
  file(ref) {
2629
- return path10.join(this.dir(ref.harness), `${safeId(ref.session)}.jsonl`);
2831
+ return path11.join(this.dir(ref.harness), `${safeId(ref.session)}.jsonl`);
2630
2832
  }
2631
2833
  seqFile(ref) {
2632
- return path10.join(this.dir(ref.harness), `${safeId(ref.session)}.seq`);
2834
+ return path11.join(this.dir(ref.harness), `${safeId(ref.session)}.seq`);
2633
2835
  }
2634
2836
  /** The next seq for a session, persisted (survives a hibernating machine and a
2635
2837
  * restart; an in-memory counter would restart at 0 and collide). */
@@ -2642,8 +2844,8 @@ var Outbox = class {
2642
2844
  } catch {
2643
2845
  }
2644
2846
  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 });
2847
+ mkdirSync5(path11.dirname(f), { recursive: true, mode: 448 });
2848
+ writeFileSync6(f, String(n + 1), { mode: 384 });
2647
2849
  return n;
2648
2850
  }
2649
2851
  lastSeqInFile(ref) {
@@ -2668,10 +2870,10 @@ var Outbox = class {
2668
2870
  /** Every session file, newest first. */
2669
2871
  sessions() {
2670
2872
  const out = [];
2671
- const base = path10.join(this.root, "outbox");
2873
+ const base = path11.join(this.root, "outbox");
2672
2874
  if (!existsSync9(base)) return [];
2673
2875
  for (const h of readdirSync5(base)) {
2674
- const d = path10.join(base, h);
2876
+ const d = path11.join(base, h);
2675
2877
  let names = [];
2676
2878
  try {
2677
2879
  names = readdirSync5(d).filter((n) => n.endsWith(".jsonl"));
@@ -2680,7 +2882,7 @@ var Outbox = class {
2680
2882
  }
2681
2883
  for (const n of names) {
2682
2884
  try {
2683
- out.push({ ref: { harness: h, session: n.slice(0, -6) }, mtime: statSync4(path10.join(d, n)).mtimeMs });
2885
+ out.push({ ref: { harness: h, session: n.slice(0, -6) }, mtime: statSync5(path11.join(d, n)).mtimeMs });
2684
2886
  } catch {
2685
2887
  }
2686
2888
  }
@@ -2696,7 +2898,7 @@ var Outbox = class {
2696
2898
  read(ref, from, maxEvents = 500) {
2697
2899
  const f = this.file(ref);
2698
2900
  if (!existsSync9(f)) return { events: [], offset: from };
2699
- const size = statSync4(f).size;
2901
+ const size = statSync5(f).size;
2700
2902
  if (from >= size) return { events: [], offset: from };
2701
2903
  const fd = openSync2(f, "r");
2702
2904
  try {
@@ -2738,7 +2940,7 @@ var Outbox = class {
2738
2940
  for (const ref of this.sessions()) {
2739
2941
  const f = this.file(ref);
2740
2942
  try {
2741
- if (statSync4(f).mtimeMs < cutoff) {
2943
+ if (statSync5(f).mtimeMs < cutoff) {
2742
2944
  unlinkSync3(f);
2743
2945
  try {
2744
2946
  unlinkSync3(this.seqFile(ref));
@@ -2768,7 +2970,7 @@ var Cursors = class {
2768
2970
  root;
2769
2971
  sink;
2770
2972
  file() {
2771
- return path10.join(this.root, "cursors", `${safeId(this.sink)}.json`);
2973
+ return path11.join(this.root, "cursors", `${safeId(this.sink)}.json`);
2772
2974
  }
2773
2975
  read() {
2774
2976
  try {
@@ -2779,9 +2981,9 @@ var Cursors = class {
2779
2981
  }
2780
2982
  write(c) {
2781
2983
  const f = this.file();
2782
- mkdirSync5(path10.dirname(f), { recursive: true, mode: 448 });
2984
+ mkdirSync5(path11.dirname(f), { recursive: true, mode: 448 });
2783
2985
  const tmp = `${f}.${process.pid}.tmp`;
2784
- writeFileSync5(tmp, JSON.stringify(c), { mode: 384 });
2986
+ writeFileSync6(tmp, JSON.stringify(c), { mode: 384 });
2785
2987
  renameSync(tmp, f);
2786
2988
  }
2787
2989
  key(ref) {
@@ -2792,16 +2994,16 @@ function takeLock(root, ref, staleMs = 5 * 6e4) {
2792
2994
  return takeNamedLock(root, `${safeId(ref.harness)}-${safeId(ref.session)}`, staleMs);
2793
2995
  }
2794
2996
  function takeNamedLock(root, name, staleMs = 5 * 6e4) {
2795
- const dir = path10.join(root, "locks");
2997
+ const dir = path11.join(root, "locks");
2796
2998
  mkdirSync5(dir, { recursive: true, mode: 448 });
2797
- const f = path10.join(dir, `${name}.lock`);
2999
+ const f = path11.join(dir, `${name}.lock`);
2798
3000
  try {
2799
- if (Date.now() - statSync4(f).mtimeMs > staleMs || !processAlive(Number(readFileSync7(f, "utf8")))) unlinkSync3(f);
3001
+ if (Date.now() - statSync5(f).mtimeMs > staleMs || !processAlive(Number(readFileSync7(f, "utf8")))) unlinkSync3(f);
2800
3002
  else return null;
2801
3003
  } catch {
2802
3004
  }
2803
3005
  try {
2804
- writeFileSync5(f, String(process.pid), { flag: "wx" });
3006
+ writeFileSync6(f, String(process.pid), { flag: "wx" });
2805
3007
  } catch {
2806
3008
  return null;
2807
3009
  }
@@ -3033,6 +3235,7 @@ var TYPE_TIER = {
3033
3235
  "subagent.started": 1,
3034
3236
  "subagent.ended": 1,
3035
3237
  "context.compacted": 1,
3238
+ "limits.observed": 1,
3036
3239
  native: 3
3037
3240
  };
3038
3241
  var DATA_ALLOW = {
@@ -3052,7 +3255,8 @@ var DATA_ALLOW = {
3052
3255
  "attention.cleared": { 0: ["attention_id", "how"] },
3053
3256
  "subagent.started": { 1: ["agent_id", "agent_type"] },
3054
3257
  "subagent.ended": { 1: ["agent_id", "agent_type"] },
3055
- "context.compacted": { 1: ["trigger"] }
3258
+ "context.compacted": { 1: ["trigger"] },
3259
+ "limits.observed": { 1: ["windows", "plan", "account_id"] }
3056
3260
  };
3057
3261
  var SESSION_ALLOW = [
3058
3262
  "id",
@@ -3117,7 +3321,9 @@ function filterEvent(event, tier, pii, opts = {}) {
3117
3321
  const s = reducePiiDeep(out.session, opts.home);
3118
3322
  if (typeof s.machine === "string") s.machine = hashMachine(s.machine);
3119
3323
  delete s.account_id;
3120
- out = { ...out, session: s, data: reducePiiDeep(out.data, opts.home) };
3324
+ const d = reducePiiDeep(out.data, opts.home);
3325
+ delete d.account_id;
3326
+ out = { ...out, session: s, data: d };
3121
3327
  rulesets.push(RULESET_PII);
3122
3328
  }
3123
3329
  out.privacy = { rulesets, pii };
@@ -3132,7 +3338,7 @@ function shortId(harness, id) {
3132
3338
 
3133
3339
  // ../core/src/sinks/file.ts
3134
3340
  import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync6 } from "node:fs";
3135
- import path11 from "node:path";
3341
+ import path12 from "node:path";
3136
3342
  var FileSink = class {
3137
3343
  constructor(name, file, tier, pii) {
3138
3344
  this.name = name;
@@ -3146,7 +3352,7 @@ var FileSink = class {
3146
3352
  pii;
3147
3353
  async send(events) {
3148
3354
  try {
3149
- mkdirSync6(path11.dirname(this.file), { recursive: true });
3355
+ mkdirSync6(path12.dirname(this.file), { recursive: true });
3150
3356
  appendFileSync2(this.file, `${events.map((e) => JSON.stringify(e)).join("\n")}
3151
3357
  `);
3152
3358
  return { ok: true, acknowledged: events.length, accepted: events.length, duplicates: 0, rejected: [] };
@@ -3180,8 +3386,8 @@ var StdoutSink = class {
3180
3386
  };
3181
3387
 
3182
3388
  // 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";
3389
+ import { chmodSync as chmodSync2, mkdirSync as mkdirSync7, readFileSync as readFileSync8, renameSync as renameSync2, writeFileSync as writeFileSync7 } from "node:fs";
3390
+ import path13 from "node:path";
3185
3391
 
3186
3392
  // src/secrets.ts
3187
3393
  import { spawnSync } from "node:child_process";
@@ -3225,7 +3431,7 @@ function startOptionsOf(v) {
3225
3431
  }
3226
3432
 
3227
3433
  // src/control/aliases.ts
3228
- var aliasFile = (controlDir) => path12.join(controlDir, "aliases.json");
3434
+ var aliasFile = (controlDir) => path13.join(controlDir, "aliases.json");
3229
3435
  function readAliases(file) {
3230
3436
  try {
3231
3437
  const j = JSON.parse(readFileSync8(file, "utf8"));
@@ -3250,11 +3456,11 @@ function receiverIdOf(all, harness, id) {
3250
3456
  // src/control/local.ts
3251
3457
  import net from "node:net";
3252
3458
  import os2 from "node:os";
3253
- import path13 from "node:path";
3459
+ import path14 from "node:path";
3254
3460
  function socketPath(state, env = process.env) {
3255
3461
  if (env.SESSIONPIPE_SOCKET) return env.SESSIONPIPE_SOCKET;
3256
3462
  if (process.platform === "win32") return `\\\\.\\pipe\\sessionpipe-control-${os2.userInfo().username}`;
3257
- return path13.join(state, "control.sock");
3463
+ return path14.join(state, "control.sock");
3258
3464
  }
3259
3465
  function ask(sock, req, timeoutMs) {
3260
3466
  return new Promise((resolve) => {
@@ -3305,8 +3511,8 @@ async function tellDaemon(state, job, session, cwd, transcript) {
3305
3511
  function enqueueJob(state, job) {
3306
3512
  const jobs = jobsDir(state);
3307
3513
  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 });
3514
+ const f = path15.join(jobs, `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}.json`);
3515
+ writeFileSync8(f, JSON.stringify(job), { mode: 384 });
3310
3516
  return f;
3311
3517
  }
3312
3518
  function releaseJob(claimed) {
@@ -3319,9 +3525,9 @@ function releaseJob(claimed) {
3319
3525
  function log(state, line) {
3320
3526
  try {
3321
3527
  mkdirSync8(state, { recursive: true, mode: 448 });
3322
- const f = path14.join(state, "log");
3528
+ const f = path15.join(state, "log");
3323
3529
  try {
3324
- if (statSync5(f).size > 1024 * 1024) renameSync3(f, `${f}.1`);
3530
+ if (statSync6(f).size > 1024 * 1024) renameSync3(f, `${f}.1`);
3325
3531
  } catch {
3326
3532
  }
3327
3533
  appendFileSync3(f, `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
@@ -3330,7 +3536,7 @@ function log(state, line) {
3330
3536
  }
3331
3537
  }
3332
3538
  function factsState(state) {
3333
- const file = (h, s) => path14.join(state, "facts", safeId(h), `${safeId(s)}.json`);
3539
+ const file = (h, s) => path15.join(state, "facts", safeId(h), `${safeId(s)}.json`);
3334
3540
  return {
3335
3541
  get(h, s) {
3336
3542
  try {
@@ -3343,8 +3549,8 @@ function factsState(state) {
3343
3549
  const cur = this.get(h, s);
3344
3550
  const next = { ...cur, ...patch };
3345
3551
  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 });
3552
+ mkdirSync8(path15.dirname(file(h, s)), { recursive: true, mode: 448 });
3553
+ writeFileSync8(file(h, s), JSON.stringify(next), { mode: 384 });
3348
3554
  }
3349
3555
  };
3350
3556
  }
@@ -3416,7 +3622,7 @@ async function runJob(job, opts = {}) {
3416
3622
  );
3417
3623
  return { events: [], delivered: {} };
3418
3624
  }
3419
- const id = receiverIdOf(readAliases(aliasFile(path14.join(state, "control"))), job.harness, r.session.id) ?? r.session.id;
3625
+ const id = receiverIdOf(readAliases(aliasFile(path15.join(state, "control"))), job.harness, r.session.id) ?? r.session.id;
3420
3626
  const claudeEnd = job.harness === "claude-code" && r.events.some((e) => e.type === "session.ended");
3421
3627
  if (!claudeEnd) await tellDaemon(state, job, id, r.session.cwd, r.transcript);
3422
3628
  const outbox = new Outbox(state);
@@ -3445,12 +3651,15 @@ async function runJob(job, opts = {}) {
3445
3651
  const heavy = !/^(PreToolUse|PostToolUse|PostToolUseFailure|BeforeTool|AfterTool|PostInvocation)$/.test(job.event);
3446
3652
  let learned = {};
3447
3653
  if (heavy) {
3448
- const f = adapter.facts(r.session, r.transcript, r.hints ?? {}, facts, { ...process.env, ...job.env });
3654
+ const hookEnv = { ...process.env, ...job.env };
3655
+ const f = adapter.facts(r.session, r.transcript, r.hints ?? {}, facts, hookEnv);
3656
+ if (cfg.remote_titles && adapter.remoteFacts)
3657
+ Object.assign(f, await adapter.remoteFacts(r.session, f, r.transcript, facts, hookEnv));
3449
3658
  const { started_at: _s, last_at: _l, first_ask: _fa, harness_version, ...rest } = f;
3450
3659
  learned = rest;
3451
3660
  if (harness_version) facts.save(job.harness, id, { harness_version });
3452
3661
  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))
3662
+ if (CWD_FROM_HOOKS.has(job.harness) && r.session.cwd && path15.isAbsolute(r.session.cwd))
3454
3663
  facts.save(job.harness, id, { cwd: r.session.cwd });
3455
3664
  }
3456
3665
  const known = mem.session ?? {};
@@ -3532,6 +3741,23 @@ async function runJob(job, opts = {}) {
3532
3741
  }
3533
3742
  if (read.turns.length) facts.save(job.harness, id, { transcript_line: read.line, transcript_turns: turnNo });
3534
3743
  }
3744
+ if (adapter.limits && r.transcript && ending && cfg.sinks.some((s) => !s.paused && Math.min(s.tier, s.max_tier ?? 3) >= 1)) {
3745
+ const limits = adapter.limits(r.transcript);
3746
+ const sig = limits && JSON.stringify(limits);
3747
+ if (limits && sig !== facts.get(job.harness, id).limits_sig) {
3748
+ events.push(
3749
+ makeEvent(
3750
+ { type: "limits.observed", harnessEvent: job.event, data: limits },
3751
+ {
3752
+ harness: { name: job.harness, ...version ? { version } : {} },
3753
+ session: { ...session, seq: outbox.nextSeq(ref) },
3754
+ now: opts.now ?? job.at
3755
+ }
3756
+ )
3757
+ );
3758
+ facts.save(job.harness, id, { limits_sig: sig });
3759
+ }
3760
+ }
3535
3761
  outbox.append(
3536
3762
  ref,
3537
3763
  events.map((e) => ({ ...e, session: redactDeep(e.session), data: redactDeep(e.data) }))
@@ -3549,11 +3775,11 @@ async function runJob(job, opts = {}) {
3549
3775
  return { events, delivered };
3550
3776
  }
3551
3777
  async function drain(state, sinkConfigs, outbox, first) {
3552
- const more = path14.join(state, "locks", "flush.more");
3778
+ const more = path15.join(state, "locks", "flush.more");
3553
3779
  let release = takeNamedLock(state, "flush");
3554
3780
  if (!release) {
3555
3781
  try {
3556
- writeFileSync7(more, "", { mode: 384 });
3782
+ writeFileSync8(more, "", { mode: 384 });
3557
3783
  } catch {
3558
3784
  }
3559
3785
  release = takeNamedLock(state, "flush");
@@ -3577,7 +3803,7 @@ async function drain(state, sinkConfigs, outbox, first) {
3577
3803
  }
3578
3804
  }
3579
3805
  var BACKOFF_MS = [2e3, 1e4, 6e4, 5 * 6e4, 30 * 6e4];
3580
- var backoffFile = (state, sink) => path14.join(state, "backoff", `${safeId(sink)}.json`);
3806
+ var backoffFile = (state, sink) => path15.join(state, "backoff", `${safeId(sink)}.json`);
3581
3807
  function readBackoff(state, sink) {
3582
3808
  try {
3583
3809
  const b = JSON.parse(readFileSync9(backoffFile(state, sink), "utf8"));
@@ -3593,8 +3819,8 @@ function noteFailure(state, sink, res, prev) {
3593
3819
  const b = { until: Date.now() + wait, failures, why: `${res.status ?? ""} ${res.error ?? ""}`.trim() };
3594
3820
  const f = backoffFile(state, sink);
3595
3821
  try {
3596
- mkdirSync8(path14.dirname(f), { recursive: true, mode: 448 });
3597
- writeFileSync7(f, JSON.stringify(b), { mode: 384 });
3822
+ mkdirSync8(path15.dirname(f), { recursive: true, mode: 448 });
3823
+ writeFileSync8(f, JSON.stringify(b), { mode: 384 });
3598
3824
  } catch {
3599
3825
  }
3600
3826
  return b;
@@ -3726,7 +3952,7 @@ function applySinkVerdict(name, res) {
3726
3952
  } catch {
3727
3953
  }
3728
3954
  }
3729
- var jobsDir = (state) => path14.join(state, "jobs");
3955
+ var jobsDir = (state) => path15.join(state, "jobs");
3730
3956
  export {
3731
3957
  HttpSink,
3732
3958
  buildSinks,