sessionpipe 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +74 -0
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +5085 -2360
  4. package/dist/cli.js.map +4 -4
  5. package/dist/control/cli.d.ts +4 -0
  6. package/dist/control/cli.d.ts.map +1 -1
  7. package/dist/control/daemon.d.ts +24 -0
  8. package/dist/control/daemon.d.ts.map +1 -1
  9. package/dist/control/local.d.ts +24 -0
  10. package/dist/control/local.d.ts.map +1 -1
  11. package/dist/control/pool.d.ts +49 -0
  12. package/dist/control/pool.d.ts.map +1 -0
  13. package/dist/control/route.d.ts +1 -1
  14. package/dist/control/route.d.ts.map +1 -1
  15. package/dist/help.d.ts.map +1 -1
  16. package/dist/hook.d.ts.map +1 -1
  17. package/dist/hook.js +37 -1
  18. package/dist/hook.js.map +2 -2
  19. package/dist/index.js +1101 -875
  20. package/dist/index.js.map +4 -4
  21. package/dist/pool/cli.d.ts +39 -0
  22. package/dist/pool/cli.d.ts.map +1 -0
  23. package/dist/pool/credentials.d.ts +44 -0
  24. package/dist/pool/credentials.d.ts.map +1 -0
  25. package/dist/pool/import.d.ts +23 -0
  26. package/dist/pool/import.d.ts.map +1 -0
  27. package/dist/pool/index.d.ts +138 -0
  28. package/dist/pool/index.d.ts.map +1 -0
  29. package/dist/pool/install.d.ts +46 -0
  30. package/dist/pool/install.d.ts.map +1 -0
  31. package/dist/pool/login.d.ts +27 -0
  32. package/dist/pool/login.d.ts.map +1 -0
  33. package/dist/pool/policy.d.ts +100 -0
  34. package/dist/pool/policy.d.ts.map +1 -0
  35. package/dist/pool/server.d.ts +76 -0
  36. package/dist/pool/server.d.ts.map +1 -0
  37. package/dist/pool/store.d.ts +89 -0
  38. package/dist/pool/store.d.ts.map +1 -0
  39. package/dist/pool/types.d.ts +92 -0
  40. package/dist/pool/types.d.ts.map +1 -0
  41. package/dist/run.d.ts +14 -1
  42. package/dist/run.d.ts.map +1 -1
  43. package/dist/worker.js +1096 -870
  44. package/dist/worker.js.map +4 -4
  45. package/package.json +1 -1
package/dist/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.11.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,48 +523,161 @@ 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(
622
+ ev("turn.ended", {
623
+ turn_id,
624
+ reason: "stop",
625
+ background: Array.isArray(s.background_tasks) ? s.background_tasks.length : void 0
626
+ })
627
+ );
528
628
  break;
529
- default:
530
- return { session, events: [], stdout };
629
+ case "StopFailure":
630
+ events.push(
631
+ ev("turn.ended", {
632
+ turn_id,
633
+ reason: "error",
634
+ error: typeof s.error === "string" ? s.error.slice(0, 200) : void 0
635
+ })
636
+ );
637
+ break;
638
+ case "SubagentStart":
639
+ events.push(
640
+ ev("subagent.started", {
641
+ agent_id: String(s.agent_id ?? ""),
642
+ agent_type: typeof s.agent_type === "string" ? s.agent_type : void 0
643
+ })
644
+ );
645
+ break;
646
+ case "SubagentStop":
647
+ events.push(
648
+ ev("subagent.ended", {
649
+ agent_id: String(s.agent_id ?? ""),
650
+ agent_type: typeof s.agent_type === "string" ? s.agent_type : void 0
651
+ })
652
+ );
653
+ break;
654
+ case "PreCompact":
655
+ events.push(ev("context.compacted", { trigger: s.trigger === "manual" ? "manual" : "auto" }));
656
+ break;
657
+ case "PostCompact":
658
+ break;
659
+ // PreCompact already said it; a second event would double-count
660
+ case "SessionEnd":
661
+ events.push(ev("session.ended", { reason: endReason(s.reason) }));
662
+ break;
663
+ case "StatusLine": {
664
+ const limits = claudeLimits(s.rate_limits);
665
+ if (!limits) return null;
666
+ events.push(ev("limits.observed", limits));
667
+ break;
668
+ }
669
+ default:
670
+ return null;
531
671
  }
532
- const r = { session, events, stdout, hints: { workspaces } };
533
- if (typeof s.transcriptPath === "string") r.transcript = s.transcriptPath;
672
+ const hints = {};
673
+ if (event === "UserPromptSubmit" && typeof s.prompt === "string") hints.prompt = s.prompt;
674
+ const host = input.env.CLAUDE_CODE_HOST_SESSION_ID;
675
+ if (host && DESKTOP_ID.test(host)) hints.desktop_id = host;
676
+ const attended = input.env.CLAUDE_CODE_SESSION_ATTENDED;
677
+ if (attended === "1" || attended === "0") hints.attended = attended === "1" || !!input.env.SESSIONPIPE_CONTROL_JOB;
678
+ if (event !== "PreToolUse" && event !== "PostToolUse") hints.claude_pid = claudePid(input.env);
679
+ const r = { session, events, hints };
680
+ if (typeof s.transcript_path === "string") r.transcript = s.transcript_path;
534
681
  return r;
535
682
  },
536
683
  readTranscript(file, fromLine) {
@@ -540,765 +687,777 @@ var antigravity = {
540
687
  } catch {
541
688
  return { turns: [], line: fromLine };
542
689
  }
543
- return readAntigravity(text.split("\n"), fromLine);
690
+ return readClaude(text.split("\n"), fromLine);
544
691
  },
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
- });
692
+ facts(session, transcript, hints, state) {
693
+ const out = {};
694
+ if (transcript) Object.assign(out, claudeFacts(transcript, session.id, hints, state));
695
+ const cwd = out.cwd ?? session.cwd;
696
+ Object.assign(out, gitFacts(cwd), out.branch ? { branch: out.branch } : {});
697
+ if (!out.url && typeof hints.desktop_id === "string") out.url = `claude://code/continue/${hints.desktop_id}`;
698
+ if (typeof hints.attended === "boolean") out.attended = hints.attended || !!out.url;
699
+ return out;
552
700
  },
701
+ remoteFacts: (session, local, transcript, state, env = process.env) => claudeRemoteFacts(session.id, local, transcript, state, env),
553
702
  backfill(sinceMs, env = process.env) {
554
703
  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
- }
704
+ const files = [];
705
+ for (const d of claudeDirs(env))
706
+ files.push(...recentFiles(path4.join(d, "projects"), 1, sinceMs, (n) => /^[0-9a-f-]{36}\.jsonl$/.test(n)));
707
+ const noState = { get: () => ({}), save: () => {
708
+ } };
709
+ for (const f of files) {
710
+ const id = path4.basename(f, ".jsonl");
711
+ const facts = claudeFacts(f, id, {}, noState);
712
+ if (!facts.started_at || !facts.last_at) continue;
713
+ const { started_at, last_at, first_ask: _fa, harness_version: _hv, ...rest } = facts;
714
+ const cwd = rest.cwd;
715
+ rows2.push({
716
+ session: { id, ...rest, ...gitFacts(cwd), ...rest.branch ? { branch: rest.branch } : {} },
717
+ started_at,
718
+ last_at,
719
+ transcript: f
720
+ });
573
721
  }
574
722
  return rows2;
575
723
  }
576
724
  };
