sessionpipe 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +74 -0
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +5165 -2399
  4. package/dist/cli.js.map +4 -4
  5. package/dist/control/cli.d.ts +4 -0
  6. package/dist/control/cli.d.ts.map +1 -1
  7. package/dist/control/daemon.d.ts +24 -0
  8. package/dist/control/daemon.d.ts.map +1 -1
  9. package/dist/control/local.d.ts +24 -0
  10. package/dist/control/local.d.ts.map +1 -1
  11. package/dist/control/pool.d.ts +49 -0
  12. package/dist/control/pool.d.ts.map +1 -0
  13. package/dist/control/route.d.ts +1 -1
  14. package/dist/control/route.d.ts.map +1 -1
  15. package/dist/help.d.ts.map +1 -1
  16. package/dist/hook.d.ts.map +1 -1
  17. package/dist/hook.js +38 -2
  18. package/dist/hook.js.map +2 -2
  19. package/dist/index.js +1135 -880
  20. package/dist/index.js.map +4 -4
  21. package/dist/pool/cli.d.ts +39 -0
  22. package/dist/pool/cli.d.ts.map +1 -0
  23. package/dist/pool/credentials.d.ts +44 -0
  24. package/dist/pool/credentials.d.ts.map +1 -0
  25. package/dist/pool/import.d.ts +23 -0
  26. package/dist/pool/import.d.ts.map +1 -0
  27. package/dist/pool/index.d.ts +138 -0
  28. package/dist/pool/index.d.ts.map +1 -0
  29. package/dist/pool/install.d.ts +46 -0
  30. package/dist/pool/install.d.ts.map +1 -0
  31. package/dist/pool/login.d.ts +27 -0
  32. package/dist/pool/login.d.ts.map +1 -0
  33. package/dist/pool/policy.d.ts +100 -0
  34. package/dist/pool/policy.d.ts.map +1 -0
  35. package/dist/pool/server.d.ts +76 -0
  36. package/dist/pool/server.d.ts.map +1 -0
  37. package/dist/pool/store.d.ts +89 -0
  38. package/dist/pool/store.d.ts.map +1 -0
  39. package/dist/pool/types.d.ts +92 -0
  40. package/dist/pool/types.d.ts.map +1 -0
  41. package/dist/run.d.ts +14 -1
  42. package/dist/run.d.ts.map +1 -1
  43. package/dist/worker.js +1143 -888
  44. package/dist/worker.js.map +4 -4
  45. package/package.json +1 -1
package/dist/worker.js CHANGED
@@ -1,24 +1,11 @@
1
1
  #!/usr/bin/env node
2
- // sessionpipe 0.10.0 · Apache-2.0 · https://sessionpipe.org
2
+ // sessionpipe 0.12.0 · Apache-2.0 · https://sessionpipe.org
3
3
 
4
4
  // src/worker.ts
5
5
  import { spawn } from "node:child_process";
6
6
  import { realpathSync as realpathSync2 } from "node:fs";
7
7
  import { pathToFileURL } from "node:url";
8
8
 
9
- // ../core/src/adapters/antigravity.ts
10
- import {
11
- existsSync as existsSync2,
12
- mkdirSync as mkdirSync2,
13
- readdirSync as readdirSync2,
14
- readFileSync as readFileSync2,
15
- realpathSync,
16
- statSync as statSync2,
17
- unlinkSync as unlinkSync2,
18
- writeFileSync as writeFileSync2
19
- } from "node:fs";
20
- import path4 from "node:path";
21
-
22
9
  // ../core/src/paths.ts
23
10
  import os from "node:os";
24
11
  import path from "node:path";
@@ -42,6 +29,13 @@ function tilde(p, home = homeDir()) {
42
29
  }
43
30
  var safeId = (s) => String(s).replace(/[^A-Za-z0-9_.-]/g, "_").slice(0, 160);
44
31
 
32
+ // ../core/src/adapters/claude-code.ts
33
+ import { execFileSync as execFileSync2 } from "node:child_process";
34
+ import { createHash } from "node:crypto";
35
+ import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
36
+ import { userInfo } from "node:os";
37
+ import path4 from "node:path";
38
+
45
39
  // ../core/src/readers/files.ts
46
40
  import { closeSync, fstatSync, openSync, readdirSync, readSync, statSync } from "node:fs";
47
41
  import path2 from "node:path";
@@ -132,6 +126,38 @@ function recentFiles(root, depth, sinceMs, match) {
132
126
  return out;
133
127
  }
134
128
 
