sessionpipe-conformance 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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.11.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
10
  import { readFileSync as readFileSync6 } from "node:fs";
11
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";
24
-
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,48 +507,161 @@ 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(
606
+ ev("turn.ended", {
607
+ turn_id,
608
+ reason: "stop",
609
+ background: Array.isArray(s.background_tasks) ? s.background_tasks.length : void 0
610
+ })
611
+ );
512
612
  break;
613
+ case "StopFailure":
614
+ events.push(
615
+ ev("turn.ended", {
616
+ turn_id,
617
+ reason: "error",
618
+ error: typeof s.error === "string" ? s.error.slice(0, 200) : void 0
619
+ })
620
+ );
621
+ break;
622
+ case "SubagentStart":
623
+ events.push(
624
+ ev("subagent.started", {
625
+ agent_id: String(s.agent_id ?? ""),
626
+ agent_type: typeof s.agent_type === "string" ? s.agent_type : void 0
627
+ })
628
+ );
629
+ break;
630
+ case "SubagentStop":
631
+ events.push(
632
+ ev("subagent.ended", {
633
+ agent_id: String(s.agent_id ?? ""),
634
+ agent_type: typeof s.agent_type === "string" ? s.agent_type : void 0
635
+ })
636
+ );
637
+ break;
638
+ case "PreCompact":
639
+ events.push(ev("context.compacted", { trigger: s.trigger === "manual" ? "manual" : "auto" }));
640
+ break;
641
+ case "PostCompact":
642
+ break;
643
+ // PreCompact already said it; a second event would double-count
644
+ case "SessionEnd":
645
+ events.push(ev("session.ended", { reason: endReason(s.reason) }));
646
+ break;
647
+ case "StatusLine": {
648
+ const limits = claudeLimits(s.rate_limits);
649
+ if (!limits) return null;
650
+ events.push(ev("limits.observed", limits));
651
+ break;
652
+ }
513
653
  default:
514
- return { session, events: [], stdout };
654
+ return null;
515
655
  }
516
- const r = { session, events, stdout, hints: { workspaces } };
517
- if (typeof s.transcriptPath === "string") r.transcript = s.transcriptPath;
656
+ const hints = {};
657
+ if (event === "UserPromptSubmit" && typeof s.prompt === "string") hints.prompt = s.prompt;
658
+ const host = input.env.CLAUDE_CODE_HOST_SESSION_ID;
659
+ if (host && DESKTOP_ID.test(host)) hints.desktop_id = host;
660
+ const attended = input.env.CLAUDE_CODE_SESSION_ATTENDED;
661
+ if (attended === "1" || attended === "0") hints.attended = attended === "1" || !!input.env.SESSIONPIPE_CONTROL_JOB;
662
+ if (event !== "PreToolUse" && event !== "PostToolUse") hints.claude_pid = claudePid(input.env);
663
+ const r = { session, events, hints };
664
+ if (typeof s.transcript_path === "string") r.transcript = s.transcript_path;
518
665
  return r;
519
666
  },
520
667
  readTranscript(file, fromLine) {
@@ -524,259 +671,571 @@ var antigravity = {
524
671
  } catch {
525
672
  return { turns: [], line: fromLine };
526
673
  }
527
- return readAntigravity(text.split("\n"), fromLine);
674
+ return readClaude(text.split("\n"), fromLine);
528
675
  },
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
- });
676
+ facts(session, transcript, hints, state) {
677
+ const out = {};
678
+ if (transcript) Object.assign(out, claudeFacts(transcript, session.id, hints, state));
679
+ const cwd = out.cwd ?? session.cwd;
680
+ Object.assign(out, gitFacts(cwd), out.branch ? { branch: out.branch } : {});
681
+ if (!out.url && typeof hints.desktop_id === "string") out.url = `claude://code/continue/${hints.desktop_id}`;
682
+ if (typeof hints.attended === "boolean") out.attended = hints.attended || !!out.url;
683
+ return out;
536
684
  },
685
+ remoteFacts: (session, local, transcript, state, env = process.env) => claudeRemoteFacts(session.id, local, transcript, state, env),
537
686
  backfill(sinceMs, env = process.env) {
538
687
  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
- }
688
+ const files = [];
689
+ for (const d of claudeDirs(env))
690
+ files.push(...recentFiles(path3.join(d, "projects"), 1, sinceMs, (n) => /^[0-9a-f-]{36}\.jsonl$/.test(n)));
691
+ const noState = { get: () => ({}), save: () => {
692
+ } };
693
+ for (const f of files) {
694
+ const id = path3.basename(f, ".jsonl");
695
+ const facts = claudeFacts(f, id, {}, noState);
696
+ if (!facts.started_at || !facts.last_at) continue;
697
+ const { started_at, last_at, first_ask: _fa, harness_version: _hv, ...rest } = facts;
698
+ const cwd = rest.cwd;
699
+ rows2.push({
700
+ session: { id, ...rest, ...gitFacts(cwd), ...rest.branch ? { branch: rest.branch } : {} },
701
+ started_at,
702
+ last_at,
703
+ transcript: f
704
+ });
557
705
  }
558
706
  return rows2;
559
707
  }
560
708
  };
