sessionpipe-conformance 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,31 +1,25 @@
1
1
  #!/usr/bin/env node
2
- // sessionpipe-conformance 0.10.0 · Apache-2.0 · https://sessionpipe.org
2
+ // sessionpipe-conformance 0.12.0 · Apache-2.0 · https://sessionpipe.org
3
3
 
4
4
  // src/cli.ts
5
- import { readdirSync as readdirSync5, realpathSync as realpathSync2, statSync as statSync4 } from "node:fs";
6
- import path8 from "node:path";
5
+ import { readdirSync as readdirSync5, realpathSync as realpathSync2, statSync as statSync5 } from "node:fs";
6
+ import path9 from "node:path";
7
7
  import { pathToFileURL } from "node:url";
8
8
 
9
9
  // src/index.ts
10
- import { readFileSync as readFileSync6 } from "node:fs";
11
-
12
- // ../core/src/adapters/antigravity.ts
13
- import {
14
- existsSync as existsSync2,
15
- mkdirSync as mkdirSync2,
16
- readdirSync as readdirSync2,
17
- readFileSync as readFileSync2,
18
- realpathSync,
19
- statSync as statSync2,
20
- unlinkSync as unlinkSync2,
21
- writeFileSync as writeFileSync2
22
- } from "node:fs";
23
- import path3 from "node:path";
10
+ import { readFileSync as readFileSync7 } from "node:fs";
24
11
 
25
12
  // ../core/src/paths.ts
26
13
  import os from "node:os";
27
14
  var homeDir = () => os.homedir();
28
15
 
16
+ // ../core/src/adapters/claude-code.ts
17
+ import { execFileSync as execFileSync2 } from "node:child_process";
18
+ import { createHash } from "node:crypto";
19
+ import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
20
+ import { userInfo } from "node:os";
21
+ import path3 from "node:path";
22
+
29
23
  // ../core/src/readers/files.ts
30
24
  import { closeSync, fstatSync, openSync, readdirSync, readSync, statSync } from "node:fs";
31
25
  import path from "node:path";
@@ -116,6 +110,38 @@ function recentFiles(root, depth, sinceMs, match) {
116
110
  return out;
117
111
  }
118
112
 