725
+ var LINE_REFRESH_S = 60;
726
+ function statusLine(file, want, cmd) {
727
+ const f = readJsonFile(file);
728
+ if (!f.ok) return null;
729
+ const cur = f.value.statusLine;
730
+ const ours = typeof cur?.command === "string" && OURS.test(cur.command);
731
+ const command = cmd?.([NAME, "StatusLine"]).command;
732
+ if (want && cur && !ours)
733
+ return { file, changed: false, skipped: true, note: "a status line of your own is set; limits are not read" };
734
+ if (want ? ours && cur?.command === command && cur?.refreshInterval === LINE_REFRESH_S : !ours) return null;
735
+ if (want) f.value.statusLine = { type: "command", command, refreshInterval: LINE_REFRESH_S };
736
+ else delete f.value.statusLine;
737
+ writeFileSync2(file, formatJson(f.value, f));
738
+ return { file, changed: true, note: want ? "status line written (account limits)" : "status line removed" };
739
+ }
740
+ var epochIso = (t) => typeof t === "number" && t > 0 && t < 1e11 ? new Date(t * 1e3).toISOString() : void 0;
741
+ var WINDOW_MINUTES = { five_hour: 300, seven_day: 10080 };
742
+ function claudeLimits(rl) {
743
+ if (!rl || typeof rl !== "object" || Array.isArray(rl)) return null;
744
+ const windows = [];
745
+ for (const [id, w] of Object.entries(rl)) {
746
+ if (!/^[a-z0-9_]{1,64}$/.test(id) || typeof w?.used_percentage !== "number" || w.used_percentage < 0) continue;
747
+ windows.push(
748
+ prune({
749
+ id,
750
+ used_percent: w.used_percentage,
751
+ window_minutes: WINDOW_MINUTES[id],
752
+ resets_at: epochIso(w.resets_at)
753
+ })
754
+ );
755
+ }
756
+ return windows.length ? { windows: windows.slice(0, 8) } : null;
757
+ }
577
758
  function prune(o) {
578
759
  for (const k of Object.keys(o)) if (o[k] === void 0) delete o[k];
579
760
  return o;
580
761
  }
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);
762
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
763
+ function notClaudeCode(id, s) {
764
+ return typeof s.transcript_path !== "string" && !UUID.test(id);
594
765
  }
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 {
766
+ function pickSource(v) {
767
+ return typeof v === "string" && ["startup", "resume", "clear", "compact", "fork"].includes(v) ? v : "unknown";
768
+ }
769
+ var harnessWord = (v) => typeof v === "string" && /^[A-Za-z0-9][A-Za-z0-9_.-]{0,39}$/.test(v) ? v : void 0;
770
+ var WRAPPER = /^<(?:task-notification|cross-session-message|system-reminder|local-command-stdout|command-[a-z-]+)[\s/>]/i;
771
+ var injectedBlock = (prompt) => WRAPPER.test(prompt.trim());
772
+ function errText(v) {
773
+ if (typeof v === "string") return v.slice(0, 2e3);
774
+ if (v && typeof v === "object") {
775
+ const e = v;
776
+ if (typeof e.error === "string") return e.error.slice(0, 2e3);
777
+ if (typeof e.message === "string") return e.message.slice(0, 2e3);
602
778
  }
603
- return out;
779
+ return void 0;
604
780
  }
605
- function agProjects(dir) {
606
- const out = [];
781
+ function describe(input) {
782
+ if (!input || typeof input !== "object") return "";
783
+ const i = input;
784
+ const v = i.command ?? i.file_path ?? i.path ?? i.url ?? i.pattern;
785
+ return typeof v === "string" ? ` ${v.slice(0, 120)}` : "";
786
+ }
787
+ var NAME_RANK = {
788
+ user: "custom",
789
+ auto: "harness",
790
+ hook: "harness",
791
+ peer: "harness"
792
+ };
793
+ function registryEntry(j) {
794
+ if (!j || typeof j !== "object") return null;
795
+ const r = j;
796
+ const source = r.nameSource ? NAME_RANK[r.nameSource] : void 0;
797
+ return {
798
+ sessionId: r.sessionId ? String(r.sessionId) : null,
799
+ name: source && typeof r.name === "string" && r.name.trim() ? r.name.trim() : null,
800
+ name_source: source ?? null,
801
+ bridge: typeof r.bridgeSessionId === "string" ? r.bridgeSessionId : null
802
+ };
803
+ }
804
+ var liveCache = /* @__PURE__ */ new Map();
805
+ function claudeLive(dir) {
806
+ const hit = liveCache.get(dir);
807
+ if (hit) return hit;
808
+ const map = /* @__PURE__ */ new Map();
607
809
  let names = [];
608
810
  try {
609
- names = readdirSync2(dir).filter((n) => n.endsWith(".json"));
811
+ names = readdirSync2(path4.join(dir, "sessions")).filter((n) => n.endsWith(".json"));
610
812
  } catch {
611
- return out;
612
813
  }
613
814
  for (const n of names) {
614
- const id = n.slice(0, -5);
615
- if (!ID_RE.test(id)) continue;
616
- let j;
617
815
  try {
618
- j = JSON.parse(readFileSync2(path4.join(dir, n), "utf8"));
816
+ const e = registryEntry(JSON.parse(readFileSync2(path4.join(dir, "sessions", n), "utf8")));
817
+ if (e?.sessionId) map.set(e.sessionId, e);
619
818
  } 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
819
  }
627
- if (folders.length) out.push({ id, folders });
628
820
  }
629
- return out;
821
+ liveCache.set(dir, map);
822
+ return map;
630
823
  }
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));
824
+ function claudeLiveByPid(dir, pid) {
825
+ if (!pid) return null;
826
+ try {
827
+ return registryEntry(JSON.parse(readFileSync2(path4.join(dir, "sessions", `${pid}.json`), "utf8")));
828
+ } catch {
829
+ return null;
636
830
  }
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;
831
+ }
832
+ function claudePid(env = process.env) {
833
+ try {
834
+ const dirs = claudeDirs(env);
835
+ const has = (pid2) => dirs.some((d) => existsSync2(path4.join(d, "sessions", `${pid2}.json`)));
836
+ let pid = process.ppid;
837
+ if (has(pid)) return pid;
838
+ const parent = parentOf();
839
+ for (let hop = 0; hop < 6 && pid > 1; hop++) {
840
+ pid = parent(pid);
841
+ if (pid > 1 && has(pid)) return pid;
842
+ }
843
+ } catch {
844
+ }
845
+ return null;
846
+ }
847
+ function parentOf() {
848
+ if (process.platform === "linux") {
849
+ return (pid) => {
850
+ try {
851
+ const st = readFileSync2(`/proc/${pid}/stat`, "utf8");
852
+ return Number(st.slice(st.lastIndexOf(")") + 2).split(" ")[1]) || 0;
853
+ } catch {
854
+ return 0;
645
855
  }
856
+ };
857
+ }
858
+ const table = /* @__PURE__ */ new Map();
859
+ try {
860
+ const out = execFileSync2("ps", ["-axo", "pid=,ppid="], {
861
+ encoding: "utf8",
862
+ timeout: 2e3,
863
+ maxBuffer: 8 * 1024 * 1024,
864
+ stdio: ["ignore", "pipe", "ignore"]
865
+ });
866
+ for (const l of out.split("\n")) {
867
+ const m = /^\s*(\d+)\s+(\d+)/.exec(l);
868
+ if (m) table.set(Number(m[1]), Number(m[2]));
646
869
  }
647
- return best;
870
+ } catch {
871
+ }
872
+ return (pid) => table.get(pid) || 0;
648
873
  }
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) {
874
+ var EMAIL_RE = /^[^@\s]{1,64}@[^@\s]{1,189}$/;
875
+ function claudeLogin(dir, home = homeDir()) {
876
+ const files = [path4.join(dir, ".claude.json")];
877
+ if (path4.resolve(dir) === path4.join(home, ".claude")) files.unshift(path4.join(home, ".claude.json"));
878
+ for (const f of files) {
653
879
  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;
880
+ const a = JSON.parse(readFileSync2(f, "utf8")).oauthAccount;
881
+ if (typeof a?.accountUuid === "string" && a.accountUuid) {
882
+ const email = typeof a.emailAddress === "string" && EMAIL_RE.test(a.emailAddress) ? a.emailAddress : void 0;
883
+ return { id: a.accountUuid.slice(0, 128), ...email ? { email } : {} };
884
+ }
663
885
  } catch {
664
886
  }
665
887
  }
666
- return null;
888
+ return apiAccount(dir);
667
889
  }
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);
890
+ var ANTHROPIC_HOST = /(^|\.)anthropic\.com$/i;
891
+ function apiAccount(dir) {
892
+ const base = readJson(settingsIn(dir)).env?.ANTHROPIC_BASE_URL;
893
+ if (typeof base !== "string") return null;
894
+ let u;
768
895
  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
- }
896
+ u = new URL(base.trim());
774
897
  } catch {
898
+ return null;
775
899
  }
776
- return out.filter((d, i) => i === 0 || existsSync3(d));
900
+ if (u.protocol !== "https:" && u.protocol !== "http:" || !u.hostname || ANTHROPIC_HOST.test(u.hostname))
901
+ return null;
902
+ const origin = `${u.protocol}//${u.host}`;
903
+ return { id: `api-${createHash("sha256").update(origin).digest("hex").slice(0, 16)}`, name: u.host };
777
904
  }
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
- });
905
+ var accountCache = /* @__PURE__ */ new Map();
906
+ function claudeAccount(dir) {
907
+ const hit = accountCache.get(dir);
908
+ if (hit !== void 0) return hit;
909
+ const acct = claudeLogin(dir)?.id ?? null;
910
+ accountCache.set(dir, acct);
911
+ return acct;
912
+ }
913
+ var LINK_RE = /^https:\/\/claude\.ai\/code\/session_[A-Za-z0-9]+$/;
914
+ function transcriptLink(records) {
915
+ for (let i = records.length - 1; i >= 0; i--) {
916
+ const j = records[i];
917
+ if (j.type === "system" && j.subtype === "bridge_status" && typeof j.url === "string" && LINK_RE.test(j.url))
918
+ return j.url;
919
+ if (j.type === "bridge-session" && typeof j.bridgeSessionId === "string") {
920
+ const m = /^(?:cse|session)_([A-Za-z0-9]+)$/.exec(j.bridgeSessionId);
921
+ if (m) return `https://claude.ai/code/session_${m[1]}`;
991
922
  }
992
- return rows2;
993
923
  }
994
- };
995
- function prune2(o) {
996
- for (const k of Object.keys(o)) if (o[k] === void 0) delete o[k];
997
- return o;
924
+ return null;
998
925
  }
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);
926
+ function appTitle(text) {
927
+ const t = String(text).replace(/\s+/g, " ").trim();
928
+ return t.length > 50 ? `${t.slice(0, 50)}...` : t;
1002
929
  }