129
+ // ../core/src/readers/git.ts
130
+ import { execFileSync } from "node:child_process";
131
+ var cache = /* @__PURE__ */ new Map();
132
+ function gitFacts(cwd) {
133
+ if (!cwd) return {};
134
+ const hit = cache.get(cwd);
135
+ if (hit) return hit;
136
+ const run = (args) => {
137
+ try {
138
+ return execFileSync("git", ["-C", cwd, ...args], {
139
+ encoding: "utf8",
140
+ timeout: 1500,
141
+ stdio: ["ignore", "pipe", "ignore"]
142
+ }).trim();
143
+ } catch {
144
+ return "";
145
+ }
146
+ };
147
+ const out = {};
148
+ const repo = run(["remote", "get-url", "origin"]);
149
+ if (repo) out.repo = stripCredentials(repo);
150
+ const branch = run(["rev-parse", "--abbrev-ref", "HEAD"]);
151
+ if (branch && branch !== "HEAD") out.branch = branch;
152
+ cache.set(cwd, out);
153
+ return out;
154
+ }
155
+ function stripCredentials(url) {
156
+ const ssh = /^(?:ssh:\/\/)?git@([^:/]+)[:/](.+?)(?:\.git)?$/.exec(url);
157
+ if (ssh) return `${ssh[1]}/${ssh[2]}`;
158
+ return url.replace(/\/\/[^/@]*@/, "//").replace(/^https?:\/\//, "").replace(/\.git$/, "");
159
+ }
160
+
135
161
  // ../core/src/adapters/codex-rollout.ts
136
162
  function codexSubagent(meta) {
137
163
  if (meta?.type !== "session_meta") return false;
@@ -291,8 +317,10 @@ function readJsonFile(file) {
291
317
  try {
292
318
  raw = readFileSync(file, "utf8");
293
319
  } catch {
294
- return { ok: true, existed: false, value: {}, raw: "", indent: " ", eol: "\n", finalNewline: true };
320
+ return { ok: true, existed: false, value: {}, raw: "", indent: " ", eol: "\n", finalNewline: true, bom: false };
295
321
  }
322
+ const bom = raw.charCodeAt(0) === 65279;
323
+ if (bom) raw = raw.slice(1);
296
324
  let value;
297
325
  try {
298
326
  value = JSON.parse(raw);
@@ -312,12 +340,13 @@ function readJsonFile(file) {
312
340
  raw,
313
341
  indent,
314
342
  eol,
315
- finalNewline: /\r?\n$/.test(raw)
343
+ finalNewline: /\r?\n$/.test(raw),
344
+ bom
316
345
  };
317
346
  }
318
347
  function formatJson(value, f) {
319
348
  const text = JSON.stringify(value, null, f.indent).replace(/\n/g, f.eol);
320
- return f.finalNewline ? text + f.eol : text;
349
+ return (f.bom ? "\uFEFF" : "") + (f.finalNewline ? text + f.eol : text);
321
350
  }
322
351
  function readJson(file) {
323
352
  const f = readJsonFile(file);
@@ -413,74 +442,79 @@ function installedClaudeShaped(file, harness, events, cmd, o) {
413
442
  return current === events.length ? "current" : "stale";
414
443
  }
415
444
 
416
- // ../core/src/adapters/antigravity.ts
417
- var NAME = "antigravity";
418
- var HOOK_NAME = "sessionpipe";
419
- var AG_EVENTS = ["PreInvocation", "PostInvocation", "PostToolUse", "Stop"];
420
- 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';
421
- var antigravityAnswer = (event) => event === "PreToolUse" ? AG_STALE_PRETOOL : "{}\n";
422
- var gemini = (env) => env.GEMINI_CLI_HOME || path4.join(homeDir(), ".gemini");
423
- var hooksFile = (env) => path4.join(gemini(env), "config", "hooks.json");
424
- var projectsDir = (env) => path4.join(gemini(env), "config", "projects");
425
- var dataDirs = (env) => ["antigravity", "antigravity-cli", "antigravity-ide"].map((d) => path4.join(gemini(env), d));
426
- var ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
427
- var validTool = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
428
- function entry(cmd) {
429
- const handler = (e) => ({ type: "command", command: cmd([NAME, e]).command, timeout: 5 });
430
- const out = { enabled: true };
431
- for (const e of AG_EVENTS) out[e] = /Tool/.test(e) ? [{ matcher: "*", hooks: [handler(e)] }] : [handler(e)];
432
- return out;
445
+ // ../core/src/adapters/claude-code.ts
446
+ var NAME = "claude-code";
447
+ var CLAUDE_EVENTS = [
448
+ "SessionStart",
449
+ "UserPromptSubmit",
450
+ "PreToolUse",
451
+ "PostToolUse",
452
+ "PostToolUseFailure",
453
+ "PermissionRequest",
454
+ "Notification",
455
+ "Stop",
456
+ "SubagentStart",
457
+ "SubagentStop",
458
+ "PreCompact",
459
+ "PostCompact",
460
+ "SessionEnd",
461
+ "StopFailure"
462
+ ];
463
+ var LEAN_SKIP = /* @__PURE__ */ new Set(["PreToolUse", "PostToolUseFailure"]);
464
+ var eventsFor = (lean) => lean ? CLAUDE_EVENTS.filter((e) => !LEAN_SKIP.has(e)) : [...CLAUDE_EVENTS];
465
+ var OPTS = {
466
+ matcherFor: (e) => /Tool|Permission/.test(e) ? "" : void 0,
467
+ timeout: 5,
468
+ // The hook waits up to 125 s for a verified control answer, and only when control
469
+ // is paired on this machine; everywhere else it exits in milliseconds as before.
470
+ timeoutFor: (e) => e === "PermissionRequest" ? 130 : void 0
471
+ };
472
+ var DESKTOP_ID = /^local_[A-Za-z0-9-]{1,64}$/;
473
+ function claudeDirs(env = process.env, home = env.HOME || homeDir()) {
474
+ const out = [];
475
+ const add = (d) => {
476
+ if (!d) return;
477
+ const r = path4.resolve(d);
478
+ if (!out.includes(r)) out.push(r);
479
+ };
480
+ add(path4.join(home, ".claude"));
481
+ add(env.CLAUDE_CONFIG_DIR);
482
+ try {
483
+ for (const e of readdirSync2(home, { withFileTypes: true })) {
484
+ if (!e.isDirectory() || !/^\.claude[-_.]/.test(e.name)) continue;
485
+ const d = path4.join(home, e.name);
486
+ if (existsSync2(path4.join(d, "projects")) || existsSync2(path4.join(d, "settings.json"))) add(d);
487
+ }
488
+ } catch {
489
+ }
490
+ return out.filter((d, i) => i === 0 || existsSync2(d));
433
491
  }
434
- var antigravity = {
492
+ var settingsIn = (dir) => path4.join(dir, "settings.json");
493
+ var configDirOf = (transcript) => path4.dirname(path4.dirname(path4.dirname(transcript)));
494
+ var validTool = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
495
+ var endReason = (r) => r === "clear" || r === "logout" || r === "exit" ? r : r === "prompt_input_exit" ? "exit" : "other";
496
+ var notifKind = (t) => t === "permission_prompt" ? "permission" : t === "idle_prompt" ? "idle" : t === "elicitation_dialog" ? "elicitation" : "question";
497
+ var claudeCode = {
435
498
  name: NAME,
436
- events: AG_EVENTS,
437
- detect: (env = process.env) => existsSync2(path4.join(gemini(env), "config")) || dataDirs(env).some((d) => existsSync2(d)),
438
- configFiles: (env = process.env) => [hooksFile(env)],
439
- install(cmd, env = process.env) {
440
- const file = hooksFile(env);
441
- const f = readJsonFile(file);
442
- if (!f.ok) return [{ file, changed: false, skipped: true, note: `${f.reason}; not touched` }];
443
- const hooks = f.value;
444
- const e = entry(cmd);
445
- if (JSON.stringify(hooks[HOOK_NAME]) === JSON.stringify(e)) return [{ file, changed: false }];
446
- hooks[HOOK_NAME] = e;
447
- mkdirSync2(path4.dirname(file), { recursive: true });
448
- writeFileSync2(file, formatJson(hooks, f));
449
- return [
450
- {
451
- file,
452
- changed: true,
453
- created: !f.existed,
454
- note: "hooks.json is read when a conversation starts; open ones report after a restart"
455
- }
456
- ];
457
- },
458
- uninstall(env = process.env, opts = {}) {
459
- const file = hooksFile(env);
460
- if (!existsSync2(file)) return [{ file, changed: false }];
461
- const f = readJsonFile(file);
462
- if (!f.ok) return [{ file, changed: false, skipped: true, note: `${f.reason}; not touched` }];
463
- const hooks = f.value;
464
- if (!(HOOK_NAME in hooks)) return [{ file, changed: false }];
465
- delete hooks[HOOK_NAME];
466
- if (opts.created?.includes(file) && !Object.keys(hooks).length) {
467
- unlinkSync2(file);
468
- return [{ file, changed: true, note: "removed (sessionpipe created it)" }];
469
- }
470
- writeFileSync2(file, formatJson(hooks, f));
471
- return [{ file, changed: true }];
472
- },
473
- installed(cmd, env = process.env) {
474
- const file = hooksFile(env);
475
- const f = readJsonFile(file);
476
- const have = f.ok ? f.value[HOOK_NAME] : void 0;
477
- return [
478
- {
479
- file,
480
- state: !have ? "missing" : JSON.stringify(have) === JSON.stringify(entry(cmd)) ? "current" : "stale"
481
- }
482
- ];
483
- },
499
+ events: CLAUDE_EVENTS,
500
+ detect: (env = process.env) => claudeDirs(env).some((d) => existsSync2(d)),
501
+ configFiles: (env = process.env) => claudeDirs(env).map(settingsIn),
502
+ install: (cmd, env = process.env, opts = {}) => claudeDirs(env).flatMap((d) => {
503
+ const hooks = installClaudeShaped(settingsIn(d), NAME, eventsFor(opts.lean), cmd, OPTS);
504
+ const line = opts.statusLine === void 0 ? null : statusLine(settingsIn(d), opts.statusLine, cmd);
505
+ return line ? [hooks, line] : [hooks];
506
+ }),
507
+ uninstall: (env = process.env, opts = {}) => claudeDirs(env).map((d) => {
508
+ const line = statusLine(settingsIn(d), false);
509
+ const hooks = uninstallClaudeShaped(settingsIn(d), CLAUDE_EVENTS, {
510
+ created: opts.created?.includes(settingsIn(d))
511
+ });
512
+ return line && !hooks.changed ? line : hooks;
513
+ }),
514
+ installed: (cmd, env = process.env, opts = {}) => claudeDirs(env).map((d) => ({
515
+ file: settingsIn(d),
516
+ state: installedClaudeShaped(settingsIn(d), NAME, eventsFor(opts.lean), cmd, OPTS)
517
+ })),
484
518
  fromHook(input) {
485
519
  const event = input.argv[1] ?? "";
486
520
  let s = {};
@@ -489,816 +523,970 @@ var antigravity = {
489
523
  if (j && typeof j === "object" && !Array.isArray(j)) s = j;
490
524
  } catch {
491
525
  }
492
- const id = s.conversationId ?? s.session_id ?? s.sessionId;
493
- const stdout = antigravityAnswer(event);
494
- if (!id) return { session: { id: "" }, events: [], stdout };
495
- const workspaces = agWorkspaces(s.workspacePaths);
496
- const session = { id: String(id), cwd: workspaces[0] ?? input.cwd };
497
- if (typeof s.modelName === "string" && s.modelName !== "auto") session.model = s.modelName;
498
- const toolCall = s.toolCall;
499
- const tool = validTool(toolCall?.name) ? toolCall?.name : validTool(s.toolName) ? s.toolName : void 0;
526
+ const id = s.session_id ?? s.sessionId;
527
+ if (!id) return null;
528
+ if (notClaudeCode(String(id), s)) return null;
529
+ const session = { id: String(id), cwd: typeof s.cwd === "string" ? s.cwd : input.cwd };
530
+ if (typeof s.model === "string") session.model = s.model;
531
+ if (typeof s.agent_id === "string" && event.startsWith("Subagent") === false && s.agent_id)
532
+ session.parent_id = null;
533
+ const turn_id = typeof s.prompt_id === "string" ? s.prompt_id : void 0;
534
+ const tool = validTool(s.tool_name) ? s.tool_name : void 0;
535
+ const call_id = typeof s.tool_use_id === "string" ? s.tool_use_id : void 0;
536
+ const ms = typeof s.duration_ms === "number" ? Math.round(s.duration_ms) : void 0;
537
+ const agent_id = typeof s.agent_id === "string" ? s.agent_id : void 0;
538
+ const permissionId = () => `perm-${createHash("sha256").update(`${turn_id ?? ""}|${tool ?? ""}|${JSON.stringify(s.tool_input ?? null)}`).digest("hex").slice(0, 16)}`;
500
539
  const ev = (type, data) => ({ type, data: prune(data), harnessEvent: event });
501
540
  const events = [];
502
541
  switch (event) {
503
- case "PreInvocation":
504
- if (Number(s.invocationNum) === 0 || s.invocationNum === void 0) {
505
- events.push(ev("session.started", { source: "unknown" }));
506
- events.push(ev("turn.started", {}));
507
- } else events.push(ev("session.heartbeat", {}));
542
+ case "SessionStart":
543
+ events.push(ev("session.started", { source: pickSource(s.source ?? s.trigger) }));
508
544
  break;
509
- case "PostInvocation":
545
+ case "UserPromptSubmit": {
546
+ const prompt = typeof s.prompt === "string" ? s.prompt : void 0;
547
+ events.push(
548
+ ev("turn.started", {
549
+ turn_id,
550
+ prompt_chars: prompt === void 0 ? void 0 : [...prompt].length,
551
+ // Claude Code's hook schema documents `source`
552
+ // (user | sdk | system | loop_wakeup | schedule_wakeup | poll_event) and says a
553
+ // payload may omit it while the field rolls out; 2.1.304's payload has no such
554
+ // key at all. Forwarded verbatim, shape-guarded only: the list is the harness's
555
+ // to extend.
556
+ submitted_by: harnessWord(s.source),
557
+ // One of Claude Code's own prompt wrappers: the prompt is a block the harness
558
+ // wrote, so it is not the person's words. Absent is unknown, never "the person" —
559
+ // and `true` is never "no person caused this turn", because a Talk or
560
+ // `sessionpipe wait` delivery hands the person's own message over inside a
561
+ // <task-notification> (PROTOCOL.md §4).
562
+ injected: prompt !== void 0 && injectedBlock(prompt) ? true : void 0
563
+ })
564
+ );
510
565
  break;
566
+ }
511
567
  case "PreToolUse":
512
- if (tool) events.push(ev("tool.started", { tool, input: toolCall?.args }));
568
+ if (tool) events.push(ev("tool.started", { tool, call_id, turn_id, agent_id, input: s.tool_input }));
513
569
  break;
514
570
  case "PostToolUse":
515
- if (!tool) events.push(ev("session.heartbeat", {}));
516
- else
571
+ if (tool)
517
572
  events.push(
518
573
  ev("tool.ended", {
519
574
  tool,
520
- ok: !s.error,
521
- error: typeof s.error === "string" ? s.error.slice(0, 2e3) : void 0,
522
- input: toolCall?.args
575
+ call_id,
576
+ turn_id,
577
+ agent_id,
578
+ ms,
579
+ ok: true,
580
+ input: s.tool_input,
581
+ output: s.tool_response
582
+ })
583
+ );
584
+ break;
585
+ case "PostToolUseFailure":
586
+ if (tool)
587
+ events.push(
588
+ ev("tool.ended", {
589
+ tool,
590
+ call_id,
591
+ turn_id,
592
+ agent_id,
593
+ ms,
594
+ ok: false,
595
+ error: errText(s.error ?? s.tool_response),
596
+ input: s.tool_input,
597
+ output: s.tool_response
523
598
  })
524
599
  );
525
600
  break;
601
+ case "PermissionRequest":
602
+ events.push(
603
+ ev("attention.needed", {
604
+ attention_id: call_id ?? permissionId(),
605
+ kind: "permission",
606
+ tool,
607
+ message: tool ? `${tool}${describe(s.tool_input)}` : void 0
608
+ })
609
+ );
610
+ break;
611
+ case "Notification":
612
+ events.push(
613
+ ev("attention.needed", {
614
+ attention_id: `notif-${Date.now()}`,
615
+ kind: notifKind(s.notification_type),
616
+ message: typeof s.message === "string" ? s.message.slice(0, 200) : void 0
617
+ })
618
+ );
619
+ break;
526
620
  case "Stop":
527
- events.push(ev("turn.ended", { reason: s.error ? "error" : "stop" }));
621
+ events.push(ev("turn.ended", { turn_id, reason: "stop", background: backgroundCount(s.background_tasks) }));
528
622
  break;
529
- default:
530
- return { session, events: [], stdout };
531
- }
532
- const r = { session, events, stdout, hints: { workspaces } };
533
- if (typeof s.transcriptPath === "string") r.transcript = s.transcriptPath;
534
- return r;
535
- },
536
- readTranscript(file, fromLine) {
623
+ case "StopFailure":
624
+ events.push(
625
+ ev("turn.ended", {
626
+ turn_id,
627
+ reason: "error",
628
+ error: typeof s.error === "string" ? s.error.slice(0, 200) : void 0
629
+ })
630
+ );
631
+ break;
632
+ case "SubagentStart":
633
+ events.push(
634
+ ev("subagent.started", {
635
+ agent_id: String(s.agent_id ?? ""),
636
+ agent_type: typeof s.agent_type === "string" ? s.agent_type : void 0
637
+ })
638
+ );
639
+ break;
640
+ case "SubagentStop":
641
+ events.push(
642
+ ev("subagent.ended", {
643
+ agent_id: String(s.agent_id ?? ""),
644
+ agent_type: typeof s.agent_type === "string" ? s.agent_type : void 0,
645
+ background: backgroundCount(s.background_tasks, String(s.agent_id ?? ""))
646
+ })
647
+ );
648
+ break;
649
+ case "PreCompact":
650
+ events.push(ev("context.compacted", { trigger: s.trigger === "manual" ? "manual" : "auto" }));
651
+ break;
652
+ case "PostCompact":
653
+ break;
654
+ // PreCompact already said it; a second event would double-count
655
+ case "SessionEnd":
656
+ events.push(ev("session.ended", { reason: endReason(s.reason) }));
657
+ break;
658
+ case "StatusLine": {
659
+ const limits = claudeLimits(s.rate_limits);
660
+ if (!limits) return null;
661
+ events.push(ev("limits.observed", limits));
662
+ break;
663
+ }
664
+ default:
665
+ return null;
666
+ }
667
+ const hints = {};
668
+ if (event === "UserPromptSubmit" && typeof s.prompt === "string") hints.prompt = s.prompt;
669
+ const host = input.env.CLAUDE_CODE_HOST_SESSION_ID;
670
+ if (host && DESKTOP_ID.test(host)) hints.desktop_id = host;
671
+ const attended = input.env.CLAUDE_CODE_SESSION_ATTENDED;
672
+ if (attended === "1" || attended === "0") hints.attended = attended === "1" || !!input.env.SESSIONPIPE_CONTROL_JOB;
673
+ if (event !== "PreToolUse" && event !== "PostToolUse") hints.claude_pid = claudePid(input.env);
674
+ const r = { session, events, hints };
675
+ if (typeof s.transcript_path === "string") r.transcript = s.transcript_path;
676
+ return r;
677
+ },
678
+ readTranscript(file, fromLine) {
537
679
  let text = "";
538
680
  try {
539
681
  text = readFileSync2(file, "utf8");
540
682
  } catch {
541
683
  return { turns: [], line: fromLine };
542
684
  }
543
- return readAntigravity(text.split("\n"), fromLine);
685
+ return readClaude(text.split("\n"), fromLine);
544
686
  },
545
- facts(session, transcript, hints) {
546
- if (!transcript) return {};
547
- return antigravityFacts(transcript, {
548
- conversation: session.id,
549
- workspaces: hints.workspaces ?? (session.cwd ? [session.cwd] : []),
550
- env: process.env
551
- });
687
+ facts(session, transcript, hints, state) {
688
+ const out = {};
689
+ if (transcript) Object.assign(out, claudeFacts(transcript, session.id, hints, state));
690
+ const cwd = out.cwd ?? session.cwd;
691
+ Object.assign(out, gitFacts(cwd), out.branch ? { branch: out.branch } : {});
692
+ if (!out.url && typeof hints.desktop_id === "string") out.url = `claude://code/continue/${hints.desktop_id}`;
693
+ if (typeof hints.attended === "boolean") out.attended = hints.attended || !!out.url;
694
+ return out;
552
695
  },
696
+ remoteFacts: (session, local, transcript, state, env = process.env) => claudeRemoteFacts(session.id, local, transcript, state, env),
553
697
  backfill(sinceMs, env = process.env) {
554
698
  const rows2 = [];
555
- for (const data of dataDirs(env)) {
556
- let ids = [];
557
- try {
558
- ids = readdirSync2(path4.join(data, "brain")).filter((n) => /^[0-9a-f-]{36}$/i.test(n));
559
- } catch {
560
- }
561
- for (const id of ids) {
562
- const f = path4.join(data, "brain", id, ".system_generated", "logs", "transcript.jsonl");
563
- try {
564
- if (statSync2(f).mtimeMs < sinceMs) continue;
565
- } catch {
566
- continue;
567
- }
568
- const facts = antigravityFacts(f, { conversation: id, workspaces: [], env });
569
- if (!facts.started_at || !facts.last_at) continue;
570
- const { started_at, last_at, first_ask: _fa, harness_version: _hv, ...rest } = facts;
571
- rows2.push({ session: { id, ...rest }, started_at, last_at });
572
- }
699
+ const files = [];
700
+ for (const d of claudeDirs(env))
701
+ files.push(...recentFiles(path4.join(d, "projects"), 1, sinceMs, (n) => /^[0-9a-f-]{36}\.jsonl$/.test(n)));
702
+ const noState = { get: () => ({}), save: () => {
703
+ } };
704
+ for (const f of files) {
705
+ const id = path4.basename(f, ".jsonl");
706
+ const facts = claudeFacts(f, id, {}, noState);
707
+ if (!facts.started_at || !facts.last_at) continue;
708
+ const { started_at, last_at, first_ask: _fa, harness_version: _hv, ...rest } = facts;
709
+ const cwd = rest.cwd;
710
+ rows2.push({
711
+ session: { id, ...rest, ...gitFacts(cwd), ...rest.branch ? { branch: rest.branch } : {} },
712
+ started_at,
713
+ last_at,
714
+ transcript: f
715
+ });
573
716
  }
574
717
  return rows2;
575
718
  }
576
719
  };
720
+ var WAITER_RE = /(?:^|[\s"'`/\\])sessionpipe(?:\.\w+)?["']?\s+wait(?:\s|$)/;
721
+ function backgroundCount(tasks, ending) {
722
+ if (!Array.isArray(tasks)) return void 0;
723
+ return tasks.filter((t) => {
724
+ const task = t;
725
+ if (ending !== void 0 && task?.id === ending) return false;
726
+ return !(typeof task?.command === "string" && WAITER_RE.test(task.command));
727
+ }).length;
728
+ }
729
+ var LINE_REFRESH_S = 60;
730
+ function statusLine(file, want, cmd) {
731
+ const f = readJsonFile(file);
732
+ if (!f.ok) return null;
733
+ const cur = f.value.statusLine;
734
+ const ours = typeof cur?.command === "string" && OURS.test(cur.command);
735
+ const command = cmd?.([NAME, "StatusLine"]).command;
736
+ if (want && cur && !ours)
737
+ return { file, changed: false, skipped: true, note: "a status line of your own is set; limits are not read" };
738
+ if (want ? ours && cur?.command === command && cur?.refreshInterval === LINE_REFRESH_S : !ours) return null;
739
+ if (want) f.value.statusLine = { type: "command", command, refreshInterval: LINE_REFRESH_S };
740
+ else delete f.value.statusLine;
741
+ writeFileSync2(file, formatJson(f.value, f));
742
+ return { file, changed: true, note: want ? "status line written (account limits)" : "status line removed" };
743
+ }
744
+ var epochIso = (t) => typeof t === "number" && t > 0 && t < 1e11 ? new Date(t * 1e3).toISOString() : void 0;
745
+ var WINDOW_MINUTES = { five_hour: 300, seven_day: 10080 };
746
+ function claudeLimits(rl) {
747
+ if (!rl || typeof rl !== "object" || Array.isArray(rl)) return null;
748
+ const windows = [];
749
+ for (const [id, w] of Object.entries(rl)) {
750
+ if (!/^[a-z0-9_]{1,64}$/.test(id) || typeof w?.used_percentage !== "number" || w.used_percentage < 0) continue;
751
+ windows.push(
752
+ prune({
753
+ id,
754
+ used_percent: w.used_percentage,
755
+ window_minutes: WINDOW_MINUTES[id],
756
+ resets_at: epochIso(w.resets_at)
757
+ })
758
+ );
759
+ }
760
+ return windows.length ? { windows: windows.slice(0, 8) } : null;
761
+ }
577
762
  function prune(o) {
578
763
  for (const k of Object.keys(o)) if (o[k] === void 0) delete o[k];
579
764
  return o;
580
765
  }
581
- function agPath(p) {
582
- if (typeof p !== "string" || !p) return null;
583
- let s = p;
584
- if (/^file:/i.test(s)) {
585
- try {
586
- s = decodeURIComponent(new URL(s).pathname);
587
- } catch {
588
- return null;
589
- }
590
- if (process.platform === "win32" && /^\/[A-Za-z]:/.test(s)) s = s.slice(1);
591
- } else if (/^[a-z][a-z0-9+.-]*:\/\//i.test(s)) return null;
592
- if (!path4.isAbsolute(s)) return null;
593
- return path4.resolve(s);
766
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
767
+ function notClaudeCode(id, s) {
768
+ return typeof s.transcript_path !== "string" && !UUID.test(id);
594
769
  }
595
- var agWorkspaces = (paths) => (Array.isArray(paths) ? paths : []).map(agPath).filter((p) => !!p).filter((p, i, a) => a.indexOf(p) === i);
596
- function agVariants(p) {
597
- const out = [p];
598
- try {
599
- const r = realpathSync.native(p);
600
- if (r !== p) out.push(r);
601
- } catch {
770
+ function pickSource(v) {
771
+ return typeof v === "string" && ["startup", "resume", "clear", "compact", "fork"].includes(v) ? v : "unknown";
772
+ }
773
+ var harnessWord = (v) => typeof v === "string" && /^[A-Za-z0-9][A-Za-z0-9_.-]{0,39}$/.test(v) ? v : void 0;
774
+ var WRAPPER = /^<(?:task-notification|cross-session-message|system-reminder|local-command-stdout|command-[a-z-]+)[\s/>]/i;
775
+ var injectedBlock = (prompt) => WRAPPER.test(prompt.trim());
776
+ function errText(v) {
777
+ if (typeof v === "string") return v.slice(0, 2e3);
778
+ if (v && typeof v === "object") {
779
+ const e = v;
780
+ if (typeof e.error === "string") return e.error.slice(0, 2e3);
781
+ if (typeof e.message === "string") return e.message.slice(0, 2e3);
602
782
  }
603
- return out;
783
+ return void 0;
604
784
  }
605
- function agProjects(dir) {
606
- const out = [];
785
+ function describe(input) {
786
+ if (!input || typeof input !== "object") return "";
787
+ const i = input;
788
+ const v = i.command ?? i.file_path ?? i.path ?? i.url ?? i.pattern;
789
+ return typeof v === "string" ? ` ${v.slice(0, 120)}` : "";
790
+ }
791
+ var NAME_RANK = {
792
+ user: "custom",
793
+ auto: "harness",
794
+ hook: "harness",
795
+ peer: "harness"
796
+ };
797
+ function registryEntry(j) {
798
+ if (!j || typeof j !== "object") return null;
799
+ const r = j;
800
+ const source = r.nameSource ? NAME_RANK[r.nameSource] : void 0;
801
+ return {
802
+ sessionId: r.sessionId ? String(r.sessionId) : null,
803
+ name: source && typeof r.name === "string" && r.name.trim() ? r.name.trim() : null,
804
+ name_source: source ?? null,
805
+ bridge: typeof r.bridgeSessionId === "string" ? r.bridgeSessionId : null
806
+ };
807
+ }
808
+ var liveCache = /* @__PURE__ */ new Map();
809
+ function claudeLive(dir) {
810
+ const hit = liveCache.get(dir);
811
+ if (hit) return hit;
812
+ const map = /* @__PURE__ */ new Map();
607
813
  let names = [];
608
814
  try {
609
- names = readdirSync2(dir).filter((n) => n.endsWith(".json"));
815
+ names = readdirSync2(path4.join(dir, "sessions")).filter((n) => n.endsWith(".json"));
610
816
  } catch {
611
- return out;
612
817
  }
613
818
  for (const n of names) {
614
- const id = n.slice(0, -5);
615
- if (!ID_RE.test(id)) continue;
616
- let j;
617
819
  try {
618
- j = JSON.parse(readFileSync2(path4.join(dir, n), "utf8"));
820
+ const e = registryEntry(JSON.parse(readFileSync2(path4.join(dir, "sessions", n), "utf8")));
821
+ if (e?.sessionId) map.set(e.sessionId, e);
619
822
  } catch {
620
- continue;
621
- }
622
- const folders = [];
623
- for (const r of j?.projectResources?.resources ?? []) {
624
- const f = agPath(r?.folderUri ?? r?.gitFolder?.folderUri);
625
- if (f) folders.push(...agVariants(f));
626
823
  }
627
- if (folders.length) out.push({ id, folders });
628
824
  }
629
- return out;
825
+ liveCache.set(dir, map);
826
+ return map;
630
827
  }
631
- function agMatchProject(projects, workspaces) {
632
- const ws = [];
633
- for (const w of workspaces) {
634
- const p = agPath(w);
635
- if (p) ws.push(...agVariants(p));
828
+ function claudeLiveByPid(dir, pid) {
829
+ if (!pid) return null;
830
+ try {
831
+ return registryEntry(JSON.parse(readFileSync2(path4.join(dir, "sessions", `${pid}.json`), "utf8")));
832
+ } catch {
833
+ return null;
636
834
  }
637
- let best = null;
638
- let bestLen = -1;
639
- for (const pr of projects)
640
- for (const f of pr.folders) {
641
- const within = ws.some((w) => w === f || w.startsWith(f.endsWith(path4.sep) ? f : f + path4.sep));
642
- if (within && f.length > bestLen) {
643
- best = pr.id;
644
- bestLen = f.length;
835
+ }
836
+ function claudePid(env = process.env) {
837
+ try {
838
+ const dirs = claudeDirs(env);
839
+ const has = (pid2) => dirs.some((d) => existsSync2(path4.join(d, "sessions", `${pid2}.json`)));
840
+ let pid = process.ppid;
841
+ if (has(pid)) return pid;
842
+ const parent = parentOf();
843
+ for (let hop = 0; hop < 6 && pid > 1; hop++) {
844
+ pid = parent(pid);
845
+ if (pid > 1 && has(pid)) return pid;
846
+ }
847
+ } catch {
848
+ }
849
+ return null;
850
+ }
851
+ function parentOf() {
852
+ if (process.platform === "linux") {
853
+ return (pid) => {
854
+ try {
855
+ const st = readFileSync2(`/proc/${pid}/stat`, "utf8");
856
+ return Number(st.slice(st.lastIndexOf(")") + 2).split(" ")[1]) || 0;
857
+ } catch {
858
+ return 0;
645
859
  }
860
+ };
861
+ }
862
+ const table = /* @__PURE__ */ new Map();
863
+ try {
864
+ const out = execFileSync2("ps", ["-axo", "pid=,ppid="], {
865
+ encoding: "utf8",
866
+ timeout: 2e3,
867
+ maxBuffer: 8 * 1024 * 1024,
868
+ stdio: ["ignore", "pipe", "ignore"]
869
+ });
870
+ for (const l of out.split("\n")) {
871
+ const m = /^\s*(\d+)\s+(\d+)/.exec(l);
872
+ if (m) table.set(Number(m[1]), Number(m[2]));
646
873
  }
647
- return best;
874
+ } catch {
875
+ }
876
+ return (pid) => table.get(pid) || 0;
648
877
  }
649
- function antigravityRemoteUrl(transcript, opts) {
650
- const i = transcript.indexOf(`${path4.sep}brain${path4.sep}`);
651
- const dirs = (i > 0 ? [transcript.slice(0, i)] : []).concat(dataDirs(opts.env));
652
- for (const d of dirs) {
878
+ var EMAIL_RE = /^[^@\s]{1,64}@[^@\s]{1,189}$/;
879
+ function claudeLogin(dir, home = homeDir()) {
880
+ const files = [path4.join(dir, ".claude.json")];
881
+ if (path4.resolve(dir) === path4.join(home, ".claude")) files.unshift(path4.join(home, ".claude.json"));
882
+ for (const f of files) {
653
883
  try {
654
- const m = /installation_uuid:\s*"([0-9a-f-]{36})"/i.exec(
655
- readFileSync2(path4.join(d, "antigravity_state.pbtxt"), "utf8")
656
- );
657
- if (!m) continue;
658
- const base = `https://antigravity.google.com/r/${m[1]}-v2`;
659
- const conv = opts.conversation ?? "";
660
- if (!ID_RE.test(conv) || !opts.workspaces?.length) return base;
661
- const project = agMatchProject(agProjects(opts.projectsDir ?? projectsDir(opts.env)), opts.workspaces);
662
- return project ? `${base}?p=${encodeURIComponent(`c/${conv}?section=${project}`)}` : base;
884
+ const a = JSON.parse(readFileSync2(f, "utf8")).oauthAccount;
885
+ if (typeof a?.accountUuid === "string" && a.accountUuid) {
886
+ const email = typeof a.emailAddress === "string" && EMAIL_RE.test(a.emailAddress) ? a.emailAddress : void 0;
887
+ return { id: a.accountUuid.slice(0, 128), ...email ? { email } : {} };
888
+ }
663
889
  } catch {
664
890
  }
665
891
  }
666
- return null;
892
+ return apiAccount(dir);
667
893
  }
668
- function antigravityFacts(file, opts) {
669
- const { head, tail } = ends(file, 128 * 1024, 64 * 1024);
670
- const H = parseLines(head);
671
- const T = parseLines(tail);
672
- const out = {};
673
- const url = antigravityRemoteUrl(file, opts);
674
- if (url) out.url = url;
675
- for (const j of H) {
676
- if (j.type !== "USER_INPUT" || typeof j.content !== "string") continue;
677
- const m = /<USER_REQUEST>([\s\S]*?)(?:<\/USER_REQUEST>|$)/.exec(j.content);
678
- const text = (m ? m[1] : j.content).trim();
679
- const line = (text.split("\n").find((l) => l.trim()) ?? "").trim();
680
- if (!line || /^<[a-z_-]+>/i.test(line)) continue;
681
- out.title = line.length > 80 ? `${line.slice(0, 80).replace(/\s+\S*$/, "")}\u2026` : line;
682
- out.title_source = "first-ask";
683
- break;
684
- }
685
- const times = H.concat(T).map((j) => Date.parse(String(j.created_at ?? j.timestamp ?? ""))).filter((n) => n > 0);
686
- if (times.length) {
687
- out.started_at = Math.min(...times);
688
- out.last_at = Math.max(...times);
689
- }
690
- return out;
691
- }
692
-
693
- // ../core/src/adapters/claude-code.ts
694
- import { execFileSync as execFileSync2 } from "node:child_process";
695
- import { createHash } from "node:crypto";
696
- import { existsSync as existsSync3, readdirSync as readdirSync3, readFileSync as readFileSync3, statSync as statSync3 } from "node:fs";
697
- import path5 from "node:path";
698
-
699
- // ../core/src/readers/git.ts
700
- import { execFileSync } from "node:child_process";
701
- var cache = /* @__PURE__ */ new Map();
702
- function gitFacts(cwd) {
703
- if (!cwd) return {};
704
- const hit = cache.get(cwd);
705
- if (hit) return hit;
706
- const run = (args) => {
707
- try {
708
- return execFileSync("git", ["-C", cwd, ...args], {
709
- encoding: "utf8",
710
- timeout: 1500,
711
- stdio: ["ignore", "pipe", "ignore"]
712
- }).trim();
713
- } catch {
714
- return "";
715
- }
716
- };
717
- const out = {};
718
- const repo = run(["remote", "get-url", "origin"]);
719
- if (repo) out.repo = stripCredentials(repo);
720
- const branch = run(["rev-parse", "--abbrev-ref", "HEAD"]);
721
- if (branch && branch !== "HEAD") out.branch = branch;
722
- cache.set(cwd, out);
723
- return out;
724
- }
725
- function stripCredentials(url) {
726
- const ssh = /^(?:ssh:\/\/)?git@([^:/]+)[:/](.+?)(?:\.git)?$/.exec(url);
727
- if (ssh) return `${ssh[1]}/${ssh[2]}`;
728
- return url.replace(/\/\/[^/@]*@/, "//").replace(/^https?:\/\//, "").replace(/\.git$/, "");
729
- }
730
-
731
- // ../core/src/adapters/claude-code.ts
732
- var NAME2 = "claude-code";
733
- var CLAUDE_EVENTS = [
734
- "SessionStart",
735
- "UserPromptSubmit",
736
- "PreToolUse",
737
- "PostToolUse",
738
- "PostToolUseFailure",
739
- "PermissionRequest",
740
- "Notification",
741
- "Stop",
742
- "SubagentStart",
743
- "SubagentStop",
744
- "PreCompact",
745
- "PostCompact",
746
- "SessionEnd",
747
- "StopFailure"
748
- ];
749
- var LEAN_SKIP = /* @__PURE__ */ new Set(["PreToolUse", "PostToolUseFailure"]);
750
- var eventsFor = (lean) => lean ? CLAUDE_EVENTS.filter((e) => !LEAN_SKIP.has(e)) : [...CLAUDE_EVENTS];
751
- var OPTS = {
752
- matcherFor: (e) => /Tool|Permission/.test(e) ? "" : void 0,
753
- timeout: 5,
754
- // The hook waits up to 125 s for a verified control answer, and only when control
755
- // is paired on this machine; everywhere else it exits in milliseconds as before.
756
- timeoutFor: (e) => e === "PermissionRequest" ? 130 : void 0
757
- };
758
- var DESKTOP_ID = /^local_[A-Za-z0-9-]{1,64}$/;
759
- function claudeDirs(env = process.env, home = env.HOME || homeDir()) {
760
- const out = [];
761
- const add = (d) => {
762
- if (!d) return;
763
- const r = path5.resolve(d);
764
- if (!out.includes(r)) out.push(r);
765
- };
766
- add(path5.join(home, ".claude"));
767
- add(env.CLAUDE_CONFIG_DIR);
894
+ var ANTHROPIC_HOST = /(^|\.)anthropic\.com$/i;
895
+ function apiAccount(dir) {
896
+ const base = readJson(settingsIn(dir)).env?.ANTHROPIC_BASE_URL;
897
+ if (typeof base !== "string") return null;
898
+ let u;
768
899
  try {
769
- for (const e of readdirSync3(home, { withFileTypes: true })) {
770
- if (!e.isDirectory() || !/^\.claude[-_.]/.test(e.name)) continue;
771
- const d = path5.join(home, e.name);
772
- if (existsSync3(path5.join(d, "projects")) || existsSync3(path5.join(d, "settings.json"))) add(d);
773
- }
900
+ u = new URL(base.trim());
774
901
  } catch {
902
+ return null;
775
903
  }
776
- return out.filter((d, i) => i === 0 || existsSync3(d));
904
+ if (u.protocol !== "https:" && u.protocol !== "http:" || !u.hostname || ANTHROPIC_HOST.test(u.hostname))
905
+ return null;
906
+ const origin = `${u.protocol}//${u.host}`;
907
+ return { id: `api-${createHash("sha256").update(origin).digest("hex").slice(0, 16)}`, name: u.host };
777
908
  }
778
- var settingsIn = (dir) => path5.join(dir, "settings.json");
779
- var configDirOf = (transcript) => path5.dirname(path5.dirname(path5.dirname(transcript)));
780
- var validTool2 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
781
- var endReason = (r) => r === "clear" || r === "logout" || r === "exit" ? r : r === "prompt_input_exit" ? "exit" : "other";
782
- var notifKind = (t) => t === "permission_prompt" ? "permission" : t === "idle_prompt" ? "idle" : t === "elicitation_dialog" ? "elicitation" : "question";
783
- var claudeCode = {
784
- name: NAME2,
785
- events: CLAUDE_EVENTS,
786
- detect: (env = process.env) => claudeDirs(env).some((d) => existsSync3(d)),
787
- configFiles: (env = process.env) => claudeDirs(env).map(settingsIn),
788
- install: (cmd, env = process.env, opts = {}) => claudeDirs(env).map((d) => installClaudeShaped(settingsIn(d), NAME2, eventsFor(opts.lean), cmd, OPTS)),
789
- uninstall: (env = process.env, opts = {}) => claudeDirs(env).map(
790
- (d) => uninstallClaudeShaped(settingsIn(d), CLAUDE_EVENTS, { created: opts.created?.includes(settingsIn(d)) })
791
- ),
792
- installed: (cmd, env = process.env, opts = {}) => claudeDirs(env).map((d) => ({
793
- file: settingsIn(d),
794
- state: installedClaudeShaped(settingsIn(d), NAME2, eventsFor(opts.lean), cmd, OPTS)
795
- })),
796
- fromHook(input) {
797
- const event = input.argv[1] ?? "";
798
- let s = {};
799
- try {
800
- const j = input.stdin ? JSON.parse(input.stdin) : {};
801
- if (j && typeof j === "object" && !Array.isArray(j)) s = j;
802
- } catch {
803
- }
804
- const id = s.session_id ?? s.sessionId;
805
- if (!id) return null;
806
- if (notClaudeCode(String(id), s)) return null;
807
- const session = { id: String(id), cwd: typeof s.cwd === "string" ? s.cwd : input.cwd };
808
- if (typeof s.model === "string") session.model = s.model;
809
- if (typeof s.agent_id === "string" && event.startsWith("Subagent") === false && s.agent_id)
810
- session.parent_id = null;
811
- const turn_id = typeof s.prompt_id === "string" ? s.prompt_id : void 0;
812
- const tool = validTool2(s.tool_name) ? s.tool_name : void 0;
813
- const call_id = typeof s.tool_use_id === "string" ? s.tool_use_id : void 0;
814
- const ms = typeof s.duration_ms === "number" ? Math.round(s.duration_ms) : void 0;
815
- const agent_id = typeof s.agent_id === "string" ? s.agent_id : void 0;
816
- const permissionId = () => `perm-${createHash("sha256").update(`${turn_id ?? ""}|${tool ?? ""}|${JSON.stringify(s.tool_input ?? null)}`).digest("hex").slice(0, 16)}`;
817
- const ev = (type, data) => ({ type, data: prune2(data), harnessEvent: event });
818
- const events = [];
819
- switch (event) {
820
- case "SessionStart":
821
- events.push(ev("session.started", { source: pickSource(s.source ?? s.trigger) }));
822
- break;
823
- case "UserPromptSubmit": {
824
- const prompt = typeof s.prompt === "string" ? s.prompt : void 0;
825
- events.push(
826
- ev("turn.started", {
827
- turn_id,
828
- prompt_chars: prompt === void 0 ? void 0 : [...prompt].length,
829
- // Claude Code's hook schema documents `source`
830
- // (user | sdk | system | loop_wakeup | schedule_wakeup | poll_event) and says a
831
- // payload may omit it while the field rolls out; 2.1.304's payload has no such
832
- // key at all. Forwarded verbatim, shape-guarded only: the list is the harness's
833
- // to extend.
834
- submitted_by: harnessWord(s.source),
835
- // One of Claude Code's own prompt wrappers: the prompt is a block the harness
836
- // wrote, so it is not the person's words. Absent is unknown, never "the person" —
837
- // and `true` is never "no person caused this turn", because a Talk or
838
- // `sessionpipe wait` delivery hands the person's own message over inside a
839
- // <task-notification> (PROTOCOL.md §4).
840
- injected: prompt !== void 0 && injectedBlock(prompt) ? true : void 0
841
- })
842
- );
843
- break;
844
- }
845
- case "PreToolUse":
846
- if (tool) events.push(ev("tool.started", { tool, call_id, turn_id, agent_id, input: s.tool_input }));
847
- break;
848
- case "PostToolUse":
849
- if (tool)
850
- events.push(
851
- ev("tool.ended", {
852
- tool,
853
- call_id,
854
- turn_id,
855
- agent_id,
856
- ms,
857
- ok: true,
858
- input: s.tool_input,
859
- output: s.tool_response
860
- })
861
- );
862
- break;
863
- case "PostToolUseFailure":
864
- if (tool)
865
- events.push(
866
- ev("tool.ended", {
867
- tool,
868
- call_id,
869
- turn_id,
870
- agent_id,
871
- ms,
872
- ok: false,
873
- error: errText(s.error ?? s.tool_response),
874
- input: s.tool_input,
875
- output: s.tool_response
876
- })
877
- );
878
- break;
879
- case "PermissionRequest":
880
- events.push(
881
- ev("attention.needed", {
882
- attention_id: call_id ?? permissionId(),
883
- kind: "permission",
884
- tool,
885
- message: tool ? `${tool}${describe(s.tool_input)}` : void 0
886
- })
887
- );
888
- break;
889
- case "Notification":
890
- events.push(
891
- ev("attention.needed", {
892
- attention_id: `notif-${Date.now()}`,
893
- kind: notifKind(s.notification_type),
894
- message: typeof s.message === "string" ? s.message.slice(0, 200) : void 0
895
- })
896
- );
897
- break;
898
- case "Stop":
899
- events.push(
900
- ev("turn.ended", {
901
- turn_id,
902
- reason: "stop",
903
- background: Array.isArray(s.background_tasks) ? s.background_tasks.length : void 0
904
- })
905
- );
906
- break;
907
- case "StopFailure":
908
- events.push(
909
- ev("turn.ended", {
910
- turn_id,
911
- reason: "error",
912
- error: typeof s.error === "string" ? s.error.slice(0, 200) : void 0
913
- })
914
- );
915
- break;
916
- case "SubagentStart":
917
- events.push(
918
- ev("subagent.started", {
919
- agent_id: String(s.agent_id ?? ""),
920
- agent_type: typeof s.agent_type === "string" ? s.agent_type : void 0
921
- })
922
- );
923
- break;
924
- case "SubagentStop":
925
- events.push(
926
- ev("subagent.ended", {
927
- agent_id: String(s.agent_id ?? ""),
928
- agent_type: typeof s.agent_type === "string" ? s.agent_type : void 0
929
- })
930
- );
931
- break;
932
- case "PreCompact":
933
- events.push(ev("context.compacted", { trigger: s.trigger === "manual" ? "manual" : "auto" }));
934
- break;
935
- case "PostCompact":
936
- break;
937
- // PreCompact already said it; a second event would double-count
938
- case "SessionEnd":
939
- events.push(ev("session.ended", { reason: endReason(s.reason) }));
940
- break;
941
- default:
942
- return null;
943
- }
944
- const hints = {};
945
- if (event === "UserPromptSubmit" && typeof s.prompt === "string") hints.prompt = s.prompt;
946
- const host = input.env.CLAUDE_CODE_HOST_SESSION_ID;
947
- if (host && DESKTOP_ID.test(host)) hints.desktop_id = host;
948
- const attended = input.env.CLAUDE_CODE_SESSION_ATTENDED;
949
- if (attended === "1" || attended === "0") hints.attended = attended === "1" || !!input.env.SESSIONPIPE_CONTROL_JOB;
950
- if (event !== "PreToolUse" && event !== "PostToolUse") hints.claude_pid = claudePid(input.env);
951
- const r = { session, events, hints };
952
- if (typeof s.transcript_path === "string") r.transcript = s.transcript_path;
953
- return r;
954
- },
955
- readTranscript(file, fromLine) {
956
- let text = "";
957
- try {
958
- text = readFileSync3(file, "utf8");
959
- } catch {
960
- return { turns: [], line: fromLine };
961
- }
962
- return readClaude(text.split("\n"), fromLine);
963
- },
964
- facts(session, transcript, hints, state) {
965
- const out = {};
966
- if (transcript) Object.assign(out, claudeFacts(transcript, session.id, hints, state));
967
- const cwd = out.cwd ?? session.cwd;
968
- Object.assign(out, gitFacts(cwd), out.branch ? { branch: out.branch } : {});
969
- if (!out.url && typeof hints.desktop_id === "string") out.url = `claude://code/continue/${hints.desktop_id}`;
970
- if (typeof hints.attended === "boolean") out.attended = hints.attended || !!out.url;
971
- return out;
972
- },
973
- backfill(sinceMs, env = process.env) {
974
- const rows2 = [];
975
- const files = [];
976
- for (const d of claudeDirs(env))
977
- files.push(...recentFiles(path5.join(d, "projects"), 1, sinceMs, (n) => /^[0-9a-f-]{36}\.jsonl$/.test(n)));
978
- const noState = { get: () => ({}), save: () => {
979
- } };
980
- for (const f of files) {
981
- const id = path5.basename(f, ".jsonl");
982
- const facts = claudeFacts(f, id, {}, noState);
983
- if (!facts.started_at || !facts.last_at) continue;
984
- const { started_at, last_at, first_ask: _fa, harness_version: _hv, ...rest } = facts;
985
- const cwd = rest.cwd;
986
- rows2.push({
987
- session: { id, ...rest, ...gitFacts(cwd), ...rest.branch ? { branch: rest.branch } : {} },
988
- started_at,
989
- last_at
990
- });
909
+ var accountCache = /* @__PURE__ */ new Map();
910
+ function claudeAccount(dir) {
911
+ const hit = accountCache.get(dir);
912
+ if (hit !== void 0) return hit;
913
+ const acct = claudeLogin(dir)?.id ?? null;
914
+ accountCache.set(dir, acct);
915
+ return acct;
916
+ }
917
+ var LINK_RE = /^https:\/\/claude\.ai\/code\/session_[A-Za-z0-9]+$/;
918
+ function transcriptLink(records) {
919
+ for (let i = records.length - 1; i >= 0; i--) {
920
+ const j = records[i];
921
+ if (j.type === "system" && j.subtype === "bridge_status" && typeof j.url === "string" && LINK_RE.test(j.url))
922
+ return j.url;
923
+ if (j.type === "bridge-session" && typeof j.bridgeSessionId === "string") {
924
+ const m = /^(?:cse|session)_([A-Za-z0-9]+)$/.exec(j.bridgeSessionId);
925
+ if (m) return `https://claude.ai/code/session_${m[1]}`;
991
926
  }
992
- return rows2;
993
927
  }
994
- };
995
- function prune2(o) {
996
- for (const k of Object.keys(o)) if (o[k] === void 0) delete o[k];
997
- return o;
928
+ return null;
998
929
  }
999
- var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1000
- function notClaudeCode(id, s) {
1001
- return typeof s.transcript_path !== "string" && !UUID.test(id);
930
+ function appTitle(text) {
931
+ const t = String(text).replace(/\s+/g, " ").trim();
932
+ return t.length > 50 ? `${t.slice(0, 50)}...` : t;
1002
933
  }
1003
- function pickSource(v) {
1004
- return typeof v === "string" && ["startup", "resume", "clear", "compact", "fork"].includes(v) ? v : "unknown";
934
+ function askText(j) {
935
+ if (!j || j.type !== "user" || j.isMeta || j.isSidechain) return null;
936
+ const origin = j.origin;
937
+ if (origin?.kind && origin.kind !== "human") return null;
938
+ const c = j.message?.content;
939
+ 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();
940
+ if (!text || isInjected(text)) return null;
941
+ return text;
1005
942
  }
1006
- var harnessWord = (v) => typeof v === "string" && /^[A-Za-z0-9][A-Za-z0-9_.-]{0,39}$/.test(v) ? v : void 0;
1007
- var WRAPPER = /^<(?:task-notification|cross-session-message|system-reminder|local-command-stdout|command-[a-z-]+)[\s/>]/i;
1008
- var injectedBlock = (prompt) => WRAPPER.test(prompt.trim());
1009
- function errText(v) {
1010
- if (typeof v === "string") return v.slice(0, 2e3);
1011
- if (v && typeof v === "object") {
1012
- const e = v;
1013
- if (typeof e.error === "string") return e.error.slice(0, 2e3);
1014
- if (typeof e.message === "string") return e.message.slice(0, 2e3);
943
+ function firstAsk(records) {
944
+ for (const j of records) {
945
+ const t = askText(j);
946
+ if (t) return appTitle(t);
1015
947
  }
1016
- return void 0;
948
+ return null;
1017
949
  }
1018
- function describe(input) {
1019
- if (!input || typeof input !== "object") return "";
1020
- const i = input;
1021
- const v = i.command ?? i.file_path ?? i.path ?? i.url ?? i.pattern;
1022
- return typeof v === "string" ? ` ${v.slice(0, 120)}` : "";
950
+ function scanTitles(file, from, found) {
951
+ const out = { custom: found.custom ?? null, ai: found.ai ?? null, upto: from };
952
+ let size = 0;
953
+ try {
954
+ size = statSync2(file).size;
955
+ } catch {
956
+ return out;
957
+ }
958
+ if (size < from) {
959
+ out.upto = 0;
960
+ out.custom = out.ai = null;
961
+ from = 0;
962
+ }
963
+ let pos = 0;
964
+ scanLines(
965
+ file,
966
+ (l) => {
967
+ pos += Buffer.byteLength(l, "utf8") + 1;
968
+ if (pos <= from || l.indexOf('-title"') < 0) return null;
969
+ try {
970
+ const j = JSON.parse(l);
971
+ if (j.type === "custom-title" && j.customTitle) out.custom = j.customTitle;
972
+ if (j.type === "ai-title" && j.aiTitle) out.ai = j.aiTitle;
973
+ } catch {
974
+ }
975
+ return null;
976
+ },
977
+ 512 * 1024 * 1024
978
+ );
979
+ out.upto = size;
980
+ return out;
1023
981
  }
1024
- var NAME_RANK = {
1025
- user: "custom",
1026
- auto: "harness",
1027
- hook: "harness",
1028
- peer: "harness"
1029
- };
1030
- function registryEntry(j) {
1031
- if (!j || typeof j !== "object") return null;
1032
- const r = j;
1033
- const source = r.nameSource ? NAME_RANK[r.nameSource] : void 0;
1034
- return {
1035
- sessionId: r.sessionId ? String(r.sessionId) : null,
1036
- name: source && typeof r.name === "string" && r.name.trim() ? r.name.trim() : null,
1037
- name_source: source ?? null,
1038
- bridge: typeof r.bridgeSessionId === "string" ? r.bridgeSessionId : null
1039
- };
982
+ function claudeFacts(file, sessionId, hints, state) {
983
+ const { head, tail } = ends(file, 64 * 1024, 256 * 1024);
984
+ const H = parseLines(head);
985
+ const T = parseLines(tail);
986
+ const out = {};
987
+ const id = sessionId || path4.basename(file, ".jsonl");
988
+ const mem = state.get(NAME, id);
989
+ let custom = null;
990
+ let ai = null;
991
+ for (const j of H.concat(T)) {
992
+ if (j.type === "custom-title" && typeof j.customTitle === "string") custom = j.customTitle;
993
+ if (j.type === "ai-title" && typeof j.aiTitle === "string") ai = j.aiTitle;
994
+ }
995
+ let size = 0;
996
+ try {
997
+ size = statSync2(file).size;
998
+ } catch {
999
+ }
1000
+ if (!custom && !ai && size > 64 * 1024 + 256 * 1024) {
1001
+ const s = scanTitles(file, Number(mem.scan_upto) || 0, {
1002
+ custom: mem.scan_custom,
1003
+ ai: mem.scan_ai
1004
+ });
1005
+ custom = s.custom;
1006
+ ai = s.ai;
1007
+ state.save(NAME, id, { scan_upto: s.upto, scan_custom: s.custom, scan_ai: s.ai });
1008
+ }
1009
+ const dir = configDirOf(file);
1010
+ const live = claudeLive(dir).get(id) ?? claudeLiveByPid(dir, hints.claude_pid);
1011
+ if (live?.name) state.save(NAME, id, { name: live.name, name_source: live.name_source });
1012
+ const bridge = live?.bridge && /^session_[A-Za-z0-9]+$/.test(live.bridge) ? `https://claude.ai/code/${live.bridge}` : null;
1013
+ const url = bridge ?? transcriptLink(T) ?? transcriptLink(H) ?? (typeof mem.url === "string" ? mem.url : null);
1014
+ if (url) {
1015
+ out.url = url;
1016
+ state.save(NAME, id, { url });
1017
+ }
1018
+ const name = live?.name ? { title: live.name, source: live.name_source } : typeof mem.name === "string" ? { title: mem.name, source: mem.name_source } : null;
1019
+ const pick = name && name.source === "custom" && name || custom && { title: custom, source: "custom" } || name || ai && { title: ai, source: "harness" };
1020
+ let ask2 = typeof mem.first_ask === "string" ? mem.first_ask : firstAsk(H);
1021
+ if (!ask2 && size > 64 * 1024 && !mem.first_ask_none) {
1022
+ ask2 = scanLines(file, (l) => {
1023
+ try {
1024
+ const t = askText(JSON.parse(l));
1025
+ return t ? appTitle(t) : null;
1026
+ } catch {
1027
+ return null;
1028
+ }
1029
+ });
1030
+ if (!ask2 && size > 16 * 1024 * 1024) state.save(NAME, id, { first_ask_none: true });
1031
+ }
1032
+ if (ask2) {
1033
+ out.first_ask = ask2;
1034
+ if (!mem.first_ask) state.save(NAME, id, { first_ask: ask2 });
1035
+ }
1036
+ if (pick) {
1037
+ out.title = String(pick.title).slice(0, 120);
1038
+ out.title_source = pick.source;
1039
+ } else if (ask2) {
1040
+ out.title = ask2;
1041
+ out.title_source = "first-ask";
1042
+ }
1043
+ const acct = claudeAccount(dir);
1044
+ if (acct) out.account_id = acct;
1045
+ for (let i = T.length - 1; i >= 0; i--) {
1046
+ const m = T[i].message?.model;
1047
+ if (T[i]?.type === "assistant" && m && m !== "<synthetic>") {
1048
+ out.model = m;
1049
+ break;
1050
+ }
1051
+ }
1052
+ const meta = [...T].reverse().find((j) => j.version) ?? H.find((j) => j.version);
1053
+ if (meta?.version) out.harness_version = String(meta.version);
1054
+ const times = H.concat(T).map((j) => Date.parse(String(j.timestamp ?? ""))).filter((n) => n > 0);
1055
+ if (times.length) {
1056
+ out.started_at = Math.min(...times);
1057
+ out.last_at = Math.max(...times);
1058
+ }
1059
+ const withCwd = H.find((j) => typeof j.cwd === "string");
1060
+ if (withCwd) {
1061
+ out.cwd = withCwd.cwd;
1062
+ if (typeof withCwd.gitBranch === "string" && withCwd.gitBranch !== "HEAD") out.branch = withCwd.gitBranch;
1063
+ }
1064
+ return out;
1040
1065
  }
1041
- var liveCache = /* @__PURE__ */ new Map();
1042
- function claudeLive(dir) {
1043
- const hit = liveCache.get(dir);
1044
- if (hit) return hit;
1045
- const map = /* @__PURE__ */ new Map();
1046
- let names = [];
1066
+ var REMOTE_RETRY_MS = 6e4;
1067
+ var REMOTE_REFRESH_MS = 30 * 6e4;
1068
+ var CLAUDE_API = "https://api.anthropic.com";
1069
+ var KEYCHAIN = "Claude Code-credentials";
1070
+ var REMOTE_TIMEOUT_MS = 3e3;
1071
+ var KEYCHAIN_TIMEOUT_MS = 2e3;
1072
+ var REMOTE_MAX_BYTES = 1 << 20;
1073
+ var cleanName = (s) => s.replace(/[\p{Cc}\u061C\u200E\u200F\u202A-\u202E\u2066-\u2069]/gu, " ").replace(/\s+/g, " ").trim();
1074
+ async function claudeRemoteFacts(id, local, transcript, state, env, now = Date.now()) {
1075
+ if (!transcript || !local.url || !LINK_RE.test(local.url) || local.title_source === "custom") return {};
1076
+ const mem = state.get(NAME, id);
1077
+ let title = typeof mem.remote_title === "string" ? mem.remote_title : null;
1078
+ if (now - (Number(mem.remote_at) || 0) >= (title ? REMOTE_REFRESH_MS : REMOTE_RETRY_MS)) {
1079
+ state.save(NAME, id, { remote_at: now });
1080
+ const token = claudeAiToken(configDirOf(transcript), env, now);
1081
+ const got = token ? await claudeAiTitle(local.url.slice(local.url.lastIndexOf("/") + 1), token) : void 0;
1082
+ if (got !== void 0) {
1083
+ title = got?.slice(0, 200) ?? null;
1084
+ state.save(NAME, id, { remote_title: title });
1085
+ }
1086
+ }
1087
+ return title ? { title: title.slice(0, 120), title_source: "harness" } : {};
1088
+ }
1089
+ function keychainServices(dir, env) {
1090
+ const named = (p) => `${KEYCHAIN}-${createHash("sha256").update(p.normalize("NFC")).digest("hex").slice(0, 8)}`;
1091
+ const out = /* @__PURE__ */ new Set();
1092
+ if (env.CLAUDE_CONFIG_DIR && path4.resolve(env.CLAUDE_CONFIG_DIR) === dir) out.add(named(env.CLAUDE_CONFIG_DIR));
1093
+ if (dir === path4.join(env.HOME || homeDir(), ".claude")) out.add(KEYCHAIN);
1094
+ out.add(named(dir));
1095
+ return [...out];
1096
+ }
1097
+ var tokenCache = /* @__PURE__ */ new Map();
1098
+ function claudeAiToken(dir, env, now) {
1099
+ const hit = tokenCache.get(dir);
1100
+ if (hit && hit.expires > now + 6e4) return hit.token;
1101
+ const raw = [];
1102
+ if (process.platform === "darwin")
1103
+ for (const service of keychainServices(dir, env)) {
1104
+ try {
1105
+ const user = env.USER || userInfo().username;
1106
+ const account = /^[\w.-]+$/.test(user) ? user : "claude-code-user";
1107
+ raw.push(
1108
+ execFileSync2("security", ["find-generic-password", "-a", account, "-w", "-s", service], {
1109
+ encoding: "utf8",
1110
+ timeout: KEYCHAIN_TIMEOUT_MS,
1111
+ stdio: ["ignore", "pipe", "ignore"]
1112
+ })
1113
+ );
1114
+ break;
1115
+ } catch {
1116
+ }
1117
+ }
1047
1118
  try {
1048
- names = readdirSync3(path5.join(dir, "sessions")).filter((n) => n.endsWith(".json"));
1119
+ raw.push(readFileSync2(path4.join(dir, ".credentials.json"), "utf8"));
1049
1120
  } catch {
1050
1121
  }
1051
- for (const n of names) {
1122
+ for (const r of raw) {
1052
1123
  try {
1053
- const e = registryEntry(JSON.parse(readFileSync3(path5.join(dir, "sessions", n), "utf8")));
1054
- if (e?.sessionId) map.set(e.sessionId, e);
1124
+ const t = r.trim();
1125
+ const o = JSON.parse(/^(?:[0-9a-f]{2})+$/i.test(t) ? Buffer.from(t, "hex").toString("utf8") : t).claudeAiOauth;
1126
+ if (typeof o?.accessToken !== "string" || !o.accessToken) continue;
1127
+ const expires = typeof o.expiresAt === "number" ? o.expiresAt : Number.POSITIVE_INFINITY;
1128
+ if (expires <= now + 6e4) continue;
1129
+ tokenCache.set(dir, { token: o.accessToken, expires });
1130
+ return o.accessToken;
1055
1131
  } catch {
1056
1132
  }
1057
1133
  }
1058
- liveCache.set(dir, map);
1059
- return map;
1060
- }
1061
- function claudeLiveByPid(dir, pid) {
1062
- if (!pid) return null;
1063
- try {
1064
- return registryEntry(JSON.parse(readFileSync3(path5.join(dir, "sessions", `${pid}.json`), "utf8")));
1065
- } catch {
1066
- return null;
1067
- }
1134
+ return null;
1068
1135
  }
1069
- function claudePid(env = process.env) {
1136
+ async function claudeAiTitle(bridge, token) {
1070
1137
  try {
1071
- const dirs = claudeDirs(env);
1072
- const has = (pid2) => dirs.some((d) => existsSync3(path5.join(d, "sessions", `${pid2}.json`)));
1073
- let pid = process.ppid;
1074
- if (has(pid)) return pid;
1075
- const parent = parentOf();
1076
- for (let hop = 0; hop < 6 && pid > 1; hop++) {
1077
- pid = parent(pid);
1078
- if (pid > 1 && has(pid)) return pid;
1079
- }
1138
+ const r = await fetch(`${CLAUDE_API}/v1/code/sessions/${bridge}`, {
1139
+ headers: { Authorization: `Bearer ${token}`, "anthropic-version": "2023-06-01", "User-Agent": "sessionpipe" },
1140
+ redirect: "error",
1141
+ signal: AbortSignal.timeout(REMOTE_TIMEOUT_MS)
1142
+ });
1143
+ if (r.status !== 200 || Number(r.headers.get("content-length")) > REMOTE_MAX_BYTES) return void 0;
1144
+ const text = await r.text();
1145
+ if (text.length > REMOTE_MAX_BYTES) return void 0;
1146
+ const j = JSON.parse(text);
1147
+ const s = j?.response_shape ?? j?.session ?? j;
1148
+ const bare = (x) => x.replace(/^(?:session|cse)_/, "");
1149
+ if (typeof s?.id !== "string" || bare(s.id) !== bare(bridge)) return void 0;
1150
+ return typeof s.title === "string" && cleanName(s.title) || null;
1080
1151
  } catch {
1152
+ return void 0;
1081
1153
  }
1082
- return null;
1083
1154
  }
1084
- function parentOf() {
1085
- if (process.platform === "linux") {
1086
- return (pid) => {
1087
- try {
1088
- const st = readFileSync3(`/proc/${pid}/stat`, "utf8");
1089
- return Number(st.slice(st.lastIndexOf(")") + 2).split(" ")[1]) || 0;
1090
- } catch {
1091
- return 0;
1092
- }
1093
- };
1094
- }
1095
- const table = /* @__PURE__ */ new Map();
1155
+
1156
+ // ../core/src/adapters/headless.ts
1157
+ import { accessSync, constants, readFileSync as readFileSync3, statSync as statSync3 } from "node:fs";
1158
+ import path5 from "node:path";
1159
+ var JOB_TIMEOUT_MS = 30 * 6e4;
1160
+ var OUT_CAP = 1024 * 1024;
1161
+ function isExe(p) {
1096
1162
  try {
1097
- const out = execFileSync2("ps", ["-axo", "pid=,ppid="], {
1098
- encoding: "utf8",
1099
- timeout: 2e3,
1100
- maxBuffer: 8 * 1024 * 1024,
1101
- stdio: ["ignore", "pipe", "ignore"]
1102
- });
1103
- for (const l of out.split("\n")) {
1104
- const m = /^\s*(\d+)\s+(\d+)/.exec(l);
1105
- if (m) table.set(Number(m[1]), Number(m[2]));
1106
- }
1163
+ if (!statSync3(p).isFile()) return false;
1164
+ accessSync(p, constants.X_OK);
1165
+ return true;
1107
1166
  } catch {
1167
+ return false;
1108
1168
  }
1109
- return (pid) => table.get(pid) || 0;
1110
1169
  }
1111
- var EMAIL_RE = /^[^@\s]{1,64}@[^@\s]{1,189}$/;
1112
- function claudeLogin(dir, home = homeDir()) {
1113
- const files = [path5.join(dir, ".claude.json")];
1114
- if (path5.resolve(dir) === path5.join(home, ".claude")) files.unshift(path5.join(home, ".claude.json"));
1115
- for (const f of files) {
1116
- try {
1117
- const a = JSON.parse(readFileSync3(f, "utf8")).oauthAccount;
1118
- if (typeof a?.accountUuid === "string" && a.accountUuid) {
1119
- const email = typeof a.emailAddress === "string" && EMAIL_RE.test(a.emailAddress) ? a.emailAddress : void 0;
1120
- return { id: a.accountUuid.slice(0, 128), ...email ? { email } : {} };
1121
- }
1122
- } catch {
1123
- }
1170
+ function findBinary(o) {
1171
+ if (o.override && isExe(o.override)) return o.override;
1172
+ for (const d of String(o.env?.PATH || "").split(path5.delimiter)) {
1173
+ if (!d || !path5.isAbsolute(d)) continue;
1174
+ for (const n of o.names) if (isExe(path5.join(d, n))) return path5.join(d, n);
1124
1175
  }
1125
- return apiAccount(dir);
1176
+ for (const d of o.dirs) for (const n of o.names) if (isExe(path5.join(d, n))) return path5.join(d, n);
1177
+ return null;
1126
1178
  }
1127
- var ANTHROPIC_HOST = /(^|\.)anthropic\.com$/i;
1128
- function apiAccount(dir) {
1129
- let base;
1179
+ function shimTarget(shim) {
1180
+ let text = "";
1130
1181
  try {
1131
- base = JSON.parse(readFileSync3(settingsIn(dir), "utf8")).env?.ANTHROPIC_BASE_URL;
1182
+ text = readFileSync3(shim, "utf8");
1132
1183
  } catch {
1133
1184
  return null;
1134
1185
  }
1135
- if (typeof base !== "string") return null;
1136
- let u;
1186
+ const m = /"%(?:~dp0|dp0%)\\([^"]+)"\s+%\*/.exec(text);
1187
+ if (!m) return null;
1188
+ const target = path5.resolve(path5.dirname(shim), m[1].split("\\").join(path5.sep));
1137
1189
  try {
1138
- u = new URL(base.trim());
1190
+ return statSync3(target).isFile() ? target : null;
1139
1191
  } catch {
1140
1192
  return null;
1141
1193
  }
1142
- if (u.protocol !== "https:" && u.protocol !== "http:" || !u.hostname || ANTHROPIC_HOST.test(u.hostname))
1143
- return null;
1144
- const origin = `${u.protocol}//${u.host}`;
1145
- return { id: `api-${createHash("sha256").update(origin).digest("hex").slice(0, 16)}`, name: u.host };
1146
1194
  }
1147
- var accountCache = /* @__PURE__ */ new Map();
1148
- function claudeAccount(dir) {
1149
- const hit = accountCache.get(dir);
1150
- if (hit !== void 0) return hit;
1151
- const acct = claudeLogin(dir)?.id ?? null;
1152
- accountCache.set(dir, acct);
1153
- return acct;
1195
+ function spawnable(bin, args) {
1196
+ if (!/\.(cmd|bat)$/i.test(bin)) return { file: bin, args };
1197
+ const target = shimTarget(bin);
1198
+ return target ? { file: process.execPath, args: [target, ...args] } : null;
1154
1199
  }
1155
- var LINK_RE = /^https:\/\/claude\.ai\/code\/session_[A-Za-z0-9]+$/;
1156
- function transcriptLink(records) {
1157
- for (let i = records.length - 1; i >= 0; i--) {
1158
- const j = records[i];
1159
- if (j.type === "system" && j.subtype === "bridge_status" && typeof j.url === "string" && LINK_RE.test(j.url))
1160
- return j.url;
1161
- if (j.type === "bridge-session" && typeof j.bridgeSessionId === "string") {
1162
- const m = /^(?:cse|session)_([A-Za-z0-9]+)$/.exec(j.bridgeSessionId);
1163
- if (m) return `https://claude.ai/code/session_${m[1]}`;
1200
+ function probe(bin, args) {
1201
+ const s = spawnable(bin, args);
1202
+ return s ? { ...s, shell: false } : { file: [`"${bin}"`, ...args].join(" "), args: [], shell: true };
1203
+ }
1204
+
1205
+ // ../core/src/adapters/antigravity.ts
1206
+ import {
1207
+ existsSync as existsSync3,
1208
+ mkdirSync as mkdirSync2,
1209
+ readdirSync as readdirSync3,
1210
+ readFileSync as readFileSync4,
1211
+ realpathSync,
1212
+ statSync as statSync4,
1213
+ unlinkSync as unlinkSync2,
1214
+ writeFileSync as writeFileSync3
1215
+ } from "node:fs";
1216
+ import path6 from "node:path";
1217
+ var NAME2 = "antigravity";
1218
+ var HOOK_NAME = "sessionpipe";
1219
+ var AG_EVENTS = ["PreInvocation", "PostInvocation", "PostToolUse", "Stop"];
1220
+ 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';
1221
+ var antigravityAnswer = (event) => event === "PreToolUse" ? AG_STALE_PRETOOL : "{}\n";
1222
+ var gemini = (env) => env.GEMINI_CLI_HOME || path6.join(homeDir(), ".gemini");
1223
+ var hooksFile = (env) => path6.join(gemini(env), "config", "hooks.json");
1224
+ var projectsDir = (env) => path6.join(gemini(env), "config", "projects");
1225
+ var dataDirs = (env) => ["antigravity", "antigravity-cli", "antigravity-ide"].map((d) => path6.join(gemini(env), d));
1226
+ var ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
1227
+ var validTool2 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
1228
+ function entry(cmd) {
1229
+ const handler = (e) => ({ type: "command", command: cmd([NAME2, e]).command, timeout: 5 });
1230
+ const out = { enabled: true };
1231
+ for (const e of AG_EVENTS) out[e] = /Tool/.test(e) ? [{ matcher: "*", hooks: [handler(e)] }] : [handler(e)];
1232
+ return out;
1233
+ }
1234
+ var antigravity = {
1235
+ name: NAME2,
1236
+ events: AG_EVENTS,
1237
+ detect: (env = process.env) => existsSync3(path6.join(gemini(env), "config")) || dataDirs(env).some((d) => existsSync3(d)),
1238
+ configFiles: (env = process.env) => [hooksFile(env)],
1239
+ install(cmd, env = process.env) {
1240
+ const file = hooksFile(env);
1241
+ const f = readJsonFile(file);
1242
+ if (!f.ok) return [{ file, changed: false, skipped: true, note: `${f.reason}; not touched` }];
1243
+ const hooks = f.value;
1244
+ const e = entry(cmd);
1245
+ if (JSON.stringify(hooks[HOOK_NAME]) === JSON.stringify(e)) return [{ file, changed: false }];
1246
+ hooks[HOOK_NAME] = e;
1247
+ mkdirSync2(path6.dirname(file), { recursive: true });
1248
+ writeFileSync3(file, formatJson(hooks, f));
1249
+ return [
1250
+ {
1251
+ file,
1252
+ changed: true,
1253
+ created: !f.existed,
1254
+ note: "hooks.json is read when a conversation starts; open ones report after a restart"
1255
+ }
1256
+ ];
1257
+ },
1258
+ uninstall(env = process.env, opts = {}) {
1259
+ const file = hooksFile(env);
1260
+ if (!existsSync3(file)) return [{ file, changed: false }];
1261
+ const f = readJsonFile(file);
1262
+ if (!f.ok) return [{ file, changed: false, skipped: true, note: `${f.reason}; not touched` }];
1263
+ const hooks = f.value;
1264
+ if (!(HOOK_NAME in hooks)) return [{ file, changed: false }];
1265
+ delete hooks[HOOK_NAME];
1266
+ if (opts.created?.includes(file) && !Object.keys(hooks).length) {
1267
+ unlinkSync2(file);
1268
+ return [{ file, changed: true, note: "removed (sessionpipe created it)" }];
1269
+ }
1270
+ writeFileSync3(file, formatJson(hooks, f));
1271
+ return [{ file, changed: true }];
1272
+ },
1273
+ installed(cmd, env = process.env) {
1274
+ const file = hooksFile(env);
1275
+ const f = readJsonFile(file);
1276
+ const have = f.ok ? f.value[HOOK_NAME] : void 0;
1277
+ return [
1278
+ {
1279
+ file,
1280
+ state: !have ? "missing" : JSON.stringify(have) === JSON.stringify(entry(cmd)) ? "current" : "stale"
1281
+ }
1282
+ ];
1283
+ },
1284
+ fromHook(input) {
1285
+ const event = input.argv[1] ?? "";
1286
+ let s = {};
1287
+ try {
1288
+ const j = input.stdin ? JSON.parse(input.stdin) : {};
1289
+ if (j && typeof j === "object" && !Array.isArray(j)) s = j;
1290
+ } catch {
1291
+ }
1292
+ const id = s.conversationId ?? s.session_id ?? s.sessionId;
1293
+ const stdout = antigravityAnswer(event);
1294
+ if (!id) return { session: { id: "" }, events: [], stdout };
1295
+ const workspaces = agWorkspaces(s.workspacePaths);
1296
+ const session = { id: String(id), cwd: workspaces[0] ?? input.cwd };
1297
+ if (typeof s.modelName === "string" && s.modelName !== "auto") session.model = s.modelName;
1298
+ const toolCall = s.toolCall;
1299
+ const tool = validTool2(toolCall?.name) ? toolCall?.name : validTool2(s.toolName) ? s.toolName : void 0;
1300
+ const ev = (type, data) => ({ type, data: prune2(data), harnessEvent: event });
1301
+ const events = [];
1302
+ switch (event) {
1303
+ case "PreInvocation":
1304
+ if (Number(s.invocationNum) === 0 || s.invocationNum === void 0) {
1305
+ events.push(ev("session.started", { source: "unknown" }));
1306
+ events.push(ev("turn.started", {}));
1307
+ } else events.push(ev("session.heartbeat", {}));
1308
+ break;
1309
+ case "PostInvocation":
1310
+ break;
1311
+ case "PreToolUse":
1312
+ if (tool) events.push(ev("tool.started", { tool, input: toolCall?.args }));
1313
+ break;
1314
+ case "PostToolUse":
1315
+ if (!tool) events.push(ev("session.heartbeat", {}));
1316
+ else
1317
+ events.push(
1318
+ ev("tool.ended", {
1319
+ tool,
1320
+ ok: !s.error,
1321
+ error: typeof s.error === "string" ? s.error.slice(0, 2e3) : void 0,
1322
+ input: toolCall?.args
1323
+ })
1324
+ );
1325
+ break;
1326
+ case "Stop":
1327
+ events.push(ev("turn.ended", { reason: s.error ? "error" : "stop" }));
1328
+ break;
1329
+ default:
1330
+ return { session, events: [], stdout };
1331
+ }
1332
+ const r = { session, events, stdout, hints: { workspaces } };
1333
+ if (typeof s.transcriptPath === "string") r.transcript = s.transcriptPath;
1334
+ return r;
1335
+ },
1336
+ readTranscript(file, fromLine) {
1337
+ let text = "";
1338
+ try {
1339
+ text = readFileSync4(file, "utf8");
1340
+ } catch {
1341
+ return { turns: [], line: fromLine };
1342
+ }
1343
+ return readAntigravity(text.split("\n"), fromLine);
1344
+ },
1345
+ facts(session, transcript, hints) {
1346
+ if (!transcript) return {};
1347
+ return antigravityFacts(transcript, {
1348
+ conversation: session.id,
1349
+ workspaces: hints.workspaces ?? (session.cwd ? [session.cwd] : []),
1350
+ env: process.env
1351
+ });
1352
+ },
1353
+ backfill(sinceMs, env = process.env) {
1354
+ const rows2 = [];
1355
+ for (const data of dataDirs(env)) {
1356
+ let ids = [];
1357
+ try {
1358
+ ids = readdirSync3(path6.join(data, "brain")).filter((n) => /^[0-9a-f-]{36}$/i.test(n));
1359
+ } catch {
1360
+ }
1361
+ for (const id of ids) {
1362
+ const f = path6.join(data, "brain", id, ".system_generated", "logs", "transcript.jsonl");
1363
+ try {
1364
+ if (statSync4(f).mtimeMs < sinceMs) continue;
1365
+ } catch {
1366
+ continue;
1367
+ }
1368
+ const facts = antigravityFacts(f, { conversation: id, workspaces: [], env });
1369
+ if (!facts.started_at || !facts.last_at) continue;
1370
+ const { started_at, last_at, first_ask: _fa, harness_version: _hv, ...rest } = facts;
1371
+ rows2.push({ session: { id, ...rest }, started_at, last_at });
1372
+ }
1164
1373
  }
1374
+ return rows2;
1165
1375
  }
1166
- return null;
1167
- }
1168
- function appTitle(text) {
1169
- const t = String(text).replace(/\s+/g, " ").trim();
1170
- return t.length > 50 ? `${t.slice(0, 50)}...` : t;
1171
- }
1172
- function askText(j) {
1173
- if (!j || j.type !== "user" || j.isMeta || j.isSidechain) return null;
1174
- const origin = j.origin;
1175
- if (origin?.kind && origin.kind !== "human") return null;
1176
- const c = j.message?.content;
1177
- 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();
1178
- if (!text || isInjected(text)) return null;
1179
- return text;
1376
+ };
1377
+ function prune2(o) {
1378
+ for (const k of Object.keys(o)) if (o[k] === void 0) delete o[k];
1379
+ return o;
1180
1380
  }
1181
- function firstAsk(records) {
1182
- for (const j of records) {
1183
- const t = askText(j);
1184
- if (t) return appTitle(t);
1185
- }
1186
- return null;
1381
+ function agPath(p) {
1382
+ if (typeof p !== "string" || !p) return null;
1383
+ let s = p;
1384
+ if (/^file:/i.test(s)) {
1385
+ try {
1386
+ s = decodeURIComponent(new URL(s).pathname);
1387
+ } catch {
1388
+ return null;
1389
+ }
1390
+ if (process.platform === "win32" && /^\/[A-Za-z]:/.test(s)) s = s.slice(1);
1391
+ } else if (/^[a-z][a-z0-9+.-]*:\/\//i.test(s)) return null;
1392
+ if (!path6.isAbsolute(s)) return null;
1393
+ return path6.resolve(s);
1187
1394
  }
1188
- function scanTitles(file, from, found) {
1189
- const out = { custom: found.custom ?? null, ai: found.ai ?? null, upto: from };
1190
- let size = 0;
1395
+ var agWorkspaces = (paths) => (Array.isArray(paths) ? paths : []).map(agPath).filter((p) => !!p).filter((p, i, a) => a.indexOf(p) === i);
1396
+ function agVariants(p) {
1397
+ const out = [p];
1191
1398
  try {
1192
- size = statSync3(file).size;
1399
+ const r = realpathSync.native(p);
1400
+ if (r !== p) out.push(r);
1193
1401
  } catch {
1194
- return out;
1195
- }
1196
- if (size < from) {
1197
- out.upto = 0;
1198
- out.custom = out.ai = null;
1199
- from = 0;
1200
1402
  }
1201
- let pos = 0;
1202
- scanLines(
1203
- file,
1204
- (l) => {
1205
- pos += Buffer.byteLength(l, "utf8") + 1;
1206
- if (pos <= from || l.indexOf('-title"') < 0) return null;
1207
- try {
1208
- const j = JSON.parse(l);
1209
- if (j.type === "custom-title" && j.customTitle) out.custom = j.customTitle;
1210
- if (j.type === "ai-title" && j.aiTitle) out.ai = j.aiTitle;
1211
- } catch {
1212
- }
1213
- return null;
1214
- },
1215
- 512 * 1024 * 1024
1216
- );
1217
- out.upto = size;
1218
1403
  return out;
1219
1404
  }
1220
- function claudeFacts(file, sessionId, hints, state) {
1221
- const { head, tail } = ends(file, 64 * 1024, 256 * 1024);
1222
- const H = parseLines(head);
1223
- const T = parseLines(tail);
1224
- const out = {};
1225
- const id = sessionId || path5.basename(file, ".jsonl");
1226
- const mem = state.get(NAME2, id);
1227
- let custom = null;
1228
- let ai = null;
1229
- for (const j of H.concat(T)) {
1230
- if (j.type === "custom-title" && typeof j.customTitle === "string") custom = j.customTitle;
1231
- if (j.type === "ai-title" && typeof j.aiTitle === "string") ai = j.aiTitle;
1232
- }
1233
- let size = 0;
1405
+ function agProjects(dir) {
1406
+ const out = [];
1407
+ let names = [];
1234
1408
  try {
1235
- size = statSync3(file).size;
1409
+ names = readdirSync3(dir).filter((n) => n.endsWith(".json"));
1236
1410
  } catch {
1411
+ return out;
1237
1412
  }
1238
- if (!custom && !ai && size > 64 * 1024 + 256 * 1024) {
1239
- const s = scanTitles(file, Number(mem.scan_upto) || 0, {
1240
- custom: mem.scan_custom,
1241
- ai: mem.scan_ai
1242
- });
1243
- custom = s.custom;
1244
- ai = s.ai;
1245
- state.save(NAME2, id, { scan_upto: s.upto, scan_custom: s.custom, scan_ai: s.ai });
1413
+ for (const n of names) {
1414
+ const id = n.slice(0, -5);
1415
+ if (!ID_RE.test(id)) continue;
1416
+ let j;
1417
+ try {
1418
+ j = JSON.parse(readFileSync4(path6.join(dir, n), "utf8"));
1419
+ } catch {
1420
+ continue;
1421
+ }
1422
+ const folders = [];
1423
+ for (const r of j?.projectResources?.resources ?? []) {
1424
+ const f = agPath(r?.folderUri ?? r?.gitFolder?.folderUri);
1425
+ if (f) folders.push(...agVariants(f));
1426
+ }
1427
+ if (folders.length) out.push({ id, folders });
1246
1428
  }
1247
- const dir = configDirOf(file);
1248
- const live = claudeLive(dir).get(id) ?? claudeLiveByPid(dir, hints.claude_pid);
1249
- if (live?.name) state.save(NAME2, id, { name: live.name, name_source: live.name_source });
1250
- const bridge = live?.bridge && /^session_[A-Za-z0-9]+$/.test(live.bridge) ? `https://claude.ai/code/${live.bridge}` : null;
1251
- const url = bridge ?? transcriptLink(T) ?? transcriptLink(H) ?? (typeof mem.url === "string" ? mem.url : null);
1252
- if (url) {
1253
- out.url = url;
1254
- state.save(NAME2, id, { url });
1429
+ return out;
1430
+ }
1431
+ function agMatchProject(projects, workspaces) {
1432
+ const ws = [];
1433
+ for (const w of workspaces) {
1434
+ const p = agPath(w);
1435
+ if (p) ws.push(...agVariants(p));
1255
1436
  }
1256
- const name = live?.name ? { title: live.name, source: live.name_source } : typeof mem.name === "string" ? { title: mem.name, source: mem.name_source } : null;
1257
- const pick = name && name.source === "custom" && name || custom && { title: custom, source: "custom" } || name || ai && { title: ai, source: "harness" };
1258
- let ask2 = typeof mem.first_ask === "string" ? mem.first_ask : firstAsk(H);
1259
- if (!ask2 && size > 64 * 1024 && !mem.first_ask_none) {
1260
- ask2 = scanLines(file, (l) => {
1261
- try {
1262
- const t = askText(JSON.parse(l));
1263
- return t ? appTitle(t) : null;
1264
- } catch {
1265
- return null;
1437
+ let best = null;
1438
+ let bestLen = -1;
1439
+ for (const pr of projects)
1440
+ for (const f of pr.folders) {
1441
+ const within = ws.some((w) => w === f || w.startsWith(f.endsWith(path6.sep) ? f : f + path6.sep));
1442
+ if (within && f.length > bestLen) {
1443
+ best = pr.id;
1444
+ bestLen = f.length;
1266
1445
  }
1267
- });
1268
- if (!ask2 && size > 16 * 1024 * 1024) state.save(NAME2, id, { first_ask_none: true });
1269
- }
1270
- if (ask2) {
1271
- out.first_ask = ask2;
1272
- if (!mem.first_ask) state.save(NAME2, id, { first_ask: ask2 });
1446
+ }
1447
+ return best;
1448
+ }
1449
+ function antigravityRemoteUrl(transcript, opts) {
1450
+ const i = transcript.indexOf(`${path6.sep}brain${path6.sep}`);
1451
+ const dirs = (i > 0 ? [transcript.slice(0, i)] : []).concat(dataDirs(opts.env));
1452
+ for (const d of dirs) {
1453
+ try {
1454
+ const m = /installation_uuid:\s*"([0-9a-f-]{36})"/i.exec(
1455
+ readFileSync4(path6.join(d, "antigravity_state.pbtxt"), "utf8")
1456
+ );
1457
+ if (!m) continue;
1458
+ const base = `https://antigravity.google.com/r/${m[1]}-v2`;
1459
+ const conv = opts.conversation ?? "";
1460
+ if (!ID_RE.test(conv) || !opts.workspaces?.length) return base;
1461
+ const project = agMatchProject(agProjects(opts.projectsDir ?? projectsDir(opts.env)), opts.workspaces);
1462
+ return project ? `${base}?p=${encodeURIComponent(`c/${conv}?section=${project}`)}` : base;
1463
+ } catch {
1464
+ }
1273
1465
  }
1274
- if (pick) {
1275
- out.title = String(pick.title).slice(0, 120);
1276
- out.title_source = pick.source;
1277
- } else if (ask2) {
1278
- out.title = ask2;
1466
+ return null;
1467
+ }
1468
+ function antigravityFacts(file, opts) {
1469
+ const { head, tail } = ends(file, 128 * 1024, 64 * 1024);
1470
+ const H = parseLines(head);
1471
+ const T = parseLines(tail);
1472
+ const out = {};
1473
+ const url = antigravityRemoteUrl(file, opts);
1474
+ if (url) out.url = url;
1475
+ for (const j of H) {
1476
+ if (j.type !== "USER_INPUT" || typeof j.content !== "string") continue;
1477
+ const m = /<USER_REQUEST>([\s\S]*?)(?:<\/USER_REQUEST>|$)/.exec(j.content);
1478
+ const text = (m ? m[1] : j.content).trim();
1479
+ const line = (text.split("\n").find((l) => l.trim()) ?? "").trim();
1480
+ if (!line || /^<[a-z_-]+>/i.test(line)) continue;
1481
+ out.title = line.length > 80 ? `${line.slice(0, 80).replace(/\s+\S*$/, "")}\u2026` : line;
1279
1482
  out.title_source = "first-ask";
1483
+ break;
1280
1484
  }
1281
- const acct = claudeAccount(dir);
1282
- if (acct) out.account_id = acct;
1283
- for (let i = T.length - 1; i >= 0; i--) {
1284
- const m = T[i].message?.model;
1285
- if (T[i]?.type === "assistant" && m && m !== "<synthetic>") {
1286
- out.model = m;
1287
- break;
1288
- }
1289
- }
1290
- const meta = [...T].reverse().find((j) => j.version) ?? H.find((j) => j.version);
1291
- if (meta?.version) out.harness_version = String(meta.version);
1292
- const times = H.concat(T).map((j) => Date.parse(String(j.timestamp ?? ""))).filter((n) => n > 0);
1485
+ const times = H.concat(T).map((j) => Date.parse(String(j.created_at ?? j.timestamp ?? ""))).filter((n) => n > 0);
1293
1486
  if (times.length) {
1294
1487
  out.started_at = Math.min(...times);
1295
1488
  out.last_at = Math.max(...times);
1296
1489
  }
1297
- const withCwd = H.find((j) => typeof j.cwd === "string");
1298
- if (withCwd) {
1299
- out.cwd = withCwd.cwd;
1300
- if (typeof withCwd.gitBranch === "string" && withCwd.gitBranch !== "HEAD") out.branch = withCwd.gitBranch;
1301
- }
1302
1490
  return out;
1303
1491
  }
1304
1492
 
@@ -1311,15 +1499,24 @@ var NONCE_TTL_MS = 25 * 36e5;
1311
1499
  var MODEL_RE = /^[A-Za-z0-9][A-Za-z0-9._:/@[\]-]*$/;
1312
1500
  var START_PERMISSIONS = ["read", "safe", "edits", "auto"];
1313
1501
  var levelRank = (l) => START_PERMISSIONS.indexOf(l);
1502
+ var FIELDS = {
1503
+ prompt: ["text", "files"],
1504
+ "permission.answer": ["for", "decision"],
1505
+ cancel: [],
1506
+ start: ["text", "cwd", "files", "permission", "model"],
1507
+ "key.add": ["key", "name"],
1508
+ pool: ["pool_action", "pool_account", "pool_order", "pool_code"]
1509
+ };
1510
+ var POOL_FIELDS = FIELDS.pool;
1314
1511
  var enc = new TextEncoder();
1315
1512
  var dec = new TextDecoder();
1316
1513
 
1317
1514
  // ../core/src/adapters/codex.ts
1318
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
1319
- import path6 from "node:path";
1515
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "node:fs";
1516
+ import path7 from "node:path";
1320
1517
 
1321
1518
  // ../core/src/adapters/codex-config.ts
1322
- var NOTIFY_LINE = /^[ \t]*notify[ \t]*=[ \t]*(\[.*\])[ \t]*(?:#.*)?$/m;
1519
+ var NOTIFY_LINE = /^[ \t]*notify[ \t]*=[ \t]*(\[.*\])[ \t]*(?:#.*)?(?=\r?$)/m;
1323
1520
  var NOTIFY_KEY = /^[ \t]*notify[ \t]*=/m;
1324
1521
  var OURS2 = /(?:hook\.js|sessionpipe-hook)",\s*"codex",\s*"notify"/;
1325
1522
  function scan(line, st) {
@@ -1409,10 +1606,11 @@ function dropLine(text, index, length) {
1409
1606
  }
1410
1607
  function insertTopLevel(text, line) {
1411
1608
  const { head, tail } = split(text);
1412
- if (!tail) return (head.trim() ? head.replace(/\s*$/, "\n") : "") + line + "\n";
1413
- return `${(head.trim() ? head.replace(/\s*$/, "\n") : "") + line}
1414
-
1415
- ${tail}`;
1609
+ const body = head.replace(/\s*$/, "");
1610
+ const eol = (body.includes("\n") ? body : text).includes("\r\n") ? "\r\n" : "\n";
1611
+ const top = head.trim() ? head.replace(/\s*$/, eol) : "";
1612
+ if (!tail) return top + line + eol;
1613
+ return `${top + line}${eol}${eol}${tail}`;
1416
1614
  }
1417
1615
  function withNotify(text, line) {
1418
1616
  const n = readNotify(text);
@@ -1461,25 +1659,25 @@ var CODEX_EVENTS = [
1461
1659
  ];
1462
1660
  var OPTS2 = { matcherFor: (e) => /Tool|Permission/.test(e) ? "" : void 0, timeout: 5 };
1463
1661
  var eventsFor2 = (lean) => lean ? CODEX_EVENTS.filter((e) => e !== "PreToolUse") : [...CODEX_EVENTS];
1464
- var codexHome = (env) => env.CODEX_HOME || path6.join(homeDir(), ".codex");
1465
- var hooksFile2 = (env) => path6.join(codexHome(env), "hooks.json");
1466
- var configFile2 = (env) => path6.join(codexHome(env), "config.toml");
1467
- var sessionsDir = (env) => path6.join(codexHome(env), "sessions");
1662
+ var codexHome = (env) => env.CODEX_HOME || path7.join(homeDir(), ".codex");
1663
+ var hooksFile2 = (env) => path7.join(codexHome(env), "hooks.json");
1664
+ var configFile2 = (env) => path7.join(codexHome(env), "config.toml");
1665
+ var sessionsDir = (env) => path7.join(codexHome(env), "sessions");
1468
1666
  var validTool3 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
1469
1667
  var endReason2 = (r) => r === "clear" || r === "logout" || r === "exit" ? r : "other";
1470
1668
  function installNotify(cmd, env) {
1471
1669
  const file = configFile2(env);
1472
1670
  let text = "";
1473
1671
  try {
1474
- text = readFileSync4(file, "utf8");
1672
+ text = readFileSync5(file, "utf8");
1475
1673
  } catch {
1476
1674
  }
1477
1675
  const argv = cmd([NAME3, "notify"]).argv;
1478
1676
  const line = `notify = ${JSON.stringify(argv)}`;
1479
1677
  const n = readNotify(text);
1480
1678
  const write = (next) => {
1481
- mkdirSync3(path6.dirname(file), { recursive: true });
1482
- writeFileSync3(file, next);
1679
+ mkdirSync3(path7.dirname(file), { recursive: true });
1680
+ writeFileSync4(file, next);
1483
1681
  };
1484
1682
  if (n && !n.other) {
1485
1683
  if (!n.misplaced && JSON.stringify(n.argv) === JSON.stringify(argv))
@@ -1517,19 +1715,19 @@ function uninstallNotify(env) {
1517
1715
  const file = configFile2(env);
1518
1716
  let text;
1519
1717
  try {
1520
- text = readFileSync4(file, "utf8");
1718
+ text = readFileSync5(file, "utf8");
1521
1719
  } catch {
1522
1720
  return { file, changed: false };
1523
1721
  }
1524
1722
  const n = readNotify(text);
1525
1723
  if (!n) return { file, changed: false };
1526
1724
  if (!n.other) {
1527
- writeFileSync3(file, withoutNotify(text));
1725
+ writeFileSync4(file, withoutNotify(text));
1528
1726
  return { file, changed: true };
1529
1727
  }
1530
1728
  const prev = previousNotify(n.argv);
1531
1729
  if (isComputerUse(n.argv) && prev && OURS2.test(JSON.stringify(prev))) {
1532
- writeFileSync3(file, replaceNotify(text, n, `notify = ${stripPreviousRaw(n.raw)}`));
1730
+ writeFileSync4(file, replaceNotify(text, n, `notify = ${stripPreviousRaw(n.raw)}`));
1533
1731
  return { file, changed: true };
1534
1732
  }
1535
1733
  return { file, changed: false };
@@ -1545,7 +1743,7 @@ function findRollout(threadId, env = process.env) {
1545
1743
  return;
1546
1744
  }
1547
1745
  for (const n of names.sort().reverse()) {
1548
- const p = path6.join(dir, n);
1746
+ const p = path7.join(dir, n);
1549
1747
  if (n.endsWith(`-${threadId}.jsonl`)) {
1550
1748
  best = p;
1551
1749
  return;
@@ -1589,7 +1787,7 @@ var codex = {
1589
1787
  ];
1590
1788
  let text = "";
1591
1789
  try {
1592
- text = readFileSync4(configFile2(env), "utf8");
1790
+ text = readFileSync5(configFile2(env), "utf8");
1593
1791
  } catch {
1594
1792
  }
1595
1793
  const n = readNotify(text);
@@ -1716,7 +1914,7 @@ var codex = {
1716
1914
  readTranscript(file, fromLine) {
1717
1915
  let text = "";
1718
1916
  try {
1719
- text = readFileSync4(file, "utf8");
1917
+ text = readFileSync5(file, "utf8");
1720
1918
  } catch {
1721
1919
  return { turns: [], line: fromLine };
1722
1920
  }
@@ -1730,7 +1928,7 @@ var codex = {
1730
1928
  backfill(sinceMs, env = process.env) {
1731
1929
  const rows2 = [];
1732
1930
  for (const f of recentFiles(sessionsDir(env), 3, sinceMs, (n) => /^rollout-.*\.jsonl$/.test(n))) {
1733
- const m = /([0-9a-f]{8}-[0-9a-f-]{27,})\.jsonl$/i.exec(path6.basename(f));
1931
+ const m = /([0-9a-f]{8}-[0-9a-f-]{27,})\.jsonl$/i.exec(path7.basename(f));
1734
1932
  if (!m?.[1]) continue;
1735
1933
  if (codexSubagent(rolloutMeta(f))) continue;
1736
1934
  const facts = codexFacts(f);
@@ -1739,12 +1937,40 @@ var codex = {
1739
1937
  rows2.push({ session: { id: m[1], ...rest }, started_at, last_at });
1740
1938
  }
1741
1939
  return rows2;
1742
- }
1940
+ },
1941
+ limits: (transcript) => codexLimits(transcript)
1743
1942
  };
1943
+ var epochIso2 = (t) => typeof t === "number" && t > 0 && t < 1e11 ? new Date(t * 1e3).toISOString() : void 0;
1744
1944
  function prune3(o) {
1745
1945
  for (const k of Object.keys(o)) if (o[k] === void 0) delete o[k];
1746
1946
  return o;
1747
1947
  }
1948
+ function codexLimits(file) {
1949
+ const { tail } = ends(file, 0, 256 * 1024);
1950
+ for (const r of parseLines(tail).reverse()) {
1951
+ const p = r.payload;
1952
+ if (r.type !== "event_msg" || p?.type !== "token_count") continue;
1953
+ if (!p.rate_limits || typeof p.rate_limits !== "object") continue;
1954
+ const rl = p.rate_limits;
1955
+ if (rl.limit_id != null && rl.limit_id !== "codex") continue;
1956
+ const windows = [];
1957
+ for (const id of ["primary", "secondary"]) {
1958
+ const w = rl[id];
1959
+ if (typeof w?.used_percent !== "number" || w.used_percent < 0) continue;
1960
+ windows.push(
1961
+ prune3({
1962
+ id,
1963
+ used_percent: w.used_percent,
1964
+ window_minutes: Number.isInteger(w.window_minutes) && Number(w.window_minutes) > 0 ? Number(w.window_minutes) : void 0,
1965
+ resets_at: epochIso2(w.resets_at)
1966
+ })
1967
+ );
1968
+ }
1969
+ if (!windows.length) return null;
1970
+ return prune3({ windows, plan: typeof rl.plan_type === "string" ? rl.plan_type.slice(0, 64) : void 0 });
1971
+ }
1972
+ return null;
1973
+ }
1748
1974
  function codexFacts(file) {
1749
1975
  const { head, tail } = ends(file, 128 * 1024, 64 * 1024);
1750
1976
  let H = parseLines(head);
@@ -1789,7 +2015,7 @@ function codexFacts(file) {
1789
2015
  // ../core/src/adapters/devin.ts
1790
2016
  import { createHash as createHash2 } from "node:crypto";
1791
2017
  import { existsSync as existsSync6, readlinkSync } from "node:fs";
1792
- import path7 from "node:path";
2018
+ import path8 from "node:path";
1793
2019
 
1794
2020
  // ../core/src/readers/sqlite.ts
1795
2021
  import { execFileSync as execFileSync3 } from "node:child_process";
@@ -1973,11 +2199,11 @@ function foreignEvent(file) {
1973
2199
  return Object.keys(hooks).find((k) => !ok.includes(k));
1974
2200
  }
1975
2201
  var homeOf = (env) => env.HOME || homeDir();
1976
- var appData = (env) => env.APPDATA || path7.join(homeOf(env), "AppData", "Roaming");
1977
- 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");
1978
- var configFile3 = (env) => path7.join(devinConfigDir(env), "config.json");
1979
- 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");
1980
- var sessionsDb = (env) => path7.join(devinDataDir(env), "sessions.db");
2202
+ var appData = (env) => env.APPDATA || path8.join(homeOf(env), "AppData", "Roaming");
2203
+ 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");
2204
+ var configFile3 = (env) => path8.join(devinConfigDir(env), "config.json");
2205
+ 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");
2206
+ var sessionsDb = (env) => path8.join(devinDataDir(env), "sessions.db");
1981
2207
  var SESSION_ID = /^[A-Za-z0-9._:-]{1,128}$/;
1982
2208
  var validTool4 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
1983
2209
  var endReason3 = (r) => r === "clear" || r === "logout" || r === "exit" ? r : "other";
@@ -2149,7 +2375,7 @@ var BACKFILL_SQL = `SELECT s.id, ${SESSION_FIELDS} FROM sessions s
2149
2375
  function sessionFacts(row) {
2150
2376
  const out = {};
2151
2377
  if (typeof row.model === "string" && row.model) out.model = row.model;
2152
- if (typeof row.working_directory === "string" && path7.isAbsolute(row.working_directory))
2378
+ if (typeof row.working_directory === "string" && path8.isAbsolute(row.working_directory))
2153
2379
  out.cwd = row.working_directory;
2154
2380
  const started = Number(row.created_at);
2155
2381
  const last = Number(row.last_activity_at);
@@ -2194,8 +2420,8 @@ function readDevinChain(db, session, fromLine) {
2194
2420
  }
2195
2421
  function devinVersion(env = process.env) {
2196
2422
  try {
2197
- const target = readlinkSync(path7.join(devinDataDir(env), "_versions", "current"));
2198
- const name = path7.basename(target);
2423
+ const target = readlinkSync(path8.join(devinDataDir(env), "_versions", "current"));
2424
+ const name = path8.basename(target);
2199
2425
  return /^[0-9][0-9A-Za-z.+-]{0,63}$/.test(name) ? name : void 0;
2200
2426
  } catch {
2201
2427
  return void 0;
@@ -2209,8 +2435,8 @@ function devinAccount(env = process.env) {
2209
2435
 
2210
2436
  // ../core/src/adapters/gemini-cli.ts
2211
2437
  import { execFileSync as execFileSync4 } from "node:child_process";
2212
- import { existsSync as existsSync7, readFileSync as readFileSync5 } from "node:fs";
2213
- import path8 from "node:path";
2438
+ import { existsSync as existsSync7, readFileSync as readFileSync6 } from "node:fs";
2439
+ import path9 from "node:path";
2214
2440
  var NAME5 = "gemini-cli";
2215
2441
  var GEMINI_EVENTS = [
2216
2442
  "SessionStart",
@@ -2223,26 +2449,31 @@ var GEMINI_EVENTS = [
2223
2449
  "SessionEnd"
2224
2450
  ];
2225
2451
  var OPTS4 = { matcherFor: (e) => /Tool/.test(e) ? ".*" : void 0, timeout: 5e3 };
2226
- var geminiDir = (env) => env.GEMINI_CLI_HOME || path8.join(homeDir(), ".gemini");
2227
- var settings = (env) => path8.join(geminiDir(env), "settings.json");
2452
+ var geminiDir = (env) => env.GEMINI_CLI_HOME || path9.join(homeDir(), ".gemini");
2453
+ var settings = (env) => path9.join(geminiDir(env), "settings.json");
2228
2454
  var validTool5 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
2229
2455
  var eventsFor4 = (lean) => lean ? GEMINI_EVENTS.filter((e) => e !== "BeforeTool") : [...GEMINI_EVENTS];
2456
+ function geminiVersion(platform = process.platform, env = process.env) {
2457
+ const win = platform === "win32";
2458
+ const bin = win ? findBinary({ names: ["gemini.exe", "gemini.cmd"], dirs: [path9.dirname(process.execPath)], env }) : "gemini";
2459
+ if (!bin) throw new Error("no gemini on PATH");
2460
+ const c = probe(bin, ["--version"]);
2461
+ return execFileSync4(c.file, c.args, {
2462
+ encoding: "utf8",
2463
+ timeout: 4e3,
2464
+ stdio: ["ignore", "pipe", "ignore"],
2465
+ env,
2466
+ shell: c.shell
2467
+ }).trim();
2468
+ }
2230
2469
  function geminiHooksActive(settingsFile) {
2231
- let tools;
2232
- try {
2233
- tools = JSON.parse(readFileSync5(settingsFile, "utf8")).tools;
2234
- } catch {
2235
- }
2470
+ const tools = readJson(settingsFile).tools;
2236
2471
  if (tools?.enableHooks === true) return { active: true };
2237
2472
  if (tools?.enableHooks === false)
2238
2473
  return { active: false, note: "inactive: tools.enableHooks is false in settings.json" };
2239
2474
  let version = "";
2240
2475
  try {
2241
- version = execFileSync4("gemini", ["--version"], {
2242
- encoding: "utf8",
2243
- timeout: 4e3,
2244
- stdio: ["ignore", "pipe", "ignore"]
2245
- }).trim();
2476
+ version = geminiVersion();
2246
2477
  } catch {
2247
2478
  return {
2248
2479
  active: true,
@@ -2260,7 +2491,7 @@ var geminiCli = {
2260
2491
  name: NAME5,
2261
2492
  events: GEMINI_EVENTS,
2262
2493
  // ~/.gemini also belongs to Antigravity; Gemini CLI is here when its settings or its chat logs exist.
2263
- detect: (env = process.env) => existsSync7(settings(env)) || existsSync7(path8.join(geminiDir(env), "tmp")),
2494
+ detect: (env = process.env) => existsSync7(settings(env)) || existsSync7(path9.join(geminiDir(env), "tmp")),
2264
2495
  configFiles: (env = process.env) => [settings(env)],
2265
2496
  install: (cmd, env = process.env, opts = {}) => {
2266
2497
  const r = installClaudeShaped(settings(env), NAME5, eventsFor4(opts.lean), cmd, OPTS4);
@@ -2358,7 +2589,7 @@ var geminiCli = {
2358
2589
  readTranscript(file, fromLine) {
2359
2590
  let text = "";
2360
2591
  try {
2361
- text = readFileSync5(file, "utf8");
2592
+ text = readFileSync6(file, "utf8");
2362
2593
  } catch {
2363
2594
  return { turns: [], line: fromLine };
2364
2595
  }
@@ -2398,12 +2629,12 @@ var ADAPTERS = [claudeCode, codex, geminiCli, antigravity, devin];
2398
2629
  var adapterByName = (name) => ADAPTERS.find((a) => a.name === name);
2399
2630
 
2400
2631
  // ../core/src/config.ts
2401
- import { chmodSync, existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "node:fs";
2402
- import path9 from "node:path";
2632
+ import { chmodSync, existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "node:fs";
2633
+ import path10 from "node:path";
2403
2634
  var DEFAULT_CONFIG = { sinks: [], harnesses: {}, update_check: true, keep_days: 30 };
2404
2635
  function readConfig(file = configFile()) {
2405
2636
  try {
2406
- const j = JSON.parse(readFileSync6(file, "utf8"));
2637
+ const j = JSON.parse(readFileSync7(file, "utf8"));
2407
2638
  return {
2408
2639
  ...DEFAULT_CONFIG,
2409
2640
  ...j,
@@ -2415,8 +2646,8 @@ function readConfig(file = configFile()) {
2415
2646
  }
2416
2647
  }
2417
2648
  function writeConfig(c, file = configFile()) {
2418
- mkdirSync4(path9.dirname(file), { recursive: true, mode: 448 });
2419
- writeFileSync4(file, `${JSON.stringify(c, null, 2)}
2649
+ mkdirSync4(path10.dirname(file), { recursive: true, mode: 448 });
2650
+ writeFileSync5(file, `${JSON.stringify(c, null, 2)}
2420
2651
  `, { mode: 384 });
2421
2652
  if (process.platform !== "win32" && existsSync8(file)) {
2422
2653
  try {
@@ -2503,14 +2734,14 @@ import {
2503
2734
  mkdirSync as mkdirSync5,
2504
2735
  openSync as openSync2,
2505
2736
  readdirSync as readdirSync5,
2506
- readFileSync as readFileSync7,
2737
+ readFileSync as readFileSync8,
2507
2738
  readSync as readSync2,
2508
2739
  renameSync,
2509
- statSync as statSync4,
2740
+ statSync as statSync5,
2510
2741
  unlinkSync as unlinkSync3,
2511
- writeFileSync as writeFileSync5
2742
+ writeFileSync as writeFileSync6
2512
2743
  } from "node:fs";
2513
- import path10 from "node:path";
2744
+ import path11 from "node:path";
2514
2745
  var READ_CHUNK = 1024 * 1024;
2515
2746
  var READ_MAX = 8 * 1024 * 1024;
2516
2747
  var Outbox = class {
@@ -2519,13 +2750,13 @@ var Outbox = class {
2519
2750
  }
2520
2751
  root;
2521
2752
  dir(harness) {
2522
- return path10.join(this.root, "outbox", safeId(harness));
2753
+ return path11.join(this.root, "outbox", safeId(harness));
2523
2754
  }
2524
2755
  file(ref) {
2525
- return path10.join(this.dir(ref.harness), `${safeId(ref.session)}.jsonl`);
2756
+ return path11.join(this.dir(ref.harness), `${safeId(ref.session)}.jsonl`);
2526
2757
  }
2527
2758
  seqFile(ref) {
2528
- return path10.join(this.dir(ref.harness), `${safeId(ref.session)}.seq`);
2759
+ return path11.join(this.dir(ref.harness), `${safeId(ref.session)}.seq`);
2529
2760
  }
2530
2761
  /** The next seq for a session, persisted (survives a hibernating machine and a
2531
2762
  * restart; an in-memory counter would restart at 0 and collide). */
@@ -2533,19 +2764,19 @@ var Outbox = class {
2533
2764
  const f = this.seqFile(ref);
2534
2765
  let n = -1;
2535
2766
  try {
2536
- const t = readFileSync7(f, "utf8").trim();
2767
+ const t = readFileSync8(f, "utf8").trim();
2537
2768
  if (/^\d+$/.test(t)) n = Number(t);
2538
2769
  } catch {
2539
2770
  }
2540
2771
  if (n < 0) n = this.lastSeqInFile(ref) + 1;
2541
- mkdirSync5(path10.dirname(f), { recursive: true, mode: 448 });
2542
- writeFileSync5(f, String(n + 1), { mode: 384 });
2772
+ mkdirSync5(path11.dirname(f), { recursive: true, mode: 448 });
2773
+ writeFileSync6(f, String(n + 1), { mode: 384 });
2543
2774
  return n;
2544
2775
  }
2545
2776
  lastSeqInFile(ref) {
2546
2777
  const f = this.file(ref);
2547
2778
  if (!existsSync9(f)) return -1;
2548
- const lines = readFileSync7(f, "utf8").trim().split("\n");
2779
+ const lines = readFileSync8(f, "utf8").trim().split("\n");
2549
2780
  for (let i = lines.length - 1; i >= 0; i--) {
2550
2781
  try {
2551
2782
  const j = JSON.parse(lines[i]);
@@ -2564,10 +2795,10 @@ var Outbox = class {
2564
2795
  /** Every session file, newest first. */
2565
2796
  sessions() {
2566
2797
  const out = [];
2567
- const base = path10.join(this.root, "outbox");
2798
+ const base = path11.join(this.root, "outbox");
2568
2799
  if (!existsSync9(base)) return [];
2569
2800
  for (const h of readdirSync5(base)) {
2570
- const d = path10.join(base, h);
2801
+ const d = path11.join(base, h);
2571
2802
  let names = [];
2572
2803
  try {
2573
2804
  names = readdirSync5(d).filter((n) => n.endsWith(".jsonl"));
@@ -2576,7 +2807,7 @@ var Outbox = class {
2576
2807
  }
2577
2808
  for (const n of names) {
2578
2809
  try {
2579
- out.push({ ref: { harness: h, session: n.slice(0, -6) }, mtime: statSync4(path10.join(d, n)).mtimeMs });
2810
+ out.push({ ref: { harness: h, session: n.slice(0, -6) }, mtime: statSync5(path11.join(d, n)).mtimeMs });
2580
2811
  } catch {
2581
2812
  }
2582
2813
  }
@@ -2592,7 +2823,7 @@ var Outbox = class {
2592
2823
  read(ref, from, maxEvents = 500) {
2593
2824
  const f = this.file(ref);
2594
2825
  if (!existsSync9(f)) return { events: [], offset: from };
2595
- const size = statSync4(f).size;
2826
+ const size = statSync5(f).size;
2596
2827
  if (from >= size) return { events: [], offset: from };
2597
2828
  const fd = openSync2(f, "r");
2598
2829
  try {
@@ -2634,7 +2865,7 @@ var Outbox = class {
2634
2865
  for (const ref of this.sessions()) {
2635
2866
  const f = this.file(ref);
2636
2867
  try {
2637
- if (statSync4(f).mtimeMs < cutoff) {
2868
+ if (statSync5(f).mtimeMs < cutoff) {
2638
2869
  unlinkSync3(f);
2639
2870
  try {
2640
2871
  unlinkSync3(this.seqFile(ref));
@@ -2664,20 +2895,20 @@ var Cursors = class {
2664
2895
  root;
2665
2896
  sink;
2666
2897
  file() {
2667
- return path10.join(this.root, "cursors", `${safeId(this.sink)}.json`);
2898
+ return path11.join(this.root, "cursors", `${safeId(this.sink)}.json`);
2668
2899
  }
2669
2900
  read() {
2670
2901
  try {
2671
- return JSON.parse(readFileSync7(this.file(), "utf8"));
2902
+ return JSON.parse(readFileSync8(this.file(), "utf8"));
2672
2903
  } catch {
2673
2904
  return {};
2674
2905
  }
2675
2906
  }
2676
2907
  write(c) {
2677
2908
  const f = this.file();
2678
- mkdirSync5(path10.dirname(f), { recursive: true, mode: 448 });
2909
+ mkdirSync5(path11.dirname(f), { recursive: true, mode: 448 });
2679
2910
  const tmp = `${f}.${process.pid}.tmp`;
2680
- writeFileSync5(tmp, JSON.stringify(c), { mode: 384 });
2911
+ writeFileSync6(tmp, JSON.stringify(c), { mode: 384 });
2681
2912
  renameSync(tmp, f);
2682
2913
  }
2683
2914
  key(ref) {
@@ -2688,16 +2919,16 @@ function takeLock(root, ref, staleMs = 5 * 6e4) {
2688
2919
  return takeNamedLock(root, `${safeId(ref.harness)}-${safeId(ref.session)}`, staleMs);
2689
2920
  }
2690
2921
  function takeNamedLock(root, name, staleMs = 5 * 6e4) {
2691
- const dir = path10.join(root, "locks");
2922
+ const dir = path11.join(root, "locks");
2692
2923
  mkdirSync5(dir, { recursive: true, mode: 448 });
2693
- const f = path10.join(dir, `${name}.lock`);
2924
+ const f = path11.join(dir, `${name}.lock`);
2694
2925
  try {
2695
- if (Date.now() - statSync4(f).mtimeMs > staleMs || !processAlive(Number(readFileSync7(f, "utf8")))) unlinkSync3(f);
2926
+ if (Date.now() - statSync5(f).mtimeMs > staleMs || !processAlive(Number(readFileSync8(f, "utf8")))) unlinkSync3(f);
2696
2927
  else return null;
2697
2928
  } catch {
2698
2929
  }
2699
2930
  try {
2700
- writeFileSync5(f, String(process.pid), { flag: "wx" });
2931
+ writeFileSync6(f, String(process.pid), { flag: "wx" });
2701
2932
  } catch {
2702
2933
  return null;
2703
2934
  }
@@ -2929,6 +3160,7 @@ var TYPE_TIER = {
2929
3160
  "subagent.started": 1,
2930
3161
  "subagent.ended": 1,
2931
3162
  "context.compacted": 1,
3163
+ "limits.observed": 1,
2932
3164
  native: 3
2933
3165
  };
2934
3166
  var DATA_ALLOW = {
@@ -2948,7 +3180,8 @@ var DATA_ALLOW = {
2948
3180
  "attention.cleared": { 0: ["attention_id", "how"] },
2949
3181
  "subagent.started": { 1: ["agent_id", "agent_type"] },
2950
3182
  "subagent.ended": { 1: ["agent_id", "agent_type"] },
2951
- "context.compacted": { 1: ["trigger"] }
3183
+ "context.compacted": { 1: ["trigger"] },
3184
+ "limits.observed": { 1: ["windows", "plan", "account_id"] }
2952
3185
  };
2953
3186
  var SESSION_ALLOW = [
2954
3187
  "id",
@@ -3013,7 +3246,9 @@ function filterEvent(event, tier, pii, opts = {}) {
3013
3246
  const s = reducePiiDeep(out.session, opts.home);
3014
3247
  if (typeof s.machine === "string") s.machine = hashMachine(s.machine);
3015
3248
  delete s.account_id;
3016
- out = { ...out, session: s, data: reducePiiDeep(out.data, opts.home) };
3249
+ const d = reducePiiDeep(out.data, opts.home);
3250
+ delete d.account_id;
3251
+ out = { ...out, session: s, data: d };
3017
3252
  rulesets.push(RULESET_PII);
3018
3253
  }
3019
3254
  out.privacy = { rulesets, pii };
@@ -3028,7 +3263,7 @@ function shortId(harness, id) {
3028
3263
 
3029
3264
  // ../core/src/sinks/file.ts
3030
3265
  import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync6 } from "node:fs";
3031
- import path11 from "node:path";
3266
+ import path12 from "node:path";
3032
3267
  var FileSink = class {
3033
3268
  constructor(name, file, tier, pii) {
3034
3269
  this.name = name;
@@ -3042,7 +3277,7 @@ var FileSink = class {
3042
3277
  pii;
3043
3278
  async send(events) {
3044
3279
  try {
3045
- mkdirSync6(path11.dirname(this.file), { recursive: true });
3280
+ mkdirSync6(path12.dirname(this.file), { recursive: true });
3046
3281
  appendFileSync2(this.file, `${events.map((e) => JSON.stringify(e)).join("\n")}
3047
3282
  `);
3048
3283
  return { ok: true, acknowledged: events.length, accepted: events.length, duplicates: 0, rejected: [] };
@@ -3081,18 +3316,18 @@ import {
3081
3316
  existsSync as existsSync10,
3082
3317
  mkdirSync as mkdirSync8,
3083
3318
  readdirSync as readdirSync6,
3084
- readFileSync as readFileSync9,
3319
+ readFileSync as readFileSync10,
3085
3320
  renameSync as renameSync3,
3086
- statSync as statSync5,
3321
+ statSync as statSync6,
3087
3322
  unlinkSync as unlinkSync4,
3088
- writeFileSync as writeFileSync7
3323
+ writeFileSync as writeFileSync8
3089
3324
  } from "node:fs";
3090
3325
  import os3 from "node:os";
3091
- import path14 from "node:path";
3326
+ import path15 from "node:path";
3092
3327
 
3093
3328
  // src/control/aliases.ts
3094
- import { chmodSync as chmodSync2, mkdirSync as mkdirSync7, readFileSync as readFileSync8, renameSync as renameSync2, writeFileSync as writeFileSync6 } from "node:fs";
3095
- import path12 from "node:path";
3329
+ import { chmodSync as chmodSync2, mkdirSync as mkdirSync7, readFileSync as readFileSync9, renameSync as renameSync2, writeFileSync as writeFileSync7 } from "node:fs";
3330
+ import path13 from "node:path";
3096
3331
 
3097
3332
  // src/secrets.ts
3098
3333
  import { spawnSync } from "node:child_process";
@@ -3136,10 +3371,10 @@ function startOptionsOf(v) {
3136
3371
  }
3137
3372
 
3138
3373
  // src/control/aliases.ts
3139
- var aliasFile = (controlDir) => path12.join(controlDir, "aliases.json");
3374
+ var aliasFile = (controlDir) => path13.join(controlDir, "aliases.json");
3140
3375
  function readAliases(file) {
3141
3376
  try {
3142
- const j = JSON.parse(readFileSync8(file, "utf8"));
3377
+ const j = JSON.parse(readFileSync9(file, "utf8"));
3143
3378
  if (!j || typeof j !== "object" || Array.isArray(j)) return {};
3144
3379
  const out = {};
3145
3380
  for (const [k, v] of Object.entries(j)) {
@@ -3161,11 +3396,11 @@ function receiverIdOf(all, harness, id) {
3161
3396
  // src/control/local.ts
3162
3397
  import net from "node:net";
3163
3398
  import os2 from "node:os";
3164
- import path13 from "node:path";
3399
+ import path14 from "node:path";
3165
3400
  function socketPath(state, env = process.env) {
3166
3401
  if (env.SESSIONPIPE_SOCKET) return env.SESSIONPIPE_SOCKET;
3167
3402
  if (process.platform === "win32") return `\\\\.\\pipe\\sessionpipe-control-${os2.userInfo().username}`;
3168
- return path13.join(state, "control.sock");
3403
+ return path14.join(state, "control.sock");
3169
3404
  }
3170
3405
  function ask(sock, req, timeoutMs) {
3171
3406
  return new Promise((resolve) => {
@@ -3312,8 +3547,8 @@ var MAX_TRIES = 10;
3312
3547
  function enqueueJob(state, job) {
3313
3548
  const jobs = jobsDir(state);
3314
3549
  mkdirSync8(jobs, { recursive: true, mode: 448 });
3315
- const f = path14.join(jobs, `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}.json`);
3316
- writeFileSync7(f, JSON.stringify(job), { mode: 384 });
3550
+ const f = path15.join(jobs, `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}.json`);
3551
+ writeFileSync8(f, JSON.stringify(job), { mode: 384 });
3317
3552
  return f;
3318
3553
  }
3319
3554
  function claimJob(file) {
@@ -3324,7 +3559,7 @@ function claimJob(file) {
3324
3559
  return null;
3325
3560
  }
3326
3561
  try {
3327
- return { job: JSON.parse(readFileSync9(claimed, "utf8")), file: claimed };
3562
+ return { job: JSON.parse(readFileSync10(claimed, "utf8")), file: claimed };
3328
3563
  } catch {
3329
3564
  releaseJob(claimed);
3330
3565
  return null;
@@ -3349,19 +3584,19 @@ async function sweepJobs(state, olderThanMs = 3e3, limit = 20, abandonedMs = 6e4
3349
3584
  for (const n of names) {
3350
3585
  const m = /^(.*\.json)\.(\d+)\.running$/.exec(n);
3351
3586
  if (!m) continue;
3352
- const f = path14.join(jobs, n);
3587
+ const f = path15.join(jobs, n);
3353
3588
  try {
3354
- if (processAlive(Number(m[2])) && Date.now() - statSync5(f).mtimeMs < abandonedMs) continue;
3355
- renameSync3(f, path14.join(jobs, m[1]));
3589
+ if (processAlive(Number(m[2])) && Date.now() - statSync6(f).mtimeMs < abandonedMs) continue;
3590
+ renameSync3(f, path15.join(jobs, m[1]));
3356
3591
  queued.push(m[1]);
3357
3592
  } catch {
3358
3593
  }
3359
3594
  }
3360
3595
  let ran = 0;
3361
3596
  for (const n of queued.sort().slice(0, limit)) {
3362
- const f = path14.join(jobs, n);
3597
+ const f = path15.join(jobs, n);
3363
3598
  try {
3364
- if (Date.now() - statSync5(f).mtimeMs < olderThanMs) continue;
3599
+ if (Date.now() - statSync6(f).mtimeMs < olderThanMs) continue;
3365
3600
  } catch {
3366
3601
  continue;
3367
3602
  }
@@ -3380,9 +3615,9 @@ async function sweepJobs(state, olderThanMs = 3e3, limit = 20, abandonedMs = 6e4
3380
3615
  function log(state, line) {
3381
3616
  try {
3382
3617
  mkdirSync8(state, { recursive: true, mode: 448 });
3383
- const f = path14.join(state, "log");
3618
+ const f = path15.join(state, "log");
3384
3619
  try {
3385
- if (statSync5(f).size > 1024 * 1024) renameSync3(f, `${f}.1`);
3620
+ if (statSync6(f).size > 1024 * 1024) renameSync3(f, `${f}.1`);
3386
3621
  } catch {
3387
3622
  }
3388
3623
  appendFileSync3(f, `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
@@ -3391,11 +3626,11 @@ function log(state, line) {
3391
3626
  }
3392
3627
  }
3393
3628
  function factsState(state) {
3394
- const file = (h, s) => path14.join(state, "facts", safeId(h), `${safeId(s)}.json`);
3629
+ const file = (h, s) => path15.join(state, "facts", safeId(h), `${safeId(s)}.json`);
3395
3630
  return {
3396
3631
  get(h, s) {
3397
3632
  try {
3398
- return JSON.parse(readFileSync9(file(h, s), "utf8"));
3633
+ return JSON.parse(readFileSync10(file(h, s), "utf8"));
3399
3634
  } catch {
3400
3635
  return {};
3401
3636
  }
@@ -3404,8 +3639,8 @@ function factsState(state) {
3404
3639
  const cur = this.get(h, s);
3405
3640
  const next = { ...cur, ...patch };
3406
3641
  if (JSON.stringify(next) === JSON.stringify(cur)) return;
3407
- mkdirSync8(path14.dirname(file(h, s)), { recursive: true, mode: 448 });
3408
- writeFileSync7(file(h, s), JSON.stringify(next), { mode: 384 });
3642
+ mkdirSync8(path15.dirname(file(h, s)), { recursive: true, mode: 448 });
3643
+ writeFileSync8(file(h, s), JSON.stringify(next), { mode: 384 });
3409
3644
  }
3410
3645
  };
3411
3646
  }
@@ -3477,7 +3712,7 @@ async function runJob(job, opts = {}) {
3477
3712
  );
3478
3713
  return { events: [], delivered: {} };
3479
3714
  }
3480
- const id = receiverIdOf(readAliases(aliasFile(path14.join(state, "control"))), job.harness, r.session.id) ?? r.session.id;
3715
+ const id = receiverIdOf(readAliases(aliasFile(path15.join(state, "control"))), job.harness, r.session.id) ?? r.session.id;
3481
3716
  const claudeEnd = job.harness === "claude-code" && r.events.some((e) => e.type === "session.ended");
3482
3717
  if (!claudeEnd) await tellDaemon(state, job, id, r.session.cwd, r.transcript);
3483
3718
  const outbox = new Outbox(state);
@@ -3506,12 +3741,15 @@ async function runJob(job, opts = {}) {
3506
3741
  const heavy = !/^(PreToolUse|PostToolUse|PostToolUseFailure|BeforeTool|AfterTool|PostInvocation)$/.test(job.event);
3507
3742
  let learned = {};
3508
3743
  if (heavy) {
3509
- const f = adapter.facts(r.session, r.transcript, r.hints ?? {}, facts, { ...process.env, ...job.env });
3744
+ const hookEnv = { ...process.env, ...job.env };
3745
+ const f = adapter.facts(r.session, r.transcript, r.hints ?? {}, facts, hookEnv);
3746
+ if (cfg.remote_titles && adapter.remoteFacts)
3747
+ Object.assign(f, await adapter.remoteFacts(r.session, f, r.transcript, facts, hookEnv));
3510
3748
  const { started_at: _s, last_at: _l, first_ask: _fa, harness_version, ...rest } = f;
3511
3749
  learned = rest;
3512
3750
  if (harness_version) facts.save(job.harness, id, { harness_version });
3513
3751
  facts.save(job.harness, id, { session: { ...mem.session, ...learned } });
3514
- if (CWD_FROM_HOOKS.has(job.harness) && r.session.cwd && path14.isAbsolute(r.session.cwd))
3752
+ if (CWD_FROM_HOOKS.has(job.harness) && r.session.cwd && path15.isAbsolute(r.session.cwd))
3515
3753
  facts.save(job.harness, id, { cwd: r.session.cwd });
3516
3754
  }
3517
3755
  const known = mem.session ?? {};
@@ -3593,6 +3831,23 @@ async function runJob(job, opts = {}) {
3593
3831
  }
3594
3832
  if (read.turns.length) facts.save(job.harness, id, { transcript_line: read.line, transcript_turns: turnNo });
3595
3833
  }
3834
+ if (adapter.limits && r.transcript && ending && cfg.sinks.some((s) => !s.paused && Math.min(s.tier, s.max_tier ?? 3) >= 1)) {
3835
+ const limits = adapter.limits(r.transcript);
3836
+ const sig = limits && JSON.stringify(limits);
3837
+ if (limits && sig !== facts.get(job.harness, id).limits_sig) {
3838
+ events.push(
3839
+ makeEvent(
3840
+ { type: "limits.observed", harnessEvent: job.event, data: limits },
3841
+ {
3842
+ harness: { name: job.harness, ...version ? { version } : {} },
3843
+ session: { ...session, seq: outbox.nextSeq(ref) },
3844
+ now: opts.now ?? job.at
3845
+ }
3846
+ )
3847
+ );
3848
+ facts.save(job.harness, id, { limits_sig: sig });
3849
+ }
3850
+ }
3596
3851
  outbox.append(
3597
3852
  ref,
3598
3853
  events.map((e) => ({ ...e, session: redactDeep(e.session), data: redactDeep(e.data) }))
@@ -3610,11 +3865,11 @@ async function runJob(job, opts = {}) {
3610
3865
  return { events, delivered };
3611
3866
  }
3612
3867
  async function drain(state, sinkConfigs, outbox, first) {
3613
- const more = path14.join(state, "locks", "flush.more");
3868
+ const more = path15.join(state, "locks", "flush.more");
3614
3869
  let release = takeNamedLock(state, "flush");
3615
3870
  if (!release) {
3616
3871
  try {
3617
- writeFileSync7(more, "", { mode: 384 });
3872
+ writeFileSync8(more, "", { mode: 384 });
3618
3873
  } catch {
3619
3874
  }
3620
3875
  release = takeNamedLock(state, "flush");
@@ -3638,10 +3893,10 @@ async function drain(state, sinkConfigs, outbox, first) {
3638
3893
  }
3639
3894
  }
3640
3895
  var BACKOFF_MS = [2e3, 1e4, 6e4, 5 * 6e4, 30 * 6e4];
3641
- var backoffFile = (state, sink) => path14.join(state, "backoff", `${safeId(sink)}.json`);
3896
+ var backoffFile = (state, sink) => path15.join(state, "backoff", `${safeId(sink)}.json`);
3642
3897
  function readBackoff(state, sink) {
3643
3898
  try {
3644
- const b = JSON.parse(readFileSync9(backoffFile(state, sink), "utf8"));
3899
+ const b = JSON.parse(readFileSync10(backoffFile(state, sink), "utf8"));
3645
3900
  return typeof b.until === "number" ? b : null;
3646
3901
  } catch {
3647
3902
  return null;
@@ -3654,8 +3909,8 @@ function noteFailure(state, sink, res, prev) {
3654
3909
  const b = { until: Date.now() + wait, failures, why: `${res.status ?? ""} ${res.error ?? ""}`.trim() };
3655
3910
  const f = backoffFile(state, sink);
3656
3911
  try {
3657
- mkdirSync8(path14.dirname(f), { recursive: true, mode: 448 });
3658
- writeFileSync7(f, JSON.stringify(b), { mode: 384 });
3912
+ mkdirSync8(path15.dirname(f), { recursive: true, mode: 448 });
3913
+ writeFileSync8(f, JSON.stringify(b), { mode: 384 });
3659
3914
  } catch {
3660
3915
  }
3661
3916
  return b;
@@ -3787,7 +4042,7 @@ function applySinkVerdict(name, res) {
3787
4042
  } catch {
3788
4043
  }
3789
4044
  }
3790
- var jobsDir = (state) => path14.join(state, "jobs");
4045
+ var jobsDir = (state) => path15.join(state, "jobs");
3791
4046
 
3792
4047
  // src/worker.ts
3793
4048
  async function workerMain(argv) {