sessionpipe-conformance 0.9.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.9.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,310 +507,735 @@ 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;
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
- },
520
- readTranscript(file, fromLine) {
521
- let text = "";
522
- try {
523
- text = readFileSync2(file, "utf8");
524
- } catch {
525
- return { turns: [], line: fromLine };
526
- }
527
- return readAntigravity(text.split("\n"), fromLine);
528
- },
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
- });
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
+ }
653
+ default:
654
+ return null;
655
+ }
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;
665
+ return r;
666
+ },
667
+ readTranscript(file, fromLine) {
668
+ let text = "";
669
+ try {
670
+ text = readFileSync2(file, "utf8");
671
+ } catch {
672
+ return { turns: [], line: fromLine };
673
+ }
674
+ return readClaude(text.split("\n"), fromLine);
675
+ },
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;
1148
+ }
1149
+ }
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);
759
1155
  }
760
- return out.filter((d, i) => i === 0 || existsSync3(d));
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;
761
1158
  }
762
- var settingsIn = (dir) => path4.join(dir, "settings.json");
763
- var configDirOf = (transcript) => path4.dirname(path4.dirname(path4.dirname(transcript)));
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,133 +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
- events.push(
809
- ev("turn.started", {
810
- turn_id,
811
- prompt_chars: typeof s.prompt === "string" ? [...s.prompt].length : void 0
812
- })
813
- );
1264
+ case "PostInvocation":
814
1265
  break;
815
1266
  case "PreToolUse":
816
- 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 }));
817
1268
  break;
818
1269
  case "PostToolUse":
819
- if (tool)
820
- events.push(
821
- ev("tool.ended", {
822
- tool,
823
- call_id,
824
- turn_id,
825
- agent_id,
826
- ms,
827
- ok: true,
828
- input: s.tool_input,
829
- output: s.tool_response
830
- })
831
- );
832
- break;
833
- case "PostToolUseFailure":
834
- if (tool)
1270
+ if (!tool) events.push(ev("session.heartbeat", {}));
1271
+ else
835
1272
  events.push(
836
1273
  ev("tool.ended", {
837
1274
  tool,
838
- call_id,
839
- turn_id,
840
- agent_id,
841
- ms,
842
- ok: false,
843
- error: errText(s.error ?? s.tool_response),
844
- input: s.tool_input,
845
- 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
846
1278
  })
847
1279
  );
848
1280
  break;
849
- case "PermissionRequest":
850
- events.push(
851
- ev("attention.needed", {
852
- attention_id: call_id ?? permissionId(),
853
- kind: "permission",
854
- tool,
855
- message: tool ? `${tool}${describe(s.tool_input)}` : void 0
856
- })
857
- );
858
- break;
859
- case "Notification":
860
- events.push(
861
- ev("attention.needed", {
862
- attention_id: `notif-${Date.now()}`,
863
- kind: notifKind(s.notification_type),
864
- message: typeof s.message === "string" ? s.message.slice(0, 200) : void 0
865
- })
866
- );
867
- break;
868
1281
  case "Stop":
869
- events.push(ev("turn.ended", { turn_id, reason: "stop" }));
870
- break;
871
- case "StopFailure":
872
- events.push(
873
- ev("turn.ended", {
874
- turn_id,
875
- reason: "error",
876
- error: typeof s.error === "string" ? s.error.slice(0, 200) : void 0
877
- })
878
- );
879
- break;
880
- case "SubagentStart":
881
- events.push(
882
- ev("subagent.started", {
883
- agent_id: String(s.agent_id ?? ""),
884
- agent_type: typeof s.agent_type === "string" ? s.agent_type : void 0
885
- })
886
- );
887
- break;
888
- case "SubagentStop":
889
- events.push(
890
- ev("subagent.ended", {
891
- agent_id: String(s.agent_id ?? ""),
892
- agent_type: typeof s.agent_type === "string" ? s.agent_type : void 0
893
- })
894
- );
895
- break;
896
- case "PreCompact":
897
- events.push(ev("context.compacted", { trigger: s.trigger === "manual" ? "manual" : "auto" }));
898
- break;
899
- case "PostCompact":
900
- break;
901
- // PreCompact already said it; a second event would double-count
902
- case "SessionEnd":
903
- events.push(ev("session.ended", { reason: endReason(s.reason) }));
1282
+ events.push(ev("turn.ended", { reason: s.error ? "error" : "stop" }));
904
1283
  break;