113
+ // ../core/src/readers/git.ts
114
+ import { execFileSync } from "node:child_process";
115
+ var cache = /* @__PURE__ */ new Map();
116
+ function gitFacts(cwd) {
117
+ if (!cwd) return {};
118
+ const hit = cache.get(cwd);
119
+ if (hit) return hit;
120
+ const run = (args) => {
121
+ try {
122
+ return execFileSync("git", ["-C", cwd, ...args], {
123
+ encoding: "utf8",
124
+ timeout: 1500,
125
+ stdio: ["ignore", "pipe", "ignore"]
126
+ }).trim();
127
+ } catch {
128
+ return "";
129
+ }
130
+ };
131
+ const out = {};
132
+ const repo = run(["remote", "get-url", "origin"]);
133
+ if (repo) out.repo = stripCredentials(repo);
134
+ const branch = run(["rev-parse", "--abbrev-ref", "HEAD"]);
135
+ if (branch && branch !== "HEAD") out.branch = branch;
136
+ cache.set(cwd, out);
137
+ return out;
138
+ }
139
+ function stripCredentials(url) {
140
+ const ssh = /^(?:ssh:\/\/)?git@([^:/]+)[:/](.+?)(?:\.git)?$/.exec(url);
141
+ if (ssh) return `${ssh[1]}/${ssh[2]}`;
142
+ return url.replace(/\/\/[^/@]*@/, "//").replace(/^https?:\/\//, "").replace(/\.git$/, "");
143
+ }
144
+
119
145
  // ../core/src/adapters/codex-rollout.ts
120
146
  function codexSubagent(meta) {
121
147
  if (meta?.type !== "session_meta") return false;
@@ -275,8 +301,10 @@ function readJsonFile(file) {
275
301
  try {
276
302
  raw = readFileSync(file, "utf8");
277
303
  } catch {
278
- return { ok: true, existed: false, value: {}, raw: "", indent: " ", eol: "\n", finalNewline: true };
304
+ return { ok: true, existed: false, value: {}, raw: "", indent: " ", eol: "\n", finalNewline: true, bom: false };
279
305
  }
306
+ const bom = raw.charCodeAt(0) === 65279;
307
+ if (bom) raw = raw.slice(1);
280
308
  let value;
281
309
  try {
282
310
  value = JSON.parse(raw);
@@ -296,12 +324,13 @@ function readJsonFile(file) {
296
324
  raw,
297
325
  indent,
298
326
  eol,
299
- finalNewline: /\r?\n$/.test(raw)
327
+ finalNewline: /\r?\n$/.test(raw),
328
+ bom
300
329
  };
301
330
  }
302
331
  function formatJson(value, f) {
303
332
  const text = JSON.stringify(value, null, f.indent).replace(/\n/g, f.eol);
304
- return f.finalNewline ? text + f.eol : text;
333
+ return (f.bom ? "\uFEFF" : "") + (f.finalNewline ? text + f.eol : text);
305
334
  }
306
335
  function readJson(file) {
307
336
  const f = readJsonFile(file);
@@ -397,74 +426,79 @@ function installedClaudeShaped(file, harness, events, cmd, o) {
397
426
  return current === events.length ? "current" : "stale";
398
427
  }
399
428
 
400
- // ../core/src/adapters/antigravity.ts
401
- var NAME = "antigravity";
402
- var HOOK_NAME = "sessionpipe";
403
- var AG_EVENTS = ["PreInvocation", "PostInvocation", "PostToolUse", "Stop"];
404
- 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';
405
- var antigravityAnswer = (event) => event === "PreToolUse" ? AG_STALE_PRETOOL : "{}\n";
406
- var gemini = (env) => env.GEMINI_CLI_HOME || path3.join(homeDir(), ".gemini");
407
- var hooksFile = (env) => path3.join(gemini(env), "config", "hooks.json");
408
- var projectsDir = (env) => path3.join(gemini(env), "config", "projects");
409
- var dataDirs = (env) => ["antigravity", "antigravity-cli", "antigravity-ide"].map((d) => path3.join(gemini(env), d));
410
- var ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
411
- var validTool = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
412
- function entry(cmd) {
413
- const handler = (e) => ({ type: "command", command: cmd([NAME, e]).command, timeout: 5 });
414
- const out = { enabled: true };
415
- for (const e of AG_EVENTS) out[e] = /Tool/.test(e) ? [{ matcher: "*", hooks: [handler(e)] }] : [handler(e)];
416
- return out;
429
+ // ../core/src/adapters/claude-code.ts
430
+ var NAME = "claude-code";
431
+ var CLAUDE_EVENTS = [
432
+ "SessionStart",
433
+ "UserPromptSubmit",
434
+ "PreToolUse",
435
+ "PostToolUse",
436
+ "PostToolUseFailure",
437
+ "PermissionRequest",
438
+ "Notification",
439
+ "Stop",
440
+ "SubagentStart",
441
+ "SubagentStop",
442
+ "PreCompact",
443
+ "PostCompact",
444
+ "SessionEnd",
445
+ "StopFailure"
446
+ ];
447
+ var LEAN_SKIP = /* @__PURE__ */ new Set(["PreToolUse", "PostToolUseFailure"]);
448
+ var eventsFor = (lean) => lean ? CLAUDE_EVENTS.filter((e) => !LEAN_SKIP.has(e)) : [...CLAUDE_EVENTS];
449
+ var OPTS = {
450
+ matcherFor: (e) => /Tool|Permission/.test(e) ? "" : void 0,
451
+ timeout: 5,
452
+ // The hook waits up to 125 s for a verified control answer, and only when control
453
+ // is paired on this machine; everywhere else it exits in milliseconds as before.
454
+ timeoutFor: (e) => e === "PermissionRequest" ? 130 : void 0
455
+ };
456
+ var DESKTOP_ID = /^local_[A-Za-z0-9-]{1,64}$/;
457
+ function claudeDirs(env = process.env, home = env.HOME || homeDir()) {
458
+ const out = [];
459
+ const add = (d) => {
460
+ if (!d) return;
461
+ const r = path3.resolve(d);
462
+ if (!out.includes(r)) out.push(r);
463
+ };
464
+ add(path3.join(home, ".claude"));
465
+ add(env.CLAUDE_CONFIG_DIR);
466
+ try {
467
+ for (const e of readdirSync2(home, { withFileTypes: true })) {
468
+ if (!e.isDirectory() || !/^\.claude[-_.]/.test(e.name)) continue;
469
+ const d = path3.join(home, e.name);
470
+ if (existsSync2(path3.join(d, "projects")) || existsSync2(path3.join(d, "settings.json"))) add(d);
471
+ }
472
+ } catch {
473
+ }
474
+ return out.filter((d, i) => i === 0 || existsSync2(d));
417
475
  }
418
- var antigravity = {
476
+ var settingsIn = (dir) => path3.join(dir, "settings.json");
477
+ var configDirOf = (transcript) => path3.dirname(path3.dirname(path3.dirname(transcript)));
478
+ var validTool = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
479
+ var endReason = (r) => r === "clear" || r === "logout" || r === "exit" ? r : r === "prompt_input_exit" ? "exit" : "other";
480
+ var notifKind = (t) => t === "permission_prompt" ? "permission" : t === "idle_prompt" ? "idle" : t === "elicitation_dialog" ? "elicitation" : "question";
481
+ var claudeCode = {
419
482
  name: NAME,
420
- events: AG_EVENTS,
421
- detect: (env = process.env) => existsSync2(path3.join(gemini(env), "config")) || dataDirs(env).some((d) => existsSync2(d)),
422
- configFiles: (env = process.env) => [hooksFile(env)],
423
- install(cmd, env = process.env) {
424
- const file = hooksFile(env);
425
- const f = readJsonFile(file);
426
- if (!f.ok) return [{ file, changed: false, skipped: true, note: `${f.reason}; not touched` }];
427
- const hooks = f.value;
428
- const e = entry(cmd);
429
- if (JSON.stringify(hooks[HOOK_NAME]) === JSON.stringify(e)) return [{ file, changed: false }];
430
- hooks[HOOK_NAME] = e;
431
- mkdirSync2(path3.dirname(file), { recursive: true });
432
- writeFileSync2(file, formatJson(hooks, f));
433
- return [
434
- {
435
- file,
436
- changed: true,
437
- created: !f.existed,
438
- note: "hooks.json is read when a conversation starts; open ones report after a restart"
439
- }
440
- ];
441
- },
442
- uninstall(env = process.env, opts = {}) {
443
- const file = hooksFile(env);
444
- if (!existsSync2(file)) return [{ file, changed: false }];
445
- const f = readJsonFile(file);
446
- if (!f.ok) return [{ file, changed: false, skipped: true, note: `${f.reason}; not touched` }];
447
- const hooks = f.value;
448
- if (!(HOOK_NAME in hooks)) return [{ file, changed: false }];
449
- delete hooks[HOOK_NAME];
450
- if (opts.created?.includes(file) && !Object.keys(hooks).length) {
451
- unlinkSync2(file);
452
- return [{ file, changed: true, note: "removed (sessionpipe created it)" }];
453
- }
454
- writeFileSync2(file, formatJson(hooks, f));
455
- return [{ file, changed: true }];
456
- },
457
- installed(cmd, env = process.env) {
458
- const file = hooksFile(env);
459
- const f = readJsonFile(file);
460
- const have = f.ok ? f.value[HOOK_NAME] : void 0;
461
- return [
462
- {
463
- file,
464
- state: !have ? "missing" : JSON.stringify(have) === JSON.stringify(entry(cmd)) ? "current" : "stale"
465
- }
466
- ];
467
- },
483
+ events: CLAUDE_EVENTS,
484
+ detect: (env = process.env) => claudeDirs(env).some((d) => existsSync2(d)),
485
+ configFiles: (env = process.env) => claudeDirs(env).map(settingsIn),
486
+ install: (cmd, env = process.env, opts = {}) => claudeDirs(env).flatMap((d) => {
487
+ const hooks = installClaudeShaped(settingsIn(d), NAME, eventsFor(opts.lean), cmd, OPTS);
488
+ const line = opts.statusLine === void 0 ? null : statusLine(settingsIn(d), opts.statusLine, cmd);
489
+ return line ? [hooks, line] : [hooks];
490
+ }),
491
+ uninstall: (env = process.env, opts = {}) => claudeDirs(env).map((d) => {
492
+ const line = statusLine(settingsIn(d), false);
493
+ const hooks = uninstallClaudeShaped(settingsIn(d), CLAUDE_EVENTS, {
494
+ created: opts.created?.includes(settingsIn(d))
495
+ });
496
+ return line && !hooks.changed ? line : hooks;
497
+ }),
498
+ installed: (cmd, env = process.env, opts = {}) => claudeDirs(env).map((d) => ({
499
+ file: settingsIn(d),
500
+ state: installedClaudeShaped(settingsIn(d), NAME, eventsFor(opts.lean), cmd, OPTS)
501
+ })),
468
502
  fromHook(input) {
469
503
  const event = input.argv[1] ?? "";
470
504
  let s = {};
@@ -473,50 +507,158 @@ var antigravity = {
473
507
  if (j && typeof j === "object" && !Array.isArray(j)) s = j;
474
508
  } catch {
475
509
  }
476
- const id = s.conversationId ?? s.session_id ?? s.sessionId;
477
- const stdout = antigravityAnswer(event);
478
- if (!id) return { session: { id: "" }, events: [], stdout };
479
- const workspaces = agWorkspaces(s.workspacePaths);
480
- const session = { id: String(id), cwd: workspaces[0] ?? input.cwd };
481
- if (typeof s.modelName === "string" && s.modelName !== "auto") session.model = s.modelName;
482
- const toolCall = s.toolCall;
483
- const tool = validTool(toolCall?.name) ? toolCall?.name : validTool(s.toolName) ? s.toolName : void 0;
510
+ const id = s.session_id ?? s.sessionId;
511
+ if (!id) return null;
512
+ if (notClaudeCode(String(id), s)) return null;
513
+ const session = { id: String(id), cwd: typeof s.cwd === "string" ? s.cwd : input.cwd };
514
+ if (typeof s.model === "string") session.model = s.model;
515
+ if (typeof s.agent_id === "string" && event.startsWith("Subagent") === false && s.agent_id)
516
+ session.parent_id = null;
517
+ const turn_id = typeof s.prompt_id === "string" ? s.prompt_id : void 0;
518
+ const tool = validTool(s.tool_name) ? s.tool_name : void 0;
519
+ const call_id = typeof s.tool_use_id === "string" ? s.tool_use_id : void 0;
520
+ const ms = typeof s.duration_ms === "number" ? Math.round(s.duration_ms) : void 0;
521
+ const agent_id = typeof s.agent_id === "string" ? s.agent_id : void 0;
522
+ const permissionId = () => `perm-${createHash("sha256").update(`${turn_id ?? ""}|${tool ?? ""}|${JSON.stringify(s.tool_input ?? null)}`).digest("hex").slice(0, 16)}`;
484
523
  const ev = (type, data) => ({ type, data: prune(data), harnessEvent: event });
485
524
  const events = [];
486
525
  switch (event) {
487
- case "PreInvocation":
488
- if (Number(s.invocationNum) === 0 || s.invocationNum === void 0) {
489
- events.push(ev("session.started", { source: "unknown" }));
490
- events.push(ev("turn.started", {}));
491
- } else events.push(ev("session.heartbeat", {}));
526
+ case "SessionStart":
527
+ events.push(ev("session.started", { source: pickSource(s.source ?? s.trigger) }));
492
528
  break;
493
- case "PostInvocation":
529
+ case "UserPromptSubmit": {
530
+ const prompt = typeof s.prompt === "string" ? s.prompt : void 0;
531
+ events.push(
532
+ ev("turn.started", {
533
+ turn_id,
534
+ prompt_chars: prompt === void 0 ? void 0 : [...prompt].length,
535
+ // Claude Code's hook schema documents `source`
536
+ // (user | sdk | system | loop_wakeup | schedule_wakeup | poll_event) and says a
537
+ // payload may omit it while the field rolls out; 2.1.304's payload has no such
538
+ // key at all. Forwarded verbatim, shape-guarded only: the list is the harness's
539
+ // to extend.
540
+ submitted_by: harnessWord(s.source),
541
+ // One of Claude Code's own prompt wrappers: the prompt is a block the harness
542
+ // wrote, so it is not the person's words. Absent is unknown, never "the person" —
543
+ // and `true` is never "no person caused this turn", because a Talk or
544
+ // `sessionpipe wait` delivery hands the person's own message over inside a
545
+ // <task-notification> (PROTOCOL.md §4).
546
+ injected: prompt !== void 0 && injectedBlock(prompt) ? true : void 0
547
+ })
548
+ );
494
549
  break;
550
+ }
495
551
  case "PreToolUse":
496
- if (tool) events.push(ev("tool.started", { tool, input: toolCall?.args }));
552
+ if (tool) events.push(ev("tool.started", { tool, call_id, turn_id, agent_id, input: s.tool_input }));
497
553
  break;
498
554
  case "PostToolUse":
499
- if (!tool) events.push(ev("session.heartbeat", {}));
500
- else
555
+ if (tool)
501
556
  events.push(
502
557
  ev("tool.ended", {
503
558
  tool,
504
- ok: !s.error,
505
- error: typeof s.error === "string" ? s.error.slice(0, 2e3) : void 0,
506
- input: toolCall?.args
559
+ call_id,
560
+ turn_id,
561
+ agent_id,
562
+ ms,
563
+ ok: true,
564
+ input: s.tool_input,
565
+ output: s.tool_response
566
+ })
567
+ );
568
+ break;
569
+ case "PostToolUseFailure":
570
+ if (tool)
571
+ events.push(
572
+ ev("tool.ended", {
573
+ tool,
574
+ call_id,
575
+ turn_id,
576
+ agent_id,
577
+ ms,
578
+ ok: false,
579
+ error: errText(s.error ?? s.tool_response),
580
+ input: s.tool_input,
581
+ output: s.tool_response
507
582
  })
508
583
  );
509
584
  break;
585
+ case "PermissionRequest":
586
+ events.push(
587
+ ev("attention.needed", {
588
+ attention_id: call_id ?? permissionId(),
589
+ kind: "permission",
590
+ tool,
591
+ message: tool ? `${tool}${describe(s.tool_input)}` : void 0
592
+ })
593
+ );
594
+ break;
595
+ case "Notification":
596
+ events.push(
597
+ ev("attention.needed", {
598
+ attention_id: `notif-${Date.now()}`,
599
+ kind: notifKind(s.notification_type),
600
+ message: typeof s.message === "string" ? s.message.slice(0, 200) : void 0
601
+ })
602
+ );
603
+ break;
510
604
  case "Stop":
511
- events.push(ev("turn.ended", { reason: s.error ? "error" : "stop" }));
605
+ events.push(ev("turn.ended", { turn_id, reason: "stop", background: backgroundCount(s.background_tasks) }));
512
606
  break;
513
- default:
514
- return { session, events: [], stdout };
515
- }
516
- const r = { session, events, stdout, hints: { workspaces } };
517
- if (typeof s.transcriptPath === "string") r.transcript = s.transcriptPath;
518
- return r;
519
- },
607
+ case "StopFailure":
608
+ events.push(
609
+ ev("turn.ended", {
610
+ turn_id,
611
+ reason: "error",
612
+ error: typeof s.error === "string" ? s.error.slice(0, 200) : void 0
613
+ })
614
+ );
615
+ break;
616
+ case "SubagentStart":
617
+ events.push(
618
+ ev("subagent.started", {
619
+ agent_id: String(s.agent_id ?? ""),
620
+ agent_type: typeof s.agent_type === "string" ? s.agent_type : void 0
621
+ })
622
+ );
623
+ break;
624
+ case "SubagentStop":
625
+ events.push(
626
+ ev("subagent.ended", {
627
+ agent_id: String(s.agent_id ?? ""),
628
+ agent_type: typeof s.agent_type === "string" ? s.agent_type : void 0,
629
+ background: backgroundCount(s.background_tasks, String(s.agent_id ?? ""))
630
+ })
631
+ );
632
+ break;
633
+ case "PreCompact":
634
+ events.push(ev("context.compacted", { trigger: s.trigger === "manual" ? "manual" : "auto" }));
635
+ break;
636
+ case "PostCompact":
637
+ break;
638
+ // PreCompact already said it; a second event would double-count
639
+ case "SessionEnd":
640
+ events.push(ev("session.ended", { reason: endReason(s.reason) }));
641
+ break;
642
+ case "StatusLine": {
643
+ const limits = claudeLimits(s.rate_limits);
644
+ if (!limits) return null;
645
+ events.push(ev("limits.observed", limits));
646
+ break;
647
+ }
648
+ default:
649
+ return null;
650
+ }
651
+ const hints = {};
652
+ if (event === "UserPromptSubmit" && typeof s.prompt === "string") hints.prompt = s.prompt;
653
+ const host = input.env.CLAUDE_CODE_HOST_SESSION_ID;
654
+ if (host && DESKTOP_ID.test(host)) hints.desktop_id = host;
655
+ const attended = input.env.CLAUDE_CODE_SESSION_ATTENDED;
656
+ if (attended === "1" || attended === "0") hints.attended = attended === "1" || !!input.env.SESSIONPIPE_CONTROL_JOB;
657
+ if (event !== "PreToolUse" && event !== "PostToolUse") hints.claude_pid = claudePid(input.env);
658
+ const r = { session, events, hints };
659
+ if (typeof s.transcript_path === "string") r.transcript = s.transcript_path;
660
+ return r;
661
+ },
520
662
  readTranscript(file, fromLine) {
521
663
  let text = "";
522
664
  try {
@@ -524,259 +666,605 @@ var antigravity = {
524
666
  } catch {
525
667
  return { turns: [], line: fromLine };
526
668
  }
527
- return readAntigravity(text.split("\n"), fromLine);
669
+ return readClaude(text.split("\n"), fromLine);
528
670
  },
529
- facts(session, transcript, hints) {
530
- if (!transcript) return {};
531
- return antigravityFacts(transcript, {
532
- conversation: session.id,
533
- workspaces: hints.workspaces ?? (session.cwd ? [session.cwd] : []),
534
- env: process.env
535
- });
671
+ facts(session, transcript, hints, state) {
672
+ const out = {};
673
+ if (transcript) Object.assign(out, claudeFacts(transcript, session.id, hints, state));
674
+ const cwd = out.cwd ?? session.cwd;
675
+ Object.assign(out, gitFacts(cwd), out.branch ? { branch: out.branch } : {});
676
+ if (!out.url && typeof hints.desktop_id === "string") out.url = `claude://code/continue/${hints.desktop_id}`;
677
+ if (typeof hints.attended === "boolean") out.attended = hints.attended || !!out.url;
678
+ return out;
536
679
  },
680
+ remoteFacts: (session, local, transcript, state, env = process.env) => claudeRemoteFacts(session.id, local, transcript, state, env),
537
681
  backfill(sinceMs, env = process.env) {
538
682
  const rows2 = [];
539
- for (const data of dataDirs(env)) {
540
- let ids = [];
541
- try {
542
- ids = readdirSync2(path3.join(data, "brain")).filter((n) => /^[0-9a-f-]{36}$/i.test(n));
543
- } catch {
544
- }
545
- for (const id of ids) {
546
- const f = path3.join(data, "brain", id, ".system_generated", "logs", "transcript.jsonl");
547
- try {
548
- if (statSync2(f).mtimeMs < sinceMs) continue;
549
- } catch {
550
- continue;
551
- }
552
- const facts = antigravityFacts(f, { conversation: id, workspaces: [], env });
553
- if (!facts.started_at || !facts.last_at) continue;
554
- const { started_at, last_at, first_ask: _fa, harness_version: _hv, ...rest } = facts;
555
- rows2.push({ session: { id, ...rest }, started_at, last_at });
556
- }
683
+ const files = [];
684
+ for (const d of claudeDirs(env))
685
+ files.push(...recentFiles(path3.join(d, "projects"), 1, sinceMs, (n) => /^[0-9a-f-]{36}\.jsonl$/.test(n)));
686
+ const noState = { get: () => ({}), save: () => {
687
+ } };
688
+ for (const f of files) {
689
+ const id = path3.basename(f, ".jsonl");
690
+ const facts = claudeFacts(f, id, {}, noState);
691
+ if (!facts.started_at || !facts.last_at) continue;
692
+ const { started_at, last_at, first_ask: _fa, harness_version: _hv, ...rest } = facts;
693
+ const cwd = rest.cwd;
694
+ rows2.push({
695
+ session: { id, ...rest, ...gitFacts(cwd), ...rest.branch ? { branch: rest.branch } : {} },
696
+ started_at,
697
+ last_at,
698
+ transcript: f
699
+ });
557
700
  }
558
701
  return rows2;
559
702
  }
560
703
  };
704
+ var WAITER_RE = /(?:^|[\s"'`/\\])sessionpipe(?:\.\w+)?["']?\s+wait(?:\s|$)/;
705
+ function backgroundCount(tasks, ending) {
706
+ if (!Array.isArray(tasks)) return void 0;
707
+ return tasks.filter((t) => {
708
+ const task = t;
709
+ if (ending !== void 0 && task?.id === ending) return false;
710
+ return !(typeof task?.command === "string" && WAITER_RE.test(task.command));
711
+ }).length;
712
+ }
713
+ var LINE_REFRESH_S = 60;
714
+ function statusLine(file, want, cmd) {
715
+ const f = readJsonFile(file);
716
+ if (!f.ok) return null;
717
+ const cur = f.value.statusLine;
718
+ const ours = typeof cur?.command === "string" && OURS.test(cur.command);
719
+ const command = cmd?.([NAME, "StatusLine"]).command;
720
+ if (want && cur && !ours)
721
+ return { file, changed: false, skipped: true, note: "a status line of your own is set; limits are not read" };
722
+ if (want ? ours && cur?.command === command && cur?.refreshInterval === LINE_REFRESH_S : !ours) return null;
723
+ if (want) f.value.statusLine = { type: "command", command, refreshInterval: LINE_REFRESH_S };
724
+ else delete f.value.statusLine;
725
+ writeFileSync2(file, formatJson(f.value, f));
726
+ return { file, changed: true, note: want ? "status line written (account limits)" : "status line removed" };
727
+ }
728
+ var epochIso = (t) => typeof t === "number" && t > 0 && t < 1e11 ? new Date(t * 1e3).toISOString() : void 0;
729
+ var WINDOW_MINUTES = { five_hour: 300, seven_day: 10080 };
730
+ function claudeLimits(rl) {
731
+ if (!rl || typeof rl !== "object" || Array.isArray(rl)) return null;
732
+ const windows = [];
733
+ for (const [id, w] of Object.entries(rl)) {
734
+ if (!/^[a-z0-9_]{1,64}$/.test(id) || typeof w?.used_percentage !== "number" || w.used_percentage < 0) continue;
735
+ windows.push(
736
+ prune({
737
+ id,
738
+ used_percent: w.used_percentage,
739
+ window_minutes: WINDOW_MINUTES[id],
740
+ resets_at: epochIso(w.resets_at)
741
+ })
742
+ );
743
+ }
744
+ return windows.length ? { windows: windows.slice(0, 8) } : null;
745
+ }
561
746
  function prune(o) {
562
747
  for (const k of Object.keys(o)) if (o[k] === void 0) delete o[k];
563
748
  return o;
564
749
  }
565
- function agPath(p) {
566
- if (typeof p !== "string" || !p) return null;
567
- let s = p;
568
- if (/^file:/i.test(s)) {
569
- try {
570
- s = decodeURIComponent(new URL(s).pathname);
571
- } catch {
572
- return null;
573
- }
574
- if (process.platform === "win32" && /^\/[A-Za-z]:/.test(s)) s = s.slice(1);
575
- } else if (/^[a-z][a-z0-9+.-]*:\/\//i.test(s)) return null;
576
- if (!path3.isAbsolute(s)) return null;
577
- return path3.resolve(s);
750
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
751
+ function notClaudeCode(id, s) {
752
+ return typeof s.transcript_path !== "string" && !UUID.test(id);
578
753
  }
579
- var agWorkspaces = (paths) => (Array.isArray(paths) ? paths : []).map(agPath).filter((p) => !!p).filter((p, i, a) => a.indexOf(p) === i);
580
- function agVariants(p) {
581
- const out = [p];
582
- try {
583
- const r = realpathSync.native(p);
584
- if (r !== p) out.push(r);
585
- } catch {
754
+ function pickSource(v) {
755
+ return typeof v === "string" && ["startup", "resume", "clear", "compact", "fork"].includes(v) ? v : "unknown";
756
+ }
757
+ var harnessWord = (v) => typeof v === "string" && /^[A-Za-z0-9][A-Za-z0-9_.-]{0,39}$/.test(v) ? v : void 0;
758
+ var WRAPPER = /^<(?:task-notification|cross-session-message|system-reminder|local-command-stdout|command-[a-z-]+)[\s/>]/i;
759
+ var injectedBlock = (prompt) => WRAPPER.test(prompt.trim());
760
+ function errText(v) {
761
+ if (typeof v === "string") return v.slice(0, 2e3);
762
+ if (v && typeof v === "object") {
763
+ const e = v;
764
+ if (typeof e.error === "string") return e.error.slice(0, 2e3);
765
+ if (typeof e.message === "string") return e.message.slice(0, 2e3);
586
766
  }
587
- return out;
767
+ return void 0;
588
768
  }
589
- function agProjects(dir) {
590
- const out = [];
769
+ function describe(input) {
770
+ if (!input || typeof input !== "object") return "";
771
+ const i = input;
772
+ const v = i.command ?? i.file_path ?? i.path ?? i.url ?? i.pattern;
773
+ return typeof v === "string" ? ` ${v.slice(0, 120)}` : "";
774
+ }
775
+ var NAME_RANK = {
776
+ user: "custom",
777
+ auto: "harness",
778
+ hook: "harness",
779
+ peer: "harness"
780
+ };
781
+ function registryEntry(j) {
782
+ if (!j || typeof j !== "object") return null;
783
+ const r = j;
784
+ const source = r.nameSource ? NAME_RANK[r.nameSource] : void 0;
785
+ return {
786
+ sessionId: r.sessionId ? String(r.sessionId) : null,
787
+ name: source && typeof r.name === "string" && r.name.trim() ? r.name.trim() : null,
788
+ name_source: source ?? null,
789
+ bridge: typeof r.bridgeSessionId === "string" ? r.bridgeSessionId : null
790
+ };
791
+ }
792
+ var liveCache = /* @__PURE__ */ new Map();
793
+ function claudeLive(dir) {
794
+ const hit = liveCache.get(dir);
795
+ if (hit) return hit;
796
+ const map = /* @__PURE__ */ new Map();
591
797
  let names = [];
592
798
  try {
593
- names = readdirSync2(dir).filter((n) => n.endsWith(".json"));
799
+ names = readdirSync2(path3.join(dir, "sessions")).filter((n) => n.endsWith(".json"));
594
800
  } catch {
595
- return out;
596
801
  }
597
802
  for (const n of names) {
598
- const id = n.slice(0, -5);
599
- if (!ID_RE.test(id)) continue;
600
- let j;
601
803
  try {
602
- j = JSON.parse(readFileSync2(path3.join(dir, n), "utf8"));
804
+ const e = registryEntry(JSON.parse(readFileSync2(path3.join(dir, "sessions", n), "utf8")));
805
+ if (e?.sessionId) map.set(e.sessionId, e);
603
806
  } catch {
604
- continue;
605
- }
606
- const folders = [];
607
- for (const r of j?.projectResources?.resources ?? []) {
608
- const f = agPath(r?.folderUri ?? r?.gitFolder?.folderUri);
609
- if (f) folders.push(...agVariants(f));
610
807
  }
611
- if (folders.length) out.push({ id, folders });
612
808
  }
613
- return out;
809
+ liveCache.set(dir, map);
810
+ return map;
614
811
  }
615
- function agMatchProject(projects, workspaces) {
616
- const ws = [];
617
- for (const w of workspaces) {
618
- const p = agPath(w);
619
- if (p) ws.push(...agVariants(p));
812
+ function claudeLiveByPid(dir, pid) {
813
+ if (!pid) return null;
814
+ try {
815
+ return registryEntry(JSON.parse(readFileSync2(path3.join(dir, "sessions", `${pid}.json`), "utf8")));
816
+ } catch {
817
+ return null;
620
818
  }
621
- let best = null;
622
- let bestLen = -1;
623
- for (const pr of projects)
624
- for (const f of pr.folders) {
625
- const within = ws.some((w) => w === f || w.startsWith(f.endsWith(path3.sep) ? f : f + path3.sep));
626
- if (within && f.length > bestLen) {
627
- best = pr.id;
628
- bestLen = f.length;
819
+ }
820
+ function claudePid(env = process.env) {
821
+ try {
822
+ const dirs = claudeDirs(env);
823
+ const has = (pid2) => dirs.some((d) => existsSync2(path3.join(d, "sessions", `${pid2}.json`)));
824
+ let pid = process.ppid;
825
+ if (has(pid)) return pid;
826
+ const parent = parentOf();
827
+ for (let hop = 0; hop < 6 && pid > 1; hop++) {
828
+ pid = parent(pid);
829
+ if (pid > 1 && has(pid)) return pid;
830
+ }
831
+ } catch {
832
+ }
833
+ return null;
834
+ }
835
+ function parentOf() {
836
+ if (process.platform === "linux") {
837
+ return (pid) => {
838
+ try {
839
+ const st = readFileSync2(`/proc/${pid}/stat`, "utf8");
840
+ return Number(st.slice(st.lastIndexOf(")") + 2).split(" ")[1]) || 0;
841
+ } catch {
842
+ return 0;
629
843
  }
844
+ };
845
+ }
846
+ const table = /* @__PURE__ */ new Map();
847
+ try {
848
+ const out = execFileSync2("ps", ["-axo", "pid=,ppid="], {
849
+ encoding: "utf8",
850
+ timeout: 2e3,
851
+ maxBuffer: 8 * 1024 * 1024,
852
+ stdio: ["ignore", "pipe", "ignore"]
853
+ });
854
+ for (const l of out.split("\n")) {
855
+ const m = /^\s*(\d+)\s+(\d+)/.exec(l);
856
+ if (m) table.set(Number(m[1]), Number(m[2]));
630
857
  }
631
- return best;
858
+ } catch {
859
+ }
860
+ return (pid) => table.get(pid) || 0;
632
861
  }
633
- function antigravityRemoteUrl(transcript, opts) {
634
- const i = transcript.indexOf(`${path3.sep}brain${path3.sep}`);
635
- const dirs = (i > 0 ? [transcript.slice(0, i)] : []).concat(dataDirs(opts.env));
636
- for (const d of dirs) {
862
+ var EMAIL_RE = /^[^@\s]{1,64}@[^@\s]{1,189}$/;
863
+ function claudeLogin(dir, home = homeDir()) {
864
+ const files = [path3.join(dir, ".claude.json")];
865
+ if (path3.resolve(dir) === path3.join(home, ".claude")) files.unshift(path3.join(home, ".claude.json"));
866
+ for (const f of files) {
637
867
  try {
638
- const m = /installation_uuid:\s*"([0-9a-f-]{36})"/i.exec(
639
- readFileSync2(path3.join(d, "antigravity_state.pbtxt"), "utf8")
640
- );
641
- if (!m) continue;
642
- const base = `https://antigravity.google.com/r/${m[1]}-v2`;
643
- const conv = opts.conversation ?? "";
644
- if (!ID_RE.test(conv) || !opts.workspaces?.length) return base;
645
- const project = agMatchProject(agProjects(opts.projectsDir ?? projectsDir(opts.env)), opts.workspaces);
646
- return project ? `${base}?p=${encodeURIComponent(`c/${conv}?section=${project}`)}` : base;
868
+ const a = JSON.parse(readFileSync2(f, "utf8")).oauthAccount;
869
+ if (typeof a?.accountUuid === "string" && a.accountUuid) {
870
+ const email = typeof a.emailAddress === "string" && EMAIL_RE.test(a.emailAddress) ? a.emailAddress : void 0;
871
+ return { id: a.accountUuid.slice(0, 128), ...email ? { email } : {} };
872
+ }
647
873
  } catch {
648
874
  }
649
875
  }
650
- return null;
876
+ return apiAccount(dir);
651
877
  }
652
- function antigravityFacts(file, opts) {
653
- const { head, tail } = ends(file, 128 * 1024, 64 * 1024);
878
+ var ANTHROPIC_HOST = /(^|\.)anthropic\.com$/i;
879
+ function apiAccount(dir) {
880
+ const base = readJson(settingsIn(dir)).env?.ANTHROPIC_BASE_URL;
881
+ if (typeof base !== "string") return null;
882
+ let u;
883
+ try {
884
+ u = new URL(base.trim());
885
+ } catch {
886
+ return null;
887
+ }
888
+ if (u.protocol !== "https:" && u.protocol !== "http:" || !u.hostname || ANTHROPIC_HOST.test(u.hostname))
889
+ return null;
890
+ const origin = `${u.protocol}//${u.host}`;
891
+ return { id: `api-${createHash("sha256").update(origin).digest("hex").slice(0, 16)}`, name: u.host };
892
+ }
893
+ var accountCache = /* @__PURE__ */ new Map();
894
+ function claudeAccount(dir) {
895
+ const hit = accountCache.get(dir);
896
+ if (hit !== void 0) return hit;
897
+ const acct = claudeLogin(dir)?.id ?? null;
898
+ accountCache.set(dir, acct);
899
+ return acct;
900
+ }
901
+ var LINK_RE = /^https:\/\/claude\.ai\/code\/session_[A-Za-z0-9]+$/;
902
+ function transcriptLink(records) {
903
+ for (let i = records.length - 1; i >= 0; i--) {
904
+ const j = records[i];
905
+ if (j.type === "system" && j.subtype === "bridge_status" && typeof j.url === "string" && LINK_RE.test(j.url))
906
+ return j.url;
907
+ if (j.type === "bridge-session" && typeof j.bridgeSessionId === "string") {
908
+ const m = /^(?:cse|session)_([A-Za-z0-9]+)$/.exec(j.bridgeSessionId);
909
+ if (m) return `https://claude.ai/code/session_${m[1]}`;
910
+ }
911
+ }
912
+ return null;
913
+ }
914
+ function appTitle(text) {
915
+ const t = String(text).replace(/\s+/g, " ").trim();
916
+ return t.length > 50 ? `${t.slice(0, 50)}...` : t;
917
+ }
918
+ function askText(j) {
919
+ if (!j || j.type !== "user" || j.isMeta || j.isSidechain) return null;
920
+ const origin = j.origin;
921
+ if (origin?.kind && origin.kind !== "human") return null;
922
+ const c = j.message?.content;
923
+ 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();
924
+ if (!text || isInjected(text)) return null;
925
+ return text;
926
+ }
927
+ function firstAsk(records) {
928
+ for (const j of records) {
929
+ const t = askText(j);
930
+ if (t) return appTitle(t);
931
+ }
932
+ return null;
933
+ }
934
+ function scanTitles(file, from, found) {
935
+ const out = { custom: found.custom ?? null, ai: found.ai ?? null, upto: from };
936
+ let size = 0;
937
+ try {
938
+ size = statSync2(file).size;
939
+ } catch {
940
+ return out;
941
+ }
942
+ if (size < from) {
943
+ out.upto = 0;
944
+ out.custom = out.ai = null;
945
+ from = 0;
946
+ }
947
+ let pos = 0;
948
+ scanLines(
949
+ file,
950
+ (l) => {
951
+ pos += Buffer.byteLength(l, "utf8") + 1;
952
+ if (pos <= from || l.indexOf('-title"') < 0) return null;
953
+ try {
954
+ const j = JSON.parse(l);
955
+ if (j.type === "custom-title" && j.customTitle) out.custom = j.customTitle;
956
+ if (j.type === "ai-title" && j.aiTitle) out.ai = j.aiTitle;
957
+ } catch {
958
+ }
959
+ return null;
960
+ },
961
+ 512 * 1024 * 1024
962
+ );
963
+ out.upto = size;
964
+ return out;
965
+ }
966
+ function claudeFacts(file, sessionId, hints, state) {
967
+ const { head, tail } = ends(file, 64 * 1024, 256 * 1024);
654
968
  const H = parseLines(head);
655
969
  const T = parseLines(tail);
656
970
  const out = {};
657
- const url = antigravityRemoteUrl(file, opts);
658
- if (url) out.url = url;
659
- for (const j of H) {
660
- if (j.type !== "USER_INPUT" || typeof j.content !== "string") continue;
661
- const m = /<USER_REQUEST>([\s\S]*?)(?:<\/USER_REQUEST>|$)/.exec(j.content);
662
- const text = (m ? m[1] : j.content).trim();
663
- const line = (text.split("\n").find((l) => l.trim()) ?? "").trim();
664
- if (!line || /^<[a-z_-]+>/i.test(line)) continue;
665
- out.title = line.length > 80 ? `${line.slice(0, 80).replace(/\s+\S*$/, "")}\u2026` : line;
971
+ const id = sessionId || path3.basename(file, ".jsonl");
972
+ const mem = state.get(NAME, id);
973
+ let custom = null;
974
+ let ai = null;
975
+ for (const j of H.concat(T)) {
976
+ if (j.type === "custom-title" && typeof j.customTitle === "string") custom = j.customTitle;
977
+ if (j.type === "ai-title" && typeof j.aiTitle === "string") ai = j.aiTitle;
978
+ }
979
+ let size = 0;
980
+ try {
981
+ size = statSync2(file).size;
982
+ } catch {
983
+ }
984
+ if (!custom && !ai && size > 64 * 1024 + 256 * 1024) {
985
+ const s = scanTitles(file, Number(mem.scan_upto) || 0, {
986
+ custom: mem.scan_custom,
987
+ ai: mem.scan_ai
988
+ });
989
+ custom = s.custom;
990
+ ai = s.ai;
991
+ state.save(NAME, id, { scan_upto: s.upto, scan_custom: s.custom, scan_ai: s.ai });
992
+ }
993
+ const dir = configDirOf(file);
994
+ const live = claudeLive(dir).get(id) ?? claudeLiveByPid(dir, hints.claude_pid);
995
+ if (live?.name) state.save(NAME, id, { name: live.name, name_source: live.name_source });
996
+ const bridge = live?.bridge && /^session_[A-Za-z0-9]+$/.test(live.bridge) ? `https://claude.ai/code/${live.bridge}` : null;
997
+ const url = bridge ?? transcriptLink(T) ?? transcriptLink(H) ?? (typeof mem.url === "string" ? mem.url : null);
998
+ if (url) {
999
+ out.url = url;
1000
+ state.save(NAME, id, { url });
1001
+ }
1002
+ const name = live?.name ? { title: live.name, source: live.name_source } : typeof mem.name === "string" ? { title: mem.name, source: mem.name_source } : null;
1003
+ const pick = name && name.source === "custom" && name || custom && { title: custom, source: "custom" } || name || ai && { title: ai, source: "harness" };
1004
+ let ask = typeof mem.first_ask === "string" ? mem.first_ask : firstAsk(H);
1005
+ if (!ask && size > 64 * 1024 && !mem.first_ask_none) {
1006
+ ask = scanLines(file, (l) => {
1007
+ try {
1008
+ const t = askText(JSON.parse(l));
1009
+ return t ? appTitle(t) : null;
1010
+ } catch {
1011
+ return null;
1012
+ }
1013
+ });
1014
+ if (!ask && size > 16 * 1024 * 1024) state.save(NAME, id, { first_ask_none: true });
1015
+ }
1016
+ if (ask) {
1017
+ out.first_ask = ask;
1018
+ if (!mem.first_ask) state.save(NAME, id, { first_ask: ask });
1019
+ }
1020
+ if (pick) {
1021
+ out.title = String(pick.title).slice(0, 120);
1022
+ out.title_source = pick.source;
1023
+ } else if (ask) {
1024
+ out.title = ask;
666
1025
  out.title_source = "first-ask";
667
- break;
668
1026
  }
669
- const times = H.concat(T).map((j) => Date.parse(String(j.created_at ?? j.timestamp ?? ""))).filter((n) => n > 0);
1027
+ const acct = claudeAccount(dir);
1028
+ if (acct) out.account_id = acct;
1029
+ for (let i = T.length - 1; i >= 0; i--) {
1030
+ const m = T[i].message?.model;
1031
+ if (T[i]?.type === "assistant" && m && m !== "<synthetic>") {
1032
+ out.model = m;
1033
+ break;
1034
+ }
1035
+ }
1036
+ const meta = [...T].reverse().find((j) => j.version) ?? H.find((j) => j.version);
1037
+ if (meta?.version) out.harness_version = String(meta.version);
1038
+ const times = H.concat(T).map((j) => Date.parse(String(j.timestamp ?? ""))).filter((n) => n > 0);
670
1039
  if (times.length) {
671
1040
  out.started_at = Math.min(...times);
672
1041
  out.last_at = Math.max(...times);
673
1042
  }
1043
+ const withCwd = H.find((j) => typeof j.cwd === "string");
1044
+ if (withCwd) {
1045
+ out.cwd = withCwd.cwd;
1046
+ if (typeof withCwd.gitBranch === "string" && withCwd.gitBranch !== "HEAD") out.branch = withCwd.gitBranch;
1047
+ }
674
1048
  return out;
675
1049
  }
676
-
677
- // ../core/src/adapters/claude-code.ts
678
- import { execFileSync as execFileSync2 } from "node:child_process";
679
- import { createHash } from "node:crypto";
680
- import { existsSync as existsSync3, readdirSync as readdirSync3, readFileSync as readFileSync3, statSync as statSync3 } from "node:fs";
681
- import path4 from "node:path";
682
-
683
- // ../core/src/readers/git.ts
684
- import { execFileSync } from "node:child_process";
685
- var cache = /* @__PURE__ */ new Map();
686
- function gitFacts(cwd) {
687
- if (!cwd) return {};
688
- const hit = cache.get(cwd);
689
- if (hit) return hit;
690
- const run = (args) => {
1050
+ var REMOTE_RETRY_MS = 6e4;
1051
+ var REMOTE_REFRESH_MS = 30 * 6e4;
1052
+ var CLAUDE_API = "https://api.anthropic.com";
1053
+ var KEYCHAIN = "Claude Code-credentials";
1054
+ var REMOTE_TIMEOUT_MS = 3e3;
1055
+ var KEYCHAIN_TIMEOUT_MS = 2e3;
1056
+ var REMOTE_MAX_BYTES = 1 << 20;
1057
+ var cleanName = (s) => s.replace(/[\p{Cc}\u061C\u200E\u200F\u202A-\u202E\u2066-\u2069]/gu, " ").replace(/\s+/g, " ").trim();
1058
+ async function claudeRemoteFacts(id, local, transcript, state, env, now = Date.now()) {
1059
+ if (!transcript || !local.url || !LINK_RE.test(local.url) || local.title_source === "custom") return {};
1060
+ const mem = state.get(NAME, id);
1061
+ let title = typeof mem.remote_title === "string" ? mem.remote_title : null;
1062
+ if (now - (Number(mem.remote_at) || 0) >= (title ? REMOTE_REFRESH_MS : REMOTE_RETRY_MS)) {
1063
+ state.save(NAME, id, { remote_at: now });
1064
+ const token = claudeAiToken(configDirOf(transcript), env, now);
1065
+ const got = token ? await claudeAiTitle(local.url.slice(local.url.lastIndexOf("/") + 1), token) : void 0;
1066
+ if (got !== void 0) {
1067
+ title = got?.slice(0, 200) ?? null;
1068
+ state.save(NAME, id, { remote_title: title });
1069
+ }
1070
+ }
1071
+ return title ? { title: title.slice(0, 120), title_source: "harness" } : {};
1072
+ }
1073
+ function keychainServices(dir, env) {
1074
+ const named = (p) => `${KEYCHAIN}-${createHash("sha256").update(p.normalize("NFC")).digest("hex").slice(0, 8)}`;
1075
+ const out = /* @__PURE__ */ new Set();
1076
+ if (env.CLAUDE_CONFIG_DIR && path3.resolve(env.CLAUDE_CONFIG_DIR) === dir) out.add(named(env.CLAUDE_CONFIG_DIR));
1077
+ if (dir === path3.join(env.HOME || homeDir(), ".claude")) out.add(KEYCHAIN);
1078
+ out.add(named(dir));
1079
+ return [...out];
1080
+ }
1081
+ var tokenCache = /* @__PURE__ */ new Map();
1082
+ function claudeAiToken(dir, env, now) {
1083
+ const hit = tokenCache.get(dir);
1084
+ if (hit && hit.expires > now + 6e4) return hit.token;
1085
+ const raw = [];
1086
+ if (process.platform === "darwin")
1087
+ for (const service of keychainServices(dir, env)) {
1088
+ try {
1089
+ const user = env.USER || userInfo().username;
1090
+ const account = /^[\w.-]+$/.test(user) ? user : "claude-code-user";
1091
+ raw.push(
1092
+ execFileSync2("security", ["find-generic-password", "-a", account, "-w", "-s", service], {
1093
+ encoding: "utf8",
1094
+ timeout: KEYCHAIN_TIMEOUT_MS,
1095
+ stdio: ["ignore", "pipe", "ignore"]
1096
+ })
1097
+ );
1098
+ break;
1099
+ } catch {
1100
+ }
1101
+ }
1102
+ try {
1103
+ raw.push(readFileSync2(path3.join(dir, ".credentials.json"), "utf8"));
1104
+ } catch {
1105
+ }
1106
+ for (const r of raw) {
691
1107
  try {
692
- return execFileSync("git", ["-C", cwd, ...args], {
693
- encoding: "utf8",
694
- timeout: 1500,
695
- stdio: ["ignore", "pipe", "ignore"]
696
- }).trim();
1108
+ const t = r.trim();
1109
+ const o = JSON.parse(/^(?:[0-9a-f]{2})+$/i.test(t) ? Buffer.from(t, "hex").toString("utf8") : t).claudeAiOauth;
1110
+ if (typeof o?.accessToken !== "string" || !o.accessToken) continue;
1111
+ const expires = typeof o.expiresAt === "number" ? o.expiresAt : Number.POSITIVE_INFINITY;
1112
+ if (expires <= now + 6e4) continue;
1113
+ tokenCache.set(dir, { token: o.accessToken, expires });
1114
+ return o.accessToken;
697
1115
  } catch {
698
- return "";
699
1116
  }
700
- };
701
- const out = {};
702
- const repo = run(["remote", "get-url", "origin"]);
703
- if (repo) out.repo = stripCredentials(repo);
704
- const branch = run(["rev-parse", "--abbrev-ref", "HEAD"]);
705
- if (branch && branch !== "HEAD") out.branch = branch;
706
- cache.set(cwd, out);
707
- return out;
1117
+ }
1118
+ return null;
708
1119
  }
709
- function stripCredentials(url) {
710
- const ssh = /^(?:ssh:\/\/)?git@([^:/]+)[:/](.+?)(?:\.git)?$/.exec(url);
711
- if (ssh) return `${ssh[1]}/${ssh[2]}`;
712
- return url.replace(/\/\/[^/@]*@/, "//").replace(/^https?:\/\//, "").replace(/\.git$/, "");
1120
+ async function claudeAiTitle(bridge, token) {
1121
+ try {
1122
+ const r = await fetch(`${CLAUDE_API}/v1/code/sessions/${bridge}`, {
1123
+ headers: { Authorization: `Bearer ${token}`, "anthropic-version": "2023-06-01", "User-Agent": "sessionpipe" },
1124
+ redirect: "error",
1125
+ signal: AbortSignal.timeout(REMOTE_TIMEOUT_MS)
1126
+ });
1127
+ if (r.status !== 200 || Number(r.headers.get("content-length")) > REMOTE_MAX_BYTES) return void 0;
1128
+ const text = await r.text();
1129
+ if (text.length > REMOTE_MAX_BYTES) return void 0;
1130
+ const j = JSON.parse(text);
1131
+ const s = j?.response_shape ?? j?.session ?? j;
1132
+ const bare = (x) => x.replace(/^(?:session|cse)_/, "");
1133
+ if (typeof s?.id !== "string" || bare(s.id) !== bare(bridge)) return void 0;
1134
+ return typeof s.title === "string" && cleanName(s.title) || null;
1135
+ } catch {
1136
+ return void 0;
1137
+ }
713
1138
  }
714
1139
 
715
- // ../core/src/adapters/claude-code.ts
716
- var NAME2 = "claude-code";
717
- var CLAUDE_EVENTS = [
718
- "SessionStart",
719
- "UserPromptSubmit",
720
- "PreToolUse",
721
- "PostToolUse",
722
- "PostToolUseFailure",
723
- "PermissionRequest",
724
- "Notification",
725
- "Stop",
726
- "SubagentStart",
727
- "SubagentStop",
728
- "PreCompact",
729
- "PostCompact",
730
- "SessionEnd",
731
- "StopFailure"
732
- ];
733
- var LEAN_SKIP = /* @__PURE__ */ new Set(["PreToolUse", "PostToolUseFailure"]);
734
- var eventsFor = (lean) => lean ? CLAUDE_EVENTS.filter((e) => !LEAN_SKIP.has(e)) : [...CLAUDE_EVENTS];
735
- var OPTS = {
736
- matcherFor: (e) => /Tool|Permission/.test(e) ? "" : void 0,
737
- timeout: 5,
738
- // The hook waits up to 125 s for a verified control answer, and only when control
739
- // is paired on this machine; everywhere else it exits in milliseconds as before.
740
- timeoutFor: (e) => e === "PermissionRequest" ? 130 : void 0
741
- };
742
- var DESKTOP_ID = /^local_[A-Za-z0-9-]{1,64}$/;
743
- function claudeDirs(env = process.env, home = env.HOME || homeDir()) {
744
- const out = [];
745
- const add = (d) => {
746
- if (!d) return;
747
- const r = path4.resolve(d);
748
- if (!out.includes(r)) out.push(r);
749
- };
750
- add(path4.join(home, ".claude"));
751
- add(env.CLAUDE_CONFIG_DIR);
1140
+ // ../core/src/adapters/headless.ts
1141
+ import { accessSync, constants, readFileSync as readFileSync3, statSync as statSync3 } from "node:fs";
1142
+ import path4 from "node:path";
1143
+ var JOB_TIMEOUT_MS = 30 * 6e4;
1144
+ var OUT_CAP = 1024 * 1024;
1145
+ function isExe(p) {
752
1146
  try {
753
- for (const e of readdirSync3(home, { withFileTypes: true })) {
754
- if (!e.isDirectory() || !/^\.claude[-_.]/.test(e.name)) continue;
755
- const d = path4.join(home, e.name);
756
- if (existsSync3(path4.join(d, "projects")) || existsSync3(path4.join(d, "settings.json"))) add(d);
757
- }
1147
+ if (!statSync3(p).isFile()) return false;
1148
+ accessSync(p, constants.X_OK);
1149
+ return true;
1150
+ } catch {
1151
+ return false;
1152
+ }
1153
+ }
1154
+ function findBinary(o) {
1155
+ if (o.override && isExe(o.override)) return o.override;
1156
+ for (const d of String(o.env?.PATH || "").split(path4.delimiter)) {
1157
+ if (!d || !path4.isAbsolute(d)) continue;
1158
+ for (const n of o.names) if (isExe(path4.join(d, n))) return path4.join(d, n);
1159
+ }
1160
+ for (const d of o.dirs) for (const n of o.names) if (isExe(path4.join(d, n))) return path4.join(d, n);
1161
+ return null;
1162
+ }
1163
+ function shimTarget(shim) {
1164
+ let text = "";
1165
+ try {
1166
+ text = readFileSync3(shim, "utf8");
1167
+ } catch {
1168
+ return null;
1169
+ }
1170
+ const m = /"%(?:~dp0|dp0%)\\([^"]+)"\s+%\*/.exec(text);
1171
+ if (!m) return null;
1172
+ const target = path4.resolve(path4.dirname(shim), m[1].split("\\").join(path4.sep));
1173
+ try {
1174
+ return statSync3(target).isFile() ? target : null;
758
1175
  } catch {
1176
+ return null;
759
1177
  }
760
- return out.filter((d, i) => i === 0 || existsSync3(d));
761
1178
  }
762
- var settingsIn = (dir) => path4.join(dir, "settings.json");
763
- var configDirOf = (transcript) => path4.dirname(path4.dirname(path4.dirname(transcript)));
1179
+ function spawnable(bin, args) {
1180
+ if (!/\.(cmd|bat)$/i.test(bin)) return { file: bin, args };
1181
+ const target = shimTarget(bin);
1182
+ return target ? { file: process.execPath, args: [target, ...args] } : null;
1183
+ }
1184
+ function probe(bin, args) {
1185
+ const s = spawnable(bin, args);
1186
+ return s ? { ...s, shell: false } : { file: [`"${bin}"`, ...args].join(" "), args: [], shell: true };
1187
+ }
1188
+
1189
+ // ../core/src/adapters/antigravity.ts
1190
+ import {
1191
+ existsSync as existsSync3,
1192
+ mkdirSync as mkdirSync2,
1193
+ readdirSync as readdirSync3,
1194
+ readFileSync as readFileSync4,
1195
+ realpathSync,
1196
+ statSync as statSync4,
1197
+ unlinkSync as unlinkSync2,
1198
+ writeFileSync as writeFileSync3
1199
+ } from "node:fs";
1200
+ import path5 from "node:path";
1201
+ var NAME2 = "antigravity";
1202
+ var HOOK_NAME = "sessionpipe";
1203
+ var AG_EVENTS = ["PreInvocation", "PostInvocation", "PostToolUse", "Stop"];
1204
+ 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';
1205
+ var antigravityAnswer = (event) => event === "PreToolUse" ? AG_STALE_PRETOOL : "{}\n";
1206
+ var gemini = (env) => env.GEMINI_CLI_HOME || path5.join(homeDir(), ".gemini");
1207
+ var hooksFile = (env) => path5.join(gemini(env), "config", "hooks.json");
1208
+ var projectsDir = (env) => path5.join(gemini(env), "config", "projects");
1209
+ var dataDirs = (env) => ["antigravity", "antigravity-cli", "antigravity-ide"].map((d) => path5.join(gemini(env), d));
1210
+ var ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
764
1211
  var validTool2 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
765
- var endReason = (r) => r === "clear" || r === "logout" || r === "exit" ? r : r === "prompt_input_exit" ? "exit" : "other";
766
- var notifKind = (t) => t === "permission_prompt" ? "permission" : t === "idle_prompt" ? "idle" : t === "elicitation_dialog" ? "elicitation" : "question";
767
- var claudeCode = {
1212
+ function entry(cmd) {
1213
+ const handler = (e) => ({ type: "command", command: cmd([NAME2, e]).command, timeout: 5 });
1214
+ const out = { enabled: true };
1215
+ for (const e of AG_EVENTS) out[e] = /Tool/.test(e) ? [{ matcher: "*", hooks: [handler(e)] }] : [handler(e)];
1216
+ return out;
1217
+ }
1218
+ var antigravity = {
768
1219
  name: NAME2,
769
- events: CLAUDE_EVENTS,
770
- detect: (env = process.env) => claudeDirs(env).some((d) => existsSync3(d)),
771
- configFiles: (env = process.env) => claudeDirs(env).map(settingsIn),
772
- install: (cmd, env = process.env, opts = {}) => claudeDirs(env).map((d) => installClaudeShaped(settingsIn(d), NAME2, eventsFor(opts.lean), cmd, OPTS)),
773
- uninstall: (env = process.env, opts = {}) => claudeDirs(env).map(
774
- (d) => uninstallClaudeShaped(settingsIn(d), CLAUDE_EVENTS, { created: opts.created?.includes(settingsIn(d)) })
775
- ),
776
- installed: (cmd, env = process.env, opts = {}) => claudeDirs(env).map((d) => ({
777
- file: settingsIn(d),
778
- state: installedClaudeShaped(settingsIn(d), NAME2, eventsFor(opts.lean), cmd, OPTS)
779
- })),
1220
+ events: AG_EVENTS,
1221
+ detect: (env = process.env) => existsSync3(path5.join(gemini(env), "config")) || dataDirs(env).some((d) => existsSync3(d)),
1222
+ configFiles: (env = process.env) => [hooksFile(env)],
1223
+ install(cmd, env = process.env) {
1224
+ const file = hooksFile(env);
1225
+ const f = readJsonFile(file);
1226
+ if (!f.ok) return [{ file, changed: false, skipped: true, note: `${f.reason}; not touched` }];
1227
+ const hooks = f.value;
1228
+ const e = entry(cmd);
1229
+ if (JSON.stringify(hooks[HOOK_NAME]) === JSON.stringify(e)) return [{ file, changed: false }];
1230
+ hooks[HOOK_NAME] = e;
1231
+ mkdirSync2(path5.dirname(file), { recursive: true });
1232
+ writeFileSync3(file, formatJson(hooks, f));
1233
+ return [
1234
+ {
1235
+ file,
1236
+ changed: true,
1237
+ created: !f.existed,
1238
+ note: "hooks.json is read when a conversation starts; open ones report after a restart"
1239
+ }
1240
+ ];
1241
+ },
1242
+ uninstall(env = process.env, opts = {}) {
1243
+ const file = hooksFile(env);
1244
+ if (!existsSync3(file)) return [{ file, changed: false }];
1245
+ const f = readJsonFile(file);
1246
+ if (!f.ok) return [{ file, changed: false, skipped: true, note: `${f.reason}; not touched` }];
1247
+ const hooks = f.value;
1248
+ if (!(HOOK_NAME in hooks)) return [{ file, changed: false }];
1249
+ delete hooks[HOOK_NAME];
1250
+ if (opts.created?.includes(file) && !Object.keys(hooks).length) {
1251
+ unlinkSync2(file);
1252
+ return [{ file, changed: true, note: "removed (sessionpipe created it)" }];
1253
+ }
1254
+ writeFileSync3(file, formatJson(hooks, f));
1255
+ return [{ file, changed: true }];
1256
+ },
1257
+ installed(cmd, env = process.env) {
1258
+ const file = hooksFile(env);
1259
+ const f = readJsonFile(file);
1260
+ const have = f.ok ? f.value[HOOK_NAME] : void 0;
1261
+ return [
1262
+ {
1263
+ file,
1264
+ state: !have ? "missing" : JSON.stringify(have) === JSON.stringify(entry(cmd)) ? "current" : "stale"
1265
+ }
1266
+ ];
1267
+ },
780
1268
  fromHook(input) {
781
1269
  const event = input.argv[1] ?? "";
782
1270
  let s = {};
@@ -785,513 +1273,213 @@ var claudeCode = {
785
1273
  if (j && typeof j === "object" && !Array.isArray(j)) s = j;
786
1274
  } catch {
787
1275
  }
788
- const id = s.session_id ?? s.sessionId;
789
- if (!id) return null;
790
- if (notClaudeCode(String(id), s)) return null;
791
- const session = { id: String(id), cwd: typeof s.cwd === "string" ? s.cwd : input.cwd };
792
- if (typeof s.model === "string") session.model = s.model;
793
- if (typeof s.agent_id === "string" && event.startsWith("Subagent") === false && s.agent_id)
794
- session.parent_id = null;
795
- const turn_id = typeof s.prompt_id === "string" ? s.prompt_id : void 0;
796
- const tool = validTool2(s.tool_name) ? s.tool_name : void 0;
797
- const call_id = typeof s.tool_use_id === "string" ? s.tool_use_id : void 0;
798
- const ms = typeof s.duration_ms === "number" ? Math.round(s.duration_ms) : void 0;
799
- const agent_id = typeof s.agent_id === "string" ? s.agent_id : void 0;
800
- const permissionId = () => `perm-${createHash("sha256").update(`${turn_id ?? ""}|${tool ?? ""}|${JSON.stringify(s.tool_input ?? null)}`).digest("hex").slice(0, 16)}`;
1276
+ const id = s.conversationId ?? s.session_id ?? s.sessionId;
1277
+ const stdout = antigravityAnswer(event);
1278
+ if (!id) return { session: { id: "" }, events: [], stdout };
1279
+ const workspaces = agWorkspaces(s.workspacePaths);
1280
+ const session = { id: String(id), cwd: workspaces[0] ?? input.cwd };
1281
+ if (typeof s.modelName === "string" && s.modelName !== "auto") session.model = s.modelName;
1282
+ const toolCall = s.toolCall;
1283
+ const tool = validTool2(toolCall?.name) ? toolCall?.name : validTool2(s.toolName) ? s.toolName : void 0;
801
1284
  const ev = (type, data) => ({ type, data: prune2(data), harnessEvent: event });
802
1285
  const events = [];
803
1286
  switch (event) {
804
- case "SessionStart":
805
- events.push(ev("session.started", { source: pickSource(s.source ?? s.trigger) }));
1287
+ case "PreInvocation":
1288
+ if (Number(s.invocationNum) === 0 || s.invocationNum === void 0) {
1289
+ events.push(ev("session.started", { source: "unknown" }));
1290
+ events.push(ev("turn.started", {}));
1291
+ } else events.push(ev("session.heartbeat", {}));
806
1292
  break;
807
- case "UserPromptSubmit": {
808
- const prompt = typeof s.prompt === "string" ? s.prompt : void 0;
809
- events.push(
810
- ev("turn.started", {
811
- turn_id,
812
- prompt_chars: prompt === void 0 ? void 0 : [...prompt].length,
813
- // Claude Code's hook schema documents `source`
814
- // (user | sdk | system | loop_wakeup | schedule_wakeup | poll_event) and says a
815
- // payload may omit it while the field rolls out; 2.1.304's payload has no such
816
- // key at all. Forwarded verbatim, shape-guarded only: the list is the harness's
817
- // to extend.
818
- submitted_by: harnessWord(s.source),
819
- // One of Claude Code's own prompt wrappers: the prompt is a block the harness
820
- // wrote, so it is not the person's words. Absent is unknown, never "the person" —
821
- // and `true` is never "no person caused this turn", because a Talk or
822
- // `sessionpipe wait` delivery hands the person's own message over inside a
823
- // <task-notification> (PROTOCOL.md §4).
824
- injected: prompt !== void 0 && injectedBlock(prompt) ? true : void 0
825
- })
826
- );
1293
+ case "PostInvocation":
827
1294
  break;
828
- }
829
1295
  case "PreToolUse":
830
- if (tool) events.push(ev("tool.started", { tool, call_id, turn_id, agent_id, input: s.tool_input }));
1296
+ if (tool) events.push(ev("tool.started", { tool, input: toolCall?.args }));
831
1297
  break;
832
1298
  case "PostToolUse":
833
- if (tool)
834
- events.push(
835
- ev("tool.ended", {
836
- tool,
837
- call_id,
838
- turn_id,
839
- agent_id,
840
- ms,
841
- ok: true,
842
- input: s.tool_input,
843
- output: s.tool_response
844
- })
845
- );
846
- break;
847
- case "PostToolUseFailure":
848
- if (tool)
1299
+ if (!tool) events.push(ev("session.heartbeat", {}));
1300
+ else
849
1301
  events.push(
850
1302
  ev("tool.ended", {
851
1303
  tool,
852
- call_id,
853
- turn_id,
854
- agent_id,
855
- ms,
856
- ok: false,
857
- error: errText(s.error ?? s.tool_response),
858
- input: s.tool_input,
859
- output: s.tool_response
1304
+ ok: !s.error,
1305
+ error: typeof s.error === "string" ? s.error.slice(0, 2e3) : void 0,
1306
+ input: toolCall?.args
860
1307
  })
861
1308
  );
862
1309
  break;
863
- case "PermissionRequest":
864
- events.push(
865
- ev("attention.needed", {
866
- attention_id: call_id ?? permissionId(),
867
- kind: "permission",
868
- tool,
869
- message: tool ? `${tool}${describe(s.tool_input)}` : void 0
870
- })
871
- );
872
- break;
873
- case "Notification":
874
- events.push(
875
- ev("attention.needed", {
876
- attention_id: `notif-${Date.now()}`,
877
- kind: notifKind(s.notification_type),
878
- message: typeof s.message === "string" ? s.message.slice(0, 200) : void 0
879
- })
880
- );
881
- break;
882
1310
  case "Stop":
883
- events.push(
884
- ev("turn.ended", {
885
- turn_id,
886
- reason: "stop",
887
- background: Array.isArray(s.background_tasks) ? s.background_tasks.length : void 0
888
- })
889
- );
890
- break;
891
- case "StopFailure":
892
- events.push(
893
- ev("turn.ended", {
894
- turn_id,
895
- reason: "error",
896
- error: typeof s.error === "string" ? s.error.slice(0, 200) : void 0
897
- })
898
- );
899
- break;
900
- case "SubagentStart":
901
- events.push(
902
- ev("subagent.started", {
903
- agent_id: String(s.agent_id ?? ""),
904
- agent_type: typeof s.agent_type === "string" ? s.agent_type : void 0
905
- })
906
- );
907
- break;
908
- case "SubagentStop":
909
- events.push(
910
- ev("subagent.ended", {
911
- agent_id: String(s.agent_id ?? ""),
912
- agent_type: typeof s.agent_type === "string" ? s.agent_type : void 0
913
- })
914
- );
915
- break;
916
- case "PreCompact":
917
- events.push(ev("context.compacted", { trigger: s.trigger === "manual" ? "manual" : "auto" }));
918
- break;
919
- case "PostCompact":
920
- break;
921
- // PreCompact already said it; a second event would double-count
922
- case "SessionEnd":
923
- events.push(ev("session.ended", { reason: endReason(s.reason) }));
1311
+ events.push(ev("turn.ended", { reason: s.error ? "error" : "stop" }));
924
1312
  break;
925
1313
  default:
926
- return null;
1314
+ return { session, events: [], stdout };
927
1315
  }
928
- const hints = {};
929
- if (event === "UserPromptSubmit" && typeof s.prompt === "string") hints.prompt = s.prompt;
930
- const host = input.env.CLAUDE_CODE_HOST_SESSION_ID;
931
- if (host && DESKTOP_ID.test(host)) hints.desktop_id = host;
932
- const attended = input.env.CLAUDE_CODE_SESSION_ATTENDED;
933
- if (attended === "1" || attended === "0") hints.attended = attended === "1" || !!input.env.SESSIONPIPE_CONTROL_JOB;
934
- if (event !== "PreToolUse" && event !== "PostToolUse") hints.claude_pid = claudePid(input.env);
935
- const r = { session, events, hints };
936
- if (typeof s.transcript_path === "string") r.transcript = s.transcript_path;
1316
+ const r = { session, events, stdout, hints: { workspaces } };
1317
+ if (typeof s.transcriptPath === "string") r.transcript = s.transcriptPath;
937
1318
  return r;
938
1319
  },
939
1320
  readTranscript(file, fromLine) {
940
1321
  let text = "";
941
1322
  try {
942
- text = readFileSync3(file, "utf8");
1323
+ text = readFileSync4(file, "utf8");
943
1324
  } catch {
944
1325
  return { turns: [], line: fromLine };
945
1326
  }
946
- return readClaude(text.split("\n"), fromLine);
1327
+ return readAntigravity(text.split("\n"), fromLine);
947
1328
  },
948
- facts(session, transcript, hints, state) {
949
- const out = {};
950
- if (transcript) Object.assign(out, claudeFacts(transcript, session.id, hints, state));
951
- const cwd = out.cwd ?? session.cwd;
952
- Object.assign(out, gitFacts(cwd), out.branch ? { branch: out.branch } : {});
953
- if (!out.url && typeof hints.desktop_id === "string") out.url = `claude://code/continue/${hints.desktop_id}`;
954
- if (typeof hints.attended === "boolean") out.attended = hints.attended || !!out.url;
955
- return out;
1329
+ facts(session, transcript, hints) {
1330
+ if (!transcript) return {};
1331
+ return antigravityFacts(transcript, {
1332
+ conversation: session.id,
1333
+ workspaces: hints.workspaces ?? (session.cwd ? [session.cwd] : []),
1334
+ env: process.env
1335
+ });
956
1336
  },
957
1337
  backfill(sinceMs, env = process.env) {
958
1338
  const rows2 = [];
959
- const files = [];
960
- for (const d of claudeDirs(env))
961
- files.push(...recentFiles(path4.join(d, "projects"), 1, sinceMs, (n) => /^[0-9a-f-]{36}\.jsonl$/.test(n)));
962
- const noState = { get: () => ({}), save: () => {
963
- } };
964
- for (const f of files) {
965
- const id = path4.basename(f, ".jsonl");
966
- const facts = claudeFacts(f, id, {}, noState);
967
- if (!facts.started_at || !facts.last_at) continue;
968
- const { started_at, last_at, first_ask: _fa, harness_version: _hv, ...rest } = facts;
969
- const cwd = rest.cwd;
970
- rows2.push({
971
- session: { id, ...rest, ...gitFacts(cwd), ...rest.branch ? { branch: rest.branch } : {} },
972
- started_at,
973
- last_at
974
- });
975
- }
976
- return rows2;
977
- }
978
- };
979
- function prune2(o) {
980
- for (const k of Object.keys(o)) if (o[k] === void 0) delete o[k];
981
- return o;
982
- }
983
- var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
984
- function notClaudeCode(id, s) {
985
- return typeof s.transcript_path !== "string" && !UUID.test(id);
986
- }
987
- function pickSource(v) {
988
- return typeof v === "string" && ["startup", "resume", "clear", "compact", "fork"].includes(v) ? v : "unknown";
989
- }
990
- var harnessWord = (v) => typeof v === "string" && /^[A-Za-z0-9][A-Za-z0-9_.-]{0,39}$/.test(v) ? v : void 0;
991
- var WRAPPER = /^<(?:task-notification|cross-session-message|system-reminder|local-command-stdout|command-[a-z-]+)[\s/>]/i;
992
- var injectedBlock = (prompt) => WRAPPER.test(prompt.trim());
993
- function errText(v) {
994
- if (typeof v === "string") return v.slice(0, 2e3);
995
- if (v && typeof v === "object") {
996
- const e = v;
997
- if (typeof e.error === "string") return e.error.slice(0, 2e3);
998
- if (typeof e.message === "string") return e.message.slice(0, 2e3);
999
- }
1000
- return void 0;
1001
- }
1002
- function describe(input) {
1003
- if (!input || typeof input !== "object") return "";
1004
- const i = input;
1005
- const v = i.command ?? i.file_path ?? i.path ?? i.url ?? i.pattern;
1006
- return typeof v === "string" ? ` ${v.slice(0, 120)}` : "";
1007
- }
1008
- var NAME_RANK = {
1009
- user: "custom",
1010
- auto: "harness",
1011
- hook: "harness",
1012
- peer: "harness"
1013
- };
1014
- function registryEntry(j) {
1015
- if (!j || typeof j !== "object") return null;
1016
- const r = j;
1017
- const source = r.nameSource ? NAME_RANK[r.nameSource] : void 0;
1018
- return {
1019
- sessionId: r.sessionId ? String(r.sessionId) : null,
1020
- name: source && typeof r.name === "string" && r.name.trim() ? r.name.trim() : null,
1021
- name_source: source ?? null,
1022
- bridge: typeof r.bridgeSessionId === "string" ? r.bridgeSessionId : null
1023
- };
1024
- }
1025
- var liveCache = /* @__PURE__ */ new Map();
1026
- function claudeLive(dir) {
1027
- const hit = liveCache.get(dir);
1028
- if (hit) return hit;
1029
- const map = /* @__PURE__ */ new Map();
1030
- let names = [];
1031
- try {
1032
- names = readdirSync3(path4.join(dir, "sessions")).filter((n) => n.endsWith(".json"));
1033
- } catch {
1034
- }
1035
- for (const n of names) {
1036
- try {
1037
- const e = registryEntry(JSON.parse(readFileSync3(path4.join(dir, "sessions", n), "utf8")));
1038
- if (e?.sessionId) map.set(e.sessionId, e);
1039
- } catch {
1040
- }
1041
- }
1042
- liveCache.set(dir, map);
1043
- return map;
1044
- }
1045
- function claudeLiveByPid(dir, pid) {
1046
- if (!pid) return null;
1047
- try {
1048
- return registryEntry(JSON.parse(readFileSync3(path4.join(dir, "sessions", `${pid}.json`), "utf8")));
1049
- } catch {
1050
- return null;
1051
- }
1052
- }
1053
- function claudePid(env = process.env) {
1054
- try {
1055
- const dirs = claudeDirs(env);
1056
- const has = (pid2) => dirs.some((d) => existsSync3(path4.join(d, "sessions", `${pid2}.json`)));
1057
- let pid = process.ppid;
1058
- if (has(pid)) return pid;
1059
- const parent = parentOf();
1060
- for (let hop = 0; hop < 6 && pid > 1; hop++) {
1061
- pid = parent(pid);
1062
- if (pid > 1 && has(pid)) return pid;
1063
- }
1064
- } catch {
1065
- }
1066
- return null;
1067
- }
1068
- function parentOf() {
1069
- if (process.platform === "linux") {
1070
- return (pid) => {
1339
+ for (const data of dataDirs(env)) {
1340
+ let ids = [];
1071
1341
  try {
1072
- const st = readFileSync3(`/proc/${pid}/stat`, "utf8");
1073
- return Number(st.slice(st.lastIndexOf(")") + 2).split(" ")[1]) || 0;
1342
+ ids = readdirSync3(path5.join(data, "brain")).filter((n) => /^[0-9a-f-]{36}$/i.test(n));
1074
1343
  } catch {
1075
- return 0;
1076
1344
  }
1077
- };
1078
- }
1079
- const table = /* @__PURE__ */ new Map();
1080
- try {
1081
- const out = execFileSync2("ps", ["-axo", "pid=,ppid="], {
1082
- encoding: "utf8",
1083
- timeout: 2e3,
1084
- maxBuffer: 8 * 1024 * 1024,
1085
- stdio: ["ignore", "pipe", "ignore"]
1086
- });
1087
- for (const l of out.split("\n")) {
1088
- const m = /^\s*(\d+)\s+(\d+)/.exec(l);
1089
- if (m) table.set(Number(m[1]), Number(m[2]));
1345
+ for (const id of ids) {
1346
+ const f = path5.join(data, "brain", id, ".system_generated", "logs", "transcript.jsonl");
1347
+ try {
1348
+ if (statSync4(f).mtimeMs < sinceMs) continue;
1349
+ } catch {
1350
+ continue;
1351
+ }
1352
+ const facts = antigravityFacts(f, { conversation: id, workspaces: [], env });
1353
+ if (!facts.started_at || !facts.last_at) continue;
1354
+ const { started_at, last_at, first_ask: _fa, harness_version: _hv, ...rest } = facts;
1355
+ rows2.push({ session: { id, ...rest }, started_at, last_at });
1356
+ }
1090
1357
  }
1091
- } catch {
1358
+ return rows2;
1092
1359
  }
1093
- return (pid) => table.get(pid) || 0;
1360
+ };
1361
+ function prune2(o) {
1362
+ for (const k of Object.keys(o)) if (o[k] === void 0) delete o[k];
1363
+ return o;
1094
1364
  }
1095
- var EMAIL_RE = /^[^@\s]{1,64}@[^@\s]{1,189}$/;
1096
- function claudeLogin(dir, home = homeDir()) {
1097
- const files = [path4.join(dir, ".claude.json")];
1098
- if (path4.resolve(dir) === path4.join(home, ".claude")) files.unshift(path4.join(home, ".claude.json"));
1099
- for (const f of files) {
1365
+ function agPath(p) {
1366
+ if (typeof p !== "string" || !p) return null;
1367
+ let s = p;
1368
+ if (/^file:/i.test(s)) {
1100
1369
  try {
1101
- const a = JSON.parse(readFileSync3(f, "utf8")).oauthAccount;
1102
- if (typeof a?.accountUuid === "string" && a.accountUuid) {
1103
- const email = typeof a.emailAddress === "string" && EMAIL_RE.test(a.emailAddress) ? a.emailAddress : void 0;
1104
- return { id: a.accountUuid.slice(0, 128), ...email ? { email } : {} };
1105
- }
1370
+ s = decodeURIComponent(new URL(s).pathname);
1106
1371
  } catch {
1372
+ return null;
1107
1373
  }
1108
- }
1109
- return apiAccount(dir);
1374
+ if (process.platform === "win32" && /^\/[A-Za-z]:/.test(s)) s = s.slice(1);
1375
+ } else if (/^[a-z][a-z0-9+.-]*:\/\//i.test(s)) return null;
1376
+ if (!path5.isAbsolute(s)) return null;
1377
+ return path5.resolve(s);
1110
1378
  }
1111
- var ANTHROPIC_HOST = /(^|\.)anthropic\.com$/i;
1112
- function apiAccount(dir) {
1113
- let base;
1379
+ var agWorkspaces = (paths) => (Array.isArray(paths) ? paths : []).map(agPath).filter((p) => !!p).filter((p, i, a) => a.indexOf(p) === i);
1380
+ function agVariants(p) {
1381
+ const out = [p];
1114
1382
  try {
1115
- base = JSON.parse(readFileSync3(settingsIn(dir), "utf8")).env?.ANTHROPIC_BASE_URL;
1383
+ const r = realpathSync.native(p);
1384
+ if (r !== p) out.push(r);
1116
1385
  } catch {
1117
- return null;
1118
1386
  }
1119
- if (typeof base !== "string") return null;
1120
- let u;
1387
+ return out;
1388
+ }
1389
+ function agProjects(dir) {
1390
+ const out = [];
1391
+ let names = [];
1121
1392
  try {
1122
- u = new URL(base.trim());
1393
+ names = readdirSync3(dir).filter((n) => n.endsWith(".json"));
1123
1394
  } catch {
1124
- return null;
1395
+ return out;
1125
1396
  }
1126
- if (u.protocol !== "https:" && u.protocol !== "http:" || !u.hostname || ANTHROPIC_HOST.test(u.hostname))
1127
- return null;
1128
- const origin = `${u.protocol}//${u.host}`;
1129
- return { id: `api-${createHash("sha256").update(origin).digest("hex").slice(0, 16)}`, name: u.host };
1130
- }
1131
- var accountCache = /* @__PURE__ */ new Map();
1132
- function claudeAccount(dir) {
1133
- const hit = accountCache.get(dir);
1134
- if (hit !== void 0) return hit;
1135
- const acct = claudeLogin(dir)?.id ?? null;
1136
- accountCache.set(dir, acct);
1137
- return acct;
1138
- }
1139
- var LINK_RE = /^https:\/\/claude\.ai\/code\/session_[A-Za-z0-9]+$/;
1140
- function transcriptLink(records) {
1141
- for (let i = records.length - 1; i >= 0; i--) {
1142
- const j = records[i];
1143
- if (j.type === "system" && j.subtype === "bridge_status" && typeof j.url === "string" && LINK_RE.test(j.url))
1144
- return j.url;
1145
- if (j.type === "bridge-session" && typeof j.bridgeSessionId === "string") {
1146
- const m = /^(?:cse|session)_([A-Za-z0-9]+)$/.exec(j.bridgeSessionId);
1147
- if (m) return `https://claude.ai/code/session_${m[1]}`;
1397
+ for (const n of names) {
1398
+ const id = n.slice(0, -5);
1399
+ if (!ID_RE.test(id)) continue;
1400
+ let j;
1401
+ try {
1402
+ j = JSON.parse(readFileSync4(path5.join(dir, n), "utf8"));
1403
+ } catch {
1404
+ continue;
1405
+ }
1406
+ const folders = [];
1407
+ for (const r of j?.projectResources?.resources ?? []) {
1408
+ const f = agPath(r?.folderUri ?? r?.gitFolder?.folderUri);
1409
+ if (f) folders.push(...agVariants(f));
1148
1410
  }
1411
+ if (folders.length) out.push({ id, folders });
1149
1412
  }
1150
- return null;
1151
- }
1152
- function appTitle(text) {
1153
- const t = String(text).replace(/\s+/g, " ").trim();
1154
- return t.length > 50 ? `${t.slice(0, 50)}...` : t;
1155
- }
1156
- function askText(j) {
1157
- if (!j || j.type !== "user" || j.isMeta || j.isSidechain) return null;
1158
- const origin = j.origin;
1159
- if (origin?.kind && origin.kind !== "human") return null;
1160
- const c = j.message?.content;
1161
- 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();
1162
- if (!text || isInjected(text)) return null;
1163
- return text;
1413
+ return out;
1164
1414
  }
1165
- function firstAsk(records) {
1166
- for (const j of records) {
1167
- const t = askText(j);
1168
- if (t) return appTitle(t);
1415
+ function agMatchProject(projects, workspaces) {
1416
+ const ws = [];
1417
+ for (const w of workspaces) {
1418
+ const p = agPath(w);
1419
+ if (p) ws.push(...agVariants(p));
1169
1420
  }
1170
- return null;
1421
+ let best = null;
1422
+ let bestLen = -1;
1423
+ for (const pr of projects)
1424
+ for (const f of pr.folders) {
1425
+ const within = ws.some((w) => w === f || w.startsWith(f.endsWith(path5.sep) ? f : f + path5.sep));
1426
+ if (within && f.length > bestLen) {
1427
+ best = pr.id;
1428
+ bestLen = f.length;
1429
+ }
1430
+ }
1431
+ return best;
1171
1432
  }
1172
- function scanTitles(file, from, found) {
1173
- const out = { custom: found.custom ?? null, ai: found.ai ?? null, upto: from };
1174
- let size = 0;
1175
- try {
1176
- size = statSync3(file).size;
1177
- } catch {
1178
- return out;
1179
- }
1180
- if (size < from) {
1181
- out.upto = 0;
1182
- out.custom = out.ai = null;
1183
- from = 0;
1433
+ function antigravityRemoteUrl(transcript, opts) {
1434
+ const i = transcript.indexOf(`${path5.sep}brain${path5.sep}`);
1435
+ const dirs = (i > 0 ? [transcript.slice(0, i)] : []).concat(dataDirs(opts.env));
1436
+ for (const d of dirs) {
1437
+ try {
1438
+ const m = /installation_uuid:\s*"([0-9a-f-]{36})"/i.exec(
1439
+ readFileSync4(path5.join(d, "antigravity_state.pbtxt"), "utf8")
1440
+ );
1441
+ if (!m) continue;
1442
+ const base = `https://antigravity.google.com/r/${m[1]}-v2`;
1443
+ const conv = opts.conversation ?? "";
1444
+ if (!ID_RE.test(conv) || !opts.workspaces?.length) return base;
1445
+ const project = agMatchProject(agProjects(opts.projectsDir ?? projectsDir(opts.env)), opts.workspaces);
1446
+ return project ? `${base}?p=${encodeURIComponent(`c/${conv}?section=${project}`)}` : base;
1447
+ } catch {
1448
+ }
1184
1449
  }
1185
- let pos = 0;
1186
- scanLines(
1187
- file,
1188
- (l) => {
1189
- pos += Buffer.byteLength(l, "utf8") + 1;
1190
- if (pos <= from || l.indexOf('-title"') < 0) return null;
1191
- try {
1192
- const j = JSON.parse(l);
1193
- if (j.type === "custom-title" && j.customTitle) out.custom = j.customTitle;
1194
- if (j.type === "ai-title" && j.aiTitle) out.ai = j.aiTitle;
1195
- } catch {
1196
- }
1197
- return null;
1198
- },
1199
- 512 * 1024 * 1024
1200
- );
1201
- out.upto = size;
1202
- return out;
1450
+ return null;
1203
1451
  }
1204
- function claudeFacts(file, sessionId, hints, state) {
1205
- const { head, tail } = ends(file, 64 * 1024, 256 * 1024);
1452
+ function antigravityFacts(file, opts) {
1453
+ const { head, tail } = ends(file, 128 * 1024, 64 * 1024);
1206
1454
  const H = parseLines(head);
1207
1455
  const T = parseLines(tail);
1208
1456
  const out = {};
1209
- const id = sessionId || path4.basename(file, ".jsonl");
1210
- const mem = state.get(NAME2, id);
1211
- let custom = null;
1212
- let ai = null;
1213
- for (const j of H.concat(T)) {
1214
- if (j.type === "custom-title" && typeof j.customTitle === "string") custom = j.customTitle;
1215
- if (j.type === "ai-title" && typeof j.aiTitle === "string") ai = j.aiTitle;
1216
- }
1217
- let size = 0;
1218
- try {
1219
- size = statSync3(file).size;
1220
- } catch {
1221
- }
1222
- if (!custom && !ai && size > 64 * 1024 + 256 * 1024) {
1223
- const s = scanTitles(file, Number(mem.scan_upto) || 0, {
1224
- custom: mem.scan_custom,
1225
- ai: mem.scan_ai
1226
- });
1227
- custom = s.custom;
1228
- ai = s.ai;
1229
- state.save(NAME2, id, { scan_upto: s.upto, scan_custom: s.custom, scan_ai: s.ai });
1230
- }
1231
- const dir = configDirOf(file);
1232
- const live = claudeLive(dir).get(id) ?? claudeLiveByPid(dir, hints.claude_pid);
1233
- if (live?.name) state.save(NAME2, id, { name: live.name, name_source: live.name_source });
1234
- const bridge = live?.bridge && /^session_[A-Za-z0-9]+$/.test(live.bridge) ? `https://claude.ai/code/${live.bridge}` : null;
1235
- const url = bridge ?? transcriptLink(T) ?? transcriptLink(H) ?? (typeof mem.url === "string" ? mem.url : null);
1236
- if (url) {
1237
- out.url = url;
1238
- state.save(NAME2, id, { url });
1239
- }
1240
- const name = live?.name ? { title: live.name, source: live.name_source } : typeof mem.name === "string" ? { title: mem.name, source: mem.name_source } : null;
1241
- const pick = name && name.source === "custom" && name || custom && { title: custom, source: "custom" } || name || ai && { title: ai, source: "harness" };
1242
- let ask = typeof mem.first_ask === "string" ? mem.first_ask : firstAsk(H);
1243
- if (!ask && size > 64 * 1024 && !mem.first_ask_none) {
1244
- ask = scanLines(file, (l) => {
1245
- try {
1246
- const t = askText(JSON.parse(l));
1247
- return t ? appTitle(t) : null;
1248
- } catch {
1249
- return null;
1250
- }
1251
- });
1252
- if (!ask && size > 16 * 1024 * 1024) state.save(NAME2, id, { first_ask_none: true });
1253
- }
1254
- if (ask) {
1255
- out.first_ask = ask;
1256
- if (!mem.first_ask) state.save(NAME2, id, { first_ask: ask });
1257
- }
1258
- if (pick) {
1259
- out.title = String(pick.title).slice(0, 120);
1260
- out.title_source = pick.source;
1261
- } else if (ask) {
1262
- out.title = ask;
1457
+ const url = antigravityRemoteUrl(file, opts);
1458
+ if (url) out.url = url;
1459
+ for (const j of H) {
1460
+ if (j.type !== "USER_INPUT" || typeof j.content !== "string") continue;
1461
+ const m = /<USER_REQUEST>([\s\S]*?)(?:<\/USER_REQUEST>|$)/.exec(j.content);
1462
+ const text = (m ? m[1] : j.content).trim();
1463
+ const line = (text.split("\n").find((l) => l.trim()) ?? "").trim();
1464
+ if (!line || /^<[a-z_-]+>/i.test(line)) continue;
1465
+ out.title = line.length > 80 ? `${line.slice(0, 80).replace(/\s+\S*$/, "")}\u2026` : line;
1263
1466
  out.title_source = "first-ask";
1467
+ break;
1264
1468
  }
1265
- const acct = claudeAccount(dir);
1266
- if (acct) out.account_id = acct;
1267
- for (let i = T.length - 1; i >= 0; i--) {
1268
- const m = T[i].message?.model;
1269
- if (T[i]?.type === "assistant" && m && m !== "<synthetic>") {
1270
- out.model = m;
1271
- break;
1272
- }
1273
- }
1274
- const meta = [...T].reverse().find((j) => j.version) ?? H.find((j) => j.version);
1275
- if (meta?.version) out.harness_version = String(meta.version);
1276
- const times = H.concat(T).map((j) => Date.parse(String(j.timestamp ?? ""))).filter((n) => n > 0);
1469
+ const times = H.concat(T).map((j) => Date.parse(String(j.created_at ?? j.timestamp ?? ""))).filter((n) => n > 0);
1277
1470
  if (times.length) {
1278
1471
  out.started_at = Math.min(...times);
1279
1472
  out.last_at = Math.max(...times);
1280
1473
  }
1281
- const withCwd = H.find((j) => typeof j.cwd === "string");
1282
- if (withCwd) {
1283
- out.cwd = withCwd.cwd;
1284
- if (typeof withCwd.gitBranch === "string" && withCwd.gitBranch !== "HEAD") out.branch = withCwd.gitBranch;
1285
- }
1286
1474
  return out;
1287
1475
  }
1288
1476
 
1289
1477
  // ../core/src/adapters/codex.ts
1290
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
1291
- import path5 from "node:path";
1478
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "node:fs";
1479
+ import path6 from "node:path";
1292
1480
 
1293
1481
  // ../core/src/adapters/codex-config.ts
1294
- var NOTIFY_LINE = /^[ \t]*notify[ \t]*=[ \t]*(\[.*\])[ \t]*(?:#.*)?$/m;
1482
+ var NOTIFY_LINE = /^[ \t]*notify[ \t]*=[ \t]*(\[.*\])[ \t]*(?:#.*)?(?=\r?$)/m;
1295
1483
  var NOTIFY_KEY = /^[ \t]*notify[ \t]*=/m;
1296
1484
  var OURS2 = /(?:hook\.js|sessionpipe-hook)",\s*"codex",\s*"notify"/;
1297
1485
  function scan(line, st) {
@@ -1381,10 +1569,11 @@ function dropLine(text, index, length) {
1381
1569
  }
1382
1570
  function insertTopLevel(text, line) {
1383
1571
  const { head, tail } = split(text);
1384
- if (!tail) return (head.trim() ? head.replace(/\s*$/, "\n") : "") + line + "\n";
1385
- return `${(head.trim() ? head.replace(/\s*$/, "\n") : "") + line}
1386
-
1387
- ${tail}`;
1572
+ const body = head.replace(/\s*$/, "");
1573
+ const eol = (body.includes("\n") ? body : text).includes("\r\n") ? "\r\n" : "\n";
1574
+ const top = head.trim() ? head.replace(/\s*$/, eol) : "";
1575
+ if (!tail) return top + line + eol;
1576
+ return `${top + line}${eol}${eol}${tail}`;
1388
1577
  }
1389
1578
  function withNotify(text, line) {
1390
1579
  const n = readNotify(text);
@@ -1433,25 +1622,25 @@ var CODEX_EVENTS = [
1433
1622
  ];
1434
1623
  var OPTS2 = { matcherFor: (e) => /Tool|Permission/.test(e) ? "" : void 0, timeout: 5 };
1435
1624
  var eventsFor2 = (lean) => lean ? CODEX_EVENTS.filter((e) => e !== "PreToolUse") : [...CODEX_EVENTS];
1436
- var codexHome = (env) => env.CODEX_HOME || path5.join(homeDir(), ".codex");
1437
- var hooksFile2 = (env) => path5.join(codexHome(env), "hooks.json");
1438
- var configFile = (env) => path5.join(codexHome(env), "config.toml");
1439
- var sessionsDir = (env) => path5.join(codexHome(env), "sessions");
1625
+ var codexHome = (env) => env.CODEX_HOME || path6.join(homeDir(), ".codex");
1626
+ var hooksFile2 = (env) => path6.join(codexHome(env), "hooks.json");
1627
+ var configFile = (env) => path6.join(codexHome(env), "config.toml");
1628
+ var sessionsDir = (env) => path6.join(codexHome(env), "sessions");
1440
1629
  var validTool3 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
1441
1630
  var endReason2 = (r) => r === "clear" || r === "logout" || r === "exit" ? r : "other";
1442
1631
  function installNotify(cmd, env) {
1443
1632
  const file = configFile(env);
1444
1633
  let text = "";
1445
1634
  try {
1446
- text = readFileSync4(file, "utf8");
1635
+ text = readFileSync5(file, "utf8");
1447
1636
  } catch {
1448
1637
  }
1449
1638
  const argv = cmd([NAME3, "notify"]).argv;
1450
1639
  const line = `notify = ${JSON.stringify(argv)}`;
1451
1640
  const n = readNotify(text);
1452
1641
  const write = (next) => {
1453
- mkdirSync3(path5.dirname(file), { recursive: true });
1454
- writeFileSync3(file, next);
1642
+ mkdirSync3(path6.dirname(file), { recursive: true });
1643
+ writeFileSync4(file, next);
1455
1644
  };
1456
1645
  if (n && !n.other) {
1457
1646
  if (!n.misplaced && JSON.stringify(n.argv) === JSON.stringify(argv))
@@ -1489,19 +1678,19 @@ function uninstallNotify(env) {
1489
1678
  const file = configFile(env);
1490
1679
  let text;
1491
1680
  try {
1492
- text = readFileSync4(file, "utf8");
1681
+ text = readFileSync5(file, "utf8");
1493
1682
  } catch {
1494
1683
  return { file, changed: false };
1495
1684
  }
1496
1685
  const n = readNotify(text);
1497
1686
  if (!n) return { file, changed: false };
1498
1687
  if (!n.other) {
1499
- writeFileSync3(file, withoutNotify(text));
1688
+ writeFileSync4(file, withoutNotify(text));
1500
1689
  return { file, changed: true };
1501
1690
  }
1502
1691
  const prev = previousNotify(n.argv);
1503
1692
  if (isComputerUse(n.argv) && prev && OURS2.test(JSON.stringify(prev))) {
1504
- writeFileSync3(file, replaceNotify(text, n, `notify = ${stripPreviousRaw(n.raw)}`));
1693
+ writeFileSync4(file, replaceNotify(text, n, `notify = ${stripPreviousRaw(n.raw)}`));
1505
1694
  return { file, changed: true };
1506
1695
  }
1507
1696
  return { file, changed: false };
@@ -1517,7 +1706,7 @@ function findRollout(threadId, env = process.env) {
1517
1706
  return;
1518
1707
  }
1519
1708
  for (const n of names.sort().reverse()) {
1520
- const p = path5.join(dir, n);
1709
+ const p = path6.join(dir, n);
1521
1710
  if (n.endsWith(`-${threadId}.jsonl`)) {
1522
1711
  best = p;
1523
1712
  return;
@@ -1561,7 +1750,7 @@ var codex = {
1561
1750
  ];
1562
1751
  let text = "";
1563
1752
  try {
1564
- text = readFileSync4(configFile(env), "utf8");
1753
+ text = readFileSync5(configFile(env), "utf8");
1565
1754
  } catch {
1566
1755
  }
1567
1756
  const n = readNotify(text);
@@ -1688,7 +1877,7 @@ var codex = {
1688
1877
  readTranscript(file, fromLine) {
1689
1878
  let text = "";
1690
1879
  try {
1691
- text = readFileSync4(file, "utf8");
1880
+ text = readFileSync5(file, "utf8");
1692
1881
  } catch {
1693
1882
  return { turns: [], line: fromLine };
1694
1883
  }
@@ -1702,7 +1891,7 @@ var codex = {
1702
1891
  backfill(sinceMs, env = process.env) {
1703
1892
  const rows2 = [];
1704
1893
  for (const f of recentFiles(sessionsDir(env), 3, sinceMs, (n) => /^rollout-.*\.jsonl$/.test(n))) {
1705
- const m = /([0-9a-f]{8}-[0-9a-f-]{27,})\.jsonl$/i.exec(path5.basename(f));
1894
+ const m = /([0-9a-f]{8}-[0-9a-f-]{27,})\.jsonl$/i.exec(path6.basename(f));
1706
1895
  if (!m?.[1]) continue;
1707
1896
  if (codexSubagent(rolloutMeta(f))) continue;
1708
1897
  const facts = codexFacts(f);
@@ -1711,12 +1900,40 @@ var codex = {
1711
1900
  rows2.push({ session: { id: m[1], ...rest }, started_at, last_at });
1712
1901
  }
1713
1902
  return rows2;
1714
- }
1903
+ },
1904
+ limits: (transcript) => codexLimits(transcript)
1715
1905
  };
1906
+ var epochIso2 = (t) => typeof t === "number" && t > 0 && t < 1e11 ? new Date(t * 1e3).toISOString() : void 0;
1716
1907
  function prune3(o) {
1717
1908
  for (const k of Object.keys(o)) if (o[k] === void 0) delete o[k];
1718
1909
  return o;
1719
1910
  }
1911
+ function codexLimits(file) {
1912
+ const { tail } = ends(file, 0, 256 * 1024);
1913
+ for (const r of parseLines(tail).reverse()) {
1914
+ const p = r.payload;
1915
+ if (r.type !== "event_msg" || p?.type !== "token_count") continue;
1916
+ if (!p.rate_limits || typeof p.rate_limits !== "object") continue;
1917
+ const rl = p.rate_limits;
1918
+ if (rl.limit_id != null && rl.limit_id !== "codex") continue;
1919
+ const windows = [];
1920
+ for (const id of ["primary", "secondary"]) {
1921
+ const w = rl[id];
1922
+ if (typeof w?.used_percent !== "number" || w.used_percent < 0) continue;
1923
+ windows.push(
1924
+ prune3({
1925
+ id,
1926
+ used_percent: w.used_percent,
1927
+ window_minutes: Number.isInteger(w.window_minutes) && Number(w.window_minutes) > 0 ? Number(w.window_minutes) : void 0,
1928
+ resets_at: epochIso2(w.resets_at)
1929
+ })
1930
+ );
1931
+ }
1932
+ if (!windows.length) return null;
1933
+ return prune3({ windows, plan: typeof rl.plan_type === "string" ? rl.plan_type.slice(0, 64) : void 0 });
1934
+ }
1935
+ return null;
1936
+ }
1720
1937
  function codexFacts(file) {
1721
1938
  const { head, tail } = ends(file, 128 * 1024, 64 * 1024);
1722
1939
  let H = parseLines(head);
@@ -1761,7 +1978,7 @@ function codexFacts(file) {
1761
1978
  // ../core/src/adapters/devin.ts
1762
1979
  import { createHash as createHash2 } from "node:crypto";
1763
1980
  import { existsSync as existsSync6, readlinkSync } from "node:fs";
1764
- import path6 from "node:path";
1981
+ import path7 from "node:path";
1765
1982
 
1766
1983
  // ../core/src/readers/sqlite.ts
1767
1984
  import { execFileSync as execFileSync3 } from "node:child_process";
@@ -1945,11 +2162,11 @@ function foreignEvent(file) {
1945
2162
  return Object.keys(hooks).find((k) => !ok.includes(k));
1946
2163
  }
1947
2164
  var homeOf = (env) => env.HOME || homeDir();
1948
- var appData = (env) => env.APPDATA || path6.join(homeOf(env), "AppData", "Roaming");
1949
- var devinConfigDir = (env = process.env) => process.platform === "win32" ? path6.join(appData(env), "devin") : path6.join(env.XDG_CONFIG_HOME || path6.join(homeOf(env), ".config"), "devin");
1950
- var configFile2 = (env) => path6.join(devinConfigDir(env), "config.json");
1951
- var devinDataDir = (env = process.env) => process.platform === "win32" ? path6.join(appData(env), "devin", "cli") : path6.join(env.XDG_DATA_HOME || path6.join(homeOf(env), ".local", "share"), "devin", "cli");
1952
- var sessionsDb = (env) => path6.join(devinDataDir(env), "sessions.db");
2165
+ var appData = (env) => env.APPDATA || path7.join(homeOf(env), "AppData", "Roaming");
2166
+ 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");
2167
+ var configFile2 = (env) => path7.join(devinConfigDir(env), "config.json");
2168
+ 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");
2169
+ var sessionsDb = (env) => path7.join(devinDataDir(env), "sessions.db");
1953
2170
  var SESSION_ID = /^[A-Za-z0-9._:-]{1,128}$/;
1954
2171
  var validTool4 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
1955
2172
  var endReason3 = (r) => r === "clear" || r === "logout" || r === "exit" ? r : "other";
@@ -2121,7 +2338,7 @@ var BACKFILL_SQL = `SELECT s.id, ${SESSION_FIELDS} FROM sessions s
2121
2338
  function sessionFacts(row) {
2122
2339
  const out = {};
2123
2340
  if (typeof row.model === "string" && row.model) out.model = row.model;
2124
- if (typeof row.working_directory === "string" && path6.isAbsolute(row.working_directory))
2341
+ if (typeof row.working_directory === "string" && path7.isAbsolute(row.working_directory))
2125
2342
  out.cwd = row.working_directory;
2126
2343
  const started = Number(row.created_at);
2127
2344
  const last = Number(row.last_activity_at);
@@ -2166,8 +2383,8 @@ function readDevinChain(db, session, fromLine) {
2166
2383
  }
2167
2384
  function devinVersion(env = process.env) {
2168
2385
  try {
2169
- const target = readlinkSync(path6.join(devinDataDir(env), "_versions", "current"));
2170
- const name = path6.basename(target);
2386
+ const target = readlinkSync(path7.join(devinDataDir(env), "_versions", "current"));
2387
+ const name = path7.basename(target);
2171
2388
  return /^[0-9][0-9A-Za-z.+-]{0,63}$/.test(name) ? name : void 0;
2172
2389
  } catch {
2173
2390
  return void 0;
@@ -2181,8 +2398,8 @@ function devinAccount(env = process.env) {
2181
2398
 
2182
2399
  // ../core/src/adapters/gemini-cli.ts
2183
2400
  import { execFileSync as execFileSync4 } from "node:child_process";
2184
- import { existsSync as existsSync7, readFileSync as readFileSync5 } from "node:fs";
2185
- import path7 from "node:path";
2401
+ import { existsSync as existsSync7, readFileSync as readFileSync6 } from "node:fs";
2402
+ import path8 from "node:path";
2186
2403
  var NAME5 = "gemini-cli";
2187
2404
  var GEMINI_EVENTS = [
2188
2405
  "SessionStart",
@@ -2195,26 +2412,31 @@ var GEMINI_EVENTS = [
2195
2412
  "SessionEnd"
2196
2413
  ];
2197
2414
  var OPTS4 = { matcherFor: (e) => /Tool/.test(e) ? ".*" : void 0, timeout: 5e3 };
2198
- var geminiDir = (env) => env.GEMINI_CLI_HOME || path7.join(homeDir(), ".gemini");
2199
- var settings = (env) => path7.join(geminiDir(env), "settings.json");
2415
+ var geminiDir = (env) => env.GEMINI_CLI_HOME || path8.join(homeDir(), ".gemini");
2416
+ var settings = (env) => path8.join(geminiDir(env), "settings.json");
2200
2417
  var validTool5 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
2201
2418
  var eventsFor4 = (lean) => lean ? GEMINI_EVENTS.filter((e) => e !== "BeforeTool") : [...GEMINI_EVENTS];
2419
+ function geminiVersion(platform = process.platform, env = process.env) {
2420
+ const win = platform === "win32";
2421
+ const bin = win ? findBinary({ names: ["gemini.exe", "gemini.cmd"], dirs: [path8.dirname(process.execPath)], env }) : "gemini";
2422
+ if (!bin) throw new Error("no gemini on PATH");
2423
+ const c = probe(bin, ["--version"]);
2424
+ return execFileSync4(c.file, c.args, {
2425
+ encoding: "utf8",
2426
+ timeout: 4e3,
2427
+ stdio: ["ignore", "pipe", "ignore"],
2428
+ env,
2429
+ shell: c.shell
2430
+ }).trim();
2431
+ }
2202
2432
  function geminiHooksActive(settingsFile) {
2203
- let tools;
2204
- try {
2205
- tools = JSON.parse(readFileSync5(settingsFile, "utf8")).tools;
2206
- } catch {
2207
- }
2433
+ const tools = readJson(settingsFile).tools;
2208
2434
  if (tools?.enableHooks === true) return { active: true };
2209
2435
  if (tools?.enableHooks === false)
2210
2436
  return { active: false, note: "inactive: tools.enableHooks is false in settings.json" };
2211
2437
  let version = "";
2212
2438
  try {
2213
- version = execFileSync4("gemini", ["--version"], {
2214
- encoding: "utf8",
2215
- timeout: 4e3,
2216
- stdio: ["ignore", "pipe", "ignore"]
2217
- }).trim();
2439
+ version = geminiVersion();
2218
2440
  } catch {
2219
2441
  return {
2220
2442
  active: true,
@@ -2232,7 +2454,7 @@ var geminiCli = {
2232
2454
  name: NAME5,
2233
2455
  events: GEMINI_EVENTS,
2234
2456
  // ~/.gemini also belongs to Antigravity; Gemini CLI is here when its settings or its chat logs exist.
2235
- detect: (env = process.env) => existsSync7(settings(env)) || existsSync7(path7.join(geminiDir(env), "tmp")),
2457
+ detect: (env = process.env) => existsSync7(settings(env)) || existsSync7(path8.join(geminiDir(env), "tmp")),
2236
2458
  configFiles: (env = process.env) => [settings(env)],
2237
2459
  install: (cmd, env = process.env, opts = {}) => {
2238
2460
  const r = installClaudeShaped(settings(env), NAME5, eventsFor4(opts.lean), cmd, OPTS4);
@@ -2330,7 +2552,7 @@ var geminiCli = {
2330
2552
  readTranscript(file, fromLine) {
2331
2553
  let text = "";
2332
2554
  try {
2333
- text = readFileSync5(file, "utf8");
2555
+ text = readFileSync6(file, "utf8");
2334
2556
  } catch {
2335
2557
  return { turns: [], line: fromLine };
2336
2558
  }
@@ -2409,7 +2631,7 @@ function matches(expect, got, pathStr = "") {
2409
2631
  async function runAdapterFixtures(files) {
2410
2632
  const results = [];
2411
2633
  for (const file of files) {
2412
- const fx = JSON.parse(readFileSync6(file, "utf8"));
2634
+ const fx = JSON.parse(readFileSync7(file, "utf8"));
2413
2635
  const adapter = adapterByName(fx.harness);
2414
2636
  if (!adapter) {
2415
2637
  results.push({ file, ok: false, why: `no adapter ${fx.harness}` });
@@ -2445,8 +2667,8 @@ async function runAdapterFixtures(files) {
2445
2667
 
2446
2668
  // src/cli.ts
2447
2669
  var walk = (d) => readdirSync5(d).flatMap((n) => {
2448
- const p = path8.join(d, n);
2449
- return statSync4(p).isDirectory() ? walk(p) : p.endsWith(".json") ? [p] : [];
2670
+ const p = path9.join(d, n);
2671
+ return statSync5(p).isDirectory() ? walk(p) : p.endsWith(".json") ? [p] : [];
2450
2672
  });
2451
2673
  async function main(argv, io = process) {
2452
2674
  const target = argv[0];