1003
- function pickSource(v) {
1004
- return typeof v === "string" && ["startup", "resume", "clear", "compact", "fork"].includes(v) ? v : "unknown";
930
+ function askText(j) {
931
+ if (!j || j.type !== "user" || j.isMeta || j.isSidechain) return null;
932
+ const origin = j.origin;
933
+ if (origin?.kind && origin.kind !== "human") return null;
934
+ const c = j.message?.content;
935
+ 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();
936
+ if (!text || isInjected(text)) return null;
937
+ return text;
1005
938
  }
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);
939
+ function firstAsk(records) {
940
+ for (const j of records) {
941
+ const t = askText(j);
942
+ if (t) return appTitle(t);
1015
943
  }
1016
- return void 0;
1017
- }
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)}` : "";
944
+ return null;
1023
945
  }
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
- };
946
+ function scanTitles(file, from, found) {
947
+ const out = { custom: found.custom ?? null, ai: found.ai ?? null, upto: from };
948
+ let size = 0;
949
+ try {
950
+ size = statSync2(file).size;
951
+ } catch {
952
+ return out;
953
+ }
954
+ if (size < from) {
955
+ out.upto = 0;
956
+ out.custom = out.ai = null;
957
+ from = 0;
958
+ }
959
+ let pos = 0;
960
+ scanLines(
961
+ file,
962
+ (l) => {
963
+ pos += Buffer.byteLength(l, "utf8") + 1;
964
+ if (pos <= from || l.indexOf('-title"') < 0) return null;
965
+ try {
966
+ const j = JSON.parse(l);
967
+ if (j.type === "custom-title" && j.customTitle) out.custom = j.customTitle;
968
+ if (j.type === "ai-title" && j.aiTitle) out.ai = j.aiTitle;
969
+ } catch {
970
+ }
971
+ return null;
972
+ },
973
+ 512 * 1024 * 1024
974
+ );
975
+ out.upto = size;
976
+ return out;
1040
977
  }
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 = [];
978
+ function claudeFacts(file, sessionId, hints, state) {
979
+ const { head, tail } = ends(file, 64 * 1024, 256 * 1024);
980
+ const H = parseLines(head);
981
+ const T = parseLines(tail);
982
+ const out = {};
983
+ const id = sessionId || path4.basename(file, ".jsonl");
984
+ const mem = state.get(NAME, id);
985
+ let custom = null;
986
+ let ai = null;
987
+ for (const j of H.concat(T)) {
988
+ if (j.type === "custom-title" && typeof j.customTitle === "string") custom = j.customTitle;
989
+ if (j.type === "ai-title" && typeof j.aiTitle === "string") ai = j.aiTitle;
990
+ }
991
+ let size = 0;
1047
992
  try {
1048
- names = readdirSync3(path5.join(dir, "sessions")).filter((n) => n.endsWith(".json"));
993
+ size = statSync2(file).size;
1049
994
  } catch {
1050
995
  }
1051
- for (const n of names) {
1052
- try {
1053
- const e = registryEntry(JSON.parse(readFileSync3(path5.join(dir, "sessions", n), "utf8")));
1054
- if (e?.sessionId) map.set(e.sessionId, e);
1055
- } catch {
996
+ if (!custom && !ai && size > 64 * 1024 + 256 * 1024) {
997
+ const s = scanTitles(file, Number(mem.scan_upto) || 0, {
998
+ custom: mem.scan_custom,
999
+ ai: mem.scan_ai
1000
+ });
1001
+ custom = s.custom;
1002
+ ai = s.ai;
1003
+ state.save(NAME, id, { scan_upto: s.upto, scan_custom: s.custom, scan_ai: s.ai });
1004
+ }
1005
+ const dir = configDirOf(file);
1006
+ const live = claudeLive(dir).get(id) ?? claudeLiveByPid(dir, hints.claude_pid);
1007
+ if (live?.name) state.save(NAME, id, { name: live.name, name_source: live.name_source });
1008
+ const bridge = live?.bridge && /^session_[A-Za-z0-9]+$/.test(live.bridge) ? `https://claude.ai/code/${live.bridge}` : null;
1009
+ const url = bridge ?? transcriptLink(T) ?? transcriptLink(H) ?? (typeof mem.url === "string" ? mem.url : null);
1010
+ if (url) {
1011
+ out.url = url;
1012
+ state.save(NAME, id, { url });
1013
+ }
1014
+ const name = live?.name ? { title: live.name, source: live.name_source } : typeof mem.name === "string" ? { title: mem.name, source: mem.name_source } : null;
1015
+ const pick = name && name.source === "custom" && name || custom && { title: custom, source: "custom" } || name || ai && { title: ai, source: "harness" };
1016
+ let ask2 = typeof mem.first_ask === "string" ? mem.first_ask : firstAsk(H);
1017
+ if (!ask2 && size > 64 * 1024 && !mem.first_ask_none) {
1018
+ ask2 = scanLines(file, (l) => {
1019
+ try {
1020
+ const t = askText(JSON.parse(l));
1021
+ return t ? appTitle(t) : null;
1022
+ } catch {
1023
+ return null;
1024
+ }
1025
+ });
1026
+ if (!ask2 && size > 16 * 1024 * 1024) state.save(NAME, id, { first_ask_none: true });
1027
+ }
1028
+ if (ask2) {
1029
+ out.first_ask = ask2;
1030
+ if (!mem.first_ask) state.save(NAME, id, { first_ask: ask2 });
1031
+ }
1032
+ if (pick) {
1033
+ out.title = String(pick.title).slice(0, 120);
1034
+ out.title_source = pick.source;
1035
+ } else if (ask2) {
1036
+ out.title = ask2;
1037
+ out.title_source = "first-ask";
1038
+ }
1039
+ const acct = claudeAccount(dir);
1040
+ if (acct) out.account_id = acct;
1041
+ for (let i = T.length - 1; i >= 0; i--) {
1042
+ const m = T[i].message?.model;
1043
+ if (T[i]?.type === "assistant" && m && m !== "<synthetic>") {
1044
+ out.model = m;
1045
+ break;
1056
1046
  }
1057
1047
  }
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;
1048
+ const meta = [...T].reverse().find((j) => j.version) ?? H.find((j) => j.version);
1049
+ if (meta?.version) out.harness_version = String(meta.version);
1050
+ const times = H.concat(T).map((j) => Date.parse(String(j.timestamp ?? ""))).filter((n) => n > 0);
1051
+ if (times.length) {
1052
+ out.started_at = Math.min(...times);
1053
+ out.last_at = Math.max(...times);
1067
1054
  }
1068
- }
1069
- function claudePid(env = process.env) {
1070
- 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
- }
1080
- } catch {
1055
+ const withCwd = H.find((j) => typeof j.cwd === "string");
1056
+ if (withCwd) {
1057
+ out.cwd = withCwd.cwd;
1058
+ if (typeof withCwd.gitBranch === "string" && withCwd.gitBranch !== "HEAD") out.branch = withCwd.gitBranch;
1081
1059
  }
1082
- return null;
1060
+ return out;
1083
1061
  }
1084
- function parentOf() {
1085
- if (process.platform === "linux") {
1086
- return (pid) => {
1062
+ var REMOTE_RETRY_MS = 6e4;
1063
+ var REMOTE_REFRESH_MS = 30 * 6e4;
1064
+ var CLAUDE_API = "https://api.anthropic.com";
1065
+ var KEYCHAIN = "Claude Code-credentials";
1066
+ var REMOTE_TIMEOUT_MS = 3e3;
1067
+ var KEYCHAIN_TIMEOUT_MS = 2e3;
1068
+ var REMOTE_MAX_BYTES = 1 << 20;
1069
+ var cleanName = (s) => s.replace(/[\p{Cc}\u061C\u200E\u200F\u202A-\u202E\u2066-\u2069]/gu, " ").replace(/\s+/g, " ").trim();
1070
+ async function claudeRemoteFacts(id, local, transcript, state, env, now = Date.now()) {
1071
+ if (!transcript || !local.url || !LINK_RE.test(local.url) || local.title_source === "custom") return {};
1072
+ const mem = state.get(NAME, id);
1073
+ let title = typeof mem.remote_title === "string" ? mem.remote_title : null;
1074
+ if (now - (Number(mem.remote_at) || 0) >= (title ? REMOTE_REFRESH_MS : REMOTE_RETRY_MS)) {
1075
+ state.save(NAME, id, { remote_at: now });
1076
+ const token = claudeAiToken(configDirOf(transcript), env, now);
1077
+ const got = token ? await claudeAiTitle(local.url.slice(local.url.lastIndexOf("/") + 1), token) : void 0;
1078
+ if (got !== void 0) {
1079
+ title = got?.slice(0, 200) ?? null;
1080
+ state.save(NAME, id, { remote_title: title });
1081
+ }
1082
+ }
1083
+ return title ? { title: title.slice(0, 120), title_source: "harness" } : {};
1084
+ }
1085
+ function keychainServices(dir, env) {
1086
+ const named = (p) => `${KEYCHAIN}-${createHash("sha256").update(p.normalize("NFC")).digest("hex").slice(0, 8)}`;
1087
+ const out = /* @__PURE__ */ new Set();
1088
+ if (env.CLAUDE_CONFIG_DIR && path4.resolve(env.CLAUDE_CONFIG_DIR) === dir) out.add(named(env.CLAUDE_CONFIG_DIR));
1089
+ if (dir === path4.join(env.HOME || homeDir(), ".claude")) out.add(KEYCHAIN);
1090
+ out.add(named(dir));
1091
+ return [...out];
1092
+ }
1093
+ var tokenCache = /* @__PURE__ */ new Map();
1094
+ function claudeAiToken(dir, env, now) {
1095
+ const hit = tokenCache.get(dir);
1096
+ if (hit && hit.expires > now + 6e4) return hit.token;
1097
+ const raw = [];
1098
+ if (process.platform === "darwin")
1099
+ for (const service of keychainServices(dir, env)) {
1087
1100
  try {
1088
- const st = readFileSync3(`/proc/${pid}/stat`, "utf8");
1089
- return Number(st.slice(st.lastIndexOf(")") + 2).split(" ")[1]) || 0;
1101
+ const user = env.USER || userInfo().username;
1102
+ const account = /^[\w.-]+$/.test(user) ? user : "claude-code-user";
1103
+ raw.push(
1104
+ execFileSync2("security", ["find-generic-password", "-a", account, "-w", "-s", service], {
1105
+ encoding: "utf8",
1106
+ timeout: KEYCHAIN_TIMEOUT_MS,
1107
+ stdio: ["ignore", "pipe", "ignore"]
1108
+ })
1109
+ );
1110
+ break;
1090
1111
  } catch {
1091
- return 0;
1092
1112
  }
1093
- };
1094
- }
1095
- const table = /* @__PURE__ */ new Map();
1096
- 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
1113
  }
1114
+ try {
1115
+ raw.push(readFileSync2(path4.join(dir, ".credentials.json"), "utf8"));
1107
1116
  } catch {
1108
1117
  }
1109
- return (pid) => table.get(pid) || 0;
1110
- }
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) {
1118
+ for (const r of raw) {
1116
1119
  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
- }
1120
+ const t = r.trim();
1121
+ const o = JSON.parse(/^(?:[0-9a-f]{2})+$/i.test(t) ? Buffer.from(t, "hex").toString("utf8") : t).claudeAiOauth;
1122
+ if (typeof o?.accessToken !== "string" || !o.accessToken) continue;
1123
+ const expires = typeof o.expiresAt === "number" ? o.expiresAt : Number.POSITIVE_INFINITY;
1124
+ if (expires <= now + 6e4) continue;
1125
+ tokenCache.set(dir, { token: o.accessToken, expires });
1126
+ return o.accessToken;
1122
1127
  } catch {
1123
1128
  }
1124
1129
  }
1125
- return apiAccount(dir);
1130
+ return null;
1126
1131
  }