709
+ var LINE_REFRESH_S = 60;
710
+ function statusLine(file, want, cmd) {
711
+ const f = readJsonFile(file);
712
+ if (!f.ok) return null;
713
+ const cur = f.value.statusLine;
714
+ const ours = typeof cur?.command === "string" && OURS.test(cur.command);
715
+ const command = cmd?.([NAME, "StatusLine"]).command;
716
+ if (want && cur && !ours)
717
+ return { file, changed: false, skipped: true, note: "a status line of your own is set; limits are not read" };
718
+ if (want ? ours && cur?.command === command && cur?.refreshInterval === LINE_REFRESH_S : !ours) return null;
719
+ if (want) f.value.statusLine = { type: "command", command, refreshInterval: LINE_REFRESH_S };
720
+ else delete f.value.statusLine;
721
+ writeFileSync2(file, formatJson(f.value, f));
722
+ return { file, changed: true, note: want ? "status line written (account limits)" : "status line removed" };
723
+ }
724
+ var epochIso = (t) => typeof t === "number" && t > 0 && t < 1e11 ? new Date(t * 1e3).toISOString() : void 0;
725
+ var WINDOW_MINUTES = { five_hour: 300, seven_day: 10080 };
726
+ function claudeLimits(rl) {
727
+ if (!rl || typeof rl !== "object" || Array.isArray(rl)) return null;
728
+ const windows = [];
729
+ for (const [id, w] of Object.entries(rl)) {
730
+ if (!/^[a-z0-9_]{1,64}$/.test(id) || typeof w?.used_percentage !== "number" || w.used_percentage < 0) continue;
731
+ windows.push(
732
+ prune({
733
+ id,
734
+ used_percent: w.used_percentage,
735
+ window_minutes: WINDOW_MINUTES[id],
736
+ resets_at: epochIso(w.resets_at)
737
+ })
738
+ );
739
+ }
740
+ return windows.length ? { windows: windows.slice(0, 8) } : null;
741
+ }
561
742
  function prune(o) {
562
743
  for (const k of Object.keys(o)) if (o[k] === void 0) delete o[k];
563
744
  return o;
564
745
  }
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);
746
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
747
+ function notClaudeCode(id, s) {
748
+ return typeof s.transcript_path !== "string" && !UUID.test(id);
578
749
  }
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 {
750
+ function pickSource(v) {
751
+ return typeof v === "string" && ["startup", "resume", "clear", "compact", "fork"].includes(v) ? v : "unknown";
752
+ }
753
+ var harnessWord = (v) => typeof v === "string" && /^[A-Za-z0-9][A-Za-z0-9_.-]{0,39}$/.test(v) ? v : void 0;
754
+ var WRAPPER = /^<(?:task-notification|cross-session-message|system-reminder|local-command-stdout|command-[a-z-]+)[\s/>]/i;
755
+ var injectedBlock = (prompt) => WRAPPER.test(prompt.trim());
756
+ function errText(v) {
757
+ if (typeof v === "string") return v.slice(0, 2e3);
758
+ if (v && typeof v === "object") {
759
+ const e = v;
760
+ if (typeof e.error === "string") return e.error.slice(0, 2e3);
761
+ if (typeof e.message === "string") return e.message.slice(0, 2e3);
586
762
  }
587
- return out;
763
+ return void 0;
588
764
  }
589
- function agProjects(dir) {
590
- const out = [];
765
+ function describe(input) {
766
+ if (!input || typeof input !== "object") return "";
767
+ const i = input;
768
+ const v = i.command ?? i.file_path ?? i.path ?? i.url ?? i.pattern;
769
+ return typeof v === "string" ? ` ${v.slice(0, 120)}` : "";
770
+ }
771
+ var NAME_RANK = {
772
+ user: "custom",
773
+ auto: "harness",
774
+ hook: "harness",
775
+ peer: "harness"
776
+ };
777
+ function registryEntry(j) {
778
+ if (!j || typeof j !== "object") return null;
779
+ const r = j;
780
+ const source = r.nameSource ? NAME_RANK[r.nameSource] : void 0;
781
+ return {
782
+ sessionId: r.sessionId ? String(r.sessionId) : null,
783
+ name: source && typeof r.name === "string" && r.name.trim() ? r.name.trim() : null,
784
+ name_source: source ?? null,
785
+ bridge: typeof r.bridgeSessionId === "string" ? r.bridgeSessionId : null
786
+ };
787
+ }
788
+ var liveCache = /* @__PURE__ */ new Map();
789
+ function claudeLive(dir) {
790
+ const hit = liveCache.get(dir);
791
+ if (hit) return hit;
792
+ const map = /* @__PURE__ */ new Map();
591
793
  let names = [];
592
794
  try {
593
- names = readdirSync2(dir).filter((n) => n.endsWith(".json"));
795
+ names = readdirSync2(path3.join(dir, "sessions")).filter((n) => n.endsWith(".json"));
594
796
  } catch {
595
- return out;
596
797
  }
597
798
  for (const n of names) {
598
- const id = n.slice(0, -5);
599
- if (!ID_RE.test(id)) continue;
600
- let j;
601
799
  try {
602
- j = JSON.parse(readFileSync2(path3.join(dir, n), "utf8"));
800
+ const e = registryEntry(JSON.parse(readFileSync2(path3.join(dir, "sessions", n), "utf8")));
801
+ if (e?.sessionId) map.set(e.sessionId, e);
603
802
  } 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
803
  }
611
- if (folders.length) out.push({ id, folders });
612
804
  }
613
- return out;
805
+ liveCache.set(dir, map);
806
+ return map;
614
807
  }
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));
808
+ function claudeLiveByPid(dir, pid) {
809
+ if (!pid) return null;
810
+ try {
811
+ return registryEntry(JSON.parse(readFileSync2(path3.join(dir, "sessions", `${pid}.json`), "utf8")));
812
+ } catch {
813
+ return null;
620
814
  }
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;
815
+ }
816
+ function claudePid(env = process.env) {
817
+ try {
818
+ const dirs = claudeDirs(env);
819
+ const has = (pid2) => dirs.some((d) => existsSync2(path3.join(d, "sessions", `${pid2}.json`)));
820
+ let pid = process.ppid;
821
+ if (has(pid)) return pid;
822
+ const parent = parentOf();
823
+ for (let hop = 0; hop < 6 && pid > 1; hop++) {
824
+ pid = parent(pid);
825
+ if (pid > 1 && has(pid)) return pid;
826
+ }
827
+ } catch {
828
+ }
829
+ return null;
830
+ }
831
+ function parentOf() {
832
+ if (process.platform === "linux") {
833
+ return (pid) => {
834
+ try {
835
+ const st = readFileSync2(`/proc/${pid}/stat`, "utf8");
836
+ return Number(st.slice(st.lastIndexOf(")") + 2).split(" ")[1]) || 0;
837
+ } catch {
838
+ return 0;
629
839
  }
840
+ };
841
+ }
842
+ const table = /* @__PURE__ */ new Map();
843
+ try {
844
+ const out = execFileSync2("ps", ["-axo", "pid=,ppid="], {
845
+ encoding: "utf8",
846
+ timeout: 2e3,
847
+ maxBuffer: 8 * 1024 * 1024,
848
+ stdio: ["ignore", "pipe", "ignore"]
849
+ });
850
+ for (const l of out.split("\n")) {
851
+ const m = /^\s*(\d+)\s+(\d+)/.exec(l);
852
+ if (m) table.set(Number(m[1]), Number(m[2]));
630
853
  }
631
- return best;
854
+ } catch {
855
+ }
856
+ return (pid) => table.get(pid) || 0;
632
857
  }
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) {
858
+ var EMAIL_RE = /^[^@\s]{1,64}@[^@\s]{1,189}$/;
859
+ function claudeLogin(dir, home = homeDir()) {
860
+ const files = [path3.join(dir, ".claude.json")];
861
+ if (path3.resolve(dir) === path3.join(home, ".claude")) files.unshift(path3.join(home, ".claude.json"));
862
+ for (const f of files) {
637
863
  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;
864
+ const a = JSON.parse(readFileSync2(f, "utf8")).oauthAccount;
865
+ if (typeof a?.accountUuid === "string" && a.accountUuid) {
866
+ const email = typeof a.emailAddress === "string" && EMAIL_RE.test(a.emailAddress) ? a.emailAddress : void 0;
867
+ return { id: a.accountUuid.slice(0, 128), ...email ? { email } : {} };
868
+ }
647
869
  } catch {
648
870
  }
649
871
  }
650
- return null;
872
+ return apiAccount(dir);
651
873
  }
