sessionpipe-core 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.
Files changed (62) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +41 -0
  3. package/README.md +5 -0
  4. package/dist/index.js +2380 -0
  5. package/dist/index.js.map +7 -0
  6. package/dist/schema/v1.d.ts +579 -0
  7. package/dist/schema/v1.d.ts.map +1 -0
  8. package/dist/schema.js +248 -0
  9. package/dist/schema.js.map +7 -0
  10. package/dist/src/adapters/antigravity.d.ts +26 -0
  11. package/dist/src/adapters/antigravity.d.ts.map +1 -0
  12. package/dist/src/adapters/claude-code.d.ts +18 -0
  13. package/dist/src/adapters/claude-code.d.ts.map +1 -0
  14. package/dist/src/adapters/claude-shaped.d.ts +21 -0
  15. package/dist/src/adapters/claude-shaped.d.ts.map +1 -0
  16. package/dist/src/adapters/codex-config.d.ts +31 -0
  17. package/dist/src/adapters/codex-config.d.ts.map +1 -0
  18. package/dist/src/adapters/codex.d.ts +6 -0
  19. package/dist/src/adapters/codex.d.ts.map +1 -0
  20. package/dist/src/adapters/gemini-cli.d.ts +4 -0
  21. package/dist/src/adapters/gemini-cli.d.ts.map +1 -0
  22. package/dist/src/adapters/index.d.ts +10 -0
  23. package/dist/src/adapters/index.d.ts.map +1 -0
  24. package/dist/src/adapters/types.d.ts +90 -0
  25. package/dist/src/adapters/types.d.ts.map +1 -0
  26. package/dist/src/config.d.ts +33 -0
  27. package/dist/src/config.d.ts.map +1 -0
  28. package/dist/src/events.d.ts +12 -0
  29. package/dist/src/events.d.ts.map +1 -0
  30. package/dist/src/index.d.ts +14 -0
  31. package/dist/src/index.d.ts.map +1 -0
  32. package/dist/src/outbox.d.ts +41 -0
  33. package/dist/src/outbox.d.ts.map +1 -0
  34. package/dist/src/paths.d.ts +8 -0
  35. package/dist/src/paths.d.ts.map +1 -0
  36. package/dist/src/privacy/filter.d.ts +10 -0
  37. package/dist/src/privacy/filter.d.ts.map +1 -0
  38. package/dist/src/privacy/index.d.ts +4 -0
  39. package/dist/src/privacy/index.d.ts.map +1 -0
  40. package/dist/src/privacy/pii.d.ts +8 -0
  41. package/dist/src/privacy/pii.d.ts.map +1 -0
  42. package/dist/src/privacy/secrets.d.ts +13 -0
  43. package/dist/src/privacy/secrets.d.ts.map +1 -0
  44. package/dist/src/readers/files.d.ts +10 -0
  45. package/dist/src/readers/files.d.ts.map +1 -0
  46. package/dist/src/readers/git.d.ts +7 -0
  47. package/dist/src/readers/git.d.ts.map +1 -0
  48. package/dist/src/readers/transcripts.d.ts +20 -0
  49. package/dist/src/readers/transcripts.d.ts.map +1 -0
  50. package/dist/src/sinks/file.d.ts +11 -0
  51. package/dist/src/sinks/file.d.ts.map +1 -0
  52. package/dist/src/sinks/index.d.ts +4 -0
  53. package/dist/src/sinks/index.d.ts.map +1 -0
  54. package/dist/src/sinks/stdout.d.ts +11 -0
  55. package/dist/src/sinks/stdout.d.ts.map +1 -0
  56. package/dist/src/sinks/types.d.ts +27 -0
  57. package/dist/src/sinks/types.d.ts.map +1 -0
  58. package/dist/src/types.d.ts +19 -0
  59. package/dist/src/types.d.ts.map +1 -0
  60. package/dist/src/ulid.d.ts +3 -0
  61. package/dist/src/ulid.d.ts.map +1 -0
  62. package/package.json +56 -0