1127
- var ANTHROPIC_HOST = /(^|\.)anthropic\.com$/i;
1128
- function apiAccount(dir) {
1129
- let base;
1132
+ async function claudeAiTitle(bridge, token) {
1130
1133
  try {
1131
- base = JSON.parse(readFileSync3(settingsIn(dir), "utf8")).env?.ANTHROPIC_BASE_URL;
1134
+ const r = await fetch(`${CLAUDE_API}/v1/code/sessions/${bridge}`, {
1135
+ headers: { Authorization: `Bearer ${token}`, "anthropic-version": "2023-06-01", "User-Agent": "sessionpipe" },
1136
+ redirect: "error",
1137
+ signal: AbortSignal.timeout(REMOTE_TIMEOUT_MS)
1138
+ });
1139
+ if (r.status !== 200 || Number(r.headers.get("content-length")) > REMOTE_MAX_BYTES) return void 0;
1140
+ const text = await r.text();
1141
+ if (text.length > REMOTE_MAX_BYTES) return void 0;
1142
+ const j = JSON.parse(text);
1143
+ const s = j?.response_shape ?? j?.session ?? j;
1144
+ const bare = (x) => x.replace(/^(?:session|cse)_/, "");
1145
+ if (typeof s?.id !== "string" || bare(s.id) !== bare(bridge)) return void 0;
1146
+ return typeof s.title === "string" && cleanName(s.title) || null;
1132
1147
  } catch {
1133
- return null;
1148
+ return void 0;
1134
1149
  }
1135
- if (typeof base !== "string") return null;
1136
- let u;
1150
+ }
1151
+
1152
+ // ../core/src/adapters/headless.ts
1153
+ import { accessSync, constants, statSync as statSync3 } from "node:fs";
1154
+ import path5 from "node:path";
1155
+ var JOB_TIMEOUT_MS = 30 * 6e4;
1156
+ var OUT_CAP = 1024 * 1024;
1157
+ function isExe(p) {
1137
1158
  try {
1138
- u = new URL(base.trim());
1159
+ if (!statSync3(p).isFile()) return false;
1160
+ accessSync(p, constants.X_OK);
1161
+ return true;
1139
1162
  } catch {
1140
- return null;
1163
+ return false;
1141
1164
  }
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
1165
  }
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;
1166
+ function findBinary(o) {
1167
+ if (o.override && isExe(o.override)) return o.override;
1168
+ for (const d of String(o.env?.PATH || "").split(path5.delimiter)) {
1169
+ if (!d || !path5.isAbsolute(d)) continue;
1170
+ for (const n of o.names) if (isExe(path5.join(d, n))) return path5.join(d, n);
1171
+ }
1172
+ for (const d of o.dirs) for (const n of o.names) if (isExe(path5.join(d, n))) return path5.join(d, n);
1173
+ return null;
1154
1174
  }
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]}`;
1175
+
1176
+ // ../core/src/adapters/antigravity.ts
1177
+ import {
1178
+ existsSync as existsSync3,
1179
+ mkdirSync as mkdirSync2,
1180
+ readdirSync as readdirSync3,
1181
+ readFileSync as readFileSync3,
1182
+ realpathSync,
1183
+ statSync as statSync4,
1184
+ unlinkSync as unlinkSync2,
1185
+ writeFileSync as writeFileSync3
1186
+ } from "node:fs";
1187
+ import path6 from "node:path";
1188
+ var NAME2 = "antigravity";
1189
+ var HOOK_NAME = "sessionpipe";
1190
+ var AG_EVENTS = ["PreInvocation", "PostInvocation", "PostToolUse", "Stop"];
1191
+ 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';
1192
+ var antigravityAnswer = (event) => event === "PreToolUse" ? AG_STALE_PRETOOL : "{}\n";
1193
+ var gemini = (env) => env.GEMINI_CLI_HOME || path6.join(homeDir(), ".gemini");
1194
+ var hooksFile = (env) => path6.join(gemini(env), "config", "hooks.json");
1195
+ var projectsDir = (env) => path6.join(gemini(env), "config", "projects");
1196
+ var dataDirs = (env) => ["antigravity", "antigravity-cli", "antigravity-ide"].map((d) => path6.join(gemini(env), d));
1197
+ var ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
1198
+ var validTool2 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
1199
+ function entry(cmd) {
1200
+ const handler = (e) => ({ type: "command", command: cmd([NAME2, e]).command, timeout: 5 });
1201
+ const out = { enabled: true };
1202
+ for (const e of AG_EVENTS) out[e] = /Tool/.test(e) ? [{ matcher: "*", hooks: [handler(e)] }] : [handler(e)];
1203
+ return out;
1204
+ }
1205
+ var antigravity = {
1206
+ name: NAME2,
1207
+ events: AG_EVENTS,
1208
+ detect: (env = process.env) => existsSync3(path6.join(gemini(env), "config")) || dataDirs(env).some((d) => existsSync3(d)),
1209
+ configFiles: (env = process.env) => [hooksFile(env)],
1210
+ install(cmd, env = process.env) {
1211
+ const file = hooksFile(env);
1212
+ const f = readJsonFile(file);
1213
+ if (!f.ok) return [{ file, changed: false, skipped: true, note: `${f.reason}; not touched` }];
1214
+ const hooks = f.value;
1215
+ const e = entry(cmd);
1216
+ if (JSON.stringify(hooks[HOOK_NAME]) === JSON.stringify(e)) return [{ file, changed: false }];
1217
+ hooks[HOOK_NAME] = e;
1218
+ mkdirSync2(path6.dirname(file), { recursive: true });
1219
+ writeFileSync3(file, formatJson(hooks, f));
1220
+ return [
1221
+ {
1222
+ file,
1223
+ changed: true,
1224
+ created: !f.existed,
1225
+ note: "hooks.json is read when a conversation starts; open ones report after a restart"
1226
+ }
1227
+ ];
1228
+ },
1229
+ uninstall(env = process.env, opts = {}) {
1230
+ const file = hooksFile(env);
1231
+ if (!existsSync3(file)) return [{ file, changed: false }];
1232
+ const f = readJsonFile(file);
1233
+ if (!f.ok) return [{ file, changed: false, skipped: true, note: `${f.reason}; not touched` }];
1234
+ const hooks = f.value;
1235
+ if (!(HOOK_NAME in hooks)) return [{ file, changed: false }];
1236
+ delete hooks[HOOK_NAME];
1237
+ if (opts.created?.includes(file) && !Object.keys(hooks).length) {
1238
+ unlinkSync2(file);
1239
+ return [{ file, changed: true, note: "removed (sessionpipe created it)" }];
1240
+ }
1241
+ writeFileSync3(file, formatJson(hooks, f));
1242
+ return [{ file, changed: true }];
1243
+ },
1244
+ installed(cmd, env = process.env) {
1245
+ const file = hooksFile(env);
1246
+ const f = readJsonFile(file);
1247
+ const have = f.ok ? f.value[HOOK_NAME] : void 0;
1248
+ return [
1249
+ {
1250
+ file,
1251
+ state: !have ? "missing" : JSON.stringify(have) === JSON.stringify(entry(cmd)) ? "current" : "stale"
1252
+ }
1253
+ ];
1254
+ },
1255
+ fromHook(input) {
1256
+ const event = input.argv[1] ?? "";
1257
+ let s = {};
1258
+ try {
1259
+ const j = input.stdin ? JSON.parse(input.stdin) : {};
1260
+ if (j && typeof j === "object" && !Array.isArray(j)) s = j;
1261
+ } catch {
1262
+ }
1263
+ const id = s.conversationId ?? s.session_id ?? s.sessionId;
1264
+ const stdout = antigravityAnswer(event);
1265
+ if (!id) return { session: { id: "" }, events: [], stdout };
1266
+ const workspaces = agWorkspaces(s.workspacePaths);
1267
+ const session = { id: String(id), cwd: workspaces[0] ?? input.cwd };
1268
+ if (typeof s.modelName === "string" && s.modelName !== "auto") session.model = s.modelName;
1269
+ const toolCall = s.toolCall;
1270
+ const tool = validTool2(toolCall?.name) ? toolCall?.name : validTool2(s.toolName) ? s.toolName : void 0;
1271
+ const ev = (type, data) => ({ type, data: prune2(data), harnessEvent: event });
1272
+ const events = [];
1273
+ switch (event) {
1274
+ case "PreInvocation":
1275
+ if (Number(s.invocationNum) === 0 || s.invocationNum === void 0) {
1276
+ events.push(ev("session.started", { source: "unknown" }));
1277
+ events.push(ev("turn.started", {}));
1278
+ } else events.push(ev("session.heartbeat", {}));
1279
+ break;
1280
+ case "PostInvocation":
1281
+ break;
1282
+ case "PreToolUse":
1283
+ if (tool) events.push(ev("tool.started", { tool, input: toolCall?.args }));
1284
+ break;
1285
+ case "PostToolUse":
1286
+ if (!tool) events.push(ev("session.heartbeat", {}));
1287
+ else
1288
+ events.push(
1289
+ ev("tool.ended", {
1290
+ tool,
1291
+ ok: !s.error,
1292
+ error: typeof s.error === "string" ? s.error.slice(0, 2e3) : void 0,
1293
+ input: toolCall?.args
1294
+ })
1295
+ );
1296
+ break;
1297
+ case "Stop":
1298
+ events.push(ev("turn.ended", { reason: s.error ? "error" : "stop" }));
1299
+ break;
1300
+ default:
1301
+ return { session, events: [], stdout };
1302
+ }
1303
+ const r = { session, events, stdout, hints: { workspaces } };
1304
+ if (typeof s.transcriptPath === "string") r.transcript = s.transcriptPath;
1305
+ return r;
1306
+ },
1307
+ readTranscript(file, fromLine) {
1308
+ let text = "";
1309
+ try {
1310
+ text = readFileSync3(file, "utf8");
1311
+ } catch {
1312
+ return { turns: [], line: fromLine };
1313
+ }
1314
+ return readAntigravity(text.split("\n"), fromLine);
1315
+ },
1316
+ facts(session, transcript, hints) {
1317
+ if (!transcript) return {};
1318
+ return antigravityFacts(transcript, {
1319
+ conversation: session.id,
1320
+ workspaces: hints.workspaces ?? (session.cwd ? [session.cwd] : []),
1321
+ env: process.env
1322
+ });
1323
+ },
1324
+ backfill(sinceMs, env = process.env) {
1325
+ const rows2 = [];
1326
+ for (const data of dataDirs(env)) {
1327
+ let ids = [];
1328
+ try {
1329
+ ids = readdirSync3(path6.join(data, "brain")).filter((n) => /^[0-9a-f-]{36}$/i.test(n));
1330
+ } catch {
1331
+ }
1332
+ for (const id of ids) {
1333
+ const f = path6.join(data, "brain", id, ".system_generated", "logs", "transcript.jsonl");
1334
+ try {
1335
+ if (statSync4(f).mtimeMs < sinceMs) continue;
1336
+ } catch {
1337
+ continue;
1338
+ }
1339
+ const facts = antigravityFacts(f, { conversation: id, workspaces: [], env });
1340
+ if (!facts.started_at || !facts.last_at) continue;
1341
+ const { started_at, last_at, first_ask: _fa, harness_version: _hv, ...rest } = facts;
1342
+ rows2.push({ session: { id, ...rest }, started_at, last_at });
1343
+ }
1164
1344
  }
1345
+ return rows2;
1165
1346
  }
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;
1347
+ };
1348
+ function prune2(o) {
1349
+ for (const k of Object.keys(o)) if (o[k] === void 0) delete o[k];
1350
+ return o;
1180
1351
  }
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;
1352
+ function agPath(p) {
1353
+ if (typeof p !== "string" || !p) return null;
1354
+ let s = p;
1355
+ if (/^file:/i.test(s)) {
1356
+ try {
1357
+ s = decodeURIComponent(new URL(s).pathname);
1358
+ } catch {
1359
+ return null;
1360
+ }
1361
+ if (process.platform === "win32" && /^\/[A-Za-z]:/.test(s)) s = s.slice(1);
1362
+ } else if (/^[a-z][a-z0-9+.-]*:\/\//i.test(s)) return null;
1363
+ if (!path6.isAbsolute(s)) return null;
1364
+ return path6.resolve(s);
1187
1365
  }
1188
- function scanTitles(file, from, found) {
1189
- const out = { custom: found.custom ?? null, ai: found.ai ?? null, upto: from };
1190
- let size = 0;
1366
+ var agWorkspaces = (paths) => (Array.isArray(paths) ? paths : []).map(agPath).filter((p) => !!p).filter((p, i, a) => a.indexOf(p) === i);
1367
+ function agVariants(p) {
1368
+ const out = [p];
1191
1369
  try {
1192
- size = statSync3(file).size;
1370
+ const r = realpathSync.native(p);
1371
+ if (r !== p) out.push(r);
1193
1372
  } catch {
1194
- return out;
1195
1373
  }
1196
- if (size < from) {
1197
- out.upto = 0;
1198
- out.custom = out.ai = null;
1199
- from = 0;
1200
- }
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
1374
  return out;
1219
1375
  }
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;
1376
+ function agProjects(dir) {
1377
+ const out = [];
1378
+ let names = [];
1234
1379
  try {
1235
- size = statSync3(file).size;
1380
+ names = readdirSync3(dir).filter((n) => n.endsWith(".json"));
1236
1381
  } catch {
1382
+ return out;
1237
1383
  }
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 });
1384
+ for (const n of names) {
1385
+ const id = n.slice(0, -5);
1386
+ if (!ID_RE.test(id)) continue;
1387
+ let j;
1388
+ try {
1389
+ j = JSON.parse(readFileSync3(path6.join(dir, n), "utf8"));
1390
+ } catch {
1391
+ continue;
1392
+ }
1393
+ const folders = [];
1394
+ for (const r of j?.projectResources?.resources ?? []) {
1395
+ const f = agPath(r?.folderUri ?? r?.gitFolder?.folderUri);
1396
+ if (f) folders.push(...agVariants(f));
1397
+ }
1398
+ if (folders.length) out.push({ id, folders });
1246
1399
  }
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 });
1400
+ return out;
1401
+ }
1402
+ function agMatchProject(projects, workspaces) {
1403
+ const ws = [];
1404
+ for (const w of workspaces) {
1405
+ const p = agPath(w);
1406
+ if (p) ws.push(...agVariants(p));
1255
1407
  }
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;
1408
+ let best = null;
1409
+ let bestLen = -1;
1410
+ for (const pr of projects)
1411
+ for (const f of pr.folders) {
1412
+ const within = ws.some((w) => w === f || w.startsWith(f.endsWith(path6.sep) ? f : f + path6.sep));
1413
+ if (within && f.length > bestLen) {
1414
+ best = pr.id;
1415
+ bestLen = f.length;
1266
1416
  }
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 });
1417
+ }
1418
+ return best;
1419
+ }
1420
+ function antigravityRemoteUrl(transcript, opts) {
1421
+ const i = transcript.indexOf(`${path6.sep}brain${path6.sep}`);
1422
+ const dirs = (i > 0 ? [transcript.slice(0, i)] : []).concat(dataDirs(opts.env));
1423
+ for (const d of dirs) {
1424
+ try {
1425
+ const m = /installation_uuid:\s*"([0-9a-f-]{36})"/i.exec(
1426
+ readFileSync3(path6.join(d, "antigravity_state.pbtxt"), "utf8")
1427
+ );
1428
+ if (!m) continue;
1429
+ const base = `https://antigravity.google.com/r/${m[1]}-v2`;
1430
+ const conv = opts.conversation ?? "";
1431
+ if (!ID_RE.test(conv) || !opts.workspaces?.length) return base;
1432
+ const project = agMatchProject(agProjects(opts.projectsDir ?? projectsDir(opts.env)), opts.workspaces);
1433
+ return project ? `${base}?p=${encodeURIComponent(`c/${conv}?section=${project}`)}` : base;
1434
+ } catch {
1435
+ }
1273
1436
  }
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;
1437
+ return null;
1438
+ }
1439
+ function antigravityFacts(file, opts) {
1440
+ const { head, tail } = ends(file, 128 * 1024, 64 * 1024);
1441
+ const H = parseLines(head);
1442
+ const T = parseLines(tail);
1443
+ const out = {};
1444
+ const url = antigravityRemoteUrl(file, opts);
1445
+ if (url) out.url = url;
1446
+ for (const j of H) {
1447
+ if (j.type !== "USER_INPUT" || typeof j.content !== "string") continue;
1448
+ const m = /<USER_REQUEST>([\s\S]*?)(?:<\/USER_REQUEST>|$)/.exec(j.content);
1449
+ const text = (m ? m[1] : j.content).trim();
1450
+ const line = (text.split("\n").find((l) => l.trim()) ?? "").trim();
1451
+ if (!line || /^<[a-z_-]+>/i.test(line)) continue;
1452
+ out.title = line.length > 80 ? `${line.slice(0, 80).replace(/\s+\S*$/, "")}\u2026` : line;
1279
1453
  out.title_source = "first-ask";
1454
+ break;
1280
1455
  }
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);
1456
+ const times = H.concat(T).map((j) => Date.parse(String(j.created_at ?? j.timestamp ?? ""))).filter((n) => n > 0);
1293
1457
  if (times.length) {
1294
1458
  out.started_at = Math.min(...times);
1295
1459
  out.last_at = Math.max(...times);
1296
1460
  }
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
1461
  return out;