905
1284
  default:
906
- return null;
1285
+ return { session, events: [], stdout };
907
1286
  }
908
- const hints = {};
909
- if (event === "UserPromptSubmit" && typeof s.prompt === "string") hints.prompt = s.prompt;
910
- const host = input.env.CLAUDE_CODE_HOST_SESSION_ID;
911
- if (host && DESKTOP_ID.test(host)) hints.desktop_id = host;
912
- if (event !== "PreToolUse" && event !== "PostToolUse") hints.claude_pid = claudePid(input.env);
913
- const r = { session, events, hints };
914
- 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;
915
1289
  return r;
916
1290
  },
917
1291
  readTranscript(file, fromLine) {
@@ -921,331 +1295,162 @@ var claudeCode = {
921
1295
  } catch {
922
1296
  return { turns: [], line: fromLine };
923
1297
  }
924
- return readClaude(text.split("\n"), fromLine);
1298
+ return readAntigravity(text.split("\n"), fromLine);
925
1299
  },
926
- facts(session, transcript, hints, state) {
927
- const out = {};
928
- if (transcript) Object.assign(out, claudeFacts(transcript, session.id, hints, state));
929
- const cwd = out.cwd ?? session.cwd;
930
- Object.assign(out, gitFacts(cwd), out.branch ? { branch: out.branch } : {});
931
- if (!out.url && typeof hints.desktop_id === "string") out.url = `claude://code/continue/${hints.desktop_id}`;
932
- 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
+ });
933
1307
  },