652
- function antigravityFacts(file, opts) {
653
- const { head, tail } = ends(file, 128 * 1024, 64 * 1024);
874
+ var ANTHROPIC_HOST = /(^|\.)anthropic\.com$/i;
875
+ function apiAccount(dir) {
876
+ const base = readJson(settingsIn(dir)).env?.ANTHROPIC_BASE_URL;
877
+ if (typeof base !== "string") return null;
878
+ let u;
879
+ try {
880
+ u = new URL(base.trim());
881
+ } catch {
882
+ return null;
883
+ }
884
+ if (u.protocol !== "https:" && u.protocol !== "http:" || !u.hostname || ANTHROPIC_HOST.test(u.hostname))
885
+ return null;
886
+ const origin = `${u.protocol}//${u.host}`;
887
+ return { id: `api-${createHash("sha256").update(origin).digest("hex").slice(0, 16)}`, name: u.host };
888
+ }
889
+ var accountCache = /* @__PURE__ */ new Map();
890
+ function claudeAccount(dir) {
891
+ const hit = accountCache.get(dir);
892
+ if (hit !== void 0) return hit;
893
+ const acct = claudeLogin(dir)?.id ?? null;
894
+ accountCache.set(dir, acct);
895
+ return acct;
896
+ }
897
+ var LINK_RE = /^https:\/\/claude\.ai\/code\/session_[A-Za-z0-9]+$/;
898
+ function transcriptLink(records) {
899
+ for (let i = records.length - 1; i >= 0; i--) {
900
+ const j = records[i];
901
+ if (j.type === "system" && j.subtype === "bridge_status" && typeof j.url === "string" && LINK_RE.test(j.url))
902
+ return j.url;
903
+ if (j.type === "bridge-session" && typeof j.bridgeSessionId === "string") {
904
+ const m = /^(?:cse|session)_([A-Za-z0-9]+)$/.exec(j.bridgeSessionId);
905
+ if (m) return `https://claude.ai/code/session_${m[1]}`;
906
+ }
907
+ }
908
+ return null;
909
+ }
910
+ function appTitle(text) {
911
+ const t = String(text).replace(/\s+/g, " ").trim();
912
+ return t.length > 50 ? `${t.slice(0, 50)}...` : t;
913
+ }
914
+ function askText(j) {
915
+ if (!j || j.type !== "user" || j.isMeta || j.isSidechain) return null;
916
+ const origin = j.origin;
917
+ if (origin?.kind && origin.kind !== "human") return null;
918
+ const c = j.message?.content;
919
+ 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();
920
+ if (!text || isInjected(text)) return null;
921
+ return text;
922
+ }
923
+ function firstAsk(records) {
924
+ for (const j of records) {
925
+ const t = askText(j);
926
+ if (t) return appTitle(t);
927
+ }
928
+ return null;
929
+ }
930
+ function scanTitles(file, from, found) {
931
+ const out = { custom: found.custom ?? null, ai: found.ai ?? null, upto: from };
932
+ let size = 0;
933
+ try {
934
+ size = statSync2(file).size;
935
+ } catch {
936
+ return out;
937
+ }
938
+ if (size < from) {
939
+ out.upto = 0;
940
+ out.custom = out.ai = null;
941
+ from = 0;
942
+ }
943
+ let pos = 0;
944
+ scanLines(
945
+ file,
946
+ (l) => {
947
+ pos += Buffer.byteLength(l, "utf8") + 1;
948
+ if (pos <= from || l.indexOf('-title"') < 0) return null;
949
+ try {
950
+ const j = JSON.parse(l);
951
+ if (j.type === "custom-title" && j.customTitle) out.custom = j.customTitle;
952
+ if (j.type === "ai-title" && j.aiTitle) out.ai = j.aiTitle;
953
+ } catch {
954
+ }
955
+ return null;
956
+ },
957
+ 512 * 1024 * 1024
958
+ );
959
+ out.upto = size;
960
+ return out;
961
+ }
962
+ function claudeFacts(file, sessionId, hints, state) {
963
+ const { head, tail } = ends(file, 64 * 1024, 256 * 1024);
654
964
  const H = parseLines(head);
655
965
  const T = parseLines(tail);
656
966
  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;
967
+ const id = sessionId || path3.basename(file, ".jsonl");
968
+ const mem = state.get(NAME, id);
969
+ let custom = null;
970
+ let ai = null;
971
+ for (const j of H.concat(T)) {
972
+ if (j.type === "custom-title" && typeof j.customTitle === "string") custom = j.customTitle;
973
+ if (j.type === "ai-title" && typeof j.aiTitle === "string") ai = j.aiTitle;
974
+ }
975
+ let size = 0;
976
+ try {
977
+ size = statSync2(file).size;
978
+ } catch {
979
+ }
980
+ if (!custom && !ai && size > 64 * 1024 + 256 * 1024) {
981
+ const s = scanTitles(file, Number(mem.scan_upto) || 0, {
982
+ custom: mem.scan_custom,
983
+ ai: mem.scan_ai
984
+ });
985
+ custom = s.custom;
986
+ ai = s.ai;
987
+ state.save(NAME, id, { scan_upto: s.upto, scan_custom: s.custom, scan_ai: s.ai });
988
+ }
989
+ const dir = configDirOf(file);
990
+ const live = claudeLive(dir).get(id) ?? claudeLiveByPid(dir, hints.claude_pid);
991
+ if (live?.name) state.save(NAME, id, { name: live.name, name_source: live.name_source });
992
+ const bridge = live?.bridge && /^session_[A-Za-z0-9]+$/.test(live.bridge) ? `https://claude.ai/code/${live.bridge}` : null;
993
+ const url = bridge ?? transcriptLink(T) ?? transcriptLink(H) ?? (typeof mem.url === "string" ? mem.url : null);
994
+ if (url) {
995
+ out.url = url;
996
+ state.save(NAME, id, { url });
997
+ }
998
+ const name = live?.name ? { title: live.name, source: live.name_source } : typeof mem.name === "string" ? { title: mem.name, source: mem.name_source } : null;
999
+ const pick = name && name.source === "custom" && name || custom && { title: custom, source: "custom" } || name || ai && { title: ai, source: "harness" };
1000
+ let ask = typeof mem.first_ask === "string" ? mem.first_ask : firstAsk(H);
1001
+ if (!ask && size > 64 * 1024 && !mem.first_ask_none) {
1002
+ ask = scanLines(file, (l) => {
1003
+ try {
1004
+ const t = askText(JSON.parse(l));
1005
+ return t ? appTitle(t) : null;
1006
+ } catch {
1007
+ return null;
1008
+ }
1009
+ });
1010
+ if (!ask && size > 16 * 1024 * 1024) state.save(NAME, id, { first_ask_none: true });
1011
+ }
1012
+ if (ask) {
1013
+ out.first_ask = ask;
1014
+ if (!mem.first_ask) state.save(NAME, id, { first_ask: ask });
1015
+ }
1016
+ if (pick) {
1017
+ out.title = String(pick.title).slice(0, 120);
1018
+ out.title_source = pick.source;
1019
+ } else if (ask) {
1020
+ out.title = ask;
666
1021
  out.title_source = "first-ask";
667
- break;
668
1022
  }
669
- const times = H.concat(T).map((j) => Date.parse(String(j.created_at ?? j.timestamp ?? ""))).filter((n) => n > 0);
1023
+ const acct = claudeAccount(dir);
1024
+ if (acct) out.account_id = acct;
1025
+ for (let i = T.length - 1; i >= 0; i--) {
1026
+ const m = T[i].message?.model;
1027
+ if (T[i]?.type === "assistant" && m && m !== "<synthetic>") {
1028
+ out.model = m;
1029
+ break;
1030
+ }
1031
+ }
1032
+ const meta = [...T].reverse().find((j) => j.version) ?? H.find((j) => j.version);
1033
+ if (meta?.version) out.harness_version = String(meta.version);
1034
+ const times = H.concat(T).map((j) => Date.parse(String(j.timestamp ?? ""))).filter((n) => n > 0);
670
1035
  if (times.length) {
671
1036
  out.started_at = Math.min(...times);
672
1037
  out.last_at = Math.max(...times);
673
1038
  }
1039
+ const withCwd = H.find((j) => typeof j.cwd === "string");
1040
+ if (withCwd) {
1041
+ out.cwd = withCwd.cwd;
1042
+ if (typeof withCwd.gitBranch === "string" && withCwd.gitBranch !== "HEAD") out.branch = withCwd.gitBranch;
1043
+ }
674
1044
  return out;
675
1045
  }
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) => {
1046
+ var REMOTE_RETRY_MS = 6e4;
1047
+ var REMOTE_REFRESH_MS = 30 * 6e4;
1048
+ var CLAUDE_API = "https://api.anthropic.com";
1049
+ var KEYCHAIN = "Claude Code-credentials";
1050
+ var REMOTE_TIMEOUT_MS = 3e3;
1051
+ var KEYCHAIN_TIMEOUT_MS = 2e3;
1052
+ var REMOTE_MAX_BYTES = 1 << 20;
1053
+ var cleanName = (s) => s.replace(/[\p{Cc}\u061C\u200E\u200F\u202A-\u202E\u2066-\u2069]/gu, " ").replace(/\s+/g, " ").trim();
1054
+ async function claudeRemoteFacts(id, local, transcript, state, env, now = Date.now()) {
1055
+ if (!transcript || !local.url || !LINK_RE.test(local.url) || local.title_source === "custom") return {};
1056
+ const mem = state.get(NAME, id);
1057
+ let title = typeof mem.remote_title === "string" ? mem.remote_title : null;
1058
+ if (now - (Number(mem.remote_at) || 0) >= (title ? REMOTE_REFRESH_MS : REMOTE_RETRY_MS)) {
1059
+ state.save(NAME, id, { remote_at: now });
1060
+ const token = claudeAiToken(configDirOf(transcript), env, now);
1061
+ const got = token ? await claudeAiTitle(local.url.slice(local.url.lastIndexOf("/") + 1), token) : void 0;
1062
+ if (got !== void 0) {
1063
+ title = got?.slice(0, 200) ?? null;
1064
+ state.save(NAME, id, { remote_title: title });
1065
+ }
1066
+ }
1067
+ return title ? { title: title.slice(0, 120), title_source: "harness" } : {};
1068
+ }
1069
+ function keychainServices(dir, env) {
1070
+ const named = (p) => `${KEYCHAIN}-${createHash("sha256").update(p.normalize("NFC")).digest("hex").slice(0, 8)}`;
1071
+ const out = /* @__PURE__ */ new Set();
1072
+ if (env.CLAUDE_CONFIG_DIR && path3.resolve(env.CLAUDE_CONFIG_DIR) === dir) out.add(named(env.CLAUDE_CONFIG_DIR));
1073
+ if (dir === path3.join(env.HOME || homeDir(), ".claude")) out.add(KEYCHAIN);
1074
+ out.add(named(dir));
1075
+ return [...out];
1076
+ }
1077
+ var tokenCache = /* @__PURE__ */ new Map();
1078
+ function claudeAiToken(dir, env, now) {
1079
+ const hit = tokenCache.get(dir);
1080
+ if (hit && hit.expires > now + 6e4) return hit.token;
1081
+ const raw = [];
1082
+ if (process.platform === "darwin")
1083
+ for (const service of keychainServices(dir, env)) {
1084
+ try {
1085
+ const user = env.USER || userInfo().username;
1086
+ const account = /^[\w.-]+$/.test(user) ? user : "claude-code-user";
1087
+ raw.push(
1088
+ execFileSync2("security", ["find-generic-password", "-a", account, "-w", "-s", service], {
1089
+ encoding: "utf8",
1090
+ timeout: KEYCHAIN_TIMEOUT_MS,
1091
+ stdio: ["ignore", "pipe", "ignore"]
1092
+ })
1093
+ );
1094
+ break;
1095
+ } catch {
1096
+ }
1097
+ }
1098
+ try {
1099
+ raw.push(readFileSync2(path3.join(dir, ".credentials.json"), "utf8"));
1100
+ } catch {
1101
+ }
1102
+ for (const r of raw) {
691
1103
  try {
692
- return execFileSync("git", ["-C", cwd, ...args], {
693
- encoding: "utf8",
694
- timeout: 1500,
695
- stdio: ["ignore", "pipe", "ignore"]
696
- }).trim();
1104
+ const t = r.trim();
1105
+ const o = JSON.parse(/^(?:[0-9a-f]{2})+$/i.test(t) ? Buffer.from(t, "hex").toString("utf8") : t).claudeAiOauth;
1106
+ if (typeof o?.accessToken !== "string" || !o.accessToken) continue;
1107
+ const expires = typeof o.expiresAt === "number" ? o.expiresAt : Number.POSITIVE_INFINITY;
1108
+ if (expires <= now + 6e4) continue;
1109
+ tokenCache.set(dir, { token: o.accessToken, expires });
1110
+ return o.accessToken;
697
1111
  } catch {
698
- return "";
699
1112
  }
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;
1113
+ }
1114
+ return null;
708
1115
  }
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$/, "");
1116
+ async function claudeAiTitle(bridge, token) {
1117
+ try {
1118
+ const r = await fetch(`${CLAUDE_API}/v1/code/sessions/${bridge}`, {
1119
+ headers: { Authorization: `Bearer ${token}`, "anthropic-version": "2023-06-01", "User-Agent": "sessionpipe" },
1120
+ redirect: "error",
1121
+ signal: AbortSignal.timeout(REMOTE_TIMEOUT_MS)
1122
+ });
1123
+ if (r.status !== 200 || Number(r.headers.get("content-length")) > REMOTE_MAX_BYTES) return void 0;
1124
+ const text = await r.text();
1125
+ if (text.length > REMOTE_MAX_BYTES) return void 0;
1126
+ const j = JSON.parse(text);
1127
+ const s = j?.response_shape ?? j?.session ?? j;
1128
+ const bare = (x) => x.replace(/^(?:session|cse)_/, "");
1129
+ if (typeof s?.id !== "string" || bare(s.id) !== bare(bridge)) return void 0;
1130
+ return typeof s.title === "string" && cleanName(s.title) || null;
1131
+ } catch {
1132
+ return void 0;
1133
+ }
713
1134
  }
