sessionpipe 0.1.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 ADDED
@@ -0,0 +1,3158 @@
1
+ #!/usr/bin/env node
2
+ // sessionpipe 0.1.0 · Apache-2.0 · https://sessionpipe.org
3
+
4
+ // src/cli.ts
5
+ import { execFileSync as execFileSync3, spawn } from "node:child_process";
6
+ import { readdirSync as readdirSync6, readFileSync as readFileSync9, realpathSync as realpathSync2, statSync as statSync6, unlinkSync as unlinkSync2 } from "node:fs";
7
+ import os3 from "node:os";
8
+ import path12 from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+
11
+ // ../core/src/adapters/antigravity.ts
12
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, realpathSync, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
13
+ import path4 from "node:path";
14
+
15
+ // ../core/src/paths.ts
16
+ import os from "node:os";
17
+ import path from "node:path";
18
+ var HOME = os.homedir();
19
+ function configFile(env = process.env) {
20
+ if (env.SESSIONPIPE_CONFIG) return path.resolve(env.SESSIONPIPE_CONFIG);
21
+ if (process.platform === "win32")
22
+ return path.join(env.APPDATA || path.join(HOME, "AppData", "Roaming"), "sessionpipe", "config.json");
23
+ return path.join(env.XDG_CONFIG_HOME || path.join(HOME, ".config"), "sessionpipe", "config.json");
24
+ }
25
+ function stateDir(env = process.env) {
26
+ if (env.SESSIONPIPE_STATE) return path.resolve(env.SESSIONPIPE_STATE);
27
+ if (process.platform === "win32")
28
+ return path.join(env.LOCALAPPDATA || path.join(HOME, "AppData", "Local"), "sessionpipe");
29
+ return path.join(env.XDG_STATE_HOME || path.join(HOME, ".local", "state"), "sessionpipe");
30
+ }
31
+ function tilde(p, home = HOME) {
32
+ if (!p) return p;
33
+ if (p === home) return "~";
34
+ return p.startsWith(home + path.sep) || p.startsWith(`${home}/`) ? `~${p.slice(home.length)}` : p;
35
+ }
36
+ var safeId = (s) => String(s).replace(/[^A-Za-z0-9_.-]/g, "_").slice(0, 160);
37
+
38
+ // ../core/src/readers/files.ts
39
+ import { closeSync, fstatSync, openSync, readdirSync, readSync, statSync } from "node:fs";
40
+ import path2 from "node:path";
41
+ function ends(file, headBytes, tailBytes) {
42
+ let fd;
43
+ try {
44
+ fd = openSync(file, "r");
45
+ const size = fstatSync(fd).size;
46
+ const read = (pos, len) => {
47
+ const b = Buffer.alloc(len);
48
+ const n = readSync(fd, b, 0, len, pos);
49
+ return b.subarray(0, n).toString("utf8");
50
+ };
51
+ const head = read(0, Math.min(size, headBytes));
52
+ const tail2 = size > headBytes ? read(Math.max(0, size - tailBytes), Math.min(size, tailBytes)) : head;
53
+ return { head: head.split("\n").slice(0, -1), tail: tail2.split("\n").slice(size > headBytes ? 1 : 0) };
54
+ } catch {
55
+ return { head: [], tail: [] };
56
+ } finally {
57
+ if (fd !== void 0)
58
+ try {
59
+ closeSync(fd);
60
+ } catch {
61
+ }
62
+ }
63
+ }
64
+ var parseLines = (lines) => lines.map((l) => {
65
+ try {
66
+ return JSON.parse(l);
67
+ } catch {
68
+ return null;
69
+ }
70
+ }).filter((x) => !!x);
71
+ function scanLines(file, visit, cap2 = 16 * 1024 * 1024) {
72
+ let fd;
73
+ try {
74
+ fd = openSync(file, "r");
75
+ const buf = Buffer.alloc(1024 * 1024);
76
+ let pos = 0;
77
+ let rest = Buffer.alloc(0);
78
+ while (pos < cap2) {
79
+ const n = readSync(fd, buf, 0, buf.length, pos);
80
+ if (!n) break;
81
+ pos += n;
82
+ let chunk = Buffer.concat([rest, buf.subarray(0, n)]);
83
+ let nl = chunk.indexOf(10);
84
+ while (nl >= 0) {
85
+ const r = visit(chunk.subarray(0, nl).toString("utf8"));
86
+ if (r !== null) return r;
87
+ chunk = chunk.subarray(nl + 1);
88
+ nl = chunk.indexOf(10);
89
+ }
90
+ rest = chunk;
91
+ }
92
+ } catch {
93
+ } finally {
94
+ if (fd !== void 0)
95
+ try {
96
+ closeSync(fd);
97
+ } catch {
98
+ }
99
+ }
100
+ return null;
101
+ }
102
+ function recentFiles(root, depth, sinceMs, match) {
103
+ const out2 = [];
104
+ const walk = (dir, d) => {
105
+ let names = [];
106
+ try {
107
+ names = readdirSync(dir, { withFileTypes: true });
108
+ } catch {
109
+ return;
110
+ }
111
+ for (const e of names) {
112
+ const p = path2.join(dir, e.name);
113
+ if (e.isDirectory()) {
114
+ if (d < depth) walk(p, d + 1);
115
+ continue;
116
+ }
117
+ if (!match(e.name)) continue;
118
+ try {
119
+ if (statSync(p).mtimeMs >= sinceMs) out2.push(p);
120
+ } catch {
121
+ }
122
+ }
123
+ };
124
+ walk(root, 0);
125
+ return out2;
126
+ }
127
+
128
+ // ../core/src/readers/transcripts.ts
129
+ function pairTurns(msgs, endLine) {
130
+ const turns = [];
131
+ let cur = null;
132
+ for (const m of msgs) {
133
+ if (m.role === "user") {
134
+ if (cur && !cur.assistant) {
135
+ cur.user += `
136
+
137
+ ${m.text}`;
138
+ continue;
139
+ }
140
+ if (cur) turns.push(cur);
141
+ cur = { user: m.text, assistant: "", at: m.at, startLine: m.line, endLine: m.line };
142
+ } else {
143
+ if (!cur) cur = { user: "", assistant: "", at: m.at, startLine: m.line, endLine: m.line };
144
+ cur.assistant += (cur.assistant ? "\n\n" : "") + m.text;
145
+ cur.endLine = m.line;
146
+ }
147
+ }
148
+ if (cur?.assistant) turns.push(cur);
149
+ const line = cur && !cur.assistant ? cur.startLine : endLine;
150
+ return { turns: turns.map((t) => ({ user: t.user, assistant: t.assistant, at: t.at, endLine: t.endLine })), line };
151
+ }
152
+ var isInjected = (text) => /^<[a-z_-]+>/i.test(text) && /<\/[a-z_-]+>\s*$/i.test(text) || /^<!--\s/.test(text) || /^<command-/.test(text);
153
+ var endOf = (lines) => lines[lines.length - 1] === "" ? lines.length - 1 : lines.length;
154
+ var parse = (raw) => {
155
+ try {
156
+ return JSON.parse(raw);
157
+ } catch {
158
+ return null;
159
+ }
160
+ };
161
+ var textParts = (c, kinds) => {
162
+ if (typeof c === "string") return c;
163
+ if (!Array.isArray(c)) return "";
164
+ return c.filter(
165
+ (b) => b && typeof b === "object" && kinds.includes(b.type ?? "") && typeof b.text === "string"
166
+ ).map((b) => b.text).join("\n");
167
+ };
168
+ function readClaude(lines, from) {
169
+ const msgs = [];
170
+ for (let i = from; i < lines.length; i++) {
171
+ const j = lines[i] ? parse(lines[i]) : null;
172
+ if (!j || j.isSidechain) continue;
173
+ const origin = j.origin;
174
+ if (j.type === "user" && (j.isMeta || origin?.kind && origin.kind !== "human")) continue;
175
+ const at = Date.parse(String(j.timestamp ?? "")) || Date.now();
176
+ const c = j.message?.content;
177
+ if (j.type === "user") {
178
+ const text = textParts(c, ["text"]).trim();
179
+ if (!text || isInjected(text)) continue;
180
+ msgs.push({ role: "user", text, at, line: i });
181
+ } else if (j.type === "assistant" && Array.isArray(c)) {
182
+ const text = textParts(c, ["text"]).trim();
183
+ if (text) msgs.push({ role: "assistant", text, at, line: i });
184
+ }
185
+ }
186
+ return pairTurns(msgs, endOf(lines));
187
+ }
188
+ function readCodex(lines, from) {
189
+ const msgs = [];
190
+ for (let i = from; i < lines.length; i++) {
191
+ const j = lines[i] ? parse(lines[i]) : null;
192
+ if (!j || j.type !== "response_item") continue;
193
+ const p = j.payload;
194
+ if (!p || p.type !== "message") continue;
195
+ const at = Date.parse(String(j.timestamp ?? "")) || Date.now();
196
+ const text = textParts(p.content, ["input_text", "output_text"]).trim();
197
+ if (!text) continue;
198
+ if (p.role === "user") {
199
+ if (/^<[a-z_-]+>/i.test(text)) continue;
200
+ msgs.push({ role: "user", text, at, line: i });
201
+ } else if (p.role === "assistant") msgs.push({ role: "assistant", text, at, line: i });
202
+ }
203
+ return pairTurns(msgs, endOf(lines));
204
+ }
205
+ function readGemini(lines, from) {
206
+ const msgs = [];
207
+ for (let i = from; i < lines.length; i++) {
208
+ const j = lines[i] ? parse(lines[i]) : null;
209
+ if (!j) continue;
210
+ const role = j.type === "user" ? "user" : j.type === "gemini" || j.type === "model" ? "assistant" : null;
211
+ if (!role) continue;
212
+ const raw = j.content ?? j.message ?? j.text;
213
+ const text = (typeof raw === "string" ? raw : textParts(raw, ["text"])).trim();
214
+ if (!text) continue;
215
+ const at = Date.parse(String(j.timestamp ?? "")) || Date.now();
216
+ msgs.push({ role, text, at, line: i });
217
+ }
218
+ return pairTurns(msgs, endOf(lines));
219
+ }
220
+ function readAntigravity(lines, from) {
221
+ const msgs = [];
222
+ for (let i = from; i < lines.length; i++) {
223
+ const j = lines[i] ? parse(lines[i]) : null;
224
+ if (!j || typeof j.content !== "string") continue;
225
+ const at = Date.parse(String(j.created_at ?? j.timestamp ?? "")) || Date.now();
226
+ if (j.type === "USER_INPUT") {
227
+ const m = /<USER_REQUEST>([\s\S]*?)(?:<\/USER_REQUEST>|$)/.exec(j.content);
228
+ const text = (m ? m[1] ?? "" : j.content).trim();
229
+ if (text) msgs.push({ role: "user", text, at, line: i });
230
+ } else if (j.type === "PLANNER_RESPONSE") {
231
+ const text = j.content.trim();
232
+ if (text) msgs.push({ role: "assistant", text, at, line: i });
233
+ }
234
+ }
235
+ return pairTurns(msgs, endOf(lines));
236
+ }
237
+
238
+ // ../core/src/adapters/claude-shaped.ts
239
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
240
+ import path3 from "node:path";
241
+ var OURS = /sessionpipe[^"\n]*[\\/]hook\.js"?\s+[a-z][a-z-]*\s+[A-Za-z]+\s*$/;
242
+ var isOurs = (h) => Array.isArray(h?.hooks) && h.hooks.some((x) => typeof x.command === "string" && OURS.test(x.command));
243
+ function readJson(file) {
244
+ try {
245
+ return JSON.parse(readFileSync(file, "utf8"));
246
+ } catch {
247
+ return {};
248
+ }
249
+ }
250
+ function entryFor(event, harness, cmd2, o) {
251
+ const matcher = o.matcherFor(event);
252
+ return {
253
+ ...matcher !== void 0 ? { matcher } : {},
254
+ hooks: [{ type: "command", command: cmd2([harness, event]).command, timeout: o.timeout }]
255
+ };
256
+ }
257
+ function installClaudeShaped(file, harness, events, cmd2, o) {
258
+ const settings2 = readJson(file);
259
+ const hooks = settings2.hooks && typeof settings2.hooks === "object" ? settings2.hooks : {};
260
+ let changed = false;
261
+ for (const ev of events) {
262
+ const list = Array.isArray(hooks[ev]) ? hooks[ev] : [];
263
+ const entry2 = entryFor(ev, harness, cmd2, o);
264
+ const mine = list.filter((h) => isOurs(h));
265
+ if (mine.length === 1 && JSON.stringify(mine[0]) === JSON.stringify(entry2)) continue;
266
+ hooks[ev] = list.filter((h) => !isOurs(h)).concat([entry2]);
267
+ changed = true;
268
+ }
269
+ if (o.top) {
270
+ for (const [k, v] of Object.entries(o.top))
271
+ if (settings2[k] !== v) {
272
+ settings2[k] = v;
273
+ changed = true;
274
+ }
275
+ }
276
+ if (changed) {
277
+ settings2.hooks = hooks;
278
+ mkdirSync(path3.dirname(file), { recursive: true });
279
+ writeFileSync(file, `${JSON.stringify(settings2, null, 2)}
280
+ `);
281
+ }
282
+ return { file, changed };
283
+ }
284
+ function uninstallClaudeShaped(file, events) {
285
+ if (!existsSync(file)) return { file, changed: false };
286
+ const raw = readFileSync(file, "utf8");
287
+ const settings2 = readJson(file);
288
+ const hooks = settings2.hooks;
289
+ if (!hooks || typeof hooks !== "object") return { file, changed: false };
290
+ let changed = false;
291
+ for (const ev of events) {
292
+ const list = Array.isArray(hooks[ev]) ? hooks[ev] : [];
293
+ const kept = list.filter((h) => !isOurs(h));
294
+ if (kept.length !== list.length) {
295
+ changed = true;
296
+ if (kept.length) hooks[ev] = kept;
297
+ else delete hooks[ev];
298
+ }
299
+ }
300
+ if (!changed) return { file, changed: false };
301
+ if (!Object.keys(hooks).length) delete settings2.hooks;
302
+ const next = `${JSON.stringify(settings2, null, 2)}
303
+ `;
304
+ if (next !== raw) writeFileSync(file, next);
305
+ return { file, changed: true };
306
+ }
307
+ function installedClaudeShaped(file, harness, events, cmd2, o) {
308
+ const hooks = readJson(file).hooks ?? {};
309
+ let present = 0;
310
+ let current = 0;
311
+ for (const ev of events) {
312
+ const list = Array.isArray(hooks[ev]) ? hooks[ev] : [];
313
+ const mine = list.filter((h) => isOurs(h));
314
+ if (mine.length) present++;
315
+ if (mine.length === 1 && JSON.stringify(mine[0]) === JSON.stringify(entryFor(ev, harness, cmd2, o))) current++;
316
+ }
317
+ if (!present) return "missing";
318
+ return current === events.length ? "current" : "stale";
319
+ }
320
+
321
+ // ../core/src/adapters/antigravity.ts
322
+ var NAME = "antigravity";
323
+ var HOOK_NAME = "sessionpipe";
324
+ var AG_EVENTS = ["PreInvocation", "PostInvocation", "PreToolUse", "PostToolUse", "Stop"];
325
+ var gemini = (env) => env.GEMINI_CLI_HOME || path4.join(HOME, ".gemini");
326
+ var hooksFile = (env) => path4.join(gemini(env), "config", "hooks.json");
327
+ var projectsDir = (env) => path4.join(gemini(env), "config", "projects");
328
+ var dataDirs = (env) => ["antigravity", "antigravity-cli", "antigravity-ide"].map((d) => path4.join(gemini(env), d));
329
+ var ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
330
+ var validTool = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
331
+ function entry(cmd2) {
332
+ const handler = (e) => ({ type: "command", command: cmd2([NAME, e]).command, timeout: 5 });
333
+ const out2 = { enabled: true };
334
+ for (const e of AG_EVENTS) out2[e] = /Tool/.test(e) ? [{ matcher: "*", hooks: [handler(e)] }] : [handler(e)];
335
+ return out2;
336
+ }
337
+ var antigravity = {
338
+ name: NAME,
339
+ events: AG_EVENTS,
340
+ detect: (env = process.env) => existsSync2(path4.join(gemini(env), "config")) || dataDirs(env).some((d) => existsSync2(d)),
341
+ configFiles: (env = process.env) => [hooksFile(env)],
342
+ install(cmd2, env = process.env) {
343
+ const file = hooksFile(env);
344
+ const hooks = readJson(file);
345
+ const e = entry(cmd2);
346
+ if (JSON.stringify(hooks[HOOK_NAME]) === JSON.stringify(e)) return [{ file, changed: false }];
347
+ hooks[HOOK_NAME] = e;
348
+ mkdirSync2(path4.dirname(file), { recursive: true });
349
+ writeFileSync2(file, `${JSON.stringify(hooks, null, 2)}
350
+ `);
351
+ return [
352
+ { file, changed: true, note: "hooks.json is read when a conversation starts; open ones report after a restart" }
353
+ ];
354
+ },
355
+ uninstall(env = process.env) {
356
+ const file = hooksFile(env);
357
+ if (!existsSync2(file)) return [{ file, changed: false }];
358
+ const hooks = readJson(file);
359
+ if (!(HOOK_NAME in hooks)) return [{ file, changed: false }];
360
+ delete hooks[HOOK_NAME];
361
+ writeFileSync2(file, `${JSON.stringify(hooks, null, 2)}
362
+ `);
363
+ return [{ file, changed: true }];
364
+ },
365
+ installed(cmd2, env = process.env) {
366
+ const file = hooksFile(env);
367
+ const have = readJson(file)[HOOK_NAME];
368
+ return [
369
+ { file, state: !have ? "missing" : JSON.stringify(have) === JSON.stringify(entry(cmd2)) ? "current" : "stale" }
370
+ ];
371
+ },
372
+ fromHook(input) {
373
+ const event = input.argv[1] ?? "";
374
+ let s = {};
375
+ try {
376
+ s = input.stdin ? JSON.parse(input.stdin) : {};
377
+ } catch {
378
+ }
379
+ const id = s.conversationId ?? s.session_id ?? s.sessionId;
380
+ if (!id) return { session: { id: "" }, events: [], stdout: "{}\n" };
381
+ const workspaces = agWorkspaces(s.workspacePaths);
382
+ const session = { id: String(id), cwd: workspaces[0] ?? input.cwd };
383
+ if (typeof s.modelName === "string" && s.modelName !== "auto") session.model = s.modelName;
384
+ const toolCall = s.toolCall;
385
+ const tool = validTool(toolCall?.name) ? toolCall?.name : validTool(s.toolName) ? s.toolName : void 0;
386
+ const ev = (type, data) => ({ type, data: prune(data), harnessEvent: event });
387
+ const events = [];
388
+ switch (event) {
389
+ case "PreInvocation":
390
+ if (Number(s.invocationNum) === 0 || s.invocationNum === void 0) {
391
+ events.push(ev("session.started", { source: "unknown" }));
392
+ events.push(ev("turn.started", {}));
393
+ } else events.push(ev("session.heartbeat", {}));
394
+ break;
395
+ case "PostInvocation":
396
+ break;
397
+ case "PreToolUse":
398
+ if (tool) events.push(ev("tool.started", { tool, input: toolCall?.args }));
399
+ break;
400
+ case "PostToolUse":
401
+ if (tool)
402
+ events.push(
403
+ ev("tool.ended", {
404
+ tool,
405
+ ok: !s.error,
406
+ error: typeof s.error === "string" ? s.error.slice(0, 2e3) : void 0,
407
+ input: toolCall?.args
408
+ })
409
+ );
410
+ break;
411
+ case "Stop":
412
+ events.push(ev("turn.ended", { reason: s.error ? "error" : "stop" }));
413
+ break;
414
+ default:
415
+ return { session, events: [], stdout: "{}\n" };
416
+ }
417
+ const r = { session, events, stdout: "{}\n", hints: { workspaces } };
418
+ if (typeof s.transcriptPath === "string") r.transcript = s.transcriptPath;
419
+ return r;
420
+ },
421
+ readTranscript(file, fromLine) {
422
+ let text = "";
423
+ try {
424
+ text = readFileSync2(file, "utf8");
425
+ } catch {
426
+ return { turns: [], line: fromLine };
427
+ }
428
+ return readAntigravity(text.split("\n"), fromLine);
429
+ },
430
+ facts(session, transcript, hints, state2) {
431
+ if (!transcript) return {};
432
+ return antigravityFacts(transcript, {
433
+ conversation: session.id,
434
+ workspaces: hints.workspaces ?? (session.cwd ? [session.cwd] : []),
435
+ state: state2,
436
+ env: process.env
437
+ });
438
+ },
439
+ backfill(sinceMs, env = process.env) {
440
+ const rows = [];
441
+ const noState = { get: () => ({}), save: () => {
442
+ } };
443
+ for (const data of dataDirs(env)) {
444
+ let ids = [];
445
+ try {
446
+ ids = readdirSync2(path4.join(data, "brain")).filter((n) => /^[0-9a-f-]{36}$/i.test(n));
447
+ } catch {
448
+ }
449
+ for (const id of ids) {
450
+ const f = path4.join(data, "brain", id, ".system_generated", "logs", "transcript.jsonl");
451
+ try {
452
+ if (statSync2(f).mtimeMs < sinceMs) continue;
453
+ } catch {
454
+ continue;
455
+ }
456
+ const facts = antigravityFacts(f, { conversation: id, workspaces: [], state: noState, env });
457
+ if (!facts.started_at || !facts.last_at) continue;
458
+ const { started_at, last_at, first_ask: _fa, harness_version: _hv, ...rest } = facts;
459
+ rows.push({ session: { id, ...rest }, started_at, last_at });
460
+ }
461
+ }
462
+ return rows;
463
+ }
464
+ };
465
+ function prune(o) {
466
+ for (const k of Object.keys(o)) if (o[k] === void 0) delete o[k];
467
+ return o;
468
+ }
469
+ function agPath(p) {
470
+ if (typeof p !== "string" || !p) return null;
471
+ let s = p;
472
+ if (/^file:/i.test(s)) {
473
+ try {
474
+ s = decodeURIComponent(new URL(s).pathname);
475
+ } catch {
476
+ return null;
477
+ }
478
+ if (process.platform === "win32" && /^\/[A-Za-z]:/.test(s)) s = s.slice(1);
479
+ } else if (/^[a-z][a-z0-9+.-]*:\/\//i.test(s)) return null;
480
+ if (!path4.isAbsolute(s)) return null;
481
+ return path4.resolve(s);
482
+ }
483
+ var agWorkspaces = (paths) => (Array.isArray(paths) ? paths : []).map(agPath).filter((p) => !!p).filter((p, i, a) => a.indexOf(p) === i);
484
+ function agVariants(p) {
485
+ const out2 = [p];
486
+ try {
487
+ const r = realpathSync.native(p);
488
+ if (r !== p) out2.push(r);
489
+ } catch {
490
+ }
491
+ return out2;
492
+ }
493
+ function agProjects(dir) {
494
+ const out2 = [];
495
+ let names = [];
496
+ try {
497
+ names = readdirSync2(dir).filter((n) => n.endsWith(".json"));
498
+ } catch {
499
+ return out2;
500
+ }
501
+ for (const n of names) {
502
+ const id = n.slice(0, -5);
503
+ if (!ID_RE.test(id)) continue;
504
+ let j;
505
+ try {
506
+ j = JSON.parse(readFileSync2(path4.join(dir, n), "utf8"));
507
+ } catch {
508
+ continue;
509
+ }
510
+ const folders = [];
511
+ for (const r of j?.projectResources?.resources ?? []) {
512
+ const f = agPath(r?.folderUri);
513
+ if (f) folders.push(...agVariants(f));
514
+ }
515
+ if (folders.length) out2.push({ id, folders });
516
+ }
517
+ return out2;
518
+ }
519
+ function agMatchProject(projects, workspaces) {
520
+ const ws = [];
521
+ for (const w of workspaces) {
522
+ const p = agPath(w);
523
+ if (p) ws.push(...agVariants(p));
524
+ }
525
+ let best = null;
526
+ let bestLen = -1;
527
+ for (const pr of projects)
528
+ for (const f of pr.folders) {
529
+ const within = ws.some((w) => w === f || w.startsWith(f.endsWith(path4.sep) ? f : f + path4.sep));
530
+ if (within && f.length > bestLen) {
531
+ best = pr.id;
532
+ bestLen = f.length;
533
+ }
534
+ }
535
+ return best;
536
+ }
537
+ function antigravityRemoteUrl(transcript, opts) {
538
+ const i = transcript.indexOf(`${path4.sep}brain${path4.sep}`);
539
+ const dirs = (i > 0 ? [transcript.slice(0, i)] : []).concat(dataDirs(opts.env));
540
+ for (const d of dirs) {
541
+ try {
542
+ const m = /installation_uuid:\s*"([0-9a-f-]{36})"/i.exec(
543
+ readFileSync2(path4.join(d, "antigravity_state.pbtxt"), "utf8")
544
+ );
545
+ if (!m) continue;
546
+ const base = `https://antigravity.google.com/r/${m[1]}-v2`;
547
+ const conv = opts.conversation ?? "";
548
+ if (!ID_RE.test(conv) || !opts.workspaces?.length) return base;
549
+ const project = agMatchProject(agProjects(opts.projectsDir ?? projectsDir(opts.env)), opts.workspaces);
550
+ return project ? `${base}?p=${encodeURIComponent(`c/${conv}?section=${project}`)}` : base;
551
+ } catch {
552
+ }
553
+ }
554
+ return null;
555
+ }
556
+ function antigravityFacts(file, opts) {
557
+ const { head, tail: tail2 } = ends(file, 128 * 1024, 64 * 1024);
558
+ const H = parseLines(head);
559
+ const T = parseLines(tail2);
560
+ const out2 = {};
561
+ const url = antigravityRemoteUrl(file, opts);
562
+ if (url) out2.url = url;
563
+ for (const j of H) {
564
+ if (j.type !== "USER_INPUT" || typeof j.content !== "string") continue;
565
+ const m = /<USER_REQUEST>([\s\S]*?)(?:<\/USER_REQUEST>|$)/.exec(j.content);
566
+ const text = (m ? m[1] ?? "" : j.content).trim();
567
+ const line = (text.split("\n").find((l) => l.trim()) ?? "").trim();
568
+ if (!line || /^<[a-z_-]+>/i.test(line)) continue;
569
+ out2.title = line.length > 80 ? `${line.slice(0, 80).replace(/\s+\S*$/, "")}\u2026` : line;
570
+ out2.title_source = "first-ask";
571
+ break;
572
+ }
573
+ const times = H.concat(T).map((j) => Date.parse(String(j.created_at ?? j.timestamp ?? ""))).filter((n) => n > 0);
574
+ if (times.length) {
575
+ out2.started_at = Math.min(...times);
576
+ out2.last_at = Math.max(...times);
577
+ }
578
+ return out2;
579
+ }
580
+
581
+ // ../core/src/adapters/claude-code.ts
582
+ import { execFileSync as execFileSync2 } from "node:child_process";
583
+ import { createHash } from "node:crypto";
584
+ import { existsSync as existsSync3, readdirSync as readdirSync3, readFileSync as readFileSync3, statSync as statSync3 } from "node:fs";
585
+ import path5 from "node:path";
586
+
587
+ // ../core/src/readers/git.ts
588
+ import { execFileSync } from "node:child_process";
589
+ var cache = /* @__PURE__ */ new Map();
590
+ function gitFacts(cwd) {
591
+ if (!cwd) return {};
592
+ const hit = cache.get(cwd);
593
+ if (hit) return hit;
594
+ const run = (args2) => {
595
+ try {
596
+ return execFileSync("git", ["-C", cwd, ...args2], {
597
+ encoding: "utf8",
598
+ timeout: 1500,
599
+ stdio: ["ignore", "pipe", "ignore"]
600
+ }).trim();
601
+ } catch {
602
+ return "";
603
+ }
604
+ };
605
+ const out2 = {};
606
+ const repo = run(["remote", "get-url", "origin"]);
607
+ if (repo) out2.repo = stripCredentials(repo);
608
+ const branch = run(["rev-parse", "--abbrev-ref", "HEAD"]);
609
+ if (branch && branch !== "HEAD") out2.branch = branch;
610
+ cache.set(cwd, out2);
611
+ return out2;
612
+ }
613
+ function stripCredentials(url) {
614
+ let u = url.replace(/\/\/[^/@]*@/, "//");
615
+ const ssh = /^(?:ssh:\/\/)?git@([^:/]+)[:/](.+?)(?:\.git)?$/.exec(u);
616
+ if (ssh) u = `${ssh[1]}/${ssh[2]}`;
617
+ return u.replace(/^https?:\/\//, "").replace(/\.git$/, "");
618
+ }
619
+
620
+ // ../core/src/adapters/claude-code.ts
621
+ var NAME2 = "claude-code";
622
+ var CLAUDE_EVENTS = [
623
+ "SessionStart",
624
+ "UserPromptSubmit",
625
+ "PreToolUse",
626
+ "PostToolUse",
627
+ "PostToolUseFailure",
628
+ "PermissionRequest",
629
+ "Notification",
630
+ "Stop",
631
+ "SubagentStart",
632
+ "SubagentStop",
633
+ "PreCompact",
634
+ "PostCompact",
635
+ "SessionEnd"
636
+ ];
637
+ var OPTS = { matcherFor: (e) => /Tool|Permission/.test(e) ? "" : void 0, timeout: 5 };
638
+ var DESKTOP_ID = /^local_[A-Za-z0-9-]{1,64}$/;
639
+ function claudeDirs(env = process.env, home = HOME) {
640
+ const out2 = [];
641
+ const add = (d) => {
642
+ if (!d) return;
643
+ const r = path5.resolve(d);
644
+ if (!out2.includes(r)) out2.push(r);
645
+ };
646
+ add(path5.join(home, ".claude"));
647
+ add(env.CLAUDE_CONFIG_DIR);
648
+ try {
649
+ for (const e of readdirSync3(home, { withFileTypes: true })) {
650
+ if (!e.isDirectory() || !/^\.claude[-_.]/.test(e.name)) continue;
651
+ const d = path5.join(home, e.name);
652
+ if (existsSync3(path5.join(d, "projects")) || existsSync3(path5.join(d, "settings.json"))) add(d);
653
+ }
654
+ } catch {
655
+ }
656
+ return out2.filter((d, i) => i === 0 || existsSync3(d));
657
+ }
658
+ var settingsIn = (dir) => path5.join(dir, "settings.json");
659
+ var configDirOf = (transcript) => path5.dirname(path5.dirname(path5.dirname(transcript)));
660
+ var validTool2 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
661
+ var endReason = (r) => r === "clear" || r === "logout" || r === "exit" ? r : r === "prompt_input_exit" ? "exit" : "other";
662
+ var notifKind = (t) => t === "permission_prompt" ? "permission" : t === "idle_prompt" ? "idle" : t === "elicitation_dialog" ? "elicitation" : "question";
663
+ var claudeCode = {
664
+ name: NAME2,
665
+ events: CLAUDE_EVENTS,
666
+ detect: (env = process.env) => claudeDirs(env).some((d) => existsSync3(d)),
667
+ configFiles: (env = process.env) => claudeDirs(env).map(settingsIn),
668
+ install: (cmd2, env = process.env) => claudeDirs(env).map((d) => installClaudeShaped(settingsIn(d), NAME2, CLAUDE_EVENTS, cmd2, OPTS)),
669
+ uninstall: (env = process.env) => claudeDirs(env).map((d) => uninstallClaudeShaped(settingsIn(d), CLAUDE_EVENTS)),
670
+ installed: (cmd2, env = process.env) => claudeDirs(env).map((d) => ({
671
+ file: settingsIn(d),
672
+ state: installedClaudeShaped(settingsIn(d), NAME2, CLAUDE_EVENTS, cmd2, OPTS)
673
+ })),
674
+ fromHook(input) {
675
+ const event = input.argv[1] ?? "";
676
+ let s = {};
677
+ try {
678
+ s = input.stdin ? JSON.parse(input.stdin) : {};
679
+ } catch {
680
+ }
681
+ const id = s.session_id ?? s.sessionId;
682
+ if (!id) return null;
683
+ const session = { id: String(id), cwd: typeof s.cwd === "string" ? s.cwd : input.cwd };
684
+ if (typeof s.model === "string") session.model = s.model;
685
+ if (typeof s.agent_id === "string" && event.startsWith("Subagent") === false && s.agent_id)
686
+ session.parent_id = null;
687
+ const turn_id = typeof s.prompt_id === "string" ? s.prompt_id : void 0;
688
+ const tool = validTool2(s.tool_name) ? s.tool_name : void 0;
689
+ const call_id = typeof s.tool_use_id === "string" ? s.tool_use_id : void 0;
690
+ const ms = typeof s.duration_ms === "number" ? Math.round(s.duration_ms) : void 0;
691
+ const agent_id = typeof s.agent_id === "string" ? s.agent_id : void 0;
692
+ const permissionId = () => `perm-${createHash("sha256").update(`${turn_id ?? ""}|${tool ?? ""}|${JSON.stringify(s.tool_input ?? null)}`).digest("hex").slice(0, 16)}`;
693
+ const ev = (type, data) => ({ type, data: prune2(data), harnessEvent: event });
694
+ const events = [];
695
+ switch (event) {
696
+ case "SessionStart":
697
+ events.push(ev("session.started", { source: pickSource(s.source ?? s.trigger) }));
698
+ break;
699
+ case "UserPromptSubmit":
700
+ events.push(
701
+ ev("turn.started", {
702
+ turn_id,
703
+ prompt_chars: typeof s.prompt === "string" ? [...s.prompt].length : void 0
704
+ })
705
+ );
706
+ break;
707
+ case "PreToolUse":
708
+ if (tool) events.push(ev("tool.started", { tool, call_id, turn_id, agent_id, input: s.tool_input }));
709
+ break;
710
+ case "PostToolUse":
711
+ if (tool)
712
+ events.push(
713
+ ev("tool.ended", {
714
+ tool,
715
+ call_id,
716
+ turn_id,
717
+ agent_id,
718
+ ms,
719
+ ok: true,
720
+ input: s.tool_input,
721
+ output: s.tool_response
722
+ })
723
+ );
724
+ break;
725
+ case "PostToolUseFailure":
726
+ if (tool)
727
+ events.push(
728
+ ev("tool.ended", {
729
+ tool,
730
+ call_id,
731
+ turn_id,
732
+ agent_id,
733
+ ms,
734
+ ok: false,
735
+ error: errText(s.error ?? s.tool_response),
736
+ input: s.tool_input,
737
+ output: s.tool_response
738
+ })
739
+ );
740
+ break;
741
+ case "PermissionRequest":
742
+ events.push(
743
+ ev("attention.needed", {
744
+ attention_id: call_id ?? permissionId(),
745
+ kind: "permission",
746
+ tool,
747
+ message: tool ? `${tool}${describe(s.tool_input)}` : void 0
748
+ })
749
+ );
750
+ break;
751
+ case "Notification":
752
+ events.push(
753
+ ev("attention.needed", {
754
+ attention_id: `notif-${Date.now()}`,
755
+ kind: notifKind(s.notification_type),
756
+ message: typeof s.message === "string" ? s.message.slice(0, 200) : void 0
757
+ })
758
+ );
759
+ break;
760
+ case "Stop":
761
+ events.push(ev("turn.ended", { turn_id, reason: "stop" }));
762
+ break;
763
+ case "SubagentStart":
764
+ events.push(
765
+ ev("subagent.started", {
766
+ agent_id: String(s.agent_id ?? ""),
767
+ agent_type: typeof s.agent_type === "string" ? s.agent_type : void 0
768
+ })
769
+ );
770
+ break;
771
+ case "SubagentStop":
772
+ events.push(
773
+ ev("subagent.ended", {
774
+ agent_id: String(s.agent_id ?? ""),
775
+ agent_type: typeof s.agent_type === "string" ? s.agent_type : void 0
776
+ })
777
+ );
778
+ break;
779
+ case "PreCompact":
780
+ events.push(ev("context.compacted", { trigger: s.trigger === "manual" ? "manual" : "auto" }));
781
+ break;
782
+ case "PostCompact":
783
+ break;
784
+ // PreCompact already said it; a second event would double-count
785
+ case "SessionEnd":
786
+ events.push(ev("session.ended", { reason: endReason(s.reason) }));
787
+ break;
788
+ default:
789
+ return null;
790
+ }
791
+ const hints = {};
792
+ const host = input.env.CLAUDE_CODE_HOST_SESSION_ID;
793
+ if (host && DESKTOP_ID.test(host)) hints.desktop_id = host;
794
+ if (event !== "PreToolUse" && event !== "PostToolUse") hints.claude_pid = claudePid(input.env);
795
+ const r = { session, events, hints };
796
+ if (typeof s.transcript_path === "string") r.transcript = s.transcript_path;
797
+ return r;
798
+ },
799
+ readTranscript(file, fromLine) {
800
+ let text = "";
801
+ try {
802
+ text = readFileSync3(file, "utf8");
803
+ } catch {
804
+ return { turns: [], line: fromLine };
805
+ }
806
+ return readClaude(text.split("\n"), fromLine);
807
+ },
808
+ facts(session, transcript, hints, state2) {
809
+ const out2 = {};
810
+ if (transcript) Object.assign(out2, claudeFacts(transcript, session.id, hints, state2));
811
+ const cwd = out2.cwd ?? session.cwd;
812
+ Object.assign(out2, gitFacts(cwd), out2.branch ? { branch: out2.branch } : {});
813
+ if (!out2.url && typeof hints.desktop_id === "string") out2.url = `claude://code/continue/${hints.desktop_id}`;
814
+ return out2;
815
+ },
816
+ backfill(sinceMs, env = process.env) {
817
+ const rows = [];
818
+ const files = [];
819
+ for (const d of claudeDirs(env))
820
+ files.push(...recentFiles(path5.join(d, "projects"), 1, sinceMs, (n) => /^[0-9a-f-]{36}\.jsonl$/.test(n)));
821
+ const noState = { get: () => ({}), save: () => {
822
+ } };
823
+ for (const f of files) {
824
+ const id = path5.basename(f, ".jsonl");
825
+ const facts = claudeFacts(f, id, {}, noState);
826
+ if (!facts.started_at || !facts.last_at) continue;
827
+ const { started_at, last_at, first_ask: _fa, harness_version: _hv, ...rest } = facts;
828
+ const cwd = rest.cwd;
829
+ rows.push({
830
+ session: { id, ...rest, ...gitFacts(cwd), ...rest.branch ? { branch: rest.branch } : {} },
831
+ started_at,
832
+ last_at
833
+ });
834
+ }
835
+ return rows;
836
+ }
837
+ };
838
+ function prune2(o) {
839
+ for (const k of Object.keys(o)) if (o[k] === void 0) delete o[k];
840
+ return o;
841
+ }
842
+ function pickSource(v) {
843
+ return typeof v === "string" && ["startup", "resume", "clear", "compact", "fork"].includes(v) ? v : "unknown";
844
+ }
845
+ function errText(v) {
846
+ if (typeof v === "string") return v.slice(0, 2e3);
847
+ if (v && typeof v === "object") {
848
+ const e = v;
849
+ if (typeof e.error === "string") return e.error.slice(0, 2e3);
850
+ if (typeof e.message === "string") return e.message.slice(0, 2e3);
851
+ }
852
+ return void 0;
853
+ }
854
+ function describe(input) {
855
+ if (!input || typeof input !== "object") return "";
856
+ const i = input;
857
+ const v = i.command ?? i.file_path ?? i.path ?? i.url ?? i.pattern;
858
+ return typeof v === "string" ? ` ${v.slice(0, 120)}` : "";
859
+ }
860
+ var NAME_RANK = {
861
+ user: "custom",
862
+ auto: "harness",
863
+ hook: "harness",
864
+ peer: "harness"
865
+ };
866
+ function registryEntry(j) {
867
+ if (!j || typeof j !== "object") return null;
868
+ const r = j;
869
+ const source = r.nameSource ? NAME_RANK[r.nameSource] : void 0;
870
+ return {
871
+ sessionId: r.sessionId ? String(r.sessionId) : null,
872
+ name: source && typeof r.name === "string" && r.name.trim() ? r.name.trim() : null,
873
+ name_source: source ?? null,
874
+ bridge: typeof r.bridgeSessionId === "string" ? r.bridgeSessionId : null
875
+ };
876
+ }
877
+ var liveCache = /* @__PURE__ */ new Map();
878
+ function claudeLive(dir) {
879
+ const hit = liveCache.get(dir);
880
+ if (hit) return hit;
881
+ const map = /* @__PURE__ */ new Map();
882
+ let names = [];
883
+ try {
884
+ names = readdirSync3(path5.join(dir, "sessions")).filter((n) => n.endsWith(".json"));
885
+ } catch {
886
+ }
887
+ for (const n of names) {
888
+ try {
889
+ const e = registryEntry(JSON.parse(readFileSync3(path5.join(dir, "sessions", n), "utf8")));
890
+ if (e?.sessionId) map.set(e.sessionId, e);
891
+ } catch {
892
+ }
893
+ }
894
+ liveCache.set(dir, map);
895
+ return map;
896
+ }
897
+ function claudeLiveByPid(dir, pid) {
898
+ if (!pid) return null;
899
+ try {
900
+ return registryEntry(JSON.parse(readFileSync3(path5.join(dir, "sessions", `${pid}.json`), "utf8")));
901
+ } catch {
902
+ return null;
903
+ }
904
+ }
905
+ function claudePid(env = process.env) {
906
+ try {
907
+ const dirs = claudeDirs(env);
908
+ const has2 = (pid2) => dirs.some((d) => existsSync3(path5.join(d, "sessions", `${pid2}.json`)));
909
+ let pid = process.ppid;
910
+ if (has2(pid)) return pid;
911
+ const parent = parentOf();
912
+ for (let hop = 0; hop < 6 && pid > 1; hop++) {
913
+ pid = parent(pid);
914
+ if (pid > 1 && has2(pid)) return pid;
915
+ }
916
+ } catch {
917
+ }
918
+ return null;
919
+ }
920
+ function parentOf() {
921
+ if (process.platform === "linux") {
922
+ return (pid) => {
923
+ try {
924
+ const st = readFileSync3(`/proc/${pid}/stat`, "utf8");
925
+ return Number(st.slice(st.lastIndexOf(")") + 2).split(" ")[1]) || 0;
926
+ } catch {
927
+ return 0;
928
+ }
929
+ };
930
+ }
931
+ const table = /* @__PURE__ */ new Map();
932
+ try {
933
+ const out2 = execFileSync2("ps", ["-axo", "pid=,ppid="], {
934
+ encoding: "utf8",
935
+ timeout: 2e3,
936
+ maxBuffer: 8 * 1024 * 1024,
937
+ stdio: ["ignore", "pipe", "ignore"]
938
+ });
939
+ for (const l of out2.split("\n")) {
940
+ const m = /^\s*(\d+)\s+(\d+)/.exec(l);
941
+ if (m) table.set(Number(m[1]), Number(m[2]));
942
+ }
943
+ } catch {
944
+ }
945
+ return (pid) => table.get(pid) || 0;
946
+ }
947
+ var accountCache = /* @__PURE__ */ new Map();
948
+ function claudeAccount(dir) {
949
+ const hit = accountCache.get(dir);
950
+ if (hit !== void 0) return hit;
951
+ const files = [path5.join(dir, ".claude.json")];
952
+ if (path5.resolve(dir) === path5.join(HOME, ".claude")) files.unshift(path5.join(HOME, ".claude.json"));
953
+ let acct = null;
954
+ for (const f of files) {
955
+ try {
956
+ const a = JSON.parse(readFileSync3(f, "utf8")).oauthAccount;
957
+ if (a?.accountUuid) {
958
+ acct = String(a.accountUuid);
959
+ break;
960
+ }
961
+ } catch {
962
+ }
963
+ }
964
+ accountCache.set(dir, acct);
965
+ return acct;
966
+ }
967
+ var LINK_RE = /^https:\/\/claude\.ai\/code\/session_[A-Za-z0-9]+$/;
968
+ function transcriptLink(records) {
969
+ for (let i = records.length - 1; i >= 0; i--) {
970
+ const j = records[i];
971
+ if (j.type === "system" && j.subtype === "bridge_status" && typeof j.url === "string" && LINK_RE.test(j.url))
972
+ return j.url;
973
+ if (j.type === "bridge-session" && typeof j.bridgeSessionId === "string") {
974
+ const m = /^(?:cse|session)_([A-Za-z0-9]+)$/.exec(j.bridgeSessionId);
975
+ if (m) return `https://claude.ai/code/session_${m[1]}`;
976
+ }
977
+ }
978
+ return null;
979
+ }
980
+ function appTitle(text) {
981
+ const t = String(text).replace(/\s+/g, " ").trim();
982
+ return t.length > 50 ? `${t.slice(0, 50)}...` : t;
983
+ }
984
+ function askText(j) {
985
+ if (!j || j.type !== "user" || j.isMeta || j.isSidechain) return null;
986
+ const origin = j.origin;
987
+ if (origin?.kind && origin.kind !== "human") return null;
988
+ const c = j.message?.content;
989
+ 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();
990
+ if (!text || isInjected(text)) return null;
991
+ return text;
992
+ }
993
+ function firstAsk(records) {
994
+ for (const j of records) {
995
+ const t = askText(j);
996
+ if (t) return appTitle(t);
997
+ }
998
+ return null;
999
+ }
1000
+ function scanTitles(file, from, found) {
1001
+ const out2 = { custom: found.custom ?? null, ai: found.ai ?? null, upto: from };
1002
+ let size = 0;
1003
+ try {
1004
+ size = statSync3(file).size;
1005
+ } catch {
1006
+ return out2;
1007
+ }
1008
+ if (size < from) {
1009
+ out2.upto = 0;
1010
+ out2.custom = out2.ai = null;
1011
+ from = 0;
1012
+ }
1013
+ let pos = 0;
1014
+ scanLines(
1015
+ file,
1016
+ (l) => {
1017
+ pos += Buffer.byteLength(l, "utf8") + 1;
1018
+ if (pos <= from || l.indexOf('-title"') < 0) return null;
1019
+ try {
1020
+ const j = JSON.parse(l);
1021
+ if (j.type === "custom-title" && j.customTitle) out2.custom = j.customTitle;
1022
+ if (j.type === "ai-title" && j.aiTitle) out2.ai = j.aiTitle;
1023
+ } catch {
1024
+ }
1025
+ return null;
1026
+ },
1027
+ 512 * 1024 * 1024
1028
+ );
1029
+ out2.upto = size;
1030
+ return out2;
1031
+ }
1032
+ function claudeFacts(file, sessionId, hints, state2) {
1033
+ const { head, tail: tail2 } = ends(file, 64 * 1024, 256 * 1024);
1034
+ const H = parseLines(head);
1035
+ const T = parseLines(tail2);
1036
+ const out2 = {};
1037
+ const id = sessionId || path5.basename(file, ".jsonl");
1038
+ const mem = state2.get(NAME2, id);
1039
+ let custom = null;
1040
+ let ai = null;
1041
+ for (const j of H.concat(T)) {
1042
+ if (j.type === "custom-title" && typeof j.customTitle === "string") custom = j.customTitle;
1043
+ if (j.type === "ai-title" && typeof j.aiTitle === "string") ai = j.aiTitle;
1044
+ }
1045
+ let size = 0;
1046
+ try {
1047
+ size = statSync3(file).size;
1048
+ } catch {
1049
+ }
1050
+ if (!custom && !ai && size > 64 * 1024 + 256 * 1024) {
1051
+ const s = scanTitles(file, Number(mem.scan_upto) || 0, {
1052
+ custom: mem.scan_custom,
1053
+ ai: mem.scan_ai
1054
+ });
1055
+ custom = s.custom;
1056
+ ai = s.ai;
1057
+ state2.save(NAME2, id, { scan_upto: s.upto, scan_custom: s.custom, scan_ai: s.ai });
1058
+ }
1059
+ const dir = configDirOf(file);
1060
+ const live = claudeLive(dir).get(id) ?? claudeLiveByPid(dir, hints.claude_pid);
1061
+ if (live?.name) state2.save(NAME2, id, { name: live.name, name_source: live.name_source });
1062
+ const bridge = live?.bridge && /^session_[A-Za-z0-9]+$/.test(live.bridge) ? `https://claude.ai/code/${live.bridge}` : null;
1063
+ const url = bridge ?? transcriptLink(T) ?? transcriptLink(H) ?? (typeof mem.url === "string" ? mem.url : null);
1064
+ if (url) {
1065
+ out2.url = url;
1066
+ state2.save(NAME2, id, { url });
1067
+ }
1068
+ const name = live?.name ? { title: live.name, source: live.name_source } : typeof mem.name === "string" ? { title: mem.name, source: mem.name_source } : null;
1069
+ const pick = name && name.source === "custom" && name || custom && { title: custom, source: "custom" } || name || ai && { title: ai, source: "harness" };
1070
+ let ask = typeof mem.first_ask === "string" ? mem.first_ask : firstAsk(H);
1071
+ if (!ask && size > 64 * 1024 && !mem.first_ask_none) {
1072
+ ask = scanLines(file, (l) => {
1073
+ try {
1074
+ const t = askText(JSON.parse(l));
1075
+ return t ? appTitle(t) : null;
1076
+ } catch {
1077
+ return null;
1078
+ }
1079
+ });
1080
+ if (!ask && size > 16 * 1024 * 1024) state2.save(NAME2, id, { first_ask_none: true });
1081
+ }
1082
+ if (ask) {
1083
+ out2.first_ask = ask;
1084
+ if (!mem.first_ask) state2.save(NAME2, id, { first_ask: ask });
1085
+ }
1086
+ if (pick) {
1087
+ out2.title = String(pick.title).slice(0, 120);
1088
+ out2.title_source = pick.source;
1089
+ } else if (ask) {
1090
+ out2.title = ask;
1091
+ out2.title_source = "first-ask";
1092
+ }
1093
+ const acct = claudeAccount(dir);
1094
+ if (acct) out2.account_id = acct;
1095
+ for (let i = T.length - 1; i >= 0; i--) {
1096
+ const m = T[i].message?.model;
1097
+ if (T[i]?.type === "assistant" && m && m !== "<synthetic>") {
1098
+ out2.model = m;
1099
+ break;
1100
+ }
1101
+ }
1102
+ const meta = [...T].reverse().find((j) => j.version) ?? H.find((j) => j.version);
1103
+ if (meta?.version) out2.harness_version = String(meta.version);
1104
+ const times = H.concat(T).map((j) => Date.parse(String(j.timestamp ?? ""))).filter((n) => n > 0);
1105
+ if (times.length) {
1106
+ out2.started_at = Math.min(...times);
1107
+ out2.last_at = Math.max(...times);
1108
+ }
1109
+ const withCwd = H.find((j) => typeof j.cwd === "string");
1110
+ if (withCwd) {
1111
+ out2.cwd = withCwd.cwd;
1112
+ if (typeof withCwd.gitBranch === "string" && withCwd.gitBranch !== "HEAD") out2.branch = withCwd.gitBranch;
1113
+ }
1114
+ return out2;
1115
+ }
1116
+
1117
+ // ../core/src/adapters/codex.ts
1118
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
1119
+ import path6 from "node:path";
1120
+
1121
+ // ../core/src/adapters/codex-config.ts
1122
+ var NOTIFY_LINE = /^[ \t]*notify[ \t]*=[ \t]*(\[.*\])[ \t]*(?:#.*)?$/m;
1123
+ var NOTIFY_KEY = /^[ \t]*notify[ \t]*=/m;
1124
+ var OURS2 = /hook\.js",\s*"codex",\s*"notify"/;
1125
+ function scan(line, st) {
1126
+ let i = 0;
1127
+ while (i < line.length) {
1128
+ if (st.str) {
1129
+ const j = line.indexOf(st.str, i);
1130
+ if (j < 0) return;
1131
+ i = j + 3;
1132
+ st.str = null;
1133
+ continue;
1134
+ }
1135
+ const ch = line[i];
1136
+ if (ch === "#") return;
1137
+ if (line.startsWith('"""', i) || line.startsWith("'''", i)) {
1138
+ st.str = line.slice(i, i + 3);
1139
+ i += 3;
1140
+ continue;
1141
+ }
1142
+ if (ch === '"') {
1143
+ i++;
1144
+ while (i < line.length && line[i] !== '"') i += line[i] === "\\" ? 2 : 1;
1145
+ i++;
1146
+ continue;
1147
+ }
1148
+ if (ch === "'") {
1149
+ const j = line.indexOf("'", i + 1);
1150
+ i = j < 0 ? line.length : j + 1;
1151
+ continue;
1152
+ }
1153
+ if (ch === "[") st.depth++;
1154
+ else if (ch === "]") st.depth = Math.max(0, st.depth - 1);
1155
+ i++;
1156
+ }
1157
+ }
1158
+ function firstTableAt(text) {
1159
+ const st = { depth: 0, str: null };
1160
+ let pos = 0;
1161
+ for (const line of text.split("\n")) {
1162
+ if (!st.str && st.depth === 0 && /^[ \t]*\[/.test(line)) return pos;
1163
+ scan(line, st);
1164
+ pos += line.length + 1;
1165
+ }
1166
+ return -1;
1167
+ }
1168
+ function split(text) {
1169
+ const at = firstTableAt(text);
1170
+ return at < 0 ? { head: text, tail: "", at: text.length } : { head: text.slice(0, at), tail: text.slice(at), at };
1171
+ }
1172
+ function parseArgv(raw) {
1173
+ try {
1174
+ const a = JSON.parse(raw);
1175
+ return Array.isArray(a) && a.every((x) => typeof x === "string") ? a : null;
1176
+ } catch {
1177
+ return null;
1178
+ }
1179
+ }
1180
+ function readNotify(text) {
1181
+ const { head, tail: tail2, at } = split(text);
1182
+ const m = NOTIFY_LINE.exec(head);
1183
+ if (m)
1184
+ return {
1185
+ raw: m[1] ?? "",
1186
+ argv: parseArgv(m[1] ?? ""),
1187
+ index: m.index,
1188
+ length: m[0].length,
1189
+ misplaced: false,
1190
+ other: !OURS2.test(m[1] ?? "")
1191
+ };
1192
+ const k = NOTIFY_KEY.exec(head);
1193
+ if (k) {
1194
+ const rest = head.slice(k.index).split("\n")[0] ?? "";
1195
+ return {
1196
+ raw: rest.replace(NOTIFY_KEY, "").trim(),
1197
+ argv: null,
1198
+ index: k.index,
1199
+ length: 0,
1200
+ misplaced: false,
1201
+ other: true
1202
+ };
1203
+ }
1204
+ const t = NOTIFY_LINE.exec(tail2);
1205
+ if (t && OURS2.test(t[1] ?? ""))
1206
+ return {
1207
+ raw: t[1] ?? "",
1208
+ argv: parseArgv(t[1] ?? ""),
1209
+ index: at + t.index,
1210
+ length: t[0].length,
1211
+ misplaced: true,
1212
+ other: false
1213
+ };
1214
+ return null;
1215
+ }
1216
+ function dropLine(text, index, length) {
1217
+ return text.slice(0, index) + text.slice(index + length).replace(/^\r?\n/, "");
1218
+ }
1219
+ function insertTopLevel(text, line) {
1220
+ const { head, tail: tail2 } = split(text);
1221
+ if (!tail2) return (head.trim() ? head.replace(/\s*$/, "\n") : "") + line + "\n";
1222
+ return `${(head.trim() ? head.replace(/\s*$/, "\n") : "") + line}
1223
+
1224
+ ${tail2}`;
1225
+ }
1226
+ function withNotify(text, line) {
1227
+ const n = readNotify(text);
1228
+ if (n && !n.misplaced && !n.other) return text.slice(0, n.index) + line + text.slice(n.index + n.length);
1229
+ if (n?.misplaced) return insertTopLevel(dropLine(text, n.index, n.length), line);
1230
+ return insertTopLevel(text, line);
1231
+ }
1232
+ function replaceNotify(text, line) {
1233
+ const n = readNotify(text);
1234
+ if (!n || n.misplaced || !n.length) return null;
1235
+ return text.slice(0, n.index) + line + text.slice(n.index + n.length);
1236
+ }
1237
+ function withoutNotify(text) {
1238
+ const n = readNotify(text);
1239
+ if (!n || n.other || !n.length) return text;
1240
+ return dropLine(text, n.index, n.length);
1241
+ }
1242
+ var PREVIOUS = "--previous-notify";
1243
+ function previousNotify(argv) {
1244
+ if (!Array.isArray(argv)) return null;
1245
+ const i = argv.indexOf(PREVIOUS);
1246
+ return i >= 0 && typeof argv[i + 1] === "string" ? parseArgv(argv[i + 1]) : null;
1247
+ }
1248
+ function stripPreviousRaw(raw) {
1249
+ return raw.replace(/,\s*"--previous-notify",\s*"(?:[^"\\]|\\.)*"\s*(?=\])/, "");
1250
+ }
1251
+ function withPreviousRaw(raw, prev) {
1252
+ const base = stripPreviousRaw(raw).replace(/\s*\]\s*$/, "");
1253
+ return `${base}, "--previous-notify", ${JSON.stringify(JSON.stringify(prev))}]`;
1254
+ }
1255
+ var isComputerUse = (argv) => Array.isArray(argv) && typeof argv[0] === "string" && /(^|\/)SkyComputerUseClient$/.test(argv[0]);
1256
+
1257
+ // ../core/src/adapters/codex.ts
1258
+ var NAME3 = "codex";
1259
+ var CODEX_EVENTS = [
1260
+ "SessionStart",
1261
+ "UserPromptSubmit",
1262
+ "PreToolUse",
1263
+ "PostToolUse",
1264
+ "PermissionRequest",
1265
+ "Stop",
1266
+ "Interrupt",
1267
+ "SubagentStart",
1268
+ "SubagentStop",
1269
+ "PreCompact",
1270
+ "PostCompact",
1271
+ "SessionEnd"
1272
+ ];
1273
+ var OPTS2 = { matcherFor: (e) => /Tool|Permission/.test(e) ? "" : void 0, timeout: 5 };
1274
+ var codexHome = (env) => env.CODEX_HOME || path6.join(HOME, ".codex");
1275
+ var hooksFile2 = (env) => path6.join(codexHome(env), "hooks.json");
1276
+ var configFile2 = (env) => path6.join(codexHome(env), "config.toml");
1277
+ var sessionsDir = (env) => path6.join(codexHome(env), "sessions");
1278
+ var validTool3 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
1279
+ var endReason2 = (r) => r === "clear" || r === "logout" || r === "exit" ? r : "other";
1280
+ function installNotify(cmd2, env) {
1281
+ const file = configFile2(env);
1282
+ let text = "";
1283
+ try {
1284
+ text = readFileSync4(file, "utf8");
1285
+ } catch {
1286
+ }
1287
+ const argv = cmd2([NAME3, "notify"]).argv;
1288
+ const line = `notify = ${JSON.stringify(argv)}`;
1289
+ const n = readNotify(text);
1290
+ const write = (next) => {
1291
+ mkdirSync3(path6.dirname(file), { recursive: true });
1292
+ writeFileSync3(file, next);
1293
+ };
1294
+ if (n && !n.other) {
1295
+ if (!n.misplaced && JSON.stringify(n.argv) === JSON.stringify(argv))
1296
+ return { file, changed: false, note: "notify current" };
1297
+ write(withNotify(text, line));
1298
+ return {
1299
+ file,
1300
+ changed: true,
1301
+ note: n.misplaced ? "notify moved to the top level (it sat inside a [table], where Codex never reads it)" : "notify updated"
1302
+ };
1303
+ }
1304
+ if (n?.other) {
1305
+ const prev = previousNotify(n.argv);
1306
+ if (isComputerUse(n.argv)) {
1307
+ if (prev && OURS2.test(JSON.stringify(prev))) {
1308
+ if (JSON.stringify(prev) === JSON.stringify(argv))
1309
+ return { file, changed: false, note: "notify current (after Codex Computer Use)" };
1310
+ }
1311
+ if (!prev || OURS2.test(JSON.stringify(prev))) {
1312
+ const next = replaceNotify(text, `notify = ${withPreviousRaw(n.raw, argv)}`);
1313
+ if (next) {
1314
+ write(next);
1315
+ return { file, changed: true, note: "chained after Codex Computer Use (--previous-notify)" };
1316
+ }
1317
+ }
1318
+ }
1319
+ return {
1320
+ file,
1321
+ changed: false,
1322
+ note: `left alone: notify runs ${n.raw.slice(0, 80)} (hooks.json carries the events; the notify fallback is off)`
1323
+ };
1324
+ }
1325
+ write(withNotify(text, line));
1326
+ return { file, changed: true, note: "notify set at the top level" };
1327
+ }
1328
+ function uninstallNotify(env) {
1329
+ const file = configFile2(env);
1330
+ let text;
1331
+ try {
1332
+ text = readFileSync4(file, "utf8");
1333
+ } catch {
1334
+ return { file, changed: false };
1335
+ }
1336
+ const n = readNotify(text);
1337
+ if (!n) return { file, changed: false };
1338
+ if (!n.other) {
1339
+ writeFileSync3(file, withoutNotify(text));
1340
+ return { file, changed: true };
1341
+ }
1342
+ const prev = previousNotify(n.argv);
1343
+ if (isComputerUse(n.argv) && prev && OURS2.test(JSON.stringify(prev))) {
1344
+ const next = replaceNotify(text, `notify = ${stripPreviousRaw(n.raw)}`);
1345
+ if (next) writeFileSync3(file, next);
1346
+ return { file, changed: true };
1347
+ }
1348
+ return { file, changed: false };
1349
+ }
1350
+ function findRollout(threadId, env) {
1351
+ let best = null;
1352
+ const walk = (dir, depth) => {
1353
+ let names = [];
1354
+ try {
1355
+ names = readdirSync4(dir);
1356
+ } catch {
1357
+ return;
1358
+ }
1359
+ for (const n of names.sort().reverse()) {
1360
+ const p = path6.join(dir, n);
1361
+ if (n.endsWith(".jsonl") && n.includes(threadId)) {
1362
+ best = p;
1363
+ return;
1364
+ }
1365
+ if (depth < 3) {
1366
+ walk(p, depth + 1);
1367
+ if (best) return;
1368
+ }
1369
+ }
1370
+ };
1371
+ walk(sessionsDir(env), 0);
1372
+ return best;
1373
+ }
1374
+ var codex = {
1375
+ name: NAME3,
1376
+ events: CODEX_EVENTS,
1377
+ detect: (env = process.env) => existsSync4(codexHome(env)),
1378
+ configFiles: (env = process.env) => [hooksFile2(env), configFile2(env)],
1379
+ install: (cmd2, env = process.env) => [
1380
+ installClaudeShaped(hooksFile2(env), NAME3, CODEX_EVENTS, cmd2, OPTS2),
1381
+ installNotify(cmd2, env)
1382
+ ],
1383
+ uninstall: (env = process.env) => [uninstallClaudeShaped(hooksFile2(env), CODEX_EVENTS), uninstallNotify(env)],
1384
+ installed: (cmd2, env = process.env) => {
1385
+ const out2 = [
1386
+ { file: hooksFile2(env), state: installedClaudeShaped(hooksFile2(env), NAME3, CODEX_EVENTS, cmd2, OPTS2) }
1387
+ ];
1388
+ let text = "";
1389
+ try {
1390
+ text = readFileSync4(configFile2(env), "utf8");
1391
+ } catch {
1392
+ }
1393
+ const n = readNotify(text);
1394
+ const argv = cmd2([NAME3, "notify"]).argv;
1395
+ const prev = n ? previousNotify(n.argv) : null;
1396
+ const ours = n && (!n.other || prev && JSON.stringify(prev) === JSON.stringify(argv));
1397
+ out2.push({
1398
+ file: configFile2(env),
1399
+ state: !n || n.other && !ours ? "missing" : n.misplaced ? "misplaced" : JSON.stringify(n.argv) === JSON.stringify(argv) || prev && JSON.stringify(prev) === JSON.stringify(argv) ? "current" : "stale"
1400
+ });
1401
+ return out2;
1402
+ },
1403
+ fromHook(input) {
1404
+ const event = input.argv[1] ?? "";
1405
+ let s = {};
1406
+ if (event === "notify") {
1407
+ const raw = input.argv.find((a) => a.startsWith("{")) ?? input.stdin;
1408
+ try {
1409
+ s = raw ? JSON.parse(raw) : {};
1410
+ } catch {
1411
+ }
1412
+ const id2 = s["thread-id"] ?? s.thread_id ?? s["session-id"] ?? s.session_id;
1413
+ if (!id2) return null;
1414
+ const r2 = {
1415
+ session: { id: String(id2), cwd: typeof s.cwd === "string" ? s.cwd : input.cwd },
1416
+ events: [
1417
+ {
1418
+ type: "turn.ended",
1419
+ data: { reason: "stop", ...typeof s["turn-id"] === "string" ? { turn_id: s["turn-id"] } : {} },
1420
+ harnessEvent: "notify"
1421
+ }
1422
+ ],
1423
+ hints: {}
1424
+ };
1425
+ const file = findRollout(String(id2), input.env);
1426
+ if (file) r2.transcript = file;
1427
+ return r2;
1428
+ }
1429
+ try {
1430
+ s = input.stdin ? JSON.parse(input.stdin) : {};
1431
+ } catch {
1432
+ }
1433
+ const id = s.session_id;
1434
+ if (!id) return null;
1435
+ const session = { id: String(id), cwd: typeof s.cwd === "string" ? s.cwd : input.cwd };
1436
+ if (typeof s.model === "string") session.model = s.model;
1437
+ const turn_id = typeof s.turn_id === "string" ? s.turn_id : void 0;
1438
+ const tool = validTool3(s.tool_name) ? s.tool_name : void 0;
1439
+ const call_id = typeof s.tool_use_id === "string" ? s.tool_use_id : void 0;
1440
+ const ev = (type, data) => ({ type, data: prune3(data), harnessEvent: event });
1441
+ const events = [];
1442
+ switch (event) {
1443
+ case "SessionStart":
1444
+ events.push(
1445
+ ev("session.started", {
1446
+ source: typeof s.source === "string" && ["startup", "resume", "clear", "compact", "fork"].includes(s.source) ? s.source : "unknown"
1447
+ })
1448
+ );
1449
+ break;
1450
+ case "UserPromptSubmit":
1451
+ events.push(
1452
+ ev("turn.started", {
1453
+ turn_id,
1454
+ prompt_chars: typeof s.prompt === "string" ? [...s.prompt].length : void 0
1455
+ })
1456
+ );
1457
+ break;
1458
+ case "PreToolUse":
1459
+ if (tool) events.push(ev("tool.started", { tool, call_id, turn_id, input: s.tool_input }));
1460
+ break;
1461
+ case "PostToolUse":
1462
+ if (tool)
1463
+ events.push(
1464
+ ev("tool.ended", { tool, call_id, turn_id, ok: true, input: s.tool_input, output: s.tool_response })
1465
+ );
1466
+ break;
1467
+ case "PermissionRequest":
1468
+ events.push(
1469
+ ev("attention.needed", { attention_id: call_id ?? `perm-${Date.now()}`, kind: "permission", tool })
1470
+ );
1471
+ break;
1472
+ case "Stop":
1473
+ events.push(ev("turn.ended", { turn_id, reason: "stop" }));
1474
+ break;
1475
+ case "Interrupt":
1476
+ events.push(ev("turn.ended", { turn_id, reason: "interrupt" }));
1477
+ break;
1478
+ case "SubagentStart":
1479
+ events.push(
1480
+ ev("subagent.started", {
1481
+ agent_id: String(s.agent_id ?? s.subagent_id ?? ""),
1482
+ agent_type: typeof s.agent_type === "string" ? s.agent_type : void 0
1483
+ })
1484
+ );
1485
+ break;
1486
+ case "SubagentStop":
1487
+ events.push(
1488
+ ev("subagent.ended", {
1489
+ agent_id: String(s.agent_id ?? s.subagent_id ?? ""),
1490
+ agent_type: typeof s.agent_type === "string" ? s.agent_type : void 0
1491
+ })
1492
+ );
1493
+ break;
1494
+ case "PreCompact":
1495
+ events.push(ev("context.compacted", { trigger: s.trigger === "manual" ? "manual" : "auto" }));
1496
+ break;
1497
+ case "PostCompact":
1498
+ break;
1499
+ case "SessionEnd":
1500
+ events.push(ev("session.ended", { reason: endReason2(s.reason) }));
1501
+ break;
1502
+ default:
1503
+ return null;
1504
+ }
1505
+ const r = { session, events, hints: {} };
1506
+ if (typeof s.transcript_path === "string") r.transcript = s.transcript_path;
1507
+ return r;
1508
+ },
1509
+ readTranscript(file, fromLine) {
1510
+ let text = "";
1511
+ try {
1512
+ text = readFileSync4(file, "utf8");
1513
+ } catch {
1514
+ return { turns: [], line: fromLine };
1515
+ }
1516
+ return readCodex(text.split("\n"), fromLine);
1517
+ },
1518
+ facts(session, transcript) {
1519
+ const out2 = {};
1520
+ if (transcript) Object.assign(out2, codexFacts(transcript));
1521
+ return out2;
1522
+ },
1523
+ backfill(sinceMs, env = process.env) {
1524
+ const rows = [];
1525
+ for (const f of recentFiles(sessionsDir(env), 3, sinceMs, (n) => /^rollout-.*\.jsonl$/.test(n))) {
1526
+ const m = /([0-9a-f]{8}-[0-9a-f-]{27,})\.jsonl$/i.exec(path6.basename(f));
1527
+ if (!m?.[1]) continue;
1528
+ const facts = codexFacts(f);
1529
+ if (!facts.started_at || !facts.last_at) continue;
1530
+ const { started_at, last_at, first_ask: _fa, harness_version: _hv, ...rest } = facts;
1531
+ rows.push({ session: { id: m[1], ...rest }, started_at, last_at });
1532
+ }
1533
+ return rows;
1534
+ }
1535
+ };
1536
+ function prune3(o) {
1537
+ for (const k of Object.keys(o)) if (o[k] === void 0) delete o[k];
1538
+ return o;
1539
+ }
1540
+ function codexFacts(file) {
1541
+ const { head, tail: tail2 } = ends(file, 128 * 1024, 64 * 1024);
1542
+ const H = parseLines(head);
1543
+ const T = parseLines(tail2);
1544
+ const out2 = {};
1545
+ const meta = H.find((j) => j.type === "session_meta" && j.payload);
1546
+ if (meta?.payload) {
1547
+ const p = meta.payload;
1548
+ if (p.cwd) out2.cwd = p.cwd;
1549
+ if (p.cli_version) out2.harness_version = String(p.cli_version);
1550
+ if (p.git?.branch) out2.branch = p.git.branch;
1551
+ if (p.git?.repository_url) out2.repo = String(p.git.repository_url).replace(/\/\/[^/@]*@/, "//");
1552
+ }
1553
+ const ctx = H.concat(T).reverse().find((j) => j.type === "turn_context" && j.payload?.model);
1554
+ if (ctx?.payload?.model) out2.model = ctx.payload.model;
1555
+ for (const j of H) {
1556
+ const p = j.payload;
1557
+ if (j.type !== "response_item" || !p || p.type !== "message" || p.role !== "user") continue;
1558
+ const text = (p.content ?? []).filter((b) => b && b.type === "input_text" && typeof b.text === "string").map((b) => b.text).join("\n").trim();
1559
+ if (!text || /^<[a-z_-]+>/i.test(text)) continue;
1560
+ const line = text.split("\n").find((l) => l.trim()) ?? "";
1561
+ out2.title = line.length > 80 ? `${line.slice(0, 80).replace(/\s+\S*$/, "")}\u2026` : line;
1562
+ out2.title_source = "first-ask";
1563
+ break;
1564
+ }
1565
+ const times = H.concat(T).map((j) => Date.parse(String(j.timestamp ?? ""))).filter((n) => n > 0);
1566
+ if (times.length) {
1567
+ out2.started_at = Math.min(...times);
1568
+ out2.last_at = Math.max(...times);
1569
+ }
1570
+ return out2;
1571
+ }
1572
+
1573
+ // ../core/src/adapters/gemini-cli.ts
1574
+ import { existsSync as existsSync5, readFileSync as readFileSync5 } from "node:fs";
1575
+ import path7 from "node:path";
1576
+ var NAME4 = "gemini-cli";
1577
+ var GEMINI_EVENTS = [
1578
+ "SessionStart",
1579
+ "BeforeAgent",
1580
+ "BeforeTool",
1581
+ "AfterTool",
1582
+ "Notification",
1583
+ "AfterAgent",
1584
+ "PreCompress",
1585
+ "SessionEnd"
1586
+ ];
1587
+ var OPTS3 = { matcherFor: (e) => /Tool/.test(e) ? ".*" : void 0, timeout: 5e3 };
1588
+ var geminiDir = (env) => env.GEMINI_CLI_HOME || path7.join(HOME, ".gemini");
1589
+ var settings = (env) => path7.join(geminiDir(env), "settings.json");
1590
+ var validTool4 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
1591
+ var geminiCli = {
1592
+ name: NAME4,
1593
+ events: GEMINI_EVENTS,
1594
+ // ~/.gemini also belongs to Antigravity; Gemini CLI is here when its settings or its chat logs exist.
1595
+ detect: (env = process.env) => existsSync5(settings(env)) || existsSync5(path7.join(geminiDir(env), "tmp")),
1596
+ configFiles: (env = process.env) => [settings(env)],
1597
+ install: (cmd2, env = process.env) => [installClaudeShaped(settings(env), NAME4, GEMINI_EVENTS, cmd2, OPTS3)],
1598
+ uninstall: (env = process.env) => [uninstallClaudeShaped(settings(env), GEMINI_EVENTS)],
1599
+ installed: (cmd2, env = process.env) => [
1600
+ { file: settings(env), state: installedClaudeShaped(settings(env), NAME4, GEMINI_EVENTS, cmd2, OPTS3) }
1601
+ ],
1602
+ fromHook(input) {
1603
+ const event = input.argv[1] ?? "";
1604
+ let s = {};
1605
+ try {
1606
+ s = input.stdin ? JSON.parse(input.stdin) : {};
1607
+ } catch {
1608
+ }
1609
+ const id = s.session_id;
1610
+ if (!id) return null;
1611
+ const session = { id: String(id), cwd: typeof s.cwd === "string" ? s.cwd : input.cwd };
1612
+ const tool = validTool4(s.tool_name) ? s.tool_name : void 0;
1613
+ const ev = (type, data) => ({ type, data: prune4(data), harnessEvent: event });
1614
+ const events = [];
1615
+ switch (event) {
1616
+ case "SessionStart":
1617
+ events.push(
1618
+ ev("session.started", {
1619
+ source: typeof s.source === "string" && ["startup", "resume", "clear"].includes(s.source) ? s.source : "unknown"
1620
+ })
1621
+ );
1622
+ break;
1623
+ case "BeforeAgent":
1624
+ events.push(
1625
+ ev("turn.started", { prompt_chars: typeof s.prompt === "string" ? [...s.prompt].length : void 0 })
1626
+ );
1627
+ break;
1628
+ case "BeforeTool":
1629
+ if (tool) events.push(ev("tool.started", { tool, input: s.tool_input }));
1630
+ break;
1631
+ case "AfterTool": {
1632
+ const resp = s.tool_response;
1633
+ const err = resp && typeof resp === "object" && resp.error ? String(typeof resp.error === "string" ? resp.error : JSON.stringify(resp.error)).slice(0, 2e3) : void 0;
1634
+ if (tool)
1635
+ events.push(
1636
+ ev("tool.ended", {
1637
+ tool,
1638
+ ok: !err,
1639
+ error: err,
1640
+ input: s.tool_input,
1641
+ output: resp?.llmContent ?? s.tool_response
1642
+ })
1643
+ );
1644
+ break;
1645
+ }
1646
+ case "Notification":
1647
+ events.push(
1648
+ ev("attention.needed", {
1649
+ attention_id: `notif-${Date.now()}`,
1650
+ kind: s.notification_type === "ToolPermission" ? "permission" : "question",
1651
+ message: typeof s.message === "string" ? s.message.slice(0, 200) : void 0
1652
+ })
1653
+ );
1654
+ break;
1655
+ case "AfterAgent":
1656
+ events.push(ev("turn.ended", { reason: "stop" }));
1657
+ break;
1658
+ case "PreCompress":
1659
+ events.push(ev("context.compacted", { trigger: s.trigger === "manual" ? "manual" : "auto" }));
1660
+ break;
1661
+ case "SessionEnd":
1662
+ events.push(
1663
+ ev("session.ended", {
1664
+ reason: s.reason === "clear" || s.reason === "logout" || s.reason === "exit" ? s.reason : s.reason === "prompt_input_exit" ? "exit" : "other"
1665
+ })
1666
+ );
1667
+ break;
1668
+ default:
1669
+ return null;
1670
+ }
1671
+ const r = { session, events, hints: {} };
1672
+ if (typeof s.transcript_path === "string") r.transcript = s.transcript_path;
1673
+ return r;
1674
+ },
1675
+ readTranscript(file, fromLine) {
1676
+ let text = "";
1677
+ try {
1678
+ text = readFileSync5(file, "utf8");
1679
+ } catch {
1680
+ return { turns: [], line: fromLine };
1681
+ }
1682
+ return readGemini(text.split("\n"), fromLine);
1683
+ },
1684
+ facts(_session, transcript) {
1685
+ const out2 = {};
1686
+ if (!transcript) return out2;
1687
+ const { head, tail: tail2 } = ends(transcript, 64 * 1024, 64 * 1024);
1688
+ const H = parseLines(head);
1689
+ const T = parseLines(tail2);
1690
+ const first = H.find((j) => j.type === "user");
1691
+ const raw = first ? first.content ?? first.message ?? first.text : void 0;
1692
+ const text = typeof raw === "string" ? raw.trim() : "";
1693
+ if (text) {
1694
+ const line = text.split("\n").find((l) => l.trim()) ?? "";
1695
+ out2.title = line.length > 80 ? `${line.slice(0, 80).replace(/\s+\S*$/, "")}\u2026` : line;
1696
+ out2.title_source = "first-ask";
1697
+ }
1698
+ const times = H.concat(T).map((j) => Date.parse(String(j.timestamp ?? ""))).filter((n) => n > 0);
1699
+ if (times.length) {
1700
+ out2.started_at = Math.min(...times);
1701
+ out2.last_at = Math.max(...times);
1702
+ }
1703
+ return out2;
1704
+ },
1705
+ backfill: () => []
1706
+ // chat logs are keyed by a project hash, not a session id we can name; the first hook of a resumed session reports it
1707
+ };
1708
+ function prune4(o) {
1709
+ for (const k of Object.keys(o)) if (o[k] === void 0) delete o[k];
1710
+ return o;
1711
+ }
1712
+
1713
+ // ../core/src/adapters/index.ts
1714
+ var ADAPTERS = [claudeCode, codex, geminiCli, antigravity];
1715
+ var adapterByName = (name) => ADAPTERS.find((a) => a.name === name);
1716
+
1717
+ // ../core/src/config.ts
1718
+ import { chmodSync, existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "node:fs";
1719
+ import path8 from "node:path";
1720
+ var DEFAULT_CONFIG = { sinks: [], harnesses: {}, update_check: true, keep_days: 30 };
1721
+ function readConfig(file = configFile()) {
1722
+ try {
1723
+ const j = JSON.parse(readFileSync6(file, "utf8"));
1724
+ return {
1725
+ ...DEFAULT_CONFIG,
1726
+ ...j,
1727
+ sinks: Array.isArray(j.sinks) ? j.sinks : [],
1728
+ harnesses: j.harnesses && typeof j.harnesses === "object" ? j.harnesses : {}
1729
+ };
1730
+ } catch {
1731
+ return { ...DEFAULT_CONFIG, sinks: [], harnesses: {} };
1732
+ }
1733
+ }
1734
+ function writeConfig(c, file = configFile()) {
1735
+ mkdirSync4(path8.dirname(file), { recursive: true, mode: 448 });
1736
+ writeFileSync4(file, `${JSON.stringify(c, null, 2)}
1737
+ `, { mode: 384 });
1738
+ if (process.platform !== "win32" && existsSync6(file)) {
1739
+ try {
1740
+ chmodSync(file, 384);
1741
+ } catch {
1742
+ }
1743
+ }
1744
+ }
1745
+ function machineName(c, hostname) {
1746
+ if (c.machine) return String(c.machine).slice(0, 80);
1747
+ return hostname.replace(/\.(local|lan|home)$/i, "");
1748
+ }
1749
+
1750
+ // ../core/src/ulid.ts
1751
+ import { randomBytes } from "node:crypto";
1752
+ var ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
1753
+ var lastTime = 0;
1754
+ var lastRand = [];
1755
+ function encodeTime(t) {
1756
+ let out2 = "";
1757
+ let n = t;
1758
+ for (let i = 0; i < 10; i++) {
1759
+ out2 = ALPHABET[n % 32] + out2;
1760
+ n = Math.floor(n / 32);
1761
+ }
1762
+ return out2;
1763
+ }
1764
+ function ulid(now = Date.now()) {
1765
+ if (now === lastTime) {
1766
+ for (let i = lastRand.length - 1; i >= 0; i--) {
1767
+ const v = (lastRand[i] ?? 0) + 1;
1768
+ if (v < 32) {
1769
+ lastRand[i] = v;
1770
+ break;
1771
+ }
1772
+ lastRand[i] = 0;
1773
+ }
1774
+ } else {
1775
+ lastTime = now;
1776
+ const b = randomBytes(16);
1777
+ lastRand = Array.from({ length: 16 }, (_, i) => (b[i] ?? 0) % 32);
1778
+ }
1779
+ return encodeTime(now) + lastRand.map((v) => ALPHABET[v]).join("");
1780
+ }
1781
+
1782
+ // ../core/src/events.ts
1783
+ function makeEvent(a, ctx) {
1784
+ const now = ctx.now ?? Date.now();
1785
+ const harness = { name: ctx.harness.name, event: a.harnessEvent };
1786
+ if (ctx.harness.version) harness.version = ctx.harness.version;
1787
+ return {
1788
+ protocol: 1,
1789
+ id: ulid(now),
1790
+ type: a.type,
1791
+ time: new Date(now).toISOString(),
1792
+ tier: 3,
1793
+ harness,
1794
+ session: ctx.session,
1795
+ privacy: { rulesets: [], pii: false },
1796
+ data: a.data
1797
+ };
1798
+ }
1799
+
1800
+ // ../core/src/outbox.ts
1801
+ import {
1802
+ appendFileSync,
1803
+ closeSync as closeSync2,
1804
+ existsSync as existsSync7,
1805
+ mkdirSync as mkdirSync5,
1806
+ openSync as openSync2,
1807
+ readdirSync as readdirSync5,
1808
+ readFileSync as readFileSync7,
1809
+ readSync as readSync2,
1810
+ renameSync,
1811
+ statSync as statSync4,
1812
+ unlinkSync,
1813
+ writeFileSync as writeFileSync5
1814
+ } from "node:fs";
1815
+ import path9 from "node:path";
1816
+ var Outbox = class {
1817
+ constructor(root) {
1818
+ this.root = root;
1819
+ }
1820
+ root;
1821
+ dir(harness) {
1822
+ return path9.join(this.root, "outbox", safeId(harness));
1823
+ }
1824
+ file(ref) {
1825
+ return path9.join(this.dir(ref.harness), `${safeId(ref.session)}.jsonl`);
1826
+ }
1827
+ seqFile(ref) {
1828
+ return path9.join(this.dir(ref.harness), `${safeId(ref.session)}.seq`);
1829
+ }
1830
+ /** The next seq for a session, persisted (survives a hibernating machine and a
1831
+ * restart; an in-memory counter would restart at 0 and collide). */
1832
+ nextSeq(ref) {
1833
+ const f = this.seqFile(ref);
1834
+ let n = 0;
1835
+ try {
1836
+ n = Number(readFileSync7(f, "utf8")) || 0;
1837
+ } catch {
1838
+ n = this.lastSeqInFile(ref) + 1;
1839
+ }
1840
+ mkdirSync5(path9.dirname(f), { recursive: true });
1841
+ writeFileSync5(f, String(n + 1));
1842
+ return n;
1843
+ }
1844
+ lastSeqInFile(ref) {
1845
+ const f = this.file(ref);
1846
+ if (!existsSync7(f)) return -1;
1847
+ const lines = readFileSync7(f, "utf8").trim().split("\n");
1848
+ for (let i = lines.length - 1; i >= 0; i--) {
1849
+ try {
1850
+ const j = JSON.parse(lines[i] ?? "");
1851
+ if (typeof j.session?.seq === "number") return j.session.seq;
1852
+ } catch {
1853
+ }
1854
+ }
1855
+ return -1;
1856
+ }
1857
+ append(ref, events) {
1858
+ if (!events.length) return;
1859
+ mkdirSync5(this.dir(ref.harness), { recursive: true });
1860
+ appendFileSync(this.file(ref), `${events.map((e) => JSON.stringify(e)).join("\n")}
1861
+ `);
1862
+ }
1863
+ /** Every session file, newest first. */
1864
+ sessions() {
1865
+ const out2 = [];
1866
+ const base = path9.join(this.root, "outbox");
1867
+ if (!existsSync7(base)) return [];
1868
+ for (const h of readdirSync5(base)) {
1869
+ const d = path9.join(base, h);
1870
+ let names = [];
1871
+ try {
1872
+ names = readdirSync5(d).filter((n) => n.endsWith(".jsonl"));
1873
+ } catch {
1874
+ continue;
1875
+ }
1876
+ for (const n of names) {
1877
+ try {
1878
+ out2.push({ ref: { harness: h, session: n.slice(0, -6) }, mtime: statSync4(path9.join(d, n)).mtimeMs });
1879
+ } catch {
1880
+ }
1881
+ }
1882
+ }
1883
+ return out2.sort((a, b) => b.mtime - a.mtime).map((x) => x.ref);
1884
+ }
1885
+ /** Read events after byte offset `from`; returns them and the new offset. Lines
1886
+ * are read whole; a partial trailing line (a write in progress) is left. */
1887
+ read(ref, from, maxEvents = 500) {
1888
+ const f = this.file(ref);
1889
+ if (!existsSync7(f)) return { events: [], offset: from };
1890
+ const size = statSync4(f).size;
1891
+ if (from >= size) return { events: [], offset: from };
1892
+ const fd = openSync2(f, "r");
1893
+ try {
1894
+ const buf = Buffer.alloc(Math.min(size - from, 8 * 1024 * 1024));
1895
+ const n = readSync2(fd, buf, 0, buf.length, from);
1896
+ const text = buf.subarray(0, n).toString("utf8");
1897
+ const events = [];
1898
+ let offset = from;
1899
+ let pos = 0;
1900
+ while (events.length < maxEvents) {
1901
+ const nl = text.indexOf("\n", pos);
1902
+ if (nl < 0) break;
1903
+ const line = text.slice(pos, nl);
1904
+ pos = nl + 1;
1905
+ offset = from + Buffer.byteLength(text.slice(0, pos), "utf8");
1906
+ if (!line.trim()) continue;
1907
+ try {
1908
+ events.push(JSON.parse(line));
1909
+ } catch {
1910
+ }
1911
+ }
1912
+ return { events, offset };
1913
+ } finally {
1914
+ closeSync2(fd);
1915
+ }
1916
+ }
1917
+ /** Files older than `days` are pruned; the cursor files with them. */
1918
+ prune(days, now = Date.now()) {
1919
+ let n = 0;
1920
+ const cutoff = now - days * 864e5;
1921
+ for (const ref of this.sessions()) {
1922
+ const f = this.file(ref);
1923
+ try {
1924
+ if (statSync4(f).mtimeMs < cutoff) {
1925
+ unlinkSync(f);
1926
+ try {
1927
+ unlinkSync(this.seqFile(ref));
1928
+ } catch {
1929
+ }
1930
+ n++;
1931
+ }
1932
+ } catch {
1933
+ }
1934
+ }
1935
+ return n;
1936
+ }
1937
+ delete(ref) {
1938
+ for (const f of [this.file(ref), this.seqFile(ref)]) {
1939
+ try {
1940
+ unlinkSync(f);
1941
+ } catch {
1942
+ }
1943
+ }
1944
+ }
1945
+ };
1946
+ var Cursors = class {
1947
+ constructor(root, sink2) {
1948
+ this.root = root;
1949
+ this.sink = sink2;
1950
+ }
1951
+ root;
1952
+ sink;
1953
+ file() {
1954
+ return path9.join(this.root, "cursors", `${safeId(this.sink)}.json`);
1955
+ }
1956
+ read() {
1957
+ try {
1958
+ return JSON.parse(readFileSync7(this.file(), "utf8"));
1959
+ } catch {
1960
+ return {};
1961
+ }
1962
+ }
1963
+ write(c) {
1964
+ const f = this.file();
1965
+ mkdirSync5(path9.dirname(f), { recursive: true });
1966
+ const tmp = `${f}.${process.pid}.tmp`;
1967
+ writeFileSync5(tmp, JSON.stringify(c));
1968
+ renameSync(tmp, f);
1969
+ }
1970
+ key(ref) {
1971
+ return `${ref.harness}/${ref.session}`;
1972
+ }
1973
+ };
1974
+ function takeLock(root, ref, staleMs = 5 * 6e4) {
1975
+ const dir = path9.join(root, "locks");
1976
+ mkdirSync5(dir, { recursive: true });
1977
+ const f = path9.join(dir, `${safeId(ref.harness)}-${safeId(ref.session)}.lock`);
1978
+ try {
1979
+ if (Date.now() - statSync4(f).mtimeMs > staleMs) unlinkSync(f);
1980
+ else return null;
1981
+ } catch {
1982
+ }
1983
+ try {
1984
+ writeFileSync5(f, String(process.pid), { flag: "wx" });
1985
+ } catch {
1986
+ return null;
1987
+ }
1988
+ return () => {
1989
+ try {
1990
+ unlinkSync(f);
1991
+ } catch {
1992
+ }
1993
+ };
1994
+ }
1995
+
1996
+ // ../core/src/privacy/pii.ts
1997
+ import { createHash as createHash2 } from "node:crypto";
1998
+ var RULESET_PII = "pii@1";
1999
+ var EMAIL = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g;
2000
+ var PHONE = /(?<![\w.])(?:\+\d{1,3}[ .-]?)?(?:\(\d{2,4}\)[ .-]?)?\d{2,4}(?:[ .-]?\d{2,4}){1,3}(?!\w)/g;
2001
+ function looksPhone(s) {
2002
+ const digits = s.replace(/\D/g, "");
2003
+ if (digits.length < 7 || digits.length > 15) return false;
2004
+ if (!/[+() .-]/.test(s)) return false;
2005
+ if (!/[+() -]/.test(s)) return false;
2006
+ return true;
2007
+ }
2008
+ function reducePii(text, home) {
2009
+ if (!text) return text;
2010
+ let out2 = text.replace(EMAIL, "[email]");
2011
+ out2 = out2.replace(PHONE, (m) => looksPhone(m) ? "[phone]" : m);
2012
+ if (home) {
2013
+ const h = home.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2014
+ out2 = out2.replace(new RegExp(`(?<![A-Za-z0-9_])${h}(?=/|$|\\s|["'\`])`, "g"), "~");
2015
+ }
2016
+ return out2;
2017
+ }
2018
+ function hashMachine(name) {
2019
+ return createHash2("sha256").update(name).digest("hex").slice(0, 8);
2020
+ }
2021
+ function reducePiiDeep(v, home) {
2022
+ if (typeof v === "string") return reducePii(v, home);
2023
+ if (Array.isArray(v)) return v.map((x) => reducePiiDeep(x, home));
2024
+ if (v && typeof v === "object") {
2025
+ const out2 = {};
2026
+ for (const [k, x] of Object.entries(v)) out2[k] = reducePiiDeep(x, home);
2027
+ return out2;
2028
+ }
2029
+ return v;
2030
+ }
2031
+
2032
+ // ../core/src/privacy/secrets.ts
2033
+ var RULESET_SECRETS = "secrets@1";
2034
+ var REDACTED = "[redacted]";
2035
+ var TOKEN_PATTERNS = [
2036
+ /-----BEGIN [A-Z0-9 ]*PRIVATE KEY(?: BLOCK)?-----[\s\S]*?(?:-----END [A-Z0-9 ]*PRIVATE KEY(?: BLOCK)?-----|$)/g,
2037
+ /\b(?:AKIA|ASIA|ABIA|ACCA)[0-9A-Z]{16}\b/g,
2038
+ // AWS access key id
2039
+ /\bgh[pousr]_[A-Za-z0-9]{30,}\b/g,
2040
+ // GitHub
2041
+ /\bgithub_pat_[A-Za-z0-9_]{30,}\b/g,
2042
+ /\bglpat-[A-Za-z0-9_-]{20,}\b/g,
2043
+ // GitLab
2044
+ /\bxox[abposre]-[A-Za-z0-9-]{10,}\b/g,
2045
+ // Slack
2046
+ /\bhttps:\/\/hooks\.slack\.com\/services\/[A-Za-z0-9/_-]+/g,
2047
+ /\bhttps:\/\/(?:ptb\.|canary\.)?discord(?:app)?\.com\/api\/webhooks\/[0-9]+\/[A-Za-z0-9_-]+/g,
2048
+ /\bsk-(?:ant-|proj-|svcacct-|admin-|or-v1-)?[A-Za-z0-9_-]{20,}\b/g,
2049
+ // Anthropic, OpenAI, DeepSeek, OpenRouter
2050
+ /\b(?:sk|rk)_(?:live|test)_[A-Za-z0-9]{16,}\b/g,
2051
+ // Stripe secret / restricted
2052
+ /\bwhsec_[A-Za-z0-9+/=]{20,}/g,
2053
+ // Stripe / Svix / Standard Webhooks secret
2054
+ /\bAIza[0-9A-Za-z_-]{35}\b/g,
2055
+ // Google API key
2056
+ /\bya29\.[0-9A-Za-z_-]{20,}/g,
2057
+ // Google OAuth access token
2058
+ /\b1\/\/0[0-9A-Za-z_-]{30,}/g,
2059
+ // Google OAuth refresh token
2060
+ /\bGOCSPX-[0-9A-Za-z_-]{20,}/g,
2061
+ // Google OAuth client secret
2062
+ /\bxai-[A-Za-z0-9]{30,}\b/g,
2063
+ /\bgsk_[A-Za-z0-9]{30,}\b/g,
2064
+ // Groq
2065
+ /\bpplx-[A-Za-z0-9]{30,}\b/g,
2066
+ // Perplexity
2067
+ /\bhf_[A-Za-z0-9]{30,}\b/g,
2068
+ // Hugging Face
2069
+ /\bpcsk_[A-Za-z0-9_]{30,}\b/g,
2070
+ // Pinecone
2071
+ /\bnpm_[A-Za-z0-9]{30,}\b/g,
2072
+ /\bpypi-[A-Za-z0-9_-]{50,}/g,
2073
+ /\bSG\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}/g,
2074
+ // SendGrid
2075
+ /\bshp(?:at|ss|ca|pa)_[a-fA-F0-9]{32}\b/g,
2076
+ // Shopify
2077
+ /\bdop_v1_[a-f0-9]{64}\b/g,
2078
+ // DigitalOcean
2079
+ /\b(?:lin_api|lin_oauth)_[A-Za-z0-9]{30,}\b/g,
2080
+ // Linear
2081
+ /\b(?:ntn|secret)_[A-Za-z0-9]{40,}\b/g,
2082
+ // Notion
2083
+ /\bsbp_[a-f0-9]{40}\b/g,
2084
+ // Supabase
2085
+ /\bfigd_[A-Za-z0-9_-]{30,}/g,
2086
+ // Figma
2087
+ /\bdp\.(?:pt|st|sa|ct)\.[A-Za-z0-9]{30,}/g,
2088
+ // Doppler
2089
+ /\bgrn_[A-Za-z0-9_-]{20,}/g,
2090
+ // Granola
2091
+ /\bss_[a-f0-9]{32,}\b/g,
2092
+ // spacesheep API keys
2093
+ /\b[0-9]{8,10}:AA[A-Za-z0-9_-]{33}\b/g,
2094
+ // Telegram bot token
2095
+ /\beyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g
2096
+ // JWT
2097
+ ];
2098
+ var URL_CREDENTIALS = /\b([a-z][a-z0-9+.-]{1,20}:\/\/[^\s:/@'"`<>]{1,100}:)([^\s@/'"`<>]{1,200})(@)/gi;
2099
+ var AUTH_HEADER = /\b((?:Bearer|Basic|Token)\s+)([A-Za-z0-9._~+/=-]{16,})/g;
2100
+ var SECRET_WORD = "(?:pass(?:word|wd|phrase|code)?s?|pwd|secret(?:[ _-]?key)?s?|client[ _-]?secret|api[ _-]?keys?|apikeys?|access[ _-]?keys?|access[ _-]?tokens?|auth[ _-]?tokens?|refresh[ _-]?tokens?|private[ _-]?keys?|tokens?|credentials?|creds)";
2101
+ var NAME_SEP = /(?<![A-Za-z0-9_])([A-Za-z][A-Za-z0-9_-]{1,79})(["']?\s{0,3}(?::=|=>|=|:)\s{0,3})/g;
2102
+ var VALUE = /"([^"\n]{1,300})"|'([^'\n]{1,300})'|`([^`\n]{1,300})`|([^\s"'`,;&)}\]]{1,300})/y;
2103
+ function redactAssignments(text) {
2104
+ let out2 = "";
2105
+ let last = 0;
2106
+ NAME_SEP.lastIndex = 0;
2107
+ for (let m = NAME_SEP.exec(text); m; m = NAME_SEP.exec(text)) {
2108
+ const name = m[1] ?? "";
2109
+ if (!isSecretName(name)) continue;
2110
+ VALUE.lastIndex = NAME_SEP.lastIndex;
2111
+ const v = VALUE.exec(text);
2112
+ if (!v) continue;
2113
+ const value = v[1] ?? v[2] ?? v[3] ?? v[4] ?? "";
2114
+ NAME_SEP.lastIndex = VALUE.lastIndex;
2115
+ if (isPlaceholder(value) || value.includes(REDACTED)) continue;
2116
+ if (/tokens?$/i.test(name) && /^\d+(?:[.,]\d+)*[km]?$/i.test(value)) continue;
2117
+ const q = v[1] != null ? '"' : v[2] != null ? "'" : v[3] != null ? "`" : "";
2118
+ out2 += text.slice(last, v.index) + q + REDACTED + q;
2119
+ last = VALUE.lastIndex;
2120
+ }
2121
+ return last ? out2 + text.slice(last) : text;
2122
+ }
2123
+ function isSecretName(name) {
2124
+ if (/^[A-Z0-9_]+_KEY$/.test(name)) return true;
2125
+ const flat = name.toLowerCase().replace(/[_-]/g, "");
2126
+ return /(?:pass(?:word|wd|phrase|code)?|pwd|secret|secretkey|secretaccesskey|clientsecret|apikey|accesskey|accesstoken|authtoken|refreshtoken|privatekey|token|credential|creds)s?$/.test(
2127
+ flat
2128
+ );
2129
+ }
2130
+ var PROSE = new RegExp(
2131
+ `(\\b${SECRET_WORD}(?:[ \\t]{1,3}(?:is|was|=|:|to|as|of|for[ \\t]{1,3}\\S{1,40}[ \\t]{1,3}is))?[ \\t]{1,3})(["'\`]?)([^\\s"'\`]{6,200})\\2`,
2132
+ "gi"
2133
+ );
2134
+ function isPlaceholder(v) {
2135
+ const s = v.replace(/[.,;:!?]+$/, "");
2136
+ if (!s) return true;
2137
+ if (/^(?:string|number|boolean|bool|str|int|any|unknown|null|undefined|none|nil|true|false|optional|required|text|varchar|password|secret|token|env|string\[\]|object|bytes)$/i.test(
2138
+ s
2139
+ ))
2140
+ return true;
2141
+ if (/^\[?redacted\]?$/i.test(s) || /^\*+$/.test(s) || /^x{3,}$/i.test(s) || /^\.{3}$|^…$/.test(s)) return true;
2142
+ if (/^[<{$%]/.test(s)) return true;
2143
+ if (/^(?:process\.env|env|os\.environ|c\.env|this\.env|import\.meta\.env|getenv|config|settings|secrets|args|opts|options|req|request|body|params|input|data|user|self|this)\b/i.test(
2144
+ s
2145
+ ))
2146
+ return true;
2147
+ if (/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)+$/.test(s)) return true;
2148
+ if (/[(]/.test(s)) return true;
2149
+ if (/^(?:your|my|the|a|an|some|new|old)[-_ ]/i.test(s)) return true;
2150
+ return false;
2151
+ }
2152
+ function looksSecret(v) {
2153
+ const s = v.replace(/[.,;:!?)]+$/, "");
2154
+ if (s.length < 6) return false;
2155
+ if (isPlaceholder(s)) return false;
2156
+ if (/^https?:\/\//i.test(s)) return false;
2157
+ if (/^[a-z]+(?:[-'][a-z]+)*$/.test(s)) return false;
2158
+ if (/^[A-Z][a-z]+$/.test(s)) return false;
2159
+ if (/^\d+(?:[.,]\d+)*[a-z%]*$/i.test(s)) return false;
2160
+ if (/^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+$/.test(s)) return false;
2161
+ const kinds = [/[a-z]/, /[A-Z]/, /[0-9]/, /[^A-Za-z0-9]/].filter((r) => r.test(s)).length;
2162
+ return kinds >= 3 || /[0-9]/.test(s) && /[A-Za-z]/.test(s);
2163
+ }
2164
+ function redactSecrets(text) {
2165
+ if (!text) return text;
2166
+ let out2 = text;
2167
+ for (const re of TOKEN_PATTERNS) out2 = out2.replace(re, REDACTED);
2168
+ out2 = out2.replace(URL_CREDENTIALS, (_m, head, _pw, at) => `${head}${REDACTED}${at}`);
2169
+ out2 = out2.replace(AUTH_HEADER, (m, head, v) => v.includes(REDACTED) ? m : `${head}${REDACTED}`);
2170
+ out2 = redactAssignments(out2);
2171
+ out2 = out2.replace(
2172
+ PROSE,
2173
+ (m, head, q, v) => !looksSecret(v) || v.includes(REDACTED) ? m : `${head}${q}${REDACTED}${q}`
2174
+ );
2175
+ return out2;
2176
+ }
2177
+ function redactDeep(v) {
2178
+ if (typeof v === "string") return redactSecrets(v);
2179
+ if (Array.isArray(v)) return v.map(redactDeep);
2180
+ if (v && typeof v === "object") {
2181
+ const out2 = {};
2182
+ for (const [k, x] of Object.entries(v)) out2[k] = redactDeep(x);
2183
+ return out2;
2184
+ }
2185
+ return v;
2186
+ }
2187
+
2188
+ // ../core/src/privacy/filter.ts
2189
+ var TYPE_TIER = {
2190
+ "session.started": 0,
2191
+ "session.heartbeat": 0,
2192
+ "session.ended": 0,
2193
+ "session.backfill": 0,
2194
+ "session.forgotten": 0,
2195
+ "turn.started": 0,
2196
+ "turn.ended": 0,
2197
+ "turn.transcript": 2,
2198
+ "tool.started": 1,
2199
+ "tool.ended": 1,
2200
+ "files.changed": 1,
2201
+ "attention.needed": 0,
2202
+ "attention.cleared": 0,
2203
+ "subagent.started": 1,
2204
+ "subagent.ended": 1,
2205
+ "context.compacted": 1,
2206
+ native: 3
2207
+ };
2208
+ var DATA_ALLOW = {
2209
+ "session.started": { 0: ["source"] },
2210
+ "session.heartbeat": { 0: [] },
2211
+ "session.ended": { 0: ["reason"] },
2212
+ "session.backfill": { 0: ["started_at", "last_at", "turns"] },
2213
+ "session.forgotten": { 0: [] },
2214
+ "turn.started": { 0: ["turn_id"], 1: ["prompt_chars"] },
2215
+ "turn.ended": { 0: ["turn_id", "reason", "ms"] },
2216
+ "turn.transcript": { 2: ["turn", "user", "assistant", "at"] },
2217
+ "tool.started": { 1: ["tool", "call_id", "turn_id", "agent_id"], 3: ["input"] },
2218
+ "tool.ended": { 1: ["tool", "call_id", "turn_id", "agent_id", "ms", "ok", "error"], 3: ["input", "output"] },
2219
+ "files.changed": { 1: ["paths"] },
2220
+ "attention.needed": { 0: ["attention_id", "kind"], 1: ["tool", "message"] },
2221
+ "attention.cleared": { 0: ["attention_id", "how"] },
2222
+ "subagent.started": { 1: ["agent_id", "agent_type"] },
2223
+ "subagent.ended": { 1: ["agent_id", "agent_type"] },
2224
+ "context.compacted": { 1: ["trigger"] }
2225
+ };
2226
+ var SESSION_ALLOW = [
2227
+ "id",
2228
+ "seq",
2229
+ "machine",
2230
+ "cwd",
2231
+ "repo",
2232
+ "branch",
2233
+ "model",
2234
+ "title",
2235
+ "title_source",
2236
+ "url",
2237
+ "account_id",
2238
+ "parent_id"
2239
+ ];
2240
+ var MAX_SIDE = 2e4;
2241
+ var MAX_IO = 64 * 1024;
2242
+ function allowedKeys(type, tier) {
2243
+ const table = DATA_ALLOW[type];
2244
+ if (!table) return null;
2245
+ const out2 = [];
2246
+ for (let t = 0; t <= tier; t++) for (const k of table[t] ?? []) out2.push(k);
2247
+ return out2;
2248
+ }
2249
+ function cap(v, max) {
2250
+ if (typeof v === "string") return v.length > max ? `${v.slice(0, max)}\u2026` : v;
2251
+ if (v && typeof v === "object") {
2252
+ const s = JSON.stringify(v);
2253
+ return s.length > max ? `${s.slice(0, max)}\u2026` : v;
2254
+ }
2255
+ return v;
2256
+ }
2257
+ function filterEvent(event, tier, pii, opts = {}) {
2258
+ const minTier = TYPE_TIER[event.type];
2259
+ if (minTier !== void 0 && minTier > tier) return null;
2260
+ if (event.type === "native" && tier < 3) return null;
2261
+ const session = {};
2262
+ for (const k of SESSION_ALLOW)
2263
+ if (event.session[k] !== void 0)
2264
+ session[k] = event.session[k];
2265
+ let data = {};
2266
+ const keys = allowedKeys(event.type, tier);
2267
+ if (keys === null) data = { ...event.data };
2268
+ else for (const k of keys) if (event.data[k] !== void 0) data[k] = event.data[k];
2269
+ if (typeof data.user === "string") data.user = cap(data.user, MAX_SIDE);
2270
+ if (typeof data.assistant === "string") data.assistant = cap(data.assistant, MAX_SIDE);
2271
+ if (data.input !== void 0) data.input = cap(data.input, MAX_IO);
2272
+ if (data.output !== void 0) data.output = cap(data.output, MAX_IO);
2273
+ if (typeof data.message === "string") data.message = cap(data.message, 200);
2274
+ const rulesets = [];
2275
+ let out2 = { ...event, tier, session, data, privacy: { rulesets, pii } };
2276
+ if (tier > 0) {
2277
+ out2 = { ...out2, session: redactDeep(out2.session), data: redactDeep(out2.data) };
2278
+ rulesets.push(RULESET_SECRETS);
2279
+ }
2280
+ if (pii) {
2281
+ const s = reducePiiDeep(out2.session, opts.home);
2282
+ if (typeof s.machine === "string") s.machine = hashMachine(s.machine);
2283
+ delete s.account_id;
2284
+ out2 = { ...out2, session: s, data: reducePiiDeep(out2.data, opts.home) };
2285
+ rulesets.push(RULESET_PII);
2286
+ }
2287
+ out2.privacy = { rulesets, pii };
2288
+ return out2;
2289
+ }
2290
+
2291
+ // ../core/src/sinks/file.ts
2292
+ import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync6 } from "node:fs";
2293
+ import path10 from "node:path";
2294
+ var FileSink = class {
2295
+ constructor(name, file, tier, pii) {
2296
+ this.name = name;
2297
+ this.file = file;
2298
+ this.tier = tier;
2299
+ this.pii = pii;
2300
+ }
2301
+ name;
2302
+ file;
2303
+ tier;
2304
+ pii;
2305
+ async send(events) {
2306
+ try {
2307
+ mkdirSync6(path10.dirname(this.file), { recursive: true });
2308
+ appendFileSync2(this.file, `${events.map((e) => JSON.stringify(e)).join("\n")}
2309
+ `);
2310
+ return { ok: true, acknowledged: events.length, accepted: events.length, duplicates: 0, rejected: [] };
2311
+ } catch (e) {
2312
+ return { ok: false, acknowledged: 0, error: String(e.message), action: "retry" };
2313
+ }
2314
+ }
2315
+ };
2316
+
2317
+ // ../core/src/sinks/stdout.ts
2318
+ var StdoutSink = class {
2319
+ constructor(name, tier, pii, write = (l) => process.stdout.write(`${l}
2320
+ `)) {
2321
+ this.name = name;
2322
+ this.tier = tier;
2323
+ this.pii = pii;
2324
+ this.write = write;
2325
+ }
2326
+ name;
2327
+ tier;
2328
+ pii;
2329
+ write;
2330
+ async send(events) {
2331
+ for (const e of events) this.write(JSON.stringify(e));
2332
+ return { ok: true, acknowledged: events.length, accepted: events.length, duplicates: 0, rejected: [] };
2333
+ }
2334
+ };
2335
+
2336
+ // src/run.ts
2337
+ import { appendFileSync as appendFileSync3, existsSync as existsSync8, mkdirSync as mkdirSync7, readFileSync as readFileSync8, renameSync as renameSync2, statSync as statSync5, writeFileSync as writeFileSync6 } from "node:fs";
2338
+ import os2 from "node:os";
2339
+ import path11 from "node:path";
2340
+
2341
+ // src/http-sink.ts
2342
+ import { createHmac, randomUUID } from "node:crypto";
2343
+ var TIMEOUT_MS = 15e3;
2344
+ var HttpSink = class {
2345
+ constructor(cfg, tier) {
2346
+ this.cfg = cfg;
2347
+ this.tier = tier;
2348
+ this.name = cfg.name;
2349
+ this.pii = !!cfg.pii;
2350
+ this.url = cfg.url.replace(/\/$/, "");
2351
+ }
2352
+ cfg;
2353
+ tier;
2354
+ name;
2355
+ pii;
2356
+ url;
2357
+ eventsUrl() {
2358
+ const ep = this.cfg.endpoints?.events ?? "/api/sessionpipe/v1/events";
2359
+ return /^https?:\/\//.test(ep) ? ep : this.url + ep;
2360
+ }
2361
+ async send(events) {
2362
+ const body = JSON.stringify({ events });
2363
+ const headers = { "content-type": "application/json", "sessionpipe-protocol": "1" };
2364
+ if (this.cfg.token) headers.authorization = `Bearer ${this.cfg.token}`;
2365
+ if (this.cfg.secret?.startsWith("whsec_")) {
2366
+ const id = `msg_${randomUUID()}`;
2367
+ const ts = Math.floor(Date.now() / 1e3);
2368
+ const key = Buffer.from(this.cfg.secret.slice(6), "base64");
2369
+ const sig = createHmac("sha256", key).update(`${id}.${ts}.${body}`).digest("base64");
2370
+ headers["webhook-id"] = id;
2371
+ headers["webhook-timestamp"] = String(ts);
2372
+ headers["webhook-signature"] = `v1,${sig}`;
2373
+ }
2374
+ const ctl = new AbortController();
2375
+ const t = setTimeout(() => ctl.abort(), TIMEOUT_MS);
2376
+ try {
2377
+ const r = await fetch(this.eventsUrl(), { method: "POST", headers, body, signal: ctl.signal });
2378
+ const text = await r.text();
2379
+ let j = {};
2380
+ try {
2381
+ j = text ? JSON.parse(text) : {};
2382
+ } catch {
2383
+ }
2384
+ if (r.status === 202 || r.status === 200) {
2385
+ return {
2386
+ ok: true,
2387
+ acknowledged: events.length,
2388
+ accepted: Number(j.accepted ?? events.length),
2389
+ duplicates: Number(j.duplicates ?? 0),
2390
+ rejected: Array.isArray(j.rejected) ? j.rejected : [],
2391
+ ...typeof j.max_tier === "number" ? { max_tier: j.max_tier } : {},
2392
+ status: r.status
2393
+ };
2394
+ }
2395
+ const err = typeof j.reason === "string" ? j.reason : text.slice(0, 200);
2396
+ if (r.status === 400) return { ok: false, acknowledged: 0, status: 400, error: err, action: "drop" };
2397
+ if (r.status === 401) return { ok: false, acknowledged: 0, status: 401, error: err, action: "pause" };
2398
+ if (r.status === 403)
2399
+ return {
2400
+ ok: false,
2401
+ acknowledged: 0,
2402
+ status: 403,
2403
+ error: err,
2404
+ action: "lower-tier",
2405
+ ...typeof j.max_tier === "number" ? { max_tier: j.max_tier } : {}
2406
+ };
2407
+ if (r.status === 413) return { ok: false, acknowledged: 0, status: 413, error: err, action: "split" };
2408
+ if (r.status === 429)
2409
+ return {
2410
+ ok: false,
2411
+ acknowledged: 0,
2412
+ status: 429,
2413
+ error: err,
2414
+ action: "retry",
2415
+ retryAfterMs: Number(r.headers.get("retry-after") ?? 60) * 1e3
2416
+ };
2417
+ return { ok: false, acknowledged: 0, status: r.status, error: err, action: "retry" };
2418
+ } catch (e) {
2419
+ return { ok: false, acknowledged: 0, error: String(e.message ?? e), action: "retry" };
2420
+ } finally {
2421
+ clearTimeout(t);
2422
+ }
2423
+ }
2424
+ };
2425
+
2426
+ // src/run.ts
2427
+ var VERSION = true ? "0.1.0" : "0.0.0";
2428
+ function log(state2, line) {
2429
+ try {
2430
+ mkdirSync7(state2, { recursive: true });
2431
+ const f = path11.join(state2, "log");
2432
+ try {
2433
+ if (statSync5(f).size > 1024 * 1024) renameSync2(f, `${f}.1`);
2434
+ } catch {
2435
+ }
2436
+ appendFileSync3(f, `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
2437
+ `);
2438
+ } catch {
2439
+ }
2440
+ }
2441
+ function factsState(state2) {
2442
+ const file = (h, s) => path11.join(state2, "facts", safeId(h), `${safeId(s)}.json`);
2443
+ return {
2444
+ get(h, s) {
2445
+ try {
2446
+ return JSON.parse(readFileSync8(file(h, s), "utf8"));
2447
+ } catch {
2448
+ return {};
2449
+ }
2450
+ },
2451
+ save(h, s, patch) {
2452
+ const cur = this.get(h, s);
2453
+ const next = { ...cur, ...patch };
2454
+ if (JSON.stringify(next) === JSON.stringify(cur)) return;
2455
+ mkdirSync7(path11.dirname(file(h, s)), { recursive: true });
2456
+ writeFileSync6(file(h, s), JSON.stringify(next));
2457
+ }
2458
+ };
2459
+ }
2460
+ function buildSinks(sinks) {
2461
+ const out2 = [];
2462
+ for (const s of sinks) {
2463
+ if (s.paused) continue;
2464
+ const tier = Math.min(s.tier, s.max_tier ?? 3);
2465
+ if (s.url === "stdout") out2.push(new StdoutSink(s.name, tier, !!s.pii));
2466
+ else if (s.url.startsWith("file:"))
2467
+ out2.push(new FileSink(s.name, s.url.slice(5).replace(/^~(?=\/|$)/, os2.homedir()), tier, !!s.pii));
2468
+ else if (/^https?:\/\//.test(s.url)) out2.push(new HttpSink(s, tier));
2469
+ }
2470
+ return out2;
2471
+ }
2472
+ async function runJob(job, opts = {}) {
2473
+ const state2 = opts.state ?? stateDir();
2474
+ const cfg = readConfig();
2475
+ const adapter = adapterByName(job.harness);
2476
+ if (!adapter) return { events: [], delivered: {} };
2477
+ const r = adapter.fromHook({ argv: job.argv, stdin: job.stdin, env: { ...process.env, ...job.env }, cwd: job.cwd });
2478
+ if (!r || !r.session.id) return { events: [], delivered: {} };
2479
+ const outbox = new Outbox(state2);
2480
+ const ref = { harness: job.harness, session: r.session.id };
2481
+ const release = takeLock(state2, ref);
2482
+ if (!release) {
2483
+ log(state2, `${job.harness} ${job.event}: session ${r.session.id} locked, re-queued`);
2484
+ const jobs = path11.join(state2, "jobs");
2485
+ mkdirSync7(jobs, { recursive: true });
2486
+ writeFileSync6(path11.join(jobs, `${Date.now().toString(36)}-requeue.json`), JSON.stringify(job));
2487
+ return { events: [], delivered: {} };
2488
+ }
2489
+ try {
2490
+ const facts = factsState(state2);
2491
+ const mem = facts.get(job.harness, r.session.id);
2492
+ const heavy = !/^(PreToolUse|PostToolUse|PostToolUseFailure|BeforeTool|AfterTool|PostInvocation)$/.test(job.event);
2493
+ let learned = {};
2494
+ if (heavy) {
2495
+ const f = adapter.facts(r.session, r.transcript, r.hints ?? {}, facts);
2496
+ const { started_at: _s, last_at: _l, first_ask: _fa, harness_version, ...rest } = f;
2497
+ learned = rest;
2498
+ if (harness_version) facts.save(job.harness, r.session.id, { harness_version });
2499
+ facts.save(job.harness, r.session.id, { session: { ...mem.session, ...learned } });
2500
+ }
2501
+ const known = mem.session ?? {};
2502
+ const cwdRaw = learned.cwd ?? r.session.cwd ?? known.cwd;
2503
+ const git = heavy ? gitFacts(cwdRaw) : {};
2504
+ const session = {
2505
+ ...known,
2506
+ ...learned,
2507
+ ...git,
2508
+ ...learned.branch ? { branch: learned.branch } : {},
2509
+ id: r.session.id,
2510
+ seq: 0,
2511
+ machine: machineName(cfg, os2.hostname()),
2512
+ ...cwdRaw ? { cwd: tilde(cwdRaw) } : {},
2513
+ ...r.session.model ? { model: r.session.model } : {}
2514
+ };
2515
+ const version = facts.get(job.harness, r.session.id).harness_version ?? void 0;
2516
+ const events = [];
2517
+ for (const a of r.events) {
2518
+ const e = makeEvent(a, {
2519
+ harness: { name: job.harness, ...version ? { version } : {} },
2520
+ session: { ...session, seq: outbox.nextSeq(ref) },
2521
+ now: opts.now ?? job.at
2522
+ });
2523
+ events.push(e);
2524
+ }
2525
+ const wantsTranscript = cfg.sinks.some((s) => !s.paused && Math.min(s.tier, s.max_tier ?? 3) >= 2);
2526
+ if (wantsTranscript && r.transcript && r.events.some((e) => e.type === "turn.ended" || e.type === "session.ended")) {
2527
+ const cur = facts.get(job.harness, r.session.id);
2528
+ const fromLine = Number(cur.transcript_line) || 0;
2529
+ let turnNo = Number(cur.transcript_turns) || 0;
2530
+ const read = adapter.readTranscript(r.transcript, fromLine);
2531
+ for (const t of read.turns) {
2532
+ events.push(
2533
+ makeEvent(
2534
+ {
2535
+ type: "turn.transcript",
2536
+ harnessEvent: job.event,
2537
+ data: { turn: turnNo++, user: t.user, assistant: t.assistant, at: new Date(t.at).toISOString() }
2538
+ },
2539
+ {
2540
+ harness: { name: job.harness, ...version ? { version } : {} },
2541
+ session: { ...session, seq: outbox.nextSeq(ref) },
2542
+ now: opts.now ?? job.at
2543
+ }
2544
+ )
2545
+ );
2546
+ }
2547
+ if (read.turns.length)
2548
+ facts.save(job.harness, r.session.id, { transcript_line: read.line, transcript_turns: turnNo });
2549
+ }
2550
+ outbox.append(ref, events);
2551
+ log(
2552
+ state2,
2553
+ `${job.harness} ${job.event}: ${events.map((e) => e.type).join(",") || "nothing"} (${r.session.id.slice(0, 8)})`
2554
+ );
2555
+ const delivered = await flush(state2, cfg.sinks, outbox, ref);
2556
+ if (Math.random() < 0.02) outbox.prune(cfg.keep_days ?? 30);
2557
+ return { events, delivered };
2558
+ } finally {
2559
+ release();
2560
+ }
2561
+ }
2562
+ var BATCH = 50;
2563
+ var MAX_PER_RUN = 500;
2564
+ async function flush(state2, sinkConfigs, outbox, first) {
2565
+ const delivered = {};
2566
+ const sinks = buildSinks(sinkConfigs);
2567
+ const refs = outbox.sessions();
2568
+ if (first) {
2569
+ const k = `${first.harness}/${safeId(first.session)}`;
2570
+ refs.sort((a, b) => `${a.harness}/${a.session}` === k ? -1 : `${b.harness}/${b.session}` === k ? 1 : 0);
2571
+ }
2572
+ for (const sink2 of sinks) {
2573
+ const cursors = new Cursors(state2, sink2.name);
2574
+ const c = cursors.read();
2575
+ let sent = 0;
2576
+ for (const ref of refs) {
2577
+ if (sent >= MAX_PER_RUN) break;
2578
+ let offset = c[cursors.key(ref)] ?? 0;
2579
+ let stalled = false;
2580
+ while (sent < MAX_PER_RUN) {
2581
+ const { events, offset: next } = outbox.read(ref, offset, BATCH);
2582
+ if (!events.length) break;
2583
+ const filtered = events.map((e) => filterEvent(e, sink2.tier, sink2.pii, { home: os2.homedir() })).filter((e) => !!e);
2584
+ let ok = true;
2585
+ if (filtered.length) {
2586
+ const res = await sink2.send(filtered);
2587
+ ok = res.ok;
2588
+ if (!ok) {
2589
+ log(state2, `sink ${sink2.name}: ${res.status ?? ""} ${res.error ?? ""} \u2192 ${res.action ?? "retry"}`);
2590
+ if (res.action === "pause" || res.action === "lower-tier") applySinkVerdict(sink2.name, res);
2591
+ if (res.action === "drop") ok = true;
2592
+ }
2593
+ if (ok) sent += filtered.length;
2594
+ }
2595
+ if (!ok) {
2596
+ stalled = true;
2597
+ break;
2598
+ }
2599
+ offset = next;
2600
+ c[cursors.key(ref)] = offset;
2601
+ cursors.write(c);
2602
+ }
2603
+ if (stalled) break;
2604
+ }
2605
+ delivered[sink2.name] = sent;
2606
+ }
2607
+ return delivered;
2608
+ }
2609
+ function applySinkVerdict(name, res) {
2610
+ try {
2611
+ const cfg = readConfig();
2612
+ const s = cfg.sinks.find((x) => x.name === name);
2613
+ if (!s) return;
2614
+ if (res.action === "pause") s.paused = `${(/* @__PURE__ */ new Date()).toISOString()} ${res.error ?? "unauthorized"}`;
2615
+ if (res.action === "lower-tier" && typeof res.max_tier === "number") s.max_tier = res.max_tier;
2616
+ writeConfig(cfg);
2617
+ } catch {
2618
+ }
2619
+ }
2620
+ var jobsDir = (state2) => path11.join(state2, "jobs");
2621
+
2622
+ // src/cli.ts
2623
+ process.stdout.on("error", (e) => {
2624
+ if (e.code === "EPIPE") process.exit(0);
2625
+ });
2626
+ var args = process.argv.slice(2);
2627
+ var cmd = args[0] ?? "help";
2628
+ var flag = (name) => {
2629
+ const i = args.indexOf(name);
2630
+ return i >= 0 ? args[i + 1] : void 0;
2631
+ };
2632
+ var has = (name) => args.includes(name);
2633
+ var out = (s = "") => {
2634
+ process.stdout.write(`${s}
2635
+ `);
2636
+ };
2637
+ var state = stateDir();
2638
+ var distDir = path12.dirname(fileURLToPath(import.meta.url));
2639
+ function hookCommand() {
2640
+ const node = stableNode(process.execPath);
2641
+ let script = path12.join(distDir, "hook.js");
2642
+ try {
2643
+ script = realpathSync2(script);
2644
+ } catch {
2645
+ }
2646
+ return (a) => ({
2647
+ argv: [node, script, ...a],
2648
+ command: `${JSON.stringify(node)} ${JSON.stringify(script)} ${a.join(" ")}`
2649
+ });
2650
+ }
2651
+ function stableNode(exe) {
2652
+ const m = /^(.*)\/Cellar\/(node(?:@\d+)?)\/[^/]+\/bin\/node$/.exec(exe);
2653
+ if (!m) return exe;
2654
+ let real;
2655
+ try {
2656
+ real = realpathSync2(exe);
2657
+ } catch {
2658
+ return exe;
2659
+ }
2660
+ for (const c of [`${m[1]}/opt/${m[2]}/bin/node`, `${m[1]}/bin/node`]) {
2661
+ try {
2662
+ if (realpathSync2(c) === real) return c;
2663
+ } catch {
2664
+ }
2665
+ }
2666
+ return exe;
2667
+ }
2668
+ var viaNpx = /[\\/]_npx[\\/]/.test(distDir) || /[\\/]\.npm[\\/]/.test(distDir);
2669
+ function selectedAdapters() {
2670
+ const named = ADAPTERS.filter((a) => has(`--${a.name}`));
2671
+ if (named.length) return named;
2672
+ return ADAPTERS.filter((a) => a.detect());
2673
+ }
2674
+ async function main() {
2675
+ switch (cmd) {
2676
+ case "install":
2677
+ return install();
2678
+ case "uninstall":
2679
+ return uninstall();
2680
+ case "sink":
2681
+ return sink();
2682
+ case "status":
2683
+ return status();
2684
+ case "doctor":
2685
+ return doctor();
2686
+ case "tail":
2687
+ return tail();
2688
+ case "backfill":
2689
+ return backfill();
2690
+ case "forget":
2691
+ return forget();
2692
+ case "replay":
2693
+ return replay();
2694
+ case "update":
2695
+ return update();
2696
+ case "worker":
2697
+ return workerFromArgv();
2698
+ case "hook":
2699
+ out("sessionpipe hook is dist/hook.js; harnesses run it directly");
2700
+ return;
2701
+ case "--version":
2702
+ case "-v":
2703
+ case "version":
2704
+ out(VERSION);
2705
+ return;
2706
+ default:
2707
+ return help();
2708
+ }
2709
+ }
2710
+ function help() {
2711
+ out(`sessionpipe ${VERSION} \u2014 an open protocol for what your coding agents are doing
2712
+
2713
+ sessionpipe install [--claude-code --codex --gemini-cli --antigravity] [--machine NAME] [--backfill DAYS] [--sink URL --tier N]
2714
+ sessionpipe uninstall [--keep-state]
2715
+ sessionpipe sink add <url|file:PATH|stdout> [--tier 0-3] [--token T] [--pii] [--control] [--name N]
2716
+ sessionpipe sink list | remove <name> | test <name>
2717
+ sessionpipe status | doctor [--json]
2718
+ sessionpipe tail [--session ID] [--tier N] [--harness NAME]
2719
+ sessionpipe backfill [--days 30] | forget <harness> <session> | replay --sink NAME
2720
+ sessionpipe update
2721
+
2722
+ Docs: https://sessionpipe.org \xB7 nothing leaves this machine until you add a sink.`);
2723
+ }
2724
+ async function install() {
2725
+ if (viaNpx) {
2726
+ out(` Installing sessionpipe@${VERSION} globally, so the hooks start in milliseconds\u2026`);
2727
+ try {
2728
+ execFileSync3("npm", ["install", "-g", `sessionpipe@${VERSION}`], { stdio: ["ignore", "ignore", "inherit"] });
2729
+ const bin = process.platform === "win32" ? "sessionpipe.cmd" : "sessionpipe";
2730
+ execFileSync3(bin, args, { stdio: "inherit", shell: process.platform === "win32" });
2731
+ return;
2732
+ } catch {
2733
+ out(
2734
+ " ! couldn't install globally (`npm install -g sessionpipe` failed). Run it yourself, then `sessionpipe install`."
2735
+ );
2736
+ process.exit(1);
2737
+ }
2738
+ }
2739
+ const cfg = readConfig();
2740
+ const machine = flag("--machine");
2741
+ if (machine) cfg.machine = machine.slice(0, 80);
2742
+ const adapters = selectedAdapters();
2743
+ if (!adapters.length) {
2744
+ out(
2745
+ " No supported harness found on this machine (Claude Code, Codex, Gemini CLI, Antigravity). Pass --<harness> to force one."
2746
+ );
2747
+ return;
2748
+ }
2749
+ out(` Machine: ${machineName(cfg, os3.hostname())}`);
2750
+ const hc = hookCommand();
2751
+ for (const a of adapters) {
2752
+ const reports = a.install(hc);
2753
+ cfg.harnesses[a.name] = { enabled: true };
2754
+ for (const r of reports)
2755
+ out(
2756
+ ` ${r.changed ? "\u2713" : "="} ${a.name}: ${tilde(r.file)}${r.note ? ` \u2014 ${r.note}` : r.changed ? " written" : " already current"}`
2757
+ );
2758
+ if (a.name === "gemini-cli")
2759
+ out(" note: Gemini CLI's own telemetry defaults logPrompts on; that is Google's setting, not sessionpipe's.");
2760
+ if (a.name === "codex")
2761
+ out(
2762
+ " note: Codex runs non-managed hooks only after you trust them: open Codex and run /hooks once. The notify fallback reports turn ends meanwhile."
2763
+ );
2764
+ }
2765
+ writeConfig(cfg);
2766
+ const sinkUrl = flag("--sink");
2767
+ if (sinkUrl) await sinkAdd(sinkUrl, cfg);
2768
+ const days = Number(flag("--backfill") ?? 30);
2769
+ if (days > 0) backfillRows(adapters, days, cfg);
2770
+ out("");
2771
+ const live = cfg.sinks.filter((s) => !s.paused);
2772
+ if (live.length) out(` Reporting to ${live.map((s) => `${s.name} (tier ${s.tier})`).join(", ")}.`);
2773
+ else
2774
+ out(
2775
+ " Nothing leaves this machine yet: events go to the local outbox. `sessionpipe tail` shows them; `sessionpipe sink add <url> --tier N` sends them."
2776
+ );
2777
+ out(" Sessions already running pick up hooks on their next start (Claude Code \u2265 2.1.280 picks them up live).");
2778
+ }
2779
+ function uninstall() {
2780
+ for (const a of ADAPTERS) {
2781
+ for (const r of a.uninstall()) if (r.changed) out(` \u2713 ${a.name}: hooks removed from ${tilde(r.file)}`);
2782
+ }
2783
+ if (!has("--keep-state"))
2784
+ out(
2785
+ ` Outbox and state kept under ${tilde(state)} (delete it yourself, or pass nothing: it prunes after ${readConfig().keep_days ?? 30} days).`
2786
+ );
2787
+ out(" Done. Every config file is byte-identical except for the entries sessionpipe wrote.");
2788
+ }
2789
+ async function sink() {
2790
+ const sub = args[1];
2791
+ const cfg = readConfig();
2792
+ if (sub === "add" && args[2]) return sinkAdd(args[2], cfg);
2793
+ if (sub === "list") {
2794
+ if (!cfg.sinks.length) out(" no sinks");
2795
+ for (const s of cfg.sinks)
2796
+ out(
2797
+ ` ${s.name.padEnd(16)} ${s.url.padEnd(44)} tier ${s.tier}${s.max_tier !== void 0 && s.max_tier < s.tier ? ` (capped ${s.max_tier})` : ""}${s.pii ? " pii" : ""}${s.control ? " control" : ""}${s.paused ? ` PAUSED ${s.paused}` : ""}`
2798
+ );
2799
+ return;
2800
+ }
2801
+ if (sub === "remove" && args[2]) {
2802
+ const n = cfg.sinks.length;
2803
+ cfg.sinks = cfg.sinks.filter((s) => s.name !== args[2]);
2804
+ writeConfig(cfg);
2805
+ out(cfg.sinks.length < n ? ` \u2713 removed ${args[2]}` : ` no sink named ${args[2]}`);
2806
+ return;
2807
+ }
2808
+ if (sub === "test" && args[2]) {
2809
+ const s = cfg.sinks.find((x) => x.name === args[2]);
2810
+ if (!s) return out(` no sink named ${args[2]}`);
2811
+ const [sk] = buildSinks([s]);
2812
+ if (!sk) return out(" sink type not supported yet");
2813
+ const e = makeEvent(
2814
+ { type: "session.heartbeat", harnessEvent: "test", data: {} },
2815
+ {
2816
+ harness: { name: "sessionpipe" },
2817
+ session: { id: `test-${Date.now()}`, seq: 0, machine: machineName(cfg, os3.hostname()) }
2818
+ }
2819
+ );
2820
+ const r = await sk.send([filterEvent(e, sk.tier, sk.pii)]);
2821
+ out(
2822
+ r.ok ? ` \u2713 ${s.name}: ${r.status ?? "ok"} accepted ${r.accepted ?? 1}` : ` \u2717 ${s.name}: ${r.status ?? ""} ${r.error ?? ""} (${r.action})`
2823
+ );
2824
+ return;
2825
+ }
2826
+ out(
2827
+ "usage: sessionpipe sink add <url|file:PATH|stdout> [--tier N] [--token T] [--pii] [--control] [--name N] | list | remove <name> | test <name>"
2828
+ );
2829
+ }
2830
+ async function sinkAdd(url, cfg) {
2831
+ const tier = Math.max(0, Math.min(3, Number(flag("--tier") ?? 0)));
2832
+ const name = flag("--name") ?? (url === "stdout" ? "stdout" : url.startsWith("file:") ? "file" : new URL(url).host);
2833
+ const s = { name, url, tier, pii: has("--pii"), control: has("--control") };
2834
+ const token = flag("--token");
2835
+ if (token) s.token = token;
2836
+ const secret = flag("--secret");
2837
+ if (secret) s.secret = secret;
2838
+ if (/^https?:\/\//.test(url)) {
2839
+ try {
2840
+ const r = await fetch(`${url.replace(/\/$/, "")}/.well-known/sessionpipe`, { signal: AbortSignal.timeout(8e3) });
2841
+ if (!r.ok) throw new Error(`HTTP ${r.status}`);
2842
+ const wk = await r.json();
2843
+ if (typeof wk.max_tier === "number") {
2844
+ s.max_tier = wk.max_tier;
2845
+ if (wk.max_tier < tier)
2846
+ out(` receiver's max_tier is ${wk.max_tier}: sending tier ${wk.max_tier}, not ${tier}`);
2847
+ }
2848
+ if (wk.endpoints) s.endpoints = wk.endpoints;
2849
+ s.well_known_at = (/* @__PURE__ */ new Date()).toISOString();
2850
+ if (s.control && !(wk.capabilities ?? []).includes("control")) {
2851
+ out(" receiver does not list `control`; --control ignored");
2852
+ s.control = false;
2853
+ }
2854
+ } catch (e) {
2855
+ out(
2856
+ ` ! ${url} has no readable /.well-known/sessionpipe (${e.message}); added anyway, at tier ${tier}`
2857
+ );
2858
+ }
2859
+ }
2860
+ cfg.sinks = cfg.sinks.filter((x) => x.name !== name).concat([s]);
2861
+ writeConfig(cfg);
2862
+ out(
2863
+ ` \u2713 sink ${name}: ${url} at tier ${Math.min(tier, s.max_tier ?? 3)}${s.pii ? ", pii" : ""}${s.control ? ", control" : ""}`
2864
+ );
2865
+ out(` tier ${tier} sends: ${TIER_TEXT[Math.min(tier, s.max_tier ?? 3)]}`);
2866
+ }
2867
+ var TIER_TEXT = {
2868
+ 0: "session state and timing, machine, folder, repo, branch, model, title, link, the KIND of attention needed \u2014 no tool names, no words",
2869
+ 1: "tier 0 + tool names, durations, ok/error, file paths, the attention message, subagents, compactions \u2014 no prompts",
2870
+ 2: "tier 1 + your prompts and the assistant's text, secrets removed",
2871
+ 3: "everything, including tool input and output"
2872
+ };
2873
+ function backfillRows(adapters, days, cfg) {
2874
+ const since = Date.now() - days * 864e5;
2875
+ const outbox = new Outbox(state);
2876
+ let n = 0;
2877
+ const machine = machineName(cfg, os3.hostname());
2878
+ for (const a of adapters) {
2879
+ for (const row of a.backfill(since)) {
2880
+ const ref = { harness: a.name, session: row.session.id };
2881
+ const facts = factsState(state).get(a.name, row.session.id);
2882
+ if (facts.backfilled) continue;
2883
+ const { id, ...rest } = row.session;
2884
+ const e = makeEvent(
2885
+ {
2886
+ type: "session.backfill",
2887
+ harnessEvent: "backfill",
2888
+ data: {
2889
+ started_at: new Date(row.started_at).toISOString(),
2890
+ last_at: new Date(row.last_at).toISOString(),
2891
+ ...row.turns !== void 0 ? { turns: row.turns } : {}
2892
+ }
2893
+ },
2894
+ {
2895
+ harness: { name: a.name },
2896
+ session: { ...rest, id, seq: outbox.nextSeq(ref), machine, ...rest.cwd ? { cwd: tilde(rest.cwd) } : {} },
2897
+ now: row.last_at
2898
+ }
2899
+ );
2900
+ outbox.append(ref, [e]);
2901
+ factsState(state).save(a.name, row.session.id, { backfilled: true });
2902
+ n++;
2903
+ }
2904
+ }
2905
+ out(` \u2713 Backfill: ${n} session${n === 1 ? "" : "s"} from the last ${days} days queued as session.backfill`);
2906
+ void flush(state, cfg.sinks, outbox);
2907
+ }
2908
+ function backfill() {
2909
+ backfillRows(selectedAdapters(), Number(flag("--days") ?? 30), readConfig());
2910
+ }
2911
+ async function forget() {
2912
+ const [, harness, session] = args;
2913
+ if (!harness || !session) return out("usage: sessionpipe forget <harness> <session>");
2914
+ const cfg = readConfig();
2915
+ const outbox = new Outbox(state);
2916
+ const ref = { harness, session };
2917
+ const e = makeEvent(
2918
+ { type: "session.forgotten", harnessEvent: "forget", data: {} },
2919
+ {
2920
+ harness: { name: harness },
2921
+ session: { id: session, seq: outbox.nextSeq(ref), machine: machineName(cfg, os3.hostname()) }
2922
+ }
2923
+ );
2924
+ const sinks = buildSinks(cfg.sinks);
2925
+ for (const s of sinks) {
2926
+ const r = await s.send([filterEvent(e, s.tier, s.pii)]);
2927
+ out(r.ok ? ` \u2713 ${s.name}: asked to delete ${harness}:${session}` : ` \u2717 ${s.name}: ${r.error ?? r.status}`);
2928
+ }
2929
+ outbox.delete(ref);
2930
+ out(" local outbox for the session removed");
2931
+ }
2932
+ async function replay() {
2933
+ const name = flag("--sink");
2934
+ const cfg = readConfig();
2935
+ const sinks = name ? cfg.sinks.filter((s) => s.name === name) : cfg.sinks;
2936
+ if (!sinks.length) return out(" no such sink");
2937
+ for (const s of sinks) {
2938
+ if (has("--from-start")) new Cursors(state, s.name).write({});
2939
+ }
2940
+ const d = await flush(state, sinks, new Outbox(state));
2941
+ for (const [k, v] of Object.entries(d)) out(` ${k}: ${v} event${v === 1 ? "" : "s"} delivered`);
2942
+ }
2943
+ function status() {
2944
+ const cfg = readConfig();
2945
+ const hc = hookCommand();
2946
+ out(` machine: ${machineName(cfg, os3.hostname())} sessionpipe ${VERSION} state ${tilde(state)}`);
2947
+ for (const a of ADAPTERS) {
2948
+ if (!a.detect() && !cfg.harnesses[a.name]) continue;
2949
+ for (const r of a.installed(hc)) out(` ${a.name.padEnd(12)} ${tilde(r.file).padEnd(44)} ${r.state}`);
2950
+ }
2951
+ if (!cfg.sinks.length) out(" sinks: none (events stay in the local outbox)");
2952
+ for (const s of cfg.sinks) {
2953
+ const c = new Cursors(state, s.name).read();
2954
+ const outbox = new Outbox(state);
2955
+ let pending = 0;
2956
+ for (const ref of outbox.sessions()) {
2957
+ const f = outbox.file(ref);
2958
+ try {
2959
+ pending += Math.max(0, statSync6(f).size - (c[`${ref.harness}/${ref.session}`] ?? 0)) > 0 ? 1 : 0;
2960
+ } catch {
2961
+ }
2962
+ }
2963
+ out(
2964
+ ` sink ${s.name.padEnd(16)} tier ${Math.min(s.tier, s.max_tier ?? 3)} ${s.paused ? `PAUSED ${s.paused}` : pending ? `${pending} session file(s) with undelivered events` : "up to date"}`
2965
+ );
2966
+ }
2967
+ const t = timing();
2968
+ if (t) out(` hook: p50 ${t.p50} ms \xB7 p95 ${t.p95} ms over ${t.n} runs`);
2969
+ const jobs = jobsDir(state);
2970
+ try {
2971
+ const n = readdirSync6(jobs).length;
2972
+ if (n) out(` jobs: ${n} waiting`);
2973
+ } catch {
2974
+ }
2975
+ }
2976
+ function timing() {
2977
+ try {
2978
+ const lines = readFileSync9(path12.join(state, "timing.jsonl"), "utf8").trim().split("\n").slice(-100);
2979
+ const ms = lines.map((l) => JSON.parse(l).ms).filter((x) => x >= 0).sort((a, b) => a - b);
2980
+ if (!ms.length) return null;
2981
+ const q = (p) => Math.round(ms[Math.min(ms.length - 1, Math.floor(p * ms.length))] ?? 0);
2982
+ return { n: ms.length, p50: q(0.5), p95: q(0.95) };
2983
+ } catch {
2984
+ return null;
2985
+ }
2986
+ }
2987
+ function doctor() {
2988
+ const cfg = readConfig();
2989
+ const hc = hookCommand();
2990
+ const report = {
2991
+ version: VERSION,
2992
+ node: process.version,
2993
+ node_path: stableNode(process.execPath),
2994
+ machine: machineName(cfg, os3.hostname()),
2995
+ state,
2996
+ config: configFile(),
2997
+ harnesses: {},
2998
+ sinks: [],
2999
+ timing: timing(),
3000
+ warnings: []
3001
+ };
3002
+ const warnings = report.warnings;
3003
+ if (/\/Cellar\/node(?:@\d+)?\/[^/]+\//.test(distDir))
3004
+ warnings.push(
3005
+ "sessionpipe is installed inside Homebrew's versioned node folder; `brew upgrade node` will delete it. Install with `npm install -g --prefix ~/.local sessionpipe`."
3006
+ );
3007
+ for (const a of ADAPTERS) {
3008
+ if (!a.detect()) continue;
3009
+ const st = a.installed(hc);
3010
+ report.harnesses[a.name] = st.map((r) => ({ file: tilde(r.file), state: r.state }));
3011
+ for (const r of st)
3012
+ if (r.state === "stale")
3013
+ warnings.push(
3014
+ `${a.name}: the hook in ${tilde(r.file)} points at an old node or script path; run \`sessionpipe install\` again.`
3015
+ );
3016
+ for (const r of st)
3017
+ if (r.state === "misplaced")
3018
+ warnings.push(
3019
+ `${a.name}: the notify line in ${tilde(r.file)} sits inside a [table], where Codex never reads it; run \`sessionpipe install\` again.`
3020
+ );
3021
+ }
3022
+ for (const s of cfg.sinks)
3023
+ report.sinks.push({
3024
+ name: s.name,
3025
+ url: s.url,
3026
+ tier: s.tier,
3027
+ max_tier: s.max_tier ?? null,
3028
+ pii: !!s.pii,
3029
+ control: !!s.control,
3030
+ paused: s.paused ?? null,
3031
+ token: s.token ? `${s.token.slice(0, 4)}\u2026` : null
3032
+ });
3033
+ const t = timing();
3034
+ if (t && t.p50 > 150)
3035
+ warnings.push(`hook p50 is ${t.p50} ms (target < 150). Is node on a slow disk or a network share?`);
3036
+ if (has("--json")) {
3037
+ out(JSON.stringify(report, null, 2));
3038
+ return;
3039
+ }
3040
+ out(` sessionpipe ${VERSION} \xB7 node ${process.version} at ${report.node_path}`);
3041
+ out(` config ${tilde(configFile())} \xB7 state ${tilde(state)}`);
3042
+ for (const [h, st] of Object.entries(report.harnesses))
3043
+ for (const r of st) out(` ${h.padEnd(12)} ${r.file.padEnd(44)} ${r.state}`);
3044
+ for (const s of report.sinks)
3045
+ out(
3046
+ ` sink ${s.name.padEnd(16)} tier ${s.tier}${s.max_tier !== null ? ` (receiver max ${s.max_tier})` : ""}${s.paused ? ` PAUSED ${s.paused}` : ""}`
3047
+ );
3048
+ out(t ? ` hook p50 ${t.p50} ms \xB7 p95 ${t.p95} ms over ${t.n} runs` : " hook timing: no runs yet");
3049
+ for (const w of warnings) out(` ! ${w}`);
3050
+ if (!warnings.length) out(" no warnings");
3051
+ }
3052
+ async function tail() {
3053
+ const tier = Math.max(0, Math.min(3, Number(flag("--tier") ?? 1)));
3054
+ const onlySession = flag("--session");
3055
+ const onlyHarness = flag("--harness");
3056
+ const outbox = new Outbox(state);
3057
+ const offsets = /* @__PURE__ */ new Map();
3058
+ const fmt = (e) => {
3059
+ const t = e.time.slice(11, 19);
3060
+ const d = e.data;
3061
+ let detail = "";
3062
+ switch (e.type) {
3063
+ case "session.started":
3064
+ detail = `source=${d.source} ${e.session.cwd ?? ""} ${e.session.branch ?? ""}`;
3065
+ break;
3066
+ case "turn.started":
3067
+ detail = d.prompt_chars !== void 0 ? `prompt_chars=${d.prompt_chars}` : "";
3068
+ break;
3069
+ case "tool.started":
3070
+ detail = String(d.tool ?? "");
3071
+ break;
3072
+ case "tool.ended":
3073
+ detail = `${d.tool} ${d.ok ? "ok" : "error"}${d.ms !== void 0 ? ` ${d.ms}ms` : ""}`;
3074
+ break;
3075
+ case "attention.needed":
3076
+ detail = `${d.kind}${d.tool ? ` ${d.tool}` : ""}${d.message ? ` "${String(d.message).slice(0, 60)}"` : ""}`;
3077
+ break;
3078
+ case "turn.ended":
3079
+ detail = String(d.reason ?? "");
3080
+ break;
3081
+ case "session.ended":
3082
+ detail = `reason=${d.reason}`;
3083
+ break;
3084
+ case "turn.transcript":
3085
+ detail = `#${d.turn} "${String(d.user).slice(0, 50).replace(/\s+/g, " ")}"`;
3086
+ break;
3087
+ case "session.backfill":
3088
+ detail = `${e.session.title ?? ""}`;
3089
+ break;
3090
+ default:
3091
+ detail = JSON.stringify(d).slice(0, 80);
3092
+ }
3093
+ return `${t} ${e.harness.name.padEnd(11)} ${e.session.id.slice(0, 4)}\u2026 ${e.type.padEnd(17)} ${detail}`;
3094
+ };
3095
+ out(`sessionpipe tail \xB7 tier ${tier} \xB7 ${tilde(state)}/outbox (Ctrl-C to stop)`);
3096
+ for (const ref of outbox.sessions()) {
3097
+ try {
3098
+ offsets.set(`${ref.harness}/${ref.session}`, has("--all") ? 0 : statSync6(outbox.file(ref)).size);
3099
+ } catch {
3100
+ }
3101
+ }
3102
+ for (; ; ) {
3103
+ for (const ref of outbox.sessions()) {
3104
+ if (onlyHarness && ref.harness !== onlyHarness) continue;
3105
+ if (onlySession && !ref.session.startsWith(onlySession)) continue;
3106
+ const k = `${ref.harness}/${ref.session}`;
3107
+ const from = offsets.get(k) ?? 0;
3108
+ const { events, offset } = outbox.read(ref, from, 200);
3109
+ offsets.set(k, offset);
3110
+ for (const e of events) {
3111
+ const f = filterEvent(e, tier, false);
3112
+ if (f) out(has("--json") ? JSON.stringify(f) : fmt(f));
3113
+ }
3114
+ }
3115
+ await new Promise((r) => setTimeout(r, 500));
3116
+ }
3117
+ }
3118
+ async function workerFromArgv() {
3119
+ const f = args[1];
3120
+ if (!f) return;
3121
+ const job = JSON.parse(readFileSync9(f, "utf8"));
3122
+ try {
3123
+ unlinkSync2(f);
3124
+ } catch {
3125
+ }
3126
+ await runJob(job);
3127
+ }
3128
+ async function update() {
3129
+ if (process.env.SESSIONPIPE_NO_UPDATE_CHECK) return out(" update check is off (SESSIONPIPE_NO_UPDATE_CHECK)");
3130
+ try {
3131
+ const r = await fetch("https://registry.npmjs.org/sessionpipe/latest", { signal: AbortSignal.timeout(8e3) });
3132
+ const j = await r.json();
3133
+ if (!j.version) throw new Error("no version");
3134
+ if (j.version === VERSION) return out(` sessionpipe ${VERSION} is current`);
3135
+ out(` ${j.version} is available (you run ${VERSION}); installing into this copy's prefix\u2026`);
3136
+ const prefix = prefixOf(distDir);
3137
+ execFileSync3("npm", ["install", "-g", `sessionpipe@${j.version}`, ...prefix ? ["--prefix", prefix] : []], {
3138
+ stdio: "inherit"
3139
+ });
3140
+ out(" re-arming hooks\u2026");
3141
+ const bin = prefix ? path12.join(prefix, "bin", "sessionpipe") : "sessionpipe";
3142
+ const p = spawn(bin, ["install", "--backfill", "0"], { stdio: "inherit" });
3143
+ await new Promise((r2) => p.on("exit", r2));
3144
+ } catch (e) {
3145
+ out(` couldn't check npm: ${e.message}`);
3146
+ }
3147
+ }
3148
+ function prefixOf(p) {
3149
+ const re = process.platform === "win32" ? /^(.*?)[\\/]node_modules[\\/]sessionpipe[\\/]/ : /^(.*)\/lib\/node_modules\/sessionpipe\//;
3150
+ const m = re.exec(p);
3151
+ return m?.[1] ?? null;
3152
+ }
3153
+ main().catch((e) => {
3154
+ process.stderr.write(`sessionpipe: ${e.message}
3155
+ `);
3156
+ process.exit(1);
3157
+ });
3158
+ //# sourceMappingURL=cli.js.map