934
1308
  backfill(sinceMs, env = process.env) {
935
1309
  const rows2 = [];
936
- const files = [];
937
- for (const d of claudeDirs(env))
938
- files.push(...recentFiles(path4.join(d, "projects"), 1, sinceMs, (n) => /^[0-9a-f-]{36}\.jsonl$/.test(n)));
939
- const noState = { get: () => ({}), save: () => {
940
- } };
941
- for (const f of files) {
942
- const id = path4.basename(f, ".jsonl");
943
- const facts = claudeFacts(f, id, {}, noState);
944
- if (!facts.started_at || !facts.last_at) continue;
945
- const { started_at, last_at, first_ask: _fa, harness_version: _hv, ...rest } = facts;
946
- const cwd = rest.cwd;
947
- rows2.push({
948
- session: { id, ...rest, ...gitFacts(cwd), ...rest.branch ? { branch: rest.branch } : {} },
949
- started_at,
950
- last_at
951
- });
952
- }
953
- return rows2;
954
- }
955
- };
956
- function prune2(o) {
957
- for (const k of Object.keys(o)) if (o[k] === void 0) delete o[k];
958
- return o;
959
- }
960
- var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
961
- function notClaudeCode(id, s) {
962
- return typeof s.transcript_path !== "string" && !UUID.test(id);
963
- }
964
- function pickSource(v) {
965
- return typeof v === "string" && ["startup", "resume", "clear", "compact", "fork"].includes(v) ? v : "unknown";
966
- }
967
- function errText(v) {
968
- if (typeof v === "string") return v.slice(0, 2e3);
969
- if (v && typeof v === "object") {
970
- const e = v;
971
- if (typeof e.error === "string") return e.error.slice(0, 2e3);
972
- if (typeof e.message === "string") return e.message.slice(0, 2e3);
973
- }
974
- return void 0;
975
- }
976
- function describe(input) {
977
- if (!input || typeof input !== "object") return "";
978
- const i = input;
979
- const v = i.command ?? i.file_path ?? i.path ?? i.url ?? i.pattern;
980
- return typeof v === "string" ? ` ${v.slice(0, 120)}` : "";
981
- }
982
- var NAME_RANK = {
983
- user: "custom",
984
- auto: "harness",
985
- hook: "harness",
986
- peer: "harness"
987
- };
988
- function registryEntry(j) {
989
- if (!j || typeof j !== "object") return null;
990
- const r = j;
991
- const source = r.nameSource ? NAME_RANK[r.nameSource] : void 0;
992
- return {
993
- sessionId: r.sessionId ? String(r.sessionId) : null,
994
- name: source && typeof r.name === "string" && r.name.trim() ? r.name.trim() : null,
995
- name_source: source ?? null,
996
- bridge: typeof r.bridgeSessionId === "string" ? r.bridgeSessionId : null
997
- };
998
- }
999
- var liveCache = /* @__PURE__ */ new Map();
1000
- function claudeLive(dir) {
1001
- const hit = liveCache.get(dir);
1002
- if (hit) return hit;
1003
- const map = /* @__PURE__ */ new Map();
1004
- let names = [];
1005
- try {
1006
- names = readdirSync3(path4.join(dir, "sessions")).filter((n) => n.endsWith(".json"));
1007
- } catch {
1008
- }
1009
- for (const n of names) {
1010
- try {
1011
- const e = registryEntry(JSON.parse(readFileSync3(path4.join(dir, "sessions", n), "utf8")));
1012
- if (e?.sessionId) map.set(e.sessionId, e);
1013
- } catch {
1014
- }
1015
- }
1016
- liveCache.set(dir, map);
1017
- return map;
1018
- }
1019
- function claudeLiveByPid(dir, pid) {
1020
- if (!pid) return null;
1021
- try {
1022
- return registryEntry(JSON.parse(readFileSync3(path4.join(dir, "sessions", `${pid}.json`), "utf8")));
1023
- } catch {
1024
- return null;
1025
- }
1026
- }
1027
- function claudePid(env = process.env) {
1028
- try {
1029
- const dirs = claudeDirs(env);
1030
- const has = (pid2) => dirs.some((d) => existsSync3(path4.join(d, "sessions", `${pid2}.json`)));
1031
- let pid = process.ppid;
1032
- if (has(pid)) return pid;
1033
- const parent = parentOf();
1034
- for (let hop = 0; hop < 6 && pid > 1; hop++) {
1035
- pid = parent(pid);
1036
- if (pid > 1 && has(pid)) return pid;
1037
- }
1038
- } catch {
1039
- }
1040
- return null;
1041
- }
1042
- function parentOf() {
1043
- if (process.platform === "linux") {
1044
- return (pid) => {
1310
+ for (const data of dataDirs(env)) {
1311
+ let ids = [];
1045
1312
  try {
1046
- const st = readFileSync3(`/proc/${pid}/stat`, "utf8");
1047
- 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));
1048
1314
  } catch {
1049
- return 0;
1050
1315
  }
1051
- };
1052
- }
1053
- const table = /* @__PURE__ */ new Map();
1054
- try {
1055
- const out = execFileSync2("ps", ["-axo", "pid=,ppid="], {
1056
- encoding: "utf8",
1057
- timeout: 2e3,
1058
- maxBuffer: 8 * 1024 * 1024,
1059
- stdio: ["ignore", "pipe", "ignore"]
1060
- });
1061
- for (const l of out.split("\n")) {
1062
- const m = /^\s*(\d+)\s+(\d+)/.exec(l);
1063
- 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
+ }
1064
1328
  }
1065
- } catch {
1329
+ return rows2;
1066
1330
  }
1067
- 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;
1068
1335
  }