714
1135
 
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);
1136
+ // ../core/src/adapters/headless.ts
1137
+ import { accessSync, constants, statSync as statSync3 } from "node:fs";
1138
+ import path4 from "node:path";
1139
+ var JOB_TIMEOUT_MS = 30 * 6e4;
1140
+ var OUT_CAP = 1024 * 1024;
1141
+ function isExe(p) {
752
1142
  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
- }
1143
+ if (!statSync3(p).isFile()) return false;
1144
+ accessSync(p, constants.X_OK);
1145
+ return true;
758
1146
  } catch {
1147
+ return false;
759
1148
  }
760
- return out.filter((d, i) => i === 0 || existsSync3(d));
761
1149
  }
762
- var settingsIn = (dir) => path4.join(dir, "settings.json");
763
- var configDirOf = (transcript) => path4.dirname(path4.dirname(path4.dirname(transcript)));
1150
+ function findBinary(o) {
1151
+ if (o.override && isExe(o.override)) return o.override;
1152
+ for (const d of String(o.env?.PATH || "").split(path4.delimiter)) {
1153
+ if (!d || !path4.isAbsolute(d)) continue;
1154
+ for (const n of o.names) if (isExe(path4.join(d, n))) return path4.join(d, n);
1155
+ }
1156
+ for (const d of o.dirs) for (const n of o.names) if (isExe(path4.join(d, n))) return path4.join(d, n);
1157
+ return null;
1158
+ }
1159
+
1160
+ // ../core/src/adapters/antigravity.ts
1161
+ import {
1162
+ existsSync as existsSync3,
1163
+ mkdirSync as mkdirSync2,
1164
+ readdirSync as readdirSync3,
1165
+ readFileSync as readFileSync3,
1166
+ realpathSync,
1167
+ statSync as statSync4,
1168
+ unlinkSync as unlinkSync2,
1169
+ writeFileSync as writeFileSync3
1170
+ } from "node:fs";
1171
+ import path5 from "node:path";
1172
+ var NAME2 = "antigravity";
1173
+ var HOOK_NAME = "sessionpipe";
1174
+ var AG_EVENTS = ["PreInvocation", "PostInvocation", "PostToolUse", "Stop"];
1175
+ 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';
1176
+ var antigravityAnswer = (event) => event === "PreToolUse" ? AG_STALE_PRETOOL : "{}\n";
1177
+ var gemini = (env) => env.GEMINI_CLI_HOME || path5.join(homeDir(), ".gemini");
1178
+ var hooksFile = (env) => path5.join(gemini(env), "config", "hooks.json");
1179
+ var projectsDir = (env) => path5.join(gemini(env), "config", "projects");
1180
+ var dataDirs = (env) => ["antigravity", "antigravity-cli", "antigravity-ide"].map((d) => path5.join(gemini(env), d));
1181
+ var ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
764
1182
  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 = {
1183
+ function entry(cmd) {
1184
+ const handler = (e) => ({ type: "command", command: cmd([NAME2, e]).command, timeout: 5 });
1185
+ const out = { enabled: true };
1186
+ for (const e of AG_EVENTS) out[e] = /Tool/.test(e) ? [{ matcher: "*", hooks: [handler(e)] }] : [handler(e)];
1187
+ return out;
1188
+ }
1189
+ var antigravity = {
768
1190
  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
- })),
1191
+ events: AG_EVENTS,
1192
+ detect: (env = process.env) => existsSync3(path5.join(gemini(env), "config")) || dataDirs(env).some((d) => existsSync3(d)),
1193
+ configFiles: (env = process.env) => [hooksFile(env)],
1194
+ install(cmd, env = process.env) {
1195
+ const file = hooksFile(env);
1196
+ const f = readJsonFile(file);
1197
+ if (!f.ok) return [{ file, changed: false, skipped: true, note: `${f.reason}; not touched` }];
1198
+ const hooks = f.value;
1199
+ const e = entry(cmd);
1200
+ if (JSON.stringify(hooks[HOOK_NAME]) === JSON.stringify(e)) return [{ file, changed: false }];
1201
+ hooks[HOOK_NAME] = e;
1202
+ mkdirSync2(path5.dirname(file), { recursive: true });
1203
+ writeFileSync3(file, formatJson(hooks, f));
1204
+ return [
1205
+ {
1206
+ file,
1207
+ changed: true,
1208
+ created: !f.existed,
1209
+ note: "hooks.json is read when a conversation starts; open ones report after a restart"
1210
+ }
1211
+ ];
1212
+ },
1213
+ uninstall(env = process.env, opts = {}) {
1214
+ const file = hooksFile(env);
1215
+ if (!existsSync3(file)) return [{ file, changed: false }];
1216
+ const f = readJsonFile(file);
1217
+ if (!f.ok) return [{ file, changed: false, skipped: true, note: `${f.reason}; not touched` }];
1218
+ const hooks = f.value;
1219
+ if (!(HOOK_NAME in hooks)) return [{ file, changed: false }];
1220
+ delete hooks[HOOK_NAME];
1221
+ if (opts.created?.includes(file) && !Object.keys(hooks).length) {
1222
+ unlinkSync2(file);
1223
+ return [{ file, changed: true, note: "removed (sessionpipe created it)" }];
1224
+ }
1225
+ writeFileSync3(file, formatJson(hooks, f));
1226
+ return [{ file, changed: true }];
1227
+ },
1228
+ installed(cmd, env = process.env) {
1229
+ const file = hooksFile(env);
1230
+ const f = readJsonFile(file);
1231
+ const have = f.ok ? f.value[HOOK_NAME] : void 0;
1232
+ return [
1233
+ {
1234
+ file,
1235
+ state: !have ? "missing" : JSON.stringify(have) === JSON.stringify(entry(cmd)) ? "current" : "stale"
1236
+ }
1237
+ ];
1238
+ },
780
1239
  fromHook(input) {
781
1240
  const event = input.argv[1] ?? "";
782
1241
  let s = {};
@@ -785,155 +1244,48 @@ var claudeCode = {
785
1244
  if (j && typeof j === "object" && !Array.isArray(j)) s = j;
786
1245
  } catch {
787
1246
  }
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)}`;
1247
+ const id = s.conversationId ?? s.session_id ?? s.sessionId;
1248
+ const stdout = antigravityAnswer(event);
1249
+ if (!id) return { session: { id: "" }, events: [], stdout };
1250
+ const workspaces = agWorkspaces(s.workspacePaths);
1251
+ const session = { id: String(id), cwd: workspaces[0] ?? input.cwd };
1252
+ if (typeof s.modelName === "string" && s.modelName !== "auto") session.model = s.modelName;
1253
+ const toolCall = s.toolCall;
1254
+ const tool = validTool2(toolCall?.name) ? toolCall?.name : validTool2(s.toolName) ? s.toolName : void 0;
801
1255
  const ev = (type, data) => ({ type, data: prune2(data), harnessEvent: event });
802
1256
  const events = [];
803
1257
  switch (event) {
804
- case "SessionStart":
805
- events.push(ev("session.started", { source: pickSource(s.source ?? s.trigger) }));
1258
+ case "PreInvocation":
1259
+ if (Number(s.invocationNum) === 0 || s.invocationNum === void 0) {
1260
+ events.push(ev("session.started", { source: "unknown" }));
1261
+ events.push(ev("turn.started", {}));
1262
+ } else events.push(ev("session.heartbeat", {}));
806
1263
  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
- );
1264
+ case "PostInvocation":
827
1265
  break;
828
- }
829
1266
  case "PreToolUse":
830
- if (tool) events.push(ev("tool.started", { tool, call_id, turn_id, agent_id, input: s.tool_input }));
1267
+ if (tool) events.push(ev("tool.started", { tool, input: toolCall?.args }));
831
1268
  break;
832
1269
  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)
1270
+ if (!tool) events.push(ev("session.heartbeat", {}));
1271
+ else
849
1272
  events.push(
850
1273
  ev("tool.ended", {
851
1274
  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
1275
+ ok: !s.error,
1276
+ error: typeof s.error === "string" ? s.error.slice(0, 2e3) : void 0,
1277
+ input: toolCall?.args
860
1278
  })
861
1279
  );
862
1280
  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
1281
  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) }));
1282
+ events.push(ev("turn.ended", { reason: s.error ? "error" : "stop" }));
924
1283
  break;
925
1284
  default:
926
- return null;
1285
+ return { session, events: [], stdout };
927
1286
  }
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;
1287
+ const r = { session, events, stdout, hints: { workspaces } };
1288
+ if (typeof s.transcriptPath === "string") r.transcript = s.transcriptPath;
937
1289
  return r;
938
1290
  },
939
1291
  readTranscript(file, fromLine) {
@@ -943,355 +1295,162 @@ var claudeCode = {
943
1295
  } catch {
944
1296
  return { turns: [], line: fromLine };
945
1297
  }
946
- return readClaude(text.split("\n"), fromLine);
1298
+ return readAntigravity(text.split("\n"), fromLine);
947
1299
  },
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;
1300
+ facts(session, transcript, hints) {
1301
+ if (!transcript) return {};
1302
+ return antigravityFacts(transcript, {
1303
+ conversation: session.id,
1304
+ workspaces: hints.workspaces ?? (session.cwd ? [session.cwd] : []),
1305
+ env: process.env
1306
+ });
956
1307
  },
957
1308
  backfill(sinceMs, env = process.env) {
958
1309
  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) => {
1310
+ for (const data of dataDirs(env)) {
1311
+ let ids = [];
1071
1312
  try {
1072
- const st = readFileSync3(`/proc/${pid}/stat`, "utf8");
1073
- return Number(st.slice(st.lastIndexOf(")") + 2).split(" ")[1]) || 0;
1313
+ ids = readdirSync3(path5.join(data, "brain")).filter((n) => /^[0-9a-f-]{36}$/i.test(n));
1074
1314
  } catch {
1075
- return 0;
1076
1315
  }
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]));
1316
+ for (const id of ids) {
1317
+ const f = path5.join(data, "brain", id, ".system_generated", "logs", "transcript.jsonl");
1318
+ try {
1319
+ if (statSync4(f).mtimeMs < sinceMs) continue;
1320
+ } catch {
1321
+ continue;
1322
+ }
1323
+ const facts = antigravityFacts(f, { conversation: id, workspaces: [], env });
1324
+ if (!facts.started_at || !facts.last_at) continue;
1325
+ const { started_at, last_at, first_ask: _fa, harness_version: _hv, ...rest } = facts;
1326
+ rows2.push({ session: { id, ...rest }, started_at, last_at });
1327
+ }
1090
1328
  }
1091
- } catch {
1329
+ return rows2;
1092
1330
  }
1093
- return (pid) => table.get(pid) || 0;
1331
+ };
1332
+ function prune2(o) {
1333
+ for (const k of Object.keys(o)) if (o[k] === void 0) delete o[k];
1334
+ return o;
1094
1335
  }
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) {
1336
+ function agPath(p) {
1337
+ if (typeof p !== "string" || !p) return null;
1338
+ let s = p;
1339
+ if (/^file:/i.test(s)) {
1100
1340
  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
- }
1341
+ s = decodeURIComponent(new URL(s).pathname);
1106
1342
  } catch {
1343
+ return null;
1107
1344
  }
1108
- }
1109
- return apiAccount(dir);
1345
+ if (process.platform === "win32" && /^\/[A-Za-z]:/.test(s)) s = s.slice(1);
1346
+ } else if (/^[a-z][a-z0-9+.-]*:\/\//i.test(s)) return null;
1347
+ if (!path5.isAbsolute(s)) return null;
1348
+ return path5.resolve(s);
1110
1349
  }
1111
- var ANTHROPIC_HOST = /(^|\.)anthropic\.com$/i;
1112
- function apiAccount(dir) {
1113
- let base;
1350
+ var agWorkspaces = (paths) => (Array.isArray(paths) ? paths : []).map(agPath).filter((p) => !!p).filter((p, i, a) => a.indexOf(p) === i);
1351
+ function agVariants(p) {
1352
+ const out = [p];
1114
1353
  try {
1115
- base = JSON.parse(readFileSync3(settingsIn(dir), "utf8")).env?.ANTHROPIC_BASE_URL;
1354
+ const r = realpathSync.native(p);
1355
+ if (r !== p) out.push(r);
1116
1356
  } catch {
1117
- return null;
1118
1357
  }
1119
- if (typeof base !== "string") return null;
1120
- let u;
1358
+ return out;
1359
+ }
1360
+ function agProjects(dir) {
1361
+ const out = [];
1362
+ let names = [];
1121
1363
  try {
1122
- u = new URL(base.trim());
1364
+ names = readdirSync3(dir).filter((n) => n.endsWith(".json"));
1123
1365
  } catch {
1124
- return null;
1366
+ return out;
1125
1367
  }
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]}`;
1368
+ for (const n of names) {
1369
+ const id = n.slice(0, -5);
1370
+ if (!ID_RE.test(id)) continue;
1371
+ let j;
1372
+ try {
1373
+ j = JSON.parse(readFileSync3(path5.join(dir, n), "utf8"));
1374
+ } catch {
1375
+ continue;
1376
+ }
1377
+ const folders = [];
1378
+ for (const r of j?.projectResources?.resources ?? []) {
1379
+ const f = agPath(r?.folderUri ?? r?.gitFolder?.folderUri);
1380
+ if (f) folders.push(...agVariants(f));
1148
1381
  }
1382
+ if (folders.length) out.push({ id, folders });
1149
1383
  }
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;
1384
+ return out;
1164
1385
  }