package/dist/index.js ADDED
@@ -0,0 +1,2380 @@
1
+ // sessionpipe-core 0.1.0 · Apache-2.0 · https://sessionpipe.org
2
+
3
+ // src/adapters/antigravity.ts
4
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, realpathSync, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
5
+ import path4 from "node:path";
6
+
7
+ // src/paths.ts
8
+ import os from "node:os";
9
+ import path from "node:path";
10
+ var HOME = os.homedir();
11
+ function configFile(env = process.env) {
12
+ if (env.SESSIONPIPE_CONFIG) return path.resolve(env.SESSIONPIPE_CONFIG);
13
+ if (process.platform === "win32")
14
+ return path.join(env.APPDATA || path.join(HOME, "AppData", "Roaming"), "sessionpipe", "config.json");
15
+ return path.join(env.XDG_CONFIG_HOME || path.join(HOME, ".config"), "sessionpipe", "config.json");
16
+ }
17
+ function stateDir(env = process.env) {
18
+ if (env.SESSIONPIPE_STATE) return path.resolve(env.SESSIONPIPE_STATE);
19
+ if (process.platform === "win32")
20
+ return path.join(env.LOCALAPPDATA || path.join(HOME, "AppData", "Local"), "sessionpipe");
21
+ return path.join(env.XDG_STATE_HOME || path.join(HOME, ".local", "state"), "sessionpipe");
22
+ }
23
+ function tilde(p, home = HOME) {
24
+ if (!p) return p;
25
+ if (p === home) return "~";
26
+ return p.startsWith(home + path.sep) || p.startsWith(`${home}/`) ? `~${p.slice(home.length)}` : p;
27
+ }
28
+ var safeId = (s) => String(s).replace(/[^A-Za-z0-9_.-]/g, "_").slice(0, 160);
29
+
30
+ // src/readers/files.ts
31
+ import { closeSync, fstatSync, openSync, readdirSync, readSync, statSync } from "node:fs";
32
+ import path2 from "node:path";
33
+ function ends(file, headBytes, tailBytes) {
34
+ let fd;
35
+ try {
36
+ fd = openSync(file, "r");
37
+ const size = fstatSync(fd).size;
38
+ const read = (pos, len) => {
39
+ const b = Buffer.alloc(len);
40
+ const n = readSync(fd, b, 0, len, pos);
41
+ return b.subarray(0, n).toString("utf8");
42
+ };
43
+ const head = read(0, Math.min(size, headBytes));
44
+ const tail = size > headBytes ? read(Math.max(0, size - tailBytes), Math.min(size, tailBytes)) : head;
45
+ return { head: head.split("\n").slice(0, -1), tail: tail.split("\n").slice(size > headBytes ? 1 : 0) };
46
+ } catch {
47
+ return { head: [], tail: [] };
48
+ } finally {
49
+ if (fd !== void 0)
50
+ try {
51
+ closeSync(fd);
52
+ } catch {
53
+ }
54
+ }
55
+ }
56
+ var parseLines = (lines) => lines.map((l) => {
57
+ try {
58
+ return JSON.parse(l);
59
+ } catch {
60
+ return null;
61
+ }
62
+ }).filter((x) => !!x);
63
+ function scanLines(file, visit, cap2 = 16 * 1024 * 1024) {
64
+ let fd;
65
+ try {
66
+ fd = openSync(file, "r");
67
+ const buf = Buffer.alloc(1024 * 1024);
68
+ let pos = 0;
69
+ let rest = Buffer.alloc(0);
70
+ while (pos < cap2) {
71
+ const n = readSync(fd, buf, 0, buf.length, pos);
72
+ if (!n) break;
73
+ pos += n;
74
+ let chunk = Buffer.concat([rest, buf.subarray(0, n)]);
75
+ let nl = chunk.indexOf(10);
76
+ while (nl >= 0) {
77
+ const r = visit(chunk.subarray(0, nl).toString("utf8"));
78
+ if (r !== null) return r;
79
+ chunk = chunk.subarray(nl + 1);
80
+ nl = chunk.indexOf(10);
81
+ }
82
+ rest = chunk;
83
+ }
84
+ } catch {
85
+ } finally {
86
+ if (fd !== void 0)
87
+ try {
88
+ closeSync(fd);
89
+ } catch {
90
+ }
91
+ }
92
+ return null;
93
+ }
94
+ function recentFiles(root, depth, sinceMs, match) {
95
+ const out = [];
96
+ const walk = (dir, d) => {
97
+ let names = [];
98
+ try {
99
+ names = readdirSync(dir, { withFileTypes: true });
100
+ } catch {
101
+ return;
102
+ }
103
+ for (const e of names) {
104
+ const p = path2.join(dir, e.name);
105
+ if (e.isDirectory()) {
106
+ if (d < depth) walk(p, d + 1);
107
+ continue;
108
+ }
109
+ if (!match(e.name)) continue;
110
+ try {
111
+ if (statSync(p).mtimeMs >= sinceMs) out.push(p);
112
+ } catch {
113
+ }
114
+ }
115
+ };
116
+ walk(root, 0);
117
+ return out;
118
+ }
119
+
120
+ // src/readers/transcripts.ts
121
+ function pairTurns(msgs, endLine) {
122
+ const turns = [];
123
+ let cur = null;
124
+ for (const m of msgs) {
125
+ if (m.role === "user") {
126
+ if (cur && !cur.assistant) {
127
+ cur.user += `
128
+
129
+ ${m.text}`;
130
+ continue;
131
+ }
132
+ if (cur) turns.push(cur);
133
+ cur = { user: m.text, assistant: "", at: m.at, startLine: m.line, endLine: m.line };
134
+ } else {
135
+ if (!cur) cur = { user: "", assistant: "", at: m.at, startLine: m.line, endLine: m.line };
136
+ cur.assistant += (cur.assistant ? "\n\n" : "") + m.text;
137
+ cur.endLine = m.line;
138
+ }
139
+ }
140
+ if (cur?.assistant) turns.push(cur);
141
+ const line = cur && !cur.assistant ? cur.startLine : endLine;
142
+ return { turns: turns.map((t) => ({ user: t.user, assistant: t.assistant, at: t.at, endLine: t.endLine })), line };
143
+ }
144
+ var isInjected = (text) => /^<[a-z_-]+>/i.test(text) && /<\/[a-z_-]+>\s*$/i.test(text) || /^<!--\s/.test(text) || /^<command-/.test(text);
145
+ var endOf = (lines) => lines[lines.length - 1] === "" ? lines.length - 1 : lines.length;
146
+ var parse = (raw) => {
147
+ try {
148
+ return JSON.parse(raw);
149
+ } catch {
150
+ return null;
151
+ }
152
+ };
153
+ var textParts = (c, kinds) => {
154
+ if (typeof c === "string") return c;
155
+ if (!Array.isArray(c)) return "";
156
+ return c.filter(
157
+ (b) => b && typeof b === "object" && kinds.includes(b.type ?? "") && typeof b.text === "string"
158
+ ).map((b) => b.text).join("\n");
159
+ };
160
+ function readClaude(lines, from) {
161
+ const msgs = [];
162
+ for (let i = from; i < lines.length; i++) {
163
+ const j = lines[i] ? parse(lines[i]) : null;
164
+ if (!j || j.isSidechain) continue;
165
+ const origin = j.origin;
166
+ if (j.type === "user" && (j.isMeta || origin?.kind && origin.kind !== "human")) continue;
167
+ const at = Date.parse(String(j.timestamp ?? "")) || Date.now();
168
+ const c = j.message?.content;
169
+ if (j.type === "user") {
170
+ const text = textParts(c, ["text"]).trim();
171
+ if (!text || isInjected(text)) continue;
172
+ msgs.push({ role: "user", text, at, line: i });
173
+ } else if (j.type === "assistant" && Array.isArray(c)) {
174
+ const text = textParts(c, ["text"]).trim();
175
+ if (text) msgs.push({ role: "assistant", text, at, line: i });
176
+ }
177
+ }
178
+ return pairTurns(msgs, endOf(lines));
179
+ }
180
+ function readCodex(lines, from) {
181
+ const msgs = [];
182
+ for (let i = from; i < lines.length; i++) {
183
+ const j = lines[i] ? parse(lines[i]) : null;
184
+ if (!j || j.type !== "response_item") continue;
185
+ const p = j.payload;
186
+ if (!p || p.type !== "message") continue;
187
+ const at = Date.parse(String(j.timestamp ?? "")) || Date.now();
188
+ const text = textParts(p.content, ["input_text", "output_text"]).trim();
189
+ if (!text) continue;
190
+ if (p.role === "user") {
191
+ if (/^<[a-z_-]+>/i.test(text)) continue;
192
+ msgs.push({ role: "user", text, at, line: i });
193
+ } else if (p.role === "assistant") msgs.push({ role: "assistant", text, at, line: i });
194
+ }
195
+ return pairTurns(msgs, endOf(lines));
196
+ }
197
+ function readGemini(lines, from) {
198
+ const msgs = [];
199
+ for (let i = from; i < lines.length; i++) {
200
+ const j = lines[i] ? parse(lines[i]) : null;
201
+ if (!j) continue;
202
+ const role = j.type === "user" ? "user" : j.type === "gemini" || j.type === "model" ? "assistant" : null;
203
+ if (!role) continue;
204
+ const raw = j.content ?? j.message ?? j.text;
205
+ const text = (typeof raw === "string" ? raw : textParts(raw, ["text"])).trim();
206
+ if (!text) continue;
207
+ const at = Date.parse(String(j.timestamp ?? "")) || Date.now();
208
+ msgs.push({ role, text, at, line: i });
209
+ }
210
+ return pairTurns(msgs, endOf(lines));
211
+ }
212
+ function readAntigravity(lines, from) {
213
+ const msgs = [];
214
+ for (let i = from; i < lines.length; i++) {
215
+ const j = lines[i] ? parse(lines[i]) : null;
216
+ if (!j || typeof j.content !== "string") continue;
217
+ const at = Date.parse(String(j.created_at ?? j.timestamp ?? "")) || Date.now();
218
+ if (j.type === "USER_INPUT") {
219
+ const m = /<USER_REQUEST>([\s\S]*?)(?:<\/USER_REQUEST>|$)/.exec(j.content);
220
+ const text = (m ? m[1] ?? "" : j.content).trim();
221
+ if (text) msgs.push({ role: "user", text, at, line: i });
222
+ } else if (j.type === "PLANNER_RESPONSE") {
223
+ const text = j.content.trim();
224
+ if (text) msgs.push({ role: "assistant", text, at, line: i });
225
+ }
226
+ }
227
+ return pairTurns(msgs, endOf(lines));
228
+ }
229
+
230
+ // src/adapters/claude-shaped.ts
231
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
232
+ import path3 from "node:path";
233
+ var OURS = /sessionpipe[^"\n]*[\\/]hook\.js"?\s+[a-z][a-z-]*\s+[A-Za-z]+\s*$/;
234
+ var isOurs = (h) => Array.isArray(h?.hooks) && h.hooks.some((x) => typeof x.command === "string" && OURS.test(x.command));
235
+ function readJson(file) {
236
+ try {
237
+ return JSON.parse(readFileSync(file, "utf8"));
238
+ } catch {
239
+ return {};
240
+ }
241
+ }
242
+ function entryFor(event, harness, cmd, o) {
243
+ const matcher = o.matcherFor(event);
244
+ return {
245
+ ...matcher !== void 0 ? { matcher } : {},
246
+ hooks: [{ type: "command", command: cmd([harness, event]).command, timeout: o.timeout }]
247
+ };
248
+ }
249
+ function installClaudeShaped(file, harness, events, cmd, o) {
250
+ const settings2 = readJson(file);
251
+ const hooks = settings2.hooks && typeof settings2.hooks === "object" ? settings2.hooks : {};
252
+ let changed = false;
253
+ for (const ev of events) {
254
+ const list = Array.isArray(hooks[ev]) ? hooks[ev] : [];
255
+ const entry2 = entryFor(ev, harness, cmd, o);
256
+ const mine = list.filter((h) => isOurs(h));
257
+ if (mine.length === 1 && JSON.stringify(mine[0]) === JSON.stringify(entry2)) continue;
258
+ hooks[ev] = list.filter((h) => !isOurs(h)).concat([entry2]);
259
+ changed = true;
260
+ }
261
+ if (o.top) {
262
+ for (const [k, v] of Object.entries(o.top))
263
+ if (settings2[k] !== v) {
264
+ settings2[k] = v;
265
+ changed = true;
266
+ }
267
+ }
268
+ if (changed) {
269
+ settings2.hooks = hooks;
270
+ mkdirSync(path3.dirname(file), { recursive: true });
271
+ writeFileSync(file, `${JSON.stringify(settings2, null, 2)}
272
+ `);
273
+ }
274
+ return { file, changed };
275
+ }
276
+ function uninstallClaudeShaped(file, events) {
277
+ if (!existsSync(file)) return { file, changed: false };
278
+ const raw = readFileSync(file, "utf8");
279
+ const settings2 = readJson(file);
280
+ const hooks = settings2.hooks;
281
+ if (!hooks || typeof hooks !== "object") return { file, changed: false };
282
+ let changed = false;
283
+ for (const ev of events) {
284
+ const list = Array.isArray(hooks[ev]) ? hooks[ev] : [];
285
+ const kept = list.filter((h) => !isOurs(h));
286
+ if (kept.length !== list.length) {
287
+ changed = true;
288
+ if (kept.length) hooks[ev] = kept;
289
+ else delete hooks[ev];
290
+ }
291
+ }
292
+ if (!changed) return { file, changed: false };
293
+ if (!Object.keys(hooks).length) delete settings2.hooks;
294
+ const next = `${JSON.stringify(settings2, null, 2)}
295
+ `;
296
+ if (next !== raw) writeFileSync(file, next);
297
+ return { file, changed: true };
298
+ }
299
+ function installedClaudeShaped(file, harness, events, cmd, o) {
300
+ const hooks = readJson(file).hooks ?? {};
301
+ let present = 0;
302
+ let current = 0;
303
+ for (const ev of events) {
304
+ const list = Array.isArray(hooks[ev]) ? hooks[ev] : [];
305
+ const mine = list.filter((h) => isOurs(h));
306
+ if (mine.length) present++;
307
+ if (mine.length === 1 && JSON.stringify(mine[0]) === JSON.stringify(entryFor(ev, harness, cmd, o))) current++;
308
+ }
309
+ if (!present) return "missing";
310
+ return current === events.length ? "current" : "stale";
311
+ }
312
+
313
+ // src/adapters/antigravity.ts
314
+ var NAME = "antigravity";
315
+ var HOOK_NAME = "sessionpipe";
316
+ var AG_EVENTS = ["PreInvocation", "PostInvocation", "PreToolUse", "PostToolUse", "Stop"];
317
+ var gemini = (env) => env.GEMINI_CLI_HOME || path4.join(HOME, ".gemini");
318
+ var hooksFile = (env) => path4.join(gemini(env), "config", "hooks.json");
319
+ var projectsDir = (env) => path4.join(gemini(env), "config", "projects");
320
+ var dataDirs = (env) => ["antigravity", "antigravity-cli", "antigravity-ide"].map((d) => path4.join(gemini(env), d));
321
+ var ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
322
+ var validTool = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
323
+ function entry(cmd) {
324
+ const handler = (e) => ({ type: "command", command: cmd([NAME, e]).command, timeout: 5 });
325
+ const out = { enabled: true };
326
+ for (const e of AG_EVENTS) out[e] = /Tool/.test(e) ? [{ matcher: "*", hooks: [handler(e)] }] : [handler(e)];
327
+ return out;
328
+ }
329
+ var antigravity = {
330
+ name: NAME,
331
+ events: AG_EVENTS,
332
+ detect: (env = process.env) => existsSync2(path4.join(gemini(env), "config")) || dataDirs(env).some((d) => existsSync2(d)),
333
+ configFiles: (env = process.env) => [hooksFile(env)],
334
+ install(cmd, env = process.env) {
335
+ const file = hooksFile(env);
336
+ const hooks = readJson(file);
337
+ const e = entry(cmd);
338
+ if (JSON.stringify(hooks[HOOK_NAME]) === JSON.stringify(e)) return [{ file, changed: false }];
339
+ hooks[HOOK_NAME] = e;
340
+ mkdirSync2(path4.dirname(file), { recursive: true });
341
+ writeFileSync2(file, `${JSON.stringify(hooks, null, 2)}
342
+ `);
343
+ return [
344
+ { file, changed: true, note: "hooks.json is read when a conversation starts; open ones report after a restart" }
345
+ ];
346
+ },
347
+ uninstall(env = process.env) {
348
+ const file = hooksFile(env);
349
+ if (!existsSync2(file)) return [{ file, changed: false }];
350
+ const hooks = readJson(file);
351
+ if (!(HOOK_NAME in hooks)) return [{ file, changed: false }];
352
+ delete hooks[HOOK_NAME];
353
+ writeFileSync2(file, `${JSON.stringify(hooks, null, 2)}
354
+ `);
355
+ return [{ file, changed: true }];
356
+ },
357
+ installed(cmd, env = process.env) {
358
+ const file = hooksFile(env);
359
+ const have = readJson(file)[HOOK_NAME];
360
+ return [
361
+ { file, state: !have ? "missing" : JSON.stringify(have) === JSON.stringify(entry(cmd)) ? "current" : "stale" }
362
+ ];
363
+ },
364
+ fromHook(input) {
365
+ const event = input.argv[1] ?? "";
366
+ let s = {};
367
+ try {
368
+ s = input.stdin ? JSON.parse(input.stdin) : {};
369
+ } catch {
370
+ }
371
+ const id = s.conversationId ?? s.session_id ?? s.sessionId;
372
+ if (!id) return { session: { id: "" }, events: [], stdout: "{}\n" };
373
+ const workspaces = agWorkspaces(s.workspacePaths);
374
+ const session = { id: String(id), cwd: workspaces[0] ?? input.cwd };
375
+ if (typeof s.modelName === "string" && s.modelName !== "auto") session.model = s.modelName;
376
+ const toolCall = s.toolCall;
377
+ const tool = validTool(toolCall?.name) ? toolCall?.name : validTool(s.toolName) ? s.toolName : void 0;
378
+ const ev = (type, data) => ({ type, data: prune(data), harnessEvent: event });
379
+ const events = [];
380
+ switch (event) {
381
+ case "PreInvocation":
382
+ if (Number(s.invocationNum) === 0 || s.invocationNum === void 0) {
383
+ events.push(ev("session.started", { source: "unknown" }));
384
+ events.push(ev("turn.started", {}));
385
+ } else events.push(ev("session.heartbeat", {}));
386
+ break;
387
+ case "PostInvocation":
388
+ break;
389
+ case "PreToolUse":
390
+ if (tool) events.push(ev("tool.started", { tool, input: toolCall?.args }));
391
+ break;
392
+ case "PostToolUse":
393
+ if (tool)
394
+ events.push(
395
+ ev("tool.ended", {
396
+ tool,
397
+ ok: !s.error,
398
+ error: typeof s.error === "string" ? s.error.slice(0, 2e3) : void 0,
399
+ input: toolCall?.args
400
+ })
401
+ );
402
+ break;
403
+ case "Stop":
404
+ events.push(ev("turn.ended", { reason: s.error ? "error" : "stop" }));
405
+ break;
406
+ default:
407
+ return { session, events: [], stdout: "{}\n" };
408
+ }
409
+ const r = { session, events, stdout: "{}\n", hints: { workspaces } };
410
+ if (typeof s.transcriptPath === "string") r.transcript = s.transcriptPath;
411
+ return r;
412
+ },
413
+ readTranscript(file, fromLine) {
414
+ let text = "";
415
+ try {
416
+ text = readFileSync2(file, "utf8");
417
+ } catch {
418
+ return { turns: [], line: fromLine };
419
+ }
420
+ return readAntigravity(text.split("\n"), fromLine);
421
+ },
422
+ facts(session, transcript, hints, state) {
423
+ if (!transcript) return {};
424
+ return antigravityFacts(transcript, {
425
+ conversation: session.id,
426
+ workspaces: hints.workspaces ?? (session.cwd ? [session.cwd] : []),
427
+ state,
428
+ env: process.env
429
+ });
430
+ },
431
+ backfill(sinceMs, env = process.env) {
432
+ const rows = [];
433
+ const noState = { get: () => ({}), save: () => {
434
+ } };
435
+ for (const data of dataDirs(env)) {
436
+ let ids = [];
437
+ try {
438
+ ids = readdirSync2(path4.join(data, "brain")).filter((n) => /^[0-9a-f-]{36}$/i.test(n));
439
+ } catch {
440
+ }
441
+ for (const id of ids) {
442
+ const f = path4.join(data, "brain", id, ".system_generated", "logs", "transcript.jsonl");
443
+ try {
444
+ if (statSync2(f).mtimeMs < sinceMs) continue;
445
+ } catch {
446
+ continue;
447
+ }
448
+ const facts = antigravityFacts(f, { conversation: id, workspaces: [], state: noState, env });
449
+ if (!facts.started_at || !facts.last_at) continue;
450
+ const { started_at, last_at, first_ask: _fa, harness_version: _hv, ...rest } = facts;
451
+ rows.push({ session: { id, ...rest }, started_at, last_at });
452
+ }
453
+ }
454
+ return rows;
455
+ }
456
+ };
457
+ function prune(o) {
458
+ for (const k of Object.keys(o)) if (o[k] === void 0) delete o[k];
459
+ return o;
460
+ }
461
+ function agPath(p) {
462
+ if (typeof p !== "string" || !p) return null;
463
+ let s = p;
464
+ if (/^file:/i.test(s)) {
465
+ try {
466
+ s = decodeURIComponent(new URL(s).pathname);
467
+ } catch {
468
+ return null;
469
+ }
470
+ if (process.platform === "win32" && /^\/[A-Za-z]:/.test(s)) s = s.slice(1);
471
+ } else if (/^[a-z][a-z0-9+.-]*:\/\//i.test(s)) return null;
472
+ if (!path4.isAbsolute(s)) return null;
473
+ return path4.resolve(s);
474
+ }
475
+ var agWorkspaces = (paths) => (Array.isArray(paths) ? paths : []).map(agPath).filter((p) => !!p).filter((p, i, a) => a.indexOf(p) === i);
476
+ function agVariants(p) {
477
+ const out = [p];
478
+ try {
479
+ const r = realpathSync.native(p);
480
+ if (r !== p) out.push(r);
481
+ } catch {
482
+ }
483
+ return out;
484
+ }
485
+ function agProjects(dir) {
486
+ const out = [];
487
+ let names = [];
488
+ try {
489
+ names = readdirSync2(dir).filter((n) => n.endsWith(".json"));
490
+ } catch {
491
+ return out;
492
+ }
493
+ for (const n of names) {
494
+ const id = n.slice(0, -5);
495
+ if (!ID_RE.test(id)) continue;
496
+ let j;
497
+ try {
498
+ j = JSON.parse(readFileSync2(path4.join(dir, n), "utf8"));
499
+ } catch {
500
+ continue;
501
+ }
502
+ const folders = [];
503
+ for (const r of j?.projectResources?.resources ?? []) {
504
+ const f = agPath(r?.folderUri);
505
+ if (f) folders.push(...agVariants(f));
506
+ }
507
+ if (folders.length) out.push({ id, folders });
508
+ }
509
+ return out;
510
+ }
511
+ function agMatchProject(projects, workspaces) {
512
+ const ws = [];
513
+ for (const w of workspaces) {
514
+ const p = agPath(w);
515
+ if (p) ws.push(...agVariants(p));
516
+ }
517
+ let best = null;
518
+ let bestLen = -1;
519
+ for (const pr of projects)
520
+ for (const f of pr.folders) {
521
+ const within = ws.some((w) => w === f || w.startsWith(f.endsWith(path4.sep) ? f : f + path4.sep));
522
+ if (within && f.length > bestLen) {
523
+ best = pr.id;
524
+ bestLen = f.length;
525
+ }
526
+ }
527
+ return best;
528
+ }
529
+ function antigravityRemoteUrl(transcript, opts) {
530
+ const i = transcript.indexOf(`${path4.sep}brain${path4.sep}`);
531
+ const dirs = (i > 0 ? [transcript.slice(0, i)] : []).concat(dataDirs(opts.env));
532
+ for (const d of dirs) {
533
+ try {
534
+ const m = /installation_uuid:\s*"([0-9a-f-]{36})"/i.exec(
535
+ readFileSync2(path4.join(d, "antigravity_state.pbtxt"), "utf8")
536
+ );
537
+ if (!m) continue;
538
+ const base = `https://antigravity.google.com/r/${m[1]}-v2`;
539
+ const conv = opts.conversation ?? "";
540
+ if (!ID_RE.test(conv) || !opts.workspaces?.length) return base;
541
+ const project = agMatchProject(agProjects(opts.projectsDir ?? projectsDir(opts.env)), opts.workspaces);
542
+ return project ? `${base}?p=${encodeURIComponent(`c/${conv}?section=${project}`)}` : base;
543
+ } catch {
544
+ }
545
+ }
546
+ return null;
547
+ }
548
+ function antigravityFacts(file, opts) {
549
+ const { head, tail } = ends(file, 128 * 1024, 64 * 1024);
550
+ const H = parseLines(head);
551
+ const T = parseLines(tail);
552
+ const out = {};
553
+ const url = antigravityRemoteUrl(file, opts);
554
+ if (url) out.url = url;
555
+ for (const j of H) {
556
+ if (j.type !== "USER_INPUT" || typeof j.content !== "string") continue;
557
+ const m = /<USER_REQUEST>([\s\S]*?)(?:<\/USER_REQUEST>|$)/.exec(j.content);
558
+ const text = (m ? m[1] ?? "" : j.content).trim();
559
+ const line = (text.split("\n").find((l) => l.trim()) ?? "").trim();
560
+ if (!line || /^<[a-z_-]+>/i.test(line)) continue;
561
+ out.title = line.length > 80 ? `${line.slice(0, 80).replace(/\s+\S*$/, "")}\u2026` : line;
562
+ out.title_source = "first-ask";
563
+ break;
564
+ }
565
+ const times = H.concat(T).map((j) => Date.parse(String(j.created_at ?? j.timestamp ?? ""))).filter((n) => n > 0);
566
+ if (times.length) {
567
+ out.started_at = Math.min(...times);
568
+ out.last_at = Math.max(...times);
569
+ }
570
+ return out;
571
+ }
572
+
573
+ // src/adapters/claude-code.ts
574
+ import { execFileSync as execFileSync2 } from "node:child_process";
575
+ import { createHash } from "node:crypto";
576
+ import { existsSync as existsSync3, readdirSync as readdirSync3, readFileSync as readFileSync3, statSync as statSync3 } from "node:fs";
577
+ import path5 from "node:path";
578
+
579
+ // src/readers/git.ts
580
+ import { execFileSync } from "node:child_process";
581
+ var cache = /* @__PURE__ */ new Map();
582
+ function gitFacts(cwd) {
583
+ if (!cwd) return {};
584
+ const hit = cache.get(cwd);
585
+ if (hit) return hit;
586
+ const run = (args) => {
587
+ try {
588
+ return execFileSync("git", ["-C", cwd, ...args], {
589
+ encoding: "utf8",
590
+ timeout: 1500,
591
+ stdio: ["ignore", "pipe", "ignore"]
592
+ }).trim();
593
+ } catch {
594
+ return "";
595
+ }
596
+ };
597
+ const out = {};
598
+ const repo = run(["remote", "get-url", "origin"]);
599
+ if (repo) out.repo = stripCredentials(repo);
600
+ const branch = run(["rev-parse", "--abbrev-ref", "HEAD"]);
601
+ if (branch && branch !== "HEAD") out.branch = branch;
602
+ cache.set(cwd, out);
603
+ return out;
604
+ }
605
+ function stripCredentials(url) {
606
+ let u = url.replace(/\/\/[^/@]*@/, "//");
607
+ const ssh = /^(?:ssh:\/\/)?git@([^:/]+)[:/](.+?)(?:\.git)?$/.exec(u);
608
+ if (ssh) u = `${ssh[1]}/${ssh[2]}`;
609
+ return u.replace(/^https?:\/\//, "").replace(/\.git$/, "");
610
+ }
611
+
612
+ // src/adapters/claude-code.ts
613
+ var NAME2 = "claude-code";
614
+ var CLAUDE_EVENTS = [
615
+ "SessionStart",
616
+ "UserPromptSubmit",
617
+ "PreToolUse",
618
+ "PostToolUse",
619
+ "PostToolUseFailure",
620
+ "PermissionRequest",
621
+ "Notification",
622
+ "Stop",
623
+ "SubagentStart",
624
+ "SubagentStop",
625
+ "PreCompact",
626
+ "PostCompact",
627
+ "SessionEnd"
628
+ ];
629
+ var OPTS = { matcherFor: (e) => /Tool|Permission/.test(e) ? "" : void 0, timeout: 5 };
630
+ var DESKTOP_ID = /^local_[A-Za-z0-9-]{1,64}$/;
631
+ function claudeDirs(env = process.env, home = HOME) {
632
+ const out = [];
633
+ const add = (d) => {
634
+ if (!d) return;
635
+ const r = path5.resolve(d);
636
+ if (!out.includes(r)) out.push(r);
637
+ };
638
+ add(path5.join(home, ".claude"));
639
+ add(env.CLAUDE_CONFIG_DIR);
640
+ try {
641
+ for (const e of readdirSync3(home, { withFileTypes: true })) {
642
+ if (!e.isDirectory() || !/^\.claude[-_.]/.test(e.name)) continue;
643
+ const d = path5.join(home, e.name);
644
+ if (existsSync3(path5.join(d, "projects")) || existsSync3(path5.join(d, "settings.json"))) add(d);
645
+ }
646
+ } catch {
647
+ }
648
+ return out.filter((d, i) => i === 0 || existsSync3(d));
649
+ }
650
+ var settingsIn = (dir) => path5.join(dir, "settings.json");
651
+ var configDirOf = (transcript) => path5.dirname(path5.dirname(path5.dirname(transcript)));
652
+ var validTool2 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
653
+ var endReason = (r) => r === "clear" || r === "logout" || r === "exit" ? r : r === "prompt_input_exit" ? "exit" : "other";
654
+ var notifKind = (t) => t === "permission_prompt" ? "permission" : t === "idle_prompt" ? "idle" : t === "elicitation_dialog" ? "elicitation" : "question";
655
+ var claudeCode = {
656
+ name: NAME2,
657
+ events: CLAUDE_EVENTS,
658
+ detect: (env = process.env) => claudeDirs(env).some((d) => existsSync3(d)),
659
+ configFiles: (env = process.env) => claudeDirs(env).map(settingsIn),
660
+ install: (cmd, env = process.env) => claudeDirs(env).map((d) => installClaudeShaped(settingsIn(d), NAME2, CLAUDE_EVENTS, cmd, OPTS)),
661
+ uninstall: (env = process.env) => claudeDirs(env).map((d) => uninstallClaudeShaped(settingsIn(d), CLAUDE_EVENTS)),
662
+ installed: (cmd, env = process.env) => claudeDirs(env).map((d) => ({
663
+ file: settingsIn(d),
664
+ state: installedClaudeShaped(settingsIn(d), NAME2, CLAUDE_EVENTS, cmd, OPTS)
665
+ })),
666
+ fromHook(input) {
667
+ const event = input.argv[1] ?? "";
668
+ let s = {};
669
+ try {
670
+ s = input.stdin ? JSON.parse(input.stdin) : {};
671
+ } catch {
672
+ }
673
+ const id = s.session_id ?? s.sessionId;
674
+ if (!id) return null;
675
+ const session = { id: String(id), cwd: typeof s.cwd === "string" ? s.cwd : input.cwd };
676
+ if (typeof s.model === "string") session.model = s.model;
677
+ if (typeof s.agent_id === "string" && event.startsWith("Subagent") === false && s.agent_id)
678
+ session.parent_id = null;
679
+ const turn_id = typeof s.prompt_id === "string" ? s.prompt_id : void 0;
680
+ const tool = validTool2(s.tool_name) ? s.tool_name : void 0;
681
+ const call_id = typeof s.tool_use_id === "string" ? s.tool_use_id : void 0;
682
+ const ms = typeof s.duration_ms === "number" ? Math.round(s.duration_ms) : void 0;
683
+ const agent_id = typeof s.agent_id === "string" ? s.agent_id : void 0;
684
+ const permissionId = () => `perm-${createHash("sha256").update(`${turn_id ?? ""}|${tool ?? ""}|${JSON.stringify(s.tool_input ?? null)}`).digest("hex").slice(0, 16)}`;
685
+ const ev = (type, data) => ({ type, data: prune2(data), harnessEvent: event });
686
+ const events = [];
687
+ switch (event) {
688
+ case "SessionStart":
689
+ events.push(ev("session.started", { source: pickSource(s.source ?? s.trigger) }));
690
+ break;
691
+ case "UserPromptSubmit":
692
+ events.push(
693
+ ev("turn.started", {
694
+ turn_id,
695
+ prompt_chars: typeof s.prompt === "string" ? [...s.prompt].length : void 0
696
+ })
697
+ );
698
+ break;
699
+ case "PreToolUse":
700
+ if (tool) events.push(ev("tool.started", { tool, call_id, turn_id, agent_id, input: s.tool_input }));
701
+ break;
702
+ case "PostToolUse":
703
+ if (tool)
704
+ events.push(
705
+ ev("tool.ended", {
706
+ tool,
707
+ call_id,
708
+ turn_id,
709
+ agent_id,
710
+ ms,
711
+ ok: true,
712
+ input: s.tool_input,
713
+ output: s.tool_response
714
+ })
715
+ );
716
+ break;
717
+ case "PostToolUseFailure":
718
+ if (tool)
719
+ events.push(
720
+ ev("tool.ended", {
721
+ tool,
722
+ call_id,
723
+ turn_id,
724
+ agent_id,
725
+ ms,
726
+ ok: false,
727
+ error: errText(s.error ?? s.tool_response),
728
+ input: s.tool_input,
729
+ output: s.tool_response
730
+ })
731
+ );
732
+ break;
733
+ case "PermissionRequest":
734
+ events.push(
735
+ ev("attention.needed", {
736
+ attention_id: call_id ?? permissionId(),
737
+ kind: "permission",
738
+ tool,
739
+ message: tool ? `${tool}${describe(s.tool_input)}` : void 0
740
+ })
741
+ );
742
+ break;
743
+ case "Notification":
744
+ events.push(
745
+ ev("attention.needed", {
746
+ attention_id: `notif-${Date.now()}`,
747
+ kind: notifKind(s.notification_type),
748
+ message: typeof s.message === "string" ? s.message.slice(0, 200) : void 0
749
+ })
750
+ );
751
+ break;
752
+ case "Stop":
753
+ events.push(ev("turn.ended", { turn_id, reason: "stop" }));
754
+ break;
755
+ case "SubagentStart":
756
+ events.push(
757
+ ev("subagent.started", {
758
+ agent_id: String(s.agent_id ?? ""),
759
+ agent_type: typeof s.agent_type === "string" ? s.agent_type : void 0
760
+ })
761
+ );
762
+ break;
763
+ case "SubagentStop":
764
+ events.push(
765
+ ev("subagent.ended", {
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 "PreCompact":
772
+ events.push(ev("context.compacted", { trigger: s.trigger === "manual" ? "manual" : "auto" }));
773
+ break;
774
+ case "PostCompact":
775
+ break;
776
+ // PreCompact already said it; a second event would double-count
777
+ case "SessionEnd":
778
+ events.push(ev("session.ended", { reason: endReason(s.reason) }));
779
+ break;
780
+ default:
781
+ return null;
782
+ }
783
+ const hints = {};
784
+ const host = input.env.CLAUDE_CODE_HOST_SESSION_ID;
785
+ if (host && DESKTOP_ID.test(host)) hints.desktop_id = host;
786
+ if (event !== "PreToolUse" && event !== "PostToolUse") hints.claude_pid = claudePid(input.env);
787
+ const r = { session, events, hints };
788
+ if (typeof s.transcript_path === "string") r.transcript = s.transcript_path;
789
+ return r;
790
+ },
791
+ readTranscript(file, fromLine) {
792
+ let text = "";
793
+ try {
794
+ text = readFileSync3(file, "utf8");
795
+ } catch {
796
+ return { turns: [], line: fromLine };
797
+ }
798
+ return readClaude(text.split("\n"), fromLine);
799
+ },
800
+ facts(session, transcript, hints, state) {
801
+ const out = {};
802
+ if (transcript) Object.assign(out, claudeFacts(transcript, session.id, hints, state));
803
+ const cwd = out.cwd ?? session.cwd;
804
+ Object.assign(out, gitFacts(cwd), out.branch ? { branch: out.branch } : {});
805
+ if (!out.url && typeof hints.desktop_id === "string") out.url = `claude://code/continue/${hints.desktop_id}`;
806
+ return out;
807
+ },
808
+ backfill(sinceMs, env = process.env) {
809
+ const rows = [];
810
+ const files = [];
811
+ for (const d of claudeDirs(env))
812
+ files.push(...recentFiles(path5.join(d, "projects"), 1, sinceMs, (n) => /^[0-9a-f-]{36}\.jsonl$/.test(n)));
813
+ const noState = { get: () => ({}), save: () => {
814
+ } };
815
+ for (const f of files) {
816
+ const id = path5.basename(f, ".jsonl");
817
+ const facts = claudeFacts(f, id, {}, noState);
818
+ if (!facts.started_at || !facts.last_at) continue;
819
+ const { started_at, last_at, first_ask: _fa, harness_version: _hv, ...rest } = facts;
820
+ const cwd = rest.cwd;
821
+ rows.push({
822
+ session: { id, ...rest, ...gitFacts(cwd), ...rest.branch ? { branch: rest.branch } : {} },
823
+ started_at,
824
+ last_at
825
+ });
826
+ }
827
+ return rows;
828
+ }
829
+ };
830
+ function prune2(o) {
831
+ for (const k of Object.keys(o)) if (o[k] === void 0) delete o[k];
832
+ return o;
833
+ }
834
+ function pickSource(v) {
835
+ return typeof v === "string" && ["startup", "resume", "clear", "compact", "fork"].includes(v) ? v : "unknown";
836
+ }
837
+ function errText(v) {
838
+ if (typeof v === "string") return v.slice(0, 2e3);
839
+ if (v && typeof v === "object") {
840
+ const e = v;
841
+ if (typeof e.error === "string") return e.error.slice(0, 2e3);
842
+ if (typeof e.message === "string") return e.message.slice(0, 2e3);
843
+ }
844
+ return void 0;
845
+ }
846
+ function describe(input) {
847
+ if (!input || typeof input !== "object") return "";
848
+ const i = input;
849
+ const v = i.command ?? i.file_path ?? i.path ?? i.url ?? i.pattern;
850
+ return typeof v === "string" ? ` ${v.slice(0, 120)}` : "";
851
+ }
852
+ var NAME_RANK = {
853
+ user: "custom",
854
+ auto: "harness",
855
+ hook: "harness",
856
+ peer: "harness"
857
+ };
858
+ function registryEntry(j) {
859
+ if (!j || typeof j !== "object") return null;
860
+ const r = j;
861
+ const source = r.nameSource ? NAME_RANK[r.nameSource] : void 0;
862
+ return {
863
+ sessionId: r.sessionId ? String(r.sessionId) : null,
864
+ name: source && typeof r.name === "string" && r.name.trim() ? r.name.trim() : null,
865
+ name_source: source ?? null,
866
+ bridge: typeof r.bridgeSessionId === "string" ? r.bridgeSessionId : null
867
+ };
868
+ }
869
+ var liveCache = /* @__PURE__ */ new Map();
870
+ function claudeLive(dir) {
871
+ const hit = liveCache.get(dir);
872
+ if (hit) return hit;
873
+ const map = /* @__PURE__ */ new Map();
874
+ let names = [];
875
+ try {
876
+ names = readdirSync3(path5.join(dir, "sessions")).filter((n) => n.endsWith(".json"));
877
+ } catch {
878
+ }
879
+ for (const n of names) {
880
+ try {
881
+ const e = registryEntry(JSON.parse(readFileSync3(path5.join(dir, "sessions", n), "utf8")));
882
+ if (e?.sessionId) map.set(e.sessionId, e);
883
+ } catch {
884
+ }
885
+ }
886
+ liveCache.set(dir, map);
887
+ return map;
888
+ }
889
+ function claudeLiveByPid(dir, pid) {
890
+ if (!pid) return null;
891
+ try {
892
+ return registryEntry(JSON.parse(readFileSync3(path5.join(dir, "sessions", `${pid}.json`), "utf8")));
893
+ } catch {
894
+ return null;
895
+ }
896
+ }
897
+ function claudePid(env = process.env) {
898
+ try {
899
+ const dirs = claudeDirs(env);
900
+ const has = (pid2) => dirs.some((d) => existsSync3(path5.join(d, "sessions", `${pid2}.json`)));
901
+ let pid = process.ppid;
902
+ if (has(pid)) return pid;
903
+ const parent = parentOf();
904
+ for (let hop = 0; hop < 6 && pid > 1; hop++) {
905
+ pid = parent(pid);
906
+ if (pid > 1 && has(pid)) return pid;
907
+ }
908
+ } catch {
909
+ }
910
+ return null;
911
+ }
912
+ function parentOf() {
913
+ if (process.platform === "linux") {
914
+ return (pid) => {
915
+ try {
916
+ const st = readFileSync3(`/proc/${pid}/stat`, "utf8");
917
+ return Number(st.slice(st.lastIndexOf(")") + 2).split(" ")[1]) || 0;
918
+ } catch {
919
+ return 0;
920
+ }
921
+ };
922
+ }
923
+ const table = /* @__PURE__ */ new Map();
924
+ try {
925
+ const out = execFileSync2("ps", ["-axo", "pid=,ppid="], {
926
+ encoding: "utf8",
927
+ timeout: 2e3,
928
+ maxBuffer: 8 * 1024 * 1024,
929
+ stdio: ["ignore", "pipe", "ignore"]
930
+ });
931
+ for (const l of out.split("\n")) {
932
+ const m = /^\s*(\d+)\s+(\d+)/.exec(l);
933
+ if (m) table.set(Number(m[1]), Number(m[2]));
934
+ }
935
+ } catch {
936
+ }
937
+ return (pid) => table.get(pid) || 0;
938
+ }
939
+ var accountCache = /* @__PURE__ */ new Map();
940
+ function claudeAccount(dir) {
941
+ const hit = accountCache.get(dir);
942
+ if (hit !== void 0) return hit;
943
+ const files = [path5.join(dir, ".claude.json")];
944
+ if (path5.resolve(dir) === path5.join(HOME, ".claude")) files.unshift(path5.join(HOME, ".claude.json"));
945
+ let acct = null;
946
+ for (const f of files) {
947
+ try {
948
+ const a = JSON.parse(readFileSync3(f, "utf8")).oauthAccount;
949
+ if (a?.accountUuid) {
950
+ acct = String(a.accountUuid);
951
+ break;
952
+ }
953
+ } catch {
954
+ }
955
+ }
956
+ accountCache.set(dir, acct);
957
+ return acct;
958
+ }
959
+ var LINK_RE = /^https:\/\/claude\.ai\/code\/session_[A-Za-z0-9]+$/;
960
+ function transcriptLink(records) {
961
+ for (let i = records.length - 1; i >= 0; i--) {
962
+ const j = records[i];
963
+ if (j.type === "system" && j.subtype === "bridge_status" && typeof j.url === "string" && LINK_RE.test(j.url))
964
+ return j.url;
965
+ if (j.type === "bridge-session" && typeof j.bridgeSessionId === "string") {
966
+ const m = /^(?:cse|session)_([A-Za-z0-9]+)$/.exec(j.bridgeSessionId);
967
+ if (m) return `https://claude.ai/code/session_${m[1]}`;
968
+ }
969
+ }
970
+ return null;
971
+ }
972
+ function appTitle(text) {
973
+ const t = String(text).replace(/\s+/g, " ").trim();
974
+ return t.length > 50 ? `${t.slice(0, 50)}...` : t;
975
+ }
976
+ function askText(j) {
977
+ if (!j || j.type !== "user" || j.isMeta || j.isSidechain) return null;
978
+ const origin = j.origin;
979
+ if (origin?.kind && origin.kind !== "human") return null;
980
+ const c = j.message?.content;
981
+ 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();
982
+ if (!text || isInjected(text)) return null;
983
+ return text;
984
+ }
985
+ function firstAsk(records) {
986
+ for (const j of records) {
987
+ const t = askText(j);
988
+ if (t) return appTitle(t);
989
+ }
990
+ return null;
991
+ }
992
+ function scanTitles(file, from, found) {
993
+ const out = { custom: found.custom ?? null, ai: found.ai ?? null, upto: from };
994
+ let size = 0;
995
+ try {
996
+ size = statSync3(file).size;
997
+ } catch {
998
+ return out;
999
+ }
1000
+ if (size < from) {
1001
+ out.upto = 0;
1002
+ out.custom = out.ai = null;
1003
+ from = 0;
1004
+ }
1005
+ let pos = 0;
1006
+ scanLines(
1007
+ file,
1008
+ (l) => {
1009
+ pos += Buffer.byteLength(l, "utf8") + 1;
1010
+ if (pos <= from || l.indexOf('-title"') < 0) return null;
1011
+ try {
1012
+ const j = JSON.parse(l);
1013
+ if (j.type === "custom-title" && j.customTitle) out.custom = j.customTitle;
1014
+ if (j.type === "ai-title" && j.aiTitle) out.ai = j.aiTitle;
1015
+ } catch {
1016
+ }
1017
+ return null;
1018
+ },
1019
+ 512 * 1024 * 1024
1020
+ );
1021
+ out.upto = size;
1022
+ return out;
1023
+ }
1024
+ function claudeFacts(file, sessionId, hints, state) {
1025
+ const { head, tail } = ends(file, 64 * 1024, 256 * 1024);
1026
+ const H = parseLines(head);
1027
+ const T = parseLines(tail);
1028
+ const out = {};
1029
+ const id = sessionId || path5.basename(file, ".jsonl");
1030
+ const mem = state.get(NAME2, id);
1031
+ let custom = null;
1032
+ let ai = null;
1033
+ for (const j of H.concat(T)) {
1034
+ if (j.type === "custom-title" && typeof j.customTitle === "string") custom = j.customTitle;
1035
+ if (j.type === "ai-title" && typeof j.aiTitle === "string") ai = j.aiTitle;
1036
+ }
1037
+ let size = 0;
1038
+ try {
1039
+ size = statSync3(file).size;
1040
+ } catch {
1041
+ }
1042
+ if (!custom && !ai && size > 64 * 1024 + 256 * 1024) {
1043
+ const s = scanTitles(file, Number(mem.scan_upto) || 0, {
1044
+ custom: mem.scan_custom,
1045
+ ai: mem.scan_ai
1046
+ });
1047
+ custom = s.custom;
1048
+ ai = s.ai;
1049
+ state.save(NAME2, id, { scan_upto: s.upto, scan_custom: s.custom, scan_ai: s.ai });
1050
+ }
1051
+ const dir = configDirOf(file);
1052
+ const live = claudeLive(dir).get(id) ?? claudeLiveByPid(dir, hints.claude_pid);
1053
+ if (live?.name) state.save(NAME2, id, { name: live.name, name_source: live.name_source });
1054
+ const bridge = live?.bridge && /^session_[A-Za-z0-9]+$/.test(live.bridge) ? `https://claude.ai/code/${live.bridge}` : null;
1055
+ const url = bridge ?? transcriptLink(T) ?? transcriptLink(H) ?? (typeof mem.url === "string" ? mem.url : null);
1056
+ if (url) {
1057
+ out.url = url;
1058
+ state.save(NAME2, id, { url });
1059
+ }
1060
+ const name = live?.name ? { title: live.name, source: live.name_source } : typeof mem.name === "string" ? { title: mem.name, source: mem.name_source } : null;
1061
+ const pick = name && name.source === "custom" && name || custom && { title: custom, source: "custom" } || name || ai && { title: ai, source: "harness" };
1062
+ let ask = typeof mem.first_ask === "string" ? mem.first_ask : firstAsk(H);
1063
+ if (!ask && size > 64 * 1024 && !mem.first_ask_none) {
1064
+ ask = scanLines(file, (l) => {
1065
+ try {
1066
+ const t = askText(JSON.parse(l));
1067
+ return t ? appTitle(t) : null;
1068
+ } catch {
1069
+ return null;
1070
+ }
1071
+ });
1072
+ if (!ask && size > 16 * 1024 * 1024) state.save(NAME2, id, { first_ask_none: true });
1073
+ }
1074
+ if (ask) {
1075
+ out.first_ask = ask;
1076
+ if (!mem.first_ask) state.save(NAME2, id, { first_ask: ask });
1077
+ }
1078
+ if (pick) {
1079
+ out.title = String(pick.title).slice(0, 120);
1080
+ out.title_source = pick.source;
1081
+ } else if (ask) {
1082
+ out.title = ask;
1083
+ out.title_source = "first-ask";
1084
+ }
1085
+ const acct = claudeAccount(dir);
1086
+ if (acct) out.account_id = acct;
1087
+ for (let i = T.length - 1; i >= 0; i--) {
1088
+ const m = T[i].message?.model;
1089
+ if (T[i]?.type === "assistant" && m && m !== "<synthetic>") {
1090
+ out.model = m;
1091
+ break;
1092
+ }
1093
+ }
1094
+ const meta = [...T].reverse().find((j) => j.version) ?? H.find((j) => j.version);
1095
+ if (meta?.version) out.harness_version = String(meta.version);
1096
+ const times = H.concat(T).map((j) => Date.parse(String(j.timestamp ?? ""))).filter((n) => n > 0);
1097
+ if (times.length) {
1098
+ out.started_at = Math.min(...times);
1099
+ out.last_at = Math.max(...times);
1100
+ }
1101
+ const withCwd = H.find((j) => typeof j.cwd === "string");
1102
+ if (withCwd) {
1103
+ out.cwd = withCwd.cwd;
1104
+ if (typeof withCwd.gitBranch === "string" && withCwd.gitBranch !== "HEAD") out.branch = withCwd.gitBranch;
1105
+ }
1106
+ return out;
1107
+ }
1108
+
1109
+ // src/adapters/codex.ts
1110
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
1111
+ import path6 from "node:path";
1112
+
1113
+ // src/adapters/codex-config.ts
1114
+ var NOTIFY_LINE = /^[ \t]*notify[ \t]*=[ \t]*(\[.*\])[ \t]*(?:#.*)?$/m;
1115
+ var NOTIFY_KEY = /^[ \t]*notify[ \t]*=/m;
1116
+ var OURS2 = /hook\.js",\s*"codex",\s*"notify"/;
1117
+ function scan(line, st) {
1118
+ let i = 0;
1119
+ while (i < line.length) {
1120
+ if (st.str) {
1121
+ const j = line.indexOf(st.str, i);
1122
+ if (j < 0) return;
1123
+ i = j + 3;
1124
+ st.str = null;
1125
+ continue;
1126
+ }
1127
+ const ch = line[i];
1128
+ if (ch === "#") return;
1129
+ if (line.startsWith('"""', i) || line.startsWith("'''", i)) {
1130
+ st.str = line.slice(i, i + 3);
1131
+ i += 3;
1132
+ continue;
1133
+ }
1134
+ if (ch === '"') {
1135
+ i++;
1136
+ while (i < line.length && line[i] !== '"') i += line[i] === "\\" ? 2 : 1;
1137
+ i++;
1138
+ continue;
1139
+ }
1140
+ if (ch === "'") {
1141
+ const j = line.indexOf("'", i + 1);
1142
+ i = j < 0 ? line.length : j + 1;
1143
+ continue;
1144
+ }
1145
+ if (ch === "[") st.depth++;
1146
+ else if (ch === "]") st.depth = Math.max(0, st.depth - 1);
1147
+ i++;
1148
+ }
1149
+ }
1150
+ function firstTableAt(text) {
1151
+ const st = { depth: 0, str: null };
1152
+ let pos = 0;
1153
+ for (const line of text.split("\n")) {
1154
+ if (!st.str && st.depth === 0 && /^[ \t]*\[/.test(line)) return pos;
1155
+ scan(line, st);
1156
+ pos += line.length + 1;
1157
+ }
1158
+ return -1;
1159
+ }
1160
+ function split(text) {
1161
+ const at = firstTableAt(text);
1162
+ return at < 0 ? { head: text, tail: "", at: text.length } : { head: text.slice(0, at), tail: text.slice(at), at };
1163
+ }
1164
+ function parseArgv(raw) {
1165
+ try {
1166
+ const a = JSON.parse(raw);
1167
+ return Array.isArray(a) && a.every((x) => typeof x === "string") ? a : null;
1168
+ } catch {
1169
+ return null;
1170
+ }
1171
+ }
1172
+ function readNotify(text) {
1173
+ const { head, tail, at } = split(text);
1174
+ const m = NOTIFY_LINE.exec(head);
1175
+ if (m)
1176
+ return {
1177
+ raw: m[1] ?? "",
1178
+ argv: parseArgv(m[1] ?? ""),
1179
+ index: m.index,
1180
+ length: m[0].length,
1181
+ misplaced: false,
1182
+ other: !OURS2.test(m[1] ?? "")
1183
+ };
1184
+ const k = NOTIFY_KEY.exec(head);
1185
+ if (k) {
1186
+ const rest = head.slice(k.index).split("\n")[0] ?? "";
1187
+ return {
1188
+ raw: rest.replace(NOTIFY_KEY, "").trim(),
1189
+ argv: null,
1190
+ index: k.index,
1191
+ length: 0,
1192
+ misplaced: false,
1193
+ other: true
1194
+ };
1195
+ }
1196
+ const t = NOTIFY_LINE.exec(tail);
1197
+ if (t && OURS2.test(t[1] ?? ""))
1198
+ return {
1199
+ raw: t[1] ?? "",
1200
+ argv: parseArgv(t[1] ?? ""),
1201
+ index: at + t.index,
1202
+ length: t[0].length,
1203
+ misplaced: true,
1204
+ other: false
1205
+ };
1206
+ return null;
1207
+ }
1208
+ function dropLine(text, index, length) {
1209
+ return text.slice(0, index) + text.slice(index + length).replace(/^\r?\n/, "");
1210
+ }
1211
+ function insertTopLevel(text, line) {
1212
+ const { head, tail } = split(text);
1213
+ if (!tail) return (head.trim() ? head.replace(/\s*$/, "\n") : "") + line + "\n";
1214
+ return `${(head.trim() ? head.replace(/\s*$/, "\n") : "") + line}
1215
+
1216
+ ${tail}`;
1217
+ }
1218
+ function withNotify(text, line) {
1219
+ const n = readNotify(text);
1220
+ if (n && !n.misplaced && !n.other) return text.slice(0, n.index) + line + text.slice(n.index + n.length);
1221
+ if (n?.misplaced) return insertTopLevel(dropLine(text, n.index, n.length), line);
1222
+ return insertTopLevel(text, line);
1223
+ }
1224
+ function replaceNotify(text, line) {
1225
+ const n = readNotify(text);
1226
+ if (!n || n.misplaced || !n.length) return null;
1227
+ return text.slice(0, n.index) + line + text.slice(n.index + n.length);
1228
+ }
1229
+ function withoutNotify(text) {
1230
+ const n = readNotify(text);
1231
+ if (!n || n.other || !n.length) return text;
1232
+ return dropLine(text, n.index, n.length);
1233
+ }
1234
+ var PREVIOUS = "--previous-notify";
1235
+ function previousNotify(argv) {
1236
+ if (!Array.isArray(argv)) return null;
1237
+ const i = argv.indexOf(PREVIOUS);
1238
+ return i >= 0 && typeof argv[i + 1] === "string" ? parseArgv(argv[i + 1]) : null;
1239
+ }
1240
+ function stripPreviousRaw(raw) {
1241
+ return raw.replace(/,\s*"--previous-notify",\s*"(?:[^"\\]|\\.)*"\s*(?=\])/, "");
1242
+ }
1243
+ function withPreviousRaw(raw, prev) {
1244
+ const base = stripPreviousRaw(raw).replace(/\s*\]\s*$/, "");
1245
+ return `${base}, "--previous-notify", ${JSON.stringify(JSON.stringify(prev))}]`;
1246
+ }
1247
+ var isComputerUse = (argv) => Array.isArray(argv) && typeof argv[0] === "string" && /(^|\/)SkyComputerUseClient$/.test(argv[0]);
1248
+
1249
+ // src/adapters/codex.ts
1250
+ var NAME3 = "codex";
1251
+ var CODEX_EVENTS = [
1252
+ "SessionStart",
1253
+ "UserPromptSubmit",
1254
+ "PreToolUse",
1255
+ "PostToolUse",
1256
+ "PermissionRequest",
1257
+ "Stop",
1258
+ "Interrupt",
1259
+ "SubagentStart",
1260
+ "SubagentStop",
1261
+ "PreCompact",
1262
+ "PostCompact",
1263
+ "SessionEnd"
1264
+ ];
1265
+ var OPTS2 = { matcherFor: (e) => /Tool|Permission/.test(e) ? "" : void 0, timeout: 5 };
1266
+ var codexHome = (env) => env.CODEX_HOME || path6.join(HOME, ".codex");
1267
+ var hooksFile2 = (env) => path6.join(codexHome(env), "hooks.json");
1268
+ var configFile2 = (env) => path6.join(codexHome(env), "config.toml");
1269
+ var sessionsDir = (env) => path6.join(codexHome(env), "sessions");
1270
+ var validTool3 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
1271
+ var endReason2 = (r) => r === "clear" || r === "logout" || r === "exit" ? r : "other";
1272
+ function installNotify(cmd, env) {
1273
+ const file = configFile2(env);
1274
+ let text = "";
1275
+ try {
1276
+ text = readFileSync4(file, "utf8");
1277
+ } catch {
1278
+ }
1279
+ const argv = cmd([NAME3, "notify"]).argv;
1280
+ const line = `notify = ${JSON.stringify(argv)}`;
1281
+ const n = readNotify(text);
1282
+ const write = (next) => {
1283
+ mkdirSync3(path6.dirname(file), { recursive: true });
1284
+ writeFileSync3(file, next);
1285
+ };
1286
+ if (n && !n.other) {
1287
+ if (!n.misplaced && JSON.stringify(n.argv) === JSON.stringify(argv))
1288
+ return { file, changed: false, note: "notify current" };
1289
+ write(withNotify(text, line));
1290
+ return {
1291
+ file,
1292
+ changed: true,
1293
+ note: n.misplaced ? "notify moved to the top level (it sat inside a [table], where Codex never reads it)" : "notify updated"
1294
+ };
1295
+ }
1296
+ if (n?.other) {
1297
+ const prev = previousNotify(n.argv);
1298
+ if (isComputerUse(n.argv)) {
1299
+ if (prev && OURS2.test(JSON.stringify(prev))) {
1300
+ if (JSON.stringify(prev) === JSON.stringify(argv))
1301
+ return { file, changed: false, note: "notify current (after Codex Computer Use)" };
1302
+ }
1303
+ if (!prev || OURS2.test(JSON.stringify(prev))) {
1304
+ const next = replaceNotify(text, `notify = ${withPreviousRaw(n.raw, argv)}`);
1305
+ if (next) {
1306
+ write(next);
1307
+ return { file, changed: true, note: "chained after Codex Computer Use (--previous-notify)" };
1308
+ }
1309
+ }
1310
+ }
1311
+ return {
1312
+ file,
1313
+ changed: false,
1314
+ note: `left alone: notify runs ${n.raw.slice(0, 80)} (hooks.json carries the events; the notify fallback is off)`
1315
+ };
1316
+ }
1317
+ write(withNotify(text, line));
1318
+ return { file, changed: true, note: "notify set at the top level" };
1319
+ }
1320
+ function uninstallNotify(env) {
1321
+ const file = configFile2(env);
1322
+ let text;
1323
+ try {
1324
+ text = readFileSync4(file, "utf8");
1325
+ } catch {
1326
+ return { file, changed: false };
1327
+ }
1328
+ const n = readNotify(text);
1329
+ if (!n) return { file, changed: false };
1330
+ if (!n.other) {
1331
+ writeFileSync3(file, withoutNotify(text));
1332
+ return { file, changed: true };
1333
+ }
1334
+ const prev = previousNotify(n.argv);
1335
+ if (isComputerUse(n.argv) && prev && OURS2.test(JSON.stringify(prev))) {
1336
+ const next = replaceNotify(text, `notify = ${stripPreviousRaw(n.raw)}`);
1337
+ if (next) writeFileSync3(file, next);
1338
+ return { file, changed: true };
1339
+ }
1340
+ return { file, changed: false };
1341
+ }
1342
+ function findRollout(threadId, env) {
1343
+ let best = null;
1344
+ const walk = (dir, depth) => {
1345
+ let names = [];
1346
+ try {
1347
+ names = readdirSync4(dir);
1348
+ } catch {
1349
+ return;
1350
+ }
1351
+ for (const n of names.sort().reverse()) {
1352
+ const p = path6.join(dir, n);
1353
+ if (n.endsWith(".jsonl") && n.includes(threadId)) {
1354
+ best = p;
1355
+ return;
1356
+ }
1357
+ if (depth < 3) {
1358
+ walk(p, depth + 1);
1359
+ if (best) return;
1360
+ }
1361
+ }
1362
+ };
1363
+ walk(sessionsDir(env), 0);
1364
+ return best;
1365
+ }
1366
+ var codex = {
1367
+ name: NAME3,
1368
+ events: CODEX_EVENTS,
1369
+ detect: (env = process.env) => existsSync4(codexHome(env)),
1370
+ configFiles: (env = process.env) => [hooksFile2(env), configFile2(env)],
1371
+ install: (cmd, env = process.env) => [
1372
+ installClaudeShaped(hooksFile2(env), NAME3, CODEX_EVENTS, cmd, OPTS2),
1373
+ installNotify(cmd, env)
1374
+ ],
1375
+ uninstall: (env = process.env) => [uninstallClaudeShaped(hooksFile2(env), CODEX_EVENTS), uninstallNotify(env)],
1376
+ installed: (cmd, env = process.env) => {
1377
+ const out = [
1378
+ { file: hooksFile2(env), state: installedClaudeShaped(hooksFile2(env), NAME3, CODEX_EVENTS, cmd, OPTS2) }
1379
+ ];
1380
+ let text = "";
1381
+ try {
1382
+ text = readFileSync4(configFile2(env), "utf8");
1383
+ } catch {
1384
+ }
1385
+ const n = readNotify(text);
1386
+ const argv = cmd([NAME3, "notify"]).argv;
1387
+ const prev = n ? previousNotify(n.argv) : null;
1388
+ const ours = n && (!n.other || prev && JSON.stringify(prev) === JSON.stringify(argv));
1389
+ out.push({
1390
+ file: configFile2(env),
1391
+ 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"
1392
+ });
1393
+ return out;
1394
+ },
1395
+ fromHook(input) {
1396
+ const event = input.argv[1] ?? "";
1397
+ let s = {};
1398
+ if (event === "notify") {
1399
+ const raw = input.argv.find((a) => a.startsWith("{")) ?? input.stdin;
1400
+ try {
1401
+ s = raw ? JSON.parse(raw) : {};
1402
+ } catch {
1403
+ }
1404
+ const id2 = s["thread-id"] ?? s.thread_id ?? s["session-id"] ?? s.session_id;
1405
+ if (!id2) return null;
1406
+ const r2 = {
1407
+ session: { id: String(id2), cwd: typeof s.cwd === "string" ? s.cwd : input.cwd },
1408
+ events: [
1409
+ {
1410
+ type: "turn.ended",
1411
+ data: { reason: "stop", ...typeof s["turn-id"] === "string" ? { turn_id: s["turn-id"] } : {} },
1412
+ harnessEvent: "notify"
1413
+ }
1414
+ ],
1415
+ hints: {}
1416
+ };
1417
+ const file = findRollout(String(id2), input.env);
1418
+ if (file) r2.transcript = file;
1419
+ return r2;
1420
+ }
1421
+ try {
1422
+ s = input.stdin ? JSON.parse(input.stdin) : {};
1423
+ } catch {
1424
+ }
1425
+ const id = s.session_id;
1426
+ if (!id) return null;
1427
+ const session = { id: String(id), cwd: typeof s.cwd === "string" ? s.cwd : input.cwd };
1428
+ if (typeof s.model === "string") session.model = s.model;
1429
+ const turn_id = typeof s.turn_id === "string" ? s.turn_id : void 0;
1430
+ const tool = validTool3(s.tool_name) ? s.tool_name : void 0;
1431
+ const call_id = typeof s.tool_use_id === "string" ? s.tool_use_id : void 0;
1432
+ const ev = (type, data) => ({ type, data: prune3(data), harnessEvent: event });
1433
+ const events = [];
1434
+ switch (event) {
1435
+ case "SessionStart":
1436
+ events.push(
1437
+ ev("session.started", {
1438
+ source: typeof s.source === "string" && ["startup", "resume", "clear", "compact", "fork"].includes(s.source) ? s.source : "unknown"
1439
+ })
1440
+ );
1441
+ break;
1442
+ case "UserPromptSubmit":
1443
+ events.push(
1444
+ ev("turn.started", {
1445
+ turn_id,
1446
+ prompt_chars: typeof s.prompt === "string" ? [...s.prompt].length : void 0
1447
+ })
1448
+ );
1449
+ break;
1450
+ case "PreToolUse":
1451
+ if (tool) events.push(ev("tool.started", { tool, call_id, turn_id, input: s.tool_input }));
1452
+ break;
1453
+ case "PostToolUse":
1454
+ if (tool)
1455
+ events.push(
1456
+ ev("tool.ended", { tool, call_id, turn_id, ok: true, input: s.tool_input, output: s.tool_response })
1457
+ );
1458
+ break;
1459
+ case "PermissionRequest":
1460
+ events.push(
1461
+ ev("attention.needed", { attention_id: call_id ?? `perm-${Date.now()}`, kind: "permission", tool })
1462
+ );
1463
+ break;
1464
+ case "Stop":
1465
+ events.push(ev("turn.ended", { turn_id, reason: "stop" }));
1466
+ break;
1467
+ case "Interrupt":
1468
+ events.push(ev("turn.ended", { turn_id, reason: "interrupt" }));
1469
+ break;
1470
+ case "SubagentStart":
1471
+ events.push(
1472
+ ev("subagent.started", {
1473
+ agent_id: String(s.agent_id ?? s.subagent_id ?? ""),
1474
+ agent_type: typeof s.agent_type === "string" ? s.agent_type : void 0
1475
+ })
1476
+ );
1477
+ break;
1478
+ case "SubagentStop":
1479
+ events.push(
1480
+ ev("subagent.ended", {
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 "PreCompact":
1487
+ events.push(ev("context.compacted", { trigger: s.trigger === "manual" ? "manual" : "auto" }));
1488
+ break;
1489
+ case "PostCompact":
1490
+ break;
1491
+ case "SessionEnd":
1492
+ events.push(ev("session.ended", { reason: endReason2(s.reason) }));
1493
+ break;
1494
+ default:
1495
+ return null;
1496
+ }
1497
+ const r = { session, events, hints: {} };
1498
+ if (typeof s.transcript_path === "string") r.transcript = s.transcript_path;
1499
+ return r;
1500
+ },
1501
+ readTranscript(file, fromLine) {
1502
+ let text = "";
1503
+ try {
1504
+ text = readFileSync4(file, "utf8");
1505
+ } catch {
1506
+ return { turns: [], line: fromLine };
1507
+ }
1508
+ return readCodex(text.split("\n"), fromLine);
1509
+ },
1510
+ facts(session, transcript) {
1511
+ const out = {};
1512
+ if (transcript) Object.assign(out, codexFacts(transcript));
1513
+ return out;
1514
+ },
1515
+ backfill(sinceMs, env = process.env) {
1516
+ const rows = [];
1517
+ for (const f of recentFiles(sessionsDir(env), 3, sinceMs, (n) => /^rollout-.*\.jsonl$/.test(n))) {
1518
+ const m = /([0-9a-f]{8}-[0-9a-f-]{27,})\.jsonl$/i.exec(path6.basename(f));
1519
+ if (!m?.[1]) continue;
1520
+ const facts = codexFacts(f);
1521
+ if (!facts.started_at || !facts.last_at) continue;
1522
+ const { started_at, last_at, first_ask: _fa, harness_version: _hv, ...rest } = facts;
1523
+ rows.push({ session: { id: m[1], ...rest }, started_at, last_at });
1524
+ }
1525
+ return rows;
1526
+ }
1527
+ };
1528
+ function prune3(o) {
1529
+ for (const k of Object.keys(o)) if (o[k] === void 0) delete o[k];
1530
+ return o;
1531
+ }
1532
+ function codexFacts(file) {
1533
+ const { head, tail } = ends(file, 128 * 1024, 64 * 1024);
1534
+ const H = parseLines(head);
1535
+ const T = parseLines(tail);
1536
+ const out = {};
1537
+ const meta = H.find((j) => j.type === "session_meta" && j.payload);
1538
+ if (meta?.payload) {
1539
+ const p = meta.payload;
1540
+ if (p.cwd) out.cwd = p.cwd;
1541
+ if (p.cli_version) out.harness_version = String(p.cli_version);
1542
+ if (p.git?.branch) out.branch = p.git.branch;
1543
+ if (p.git?.repository_url) out.repo = String(p.git.repository_url).replace(/\/\/[^/@]*@/, "//");
1544
+ }
1545
+ const ctx = H.concat(T).reverse().find((j) => j.type === "turn_context" && j.payload?.model);
1546
+ if (ctx?.payload?.model) out.model = ctx.payload.model;
1547
+ for (const j of H) {
1548
+ const p = j.payload;
1549
+ if (j.type !== "response_item" || !p || p.type !== "message" || p.role !== "user") continue;
1550
+ const text = (p.content ?? []).filter((b) => b && b.type === "input_text" && typeof b.text === "string").map((b) => b.text).join("\n").trim();
1551
+ if (!text || /^<[a-z_-]+>/i.test(text)) continue;
1552
+ const line = text.split("\n").find((l) => l.trim()) ?? "";
1553
+ out.title = line.length > 80 ? `${line.slice(0, 80).replace(/\s+\S*$/, "")}\u2026` : line;
1554
+ out.title_source = "first-ask";
1555
+ break;
1556
+ }
1557
+ const times = H.concat(T).map((j) => Date.parse(String(j.timestamp ?? ""))).filter((n) => n > 0);
1558
+ if (times.length) {
1559
+ out.started_at = Math.min(...times);
1560
+ out.last_at = Math.max(...times);
1561
+ }
1562
+ return out;
1563
+ }
1564
+
1565
+ // src/adapters/gemini-cli.ts
1566
+ import { existsSync as existsSync5, readFileSync as readFileSync5 } from "node:fs";
1567
+ import path7 from "node:path";
1568
+ var NAME4 = "gemini-cli";
1569
+ var GEMINI_EVENTS = [
1570
+ "SessionStart",
1571
+ "BeforeAgent",
1572
+ "BeforeTool",
1573
+ "AfterTool",
1574
+ "Notification",
1575
+ "AfterAgent",
1576
+ "PreCompress",
1577
+ "SessionEnd"
1578
+ ];
1579
+ var OPTS3 = { matcherFor: (e) => /Tool/.test(e) ? ".*" : void 0, timeout: 5e3 };
1580
+ var geminiDir = (env) => env.GEMINI_CLI_HOME || path7.join(HOME, ".gemini");
1581
+ var settings = (env) => path7.join(geminiDir(env), "settings.json");
1582
+ var validTool4 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
1583
+ var geminiCli = {
1584
+ name: NAME4,
1585
+ events: GEMINI_EVENTS,
1586
+ // ~/.gemini also belongs to Antigravity; Gemini CLI is here when its settings or its chat logs exist.
1587
+ detect: (env = process.env) => existsSync5(settings(env)) || existsSync5(path7.join(geminiDir(env), "tmp")),
1588
+ configFiles: (env = process.env) => [settings(env)],
1589
+ install: (cmd, env = process.env) => [installClaudeShaped(settings(env), NAME4, GEMINI_EVENTS, cmd, OPTS3)],
1590
+ uninstall: (env = process.env) => [uninstallClaudeShaped(settings(env), GEMINI_EVENTS)],
1591
+ installed: (cmd, env = process.env) => [
1592
+ { file: settings(env), state: installedClaudeShaped(settings(env), NAME4, GEMINI_EVENTS, cmd, OPTS3) }
1593
+ ],
1594
+ fromHook(input) {
1595
+ const event = input.argv[1] ?? "";
1596
+ let s = {};
1597
+ try {
1598
+ s = input.stdin ? JSON.parse(input.stdin) : {};
1599
+ } catch {
1600
+ }
1601
+ const id = s.session_id;
1602
+ if (!id) return null;
1603
+ const session = { id: String(id), cwd: typeof s.cwd === "string" ? s.cwd : input.cwd };
1604
+ const tool = validTool4(s.tool_name) ? s.tool_name : void 0;
1605
+ const ev = (type, data) => ({ type, data: prune4(data), harnessEvent: event });
1606
+ const events = [];
1607
+ switch (event) {
1608
+ case "SessionStart":
1609
+ events.push(
1610
+ ev("session.started", {
1611
+ source: typeof s.source === "string" && ["startup", "resume", "clear"].includes(s.source) ? s.source : "unknown"
1612
+ })
1613
+ );
1614
+ break;
1615
+ case "BeforeAgent":
1616
+ events.push(
1617
+ ev("turn.started", { prompt_chars: typeof s.prompt === "string" ? [...s.prompt].length : void 0 })
1618
+ );
1619
+ break;
1620
+ case "BeforeTool":
1621
+ if (tool) events.push(ev("tool.started", { tool, input: s.tool_input }));
1622
+ break;
1623
+ case "AfterTool": {
1624
+ const resp = s.tool_response;
1625
+ const err = resp && typeof resp === "object" && resp.error ? String(typeof resp.error === "string" ? resp.error : JSON.stringify(resp.error)).slice(0, 2e3) : void 0;
1626
+ if (tool)
1627
+ events.push(
1628
+ ev("tool.ended", {
1629
+ tool,
1630
+ ok: !err,
1631
+ error: err,
1632
+ input: s.tool_input,
1633
+ output: resp?.llmContent ?? s.tool_response
1634
+ })
1635
+ );
1636
+ break;
1637
+ }
1638
+ case "Notification":
1639
+ events.push(
1640
+ ev("attention.needed", {
1641
+ attention_id: `notif-${Date.now()}`,
1642
+ kind: s.notification_type === "ToolPermission" ? "permission" : "question",
1643
+ message: typeof s.message === "string" ? s.message.slice(0, 200) : void 0
1644
+ })
1645
+ );
1646
+ break;
1647
+ case "AfterAgent":
1648
+ events.push(ev("turn.ended", { reason: "stop" }));
1649
+ break;
1650
+ case "PreCompress":
1651
+ events.push(ev("context.compacted", { trigger: s.trigger === "manual" ? "manual" : "auto" }));
1652
+ break;
1653
+ case "SessionEnd":
1654
+ events.push(
1655
+ ev("session.ended", {
1656
+ reason: s.reason === "clear" || s.reason === "logout" || s.reason === "exit" ? s.reason : s.reason === "prompt_input_exit" ? "exit" : "other"
1657
+ })
1658
+ );
1659
+ break;
1660
+ default:
1661
+ return null;
1662
+ }
1663
+ const r = { session, events, hints: {} };
1664
+ if (typeof s.transcript_path === "string") r.transcript = s.transcript_path;
1665
+ return r;
1666
+ },
1667
+ readTranscript(file, fromLine) {
1668
+ let text = "";
1669
+ try {
1670
+ text = readFileSync5(file, "utf8");
1671
+ } catch {
1672
+ return { turns: [], line: fromLine };
1673
+ }
1674
+ return readGemini(text.split("\n"), fromLine);
1675
+ },
1676
+ facts(_session, transcript) {
1677
+ const out = {};
1678
+ if (!transcript) return out;
1679
+ const { head, tail } = ends(transcript, 64 * 1024, 64 * 1024);
1680
+ const H = parseLines(head);
1681
+ const T = parseLines(tail);
1682
+ const first = H.find((j) => j.type === "user");
1683
+ const raw = first ? first.content ?? first.message ?? first.text : void 0;
1684
+ const text = typeof raw === "string" ? raw.trim() : "";
1685
+ if (text) {
1686
+ const line = text.split("\n").find((l) => l.trim()) ?? "";
1687
+ out.title = line.length > 80 ? `${line.slice(0, 80).replace(/\s+\S*$/, "")}\u2026` : line;
1688
+ out.title_source = "first-ask";
1689
+ }
1690
+ const times = H.concat(T).map((j) => Date.parse(String(j.timestamp ?? ""))).filter((n) => n > 0);
1691
+ if (times.length) {
1692
+ out.started_at = Math.min(...times);
1693
+ out.last_at = Math.max(...times);
1694
+ }
1695
+ return out;
1696
+ },
1697
+ backfill: () => []
1698
+ // chat logs are keyed by a project hash, not a session id we can name; the first hook of a resumed session reports it
1699
+ };
1700
+ function prune4(o) {
1701
+ for (const k of Object.keys(o)) if (o[k] === void 0) delete o[k];
1702
+ return o;
1703
+ }
1704
+
1705
+ // src/adapters/index.ts
1706
+ var ADAPTERS = [claudeCode, codex, geminiCli, antigravity];
1707
+ var adapterByName = (name) => ADAPTERS.find((a) => a.name === name);
1708
+
1709
+ // src/config.ts
1710
+ import { chmodSync, existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "node:fs";
1711
+ import path8 from "node:path";
1712
+ var DEFAULT_CONFIG = { sinks: [], harnesses: {}, update_check: true, keep_days: 30 };
1713
+ function readConfig(file = configFile()) {
1714
+ try {
1715
+ const j = JSON.parse(readFileSync6(file, "utf8"));
1716
+ return {
1717
+ ...DEFAULT_CONFIG,
1718
+ ...j,
1719
+ sinks: Array.isArray(j.sinks) ? j.sinks : [],
1720
+ harnesses: j.harnesses && typeof j.harnesses === "object" ? j.harnesses : {}
1721
+ };
1722
+ } catch {
1723
+ return { ...DEFAULT_CONFIG, sinks: [], harnesses: {} };
1724
+ }
1725
+ }
1726
+ function writeConfig(c, file = configFile()) {
1727
+ mkdirSync4(path8.dirname(file), { recursive: true, mode: 448 });
1728
+ writeFileSync4(file, `${JSON.stringify(c, null, 2)}
1729
+ `, { mode: 384 });
1730
+ if (process.platform !== "win32" && existsSync6(file)) {
1731
+ try {
1732
+ chmodSync(file, 384);
1733
+ } catch {
1734
+ }
1735
+ }
1736
+ }
1737
+ function machineName(c, hostname) {
1738
+ if (c.machine) return String(c.machine).slice(0, 80);
1739
+ return hostname.replace(/\.(local|lan|home)$/i, "");
1740
+ }
1741
+
1742
+ // src/ulid.ts
1743
+ import { randomBytes } from "node:crypto";
1744
+ var ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
1745
+ var lastTime = 0;
1746
+ var lastRand = [];
1747
+ function encodeTime(t) {
1748
+ let out = "";
1749
+ let n = t;
1750
+ for (let i = 0; i < 10; i++) {
1751
+ out = ALPHABET[n % 32] + out;
1752
+ n = Math.floor(n / 32);
1753
+ }
1754
+ return out;
1755
+ }
1756
+ function ulid(now = Date.now()) {
1757
+ if (now === lastTime) {
1758
+ for (let i = lastRand.length - 1; i >= 0; i--) {
1759
+ const v = (lastRand[i] ?? 0) + 1;
1760
+ if (v < 32) {
1761
+ lastRand[i] = v;
1762
+ break;
1763
+ }
1764
+ lastRand[i] = 0;
1765
+ }
1766
+ } else {
1767
+ lastTime = now;
1768
+ const b = randomBytes(16);
1769
+ lastRand = Array.from({ length: 16 }, (_, i) => (b[i] ?? 0) % 32);
1770
+ }
1771
+ return encodeTime(now) + lastRand.map((v) => ALPHABET[v]).join("");
1772
+ }
1773
+ var isUlid = (s) => /^[0-9A-HJKMNP-TV-Z]{26}$/.test(s);
1774
+
1775
+ // src/events.ts
1776
+ function makeEvent(a, ctx) {
1777
+ const now = ctx.now ?? Date.now();
1778
+ const harness = { name: ctx.harness.name, event: a.harnessEvent };
1779
+ if (ctx.harness.version) harness.version = ctx.harness.version;
1780
+ return {
1781
+ protocol: 1,
1782
+ id: ulid(now),
1783
+ type: a.type,
1784
+ time: new Date(now).toISOString(),
1785
+ tier: 3,
1786
+ harness,
1787
+ session: ctx.session,
1788
+ privacy: { rulesets: [], pii: false },
1789
+ data: a.data
1790
+ };
1791
+ }
1792
+
1793
+ // src/outbox.ts
1794
+ import {
1795
+ appendFileSync,
1796
+ closeSync as closeSync2,
1797
+ existsSync as existsSync7,
1798
+ mkdirSync as mkdirSync5,
1799
+ openSync as openSync2,
1800
+ readdirSync as readdirSync5,
1801
+ readFileSync as readFileSync7,
1802
+ readSync as readSync2,
1803
+ renameSync,
1804
+ statSync as statSync4,
1805
+ unlinkSync,
1806
+ writeFileSync as writeFileSync5
1807
+ } from "node:fs";
1808
+ import path9 from "node:path";
1809
+ var Outbox = class {
1810
+ constructor(root) {
1811
+ this.root = root;
1812
+ }
1813
+ root;
1814
+ dir(harness) {
1815
+ return path9.join(this.root, "outbox", safeId(harness));
1816
+ }
1817
+ file(ref) {
1818
+ return path9.join(this.dir(ref.harness), `${safeId(ref.session)}.jsonl`);
1819
+ }
1820
+ seqFile(ref) {
1821
+ return path9.join(this.dir(ref.harness), `${safeId(ref.session)}.seq`);
1822
+ }
1823
+ /** The next seq for a session, persisted (survives a hibernating machine and a
1824
+ * restart; an in-memory counter would restart at 0 and collide). */
1825
+ nextSeq(ref) {
1826
+ const f = this.seqFile(ref);
1827
+ let n = 0;
1828
+ try {
1829
+ n = Number(readFileSync7(f, "utf8")) || 0;
1830
+ } catch {
1831
+ n = this.lastSeqInFile(ref) + 1;
1832
+ }
1833
+ mkdirSync5(path9.dirname(f), { recursive: true });
1834
+ writeFileSync5(f, String(n + 1));
1835
+ return n;
1836
+ }
1837
+ lastSeqInFile(ref) {
1838
+ const f = this.file(ref);
1839
+ if (!existsSync7(f)) return -1;
1840
+ const lines = readFileSync7(f, "utf8").trim().split("\n");
1841
+ for (let i = lines.length - 1; i >= 0; i--) {
1842
+ try {
1843
+ const j = JSON.parse(lines[i] ?? "");
1844
+ if (typeof j.session?.seq === "number") return j.session.seq;
1845
+ } catch {
1846
+ }
1847
+ }
1848
+ return -1;
1849
+ }
1850
+ append(ref, events) {
1851
+ if (!events.length) return;
1852
+ mkdirSync5(this.dir(ref.harness), { recursive: true });
1853
+ appendFileSync(this.file(ref), `${events.map((e) => JSON.stringify(e)).join("\n")}
1854
+ `);
1855
+ }
1856
+ /** Every session file, newest first. */
1857
+ sessions() {
1858
+ const out = [];
1859
+ const base = path9.join(this.root, "outbox");
1860
+ if (!existsSync7(base)) return [];
1861
+ for (const h of readdirSync5(base)) {
1862
+ const d = path9.join(base, h);
1863
+ let names = [];
1864
+ try {
1865
+ names = readdirSync5(d).filter((n) => n.endsWith(".jsonl"));
1866
+ } catch {
1867
+ continue;
1868
+ }
1869
+ for (const n of names) {
1870
+ try {
1871
+ out.push({ ref: { harness: h, session: n.slice(0, -6) }, mtime: statSync4(path9.join(d, n)).mtimeMs });
1872
+ } catch {
1873
+ }
1874
+ }
1875
+ }
1876
+ return out.sort((a, b) => b.mtime - a.mtime).map((x) => x.ref);
1877
+ }
1878
+ /** Read events after byte offset `from`; returns them and the new offset. Lines
1879
+ * are read whole; a partial trailing line (a write in progress) is left. */
1880
+ read(ref, from, maxEvents = 500) {
1881
+ const f = this.file(ref);
1882
+ if (!existsSync7(f)) return { events: [], offset: from };
1883
+ const size = statSync4(f).size;
1884
+ if (from >= size) return { events: [], offset: from };
1885
+ const fd = openSync2(f, "r");
1886
+ try {
1887
+ const buf = Buffer.alloc(Math.min(size - from, 8 * 1024 * 1024));
1888
+ const n = readSync2(fd, buf, 0, buf.length, from);
1889
+ const text = buf.subarray(0, n).toString("utf8");
1890
+ const events = [];
1891
+ let offset = from;
1892
+ let pos = 0;
1893
+ while (events.length < maxEvents) {
1894
+ const nl = text.indexOf("\n", pos);
1895
+ if (nl < 0) break;
1896
+ const line = text.slice(pos, nl);
1897
+ pos = nl + 1;
1898
+ offset = from + Buffer.byteLength(text.slice(0, pos), "utf8");
1899
+ if (!line.trim()) continue;
1900
+ try {
1901
+ events.push(JSON.parse(line));
1902
+ } catch {
1903
+ }
1904
+ }
1905
+ return { events, offset };
1906
+ } finally {
1907
+ closeSync2(fd);
1908
+ }
1909
+ }
1910
+ /** Files older than `days` are pruned; the cursor files with them. */
1911
+ prune(days, now = Date.now()) {
1912
+ let n = 0;
1913
+ const cutoff = now - days * 864e5;
1914
+ for (const ref of this.sessions()) {
1915
+ const f = this.file(ref);
1916
+ try {
1917
+ if (statSync4(f).mtimeMs < cutoff) {
1918
+ unlinkSync(f);
1919
+ try {
1920
+ unlinkSync(this.seqFile(ref));
1921
+ } catch {
1922
+ }
1923
+ n++;
1924
+ }
1925
+ } catch {
1926
+ }
1927
+ }
1928
+ return n;
1929
+ }
1930
+ delete(ref) {
1931
+ for (const f of [this.file(ref), this.seqFile(ref)]) {
1932
+ try {
1933
+ unlinkSync(f);
1934
+ } catch {
1935
+ }
1936
+ }
1937
+ }
1938
+ };
1939
+ var Cursors = class {
1940
+ constructor(root, sink) {
1941
+ this.root = root;
1942
+ this.sink = sink;
1943
+ }
1944
+ root;
1945
+ sink;
1946
+ file() {
1947
+ return path9.join(this.root, "cursors", `${safeId(this.sink)}.json`);
1948
+ }
1949
+ read() {
1950
+ try {
1951
+ return JSON.parse(readFileSync7(this.file(), "utf8"));
1952
+ } catch {
1953
+ return {};
1954
+ }
1955
+ }
1956
+ write(c) {
1957
+ const f = this.file();
1958
+ mkdirSync5(path9.dirname(f), { recursive: true });
1959
+ const tmp = `${f}.${process.pid}.tmp`;
1960
+ writeFileSync5(tmp, JSON.stringify(c));
1961
+ renameSync(tmp, f);
1962
+ }
1963
+ key(ref) {
1964
+ return `${ref.harness}/${ref.session}`;
1965
+ }
1966
+ };
1967
+ function takeLock(root, ref, staleMs = 5 * 6e4) {
1968
+ const dir = path9.join(root, "locks");
1969
+ mkdirSync5(dir, { recursive: true });
1970
+ const f = path9.join(dir, `${safeId(ref.harness)}-${safeId(ref.session)}.lock`);
1971
+ try {
1972
+ if (Date.now() - statSync4(f).mtimeMs > staleMs) unlinkSync(f);
1973
+ else return null;
1974
+ } catch {
1975
+ }
1976
+ try {
1977
+ writeFileSync5(f, String(process.pid), { flag: "wx" });
1978
+ } catch {
1979
+ return null;
1980
+ }
1981
+ return () => {
1982
+ try {
1983
+ unlinkSync(f);
1984
+ } catch {
1985
+ }
1986
+ };
1987
+ }
1988
+
1989
+ // src/privacy/pii.ts
1990
+ import { createHash as createHash2 } from "node:crypto";
1991
+ var RULESET_PII = "pii@1";
1992
+ var EMAIL = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g;
1993
+ var PHONE = /(?<![\w.])(?:\+\d{1,3}[ .-]?)?(?:\(\d{2,4}\)[ .-]?)?\d{2,4}(?:[ .-]?\d{2,4}){1,3}(?!\w)/g;
1994
+ function looksPhone(s) {
1995
+ const digits = s.replace(/\D/g, "");
1996
+ if (digits.length < 7 || digits.length > 15) return false;
1997
+ if (!/[+() .-]/.test(s)) return false;
1998
+ if (!/[+() -]/.test(s)) return false;
1999
+ return true;
2000
+ }
2001
+ function reducePii(text, home) {
2002
+ if (!text) return text;
2003
+ let out = text.replace(EMAIL, "[email]");
2004
+ out = out.replace(PHONE, (m) => looksPhone(m) ? "[phone]" : m);
2005
+ if (home) {
2006
+ const h = home.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2007
+ out = out.replace(new RegExp(`(?<![A-Za-z0-9_])${h}(?=/|$|\\s|["'\`])`, "g"), "~");
2008
+ }
2009
+ return out;
2010
+ }
2011
+ function hashMachine(name) {
2012
+ return createHash2("sha256").update(name).digest("hex").slice(0, 8);
2013
+ }
2014
+ function reducePiiDeep(v, home) {
2015
+ if (typeof v === "string") return reducePii(v, home);
2016
+ if (Array.isArray(v)) return v.map((x) => reducePiiDeep(x, home));
2017
+ if (v && typeof v === "object") {
2018
+ const out = {};
2019
+ for (const [k, x] of Object.entries(v)) out[k] = reducePiiDeep(x, home);
2020
+ return out;
2021
+ }
2022
+ return v;
2023
+ }
2024
+
2025
+ // src/privacy/secrets.ts
2026
+ var RULESET_SECRETS = "secrets@1";
2027
+ var REDACTED = "[redacted]";
2028
+ var TOKEN_PATTERNS = [
2029
+ /-----BEGIN [A-Z0-9 ]*PRIVATE KEY(?: BLOCK)?-----[\s\S]*?(?:-----END [A-Z0-9 ]*PRIVATE KEY(?: BLOCK)?-----|$)/g,
2030
+ /\b(?:AKIA|ASIA|ABIA|ACCA)[0-9A-Z]{16}\b/g,
2031
+ // AWS access key id
2032
+ /\bgh[pousr]_[A-Za-z0-9]{30,}\b/g,
2033
+ // GitHub
2034
+ /\bgithub_pat_[A-Za-z0-9_]{30,}\b/g,
2035
+ /\bglpat-[A-Za-z0-9_-]{20,}\b/g,
2036
+ // GitLab
2037
+ /\bxox[abposre]-[A-Za-z0-9-]{10,}\b/g,
2038
+ // Slack
2039
+ /\bhttps:\/\/hooks\.slack\.com\/services\/[A-Za-z0-9/_-]+/g,
2040
+ /\bhttps:\/\/(?:ptb\.|canary\.)?discord(?:app)?\.com\/api\/webhooks\/[0-9]+\/[A-Za-z0-9_-]+/g,
2041
+ /\bsk-(?:ant-|proj-|svcacct-|admin-|or-v1-)?[A-Za-z0-9_-]{20,}\b/g,
2042
+ // Anthropic, OpenAI, DeepSeek, OpenRouter
2043
+ /\b(?:sk|rk)_(?:live|test)_[A-Za-z0-9]{16,}\b/g,
2044
+ // Stripe secret / restricted
2045
+ /\bwhsec_[A-Za-z0-9+/=]{20,}/g,
2046
+ // Stripe / Svix / Standard Webhooks secret
2047
+ /\bAIza[0-9A-Za-z_-]{35}\b/g,
2048
+ // Google API key
2049
+ /\bya29\.[0-9A-Za-z_-]{20,}/g,
2050
+ // Google OAuth access token
2051
+ /\b1\/\/0[0-9A-Za-z_-]{30,}/g,
2052
+ // Google OAuth refresh token
2053
+ /\bGOCSPX-[0-9A-Za-z_-]{20,}/g,
2054
+ // Google OAuth client secret
2055
+ /\bxai-[A-Za-z0-9]{30,}\b/g,
2056
+ /\bgsk_[A-Za-z0-9]{30,}\b/g,
2057
+ // Groq
2058
+ /\bpplx-[A-Za-z0-9]{30,}\b/g,
2059
+ // Perplexity
2060
+ /\bhf_[A-Za-z0-9]{30,}\b/g,
2061
+ // Hugging Face
2062
+ /\bpcsk_[A-Za-z0-9_]{30,}\b/g,
2063
+ // Pinecone
2064
+ /\bnpm_[A-Za-z0-9]{30,}\b/g,
2065
+ /\bpypi-[A-Za-z0-9_-]{50,}/g,
2066
+ /\bSG\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}/g,
2067
+ // SendGrid
2068
+ /\bshp(?:at|ss|ca|pa)_[a-fA-F0-9]{32}\b/g,
2069
+ // Shopify
2070
+ /\bdop_v1_[a-f0-9]{64}\b/g,
2071
+ // DigitalOcean
2072
+ /\b(?:lin_api|lin_oauth)_[A-Za-z0-9]{30,}\b/g,
2073
+ // Linear
2074
+ /\b(?:ntn|secret)_[A-Za-z0-9]{40,}\b/g,
2075
+ // Notion
2076
+ /\bsbp_[a-f0-9]{40}\b/g,
2077
+ // Supabase
2078
+ /\bfigd_[A-Za-z0-9_-]{30,}/g,
2079
+ // Figma
2080
+ /\bdp\.(?:pt|st|sa|ct)\.[A-Za-z0-9]{30,}/g,
2081
+ // Doppler
2082
+ /\bgrn_[A-Za-z0-9_-]{20,}/g,
2083
+ // Granola
2084
+ /\bss_[a-f0-9]{32,}\b/g,
2085
+ // spacesheep API keys
2086
+ /\b[0-9]{8,10}:AA[A-Za-z0-9_-]{33}\b/g,
2087
+ // Telegram bot token
2088
+ /\beyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g
2089
+ // JWT
2090
+ ];
2091
+ var URL_CREDENTIALS = /\b([a-z][a-z0-9+.-]{1,20}:\/\/[^\s:/@'"`<>]{1,100}:)([^\s@/'"`<>]{1,200})(@)/gi;
2092
+ var AUTH_HEADER = /\b((?:Bearer|Basic|Token)\s+)([A-Za-z0-9._~+/=-]{16,})/g;
2093
+ 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)";
2094
+ var NAME_SEP = /(?<![A-Za-z0-9_])([A-Za-z][A-Za-z0-9_-]{1,79})(["']?\s{0,3}(?::=|=>|=|:)\s{0,3})/g;
2095
+ var VALUE = /"([^"\n]{1,300})"|'([^'\n]{1,300})'|`([^`\n]{1,300})`|([^\s"'`,;&)}\]]{1,300})/y;
2096
+ function redactAssignments(text) {
2097
+ let out = "";
2098
+ let last = 0;
2099
+ NAME_SEP.lastIndex = 0;
2100
+ for (let m = NAME_SEP.exec(text); m; m = NAME_SEP.exec(text)) {
2101
+ const name = m[1] ?? "";
2102
+ if (!isSecretName(name)) continue;
2103
+ VALUE.lastIndex = NAME_SEP.lastIndex;
2104
+ const v = VALUE.exec(text);
2105
+ if (!v) continue;
2106
+ const value = v[1] ?? v[2] ?? v[3] ?? v[4] ?? "";
2107
+ NAME_SEP.lastIndex = VALUE.lastIndex;
2108
+ if (isPlaceholder(value) || value.includes(REDACTED)) continue;
2109
+ if (/tokens?$/i.test(name) && /^\d+(?:[.,]\d+)*[km]?$/i.test(value)) continue;
2110
+ const q = v[1] != null ? '"' : v[2] != null ? "'" : v[3] != null ? "`" : "";
2111
+ out += text.slice(last, v.index) + q + REDACTED + q;
2112
+ last = VALUE.lastIndex;
2113
+ }
2114
+ return last ? out + text.slice(last) : text;
2115
+ }
2116
+ function isSecretName(name) {
2117
+ if (/^[A-Z0-9_]+_KEY$/.test(name)) return true;
2118
+ const flat = name.toLowerCase().replace(/[_-]/g, "");
2119
+ return /(?:pass(?:word|wd|phrase|code)?|pwd|secret|secretkey|secretaccesskey|clientsecret|apikey|accesskey|accesstoken|authtoken|refreshtoken|privatekey|token|credential|creds)s?$/.test(
2120
+ flat
2121
+ );
2122
+ }
2123
+ var PROSE = new RegExp(
2124
+ `(\\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`,
2125
+ "gi"
2126
+ );
2127
+ function isPlaceholder(v) {
2128
+ const s = v.replace(/[.,;:!?]+$/, "");
2129
+ if (!s) return true;
2130
+ 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(
2131
+ s
2132
+ ))
2133
+ return true;
2134
+ if (/^\[?redacted\]?$/i.test(s) || /^\*+$/.test(s) || /^x{3,}$/i.test(s) || /^\.{3}$|^…$/.test(s)) return true;
2135
+ if (/^[<{$%]/.test(s)) return true;
2136
+ 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(
2137
+ s
2138
+ ))
2139
+ return true;
2140
+ if (/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)+$/.test(s)) return true;
2141
+ if (/[(]/.test(s)) return true;
2142
+ if (/^(?:your|my|the|a|an|some|new|old)[-_ ]/i.test(s)) return true;
2143
+ return false;
2144
+ }
2145
+ function looksSecret(v) {
2146
+ const s = v.replace(/[.,;:!?)]+$/, "");
2147
+ if (s.length < 6) return false;
2148
+ if (isPlaceholder(s)) return false;
2149
+ if (/^https?:\/\//i.test(s)) return false;
2150
+ if (/^[a-z]+(?:[-'][a-z]+)*$/.test(s)) return false;
2151
+ if (/^[A-Z][a-z]+$/.test(s)) return false;
2152
+ if (/^\d+(?:[.,]\d+)*[a-z%]*$/i.test(s)) return false;
2153
+ if (/^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+$/.test(s)) return false;
2154
+ const kinds = [/[a-z]/, /[A-Z]/, /[0-9]/, /[^A-Za-z0-9]/].filter((r) => r.test(s)).length;
2155
+ return kinds >= 3 || /[0-9]/.test(s) && /[A-Za-z]/.test(s);
2156
+ }
2157
+ function redactSecrets(text) {
2158
+ if (!text) return text;
2159
+ let out = text;
2160
+ for (const re of TOKEN_PATTERNS) out = out.replace(re, REDACTED);
2161
+ out = out.replace(URL_CREDENTIALS, (_m, head, _pw, at) => `${head}${REDACTED}${at}`);
2162
+ out = out.replace(AUTH_HEADER, (m, head, v) => v.includes(REDACTED) ? m : `${head}${REDACTED}`);
2163
+ out = redactAssignments(out);
2164
+ out = out.replace(
2165
+ PROSE,
2166
+ (m, head, q, v) => !looksSecret(v) || v.includes(REDACTED) ? m : `${head}${q}${REDACTED}${q}`
2167
+ );
2168
+ return out;
2169
+ }
2170
+ function hasSecret(text) {
2171
+ return !!text && redactSecrets(text) !== text;
2172
+ }
2173
+ function redactDeep(v) {
2174
+ if (typeof v === "string") return redactSecrets(v);
2175
+ if (Array.isArray(v)) return v.map(redactDeep);
2176
+ if (v && typeof v === "object") {
2177
+ const out = {};
2178
+ for (const [k, x] of Object.entries(v)) out[k] = redactDeep(x);
2179
+ return out;
2180
+ }
2181
+ return v;
2182
+ }
2183
+
2184
+ // src/privacy/filter.ts
2185
+ var TYPE_TIER = {
2186
+ "session.started": 0,
2187
+ "session.heartbeat": 0,
2188
+ "session.ended": 0,
2189
+ "session.backfill": 0,
2190
+ "session.forgotten": 0,
2191
+ "turn.started": 0,
2192
+ "turn.ended": 0,
2193
+ "turn.transcript": 2,
2194
+ "tool.started": 1,
2195
+ "tool.ended": 1,
2196
+ "files.changed": 1,
2197
+ "attention.needed": 0,
2198
+ "attention.cleared": 0,
2199
+ "subagent.started": 1,
2200
+ "subagent.ended": 1,
2201
+ "context.compacted": 1,
2202
+ native: 3
2203
+ };
2204
+ var DATA_ALLOW = {
2205
+ "session.started": { 0: ["source"] },
2206
+ "session.heartbeat": { 0: [] },
2207
+ "session.ended": { 0: ["reason"] },
2208
+ "session.backfill": { 0: ["started_at", "last_at", "turns"] },
2209
+ "session.forgotten": { 0: [] },
2210
+ "turn.started": { 0: ["turn_id"], 1: ["prompt_chars"] },
2211
+ "turn.ended": { 0: ["turn_id", "reason", "ms"] },
2212
+ "turn.transcript": { 2: ["turn", "user", "assistant", "at"] },
2213
+ "tool.started": { 1: ["tool", "call_id", "turn_id", "agent_id"], 3: ["input"] },
2214
+ "tool.ended": { 1: ["tool", "call_id", "turn_id", "agent_id", "ms", "ok", "error"], 3: ["input", "output"] },
2215
+ "files.changed": { 1: ["paths"] },
2216
+ "attention.needed": { 0: ["attention_id", "kind"], 1: ["tool", "message"] },
2217
+ "attention.cleared": { 0: ["attention_id", "how"] },
2218
+ "subagent.started": { 1: ["agent_id", "agent_type"] },
2219
+ "subagent.ended": { 1: ["agent_id", "agent_type"] },
2220
+ "context.compacted": { 1: ["trigger"] }
2221
+ };
2222
+ var SESSION_ALLOW = [
2223
+ "id",
2224
+ "seq",
2225
+ "machine",
2226
+ "cwd",
2227
+ "repo",
2228
+ "branch",
2229
+ "model",
2230
+ "title",
2231
+ "title_source",
2232
+ "url",
2233
+ "account_id",
2234
+ "parent_id"
2235
+ ];
2236
+ var MAX_SIDE = 2e4;
2237
+ var MAX_IO = 64 * 1024;
2238
+ function allowedKeys(type, tier) {
2239
+ const table = DATA_ALLOW[type];
2240
+ if (!table) return null;
2241
+ const out = [];
2242
+ for (let t = 0; t <= tier; t++) for (const k of table[t] ?? []) out.push(k);
2243
+ return out;
2244
+ }
2245
+ function cap(v, max) {
2246
+ if (typeof v === "string") return v.length > max ? `${v.slice(0, max)}\u2026` : v;
2247
+ if (v && typeof v === "object") {
2248
+ const s = JSON.stringify(v);
2249
+ return s.length > max ? `${s.slice(0, max)}\u2026` : v;
2250
+ }
2251
+ return v;
2252
+ }
2253
+ function filterEvent(event, tier, pii, opts = {}) {
2254
+ const minTier = TYPE_TIER[event.type];
2255
+ if (minTier !== void 0 && minTier > tier) return null;
2256
+ if (event.type === "native" && tier < 3) return null;
2257
+ const session = {};
2258
+ for (const k of SESSION_ALLOW)
2259
+ if (event.session[k] !== void 0)
2260
+ session[k] = event.session[k];
2261
+ let data = {};
2262
+ const keys = allowedKeys(event.type, tier);
2263
+ if (keys === null) data = { ...event.data };
2264
+ else for (const k of keys) if (event.data[k] !== void 0) data[k] = event.data[k];
2265
+ if (typeof data.user === "string") data.user = cap(data.user, MAX_SIDE);
2266
+ if (typeof data.assistant === "string") data.assistant = cap(data.assistant, MAX_SIDE);
2267
+ if (data.input !== void 0) data.input = cap(data.input, MAX_IO);
2268
+ if (data.output !== void 0) data.output = cap(data.output, MAX_IO);
2269
+ if (typeof data.message === "string") data.message = cap(data.message, 200);
2270
+ const rulesets = [];
2271
+ let out = { ...event, tier, session, data, privacy: { rulesets, pii } };
2272
+ if (tier > 0) {
2273
+ out = { ...out, session: redactDeep(out.session), data: redactDeep(out.data) };
2274
+ rulesets.push(RULESET_SECRETS);
2275
+ }
2276
+ if (pii) {
2277
+ const s = reducePiiDeep(out.session, opts.home);
2278
+ if (typeof s.machine === "string") s.machine = hashMachine(s.machine);
2279
+ delete s.account_id;
2280
+ out = { ...out, session: s, data: reducePiiDeep(out.data, opts.home) };
2281
+ rulesets.push(RULESET_PII);
2282
+ }
2283
+ out.privacy = { rulesets, pii };
2284
+ return out;
2285
+ }
2286
+
2287
+ // src/sinks/file.ts
2288
+ import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync6 } from "node:fs";
2289
+ import path10 from "node:path";
2290
+ var FileSink = class {
2291
+ constructor(name, file, tier, pii) {
2292
+ this.name = name;
2293
+ this.file = file;
2294
+ this.tier = tier;
2295
+ this.pii = pii;
2296
+ }
2297
+ name;
2298
+ file;
2299
+ tier;
2300
+ pii;
2301
+ async send(events) {
2302
+ try {
2303
+ mkdirSync6(path10.dirname(this.file), { recursive: true });
2304
+ appendFileSync2(this.file, `${events.map((e) => JSON.stringify(e)).join("\n")}
2305
+ `);
2306
+ return { ok: true, acknowledged: events.length, accepted: events.length, duplicates: 0, rejected: [] };
2307
+ } catch (e) {
2308
+ return { ok: false, acknowledged: 0, error: String(e.message), action: "retry" };
2309
+ }
2310
+ }
2311
+ };
2312
+
2313
+ // src/sinks/stdout.ts
2314
+ var StdoutSink = class {
2315
+ constructor(name, tier, pii, write = (l) => process.stdout.write(`${l}
2316
+ `)) {
2317
+ this.name = name;
2318
+ this.tier = tier;
2319
+ this.pii = pii;
2320
+ this.write = write;
2321
+ }
2322
+ name;
2323
+ tier;
2324
+ pii;
2325
+ write;
2326
+ async send(events) {
2327
+ for (const e of events) this.write(JSON.stringify(e));
2328
+ return { ok: true, acknowledged: events.length, accepted: events.length, duplicates: 0, rejected: [] };
2329
+ }
2330
+ };
2331
+
2332
+ // src/index.ts
2333
+ var PROTOCOL_VERSION = 1;
2334
+ export {
2335
+ ADAPTERS,
2336
+ Cursors,
2337
+ DEFAULT_CONFIG,
2338
+ FileSink,
2339
+ HOME,
2340
+ Outbox,
2341
+ PROTOCOL_VERSION,
2342
+ REDACTED,
2343
+ RULESET_PII,
2344
+ RULESET_SECRETS,
2345
+ StdoutSink,
2346
+ TYPE_TIER,
2347
+ adapterByName,
2348
+ antigravity,
2349
+ claudeCode,
2350
+ codex,
2351
+ configFile,
2352
+ filterEvent,
2353
+ geminiCli,
2354
+ gitFacts,
2355
+ hasSecret,
2356
+ hashMachine,
2357
+ isInjected,
2358
+ isSecretName,
2359
+ isUlid,
2360
+ machineName,
2361
+ makeEvent,
2362
+ pairTurns,
2363
+ readAntigravity,
2364
+ readClaude,
2365
+ readCodex,
2366
+ readConfig,
2367
+ readGemini,
2368
+ redactDeep,
2369
+ redactSecrets,
2370
+ reducePii,
2371
+ reducePiiDeep,
2372
+ safeId,
2373
+ stateDir,
2374
+ stripCredentials,
2375
+ takeLock,
2376
+ tilde,
2377
+ ulid,
2378
+ writeConfig
2379
+ };
2380
+ //# sourceMappingURL=index.js.map