1069
- var EMAIL_RE = /^[^@\s]{1,64}@[^@\s]{1,189}$/;
1070
- function claudeLogin(dir, home = homeDir()) {
1071
- const files = [path4.join(dir, ".claude.json")];
1072
- if (path4.resolve(dir) === path4.join(home, ".claude")) files.unshift(path4.join(home, ".claude.json"));
1073
- 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)) {
1074
1340
  try {
1075
- const a = JSON.parse(readFileSync3(f, "utf8")).oauthAccount;
1076
- if (typeof a?.accountUuid === "string" && a.accountUuid) {
1077
- const email = typeof a.emailAddress === "string" && EMAIL_RE.test(a.emailAddress) ? a.emailAddress : void 0;
1078
- return { id: a.accountUuid.slice(0, 128), ...email ? { email } : {} };
1079
- }
1341
+ s = decodeURIComponent(new URL(s).pathname);
1080
1342
  } catch {
1343
+ return null;
1081
1344
  }
1082
- }
1083
- return null;
1084
- }
1085
- var accountCache = /* @__PURE__ */ new Map();
1086
- function claudeAccount(dir) {
1087
- const hit = accountCache.get(dir);
1088
- if (hit !== void 0) return hit;
1089
- const acct = claudeLogin(dir)?.id ?? null;
1090
- accountCache.set(dir, acct);
1091
- return acct;
1092
- }
1093
- var LINK_RE = /^https:\/\/claude\.ai\/code\/session_[A-Za-z0-9]+$/;
1094
- function transcriptLink(records) {
1095
- for (let i = records.length - 1; i >= 0; i--) {
1096
- const j = records[i];
1097
- if (j.type === "system" && j.subtype === "bridge_status" && typeof j.url === "string" && LINK_RE.test(j.url))
1098
- return j.url;
1099
- if (j.type === "bridge-session" && typeof j.bridgeSessionId === "string") {
1100
- const m = /^(?:cse|session)_([A-Za-z0-9]+)$/.exec(j.bridgeSessionId);
1101
- if (m) return `https://claude.ai/code/session_${m[1]}`;
1102
- }
1103
- }
1104
- return null;
1105
- }
1106
- function appTitle(text) {
1107
- const t = String(text).replace(/\s+/g, " ").trim();
1108
- return t.length > 50 ? `${t.slice(0, 50)}...` : t;
1109
- }
1110
- function askText(j) {
1111
- if (!j || j.type !== "user" || j.isMeta || j.isSidechain) return null;
1112
- const origin = j.origin;
1113
- if (origin?.kind && origin.kind !== "human") return null;
1114
- const c = j.message?.content;
1115
- 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();
1116
- if (!text || isInjected(text)) return null;
1117
- return text;
1118
- }
1119
- function firstAsk(records) {
1120
- for (const j of records) {
1121
- const t = askText(j);
1122
- if (t) return appTitle(t);
1123
- }
1124
- return null;
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);
1125
1349
  }
1126
- function scanTitles(file, from, found) {
1127
- const out = { custom: found.custom ?? null, ai: found.ai ?? null, upto: from };
1128
- let size = 0;
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];
1129
1353
  try {
1130
- size = statSync3(file).size;
1354
+ const r = realpathSync.native(p);
1355
+ if (r !== p) out.push(r);
1131
1356
  } catch {
1132
- return out;
1133
- }
1134
- if (size < from) {
1135
- out.upto = 0;
1136
- out.custom = out.ai = null;
1137
- from = 0;
1138
1357
  }
1139
- let pos = 0;
1140
- scanLines(
1141
- file,
1142
- (l) => {
1143
- pos += Buffer.byteLength(l, "utf8") + 1;
1144
- if (pos <= from || l.indexOf('-title"') < 0) return null;
1145
- try {
1146
- const j = JSON.parse(l);
1147
- if (j.type === "custom-title" && j.customTitle) out.custom = j.customTitle;
1148
- if (j.type === "ai-title" && j.aiTitle) out.ai = j.aiTitle;
1149
- } catch {
1150
- }
1151
- return null;
1152
- },
1153
- 512 * 1024 * 1024
1154
- );
1155
- out.upto = size;
1156
1358
  return out;
1157
1359
  }
1158
- function claudeFacts(file, sessionId, hints, state) {
1159
- const { head, tail } = ends(file, 64 * 1024, 256 * 1024);
1160
- const H = parseLines(head);
1161
- const T = parseLines(tail);
1162
- const out = {};
1163
- const id = sessionId || path4.basename(file, ".jsonl");
1164
- const mem = state.get(NAME2, id);
1165
- let custom = null;
1166
- let ai = null;
1167
- for (const j of H.concat(T)) {
1168
- if (j.type === "custom-title" && typeof j.customTitle === "string") custom = j.customTitle;
1169
- if (j.type === "ai-title" && typeof j.aiTitle === "string") ai = j.aiTitle;
1170
- }
1171
- let size = 0;
1360
+ function agProjects(dir) {
1361
+ const out = [];
1362
+ let names = [];
1172
1363
  try {
1173
- size = statSync3(file).size;
1364
+ names = readdirSync3(dir).filter((n) => n.endsWith(".json"));
1174
1365
  } catch {
1366
+ return out;
1175
1367
  }
1176
- if (!custom && !ai && size > 64 * 1024 + 256 * 1024) {
1177
- const s = scanTitles(file, Number(mem.scan_upto) || 0, {
1178
- custom: mem.scan_custom,
1179
- ai: mem.scan_ai
1180
- });
1181
- custom = s.custom;
1182
- ai = s.ai;
1183
- state.save(NAME2, id, { scan_upto: s.upto, scan_custom: s.custom, scan_ai: s.ai });
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));
1381
+ }
1382
+ if (folders.length) out.push({ id, folders });
1184
1383
  }
