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