1303
1462
  }
1304
1463
 
@@ -1311,15 +1470,24 @@ var NONCE_TTL_MS = 25 * 36e5;
1311
1470
  var MODEL_RE = /^[A-Za-z0-9][A-Za-z0-9._:/@[\]-]*$/;
1312
1471
  var START_PERMISSIONS = ["read", "safe", "edits", "auto"];
1313
1472
  var levelRank = (l) => START_PERMISSIONS.indexOf(l);
1473
+ var FIELDS = {
1474
+ prompt: ["text", "files"],
1475
+ "permission.answer": ["for", "decision"],
1476
+ cancel: [],
1477
+ start: ["text", "cwd", "files", "permission", "model"],
1478
+ "key.add": ["key", "name"],
1479
+ pool: ["pool_action", "pool_account", "pool_order", "pool_code"]
1480
+ };
1481
+ var POOL_FIELDS = FIELDS.pool;
1314
1482
  var enc = new TextEncoder();
1315
1483
  var dec = new TextDecoder();
1316
1484
 
1317
1485
  // ../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";
1486
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
1487
+ import path7 from "node:path";
1320
1488
 
1321
1489
  // ../core/src/adapters/codex-config.ts
1322
- var NOTIFY_LINE = /^[ \t]*notify[ \t]*=[ \t]*(\[.*\])[ \t]*(?:#.*)?$/m;
1490
+ var NOTIFY_LINE = /^[ \t]*notify[ \t]*=[ \t]*(\[.*\])[ \t]*(?:#.*)?(?=\r?$)/m;
1323
1491
  var NOTIFY_KEY = /^[ \t]*notify[ \t]*=/m;
1324
1492
  var OURS2 = /(?:hook\.js|sessionpipe-hook)",\s*"codex",\s*"notify"/;
1325
1493
  function scan(line, st) {
@@ -1409,10 +1577,11 @@ function dropLine(text, index, length) {
1409
1577
  }
1410
1578
  function insertTopLevel(text, line) {
1411
1579
  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}`;
1580
+ const body = head.replace(/\s*$/, "");
1581
+ const eol = (body.includes("\n") ? body : text).includes("\r\n") ? "\r\n" : "\n";
1582
+ const top = head.trim() ? head.replace(/\s*$/, eol) : "";
1583
+ if (!tail) return top + line + eol;
1584
+ return `${top + line}${eol}${eol}${tail}`;
1416
1585
  }
1417
1586
  function withNotify(text, line) {
1418
1587
  const n = readNotify(text);
@@ -1461,10 +1630,10 @@ var CODEX_EVENTS = [
1461
1630
  ];
1462
1631
  var OPTS2 = { matcherFor: (e) => /Tool|Permission/.test(e) ? "" : void 0, timeout: 5 };
1463
1632
  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");
1633
+ var codexHome = (env) => env.CODEX_HOME || path7.join(homeDir(), ".codex");
1634
+ var hooksFile2 = (env) => path7.join(codexHome(env), "hooks.json");
1635
+ var configFile2 = (env) => path7.join(codexHome(env), "config.toml");
1636
+ var sessionsDir = (env) => path7.join(codexHome(env), "sessions");
1468
1637
  var validTool3 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
1469
1638
  var endReason2 = (r) => r === "clear" || r === "logout" || r === "exit" ? r : "other";
1470
1639
  function installNotify(cmd, env) {
@@ -1478,8 +1647,8 @@ function installNotify(cmd, env) {
1478
1647
  const line = `notify = ${JSON.stringify(argv)}`;
1479
1648
  const n = readNotify(text);
1480
1649
  const write = (next) => {
1481
- mkdirSync3(path6.dirname(file), { recursive: true });
1482
- writeFileSync3(file, next);
1650
+ mkdirSync3(path7.dirname(file), { recursive: true });
1651
+ writeFileSync4(file, next);
1483
1652
  };
1484
1653
  if (n && !n.other) {
1485
1654
  if (!n.misplaced && JSON.stringify(n.argv) === JSON.stringify(argv))
@@ -1524,12 +1693,12 @@ function uninstallNotify(env) {
1524
1693
  const n = readNotify(text);
1525
1694
  if (!n) return { file, changed: false };
1526
1695
  if (!n.other) {
1527
- writeFileSync3(file, withoutNotify(text));
1696
+ writeFileSync4(file, withoutNotify(text));
1528
1697
  return { file, changed: true };
1529
1698
  }
1530
1699
  const prev = previousNotify(n.argv);
1531
1700
  if (isComputerUse(n.argv) && prev && OURS2.test(JSON.stringify(prev))) {
1532
- writeFileSync3(file, replaceNotify(text, n, `notify = ${stripPreviousRaw(n.raw)}`));
1701
+ writeFileSync4(file, replaceNotify(text, n, `notify = ${stripPreviousRaw(n.raw)}`));
1533
1702
  return { file, changed: true };
1534
1703
  }
1535
1704
  return { file, changed: false };
@@ -1545,7 +1714,7 @@ function findRollout(threadId, env = process.env) {
1545
1714
  return;
1546
1715
  }
1547
1716
  for (const n of names.sort().reverse()) {
1548
- const p = path6.join(dir, n);
1717
+ const p = path7.join(dir, n);
1549
1718
  if (n.endsWith(`-${threadId}.jsonl`)) {
1550
1719
  best = p;
1551
1720
  return;
@@ -1730,7 +1899,7 @@ var codex = {
1730
1899
  backfill(sinceMs, env = process.env) {
1731
1900
  const rows2 = [];
1732
1901
  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));
1902
+ const m = /([0-9a-f]{8}-[0-9a-f-]{27,})\.jsonl$/i.exec(path7.basename(f));
1734
1903
  if (!m?.[1]) continue;
1735
1904
  if (codexSubagent(rolloutMeta(f))) continue;
1736
1905
  const facts = codexFacts(f);
@@ -1739,12 +1908,40 @@ var codex = {
1739
1908
  rows2.push({ session: { id: m[1], ...rest }, started_at, last_at });
1740
1909
  }
1741
1910
  return rows2;
1742
- }
1911
+ },
1912
+ limits: (transcript) => codexLimits(transcript)
1743
1913
  };
1914
+ var epochIso2 = (t) => typeof t === "number" && t > 0 && t < 1e11 ? new Date(t * 1e3).toISOString() : void 0;
1744
1915
  function prune3(o) {
1745
1916
  for (const k of Object.keys(o)) if (o[k] === void 0) delete o[k];
1746
1917
  return o;
1747
1918
  }
1919
+ function codexLimits(file) {
1920
+ const { tail } = ends(file, 0, 256 * 1024);
1921
+ for (const r of parseLines(tail).reverse()) {
1922
+ const p = r.payload;
1923
+ if (r.type !== "event_msg" || p?.type !== "token_count") continue;
1924
+ if (!p.rate_limits || typeof p.rate_limits !== "object") continue;
1925
+ const rl = p.rate_limits;
1926
+ if (rl.limit_id != null && rl.limit_id !== "codex") continue;
1927
+ const windows = [];
1928
+ for (const id of ["primary", "secondary"]) {
1929
+ const w = rl[id];
1930
+ if (typeof w?.used_percent !== "number" || w.used_percent < 0) continue;
1931
+ windows.push(
1932
+ prune3({
1933
+ id,
1934
+ used_percent: w.used_percent,
1935
+ window_minutes: Number.isInteger(w.window_minutes) && Number(w.window_minutes) > 0 ? Number(w.window_minutes) : void 0,
1936
+ resets_at: epochIso2(w.resets_at)
1937
+ })
1938
+ );
1939
+ }
1940
+ if (!windows.length) return null;
1941
+ return prune3({ windows, plan: typeof rl.plan_type === "string" ? rl.plan_type.slice(0, 64) : void 0 });
1942
+ }
1943
+ return null;
1944
+ }
1748
1945
  function codexFacts(file) {
1749
1946
  const { head, tail } = ends(file, 128 * 1024, 64 * 1024);
1750
1947
  let H = parseLines(head);
@@ -1789,7 +1986,7 @@ function codexFacts(file) {
1789
1986
  // ../core/src/adapters/devin.ts
1790
1987
  import { createHash as createHash2 } from "node:crypto";
1791
1988
  import { existsSync as existsSync6, readlinkSync } from "node:fs";
1792
- import path7 from "node:path";
1989
+ import path8 from "node:path";
1793
1990
 
1794
1991
  // ../core/src/readers/sqlite.ts
1795
1992
  import { execFileSync as execFileSync3 } from "node:child_process";
@@ -1973,11 +2170,11 @@ function foreignEvent(file) {
1973
2170
  return Object.keys(hooks).find((k) => !ok.includes(k));
1974
2171
  }
1975
2172
  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");
2173
+ var appData = (env) => env.APPDATA || path8.join(homeOf(env), "AppData", "Roaming");
2174
+ 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");
2175
+ var configFile3 = (env) => path8.join(devinConfigDir(env), "config.json");
2176
+ 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");
2177
+ var sessionsDb = (env) => path8.join(devinDataDir(env), "sessions.db");
1981
2178
  var SESSION_ID = /^[A-Za-z0-9._:-]{1,128}$/;
1982
2179
  var validTool4 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
1983
2180
  var endReason3 = (r) => r === "clear" || r === "logout" || r === "exit" ? r : "other";
@@ -2149,7 +2346,7 @@ var BACKFILL_SQL = `SELECT s.id, ${SESSION_FIELDS} FROM sessions s
2149
2346
  function sessionFacts(row) {
2150
2347
  const out = {};
2151
2348
  if (typeof row.model === "string" && row.model) out.model = row.model;
2152
- if (typeof row.working_directory === "string" && path7.isAbsolute(row.working_directory))
2349
+ if (typeof row.working_directory === "string" && path8.isAbsolute(row.working_directory))
2153
2350
  out.cwd = row.working_directory;
2154
2351
  const started = Number(row.created_at);
2155
2352
  const last = Number(row.last_activity_at);
@@ -2194,8 +2391,8 @@ function readDevinChain(db, session, fromLine) {
2194
2391
  }
2195
2392
  function devinVersion(env = process.env) {
2196
2393
  try {
2197
- const target = readlinkSync(path7.join(devinDataDir(env), "_versions", "current"));
2198
- const name = path7.basename(target);
2394
+ const target = readlinkSync(path8.join(devinDataDir(env), "_versions", "current"));
2395
+ const name = path8.basename(target);
2199
2396
  return /^[0-9][0-9A-Za-z.+-]{0,63}$/.test(name) ? name : void 0;
2200
2397
  } catch {
2201
2398
  return void 0;
@@ -2210,7 +2407,7 @@ function devinAccount(env = process.env) {
2210
2407
  // ../core/src/adapters/gemini-cli.ts
2211
2408
  import { execFileSync as execFileSync4 } from "node:child_process";
2212
2409
  import { existsSync as existsSync7, readFileSync as readFileSync5 } from "node:fs";
2213
- import path8 from "node:path";
2410
+ import path9 from "node:path";
2214
2411
  var NAME5 = "gemini-cli";
2215
2412
  var GEMINI_EVENTS = [
2216
2413
  "SessionStart",
@@ -2223,26 +2420,31 @@ var GEMINI_EVENTS = [
2223
2420
  "SessionEnd"
2224
2421
  ];
2225
2422
  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");
2423
+ var geminiDir = (env) => env.GEMINI_CLI_HOME || path9.join(homeDir(), ".gemini");
2424
+ var settings = (env) => path9.join(geminiDir(env), "settings.json");
2228
2425
  var validTool5 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
2229
2426
  var eventsFor4 = (lean) => lean ? GEMINI_EVENTS.filter((e) => e !== "BeforeTool") : [...GEMINI_EVENTS];
2427
+ function geminiVersion(platform = process.platform, env = process.env) {
2428
+ const win = platform === "win32";
2429
+ const bin = win ? findBinary({ names: ["gemini.exe", "gemini.cmd"], dirs: [path9.dirname(process.execPath)], env }) : "gemini";
2430
+ if (!bin) throw new Error("no gemini on PATH");
2431
+ const o = {
2432
+ encoding: "utf8",
2433
+ timeout: 4e3,
2434
+ stdio: ["ignore", "pipe", "ignore"],
2435
+ env
2436
+ };
2437
+ const out = /\.cmd$/i.test(bin) ? execFileSync4(`"${bin}" --version`, { ...o, shell: true }) : execFileSync4(bin, ["--version"], o);
2438
+ return out.trim();
2439
+ }
2230
2440
  function geminiHooksActive(settingsFile) {
2231
- let tools;
2232
- try {
2233
- tools = JSON.parse(readFileSync5(settingsFile, "utf8")).tools;
2234
- } catch {
2235
- }
2441
+ const tools = readJson(settingsFile).tools;
2236
2442
  if (tools?.enableHooks === true) return { active: true };
2237
2443
  if (tools?.enableHooks === false)
2238
2444
  return { active: false, note: "inactive: tools.enableHooks is false in settings.json" };
2239
2445
  let version = "";
2240
2446
  try {
2241
- version = execFileSync4("gemini", ["--version"], {
2242
- encoding: "utf8",
2243
- timeout: 4e3,
2244
- stdio: ["ignore", "pipe", "ignore"]
2245
- }).trim();
2447
+ version = geminiVersion();
2246
2448
  } catch {
2247
2449
  return {
2248
2450
  active: true,
@@ -2260,7 +2462,7 @@ var geminiCli = {
2260
2462
  name: NAME5,
2261
2463
  events: GEMINI_EVENTS,
2262
2464
  // ~/.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")),
2465
+ detect: (env = process.env) => existsSync7(settings(env)) || existsSync7(path9.join(geminiDir(env), "tmp")),
2264
2466
  configFiles: (env = process.env) => [settings(env)],
2265
2467
  install: (cmd, env = process.env, opts = {}) => {
2266
2468
  const r = installClaudeShaped(settings(env), NAME5, eventsFor4(opts.lean), cmd, OPTS4);
@@ -2398,8 +2600,8 @@ var ADAPTERS = [claudeCode, codex, geminiCli, antigravity, devin];
2398
2600
  var adapterByName = (name) => ADAPTERS.find((a) => a.name === name);
2399
2601
 
2400
2602
  // ../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";
2603
+ import { chmodSync, existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "node:fs";
2604
+ import path10 from "node:path";
2403
2605
  var DEFAULT_CONFIG = { sinks: [], harnesses: {}, update_check: true, keep_days: 30 };
2404
2606
  function readConfig(file = configFile()) {
2405
2607
  try {
@@ -2415,8 +2617,8 @@ function readConfig(file = configFile()) {
2415
2617
  }
2416
2618
  }
2417
2619
  function writeConfig(c, file = configFile()) {
2418
- mkdirSync4(path9.dirname(file), { recursive: true, mode: 448 });
2419
- writeFileSync4(file, `${JSON.stringify(c, null, 2)}
2620
+ mkdirSync4(path10.dirname(file), { recursive: true, mode: 448 });
2621
+ writeFileSync5(file, `${JSON.stringify(c, null, 2)}
2420
2622
  `, { mode: 384 });
2421
2623
  if (process.platform !== "win32" && existsSync8(file)) {
2422
2624
  try {
@@ -2506,11 +2708,11 @@ import {
2506
2708
  readFileSync as readFileSync7,
2507
2709
  readSync as readSync2,
2508
2710
  renameSync,
2509
- statSync as statSync4,
2711
+ statSync as statSync5,
2510
2712
  unlinkSync as unlinkSync3,
2511
- writeFileSync as writeFileSync5
2713
+ writeFileSync as writeFileSync6
2512
2714
  } from "node:fs";
2513
- import path10 from "node:path";
2715
+ import path11 from "node:path";
2514
2716
  var READ_CHUNK = 1024 * 1024;
2515
2717
  var READ_MAX = 8 * 1024 * 1024;
2516
2718
  var Outbox = class {
@@ -2519,13 +2721,13 @@ var Outbox = class {
2519
2721
  }
2520
2722
  root;
2521
2723
  dir(harness) {
2522
- return path10.join(this.root, "outbox", safeId(harness));
2724
+ return path11.join(this.root, "outbox", safeId(harness));
2523
2725
  }
2524
2726
  file(ref) {
2525
- return path10.join(this.dir(ref.harness), `${safeId(ref.session)}.jsonl`);
2727
+ return path11.join(this.dir(ref.harness), `${safeId(ref.session)}.jsonl`);
2526
2728
  }
2527
2729
  seqFile(ref) {
2528
- return path10.join(this.dir(ref.harness), `${safeId(ref.session)}.seq`);
2730
+ return path11.join(this.dir(ref.harness), `${safeId(ref.session)}.seq`);
2529
2731
  }
2530
2732
  /** The next seq for a session, persisted (survives a hibernating machine and a
2531
2733
  * restart; an in-memory counter would restart at 0 and collide). */
@@ -2538,8 +2740,8 @@ var Outbox = class {
2538
2740
  } catch {
2539
2741
  }
2540
2742
  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 });
2743
+ mkdirSync5(path11.dirname(f), { recursive: true, mode: 448 });
2744
+ writeFileSync6(f, String(n + 1), { mode: 384 });
2543
2745
  return n;
2544
2746
  }
2545
2747
  lastSeqInFile(ref) {
@@ -2564,10 +2766,10 @@ var Outbox = class {
2564
2766
  /** Every session file, newest first. */
2565
2767
  sessions() {
2566
2768
  const out = [];
2567
- const base = path10.join(this.root, "outbox");
2769
+ const base = path11.join(this.root, "outbox");
2568
2770
  if (!existsSync9(base)) return [];
2569
2771
  for (const h of readdirSync5(base)) {
2570
- const d = path10.join(base, h);
2772
+ const d = path11.join(base, h);
2571
2773
  let names = [];
2572
2774
  try {
2573
2775
  names = readdirSync5(d).filter((n) => n.endsWith(".jsonl"));
@@ -2576,7 +2778,7 @@ var Outbox = class {
2576
2778
  }
2577
2779
  for (const n of names) {
2578
2780
  try {
2579
- out.push({ ref: { harness: h, session: n.slice(0, -6) }, mtime: statSync4(path10.join(d, n)).mtimeMs });
2781
+ out.push({ ref: { harness: h, session: n.slice(0, -6) }, mtime: statSync5(path11.join(d, n)).mtimeMs });
2580
2782
  } catch {
2581
2783
  }
2582
2784
  }
@@ -2592,7 +2794,7 @@ var Outbox = class {
2592
2794
  read(ref, from, maxEvents = 500) {
2593
2795
  const f = this.file(ref);
2594
2796
  if (!existsSync9(f)) return { events: [], offset: from };
2595
- const size = statSync4(f).size;
2797
+ const size = statSync5(f).size;
2596
2798
  if (from >= size) return { events: [], offset: from };
2597
2799
  const fd = openSync2(f, "r");
2598
2800
  try {
@@ -2634,7 +2836,7 @@ var Outbox = class {
2634
2836
  for (const ref of this.sessions()) {
2635
2837
  const f = this.file(ref);
2636
2838
  try {
2637
- if (statSync4(f).mtimeMs < cutoff) {
2839
+ if (statSync5(f).mtimeMs < cutoff) {
2638
2840
  unlinkSync3(f);
2639
2841
  try {
2640
2842
  unlinkSync3(this.seqFile(ref));
@@ -2664,7 +2866,7 @@ var Cursors = class {
2664
2866
  root;
2665
2867
  sink;
2666
2868
  file() {
2667
- return path10.join(this.root, "cursors", `${safeId(this.sink)}.json`);
2869
+ return path11.join(this.root, "cursors", `${safeId(this.sink)}.json`);
2668
2870
  }
2669
2871
  read() {
2670
2872
  try {
@@ -2675,9 +2877,9 @@ var Cursors = class {
2675
2877
  }
2676
2878
  write(c) {
2677
2879
  const f = this.file();
2678
- mkdirSync5(path10.dirname(f), { recursive: true, mode: 448 });
2880
+ mkdirSync5(path11.dirname(f), { recursive: true, mode: 448 });
2679
2881
  const tmp = `${f}.${process.pid}.tmp`;
2680
- writeFileSync5(tmp, JSON.stringify(c), { mode: 384 });
2882
+ writeFileSync6(tmp, JSON.stringify(c), { mode: 384 });
2681
2883
  renameSync(tmp, f);
2682
2884
  }
2683
2885
  key(ref) {
@@ -2688,16 +2890,16 @@ function takeLock(root, ref, staleMs = 5 * 6e4) {
2688
2890
  return takeNamedLock(root, `${safeId(ref.harness)}-${safeId(ref.session)}`, staleMs);
2689
2891
  }
2690
2892
  function takeNamedLock(root, name, staleMs = 5 * 6e4) {
2691
- const dir = path10.join(root, "locks");
2893
+ const dir = path11.join(root, "locks");
2692
2894
  mkdirSync5(dir, { recursive: true, mode: 448 });
2693
- const f = path10.join(dir, `${name}.lock`);
2895
+ const f = path11.join(dir, `${name}.lock`);
2694
2896
  try {
2695
- if (Date.now() - statSync4(f).mtimeMs > staleMs || !processAlive(Number(readFileSync7(f, "utf8")))) unlinkSync3(f);
2897
+ if (Date.now() - statSync5(f).mtimeMs > staleMs || !processAlive(Number(readFileSync7(f, "utf8")))) unlinkSync3(f);
2696
2898
  else return null;
2697
2899
  } catch {
2698
2900
  }
2699
2901
  try {
2700
- writeFileSync5(f, String(process.pid), { flag: "wx" });
2902
+ writeFileSync6(f, String(process.pid), { flag: "wx" });
2701
2903
  } catch {
2702
2904
  return null;
2703
2905
  }
@@ -2929,6 +3131,7 @@ var TYPE_TIER = {
2929
3131
  "subagent.started": 1,
2930
3132
  "subagent.ended": 1,
2931
3133
  "context.compacted": 1,
3134
+ "limits.observed": 1,
2932
3135
  native: 3
2933
3136
  };
2934
3137
  var DATA_ALLOW = {
@@ -2948,7 +3151,8 @@ var DATA_ALLOW = {
2948
3151
  "attention.cleared": { 0: ["attention_id", "how"] },
2949
3152
  "subagent.started": { 1: ["agent_id", "agent_type"] },
2950
3153
  "subagent.ended": { 1: ["agent_id", "agent_type"] },
2951
- "context.compacted": { 1: ["trigger"] }
3154
+ "context.compacted": { 1: ["trigger"] },
3155
+ "limits.observed": { 1: ["windows", "plan", "account_id"] }
2952
3156
  };
2953
3157
  var SESSION_ALLOW = [
2954
3158
  "id",
@@ -3013,7 +3217,9 @@ function filterEvent(event, tier, pii, opts = {}) {
3013
3217
  const s = reducePiiDeep(out.session, opts.home);
3014
3218
  if (typeof s.machine === "string") s.machine = hashMachine(s.machine);
3015
3219
  delete s.account_id;
3016
- out = { ...out, session: s, data: reducePiiDeep(out.data, opts.home) };
3220
+ const d = reducePiiDeep(out.data, opts.home);
3221
+ delete d.account_id;
3222
+ out = { ...out, session: s, data: d };
3017
3223
  rulesets.push(RULESET_PII);
3018
3224
  }
3019
3225
  out.privacy = { rulesets, pii };
@@ -3028,7 +3234,7 @@ function shortId(harness, id) {
3028
3234
 
3029
3235
  // ../core/src/sinks/file.ts
3030
3236
  import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync6 } from "node:fs";
3031
- import path11 from "node:path";
3237
+ import path12 from "node:path";
3032
3238
  var FileSink = class {
3033
3239
  constructor(name, file, tier, pii) {
3034
3240
  this.name = name;
@@ -3042,7 +3248,7 @@ var FileSink = class {
3042
3248
  pii;
3043
3249
  async send(events) {
3044
3250
  try {
3045
- mkdirSync6(path11.dirname(this.file), { recursive: true });
3251
+ mkdirSync6(path12.dirname(this.file), { recursive: true });
3046
3252
  appendFileSync2(this.file, `${events.map((e) => JSON.stringify(e)).join("\n")}
3047
3253
  `);
3048
3254
  return { ok: true, acknowledged: events.length, accepted: events.length, duplicates: 0, rejected: [] };
@@ -3083,16 +3289,16 @@ import {
3083
3289
  readdirSync as readdirSync6,
3084
3290
  readFileSync as readFileSync9,
3085
3291
  renameSync as renameSync3,
3086
- statSync as statSync5,
3292
+ statSync as statSync6,
3087
3293
  unlinkSync as unlinkSync4,
3088
- writeFileSync as writeFileSync7
3294
+ writeFileSync as writeFileSync8
3089
3295
  } from "node:fs";
3090
3296
  import os3 from "node:os";
3091
- import path14 from "node:path";
3297
+ import path15 from "node:path";
3092
3298
 
3093
3299
  // 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";
3300
+ import { chmodSync as chmodSync2, mkdirSync as mkdirSync7, readFileSync as readFileSync8, renameSync as renameSync2, writeFileSync as writeFileSync7 } from "node:fs";
3301
+ import path13 from "node:path";
3096
3302
 
3097
3303
  // src/secrets.ts
3098
3304
  import { spawnSync } from "node:child_process";
@@ -3136,7 +3342,7 @@ function startOptionsOf(v) {
3136
3342
  }
3137
3343
 
3138
3344
  // src/control/aliases.ts
3139
- var aliasFile = (controlDir) => path12.join(controlDir, "aliases.json");
3345
+ var aliasFile = (controlDir) => path13.join(controlDir, "aliases.json");
3140
3346
  function readAliases(file) {
3141
3347
  try {
3142
3348
  const j = JSON.parse(readFileSync8(file, "utf8"));
@@ -3161,11 +3367,11 @@ function receiverIdOf(all, harness, id) {
3161
3367
  // src/control/local.ts
3162
3368
  import net from "node:net";
3163
3369
  import os2 from "node:os";
3164
- import path13 from "node:path";
3370
+ import path14 from "node:path";
3165
3371
  function socketPath(state, env = process.env) {
3166
3372
  if (env.SESSIONPIPE_SOCKET) return env.SESSIONPIPE_SOCKET;
3167
3373
  if (process.platform === "win32") return `\\\\.\\pipe\\sessionpipe-control-${os2.userInfo().username}`;
3168
- return path13.join(state, "control.sock");
3374
+ return path14.join(state, "control.sock");
3169
3375
  }
3170
3376
  function ask(sock, req, timeoutMs) {
3171
3377
  return new Promise((resolve) => {
@@ -3312,8 +3518,8 @@ var MAX_TRIES = 10;
3312
3518
  function enqueueJob(state, job) {
3313
3519
  const jobs = jobsDir(state);
3314
3520
  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 });
3521
+ const f = path15.join(jobs, `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}.json`);
3522
+ writeFileSync8(f, JSON.stringify(job), { mode: 384 });
3317
3523
  return f;
3318
3524
  }
3319
3525
  function claimJob(file) {
@@ -3349,19 +3555,19 @@ async function sweepJobs(state, olderThanMs = 3e3, limit = 20, abandonedMs = 6e4
3349
3555
  for (const n of names) {
3350
3556
  const m = /^(.*\.json)\.(\d+)\.running$/.exec(n);
3351
3557
  if (!m) continue;
3352
- const f = path14.join(jobs, n);
3558
+ const f = path15.join(jobs, n);
3353
3559
  try {
3354
- if (processAlive(Number(m[2])) && Date.now() - statSync5(f).mtimeMs < abandonedMs) continue;
3355
- renameSync3(f, path14.join(jobs, m[1]));
3560
+ if (processAlive(Number(m[2])) && Date.now() - statSync6(f).mtimeMs < abandonedMs) continue;
3561
+ renameSync3(f, path15.join(jobs, m[1]));
3356
3562
  queued.push(m[1]);
3357
3563
  } catch {
3358
3564
  }
3359
3565
  }
3360
3566
  let ran = 0;
3361
3567
  for (const n of queued.sort().slice(0, limit)) {
3362
- const f = path14.join(jobs, n);
3568
+ const f = path15.join(jobs, n);
3363
3569
  try {
3364
- if (Date.now() - statSync5(f).mtimeMs < olderThanMs) continue;
3570
+ if (Date.now() - statSync6(f).mtimeMs < olderThanMs) continue;
3365
3571
  } catch {
3366
3572
  continue;
3367
3573
  }
@@ -3380,9 +3586,9 @@ async function sweepJobs(state, olderThanMs = 3e3, limit = 20, abandonedMs = 6e4
3380
3586
  function log(state, line) {
3381
3587
  try {
3382
3588
  mkdirSync8(state, { recursive: true, mode: 448 });
3383
- const f = path14.join(state, "log");
3589
+ const f = path15.join(state, "log");
3384
3590
  try {
3385
- if (statSync5(f).size > 1024 * 1024) renameSync3(f, `${f}.1`);
3591
+ if (statSync6(f).size > 1024 * 1024) renameSync3(f, `${f}.1`);
3386
3592
  } catch {
3387
3593
  }
3388
3594
  appendFileSync3(f, `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
@@ -3391,7 +3597,7 @@ function log(state, line) {
3391
3597
  }
3392
3598
  }
3393
3599
  function factsState(state) {
3394
- const file = (h, s) => path14.join(state, "facts", safeId(h), `${safeId(s)}.json`);
3600
+ const file = (h, s) => path15.join(state, "facts", safeId(h), `${safeId(s)}.json`);
3395
3601
  return {
3396
3602
  get(h, s) {
3397
3603
  try {
@@ -3404,8 +3610,8 @@ function factsState(state) {
3404
3610
  const cur = this.get(h, s);
3405
3611
  const next = { ...cur, ...patch };
3406
3612
  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 });
3613
+ mkdirSync8(path15.dirname(file(h, s)), { recursive: true, mode: 448 });
3614
+ writeFileSync8(file(h, s), JSON.stringify(next), { mode: 384 });
3409
3615
  }
3410
3616
  };
3411
3617
  }
@@ -3477,7 +3683,7 @@ async function runJob(job, opts = {}) {
3477
3683
  );
3478
3684
  return { events: [], delivered: {} };
3479
3685
  }
3480
- const id = receiverIdOf(readAliases(aliasFile(path14.join(state, "control"))), job.harness, r.session.id) ?? r.session.id;
3686
+ const id = receiverIdOf(readAliases(aliasFile(path15.join(state, "control"))), job.harness, r.session.id) ?? r.session.id;
3481
3687
  const claudeEnd = job.harness === "claude-code" && r.events.some((e) => e.type === "session.ended");
3482
3688
  if (!claudeEnd) await tellDaemon(state, job, id, r.session.cwd, r.transcript);
3483
3689
  const outbox = new Outbox(state);
@@ -3506,12 +3712,15 @@ async function runJob(job, opts = {}) {
3506
3712
  const heavy = !/^(PreToolUse|PostToolUse|PostToolUseFailure|BeforeTool|AfterTool|PostInvocation)$/.test(job.event);
3507
3713
  let learned = {};
3508
3714
  if (heavy) {
3509
- const f = adapter.facts(r.session, r.transcript, r.hints ?? {}, facts, { ...process.env, ...job.env });
3715
+ const hookEnv = { ...process.env, ...job.env };
3716
+ const f = adapter.facts(r.session, r.transcript, r.hints ?? {}, facts, hookEnv);
3717
+ if (cfg.remote_titles && adapter.remoteFacts)
3718
+ Object.assign(f, await adapter.remoteFacts(r.session, f, r.transcript, facts, hookEnv));
3510
3719
  const { started_at: _s, last_at: _l, first_ask: _fa, harness_version, ...rest } = f;
3511
3720
  learned = rest;
3512
3721
  if (harness_version) facts.save(job.harness, id, { harness_version });
3513
3722
  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))
3723
+ if (CWD_FROM_HOOKS.has(job.harness) && r.session.cwd && path15.isAbsolute(r.session.cwd))
3515
3724
  facts.save(job.harness, id, { cwd: r.session.cwd });
3516
3725
  }
3517
3726
  const known = mem.session ?? {};
@@ -3593,6 +3802,23 @@ async function runJob(job, opts = {}) {
3593
3802
  }
3594
3803
  if (read.turns.length) facts.save(job.harness, id, { transcript_line: read.line, transcript_turns: turnNo });
3595
3804
  }
3805
+ if (adapter.limits && r.transcript && ending && cfg.sinks.some((s) => !s.paused && Math.min(s.tier, s.max_tier ?? 3) >= 1)) {
3806
+ const limits = adapter.limits(r.transcript);
3807
+ const sig = limits && JSON.stringify(limits);
3808
+ if (limits && sig !== facts.get(job.harness, id).limits_sig) {
3809
+ events.push(
3810
+ makeEvent(
3811
+ { type: "limits.observed", harnessEvent: job.event, data: limits },
3812
+ {
3813
+ harness: { name: job.harness, ...version ? { version } : {} },
3814
+ session: { ...session, seq: outbox.nextSeq(ref) },
3815
+ now: opts.now ?? job.at
3816
+ }
3817
+ )
3818
+ );
3819
+ facts.save(job.harness, id, { limits_sig: sig });
3820
+ }
3821
+ }
3596
3822
  outbox.append(
3597
3823
  ref,
3598
3824
  events.map((e) => ({ ...e, session: redactDeep(e.session), data: redactDeep(e.data) }))
@@ -3610,11 +3836,11 @@ async function runJob(job, opts = {}) {
3610
3836
  return { events, delivered };
3611
3837
  }
3612
3838
  async function drain(state, sinkConfigs, outbox, first) {
3613
- const more = path14.join(state, "locks", "flush.more");
3839
+ const more = path15.join(state, "locks", "flush.more");
3614
3840
  let release = takeNamedLock(state, "flush");
3615
3841
  if (!release) {
3616
3842
  try {
3617
- writeFileSync7(more, "", { mode: 384 });
3843
+ writeFileSync8(more, "", { mode: 384 });
3618
3844
  } catch {
3619
3845
  }
3620
3846
  release = takeNamedLock(state, "flush");
@@ -3638,7 +3864,7 @@ async function drain(state, sinkConfigs, outbox, first) {
3638
3864
  }
3639
3865
  }
3640
3866
  var BACKOFF_MS = [2e3, 1e4, 6e4, 5 * 6e4, 30 * 6e4];
3641
- var backoffFile = (state, sink) => path14.join(state, "backoff", `${safeId(sink)}.json`);
3867
+ var backoffFile = (state, sink) => path15.join(state, "backoff", `${safeId(sink)}.json`);
3642
3868
  function readBackoff(state, sink) {
3643
3869
  try {
3644
3870
  const b = JSON.parse(readFileSync9(backoffFile(state, sink), "utf8"));
@@ -3654,8 +3880,8 @@ function noteFailure(state, sink, res, prev) {
3654
3880
  const b = { until: Date.now() + wait, failures, why: `${res.status ?? ""} ${res.error ?? ""}`.trim() };
3655
3881
  const f = backoffFile(state, sink);
3656
3882
  try {
3657
- mkdirSync8(path14.dirname(f), { recursive: true, mode: 448 });
3658
- writeFileSync7(f, JSON.stringify(b), { mode: 384 });
3883
+ mkdirSync8(path15.dirname(f), { recursive: true, mode: 448 });
3884
+ writeFileSync8(f, JSON.stringify(b), { mode: 384 });
3659
3885
  } catch {
3660
3886
  }
3661
3887
  return b;
@@ -3787,7 +4013,7 @@ function applySinkVerdict(name, res) {
3787
4013
  } catch {
3788
4014
  }
3789
4015
  }
3790
- var jobsDir = (state) => path14.join(state, "jobs");
4016
+ var jobsDir = (state) => path15.join(state, "jobs");
3791
4017
 
3792
4018
  // src/worker.ts
3793
4019
  async function workerMain(argv) {