1185
- const dir = configDirOf(file);
1186
- const live = claudeLive(dir).get(id) ?? claudeLiveByPid(dir, hints.claude_pid);
1187
- if (live?.name) state.save(NAME2, id, { name: live.name, name_source: live.name_source });
1188
- const bridge = live?.bridge && /^session_[A-Za-z0-9]+$/.test(live.bridge) ? `https://claude.ai/code/${live.bridge}` : null;
1189
- const url = bridge ?? transcriptLink(T) ?? transcriptLink(H) ?? (typeof mem.url === "string" ? mem.url : null);
1190
- if (url) {
1191
- out.url = url;
1192
- state.save(NAME2, id, { url });
1384
+ return out;
1385
+ }
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));
1193
1391
  }
1194
- const name = live?.name ? { title: live.name, source: live.name_source } : typeof mem.name === "string" ? { title: mem.name, source: mem.name_source } : null;
1195
- const pick = name && name.source === "custom" && name || custom && { title: custom, source: "custom" } || name || ai && { title: ai, source: "harness" };
1196
- let ask = typeof mem.first_ask === "string" ? mem.first_ask : firstAsk(H);
1197
- if (!ask && size > 64 * 1024 && !mem.first_ask_none) {
1198
- ask = scanLines(file, (l) => {
1199
- try {
1200
- const t = askText(JSON.parse(l));
1201
- return t ? appTitle(t) : null;
1202
- } catch {
1203
- 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;
1204
1400
  }
1205
- });
1206
- if (!ask && size > 16 * 1024 * 1024) state.save(NAME2, id, { first_ask_none: true });
1207
- }
1208
- if (ask) {
1209
- out.first_ask = ask;
1210
- if (!mem.first_ask) state.save(NAME2, id, { first_ask: ask });
1401
+ }
1402
+ return best;
1403
+ }
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
+ }
1211
1420
  }
1212
- if (pick) {
1213
- out.title = String(pick.title).slice(0, 120);
1214
- out.title_source = pick.source;
1215
- } else if (ask) {
1216
- out.title = ask;
1421
+ return null;
1422
+ }
1423
+ function antigravityFacts(file, opts) {
1424
+ const { head, tail } = ends(file, 128 * 1024, 64 * 1024);
1425
+ const H = parseLines(head);
1426
+ const T = parseLines(tail);
1427
+ const out = {};
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;
1217
1437
  out.title_source = "first-ask";
1438
+ break;
1218
1439
  }
1219
- const acct = claudeAccount(dir);
1220
- if (acct) out.account_id = acct;
1221
- for (let i = T.length - 1; i >= 0; i--) {
1222
- const m = T[i].message?.model;
1223
- if (T[i]?.type === "assistant" && m && m !== "<synthetic>") {
1224
- out.model = m;
1225
- break;
1226
- }
1227
- }
1228
- const meta = [...T].reverse().find((j) => j.version) ?? H.find((j) => j.version);
1229
- if (meta?.version) out.harness_version = String(meta.version);
1230
- 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);
1231
1441
  if (times.length) {
1232
1442
  out.started_at = Math.min(...times);
1233
1443
  out.last_at = Math.max(...times);
1234
1444
  }
1235
- const withCwd = H.find((j) => typeof j.cwd === "string");
1236
- if (withCwd) {
1237
- out.cwd = withCwd.cwd;
1238
- if (typeof withCwd.gitBranch === "string" && withCwd.gitBranch !== "HEAD") out.branch = withCwd.gitBranch;
1239
- }
1240
1445
  return out;
1241
1446
  }
1242
1447
 
1243
1448
  // ../core/src/adapters/codex.ts
1244
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
1245
- 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";
1246
1451
 
1247
1452
  // ../core/src/adapters/codex-config.ts