1165
- function firstAsk(records) {
1166
- for (const j of records) {
1167
- const t = askText(j);
1168
- if (t) return appTitle(t);
1386
+ function agMatchProject(projects, workspaces) {
1387
+ const ws = [];
1388
+ for (const w of workspaces) {
1389
+ const p = agPath(w);
1390
+ if (p) ws.push(...agVariants(p));
1169
1391
  }
1170
- return null;
1392
+ let best = null;
1393
+ let bestLen = -1;
1394
+ for (const pr of projects)
1395
+ for (const f of pr.folders) {
1396
+ const within = ws.some((w) => w === f || w.startsWith(f.endsWith(path5.sep) ? f : f + path5.sep));
1397
+ if (within && f.length > bestLen) {
1398
+ best = pr.id;
1399
+ bestLen = f.length;
1400
+ }
1401
+ }
1402
+ return best;
1171
1403
  }
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;
1404
+ function antigravityRemoteUrl(transcript, opts) {
1405
+ const i = transcript.indexOf(`${path5.sep}brain${path5.sep}`);
1406
+ const dirs = (i > 0 ? [transcript.slice(0, i)] : []).concat(dataDirs(opts.env));
1407
+ for (const d of dirs) {
1408
+ try {
1409
+ const m = /installation_uuid:\s*"([0-9a-f-]{36})"/i.exec(
1410
+ readFileSync3(path5.join(d, "antigravity_state.pbtxt"), "utf8")
1411
+ );
1412
+ if (!m) continue;
1413
+ const base = `https://antigravity.google.com/r/${m[1]}-v2`;
1414
+ const conv = opts.conversation ?? "";
1415
+ if (!ID_RE.test(conv) || !opts.workspaces?.length) return base;
1416
+ const project = agMatchProject(agProjects(opts.projectsDir ?? projectsDir(opts.env)), opts.workspaces);
1417
+ return project ? `${base}?p=${encodeURIComponent(`c/${conv}?section=${project}`)}` : base;
1418
+ } catch {
1419
+ }
1184
1420
  }
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;
1421
+ return null;
1203
1422
  }
1204
- function claudeFacts(file, sessionId, hints, state) {
1205
- const { head, tail } = ends(file, 64 * 1024, 256 * 1024);
1423
+ function antigravityFacts(file, opts) {
1424
+ const { head, tail } = ends(file, 128 * 1024, 64 * 1024);
1206
1425
  const H = parseLines(head);
1207
1426
  const T = parseLines(tail);
1208
1427
  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;
1428
+ const url = antigravityRemoteUrl(file, opts);
1429
+ if (url) out.url = url;
1430
+ for (const j of H) {
1431
+ if (j.type !== "USER_INPUT" || typeof j.content !== "string") continue;
1432
+ const m = /<USER_REQUEST>([\s\S]*?)(?:<\/USER_REQUEST>|$)/.exec(j.content);
1433
+ const text = (m ? m[1] : j.content).trim();
1434
+ const line = (text.split("\n").find((l) => l.trim()) ?? "").trim();
1435
+ if (!line || /^<[a-z_-]+>/i.test(line)) continue;
1436
+ out.title = line.length > 80 ? `${line.slice(0, 80).replace(/\s+\S*$/, "")}\u2026` : line;
1263
1437
  out.title_source = "first-ask";
1438
+ break;
1264
1439
  }
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);
1440
+ const times = H.concat(T).map((j) => Date.parse(String(j.created_at ?? j.timestamp ?? ""))).filter((n) => n > 0);
1277
1441
  if (times.length) {
1278
1442
  out.started_at = Math.min(...times);
1279
1443
  out.last_at = Math.max(...times);
1280
1444
  }
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
1445
  return out;
1287
1446
  }
1288
1447
 
1289
1448
  // ../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";
1449
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
1450
+ import path6 from "node:path";
1292
1451
 
1293
1452
  // ../core/src/adapters/codex-config.ts
1294
- var NOTIFY_LINE = /^[ \t]*notify[ \t]*=[ \t]*(\[.*\])[ \t]*(?:#.*)?$/m;
1453
+ var NOTIFY_LINE = /^[ \t]*notify[ \t]*=[ \t]*(\[.*\])[ \t]*(?:#.*)?(?=\r?$)/m;
1295
1454
  var NOTIFY_KEY = /^[ \t]*notify[ \t]*=/m;
1296
1455
  var OURS2 = /(?:hook\.js|sessionpipe-hook)",\s*"codex",\s*"notify"/;
1297
1456
  function scan(line, st) {
@@ -1381,10 +1540,11 @@ function dropLine(text, index, length) {
1381
1540
  }
1382
1541
  function insertTopLevel(text, line) {
1383
1542
  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}`;
1543
+ const body = head.replace(/\s*$/, "");
1544
+ const eol = (body.includes("\n") ? body : text).includes("\r\n") ? "\r\n" : "\n";
1545
+ const top = head.trim() ? head.replace(/\s*$/, eol) : "";
1546
+ if (!tail) return top + line + eol;
1547
+ return `${top + line}${eol}${eol}${tail}`;
1388
1548
  }
1389
1549
  function withNotify(text, line) {
1390
1550
  const n = readNotify(text);
@@ -1433,10 +1593,10 @@ var CODEX_EVENTS = [
1433
1593
  ];
1434
1594
  var OPTS2 = { matcherFor: (e) => /Tool|Permission/.test(e) ? "" : void 0, timeout: 5 };
1435
1595
  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");
1596
+ var codexHome = (env) => env.CODEX_HOME || path6.join(homeDir(), ".codex");
1597
+ var hooksFile2 = (env) => path6.join(codexHome(env), "hooks.json");
1598
+ var configFile = (env) => path6.join(codexHome(env), "config.toml");
1599
+ var sessionsDir = (env) => path6.join(codexHome(env), "sessions");
1440
1600
  var validTool3 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
1441
1601
  var endReason2 = (r) => r === "clear" || r === "logout" || r === "exit" ? r : "other";
1442
1602
  function installNotify(cmd, env) {
@@ -1450,8 +1610,8 @@ function installNotify(cmd, env) {
1450
1610
  const line = `notify = ${JSON.stringify(argv)}`;
1451
1611
  const n = readNotify(text);
1452
1612
  const write = (next) => {
1453
- mkdirSync3(path5.dirname(file), { recursive: true });
1454
- writeFileSync3(file, next);
1613
+ mkdirSync3(path6.dirname(file), { recursive: true });
1614
+ writeFileSync4(file, next);
1455
1615
  };
1456
1616
  if (n && !n.other) {
1457
1617
  if (!n.misplaced && JSON.stringify(n.argv) === JSON.stringify(argv))
@@ -1496,12 +1656,12 @@ function uninstallNotify(env) {
1496
1656
  const n = readNotify(text);
1497
1657
  if (!n) return { file, changed: false };
1498
1658
  if (!n.other) {
1499
- writeFileSync3(file, withoutNotify(text));
1659
+ writeFileSync4(file, withoutNotify(text));
1500
1660
  return { file, changed: true };
1501
1661
  }
1502
1662
  const prev = previousNotify(n.argv);
1503
1663
  if (isComputerUse(n.argv) && prev && OURS2.test(JSON.stringify(prev))) {
1504
- writeFileSync3(file, replaceNotify(text, n, `notify = ${stripPreviousRaw(n.raw)}`));
1664
+ writeFileSync4(file, replaceNotify(text, n, `notify = ${stripPreviousRaw(n.raw)}`));
1505
1665
  return { file, changed: true };
1506
1666
  }
1507
1667
  return { file, changed: false };
@@ -1517,7 +1677,7 @@ function findRollout(threadId, env = process.env) {
1517
1677
  return;
1518
1678
  }
1519
1679
  for (const n of names.sort().reverse()) {
1520
- const p = path5.join(dir, n);
1680
+ const p = path6.join(dir, n);
1521
1681
  if (n.endsWith(`-${threadId}.jsonl`)) {
1522
1682
  best = p;
1523
1683
  return;
@@ -1702,7 +1862,7 @@ var codex = {
1702
1862
  backfill(sinceMs, env = process.env) {
1703
1863
  const rows2 = [];
1704
1864
  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));
1865
+ const m = /([0-9a-f]{8}-[0-9a-f-]{27,})\.jsonl$/i.exec(path6.basename(f));
1706
1866
  if (!m?.[1]) continue;
1707
1867
  if (codexSubagent(rolloutMeta(f))) continue;
1708
1868
  const facts = codexFacts(f);
@@ -1711,12 +1871,40 @@ var codex = {
1711
1871
  rows2.push({ session: { id: m[1], ...rest }, started_at, last_at });
1712
1872
  }
1713
1873
  return rows2;
1714
- }
1874
+ },
1875
+ limits: (transcript) => codexLimits(transcript)
1715
1876
  };
1877
+ var epochIso2 = (t) => typeof t === "number" && t > 0 && t < 1e11 ? new Date(t * 1e3).toISOString() : void 0;
1716
1878
  function prune3(o) {
1717
1879
  for (const k of Object.keys(o)) if (o[k] === void 0) delete o[k];
1718
1880
  return o;
1719
1881
  }
1882
+ function codexLimits(file) {
1883
+ const { tail } = ends(file, 0, 256 * 1024);
1884
+ for (const r of parseLines(tail).reverse()) {
1885
+ const p = r.payload;
1886
+ if (r.type !== "event_msg" || p?.type !== "token_count") continue;
1887
+ if (!p.rate_limits || typeof p.rate_limits !== "object") continue;
1888
+ const rl = p.rate_limits;
1889
+ if (rl.limit_id != null && rl.limit_id !== "codex") continue;
1890
+ const windows = [];
1891
+ for (const id of ["primary", "secondary"]) {
1892
+ const w = rl[id];
1893
+ if (typeof w?.used_percent !== "number" || w.used_percent < 0) continue;
1894
+ windows.push(
1895
+ prune3({
1896
+ id,
1897
+ used_percent: w.used_percent,
1898
+ window_minutes: Number.isInteger(w.window_minutes) && Number(w.window_minutes) > 0 ? Number(w.window_minutes) : void 0,
1899
+ resets_at: epochIso2(w.resets_at)
1900
+ })
1901
+ );
1902
+ }
1903
+ if (!windows.length) return null;
1904
+ return prune3({ windows, plan: typeof rl.plan_type === "string" ? rl.plan_type.slice(0, 64) : void 0 });
1905
+ }
1906
+ return null;
1907
+ }
1720
1908
  function codexFacts(file) {
1721
1909
  const { head, tail } = ends(file, 128 * 1024, 64 * 1024);
1722
1910
  let H = parseLines(head);
@@ -1761,7 +1949,7 @@ function codexFacts(file) {
1761
1949
  // ../core/src/adapters/devin.ts
1762
1950
  import { createHash as createHash2 } from "node:crypto";
1763
1951
  import { existsSync as existsSync6, readlinkSync } from "node:fs";
1764
- import path6 from "node:path";
1952
+ import path7 from "node:path";
1765
1953
 
1766
1954
  // ../core/src/readers/sqlite.ts
1767
1955
  import { execFileSync as execFileSync3 } from "node:child_process";
@@ -1945,11 +2133,11 @@ function foreignEvent(file) {
1945
2133
  return Object.keys(hooks).find((k) => !ok.includes(k));
1946
2134
  }
1947
2135
  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");
2136
+ var appData = (env) => env.APPDATA || path7.join(homeOf(env), "AppData", "Roaming");
2137
+ 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");
2138
+ var configFile2 = (env) => path7.join(devinConfigDir(env), "config.json");
2139
+ 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");
2140
+ var sessionsDb = (env) => path7.join(devinDataDir(env), "sessions.db");
1953
2141
  var SESSION_ID = /^[A-Za-z0-9._:-]{1,128}$/;
1954
2142
  var validTool4 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
1955
2143
  var endReason3 = (r) => r === "clear" || r === "logout" || r === "exit" ? r : "other";
@@ -2121,7 +2309,7 @@ var BACKFILL_SQL = `SELECT s.id, ${SESSION_FIELDS} FROM sessions s
2121
2309
  function sessionFacts(row) {
2122
2310
  const out = {};
2123
2311
  if (typeof row.model === "string" && row.model) out.model = row.model;
2124
- if (typeof row.working_directory === "string" && path6.isAbsolute(row.working_directory))
2312
+ if (typeof row.working_directory === "string" && path7.isAbsolute(row.working_directory))
2125
2313
  out.cwd = row.working_directory;
2126
2314
  const started = Number(row.created_at);
2127
2315
  const last = Number(row.last_activity_at);
@@ -2166,8 +2354,8 @@ function readDevinChain(db, session, fromLine) {
2166
2354
  }
2167
2355
  function devinVersion(env = process.env) {
2168
2356
  try {
2169
- const target = readlinkSync(path6.join(devinDataDir(env), "_versions", "current"));
2170
- const name = path6.basename(target);
2357
+ const target = readlinkSync(path7.join(devinDataDir(env), "_versions", "current"));
2358
+ const name = path7.basename(target);
2171
2359
  return /^[0-9][0-9A-Za-z.+-]{0,63}$/.test(name) ? name : void 0;
2172
2360
  } catch {
2173
2361
  return void 0;
@@ -2182,7 +2370,7 @@ function devinAccount(env = process.env) {
2182
2370
  // ../core/src/adapters/gemini-cli.ts
2183
2371
  import { execFileSync as execFileSync4 } from "node:child_process";
2184
2372
  import { existsSync as existsSync7, readFileSync as readFileSync5 } from "node:fs";
2185
- import path7 from "node:path";
2373
+ import path8 from "node:path";
2186
2374
  var NAME5 = "gemini-cli";
2187
2375
  var GEMINI_EVENTS = [
2188
2376
  "SessionStart",
@@ -2195,26 +2383,31 @@ var GEMINI_EVENTS = [
2195
2383
  "SessionEnd"
2196
2384
  ];
2197
2385
  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");
2386
+ var geminiDir = (env) => env.GEMINI_CLI_HOME || path8.join(homeDir(), ".gemini");
2387
+ var settings = (env) => path8.join(geminiDir(env), "settings.json");
2200
2388
  var validTool5 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
2201
2389
  var eventsFor4 = (lean) => lean ? GEMINI_EVENTS.filter((e) => e !== "BeforeTool") : [...GEMINI_EVENTS];
2390
+ function geminiVersion(platform = process.platform, env = process.env) {
2391
+ const win = platform === "win32";
2392
+ const bin = win ? findBinary({ names: ["gemini.exe", "gemini.cmd"], dirs: [path8.dirname(process.execPath)], env }) : "gemini";
2393
+ if (!bin) throw new Error("no gemini on PATH");
2394
+ const o = {
2395
+ encoding: "utf8",
2396
+ timeout: 4e3,
2397
+ stdio: ["ignore", "pipe", "ignore"],
2398
+ env
2399
+ };
2400
+ const out = /\.cmd$/i.test(bin) ? execFileSync4(`"${bin}" --version`, { ...o, shell: true }) : execFileSync4(bin, ["--version"], o);
2401
+ return out.trim();
2402
+ }
2202
2403
  function geminiHooksActive(settingsFile) {
2203
- let tools;
2204
- try {
2205
- tools = JSON.parse(readFileSync5(settingsFile, "utf8")).tools;
2206
- } catch {
2207
- }
2404
+ const tools = readJson(settingsFile).tools;
2208
2405
  if (tools?.enableHooks === true) return { active: true };
2209
2406
  if (tools?.enableHooks === false)
2210
2407
  return { active: false, note: "inactive: tools.enableHooks is false in settings.json" };
2211
2408
  let version = "";
2212
2409
  try {
2213
- version = execFileSync4("gemini", ["--version"], {
2214
- encoding: "utf8",
2215
- timeout: 4e3,
2216
- stdio: ["ignore", "pipe", "ignore"]
2217
- }).trim();
2410
+ version = geminiVersion();
2218
2411
  } catch {
2219
2412
  return {
2220
2413
  active: true,
@@ -2232,7 +2425,7 @@ var geminiCli = {
2232
2425
  name: NAME5,
2233
2426
  events: GEMINI_EVENTS,
2234
2427
  // ~/.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")),
2428
+ detect: (env = process.env) => existsSync7(settings(env)) || existsSync7(path8.join(geminiDir(env), "tmp")),
2236
2429
  configFiles: (env = process.env) => [settings(env)],
2237
2430
  install: (cmd, env = process.env, opts = {}) => {
2238
2431
  const r = installClaudeShaped(settings(env), NAME5, eventsFor4(opts.lean), cmd, OPTS4);
@@ -2445,8 +2638,8 @@ async function runAdapterFixtures(files) {
2445
2638
 
2446
2639
  // src/cli.ts
2447
2640
  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] : [];
2641
+ const p = path9.join(d, n);
2642
+ return statSync5(p).isDirectory() ? walk(p) : p.endsWith(".json") ? [p] : [];
2450
2643
  });
2451
2644
  async function main(argv, io = process) {
2452
2645
  const target = argv[0];