1248
- var NOTIFY_LINE = /^[ \t]*notify[ \t]*=[ \t]*(\[.*\])[ \t]*(?:#.*)?$/m;
1453
+ var NOTIFY_LINE = /^[ \t]*notify[ \t]*=[ \t]*(\[.*\])[ \t]*(?:#.*)?(?=\r?$)/m;
1249
1454
  var NOTIFY_KEY = /^[ \t]*notify[ \t]*=/m;
1250
1455
  var OURS2 = /(?:hook\.js|sessionpipe-hook)",\s*"codex",\s*"notify"/;
1251
1456
  function scan(line, st) {
@@ -1335,10 +1540,11 @@ function dropLine(text, index, length) {
1335
1540
  }
1336
1541
  function insertTopLevel(text, line) {
1337
1542
  const { head, tail } = split(text);
1338
- if (!tail) return (head.trim() ? head.replace(/\s*$/, "\n") : "") + line + "\n";
1339
- return `${(head.trim() ? head.replace(/\s*$/, "\n") : "") + line}
1340
-
1341
- ${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}`;
1342
1548
  }
1343
1549
  function withNotify(text, line) {
1344
1550
  const n = readNotify(text);
@@ -1387,10 +1593,10 @@ var CODEX_EVENTS = [
1387
1593
  ];
1388
1594
  var OPTS2 = { matcherFor: (e) => /Tool|Permission/.test(e) ? "" : void 0, timeout: 5 };
1389
1595
  var eventsFor2 = (lean) => lean ? CODEX_EVENTS.filter((e) => e !== "PreToolUse") : [...CODEX_EVENTS];
1390
- var codexHome = (env) => env.CODEX_HOME || path5.join(homeDir(), ".codex");
1391
- var hooksFile2 = (env) => path5.join(codexHome(env), "hooks.json");
1392
- var configFile = (env) => path5.join(codexHome(env), "config.toml");
1393
- 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");
1394
1600
  var validTool3 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
1395
1601
  var endReason2 = (r) => r === "clear" || r === "logout" || r === "exit" ? r : "other";
1396
1602
  function installNotify(cmd, env) {
@@ -1404,8 +1610,8 @@ function installNotify(cmd, env) {
1404
1610
  const line = `notify = ${JSON.stringify(argv)}`;
1405
1611
  const n = readNotify(text);
1406
1612
  const write = (next) => {
1407
- mkdirSync3(path5.dirname(file), { recursive: true });
1408
- writeFileSync3(file, next);
1613
+ mkdirSync3(path6.dirname(file), { recursive: true });
1614
+ writeFileSync4(file, next);
1409
1615
  };
1410
1616
  if (n && !n.other) {
1411
1617
  if (!n.misplaced && JSON.stringify(n.argv) === JSON.stringify(argv))
@@ -1450,12 +1656,12 @@ function uninstallNotify(env) {
1450
1656
  const n = readNotify(text);
1451
1657
  if (!n) return { file, changed: false };
1452
1658
  if (!n.other) {
1453
- writeFileSync3(file, withoutNotify(text));
1659
+ writeFileSync4(file, withoutNotify(text));
1454
1660
  return { file, changed: true };
1455
1661
  }
1456
1662
  const prev = previousNotify(n.argv);
1457
1663
  if (isComputerUse(n.argv) && prev && OURS2.test(JSON.stringify(prev))) {
1458
- writeFileSync3(file, replaceNotify(text, n, `notify = ${stripPreviousRaw(n.raw)}`));
1664
+ writeFileSync4(file, replaceNotify(text, n, `notify = ${stripPreviousRaw(n.raw)}`));
1459
1665
  return { file, changed: true };
1460
1666
  }
1461
1667
  return { file, changed: false };
@@ -1471,7 +1677,7 @@ function findRollout(threadId, env = process.env) {
1471
1677
  return;
1472
1678
  }
1473
1679
  for (const n of names.sort().reverse()) {
1474
- const p = path5.join(dir, n);
1680
+ const p = path6.join(dir, n);
1475
1681
  if (n.endsWith(`-${threadId}.jsonl`)) {
1476
1682
  best = p;
1477
1683
  return;
@@ -1561,7 +1767,7 @@ var codex = {
1561
1767
  if (j && typeof j === "object" && !Array.isArray(j)) s = j;
1562
1768
  } catch {
1563
1769
  }
1564
- const id = s.session_id;
1770
+ const id = s.session_id ?? s.thread_id ?? s.conversation_id ?? s["thread-id"] ?? s["session-id"];
1565
1771
  if (!id) return null;
1566
1772
  if (s.transcript_path === null) return null;
1567
1773
  const session = { id: String(id), cwd: typeof s.cwd === "string" ? s.cwd : input.cwd };
@@ -1656,7 +1862,7 @@ var codex = {
1656
1862
  backfill(sinceMs, env = process.env) {
1657
1863
  const rows2 = [];
1658
1864
  for (const f of recentFiles(sessionsDir(env), 3, sinceMs, (n) => /^rollout-.*\.jsonl$/.test(n))) {
1659
- 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));
1660
1866
  if (!m?.[1]) continue;
1661
1867
  if (codexSubagent(rolloutMeta(f))) continue;
1662
1868
  const facts = codexFacts(f);
@@ -1665,12 +1871,40 @@ var codex = {
1665
1871
  rows2.push({ session: { id: m[1], ...rest }, started_at, last_at });
1666
1872
  }
1667
1873
  return rows2;
1668
- }
1874
+ },
1875
+ limits: (transcript) => codexLimits(transcript)
1669
1876
  };
1877
+ var epochIso2 = (t) => typeof t === "number" && t > 0 && t < 1e11 ? new Date(t * 1e3).toISOString() : void 0;
1670
1878
  function prune3(o) {
1671
1879
  for (const k of Object.keys(o)) if (o[k] === void 0) delete o[k];
1672
1880
  return o;
1673
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
+ }
1674
1908
  function codexFacts(file) {
1675
1909
  const { head, tail } = ends(file, 128 * 1024, 64 * 1024);
1676
1910
  let H = parseLines(head);
@@ -1715,7 +1949,7 @@ function codexFacts(file) {
1715
1949
  // ../core/src/adapters/devin.ts
1716
1950
  import { createHash as createHash2 } from "node:crypto";
1717
1951
  import { existsSync as existsSync6, readlinkSync } from "node:fs";
1718
- import path6 from "node:path";
1952
+ import path7 from "node:path";
1719
1953
 
1720
1954
  // ../core/src/readers/sqlite.ts
1721
1955
  import { execFileSync as execFileSync3 } from "node:child_process";
@@ -1899,11 +2133,11 @@ function foreignEvent(file) {
1899
2133
  return Object.keys(hooks).find((k) => !ok.includes(k));
1900
2134
  }
1901
2135
  var homeOf = (env) => env.HOME || homeDir();
1902
- var appData = (env) => env.APPDATA || path6.join(homeOf(env), "AppData", "Roaming");
1903
- 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");
1904
- var configFile2 = (env) => path6.join(devinConfigDir(env), "config.json");
1905
- 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");
1906
- 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");
1907
2141
  var SESSION_ID = /^[A-Za-z0-9._:-]{1,128}$/;
1908
2142
  var validTool4 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
1909
2143
  var endReason3 = (r) => r === "clear" || r === "logout" || r === "exit" ? r : "other";
@@ -2075,7 +2309,7 @@ var BACKFILL_SQL = `SELECT s.id, ${SESSION_FIELDS} FROM sessions s
2075
2309
  function sessionFacts(row) {
2076
2310
  const out = {};
2077
2311
  if (typeof row.model === "string" && row.model) out.model = row.model;
2078
- if (typeof row.working_directory === "string" && path6.isAbsolute(row.working_directory))
2312
+ if (typeof row.working_directory === "string" && path7.isAbsolute(row.working_directory))
2079
2313
  out.cwd = row.working_directory;
2080
2314
  const started = Number(row.created_at);
2081
2315
  const last = Number(row.last_activity_at);
@@ -2120,8 +2354,8 @@ function readDevinChain(db, session, fromLine) {
2120
2354
  }
2121
2355
  function devinVersion(env = process.env) {
2122
2356
  try {
2123
- const target = readlinkSync(path6.join(devinDataDir(env), "_versions", "current"));
2124
- const name = path6.basename(target);
2357
+ const target = readlinkSync(path7.join(devinDataDir(env), "_versions", "current"));
2358
+ const name = path7.basename(target);
2125
2359
  return /^[0-9][0-9A-Za-z.+-]{0,63}$/.test(name) ? name : void 0;
2126
2360
  } catch {
2127
2361
  return void 0;
@@ -2136,7 +2370,7 @@ function devinAccount(env = process.env) {
2136
2370
  // ../core/src/adapters/gemini-cli.ts
2137
2371
  import { execFileSync as execFileSync4 } from "node:child_process";
2138
2372
  import { existsSync as existsSync7, readFileSync as readFileSync5 } from "node:fs";
2139
- import path7 from "node:path";
2373
+ import path8 from "node:path";
2140
2374
  var NAME5 = "gemini-cli";
2141
2375
  var GEMINI_EVENTS = [
2142
2376
  "SessionStart",
@@ -2149,26 +2383,31 @@ var GEMINI_EVENTS = [
2149
2383
  "SessionEnd"
2150
2384
  ];
2151
2385
  var OPTS4 = { matcherFor: (e) => /Tool/.test(e) ? ".*" : void 0, timeout: 5e3 };
2152
- var geminiDir = (env) => env.GEMINI_CLI_HOME || path7.join(homeDir(), ".gemini");
2153
- 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");
2154
2388
  var validTool5 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
2155
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
+ }
2156
2403
  function geminiHooksActive(settingsFile) {
2157
- let tools;
2158
- try {
2159
- tools = JSON.parse(readFileSync5(settingsFile, "utf8")).tools;
2160
- } catch {
2161
- }
2404
+ const tools = readJson(settingsFile).tools;
2162
2405
  if (tools?.enableHooks === true) return { active: true };
2163
2406
  if (tools?.enableHooks === false)
2164
2407
  return { active: false, note: "inactive: tools.enableHooks is false in settings.json" };
2165
2408
  let version = "";
2166
2409
  try {
2167
- version = execFileSync4("gemini", ["--version"], {
2168
- encoding: "utf8",
2169
- timeout: 4e3,
2170
- stdio: ["ignore", "pipe", "ignore"]
2171
- }).trim();
2410
+ version = geminiVersion();
2172
2411
  } catch {
2173
2412
  return {
2174
2413
  active: true,
@@ -2186,7 +2425,7 @@ var geminiCli = {
2186
2425
  name: NAME5,
2187
2426
  events: GEMINI_EVENTS,
2188
2427
  // ~/.gemini also belongs to Antigravity; Gemini CLI is here when its settings or its chat logs exist.
2189
- 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")),
2190
2429
  configFiles: (env = process.env) => [settings(env)],
2191
2430
  install: (cmd, env = process.env, opts = {}) => {
2192
2431
  const r = installClaudeShaped(settings(env), NAME5, eventsFor4(opts.lean), cmd, OPTS4);
@@ -2323,6 +2562,22 @@ function prune5(o) {
2323
2562
  var ADAPTERS = [claudeCode, codex, geminiCli, antigravity, devin];
2324
2563
  var adapterByName = (name) => ADAPTERS.find((a) => a.name === name);
2325
2564
 
2565
+ // ../core/src/launched-by.ts
2566
+ var LAUNCHED_BY_ENV = "SESSIONPIPE_LAUNCHED_BY";
2567
+ var LAUNCHED_BY = /^[a-z][a-z0-9-]{0,39}:[A-Za-z0-9._:-]{1,128}$/;
2568
+ function launchedBy(env) {
2569
+ const v = env[LAUNCHED_BY_ENV]?.trim();
2570
+ return v && LAUNCHED_BY.test(v) ? v : void 0;
2571
+ }
2572
+ function readHook(adapter, input) {
2573
+ const r = adapter.fromHook(input);
2574
+ if (r && !r.session.parent_id) {
2575
+ const launcher = launchedBy(input.env);
2576
+ if (launcher && launcher !== `${adapter.name}:${r.session.id}`) r.session.launched_by = launcher;
2577
+ }
2578
+ return r;
2579
+ }
2580
+
2326
2581
  // src/index.ts
2327
2582
  function matches(expect, got, pathStr = "") {
2328
2583
  if (expect === "*") return null;
@@ -2353,7 +2608,7 @@ async function runAdapterFixtures(files) {
2353
2608
  results.push({ file, ok: false, why: `no adapter ${fx.harness}` });
2354
2609
  continue;
2355
2610
  }
2356
- const r = adapter.fromHook({
2611
+ const r = readHook(adapter, {
2357
2612
  argv: fx.input.argv,
2358
2613
  stdin: fx.input.stdin,
2359
2614
  env: fx.input.env ?? {},
@@ -2383,8 +2638,8 @@ async function runAdapterFixtures(files) {
2383
2638
 
2384
2639
  // src/cli.ts
2385
2640
  var walk = (d) => readdirSync5(d).flatMap((n) => {
2386
- const p = path8.join(d, n);
2387
- 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] : [];
2388
2643
  });
2389
2644
  async function main(argv, io = process) {
2390
2645
  const target = argv[0];