evrex-mcp 0.7.0 → 0.8.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/README.md +11 -1
- package/dist/account.js +210 -0
- package/dist/capture.js +2592 -167
- package/dist/continuity.js +83 -10
- package/dist/hook.js +106 -11
- package/dist/import.js +2286 -350
- package/dist/index.js +223 -13
- package/dist/pretool.js +67 -16
- package/dist/tickets.js +24 -1
- package/package.json +18 -17
package/dist/capture.js
CHANGED
|
@@ -9,72 +9,82 @@ var __export = (target, all) => {
|
|
|
9
9
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
-
// src/
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
evrexApi: () => evrexApi
|
|
16
|
-
});
|
|
17
|
-
function headers() {
|
|
18
|
-
const base = { "Content-Type": "application/json" };
|
|
19
|
-
if (EVREX_TOKEN) base.Authorization = `Bearer ${EVREX_TOKEN}`;
|
|
20
|
-
return base;
|
|
12
|
+
// ../../packages/ingest-core/src/types.ts
|
|
13
|
+
function isReferenceKind(kind) {
|
|
14
|
+
return REFERENCE_KINDS.includes(kind ?? "");
|
|
21
15
|
}
|
|
22
|
-
function
|
|
23
|
-
|
|
24
|
-
return EVREX_TOKEN ? `${method} ${path} -> ${status}: the EVREX_TOKEN this server was started with was rejected. It may be revoked or expired.` : `${method} ${path} -> ${status}: this evrex backend requires a credential, but no EVREX_TOKEN is set for this MCP server.`;
|
|
25
|
-
}
|
|
26
|
-
return `${method} ${path} -> ${status} ${statusText}`;
|
|
16
|
+
function lacksFileAttribution(kind) {
|
|
17
|
+
return kind === "slack" || isReferenceKind(kind);
|
|
27
18
|
}
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
headers: headers(),
|
|
32
|
-
...body === void 0 ? {} : { body: JSON.stringify(body) }
|
|
33
|
-
});
|
|
34
|
-
if (res.status === 404 && absentIsAnswer) return null;
|
|
35
|
-
if (!res.ok) throw new Error(describeFailure(method, path, res.status, res.statusText));
|
|
36
|
-
return await res.json();
|
|
19
|
+
function provenanceTrailerFor(key) {
|
|
20
|
+
const k = key.toLowerCase();
|
|
21
|
+
return PROVENANCE_TRAILERS.find((t) => t.key.toLowerCase() === k);
|
|
37
22
|
}
|
|
38
|
-
var
|
|
39
|
-
var
|
|
40
|
-
"src/
|
|
23
|
+
var CONVERSATION_KINDS, REFERENCE_KINDS, SOURCE_KINDS, EMPTY_USAGE, PROVENANCE_TRAILERS, STATED_TRAILERS;
|
|
24
|
+
var init_types = __esm({
|
|
25
|
+
"../../packages/ingest-core/src/types.ts"() {
|
|
41
26
|
"use strict";
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
session
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
27
|
+
CONVERSATION_KINDS = [
|
|
28
|
+
"claude-code",
|
|
29
|
+
// Xcode's coding assistant — the Claude Agent SDK embedded, writing the
|
|
30
|
+
// identical transcript format under ~/Library/Developer/Xcode. A separate
|
|
31
|
+
// kind because a conversation in an IDE panel is not a CLI session, and the
|
|
32
|
+
// Slack mislabel already taught this list what an absent entry costs.
|
|
33
|
+
"claude-xcode",
|
|
34
|
+
"cursor",
|
|
35
|
+
"codex",
|
|
36
|
+
"gemini",
|
|
37
|
+
// OpenCode keeps its sessions in a SQLite store rather than files; the
|
|
38
|
+
// parser reads the store, so this kind arrives through the same importer
|
|
39
|
+
// as Cursor's.
|
|
40
|
+
"opencode",
|
|
41
|
+
// GitHub Copilot CLI writes an events file per session under ~/.copilot;
|
|
42
|
+
// the coding agent on github.com is a different thing and arrives as a
|
|
43
|
+
// reference through the Agent-Logs-Url trailer, not as this kind.
|
|
44
|
+
"copilot",
|
|
45
|
+
"slack"
|
|
46
|
+
];
|
|
47
|
+
REFERENCE_KINDS = ["linear", "jira", "confluence"];
|
|
48
|
+
SOURCE_KINDS = [
|
|
49
|
+
...CONVERSATION_KINDS,
|
|
50
|
+
...REFERENCE_KINDS
|
|
51
|
+
];
|
|
52
|
+
EMPTY_USAGE = {
|
|
53
|
+
inputTokens: null,
|
|
54
|
+
outputTokens: null,
|
|
55
|
+
cacheReadTokens: null,
|
|
56
|
+
cacheWriteTokens: null,
|
|
57
|
+
model: null
|
|
67
58
|
};
|
|
59
|
+
PROVENANCE_TRAILERS = [
|
|
60
|
+
// GitHub's Copilot coding agent stamps every commit it makes with a link to
|
|
61
|
+
// its session logs (changelog, 2026-03-20). Copilot exposes no lifecycle
|
|
62
|
+
// hooks evrex could capture through, so this is the only way one of its
|
|
63
|
+
// commits ever gets a session behind it.
|
|
64
|
+
{ key: "Agent-Logs-Url", agent: "copilot", label: "GitHub Copilot coding agent" },
|
|
65
|
+
// Entire's Checkpoints CLI: a checkpoint id, resolvable on entire.io or in
|
|
66
|
+
// the repo's own `refs/entire/checkpoints/` refs.
|
|
67
|
+
{ key: "Entire-Checkpoint", agent: "entire", label: "Entire checkpoint" },
|
|
68
|
+
// AgentsRoom's Commit Context: the whole conversation, as an unlisted gist.
|
|
69
|
+
{ key: "Agent-Conversation", agent: "agentsroom", label: "AgentsRoom conversation" }
|
|
70
|
+
];
|
|
71
|
+
STATED_TRAILERS = [
|
|
72
|
+
{ key: "Evrex-Rejected", kind: "rejected" },
|
|
73
|
+
{ key: "Evrex-Constraint", kind: "constraint" },
|
|
74
|
+
{ key: "Evrex-Decision", kind: "decision" }
|
|
75
|
+
];
|
|
68
76
|
}
|
|
69
77
|
});
|
|
70
78
|
|
|
71
|
-
// src/capture.ts
|
|
72
|
-
import { statSync as statSync3 } from "node:fs";
|
|
73
|
-
import { homedir as homedir2, hostname } from "node:os";
|
|
74
|
-
import { join as join2 } from "node:path";
|
|
75
|
-
|
|
76
79
|
// ../../packages/ingest-core/src/git-history.ts
|
|
77
80
|
import { execFileSync } from "node:child_process";
|
|
81
|
+
function git(repoPath, args, input) {
|
|
82
|
+
return execFileSync("git", args, {
|
|
83
|
+
cwd: repoPath,
|
|
84
|
+
maxBuffer: 1024 * 1024 * 64,
|
|
85
|
+
input
|
|
86
|
+
}).toString("utf-8");
|
|
87
|
+
}
|
|
78
88
|
function gitQuiet(repoPath, args) {
|
|
79
89
|
return execFileSync("git", args, {
|
|
80
90
|
cwd: repoPath,
|
|
@@ -116,6 +126,10 @@ function repoIdFromRootCommit(sha) {
|
|
|
116
126
|
function repoIdFromPath(repoPath) {
|
|
117
127
|
return `path:${repoPath}`;
|
|
118
128
|
}
|
|
129
|
+
function repoNameFromId(repoId) {
|
|
130
|
+
const withoutPrefix = repoId.replace(/^(remote|root|path):/, "");
|
|
131
|
+
return withoutPrefix.split("/").filter(Boolean).pop() ?? repoId;
|
|
132
|
+
}
|
|
119
133
|
function firstRemoteUrl(repoPath) {
|
|
120
134
|
try {
|
|
121
135
|
const origin = gitQuiet(repoPath, ["remote", "get-url", "origin"]).trim();
|
|
@@ -150,26 +164,268 @@ function deriveRepoId(repoPath) {
|
|
|
150
164
|
if (root) return repoIdFromRootCommit(root);
|
|
151
165
|
return repoIdFromPath(repoPath);
|
|
152
166
|
}
|
|
167
|
+
function getGitUserName(repoPath) {
|
|
168
|
+
try {
|
|
169
|
+
const name = git(repoPath, ["config", "user.name"]).trim();
|
|
170
|
+
return name || null;
|
|
171
|
+
} catch {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function listShas(repoPath) {
|
|
176
|
+
const out = git(repoPath, ["rev-list", "--reverse", "HEAD"]).trim();
|
|
177
|
+
return out.length ? out.split("\n") : [];
|
|
178
|
+
}
|
|
179
|
+
function commitMeta(repoPath, sha) {
|
|
180
|
+
const line = git(repoPath, [
|
|
181
|
+
"show",
|
|
182
|
+
"-s",
|
|
183
|
+
`--format=%H${FIELD_SEP}%an${FIELD_SEP}%ae${FIELD_SEP}%aI`,
|
|
184
|
+
sha
|
|
185
|
+
]).trim();
|
|
186
|
+
const parts = line.split(FIELD_SEP);
|
|
187
|
+
const message = git(repoPath, ["show", "-s", "--format=%B", sha]).replace(/\n+$/, "");
|
|
188
|
+
return {
|
|
189
|
+
sha: parts[0] ?? sha,
|
|
190
|
+
author: parts[1] ?? "",
|
|
191
|
+
authorEmail: parts[2] ?? "",
|
|
192
|
+
ts: parts[3] ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
193
|
+
message
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function parseTrailers(repoPath, message) {
|
|
197
|
+
try {
|
|
198
|
+
const out = git(
|
|
199
|
+
repoPath,
|
|
200
|
+
["interpret-trailers", "--parse", "--no-divider"],
|
|
201
|
+
message
|
|
202
|
+
).trim();
|
|
203
|
+
const trailers = [];
|
|
204
|
+
for (const line of out.split("\n")) {
|
|
205
|
+
const idx = line.indexOf(":");
|
|
206
|
+
if (idx === -1) continue;
|
|
207
|
+
const key = line.slice(0, idx).trim();
|
|
208
|
+
const value = line.slice(idx + 1).trim();
|
|
209
|
+
if (key && value) trailers.push({ key, value });
|
|
210
|
+
}
|
|
211
|
+
return trailers;
|
|
212
|
+
} catch {
|
|
213
|
+
return [];
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function trailersFromMessage(message) {
|
|
217
|
+
const paragraphs = message.replace(/\r\n/g, "\n").trim().split(/\n{2,}/);
|
|
218
|
+
const last = paragraphs[paragraphs.length - 1] ?? "";
|
|
219
|
+
if (paragraphs.length < 2) return [];
|
|
220
|
+
const lines = last.split("\n");
|
|
221
|
+
const trailers = [];
|
|
222
|
+
for (const line of lines) {
|
|
223
|
+
const m = /^([A-Za-z][A-Za-z0-9-]*):\s+(.+?)\s*$/.exec(line);
|
|
224
|
+
if (!m) return [];
|
|
225
|
+
trailers.push({ key: m[1], value: m[2] });
|
|
226
|
+
}
|
|
227
|
+
return trailers;
|
|
228
|
+
}
|
|
229
|
+
function statedInsightsOf(trailers) {
|
|
230
|
+
const out = [];
|
|
231
|
+
for (const t of trailers) {
|
|
232
|
+
const known = STATED_TRAILERS.find((s) => s.key.toLowerCase() === t.key.toLowerCase());
|
|
233
|
+
if (known && t.value.trim()) out.push({ kind: known.kind, text: t.value.trim() });
|
|
234
|
+
}
|
|
235
|
+
return out;
|
|
236
|
+
}
|
|
237
|
+
function trailerValue(trailers, key) {
|
|
238
|
+
const k = key.toLowerCase();
|
|
239
|
+
return trailers.find((t) => t.key.toLowerCase() === k)?.value ?? null;
|
|
240
|
+
}
|
|
241
|
+
function agentTrailersOf(trailers) {
|
|
242
|
+
const out = [];
|
|
243
|
+
for (const t of trailers) {
|
|
244
|
+
const known = provenanceTrailerFor(t.key);
|
|
245
|
+
if (known) out.push({ key: known.key, value: t.value, agent: known.agent });
|
|
246
|
+
}
|
|
247
|
+
return out;
|
|
248
|
+
}
|
|
249
|
+
function commitBranch(repoPath, sha) {
|
|
250
|
+
try {
|
|
251
|
+
const out = git(repoPath, ["branch", "--contains", sha, "--format=%(refname:short)"]).trim();
|
|
252
|
+
const first = out.split("\n").find((b) => b.length > 0);
|
|
253
|
+
return first ?? null;
|
|
254
|
+
} catch {
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
function numstat(repoPath, sha) {
|
|
259
|
+
const out = git(repoPath, ["show", "--numstat", "--format=", sha]).trim();
|
|
260
|
+
const map = /* @__PURE__ */ new Map();
|
|
261
|
+
if (!out) return map;
|
|
262
|
+
for (const line of out.split("\n")) {
|
|
263
|
+
const [ins, del, path] = line.split(" ");
|
|
264
|
+
if (!path) continue;
|
|
265
|
+
map.set(path, {
|
|
266
|
+
insertions: !ins || ins === "-" ? 0 : Number.parseInt(ins, 10) || 0,
|
|
267
|
+
deletions: !del || del === "-" ? 0 : Number.parseInt(del, 10) || 0
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
return map;
|
|
271
|
+
}
|
|
272
|
+
function parseDiffBlocks(diff) {
|
|
273
|
+
const map = /* @__PURE__ */ new Map();
|
|
274
|
+
if (!diff.trim()) return map;
|
|
275
|
+
const blocks = diff.split(/^diff --git /m).filter(Boolean);
|
|
276
|
+
for (const block of blocks) {
|
|
277
|
+
const full = `diff --git ${block}`;
|
|
278
|
+
const pathMatch = block.match(/^a\/(.+?) b\/(.+?)\n/);
|
|
279
|
+
const path = pathMatch?.[2] ?? block.match(/^\S+/)?.[0] ?? "unknown";
|
|
280
|
+
const hunkHeaders = [...full.matchAll(/^@@ .+? @@.*$/gm)].map((m) => m[0]);
|
|
281
|
+
map.set(path, {
|
|
282
|
+
hunkHeaders,
|
|
283
|
+
diffText: full.length > DIFF_CAP ? full.slice(0, DIFF_CAP) : full
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
return map;
|
|
287
|
+
}
|
|
288
|
+
function commitFiles(repoPath, sha) {
|
|
289
|
+
const stats = numstat(repoPath, sha);
|
|
290
|
+
const diff = git(repoPath, ["show", "--unified=0", "--format=", sha]);
|
|
291
|
+
const blocks = parseDiffBlocks(diff);
|
|
292
|
+
const paths = /* @__PURE__ */ new Set([...stats.keys(), ...blocks.keys()]);
|
|
293
|
+
const files = [];
|
|
294
|
+
for (const path of paths) {
|
|
295
|
+
const stat = stats.get(path) ?? { insertions: 0, deletions: 0 };
|
|
296
|
+
const block = blocks.get(path) ?? { hunkHeaders: [], diffText: "" };
|
|
297
|
+
files.push({
|
|
298
|
+
path,
|
|
299
|
+
insertions: stat.insertions,
|
|
300
|
+
deletions: stat.deletions,
|
|
301
|
+
hunkHeaders: block.hunkHeaders,
|
|
302
|
+
diffText: block.diffText,
|
|
303
|
+
diffTruncated: block.diffText.length >= DIFF_CAP
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
return files;
|
|
307
|
+
}
|
|
308
|
+
function parseGitLog(repoPath, repoId, known) {
|
|
309
|
+
const shas = listShas(repoPath).filter((sha) => !known?.has(sha));
|
|
310
|
+
const id = repoId ?? deriveRepoId(repoPath);
|
|
311
|
+
return shas.map((sha) => {
|
|
312
|
+
const meta = commitMeta(repoPath, sha);
|
|
313
|
+
const trailers = parseTrailers(repoPath, meta.message);
|
|
314
|
+
return {
|
|
315
|
+
sha: meta.sha,
|
|
316
|
+
repoId: id,
|
|
317
|
+
repoPath,
|
|
318
|
+
author: meta.author,
|
|
319
|
+
authorEmail: meta.authorEmail,
|
|
320
|
+
ts: meta.ts,
|
|
321
|
+
message: meta.message,
|
|
322
|
+
branch: commitBranch(repoPath, sha),
|
|
323
|
+
evrexSessionTrailer: trailerValue(trailers, EVREX_SESSION_TRAILER_KEY),
|
|
324
|
+
agentTrailers: agentTrailersOf(trailers),
|
|
325
|
+
statedInsights: statedInsightsOf(trailers),
|
|
326
|
+
files: commitFiles(repoPath, sha)
|
|
327
|
+
};
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
var DIFF_CAP, FIELD_SEP, EVREX_SESSION_TRAILER_KEY;
|
|
331
|
+
var init_git_history = __esm({
|
|
332
|
+
"../../packages/ingest-core/src/git-history.ts"() {
|
|
333
|
+
"use strict";
|
|
334
|
+
init_types();
|
|
335
|
+
DIFF_CAP = 2e4;
|
|
336
|
+
FIELD_SEP = "";
|
|
337
|
+
EVREX_SESSION_TRAILER_KEY = "Evrex-Session";
|
|
338
|
+
}
|
|
339
|
+
});
|
|
153
340
|
|
|
154
|
-
// ../../packages/ingest-core/src/
|
|
155
|
-
import {
|
|
341
|
+
// ../../packages/ingest-core/src/incremental.ts
|
|
342
|
+
import { statSync } from "node:fs";
|
|
343
|
+
function planRead(path, cursor) {
|
|
344
|
+
let size;
|
|
345
|
+
let modifiedAt;
|
|
346
|
+
if (cursor && (cursor.parserVersion ?? 0) < PARSER_VERSION) {
|
|
347
|
+
cursor = void 0;
|
|
348
|
+
}
|
|
349
|
+
try {
|
|
350
|
+
const stat = statSync(path);
|
|
351
|
+
size = stat.size;
|
|
352
|
+
modifiedAt = stat.mtimeMs;
|
|
353
|
+
} catch {
|
|
354
|
+
return { path, from: 0, reason: "unchanged" };
|
|
355
|
+
}
|
|
356
|
+
if (!cursor) return { path, from: 0, reason: "new" };
|
|
357
|
+
if (size < cursor.size) return { path, from: 0, reason: "rewritten" };
|
|
358
|
+
if (size === cursor.size && modifiedAt <= cursor.modifiedAt) {
|
|
359
|
+
return { path, from: cursor.offset, reason: "unchanged" };
|
|
360
|
+
}
|
|
361
|
+
if (size === cursor.offset) return { path, from: cursor.offset, reason: "unchanged" };
|
|
362
|
+
return { path, from: cursor.offset, reason: "appended" };
|
|
363
|
+
}
|
|
364
|
+
function advanceCursor(path, consumedTo) {
|
|
365
|
+
let size = consumedTo;
|
|
366
|
+
let modifiedAt = Date.now();
|
|
367
|
+
try {
|
|
368
|
+
const stat = statSync(path);
|
|
369
|
+
size = stat.size;
|
|
370
|
+
modifiedAt = stat.mtimeMs;
|
|
371
|
+
} catch {
|
|
372
|
+
}
|
|
373
|
+
return { offset: consumedTo, size, modifiedAt, parserVersion: PARSER_VERSION };
|
|
374
|
+
}
|
|
375
|
+
function splitCompleteLines(chunk) {
|
|
376
|
+
const lastNewline = chunk.lastIndexOf("\n");
|
|
377
|
+
if (lastNewline === -1) return { lines: [], consumedBytes: 0 };
|
|
378
|
+
const complete = chunk.slice(0, lastNewline);
|
|
379
|
+
return {
|
|
380
|
+
lines: complete.split("\n").filter((l) => l.trim().length > 0),
|
|
381
|
+
consumedBytes: Buffer.byteLength(complete, "utf-8") + 1
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
function sessionSignature(session) {
|
|
385
|
+
const last = session.turns[session.turns.length - 1]?.ts ?? session.endedAt ?? "";
|
|
386
|
+
return `v${PARSER_VERSION}:${session.turnCount}:${last}`;
|
|
387
|
+
}
|
|
388
|
+
function changedSessions(sessions, known) {
|
|
389
|
+
const signatures = {};
|
|
390
|
+
const changed = [];
|
|
391
|
+
for (const session of sessions) {
|
|
392
|
+
const signature = sessionSignature(session);
|
|
393
|
+
signatures[session.id] = signature;
|
|
394
|
+
if (known[session.id] !== signature) changed.push(session);
|
|
395
|
+
}
|
|
396
|
+
return { changed, signatures };
|
|
397
|
+
}
|
|
398
|
+
var PARSER_VERSION;
|
|
399
|
+
var init_incremental = __esm({
|
|
400
|
+
"../../packages/ingest-core/src/incremental.ts"() {
|
|
401
|
+
"use strict";
|
|
402
|
+
PARSER_VERSION = 4;
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// ../../packages/ingest-core/src/derive-uuid.ts
|
|
407
|
+
import { createHash } from "node:crypto";
|
|
408
|
+
function deriveUuid(name) {
|
|
409
|
+
const h = createHash("sha1").update(name).digest("hex");
|
|
410
|
+
const variant = (parseInt(h.slice(16, 17) || "0", 16) & 3 | 8).toString(16);
|
|
411
|
+
const s = h.slice(0, 12) + // time-low + time-mid
|
|
412
|
+
"5" + // version 5 (name-based, SHA-1)
|
|
413
|
+
h.slice(13, 16) + variant + h.slice(17, 32);
|
|
414
|
+
return `${s.slice(0, 8)}-${s.slice(8, 12)}-${s.slice(12, 16)}-${s.slice(16, 20)}-${s.slice(20, 32)}`;
|
|
415
|
+
}
|
|
416
|
+
function deriveSlackSessionId(channel, threadTs) {
|
|
417
|
+
return deriveUuid(`evrex-slack-session ${channel} ${threadTs}`);
|
|
418
|
+
}
|
|
419
|
+
function deriveSlackTurnId(channel, messageTs) {
|
|
420
|
+
return deriveUuid(`evrex-slack-turn ${channel} ${messageTs}`);
|
|
421
|
+
}
|
|
422
|
+
var init_derive_uuid = __esm({
|
|
423
|
+
"../../packages/ingest-core/src/derive-uuid.ts"() {
|
|
424
|
+
"use strict";
|
|
425
|
+
}
|
|
426
|
+
});
|
|
156
427
|
|
|
157
428
|
// ../../packages/ingest-core/src/redact.ts
|
|
158
|
-
var PATTERNS = [
|
|
159
|
-
{ type: "private_key", regex: /-----BEGIN(?: [A-Z]+)? PRIVATE KEY-----[\s\S]*?-----END(?: [A-Z]+)? PRIVATE KEY-----/g },
|
|
160
|
-
{ type: "aws_access_key", regex: /AKIA[0-9A-Z]{16}/g },
|
|
161
|
-
{ type: "github_token", regex: /gh[pousr]_[A-Za-z0-9]{36,}/g },
|
|
162
|
-
{ type: "slack_token", regex: /xox[baprs]-[A-Za-z0-9-]{10,}/g },
|
|
163
|
-
{ type: "stripe_key", regex: /sk_(live|test)_[A-Za-z0-9]{24,}/g },
|
|
164
|
-
{ type: "openai_key", regex: /sk-[A-Za-z0-9]{20,}/g },
|
|
165
|
-
{ type: "anthropic_key", regex: /sk-ant-[A-Za-z0-9-_]{20,}/g },
|
|
166
|
-
{ type: "jwt", regex: /eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g },
|
|
167
|
-
{ type: "bearer_token", regex: /Bearer\s+[A-Za-z0-9\-_.]{20,}/g },
|
|
168
|
-
{
|
|
169
|
-
type: "env_secret",
|
|
170
|
-
regex: /\b([A-Z0-9_]*(?:_KEY|_SECRET|_TOKEN|_PASSWORD|PASSWD)[A-Z0-9_]*)\s*[:=]\s*["']?[^\s"'\n]{6,}["']?/g
|
|
171
|
-
}
|
|
172
|
-
];
|
|
173
429
|
function redactJsonValue(value) {
|
|
174
430
|
let count = 0;
|
|
175
431
|
const walk = (v) => {
|
|
@@ -190,7 +446,31 @@ function redactJsonValue(value) {
|
|
|
190
446
|
};
|
|
191
447
|
return { value: walk(value), count };
|
|
192
448
|
}
|
|
193
|
-
|
|
449
|
+
function redactRawTranscript(content, format) {
|
|
450
|
+
let count = 0;
|
|
451
|
+
if (format === "jsonl") {
|
|
452
|
+
const out = content.split("\n").map((line) => {
|
|
453
|
+
if (!line.trim()) return line;
|
|
454
|
+
try {
|
|
455
|
+
const r = redactJsonValue(JSON.parse(line));
|
|
456
|
+
count += r.count;
|
|
457
|
+
return JSON.stringify(r.value);
|
|
458
|
+
} catch {
|
|
459
|
+
const r = redactSecrets(line);
|
|
460
|
+
count += r.count;
|
|
461
|
+
return r.text;
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
return { content: out.join("\n"), count };
|
|
465
|
+
}
|
|
466
|
+
try {
|
|
467
|
+
const r = redactJsonValue(JSON.parse(content));
|
|
468
|
+
return { content: JSON.stringify(r.value), count: r.count };
|
|
469
|
+
} catch {
|
|
470
|
+
const r = redactSecrets(content);
|
|
471
|
+
return { content: r.text, count: r.count };
|
|
472
|
+
}
|
|
473
|
+
}
|
|
194
474
|
function redactSecrets(input) {
|
|
195
475
|
let text = input;
|
|
196
476
|
let count = 0;
|
|
@@ -209,8 +489,74 @@ function redactSecrets(input) {
|
|
|
209
489
|
}
|
|
210
490
|
return { text, count };
|
|
211
491
|
}
|
|
492
|
+
var PATTERNS, PRIVATE_BLOCK;
|
|
493
|
+
var init_redact = __esm({
|
|
494
|
+
"../../packages/ingest-core/src/redact.ts"() {
|
|
495
|
+
"use strict";
|
|
496
|
+
PATTERNS = [
|
|
497
|
+
{ type: "private_key", regex: /-----BEGIN(?: [A-Z]+)? PRIVATE KEY-----[\s\S]*?-----END(?: [A-Z]+)? PRIVATE KEY-----/g },
|
|
498
|
+
{ type: "aws_access_key", regex: /AKIA[0-9A-Z]{16}/g },
|
|
499
|
+
{ type: "github_token", regex: /gh[pousr]_[A-Za-z0-9]{36,}/g },
|
|
500
|
+
{ type: "slack_token", regex: /xox[baprs]-[A-Za-z0-9-]{10,}/g },
|
|
501
|
+
{ type: "stripe_key", regex: /sk_(live|test)_[A-Za-z0-9]{24,}/g },
|
|
502
|
+
{ type: "openai_key", regex: /sk-[A-Za-z0-9]{20,}/g },
|
|
503
|
+
{ type: "anthropic_key", regex: /sk-ant-[A-Za-z0-9-_]{20,}/g },
|
|
504
|
+
{ type: "jwt", regex: /eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g },
|
|
505
|
+
{ type: "bearer_token", regex: /Bearer\s+[A-Za-z0-9\-_.]{20,}/g },
|
|
506
|
+
{
|
|
507
|
+
type: "env_secret",
|
|
508
|
+
regex: /\b([A-Z0-9_]*(?:_KEY|_SECRET|_TOKEN|_PASSWORD|PASSWD)[A-Z0-9_]*)\s*[:=]\s*["']?[^\s"'\n]{6,}["']?/g
|
|
509
|
+
}
|
|
510
|
+
];
|
|
511
|
+
PRIVATE_BLOCK = /<private>[\s\S]*?(?:<\/private>|$)/gi;
|
|
512
|
+
}
|
|
513
|
+
});
|
|
212
514
|
|
|
213
515
|
// ../../packages/ingest-core/src/subject-linking.ts
|
|
516
|
+
function matchCommitToSession(commit, sessions) {
|
|
517
|
+
const subject = commit.subject.trim();
|
|
518
|
+
if (subject.length === 0) {
|
|
519
|
+
return { sha: commit.sha, sessionId: null, reason: "absent" };
|
|
520
|
+
}
|
|
521
|
+
const ran = sessions.filter((s) => s.committedSubjects.includes(subject));
|
|
522
|
+
if (ran.length === 0) {
|
|
523
|
+
return { sha: commit.sha, sessionId: null, reason: "absent" };
|
|
524
|
+
}
|
|
525
|
+
if (ran.length === 1) {
|
|
526
|
+
return {
|
|
527
|
+
sha: commit.sha,
|
|
528
|
+
sessionId: ran[0].sessionId,
|
|
529
|
+
evidence: "ran-the-commit",
|
|
530
|
+
confidence: INFERRED_CONFIDENCE
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
const oneLineage = ran.every((s) => s.startedAt === ran[0].startedAt);
|
|
534
|
+
if (!oneLineage) {
|
|
535
|
+
return { sha: commit.sha, sessionId: null, reason: "ambiguous" };
|
|
536
|
+
}
|
|
537
|
+
const original = [...ran].sort((a, b) => a.endedAt - b.endedAt)[0];
|
|
538
|
+
return {
|
|
539
|
+
sha: commit.sha,
|
|
540
|
+
sessionId: original.sessionId,
|
|
541
|
+
evidence: "ran-the-commit-then-lineage",
|
|
542
|
+
confidence: INFERRED_CONFIDENCE
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
function proposeLinks(commits, sessions) {
|
|
546
|
+
const out = [];
|
|
547
|
+
for (const commit of commits) {
|
|
548
|
+
const match = matchCommitToSession(commit, sessions);
|
|
549
|
+
if (match.sessionId !== null) {
|
|
550
|
+
out.push({
|
|
551
|
+
sha: match.sha,
|
|
552
|
+
sessionId: match.sessionId,
|
|
553
|
+
confidence: match.confidence,
|
|
554
|
+
evidence: match.evidence
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
return out;
|
|
559
|
+
}
|
|
214
560
|
function committedSubjects(command) {
|
|
215
561
|
const out = [];
|
|
216
562
|
const invocation = /\bgit\s+(?:-C\s+\S+\s+)?commit\b/g;
|
|
@@ -230,30 +576,18 @@ function committedSubjects(command) {
|
|
|
230
576
|
}
|
|
231
577
|
return out;
|
|
232
578
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
"slack"
|
|
241
|
-
];
|
|
242
|
-
var REFERENCE_KINDS = ["linear", "jira", "confluence"];
|
|
243
|
-
var SOURCE_KINDS = [
|
|
244
|
-
...CONVERSATION_KINDS,
|
|
245
|
-
...REFERENCE_KINDS
|
|
246
|
-
];
|
|
247
|
-
var EMPTY_USAGE = {
|
|
248
|
-
inputTokens: null,
|
|
249
|
-
outputTokens: null,
|
|
250
|
-
cacheReadTokens: null,
|
|
251
|
-
cacheWriteTokens: null,
|
|
252
|
-
model: null
|
|
253
|
-
};
|
|
579
|
+
var INFERRED_CONFIDENCE;
|
|
580
|
+
var init_subject_linking = __esm({
|
|
581
|
+
"../../packages/ingest-core/src/subject-linking.ts"() {
|
|
582
|
+
"use strict";
|
|
583
|
+
INFERRED_CONFIDENCE = 0.9;
|
|
584
|
+
}
|
|
585
|
+
});
|
|
254
586
|
|
|
255
587
|
// ../../packages/ingest-core/src/claude-sessions.ts
|
|
256
|
-
|
|
588
|
+
import { closeSync, existsSync, openSync, readdirSync, readFileSync, readSync, statSync as statSync2 } from "node:fs";
|
|
589
|
+
import { homedir } from "node:os";
|
|
590
|
+
import { join } from "node:path";
|
|
257
591
|
function meaningfulLines(lines) {
|
|
258
592
|
return lines.map((l) => l.trim()).filter((l) => l.length >= MIN_MEANINGFUL_LINE_LENGTH);
|
|
259
593
|
}
|
|
@@ -280,9 +614,56 @@ function extractEditedLines(toolUseResult) {
|
|
|
280
614
|
if (meaningfulAdded.length === 0 && meaningfulRemoved.length === 0) return null;
|
|
281
615
|
return { path: r.filePath, added: meaningfulAdded, removed: meaningfulRemoved };
|
|
282
616
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
617
|
+
function slugifyCwd(repoPath) {
|
|
618
|
+
return repoPath.replace(/[^A-Za-z0-9]/g, "-");
|
|
619
|
+
}
|
|
620
|
+
function claudeProjectsDir() {
|
|
621
|
+
return join(homedir(), ".claude", "projects");
|
|
622
|
+
}
|
|
623
|
+
function xcodeAssistantProjectsDir() {
|
|
624
|
+
return join(
|
|
625
|
+
homedir(),
|
|
626
|
+
"Library",
|
|
627
|
+
"Developer",
|
|
628
|
+
"Xcode",
|
|
629
|
+
"CodingAssistant",
|
|
630
|
+
"ClaudeAgentConfig",
|
|
631
|
+
"projects"
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
function findSessionFiles(repoPath) {
|
|
635
|
+
const roots = [
|
|
636
|
+
{ dir: claudeProjectsDir(), agentKind: "claude-code" },
|
|
637
|
+
{ dir: xcodeAssistantProjectsDir(), agentKind: "claude-xcode" }
|
|
638
|
+
];
|
|
639
|
+
const out = [];
|
|
640
|
+
for (const { dir, agentKind } of roots) {
|
|
641
|
+
const slug = join(dir, slugifyCwd(repoPath));
|
|
642
|
+
if (!existsSync(slug)) continue;
|
|
643
|
+
for (const name of readdirSync(slug)) {
|
|
644
|
+
if (name.endsWith(".jsonl")) out.push({ file: join(slug, name), agentKind });
|
|
645
|
+
const subagents = join(slug, name, "subagents");
|
|
646
|
+
if (!name.endsWith(".jsonl") && existsSync(subagents)) {
|
|
647
|
+
for (const child of readdirSync(subagents)) {
|
|
648
|
+
if (child.endsWith(".jsonl")) out.push({ file: join(subagents, child), agentKind });
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
return out;
|
|
654
|
+
}
|
|
655
|
+
function editFromXcodeInput(input) {
|
|
656
|
+
const filePath = input.filePath;
|
|
657
|
+
if (typeof filePath !== "string" || !filePath) return null;
|
|
658
|
+
const oldLines = typeof input.oldString === "string" ? input.oldString.split("\n") : [];
|
|
659
|
+
const newLines = typeof input.newString === "string" ? input.newString.split("\n") : [];
|
|
660
|
+
const oldSet = new Set(oldLines);
|
|
661
|
+
const newSet = new Set(newLines);
|
|
662
|
+
const added = meaningfulLines(newLines.filter((l) => !oldSet.has(l)));
|
|
663
|
+
const removed = meaningfulLines(oldLines.filter((l) => !newSet.has(l)));
|
|
664
|
+
if (added.length === 0 && removed.length === 0) return null;
|
|
665
|
+
return { path: filePath, added, removed };
|
|
666
|
+
}
|
|
286
667
|
function extractPathsFromText(text) {
|
|
287
668
|
const matches = text.match(PATH_TOKEN_RE) ?? [];
|
|
288
669
|
return [...new Set(matches)].filter((p) => p.length > 3 && p.length < 300);
|
|
@@ -297,6 +678,7 @@ function blockText(content) {
|
|
|
297
678
|
function extractFromAssistantContent(content) {
|
|
298
679
|
const textParts = [];
|
|
299
680
|
const filesTouched = [];
|
|
681
|
+
const editedLines = [];
|
|
300
682
|
for (const block of content) {
|
|
301
683
|
if (block.type === "text" && block.text) {
|
|
302
684
|
textParts.push(block.text);
|
|
@@ -306,7 +688,15 @@ function extractFromAssistantContent(content) {
|
|
|
306
688
|
} else if (block.type === "tool_use") {
|
|
307
689
|
const name = block.name ?? "tool";
|
|
308
690
|
const input = block.input ?? {};
|
|
309
|
-
if (
|
|
691
|
+
if (XCODE_EDIT_TOOL.test(name) && typeof input.filePath === "string") {
|
|
692
|
+
textParts.push(`[tool_call: ${name}] ${input.filePath}`);
|
|
693
|
+
filesTouched.push({ path: input.filePath, source: "tool_path", tool: name });
|
|
694
|
+
const edit = editFromXcodeInput(input);
|
|
695
|
+
if (edit) editedLines.push(edit);
|
|
696
|
+
} else if (XCODE_PATH_TOOLS.test(name) && typeof input.filePath === "string") {
|
|
697
|
+
textParts.push(`[tool_call: ${name}] ${input.filePath}`);
|
|
698
|
+
filesTouched.push({ path: input.filePath, source: "tool_path", tool: name });
|
|
699
|
+
} else if (FILE_PATH_TOOLS.has(name) && typeof input.file_path === "string") {
|
|
310
700
|
textParts.push(`[tool_call: ${name}] ${input.file_path}`);
|
|
311
701
|
filesTouched.push({ path: input.file_path, source: "tool_path", tool: name });
|
|
312
702
|
} else if (name === "Bash" && typeof input.command === "string") {
|
|
@@ -320,9 +710,8 @@ function extractFromAssistantContent(content) {
|
|
|
320
710
|
}
|
|
321
711
|
}
|
|
322
712
|
}
|
|
323
|
-
return { text: textParts.join("\n"), filesTouched };
|
|
713
|
+
return { text: textParts.join("\n"), filesTouched, editedLines };
|
|
324
714
|
}
|
|
325
|
-
var SYNTHETIC_CONTENT_RE = /^\s*(<task-notification>|<system-reminder>|\[SYSTEM NOTIFICATION)/;
|
|
326
715
|
function extractFromUserContent(content) {
|
|
327
716
|
if (typeof content === "string") {
|
|
328
717
|
return {
|
|
@@ -334,9 +723,11 @@ function extractFromUserContent(content) {
|
|
|
334
723
|
if (Array.isArray(content)) {
|
|
335
724
|
const toolParts = [];
|
|
336
725
|
const textParts = [];
|
|
726
|
+
let isToolError = false;
|
|
337
727
|
for (const block of content) {
|
|
338
728
|
if (block.type === "tool_result") {
|
|
339
729
|
toolParts.push(blockText(block.content));
|
|
730
|
+
if (block.is_error === true) isToolError = true;
|
|
340
731
|
} else if (block.type === "text" && typeof block.text === "string") {
|
|
341
732
|
textParts.push(block.text);
|
|
342
733
|
}
|
|
@@ -345,7 +736,8 @@ function extractFromUserContent(content) {
|
|
|
345
736
|
return {
|
|
346
737
|
text: toolParts.join("\n"),
|
|
347
738
|
filesTouched: [],
|
|
348
|
-
isSyntheticInput: true
|
|
739
|
+
isSyntheticInput: true,
|
|
740
|
+
isToolError
|
|
349
741
|
};
|
|
350
742
|
}
|
|
351
743
|
const text = textParts.join("\n");
|
|
@@ -381,8 +773,35 @@ function usageOnce(record, billed) {
|
|
|
381
773
|
model: typeof message?.model === "string" ? message.model : null
|
|
382
774
|
};
|
|
383
775
|
}
|
|
384
|
-
function
|
|
385
|
-
const
|
|
776
|
+
function readTranscript(filePath) {
|
|
777
|
+
const size = statSync2(filePath).size;
|
|
778
|
+
if (size <= MAX_TRANSCRIPT_BYTES) {
|
|
779
|
+
return { text: readFileSync(filePath, "utf-8"), capped: false };
|
|
780
|
+
}
|
|
781
|
+
const fd = openSync(filePath, "r");
|
|
782
|
+
try {
|
|
783
|
+
const buf = Buffer.alloc(MAX_TRANSCRIPT_BYTES);
|
|
784
|
+
readSync(fd, buf, 0, MAX_TRANSCRIPT_BYTES, size - MAX_TRANSCRIPT_BYTES);
|
|
785
|
+
const text = buf.toString("utf-8");
|
|
786
|
+
return { text: text.slice(text.indexOf("\n") + 1), capped: true };
|
|
787
|
+
} finally {
|
|
788
|
+
closeSync(fd);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
function readSubagentMeta(transcriptPath) {
|
|
792
|
+
const metaPath = transcriptPath.replace(/\.jsonl$/, ".meta.json");
|
|
793
|
+
try {
|
|
794
|
+
const raw = JSON.parse(readFileSync(metaPath, "utf-8"));
|
|
795
|
+
return {
|
|
796
|
+
agentType: typeof raw.agentType === "string" ? raw.agentType : null,
|
|
797
|
+
description: typeof raw.description === "string" ? raw.description : null
|
|
798
|
+
};
|
|
799
|
+
} catch {
|
|
800
|
+
return null;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
function parseSessionFile(filePath, repoPath, repoId = deriveRepoId(repoPath), agentKind = "claude-code") {
|
|
804
|
+
const { text: raw, capped } = readTranscript(filePath);
|
|
386
805
|
const lines = raw.split("\n").filter((l) => l.trim().length > 0);
|
|
387
806
|
const turns = [];
|
|
388
807
|
const billedMessages = /* @__PURE__ */ new Set();
|
|
@@ -390,6 +809,8 @@ function parseSessionFile(filePath, repoPath, repoId = deriveRepoId(repoPath)) {
|
|
|
390
809
|
const cwd = repoPath;
|
|
391
810
|
let aiTitle = null;
|
|
392
811
|
let totalRedactions = 0;
|
|
812
|
+
let branch = null;
|
|
813
|
+
let agentId = null;
|
|
393
814
|
for (const line of lines) {
|
|
394
815
|
let record;
|
|
395
816
|
try {
|
|
@@ -406,22 +827,27 @@ function parseSessionFile(filePath, repoPath, repoId = deriveRepoId(repoPath)) {
|
|
|
406
827
|
const ts = record.timestamp;
|
|
407
828
|
if (!id || !ts) continue;
|
|
408
829
|
sessionId ??= record.sessionId ?? record.session_id ?? null;
|
|
830
|
+
if (typeof record.gitBranch === "string" && record.gitBranch) branch = record.gitBranch;
|
|
831
|
+
if (typeof record.agentId === "string" && record.agentId) agentId ??= record.agentId;
|
|
409
832
|
let text = "";
|
|
410
833
|
let filesTouched = [];
|
|
411
834
|
let editedLines = [];
|
|
412
835
|
let isSyntheticInput = false;
|
|
836
|
+
let isToolError = false;
|
|
413
837
|
if (record.type === "assistant") {
|
|
414
838
|
const content = record.message?.content;
|
|
415
839
|
if (Array.isArray(content)) {
|
|
416
840
|
const extracted = extractFromAssistantContent(content);
|
|
417
841
|
text = extracted.text;
|
|
418
842
|
filesTouched = extracted.filesTouched;
|
|
843
|
+
editedLines = extracted.editedLines;
|
|
419
844
|
}
|
|
420
845
|
} else {
|
|
421
846
|
const extracted = extractFromUserContent(record.message?.content);
|
|
422
847
|
text = extracted.text;
|
|
423
848
|
filesTouched = extracted.filesTouched;
|
|
424
849
|
isSyntheticInput = extracted.isSyntheticInput;
|
|
850
|
+
isToolError = extracted.isToolError ?? false;
|
|
425
851
|
const edited = extractEditedLines(record.toolUseResult);
|
|
426
852
|
if (edited) editedLines = [edited];
|
|
427
853
|
}
|
|
@@ -439,12 +865,21 @@ function parseSessionFile(filePath, repoPath, repoId = deriveRepoId(repoPath)) {
|
|
|
439
865
|
isSidechain: Boolean(record.isSidechain),
|
|
440
866
|
redacted: redacted.count > 0,
|
|
441
867
|
isSyntheticInput,
|
|
868
|
+
isToolError,
|
|
442
869
|
usage: usageOnce(record, billedMessages)
|
|
443
870
|
});
|
|
444
871
|
}
|
|
445
872
|
if (!sessionId || turns.length === 0) return null;
|
|
873
|
+
const parentSessionId = agentId ? sessionId : null;
|
|
874
|
+
if (agentId) {
|
|
875
|
+
sessionId = deriveUuid(`evrex-subagent\0${parentSessionId}\0${agentId}`);
|
|
876
|
+
for (const t of turns) t.sessionId = sessionId;
|
|
877
|
+
}
|
|
878
|
+
const meta = agentId ? readSubagentMeta(filePath) : null;
|
|
879
|
+
const subagent = agentId ? { agentId, agentType: meta?.agentType ?? null, description: meta?.description ?? null } : null;
|
|
880
|
+
if (agentId && !aiTitle && meta?.description) aiTitle = meta.description;
|
|
446
881
|
const sortedTs = turns.map((t) => t.ts).sort();
|
|
447
|
-
const rawContent = lines.map((line) => {
|
|
882
|
+
const rawContent = capped ? "" : lines.map((line) => {
|
|
448
883
|
try {
|
|
449
884
|
return JSON.stringify(redactJsonValue(JSON.parse(line)).value);
|
|
450
885
|
} catch {
|
|
@@ -456,7 +891,7 @@ function parseSessionFile(filePath, repoPath, repoId = deriveRepoId(repoPath)) {
|
|
|
456
891
|
}
|
|
457
892
|
return {
|
|
458
893
|
id: sessionId,
|
|
459
|
-
agentKind
|
|
894
|
+
agentKind,
|
|
460
895
|
repoId,
|
|
461
896
|
cwd,
|
|
462
897
|
startedAt: sortedTs[0] ?? null,
|
|
@@ -468,6 +903,9 @@ function parseSessionFile(filePath, repoPath, repoId = deriveRepoId(repoPath)) {
|
|
|
468
903
|
sourceFile: filePath,
|
|
469
904
|
redactionCount: totalRedactions,
|
|
470
905
|
committedSubjects: collectCommittedSubjects(lines),
|
|
906
|
+
branch,
|
|
907
|
+
parentSessionId,
|
|
908
|
+
subagent,
|
|
471
909
|
rawContent,
|
|
472
910
|
rawFormat: "jsonl",
|
|
473
911
|
turns
|
|
@@ -494,47 +932,913 @@ function collectCommittedSubjects(lines) {
|
|
|
494
932
|
}
|
|
495
933
|
return [...subjects];
|
|
496
934
|
}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
935
|
+
function parseAllSessions(repoPath, repoId, cursors = {}) {
|
|
936
|
+
const id = repoId ?? deriveRepoId(repoPath);
|
|
937
|
+
const next = { ...cursors };
|
|
938
|
+
const sessions = [];
|
|
939
|
+
let skipped = 0;
|
|
940
|
+
for (const { file, agentKind } of findSessionFiles(repoPath)) {
|
|
941
|
+
const plan = planRead(file, cursors[file]);
|
|
942
|
+
if (plan.reason === "unchanged") {
|
|
943
|
+
skipped++;
|
|
944
|
+
continue;
|
|
945
|
+
}
|
|
946
|
+
const parsed = parseSessionFile(file, repoPath, id, agentKind);
|
|
947
|
+
if (!parsed) continue;
|
|
948
|
+
sessions.push(parsed);
|
|
949
|
+
next[file] = advanceCursor(file, statSync2(file).size);
|
|
950
|
+
}
|
|
951
|
+
return { sessions, cursors: next, skipped };
|
|
507
952
|
}
|
|
953
|
+
var MIN_MEANINGFUL_LINE_LENGTH, MAX_TURN_TEXT_LENGTH, FILE_PATH_TOOLS, XCODE_EDIT_TOOL, XCODE_PATH_TOOLS, PATH_TOKEN_RE, SYNTHETIC_CONTENT_RE, MAX_TRANSCRIPT_BYTES;
|
|
954
|
+
var init_claude_sessions = __esm({
|
|
955
|
+
"../../packages/ingest-core/src/claude-sessions.ts"() {
|
|
956
|
+
"use strict";
|
|
957
|
+
init_incremental();
|
|
958
|
+
init_git_history();
|
|
959
|
+
init_derive_uuid();
|
|
960
|
+
init_redact();
|
|
961
|
+
init_subject_linking();
|
|
962
|
+
init_types();
|
|
963
|
+
MIN_MEANINGFUL_LINE_LENGTH = 6;
|
|
964
|
+
MAX_TURN_TEXT_LENGTH = 4e3;
|
|
965
|
+
FILE_PATH_TOOLS = /* @__PURE__ */ new Set(["Read", "Edit", "Write", "NotebookEdit"]);
|
|
966
|
+
XCODE_EDIT_TOOL = /XcodeUpdate$/;
|
|
967
|
+
XCODE_PATH_TOOLS = /Xcode(Read|Grep|Glob|RefreshCodeIssuesInFile)$/;
|
|
968
|
+
PATH_TOKEN_RE = /(?:[.~]?\/)?(?:[\w-]+\/)+[\w.-]+/g;
|
|
969
|
+
SYNTHETIC_CONTENT_RE = /^\s*(<task-notification>|<system-reminder>|\[SYSTEM NOTIFICATION)/;
|
|
970
|
+
MAX_TRANSCRIPT_BYTES = 256 * 1024 * 1024;
|
|
971
|
+
}
|
|
972
|
+
});
|
|
508
973
|
|
|
509
|
-
// ../../packages/ingest-core/src/
|
|
510
|
-
import {
|
|
511
|
-
import { existsSync as existsSync2
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
const
|
|
516
|
-
return
|
|
974
|
+
// ../../packages/ingest-core/src/cursor-sessions.ts
|
|
975
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
976
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
977
|
+
import { homedir as homedir2 } from "node:os";
|
|
978
|
+
import { join as join2, sep } from "node:path";
|
|
979
|
+
function extractPathsFromText2(text) {
|
|
980
|
+
const matches = text.match(PATH_TOKEN_RE2) ?? [];
|
|
981
|
+
return [...new Set(matches)].filter((p) => p.length > 3 && p.length < 300);
|
|
517
982
|
}
|
|
518
|
-
function
|
|
519
|
-
|
|
983
|
+
function cursorStateDbPath() {
|
|
984
|
+
const home = homedir2();
|
|
985
|
+
if (process.platform === "darwin") {
|
|
986
|
+
return join2(home, "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb");
|
|
987
|
+
}
|
|
988
|
+
if (process.platform === "win32") {
|
|
989
|
+
const appData = process.env.APPDATA ?? join2(home, "AppData", "Roaming");
|
|
990
|
+
return join2(appData, "Cursor", "User", "globalStorage", "state.vscdb");
|
|
991
|
+
}
|
|
992
|
+
return join2(home, ".config", "Cursor", "User", "globalStorage", "state.vscdb");
|
|
520
993
|
}
|
|
521
|
-
function
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
994
|
+
function sqliteJson(dbPath, sql) {
|
|
995
|
+
try {
|
|
996
|
+
const out = execFileSync2("sqlite3", ["-readonly", "-json", dbPath, sql], {
|
|
997
|
+
maxBuffer: 1024 * 1024 * 256,
|
|
998
|
+
timeout: 6e4
|
|
999
|
+
}).toString("utf-8").trim();
|
|
1000
|
+
if (!out) return [];
|
|
1001
|
+
return JSON.parse(out);
|
|
1002
|
+
} catch {
|
|
1003
|
+
return [];
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
function sqlLiteral(value) {
|
|
1007
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
1008
|
+
}
|
|
1009
|
+
function deriveCursorSessionId(composerId, repoPath) {
|
|
1010
|
+
return deriveUuid(`evrex-cursor-session\0${composerId}\0${repoPath}`);
|
|
1011
|
+
}
|
|
1012
|
+
function deriveCursorTurnId(bubbleId, repoPath) {
|
|
1013
|
+
return deriveUuid(`evrex-cursor-turn\0${bubbleId}\0${repoPath}`);
|
|
1014
|
+
}
|
|
1015
|
+
function loadComposers(dbPath) {
|
|
1016
|
+
return sqliteJson(
|
|
1017
|
+
dbPath,
|
|
1018
|
+
`SELECT
|
|
1019
|
+
json_extract(value,'$.composerId') AS composerId,
|
|
1020
|
+
json_extract(value,'$.name') AS name,
|
|
1021
|
+
json_extract(value,'$.createdAt') AS createdAt,
|
|
1022
|
+
json_extract(value,'$.lastUpdatedAt') AS lastUpdatedAt,
|
|
1023
|
+
json_extract(value,'$.workspaceIdentifier.uri.path') AS workspacePath,
|
|
1024
|
+
json_extract(value,'$.fullConversationHeadersOnly') AS headers
|
|
1025
|
+
FROM cursorDiskKV
|
|
1026
|
+
WHERE key GLOB 'composerData:*'`
|
|
1027
|
+
);
|
|
1028
|
+
}
|
|
1029
|
+
function composerIdsReferencingRepo(dbPath, repoPath) {
|
|
1030
|
+
const needle = sqlLiteral(`%${repoPath}%`);
|
|
1031
|
+
const rows = sqliteJson(
|
|
1032
|
+
dbPath,
|
|
1033
|
+
`SELECT DISTINCT substr(key, 10, 36) AS composerId
|
|
1034
|
+
FROM cursorDiskKV
|
|
1035
|
+
WHERE key GLOB 'bubbleId:*' AND value LIKE ${needle}`
|
|
1036
|
+
);
|
|
1037
|
+
return rows.map((r) => r.composerId).filter((id) => !!id);
|
|
1038
|
+
}
|
|
1039
|
+
function bubbleIdsReferencingRepo(dbPath, composerId, repoPath) {
|
|
1040
|
+
const prefix = sqlLiteral(`bubbleId:${composerId}:%`);
|
|
1041
|
+
const needle = sqlLiteral(`%${repoPath}%`);
|
|
1042
|
+
const rows = sqliteJson(
|
|
1043
|
+
dbPath,
|
|
1044
|
+
`SELECT substr(key, ${10 + composerId.length + 1}) AS bubbleId
|
|
1045
|
+
FROM cursorDiskKV
|
|
1046
|
+
WHERE key LIKE ${prefix} AND value LIKE ${needle}`
|
|
1047
|
+
);
|
|
1048
|
+
return new Set(rows.map((r) => r.bubbleId).filter((id) => !!id));
|
|
1049
|
+
}
|
|
1050
|
+
function loadBubbles(dbPath, composerId) {
|
|
1051
|
+
const prefix = sqlLiteral(`bubbleId:${composerId}:%`);
|
|
1052
|
+
const rows = sqliteJson(
|
|
1053
|
+
dbPath,
|
|
1054
|
+
`SELECT
|
|
1055
|
+
substr(key, ${10 + composerId.length + 1}) AS bubbleId,
|
|
1056
|
+
json_extract(value,'$.type') AS type,
|
|
1057
|
+
json_extract(value,'$.text') AS text,
|
|
1058
|
+
json_extract(value,'$.thinking.text') AS thinking,
|
|
1059
|
+
json_extract(value,'$.toolFormerData.name') AS toolName,
|
|
1060
|
+
json_extract(value,'$.toolFormerData.rawArgs') AS rawArgs,
|
|
1061
|
+
json_extract(value,'$.createdAt') AS createdAt
|
|
1062
|
+
FROM cursorDiskKV
|
|
1063
|
+
WHERE key LIKE ${prefix}`
|
|
1064
|
+
);
|
|
1065
|
+
const map = /* @__PURE__ */ new Map();
|
|
1066
|
+
for (const r of rows) {
|
|
1067
|
+
if (r.bubbleId) map.set(r.bubbleId, r);
|
|
1068
|
+
}
|
|
1069
|
+
return map;
|
|
1070
|
+
}
|
|
1071
|
+
function loadRawRecords(dbPath, composerId, bubbleIds) {
|
|
1072
|
+
const keys = [
|
|
1073
|
+
`composerData:${composerId}`,
|
|
1074
|
+
...bubbleIds.map((b) => `bubbleId:${composerId}:${b}`)
|
|
1075
|
+
];
|
|
1076
|
+
const inList = keys.map((k) => sqlLiteral(k)).join(", ");
|
|
1077
|
+
const rows = sqliteJson(
|
|
1078
|
+
dbPath,
|
|
1079
|
+
`SELECT key, value FROM cursorDiskKV WHERE key IN (${inList})`
|
|
1080
|
+
);
|
|
1081
|
+
const records = {};
|
|
1082
|
+
for (const { key, value } of rows) {
|
|
1083
|
+
if (!key) continue;
|
|
525
1084
|
try {
|
|
526
|
-
|
|
1085
|
+
records[key] = value ? JSON.parse(value) : null;
|
|
527
1086
|
} catch {
|
|
528
|
-
|
|
1087
|
+
records[key] = value;
|
|
529
1088
|
}
|
|
530
1089
|
}
|
|
531
|
-
return
|
|
1090
|
+
return records;
|
|
532
1091
|
}
|
|
533
|
-
function
|
|
534
|
-
|
|
535
|
-
|
|
1092
|
+
function parseHeaders(headers2) {
|
|
1093
|
+
if (!headers2) return [];
|
|
1094
|
+
try {
|
|
1095
|
+
const parsed = JSON.parse(headers2);
|
|
1096
|
+
return Array.isArray(parsed) ? parsed.filter((h) => h && h.bubbleId) : [];
|
|
1097
|
+
} catch {
|
|
1098
|
+
return [];
|
|
1099
|
+
}
|
|
536
1100
|
}
|
|
537
|
-
function
|
|
1101
|
+
function filesFromTool(toolName, rawArgs) {
|
|
1102
|
+
const files = [];
|
|
1103
|
+
let label = `[tool_call: ${toolName}]`;
|
|
1104
|
+
if (!rawArgs) return { label, files };
|
|
1105
|
+
let args = {};
|
|
1106
|
+
try {
|
|
1107
|
+
args = JSON.parse(rawArgs);
|
|
1108
|
+
} catch {
|
|
1109
|
+
return { label, files };
|
|
1110
|
+
}
|
|
1111
|
+
const filePath = typeof args.target_file === "string" && args.target_file || typeof args.path === "string" && args.path || typeof args.relative_workspace_path === "string" && args.relative_workspace_path || null;
|
|
1112
|
+
const command = typeof args.command === "string" ? args.command : null;
|
|
1113
|
+
if (filePath) {
|
|
1114
|
+
label = `[tool_call: ${toolName}] ${filePath}`;
|
|
1115
|
+
files.push({ path: filePath, source: "tool_path", tool: toolName });
|
|
1116
|
+
} else if (command) {
|
|
1117
|
+
label = `[tool_call: ${toolName}] ${command}`;
|
|
1118
|
+
for (const p of extractPathsFromText2(command)) {
|
|
1119
|
+
files.push({ path: p, source: "tool_bash", tool: toolName });
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
return { label, files };
|
|
1123
|
+
}
|
|
1124
|
+
function toIso(value, fallbackMs) {
|
|
1125
|
+
if (typeof value === "string" && value) {
|
|
1126
|
+
const t = Date.parse(value);
|
|
1127
|
+
if (!Number.isNaN(t)) return new Date(t).toISOString();
|
|
1128
|
+
}
|
|
1129
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
1130
|
+
return new Date(value).toISOString();
|
|
1131
|
+
}
|
|
1132
|
+
return new Date(fallbackMs).toISOString();
|
|
1133
|
+
}
|
|
1134
|
+
function parseCursorComposer(dbPath, composer, repoPath, options = { scoped: false }, repoId = deriveRepoId(repoPath)) {
|
|
1135
|
+
const composerId = composer.composerId;
|
|
1136
|
+
if (!composerId) return null;
|
|
1137
|
+
const headers2 = parseHeaders(composer.headers);
|
|
1138
|
+
if (headers2.length === 0) return null;
|
|
1139
|
+
const bubbles = loadBubbles(dbPath, composerId);
|
|
1140
|
+
const composerCreatedMs = typeof composer.createdAt === "number" ? composer.createdAt : Date.now();
|
|
1141
|
+
const referencing = options.scoped ? bubbleIdsReferencingRepo(dbPath, composerId, repoPath) : null;
|
|
1142
|
+
const built = [];
|
|
1143
|
+
for (const header of headers2) {
|
|
1144
|
+
const bubble = bubbles.get(header.bubbleId);
|
|
1145
|
+
if (!bubble) continue;
|
|
1146
|
+
const role = header.type === 1 ? "user" : "assistant";
|
|
1147
|
+
const textParts = [];
|
|
1148
|
+
const filesTouched = [];
|
|
1149
|
+
if (bubble.text && bubble.text.trim()) {
|
|
1150
|
+
textParts.push(bubble.text);
|
|
1151
|
+
for (const p of extractPathsFromText2(bubble.text)) {
|
|
1152
|
+
filesTouched.push({ path: p, source: "prose" });
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
if (role === "assistant" && bubble.thinking && bubble.thinking.trim()) {
|
|
1156
|
+
textParts.push(bubble.thinking);
|
|
1157
|
+
}
|
|
1158
|
+
if (bubble.toolName) {
|
|
1159
|
+
const { label, files } = filesFromTool(bubble.toolName, bubble.rawArgs);
|
|
1160
|
+
textParts.push(label);
|
|
1161
|
+
filesTouched.push(...files);
|
|
1162
|
+
}
|
|
1163
|
+
const text = textParts.join("\n");
|
|
1164
|
+
if (!text.trim() && filesTouched.length === 0) continue;
|
|
1165
|
+
const redacted = redactSecrets(text);
|
|
1166
|
+
built.push({
|
|
1167
|
+
bubbleId: header.bubbleId,
|
|
1168
|
+
role,
|
|
1169
|
+
referencesRepo: referencing ? referencing.has(header.bubbleId) : true,
|
|
1170
|
+
redactionCount: redacted.count,
|
|
1171
|
+
turn: {
|
|
1172
|
+
id: header.bubbleId,
|
|
1173
|
+
// rewritten to the per-repo derived id below
|
|
1174
|
+
sessionId: composerId,
|
|
1175
|
+
// rewritten to the per-repo derived id below
|
|
1176
|
+
role,
|
|
1177
|
+
ts: toIso(bubble.createdAt, composerCreatedMs),
|
|
1178
|
+
text: redacted.text.slice(0, MAX_TURN_TEXT_LENGTH2),
|
|
1179
|
+
filesTouched,
|
|
1180
|
+
// Cursor's bubble format doesn't expose a structuredPatch-equivalent
|
|
1181
|
+
// the way Claude Code's toolUseResult does — content-overlap scoring
|
|
1182
|
+
// (see scoreContentOverlap in the linker) simply has no signal here
|
|
1183
|
+
// yet, same tier as any other not-yet-supported evidence source.
|
|
1184
|
+
editedLines: [],
|
|
1185
|
+
// Cursor has no parent-pointer chain; header order is authoritative, so
|
|
1186
|
+
// parentUuid is left null (the backend/linker never require it).
|
|
1187
|
+
parentUuid: null,
|
|
1188
|
+
isSidechain: false,
|
|
1189
|
+
redacted: redacted.count > 0,
|
|
1190
|
+
// Cursor's "user" bubbles (header.type === 1, see `role` above) are a
|
|
1191
|
+
// structural field distinct from tool output — not the Claude Code
|
|
1192
|
+
// wire-format ambiguity where a tool result also arrives as a
|
|
1193
|
+
// `role: "user"` record. No synthetic-input misattribution risk here.
|
|
1194
|
+
isSyntheticInput: false,
|
|
1195
|
+
isToolError: false,
|
|
1196
|
+
// Neither source reports what a turn cost, so it is unknown rather than free.
|
|
1197
|
+
usage: { ...EMPTY_USAGE }
|
|
1198
|
+
}
|
|
1199
|
+
});
|
|
1200
|
+
}
|
|
1201
|
+
let kept;
|
|
1202
|
+
if (referencing) {
|
|
1203
|
+
const keep2 = /* @__PURE__ */ new Set();
|
|
1204
|
+
for (let i = 0; i < built.length; i++) {
|
|
1205
|
+
const cur = built[i];
|
|
1206
|
+
if (!cur || !cur.referencesRepo) continue;
|
|
1207
|
+
keep2.add(i);
|
|
1208
|
+
const prev = built[i - 1];
|
|
1209
|
+
if (prev && prev.role === "user") keep2.add(i - 1);
|
|
1210
|
+
}
|
|
1211
|
+
kept = built.filter((_, i) => keep2.has(i));
|
|
1212
|
+
} else {
|
|
1213
|
+
kept = built;
|
|
1214
|
+
}
|
|
1215
|
+
if (kept.length === 0) return null;
|
|
1216
|
+
const sessionId = deriveCursorSessionId(composerId, repoPath);
|
|
1217
|
+
const turns = kept.map((b) => ({
|
|
1218
|
+
...b.turn,
|
|
1219
|
+
id: deriveCursorTurnId(b.bubbleId, repoPath),
|
|
1220
|
+
sessionId
|
|
1221
|
+
}));
|
|
1222
|
+
const totalRedactions = kept.reduce((sum, b) => sum + b.redactionCount, 0);
|
|
1223
|
+
const sortedTs = turns.map((t) => t.ts).sort();
|
|
1224
|
+
if (totalRedactions > 0) {
|
|
1225
|
+
console.log(`[ingest-core] redacted ${totalRedactions} potential secret(s) in cursor session ${composerId}`);
|
|
1226
|
+
}
|
|
1227
|
+
const rawRecords = loadRawRecords(dbPath, composerId, kept.map((b) => b.bubbleId));
|
|
1228
|
+
const rawContent = JSON.stringify(
|
|
1229
|
+
redactJsonValue({ format: "cursor-composer", composerId, records: rawRecords }).value
|
|
1230
|
+
);
|
|
1231
|
+
return {
|
|
1232
|
+
id: sessionId,
|
|
1233
|
+
agentKind: "cursor",
|
|
1234
|
+
repoId,
|
|
1235
|
+
cwd: repoPath,
|
|
1236
|
+
startedAt: sortedTs[0] ?? null,
|
|
1237
|
+
endedAt: sortedTs[sortedTs.length - 1] ?? null,
|
|
1238
|
+
turnCount: turns.length,
|
|
1239
|
+
aiTitle: composer.name && composer.name.trim() ? composer.name : null,
|
|
1240
|
+
author: null,
|
|
1241
|
+
// filled in by the caller — see collectRepoData
|
|
1242
|
+
// Includes repoPath so a shared composer's per-repo copies have distinct,
|
|
1243
|
+
// traceable source refs (the raw composerId alone is no longer unique).
|
|
1244
|
+
sourceFile: `${dbPath}#${composerId}#${repoPath}`,
|
|
1245
|
+
redactionCount: totalRedactions,
|
|
1246
|
+
// Read back off the turns rather than the raw records: unlike Claude Code,
|
|
1247
|
+
// a Cursor tool call's label keeps the command itself, so the invocation is
|
|
1248
|
+
// still there to read.
|
|
1249
|
+
committedSubjects: [
|
|
1250
|
+
...new Set(turns.flatMap((t) => committedSubjects(t.text)))
|
|
1251
|
+
],
|
|
1252
|
+
rawContent,
|
|
1253
|
+
branch: null,
|
|
1254
|
+
parentSessionId: null,
|
|
1255
|
+
subagent: null,
|
|
1256
|
+
rawFormat: "json",
|
|
1257
|
+
turns
|
|
1258
|
+
};
|
|
1259
|
+
}
|
|
1260
|
+
function workspaceMatchesRepo(workspacePath, repoPath) {
|
|
1261
|
+
if (!workspacePath) return false;
|
|
1262
|
+
return workspacePath === repoPath || workspacePath.startsWith(`${repoPath}${sep}`);
|
|
1263
|
+
}
|
|
1264
|
+
function parseCursorConversation(composerId, repoPath, repoId, dbPath = cursorStateDbPath()) {
|
|
1265
|
+
if (!existsSync2(dbPath)) return null;
|
|
1266
|
+
const composer = loadComposers(dbPath).find((c) => c.composerId === composerId);
|
|
1267
|
+
if (!composer) return null;
|
|
1268
|
+
return parseCursorComposer(dbPath, composer, repoPath, { scoped: false }, repoId);
|
|
1269
|
+
}
|
|
1270
|
+
function parseAllCursorSessions(repoPath, repoId) {
|
|
1271
|
+
const dbPath = cursorStateDbPath();
|
|
1272
|
+
if (!existsSync2(dbPath)) return [];
|
|
1273
|
+
const composers = loadComposers(dbPath);
|
|
1274
|
+
if (composers.length === 0) return [];
|
|
1275
|
+
const byId = /* @__PURE__ */ new Map();
|
|
1276
|
+
for (const c of composers) {
|
|
1277
|
+
if (c.composerId) byId.set(c.composerId, c);
|
|
1278
|
+
}
|
|
1279
|
+
const workspaceDedicated = /* @__PURE__ */ new Set();
|
|
1280
|
+
for (const c of composers) {
|
|
1281
|
+
if (c.composerId && workspaceMatchesRepo(c.workspacePath, repoPath)) {
|
|
1282
|
+
workspaceDedicated.add(c.composerId);
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
const attributed = new Set(workspaceDedicated);
|
|
1286
|
+
for (const id of composerIdsReferencingRepo(dbPath, repoPath)) {
|
|
1287
|
+
attributed.add(id);
|
|
1288
|
+
}
|
|
1289
|
+
const resolvedRepoId = repoId ?? deriveRepoId(repoPath);
|
|
1290
|
+
const sessions = [];
|
|
1291
|
+
for (const id of attributed) {
|
|
1292
|
+
const composer = byId.get(id);
|
|
1293
|
+
if (!composer) continue;
|
|
1294
|
+
const scoped = !workspaceDedicated.has(id);
|
|
1295
|
+
const parsed = parseCursorComposer(dbPath, composer, repoPath, { scoped }, resolvedRepoId);
|
|
1296
|
+
if (parsed) sessions.push(parsed);
|
|
1297
|
+
}
|
|
1298
|
+
return sessions;
|
|
1299
|
+
}
|
|
1300
|
+
var MAX_TURN_TEXT_LENGTH2, PATH_TOKEN_RE2;
|
|
1301
|
+
var init_cursor_sessions = __esm({
|
|
1302
|
+
"../../packages/ingest-core/src/cursor-sessions.ts"() {
|
|
1303
|
+
"use strict";
|
|
1304
|
+
init_git_history();
|
|
1305
|
+
init_redact();
|
|
1306
|
+
init_derive_uuid();
|
|
1307
|
+
init_subject_linking();
|
|
1308
|
+
init_types();
|
|
1309
|
+
MAX_TURN_TEXT_LENGTH2 = 4e3;
|
|
1310
|
+
PATH_TOKEN_RE2 = /(?:[.~]?\/)?(?:[\w-]+\/)+[\w.-]+/g;
|
|
1311
|
+
}
|
|
1312
|
+
});
|
|
1313
|
+
|
|
1314
|
+
// ../../packages/ingest-core/src/opencode-sessions.ts
|
|
1315
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
1316
|
+
import { homedir as homedir3 } from "node:os";
|
|
1317
|
+
import { join as join3 } from "node:path";
|
|
1318
|
+
function opencodeDbPath(env = process.env, home = homedir3()) {
|
|
1319
|
+
const data = env.XDG_DATA_HOME || join3(home, ".local", "share");
|
|
1320
|
+
return join3(data, "opencode", "opencode.db");
|
|
1321
|
+
}
|
|
1322
|
+
function q(value) {
|
|
1323
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
1324
|
+
}
|
|
1325
|
+
function parseJson(raw) {
|
|
1326
|
+
try {
|
|
1327
|
+
return JSON.parse(raw);
|
|
1328
|
+
} catch {
|
|
1329
|
+
return null;
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
function deriveOpenCodeSessionId(sessionId) {
|
|
1333
|
+
return deriveUuid(`evrex-opencode\0${sessionId}`);
|
|
1334
|
+
}
|
|
1335
|
+
function deriveTurnId(partOrMessageId, suffix = "") {
|
|
1336
|
+
return deriveUuid(`evrex-opencode-turn\0${partOrMessageId}${suffix}`);
|
|
1337
|
+
}
|
|
1338
|
+
function filesFromTool2(tool, input) {
|
|
1339
|
+
const out = [];
|
|
1340
|
+
for (const key of ["filePath", "path", "file"]) {
|
|
1341
|
+
const v = input[key];
|
|
1342
|
+
if (typeof v === "string" && v.length > 0) out.push({ path: v, source: "tool_path", tool });
|
|
1343
|
+
}
|
|
1344
|
+
const command = input.command;
|
|
1345
|
+
if (tool === "bash" && typeof command === "string") {
|
|
1346
|
+
for (const p of extractPathsFromText(command)) out.push({ path: p, source: "tool_bash", tool });
|
|
1347
|
+
}
|
|
1348
|
+
return out;
|
|
1349
|
+
}
|
|
1350
|
+
function usageOf(message) {
|
|
1351
|
+
const t = message.tokens;
|
|
1352
|
+
if (!t) return { ...EMPTY_USAGE, model: message.modelID ?? null };
|
|
1353
|
+
return {
|
|
1354
|
+
inputTokens: t.input ?? null,
|
|
1355
|
+
outputTokens: t.output ?? null,
|
|
1356
|
+
cacheReadTokens: t.cache?.read ?? null,
|
|
1357
|
+
cacheWriteTokens: t.cache?.write ?? null,
|
|
1358
|
+
model: message.modelID ?? null
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1361
|
+
function buildOpenCodeSession(session, messages, parts, repoId, sourceFile) {
|
|
1362
|
+
const id = deriveOpenCodeSessionId(session.id);
|
|
1363
|
+
const partsByMessage = /* @__PURE__ */ new Map();
|
|
1364
|
+
for (const p of parts) {
|
|
1365
|
+
const list = partsByMessage.get(p.message_id) ?? [];
|
|
1366
|
+
list.push(p);
|
|
1367
|
+
partsByMessage.set(p.message_id, list);
|
|
1368
|
+
}
|
|
1369
|
+
const turns = [];
|
|
1370
|
+
let redactionCount = 0;
|
|
1371
|
+
let previous = null;
|
|
1372
|
+
const subjects = [];
|
|
1373
|
+
const push = (turn) => {
|
|
1374
|
+
const redacted = redactSecrets(turn.text);
|
|
1375
|
+
redactionCount += redacted.count;
|
|
1376
|
+
turns.push({
|
|
1377
|
+
...turn,
|
|
1378
|
+
text: redacted.text,
|
|
1379
|
+
sessionId: id,
|
|
1380
|
+
parentUuid: previous,
|
|
1381
|
+
isSidechain: false,
|
|
1382
|
+
redacted: redacted.count > 0,
|
|
1383
|
+
editedLines: []
|
|
1384
|
+
});
|
|
1385
|
+
previous = turn.id;
|
|
1386
|
+
};
|
|
1387
|
+
const at = (ms) => new Date(ms).toISOString();
|
|
1388
|
+
for (const m of [...messages].sort((a, b) => a.time_created - b.time_created)) {
|
|
1389
|
+
const data = parseJson(m.data);
|
|
1390
|
+
if (!data?.role) continue;
|
|
1391
|
+
const mparts = (partsByMessage.get(m.id) ?? []).sort((a, b) => a.time_created - b.time_created);
|
|
1392
|
+
if (data.role === "user") {
|
|
1393
|
+
const text = mparts.map((p) => parseJson(p.data)).filter((p) => !!p && p.type === "text" && typeof p.text === "string").map((p) => p.text).join("\n").trim();
|
|
1394
|
+
if (!text) continue;
|
|
1395
|
+
push({
|
|
1396
|
+
id: deriveTurnId(m.id),
|
|
1397
|
+
role: "user",
|
|
1398
|
+
ts: at(data.time?.created ?? m.time_created),
|
|
1399
|
+
text,
|
|
1400
|
+
filesTouched: extractPathsFromText(text).map((path) => ({ path, source: "prose" })),
|
|
1401
|
+
usage: EMPTY_USAGE,
|
|
1402
|
+
isSyntheticInput: false,
|
|
1403
|
+
isToolError: false
|
|
1404
|
+
});
|
|
1405
|
+
continue;
|
|
1406
|
+
}
|
|
1407
|
+
const usage = usageOf(data);
|
|
1408
|
+
let usageGiven = false;
|
|
1409
|
+
const take = () => {
|
|
1410
|
+
if (usageGiven) return EMPTY_USAGE;
|
|
1411
|
+
usageGiven = true;
|
|
1412
|
+
return usage;
|
|
1413
|
+
};
|
|
1414
|
+
for (const p of mparts) {
|
|
1415
|
+
const part = parseJson(p.data);
|
|
1416
|
+
if (!part) continue;
|
|
1417
|
+
if (part.type === "text" && typeof part.text === "string" && part.text.trim()) {
|
|
1418
|
+
push({
|
|
1419
|
+
id: deriveTurnId(p.id),
|
|
1420
|
+
role: "assistant",
|
|
1421
|
+
ts: at(p.time_created),
|
|
1422
|
+
text: part.text.trim(),
|
|
1423
|
+
filesTouched: [],
|
|
1424
|
+
usage: take(),
|
|
1425
|
+
isSyntheticInput: false,
|
|
1426
|
+
isToolError: false
|
|
1427
|
+
});
|
|
1428
|
+
} else if (part.type === "tool" && typeof part.tool === "string") {
|
|
1429
|
+
const input = part.state?.input ?? {};
|
|
1430
|
+
const args = JSON.stringify(input);
|
|
1431
|
+
const files = filesFromTool2(part.tool, input);
|
|
1432
|
+
if (part.tool === "bash" && typeof input.command === "string") {
|
|
1433
|
+
subjects.push(...committedSubjects(input.command));
|
|
1434
|
+
}
|
|
1435
|
+
push({
|
|
1436
|
+
id: deriveTurnId(p.id, ":call"),
|
|
1437
|
+
role: "assistant",
|
|
1438
|
+
ts: at(p.time_created),
|
|
1439
|
+
text: `[tool: ${part.tool}] ${args.length > 600 ? `${args.slice(0, 600)}\u2026` : args}`,
|
|
1440
|
+
filesTouched: files,
|
|
1441
|
+
usage: take(),
|
|
1442
|
+
isSyntheticInput: false,
|
|
1443
|
+
isToolError: false
|
|
1444
|
+
});
|
|
1445
|
+
const failed = part.state?.status === "error";
|
|
1446
|
+
const output = failed ? part.state?.error ?? part.state?.output ?? "" : part.state?.output ?? "";
|
|
1447
|
+
push({
|
|
1448
|
+
id: deriveTurnId(p.id, ":result"),
|
|
1449
|
+
role: "user",
|
|
1450
|
+
ts: at(p.time_created),
|
|
1451
|
+
text: output.length > MAX_OUTPUT_CHARS ? `${output.slice(0, MAX_OUTPUT_CHARS)}\u2026` : output,
|
|
1452
|
+
filesTouched: [],
|
|
1453
|
+
usage: EMPTY_USAGE,
|
|
1454
|
+
isSyntheticInput: true,
|
|
1455
|
+
isToolError: failed
|
|
1456
|
+
});
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
if (turns.length === 0) return null;
|
|
1461
|
+
return {
|
|
1462
|
+
id,
|
|
1463
|
+
agentKind: "opencode",
|
|
1464
|
+
repoId,
|
|
1465
|
+
cwd: session.directory,
|
|
1466
|
+
startedAt: new Date(session.time_created).toISOString(),
|
|
1467
|
+
endedAt: new Date(session.time_updated).toISOString(),
|
|
1468
|
+
turnCount: turns.length,
|
|
1469
|
+
aiTitle: session.title || null,
|
|
1470
|
+
author: null,
|
|
1471
|
+
sourceFile,
|
|
1472
|
+
redactionCount,
|
|
1473
|
+
committedSubjects: [...new Set(subjects)],
|
|
1474
|
+
parentSessionId: session.parent_id ? deriveOpenCodeSessionId(session.parent_id) : null,
|
|
1475
|
+
subagent: session.parent_id ? { agentId: session.id, agentType: session.agent ?? null, description: session.title || null } : null,
|
|
1476
|
+
branch: null,
|
|
1477
|
+
// The archive is the rows themselves, so a reader later has what the
|
|
1478
|
+
// store had — minus the reasoning parts, which OpenCode itself hides.
|
|
1479
|
+
rawContent: JSON.stringify({
|
|
1480
|
+
session,
|
|
1481
|
+
messages,
|
|
1482
|
+
parts: parts.filter((p) => !p.data.includes('"type":"reasoning"'))
|
|
1483
|
+
}),
|
|
1484
|
+
rawFormat: "json",
|
|
1485
|
+
turns
|
|
1486
|
+
};
|
|
1487
|
+
}
|
|
1488
|
+
function parseAllOpenCodeSessions(repoPath, repoId = deriveRepoId(repoPath), dbPath = opencodeDbPath()) {
|
|
1489
|
+
if (!existsSync3(dbPath)) return [];
|
|
1490
|
+
const sessions = sqliteJson(
|
|
1491
|
+
dbPath,
|
|
1492
|
+
`SELECT s.id, s.parent_id, s.directory, s.title, s.agent, s.model, s.time_created, s.time_updated, p.worktree
|
|
1493
|
+
FROM session s LEFT JOIN project p ON p.id = s.project_id
|
|
1494
|
+
WHERE s.time_archived IS NULL`
|
|
1495
|
+
).filter((s) => workspaceMatchesRepo(s.directory, repoPath) || workspaceMatchesRepo(s.worktree, repoPath));
|
|
1496
|
+
const out = [];
|
|
1497
|
+
for (const s of sessions) {
|
|
1498
|
+
const messages = sqliteJson(
|
|
1499
|
+
dbPath,
|
|
1500
|
+
`SELECT id, session_id, time_created, data FROM message WHERE session_id = ${q(s.id)} ORDER BY time_created, id`
|
|
1501
|
+
);
|
|
1502
|
+
const parts = sqliteJson(
|
|
1503
|
+
dbPath,
|
|
1504
|
+
`SELECT id, message_id, time_created, data FROM part WHERE session_id = ${q(s.id)} ORDER BY time_created, id`
|
|
1505
|
+
);
|
|
1506
|
+
const parsed = buildOpenCodeSession(s, messages, parts, repoId, `${dbPath}#${s.id}`);
|
|
1507
|
+
if (parsed) out.push(parsed);
|
|
1508
|
+
}
|
|
1509
|
+
return out;
|
|
1510
|
+
}
|
|
1511
|
+
var MAX_OUTPUT_CHARS;
|
|
1512
|
+
var init_opencode_sessions = __esm({
|
|
1513
|
+
"../../packages/ingest-core/src/opencode-sessions.ts"() {
|
|
1514
|
+
"use strict";
|
|
1515
|
+
init_claude_sessions();
|
|
1516
|
+
init_cursor_sessions();
|
|
1517
|
+
init_derive_uuid();
|
|
1518
|
+
init_git_history();
|
|
1519
|
+
init_redact();
|
|
1520
|
+
init_subject_linking();
|
|
1521
|
+
init_types();
|
|
1522
|
+
MAX_OUTPUT_CHARS = 2e3;
|
|
1523
|
+
}
|
|
1524
|
+
});
|
|
1525
|
+
|
|
1526
|
+
// ../../packages/ingest-core/src/copilot-sessions.ts
|
|
1527
|
+
import { existsSync as existsSync4, readFileSync as readFileSync2, readdirSync as readdirSync2, statSync as statSync3 } from "node:fs";
|
|
1528
|
+
import { homedir as homedir4 } from "node:os";
|
|
1529
|
+
import { join as join4 } from "node:path";
|
|
1530
|
+
function copilotSessionsDir(home = homedir4()) {
|
|
1531
|
+
return join4(home, ".copilot", "session-state");
|
|
1532
|
+
}
|
|
1533
|
+
function parseCopilotWorkspace(text) {
|
|
1534
|
+
const map = /* @__PURE__ */ new Map();
|
|
1535
|
+
for (const line of text.split("\n")) {
|
|
1536
|
+
const m = /^([a-z_]+):\s*(.*)$/.exec(line);
|
|
1537
|
+
if (!m) continue;
|
|
1538
|
+
let value = m[2].trim();
|
|
1539
|
+
if (value.length >= 2 && value.startsWith("'") && value.endsWith("'")) {
|
|
1540
|
+
value = value.slice(1, -1).replace(/''/g, "'");
|
|
1541
|
+
} else if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
|
|
1542
|
+
try {
|
|
1543
|
+
value = JSON.parse(value);
|
|
1544
|
+
} catch {
|
|
1545
|
+
value = value.slice(1, -1);
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
map.set(m[1], value);
|
|
1549
|
+
}
|
|
1550
|
+
const get2 = (k) => {
|
|
1551
|
+
const v = map.get(k);
|
|
1552
|
+
return v === void 0 || v === "" || v === "null" ? null : v;
|
|
1553
|
+
};
|
|
1554
|
+
return {
|
|
1555
|
+
id: get2("id"),
|
|
1556
|
+
cwd: get2("cwd"),
|
|
1557
|
+
gitRoot: get2("git_root"),
|
|
1558
|
+
branch: get2("branch"),
|
|
1559
|
+
name: get2("name"),
|
|
1560
|
+
createdAt: get2("created_at"),
|
|
1561
|
+
updatedAt: get2("updated_at")
|
|
1562
|
+
};
|
|
1563
|
+
}
|
|
1564
|
+
function findCopilotSessionDirs(root = copilotSessionsDir()) {
|
|
1565
|
+
if (!existsSync4(root)) return [];
|
|
1566
|
+
return readdirSync2(root).map((name) => join4(root, name)).filter((dir) => {
|
|
1567
|
+
try {
|
|
1568
|
+
return statSync3(dir).isDirectory() && existsSync4(join4(dir, "events.jsonl"));
|
|
1569
|
+
} catch {
|
|
1570
|
+
return false;
|
|
1571
|
+
}
|
|
1572
|
+
}).sort((a, b) => statSync3(b).mtimeMs - statSync3(a).mtimeMs);
|
|
1573
|
+
}
|
|
1574
|
+
function deriveTurnId2(key) {
|
|
1575
|
+
return deriveUuid(`evrex-copilot-turn ${key}`);
|
|
1576
|
+
}
|
|
1577
|
+
function filesFromTool3(tool, args) {
|
|
1578
|
+
const out = [];
|
|
1579
|
+
for (const key of ["path", "filePath", "file"]) {
|
|
1580
|
+
const v = args[key];
|
|
1581
|
+
if (typeof v === "string" && v.length > 0) out.push({ path: v, source: "tool_path", tool });
|
|
1582
|
+
}
|
|
1583
|
+
if (tool === "bash" && typeof args.command === "string") {
|
|
1584
|
+
for (const p of extractPathsFromText(args.command)) out.push({ path: p, source: "tool_bash", tool });
|
|
1585
|
+
}
|
|
1586
|
+
return out;
|
|
1587
|
+
}
|
|
1588
|
+
function shutdownUsage(data, model) {
|
|
1589
|
+
const details = data.tokenDetails;
|
|
1590
|
+
if (!details) return { ...EMPTY_USAGE, model };
|
|
1591
|
+
const n = (k) => typeof details[k]?.tokenCount === "number" ? details[k].tokenCount : null;
|
|
1592
|
+
return {
|
|
1593
|
+
inputTokens: n("input"),
|
|
1594
|
+
outputTokens: n("output"),
|
|
1595
|
+
cacheReadTokens: n("cache_read"),
|
|
1596
|
+
cacheWriteTokens: n("cache_write"),
|
|
1597
|
+
model
|
|
1598
|
+
};
|
|
1599
|
+
}
|
|
1600
|
+
function buildCopilotSession(eventsText, workspace, repoId, sourceFile) {
|
|
1601
|
+
const events = [];
|
|
1602
|
+
const archived = [];
|
|
1603
|
+
for (const line of eventsText.split("\n")) {
|
|
1604
|
+
if (!line.trim()) continue;
|
|
1605
|
+
let e;
|
|
1606
|
+
try {
|
|
1607
|
+
e = JSON.parse(line);
|
|
1608
|
+
} catch {
|
|
1609
|
+
continue;
|
|
1610
|
+
}
|
|
1611
|
+
events.push(e);
|
|
1612
|
+
if (e.data && Object.keys(e.data).some((k) => REASONING_KEYS.has(k))) {
|
|
1613
|
+
const data = Object.fromEntries(Object.entries(e.data).filter(([k]) => !REASONING_KEYS.has(k)));
|
|
1614
|
+
archived.push(JSON.stringify({ ...e, data }));
|
|
1615
|
+
} else {
|
|
1616
|
+
archived.push(line);
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
const start = events.find((e) => e.type === "session.start");
|
|
1620
|
+
const startData = start?.data ?? {};
|
|
1621
|
+
const sessionId = workspace.id ?? startData.sessionId ?? null;
|
|
1622
|
+
if (!sessionId) return null;
|
|
1623
|
+
const cwd = workspace.cwd ?? startData.context?.cwd ?? null;
|
|
1624
|
+
if (!cwd) return null;
|
|
1625
|
+
const turns = [];
|
|
1626
|
+
let redactionCount = 0;
|
|
1627
|
+
let previous = null;
|
|
1628
|
+
const subjects = [];
|
|
1629
|
+
let lastModel = null;
|
|
1630
|
+
const push = (turn) => {
|
|
1631
|
+
const redacted = redactSecrets(turn.text);
|
|
1632
|
+
redactionCount += redacted.count;
|
|
1633
|
+
turns.push({
|
|
1634
|
+
...turn,
|
|
1635
|
+
text: redacted.text,
|
|
1636
|
+
sessionId,
|
|
1637
|
+
parentUuid: previous,
|
|
1638
|
+
isSidechain: false,
|
|
1639
|
+
redacted: redacted.count > 0,
|
|
1640
|
+
editedLines: []
|
|
1641
|
+
});
|
|
1642
|
+
previous = turn.id;
|
|
1643
|
+
};
|
|
1644
|
+
const toolNames = /* @__PURE__ */ new Map();
|
|
1645
|
+
for (const e of events) {
|
|
1646
|
+
const d = e.data ?? {};
|
|
1647
|
+
const ts = e.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
|
|
1648
|
+
const id = e.id ?? deriveTurnId2(`${sessionId}:${turns.length}`);
|
|
1649
|
+
switch (e.type) {
|
|
1650
|
+
case "user.message": {
|
|
1651
|
+
const text = typeof d.content === "string" ? d.content.trim() : "";
|
|
1652
|
+
if (!text) break;
|
|
1653
|
+
push({
|
|
1654
|
+
id,
|
|
1655
|
+
role: "user",
|
|
1656
|
+
ts,
|
|
1657
|
+
text,
|
|
1658
|
+
filesTouched: extractPathsFromText(text).map((path) => ({ path, source: "prose" })),
|
|
1659
|
+
usage: EMPTY_USAGE,
|
|
1660
|
+
isSyntheticInput: false,
|
|
1661
|
+
isToolError: false
|
|
1662
|
+
});
|
|
1663
|
+
break;
|
|
1664
|
+
}
|
|
1665
|
+
case "assistant.message": {
|
|
1666
|
+
if (typeof d.model === "string") lastModel = d.model;
|
|
1667
|
+
const text = typeof d.content === "string" ? d.content.trim() : "";
|
|
1668
|
+
if (!text) break;
|
|
1669
|
+
push({ id, role: "assistant", ts, text, filesTouched: [], usage: EMPTY_USAGE, isSyntheticInput: false, isToolError: false });
|
|
1670
|
+
break;
|
|
1671
|
+
}
|
|
1672
|
+
case "tool.execution_start": {
|
|
1673
|
+
const tool = typeof d.toolName === "string" ? d.toolName : "tool";
|
|
1674
|
+
const callId = typeof d.toolCallId === "string" ? d.toolCallId : id;
|
|
1675
|
+
const args = d.arguments && typeof d.arguments === "object" ? d.arguments : {};
|
|
1676
|
+
toolNames.set(callId, tool);
|
|
1677
|
+
if (tool === "bash" && typeof args.command === "string") subjects.push(...committedSubjects(args.command));
|
|
1678
|
+
const rendered = JSON.stringify(args);
|
|
1679
|
+
push({
|
|
1680
|
+
id: deriveTurnId2(`${callId}:call`),
|
|
1681
|
+
role: "assistant",
|
|
1682
|
+
ts,
|
|
1683
|
+
text: `[tool: ${tool}] ${rendered.length > 600 ? `${rendered.slice(0, 600)}\u2026` : rendered}`,
|
|
1684
|
+
filesTouched: filesFromTool3(tool, args),
|
|
1685
|
+
usage: EMPTY_USAGE,
|
|
1686
|
+
isSyntheticInput: false,
|
|
1687
|
+
isToolError: false
|
|
1688
|
+
});
|
|
1689
|
+
break;
|
|
1690
|
+
}
|
|
1691
|
+
case "tool.execution_complete": {
|
|
1692
|
+
const callId = typeof d.toolCallId === "string" ? d.toolCallId : id;
|
|
1693
|
+
const failed = d.success === false;
|
|
1694
|
+
const result = d.result ?? {};
|
|
1695
|
+
const error = d.error ?? {};
|
|
1696
|
+
const output = (failed ? error.message ?? result.content ?? "Tool call failed" : result.content ?? "").trim();
|
|
1697
|
+
push({
|
|
1698
|
+
id: deriveTurnId2(`${callId}:result`),
|
|
1699
|
+
role: "user",
|
|
1700
|
+
ts,
|
|
1701
|
+
text: output.length > MAX_OUTPUT_CHARS2 ? `${output.slice(0, MAX_OUTPUT_CHARS2)}\u2026` : output,
|
|
1702
|
+
filesTouched: [],
|
|
1703
|
+
usage: EMPTY_USAGE,
|
|
1704
|
+
isSyntheticInput: true,
|
|
1705
|
+
isToolError: failed
|
|
1706
|
+
});
|
|
1707
|
+
break;
|
|
1708
|
+
}
|
|
1709
|
+
case "session.shutdown": {
|
|
1710
|
+
const usage = shutdownUsage(d, typeof d.currentModel === "string" ? d.currentModel : lastModel);
|
|
1711
|
+
for (let i = turns.length - 1; i >= 0; i--) {
|
|
1712
|
+
if (turns[i].role === "assistant") {
|
|
1713
|
+
turns[i].usage = usage;
|
|
1714
|
+
break;
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
break;
|
|
1718
|
+
}
|
|
1719
|
+
default:
|
|
1720
|
+
break;
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
if (turns.length === 0) return null;
|
|
1724
|
+
const first = turns[0].ts;
|
|
1725
|
+
const last = turns[turns.length - 1].ts;
|
|
1726
|
+
return {
|
|
1727
|
+
id: sessionId,
|
|
1728
|
+
agentKind: "copilot",
|
|
1729
|
+
repoId,
|
|
1730
|
+
cwd,
|
|
1731
|
+
startedAt: workspace.createdAt ?? startData.startTime ?? first,
|
|
1732
|
+
endedAt: events[events.length - 1]?.timestamp ?? workspace.updatedAt ?? last,
|
|
1733
|
+
turnCount: turns.length,
|
|
1734
|
+
aiTitle: workspace.name,
|
|
1735
|
+
author: null,
|
|
1736
|
+
sourceFile,
|
|
1737
|
+
redactionCount,
|
|
1738
|
+
committedSubjects: [...new Set(subjects)],
|
|
1739
|
+
parentSessionId: null,
|
|
1740
|
+
subagent: null,
|
|
1741
|
+
branch: workspace.branch,
|
|
1742
|
+
rawContent: archived.join("\n"),
|
|
1743
|
+
rawFormat: "jsonl",
|
|
1744
|
+
turns
|
|
1745
|
+
};
|
|
1746
|
+
}
|
|
1747
|
+
function parseCopilotSessionDir(dir, repoId) {
|
|
1748
|
+
const eventsPath = join4(dir, "events.jsonl");
|
|
1749
|
+
if (!existsSync4(eventsPath)) return null;
|
|
1750
|
+
const workspacePath = join4(dir, "workspace.yaml");
|
|
1751
|
+
const workspace = existsSync4(workspacePath) ? parseCopilotWorkspace(readFileSync2(workspacePath, "utf-8")) : parseCopilotWorkspace("");
|
|
1752
|
+
return buildCopilotSession(readFileSync2(eventsPath, "utf-8"), workspace, repoId, eventsPath);
|
|
1753
|
+
}
|
|
1754
|
+
function parseAllCopilotSessions(repoPath, repoId = deriveRepoId(repoPath), root = copilotSessionsDir()) {
|
|
1755
|
+
const out = [];
|
|
1756
|
+
for (const dir of findCopilotSessionDirs(root)) {
|
|
1757
|
+
const workspacePath = join4(dir, "workspace.yaml");
|
|
1758
|
+
const workspace = existsSync4(workspacePath) ? parseCopilotWorkspace(readFileSync2(workspacePath, "utf-8")) : parseCopilotWorkspace("");
|
|
1759
|
+
const known = workspace.gitRoot ?? workspace.cwd;
|
|
1760
|
+
if (known && !workspaceMatchesRepo(known, repoPath)) continue;
|
|
1761
|
+
const parsed = buildCopilotSession(readFileSync2(join4(dir, "events.jsonl"), "utf-8"), workspace, repoId, join4(dir, "events.jsonl"));
|
|
1762
|
+
if (!parsed) continue;
|
|
1763
|
+
if (!known && !workspaceMatchesRepo(parsed.cwd, repoPath)) continue;
|
|
1764
|
+
out.push(parsed);
|
|
1765
|
+
}
|
|
1766
|
+
return out;
|
|
1767
|
+
}
|
|
1768
|
+
var MAX_OUTPUT_CHARS2, REASONING_KEYS;
|
|
1769
|
+
var init_copilot_sessions = __esm({
|
|
1770
|
+
"../../packages/ingest-core/src/copilot-sessions.ts"() {
|
|
1771
|
+
"use strict";
|
|
1772
|
+
init_claude_sessions();
|
|
1773
|
+
init_cursor_sessions();
|
|
1774
|
+
init_derive_uuid();
|
|
1775
|
+
init_git_history();
|
|
1776
|
+
init_redact();
|
|
1777
|
+
init_subject_linking();
|
|
1778
|
+
init_types();
|
|
1779
|
+
MAX_OUTPUT_CHARS2 = 2e3;
|
|
1780
|
+
REASONING_KEYS = /* @__PURE__ */ new Set(["reasoningOpaque", "reasoningText", "reasoningBlocks"]);
|
|
1781
|
+
}
|
|
1782
|
+
});
|
|
1783
|
+
|
|
1784
|
+
// ../../packages/ingest-core/src/codex-sessions.ts
|
|
1785
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
1786
|
+
import { existsSync as existsSync5, readFileSync as readFileSync3, readdirSync as readdirSync3, statSync as statSync4 } from "node:fs";
|
|
1787
|
+
import { homedir as homedir5 } from "node:os";
|
|
1788
|
+
import { join as join5 } from "node:path";
|
|
1789
|
+
function codexSessionsDir() {
|
|
1790
|
+
return join5(homedir5(), CODEX_DIR, "sessions");
|
|
1791
|
+
}
|
|
1792
|
+
function deriveUuid2(name) {
|
|
1793
|
+
const h = createHash2("sha1").update(name).digest("hex");
|
|
1794
|
+
const variant = (parseInt(h.slice(16, 17) || "0", 16) & 3 | 8).toString(16);
|
|
1795
|
+
const s = h.slice(0, 12) + "5" + h.slice(13, 16) + variant + h.slice(17, 32);
|
|
1796
|
+
return `${s.slice(0, 8)}-${s.slice(8, 12)}-${s.slice(12, 16)}-${s.slice(16, 20)}-${s.slice(20, 32)}`;
|
|
1797
|
+
}
|
|
1798
|
+
function deriveCodexTurnId(sessionId, ordinal) {
|
|
1799
|
+
return deriveUuid2(`evrex-codex-turn\0${sessionId}\0${ordinal}`);
|
|
1800
|
+
}
|
|
1801
|
+
function findCodexSessionFiles(root = codexSessionsDir()) {
|
|
1802
|
+
const found = [];
|
|
1803
|
+
const walk = (dir) => {
|
|
1804
|
+
let entries;
|
|
1805
|
+
try {
|
|
1806
|
+
entries = readdirSync3(dir);
|
|
1807
|
+
} catch {
|
|
1808
|
+
return;
|
|
1809
|
+
}
|
|
1810
|
+
for (const entry of entries) {
|
|
1811
|
+
const full = join5(dir, entry);
|
|
1812
|
+
let isDir = false;
|
|
1813
|
+
try {
|
|
1814
|
+
isDir = statSync4(full).isDirectory();
|
|
1815
|
+
} catch {
|
|
1816
|
+
continue;
|
|
1817
|
+
}
|
|
1818
|
+
if (isDir) walk(full);
|
|
1819
|
+
else if (entry.endsWith(".jsonl")) found.push(full);
|
|
1820
|
+
}
|
|
1821
|
+
};
|
|
1822
|
+
if (existsSync5(root)) walk(root);
|
|
1823
|
+
return found.sort();
|
|
1824
|
+
}
|
|
1825
|
+
function parseLines(raw) {
|
|
1826
|
+
const out = [];
|
|
1827
|
+
for (const line of raw.split("\n")) {
|
|
1828
|
+
if (!line.trim()) continue;
|
|
1829
|
+
try {
|
|
1830
|
+
out.push(JSON.parse(line));
|
|
1831
|
+
} catch {
|
|
1832
|
+
continue;
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
return out;
|
|
1836
|
+
}
|
|
1837
|
+
function payloadType(line) {
|
|
1838
|
+
const t = line.payload?.type;
|
|
1839
|
+
return typeof t === "string" ? t : void 0;
|
|
1840
|
+
}
|
|
1841
|
+
function asString(value) {
|
|
538
1842
|
return typeof value === "string" && value.length > 0 ? value : null;
|
|
539
1843
|
}
|
|
540
1844
|
function conversationEvents(lines) {
|
|
@@ -620,7 +1924,7 @@ function filesFromToolCalls(lines) {
|
|
|
620
1924
|
function parseCodexSessionFile(filePath, repoPath, repoId) {
|
|
621
1925
|
let raw;
|
|
622
1926
|
try {
|
|
623
|
-
raw =
|
|
1927
|
+
raw = readFileSync3(filePath, "utf-8");
|
|
624
1928
|
} catch {
|
|
625
1929
|
return null;
|
|
626
1930
|
}
|
|
@@ -653,6 +1957,7 @@ function parseCodexSessionFile(filePath, repoPath, repoId) {
|
|
|
653
1957
|
isSidechain: false,
|
|
654
1958
|
redacted: count > 0,
|
|
655
1959
|
isSyntheticInput: false,
|
|
1960
|
+
isToolError: false,
|
|
656
1961
|
usage: event.usage
|
|
657
1962
|
};
|
|
658
1963
|
});
|
|
@@ -662,6 +1967,9 @@ function parseCodexSessionFile(filePath, repoPath, repoId) {
|
|
|
662
1967
|
return {
|
|
663
1968
|
id: sessionId,
|
|
664
1969
|
agentKind: "codex",
|
|
1970
|
+
branch: null,
|
|
1971
|
+
parentSessionId: null,
|
|
1972
|
+
subagent: null,
|
|
665
1973
|
// Codex records the remote itself, so identity survives a moved or deleted
|
|
666
1974
|
// checkout. Falls back to the caller's derivation when it is absent.
|
|
667
1975
|
repoId: repoIdFromMeta(meta) ?? repoId,
|
|
@@ -680,9 +1988,9 @@ function parseCodexSessionFile(filePath, repoPath, repoId) {
|
|
|
680
1988
|
};
|
|
681
1989
|
}
|
|
682
1990
|
function repoIdFromMeta(meta) {
|
|
683
|
-
const
|
|
684
|
-
if (typeof
|
|
685
|
-
const url = asString(
|
|
1991
|
+
const git3 = meta.git;
|
|
1992
|
+
if (typeof git3 !== "object" || git3 === null) return null;
|
|
1993
|
+
const url = asString(git3.repository_url);
|
|
686
1994
|
if (!url) return null;
|
|
687
1995
|
const normalized = normalizeRepoRemote(url);
|
|
688
1996
|
return normalized ? `remote:${normalized}` : null;
|
|
@@ -697,13 +2005,494 @@ function redactRollout(raw) {
|
|
|
697
2005
|
});
|
|
698
2006
|
return { content: lines.join("\n"), count };
|
|
699
2007
|
}
|
|
2008
|
+
function parseAllCodexSessions(repoPath, repoId) {
|
|
2009
|
+
const sessions = [];
|
|
2010
|
+
for (const file of findCodexSessionFiles()) {
|
|
2011
|
+
const parsed = parseCodexSessionFile(file, repoPath, repoId);
|
|
2012
|
+
if (parsed) sessions.push(parsed);
|
|
2013
|
+
}
|
|
2014
|
+
return sessions;
|
|
2015
|
+
}
|
|
2016
|
+
var CODEX_DIR;
|
|
2017
|
+
var init_codex_sessions = __esm({
|
|
2018
|
+
"../../packages/ingest-core/src/codex-sessions.ts"() {
|
|
2019
|
+
"use strict";
|
|
2020
|
+
init_claude_sessions();
|
|
2021
|
+
init_git_history();
|
|
2022
|
+
init_redact();
|
|
2023
|
+
init_subject_linking();
|
|
2024
|
+
init_types();
|
|
2025
|
+
CODEX_DIR = ".codex";
|
|
2026
|
+
}
|
|
2027
|
+
});
|
|
700
2028
|
|
|
701
2029
|
// ../../packages/ingest-core/src/sanitize.ts
|
|
702
|
-
|
|
2030
|
+
function stripNulls(value) {
|
|
2031
|
+
return value.includes(NUL) ? value.split(NUL).join("") : value;
|
|
2032
|
+
}
|
|
2033
|
+
function stripNullsDeep(value) {
|
|
2034
|
+
if (typeof value === "string") return stripNulls(value);
|
|
2035
|
+
if (Array.isArray(value)) return value.map((v) => stripNullsDeep(v));
|
|
2036
|
+
if (value && typeof value === "object") {
|
|
2037
|
+
if (value instanceof Date) return value;
|
|
2038
|
+
const out = {};
|
|
2039
|
+
for (const [k, v] of Object.entries(value)) {
|
|
2040
|
+
out[k] = stripNullsDeep(v);
|
|
2041
|
+
}
|
|
2042
|
+
return out;
|
|
2043
|
+
}
|
|
2044
|
+
return value;
|
|
2045
|
+
}
|
|
2046
|
+
var NUL;
|
|
2047
|
+
var init_sanitize = __esm({
|
|
2048
|
+
"../../packages/ingest-core/src/sanitize.ts"() {
|
|
2049
|
+
"use strict";
|
|
2050
|
+
NUL = String.fromCharCode(0);
|
|
2051
|
+
}
|
|
2052
|
+
});
|
|
2053
|
+
|
|
2054
|
+
// ../../packages/ingest-core/src/tickets.ts
|
|
2055
|
+
function deriveTicketId(args) {
|
|
2056
|
+
return deriveUuid(
|
|
2057
|
+
`evrex-ticket ${args.orgId} ${args.kind} ${args.workspace} ${args.externalId}`
|
|
2058
|
+
);
|
|
2059
|
+
}
|
|
2060
|
+
function ticketReferencesIn(text) {
|
|
2061
|
+
return [...new Set([...text.matchAll(TICKET_REFERENCE)].map((m) => m[0]))];
|
|
2062
|
+
}
|
|
2063
|
+
var TICKET_REFERENCE;
|
|
2064
|
+
var init_tickets = __esm({
|
|
2065
|
+
"../../packages/ingest-core/src/tickets.ts"() {
|
|
2066
|
+
"use strict";
|
|
2067
|
+
init_derive_uuid();
|
|
2068
|
+
TICKET_REFERENCE = /(?<![A-Za-z0-9_-])([A-Z][A-Z0-9]{1,9})-(\d{1,6})(?![A-Za-z0-9_-])/g;
|
|
2069
|
+
}
|
|
2070
|
+
});
|
|
2071
|
+
|
|
2072
|
+
// ../../packages/ingest-core/src/linear.ts
|
|
2073
|
+
function linearIssueToTicket(issue) {
|
|
2074
|
+
if (!issue.identifier) return null;
|
|
2075
|
+
const attributes = {
|
|
2076
|
+
title: issue.title ?? issue.identifier,
|
|
2077
|
+
status: issue.state?.name ?? null,
|
|
2078
|
+
statusCategory: issue.state?.type ? STATE_TYPE[issue.state.type] ?? null : null,
|
|
2079
|
+
assignee: issue.assignee?.displayName ?? null,
|
|
2080
|
+
assigneeId: issue.assignee?.id ?? null,
|
|
2081
|
+
team: issue.team?.key ?? null,
|
|
2082
|
+
project: issue.project?.name ?? null,
|
|
2083
|
+
// Linear has one kind of issue; the field exists so a Jira row and a
|
|
2084
|
+
// Linear row are the same shape, which is what U5 exists to prove.
|
|
2085
|
+
issueType: null,
|
|
2086
|
+
// Null rather than "No priority" when the field is absent: unset and
|
|
2087
|
+
// explicitly-not-prioritised are different claims.
|
|
2088
|
+
priority: typeof issue.priority === "number" ? PRIORITY[issue.priority] ?? null : null,
|
|
2089
|
+
// Linear records completion as a state, not as a separate resolution.
|
|
2090
|
+
resolution: null
|
|
2091
|
+
};
|
|
2092
|
+
return {
|
|
2093
|
+
externalId: issue.identifier,
|
|
2094
|
+
kind: "linear",
|
|
2095
|
+
url: issue.url ?? `https://linear.app/issue/${issue.identifier}`,
|
|
2096
|
+
createdAt: issue.createdAt ?? null,
|
|
2097
|
+
updatedAt: issue.updatedAt ?? null,
|
|
2098
|
+
attributes
|
|
2099
|
+
};
|
|
2100
|
+
}
|
|
2101
|
+
function linearAuthHeader(auth) {
|
|
2102
|
+
if (auth.accessToken) return `Bearer ${auth.accessToken}`;
|
|
2103
|
+
if (auth.apiKey) return auth.apiKey;
|
|
2104
|
+
throw new Error(
|
|
2105
|
+
"Linear needs either an API key or an OAuth access token; got neither."
|
|
2106
|
+
);
|
|
2107
|
+
}
|
|
2108
|
+
async function fetchLinearIssues(options) {
|
|
2109
|
+
const call2 = options.fetch ?? globalThis.fetch;
|
|
2110
|
+
const pageSize = options.pageSize ?? 50;
|
|
2111
|
+
const limit = options.limit ?? Infinity;
|
|
2112
|
+
const tickets = [];
|
|
2113
|
+
let after = null;
|
|
2114
|
+
while (tickets.length < limit) {
|
|
2115
|
+
const response = await call2(LINEAR_GRAPHQL_URL, {
|
|
2116
|
+
method: "POST",
|
|
2117
|
+
headers: {
|
|
2118
|
+
Authorization: linearAuthHeader(options.auth),
|
|
2119
|
+
"Content-Type": "application/json"
|
|
2120
|
+
},
|
|
2121
|
+
body: JSON.stringify({
|
|
2122
|
+
query: LINEAR_ISSUES_QUERY,
|
|
2123
|
+
variables: {
|
|
2124
|
+
first: Math.min(pageSize, limit - tickets.length),
|
|
2125
|
+
after
|
|
2126
|
+
}
|
|
2127
|
+
})
|
|
2128
|
+
});
|
|
2129
|
+
if (!response.ok) {
|
|
2130
|
+
throw new Error(
|
|
2131
|
+
`Linear returned ${response.status} ${response.statusText}`
|
|
2132
|
+
);
|
|
2133
|
+
}
|
|
2134
|
+
const body = await response.json();
|
|
2135
|
+
if (body.errors?.length) {
|
|
2136
|
+
throw new Error(
|
|
2137
|
+
`Linear rejected the query: ${body.errors.map((e) => e.message).join("; ")}`
|
|
2138
|
+
);
|
|
2139
|
+
}
|
|
2140
|
+
const page = body.data?.issues;
|
|
2141
|
+
for (const node of page?.nodes ?? []) {
|
|
2142
|
+
const ticket = linearIssueToTicket(node);
|
|
2143
|
+
if (ticket) tickets.push(ticket);
|
|
2144
|
+
}
|
|
2145
|
+
if (!page?.pageInfo?.hasNextPage || !page.pageInfo.endCursor) break;
|
|
2146
|
+
after = page.pageInfo.endCursor;
|
|
2147
|
+
}
|
|
2148
|
+
return tickets;
|
|
2149
|
+
}
|
|
2150
|
+
var LINEAR_GRAPHQL_URL, LINEAR_AUTHORIZE_URL, LINEAR_TOKEN_URL, LINEAR_READ_SCOPE, PRIORITY, STATE_TYPE, LINEAR_ISSUES_QUERY;
|
|
2151
|
+
var init_linear = __esm({
|
|
2152
|
+
"../../packages/ingest-core/src/linear.ts"() {
|
|
2153
|
+
"use strict";
|
|
2154
|
+
LINEAR_GRAPHQL_URL = "https://api.linear.app/graphql";
|
|
2155
|
+
LINEAR_AUTHORIZE_URL = "https://linear.app/oauth/authorize";
|
|
2156
|
+
LINEAR_TOKEN_URL = "https://api.linear.app/oauth/token";
|
|
2157
|
+
LINEAR_READ_SCOPE = "read";
|
|
2158
|
+
PRIORITY = {
|
|
2159
|
+
0: "No priority",
|
|
2160
|
+
1: "Urgent",
|
|
2161
|
+
2: "High",
|
|
2162
|
+
3: "Medium",
|
|
2163
|
+
4: "Low"
|
|
2164
|
+
};
|
|
2165
|
+
STATE_TYPE = {
|
|
2166
|
+
triage: "triage",
|
|
2167
|
+
backlog: "backlog",
|
|
2168
|
+
unstarted: "todo",
|
|
2169
|
+
started: "in-progress",
|
|
2170
|
+
completed: "done",
|
|
2171
|
+
canceled: "cancelled"
|
|
2172
|
+
};
|
|
2173
|
+
LINEAR_ISSUES_QUERY = `
|
|
2174
|
+
query EvrexIssues($first: Int!, $after: String) {
|
|
2175
|
+
issues(first: $first, after: $after, orderBy: updatedAt) {
|
|
2176
|
+
nodes {
|
|
2177
|
+
identifier
|
|
2178
|
+
title
|
|
2179
|
+
url
|
|
2180
|
+
priority
|
|
2181
|
+
createdAt
|
|
2182
|
+
updatedAt
|
|
2183
|
+
state { name type }
|
|
2184
|
+
assignee { id displayName }
|
|
2185
|
+
team { key name }
|
|
2186
|
+
project { name }
|
|
2187
|
+
}
|
|
2188
|
+
pageInfo { hasNextPage endCursor }
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
`;
|
|
2192
|
+
}
|
|
2193
|
+
});
|
|
2194
|
+
|
|
2195
|
+
// ../../packages/ingest-core/src/jira.ts
|
|
2196
|
+
function jiraIssueToTicket(issue, siteUrl) {
|
|
2197
|
+
if (!issue.key) return null;
|
|
2198
|
+
const f = issue.fields ?? {};
|
|
2199
|
+
const attributes = {
|
|
2200
|
+
title: f.summary ?? issue.key,
|
|
2201
|
+
status: f.status?.name ?? null,
|
|
2202
|
+
statusCategory: f.status?.statusCategory?.key ? STATUS_CATEGORY[f.status.statusCategory.key] ?? null : null,
|
|
2203
|
+
// Null when the assignee's privacy settings withhold it, which is not the
|
|
2204
|
+
// same claim as an unassigned ticket — `assigneeId` distinguishes them.
|
|
2205
|
+
assignee: f.assignee?.displayName ?? null,
|
|
2206
|
+
assigneeId: f.assignee?.accountId ?? null,
|
|
2207
|
+
// Jira has no team on an issue; the project is the closest equivalent and
|
|
2208
|
+
// is reported as itself rather than smuggled into `team`.
|
|
2209
|
+
team: null,
|
|
2210
|
+
project: f.project?.key ?? null,
|
|
2211
|
+
issueType: f.issuetype?.name ?? null,
|
|
2212
|
+
priority: f.priority?.name ?? null,
|
|
2213
|
+
resolution: f.resolution?.name ?? null
|
|
2214
|
+
};
|
|
2215
|
+
return {
|
|
2216
|
+
externalId: issue.key,
|
|
2217
|
+
kind: "jira",
|
|
2218
|
+
url: `${siteUrl.replace(/\/+$/, "")}/browse/${issue.key}`,
|
|
2219
|
+
createdAt: f.created ?? null,
|
|
2220
|
+
updatedAt: f.updated ?? null,
|
|
2221
|
+
attributes
|
|
2222
|
+
};
|
|
2223
|
+
}
|
|
2224
|
+
function jiraBasicAuth(email, apiToken) {
|
|
2225
|
+
return `Basic ${Buffer.from(`${email}:${apiToken}`).toString("base64")}`;
|
|
2226
|
+
}
|
|
2227
|
+
async function fetchJiraIssues(options) {
|
|
2228
|
+
const call2 = options.fetch ?? globalThis.fetch;
|
|
2229
|
+
const site = options.siteUrl.replace(/\/+$/, "");
|
|
2230
|
+
const pageSize = options.pageSize ?? 100;
|
|
2231
|
+
const limit = options.limit ?? Infinity;
|
|
2232
|
+
const jql = options.jql ?? "ORDER BY updated DESC";
|
|
2233
|
+
const tickets = [];
|
|
2234
|
+
let nextPageToken = null;
|
|
2235
|
+
while (tickets.length < limit) {
|
|
2236
|
+
const response = await call2(`${site}${JIRA_SEARCH_PATH}`, {
|
|
2237
|
+
method: "POST",
|
|
2238
|
+
headers: {
|
|
2239
|
+
Authorization: jiraBasicAuth(options.email, options.apiToken),
|
|
2240
|
+
"Content-Type": "application/json",
|
|
2241
|
+
Accept: "application/json"
|
|
2242
|
+
},
|
|
2243
|
+
body: JSON.stringify({
|
|
2244
|
+
jql,
|
|
2245
|
+
fields: [...JIRA_FIELDS],
|
|
2246
|
+
maxResults: Math.min(pageSize, limit - tickets.length),
|
|
2247
|
+
...nextPageToken ? { nextPageToken } : {}
|
|
2248
|
+
})
|
|
2249
|
+
});
|
|
2250
|
+
if (!response.ok) {
|
|
2251
|
+
const hint = response.status === 410 ? ` \u2014 that is what the removed /rest/api/3/search returns; this client uses ${JIRA_SEARCH_PATH}` : "";
|
|
2252
|
+
throw new Error(
|
|
2253
|
+
`Jira returned ${response.status} ${response.statusText}${hint}`
|
|
2254
|
+
);
|
|
2255
|
+
}
|
|
2256
|
+
const body = await response.json();
|
|
2257
|
+
for (const issue of body.issues ?? []) {
|
|
2258
|
+
const ticket = jiraIssueToTicket(issue, site);
|
|
2259
|
+
if (ticket) tickets.push(ticket);
|
|
2260
|
+
}
|
|
2261
|
+
if (!body.nextPageToken) break;
|
|
2262
|
+
nextPageToken = body.nextPageToken;
|
|
2263
|
+
}
|
|
2264
|
+
return tickets;
|
|
2265
|
+
}
|
|
2266
|
+
var JIRA_SEARCH_PATH, JIRA_FIELDS, STATUS_CATEGORY;
|
|
2267
|
+
var init_jira = __esm({
|
|
2268
|
+
"../../packages/ingest-core/src/jira.ts"() {
|
|
2269
|
+
"use strict";
|
|
2270
|
+
JIRA_SEARCH_PATH = "/rest/api/3/search/jql";
|
|
2271
|
+
JIRA_FIELDS = [
|
|
2272
|
+
"summary",
|
|
2273
|
+
"status",
|
|
2274
|
+
"assignee",
|
|
2275
|
+
"resolution",
|
|
2276
|
+
"created",
|
|
2277
|
+
"updated",
|
|
2278
|
+
"project",
|
|
2279
|
+
"issuetype",
|
|
2280
|
+
"priority"
|
|
2281
|
+
];
|
|
2282
|
+
STATUS_CATEGORY = {
|
|
2283
|
+
new: "todo",
|
|
2284
|
+
indeterminate: "in-progress",
|
|
2285
|
+
done: "done"
|
|
2286
|
+
};
|
|
2287
|
+
}
|
|
2288
|
+
});
|
|
2289
|
+
|
|
2290
|
+
// ../../packages/ingest-core/src/diff-parser.ts
|
|
2291
|
+
function parseUnifiedDiffHunks(diffText) {
|
|
2292
|
+
const hunks = [];
|
|
2293
|
+
let current = null;
|
|
2294
|
+
for (const line of diffText.split("\n")) {
|
|
2295
|
+
if (HUNK_HEADER_RE.test(line)) {
|
|
2296
|
+
current = { header: line, lines: [] };
|
|
2297
|
+
hunks.push(current);
|
|
2298
|
+
continue;
|
|
2299
|
+
}
|
|
2300
|
+
if (!current) continue;
|
|
2301
|
+
if (line.startsWith("+++") || line.startsWith("---")) continue;
|
|
2302
|
+
if (line.startsWith("+")) {
|
|
2303
|
+
current.lines.push({ kind: "add", text: line.slice(1) });
|
|
2304
|
+
} else if (line.startsWith("-")) {
|
|
2305
|
+
current.lines.push({ kind: "del", text: line.slice(1) });
|
|
2306
|
+
} else if (line.startsWith(" ")) {
|
|
2307
|
+
current.lines.push({ kind: "ctx", text: line.slice(1) });
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
return hunks;
|
|
2311
|
+
}
|
|
2312
|
+
var HUNK_HEADER_RE;
|
|
2313
|
+
var init_diff_parser = __esm({
|
|
2314
|
+
"../../packages/ingest-core/src/diff-parser.ts"() {
|
|
2315
|
+
"use strict";
|
|
2316
|
+
HUNK_HEADER_RE = /^@@ .+? @@.*$/;
|
|
2317
|
+
}
|
|
2318
|
+
});
|
|
2319
|
+
|
|
2320
|
+
// ../../packages/ingest-core/src/trace-lines.ts
|
|
2321
|
+
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
2322
|
+
function isHunkHeader(line) {
|
|
2323
|
+
return line.startsWith("@@");
|
|
2324
|
+
}
|
|
2325
|
+
function parseLineLog(raw) {
|
|
2326
|
+
const commits = [];
|
|
2327
|
+
let current = null;
|
|
2328
|
+
let hunk = null;
|
|
2329
|
+
const closeHunk = () => {
|
|
2330
|
+
if (current && hunk) current.hunks.push(hunk);
|
|
2331
|
+
hunk = null;
|
|
2332
|
+
};
|
|
2333
|
+
for (const line of raw.split("\n")) {
|
|
2334
|
+
if (line.startsWith(COMMIT_MARK)) {
|
|
2335
|
+
closeHunk();
|
|
2336
|
+
const [sha, at, author, ...rest] = line.slice(COMMIT_MARK.length).split(FIELD_SEP2);
|
|
2337
|
+
if (!sha || !at) {
|
|
2338
|
+
current = null;
|
|
2339
|
+
continue;
|
|
2340
|
+
}
|
|
2341
|
+
current = {
|
|
2342
|
+
sha,
|
|
2343
|
+
at,
|
|
2344
|
+
author: author ?? "",
|
|
2345
|
+
subject: rest.join(FIELD_SEP2),
|
|
2346
|
+
hunks: [],
|
|
2347
|
+
createdFile: false
|
|
2348
|
+
};
|
|
2349
|
+
commits.push(current);
|
|
2350
|
+
continue;
|
|
2351
|
+
}
|
|
2352
|
+
if (!current) continue;
|
|
2353
|
+
if (line.startsWith("--- ")) {
|
|
2354
|
+
if (line.trim() === "--- /dev/null") current.createdFile = true;
|
|
2355
|
+
continue;
|
|
2356
|
+
}
|
|
2357
|
+
if (line.startsWith("+++ ") || line.startsWith("diff --git ")) continue;
|
|
2358
|
+
if (isHunkHeader(line)) {
|
|
2359
|
+
closeHunk();
|
|
2360
|
+
hunk = { header: line, lines: [] };
|
|
2361
|
+
continue;
|
|
2362
|
+
}
|
|
2363
|
+
if (hunk && (line.startsWith(" ") || line.startsWith("+") || line.startsWith("-"))) {
|
|
2364
|
+
hunk.lines.push(line);
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
closeHunk();
|
|
2368
|
+
return commits;
|
|
2369
|
+
}
|
|
2370
|
+
function detectTruncatedHistory(commits, repoPath) {
|
|
2371
|
+
const oldest = commits[commits.length - 1];
|
|
2372
|
+
if (!oldest) return false;
|
|
2373
|
+
const added = oldest.hunks.flatMap((h) => h.lines).filter((l) => l.startsWith("+")).map((l) => l.slice(1).trim()).filter((l) => l.length >= MOVED_LINE_MIN_LENGTH);
|
|
2374
|
+
const body = oldest.hunks.flatMap((h) => h.lines);
|
|
2375
|
+
if (body.length === 0 || !body.every((l) => l.startsWith("+"))) return false;
|
|
2376
|
+
if (added.length === 0 || !repoPath) return false;
|
|
2377
|
+
const removedElsewhere = deletedLinesElsewhere(repoPath, oldest.sha);
|
|
2378
|
+
if (removedElsewhere.size === 0) return false;
|
|
2379
|
+
const matched = added.filter((l) => removedElsewhere.has(l)).length;
|
|
2380
|
+
return matched / added.length >= MOVED_LINE_FRACTION;
|
|
2381
|
+
}
|
|
2382
|
+
function deletedLinesElsewhere(repoPath, sha) {
|
|
2383
|
+
const out = /* @__PURE__ */ new Set();
|
|
2384
|
+
let raw;
|
|
2385
|
+
try {
|
|
2386
|
+
raw = execFileSync3(
|
|
2387
|
+
"git",
|
|
2388
|
+
[
|
|
2389
|
+
"show",
|
|
2390
|
+
"--format=",
|
|
2391
|
+
"--unified=0",
|
|
2392
|
+
"--no-color",
|
|
2393
|
+
// Without this git collapses a delete-plus-identical-add into
|
|
2394
|
+
// "rename from/to" with no line content at all — so the very case
|
|
2395
|
+
// this function exists to detect would produce nothing to match.
|
|
2396
|
+
"--no-renames",
|
|
2397
|
+
sha
|
|
2398
|
+
],
|
|
2399
|
+
{
|
|
2400
|
+
cwd: repoPath,
|
|
2401
|
+
maxBuffer: 1024 * 1024 * 64,
|
|
2402
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
2403
|
+
}
|
|
2404
|
+
).toString("utf-8");
|
|
2405
|
+
} catch {
|
|
2406
|
+
return out;
|
|
2407
|
+
}
|
|
2408
|
+
for (const line of raw.split("\n")) {
|
|
2409
|
+
if (!line.startsWith("-") || line.startsWith("---")) continue;
|
|
2410
|
+
const text = line.slice(1).trim();
|
|
2411
|
+
if (text.length >= MOVED_LINE_MIN_LENGTH) out.add(text);
|
|
2412
|
+
}
|
|
2413
|
+
return out;
|
|
2414
|
+
}
|
|
2415
|
+
function committedLineCount(repoPath, file, rev = "HEAD") {
|
|
2416
|
+
try {
|
|
2417
|
+
const out = execFileSync3("git", ["show", `${rev}:${file}`], {
|
|
2418
|
+
cwd: repoPath,
|
|
2419
|
+
maxBuffer: 1024 * 1024 * 64,
|
|
2420
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
2421
|
+
}).toString("utf-8");
|
|
2422
|
+
const lines = out.split("\n");
|
|
2423
|
+
const count = lines[lines.length - 1] === "" ? lines.length - 1 : lines.length;
|
|
2424
|
+
return count > 0 ? count : null;
|
|
2425
|
+
} catch {
|
|
2426
|
+
return null;
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
function traceLines(repoPath, file, from, to, rev) {
|
|
2430
|
+
const start = Math.max(MIN_LINE, Math.floor(from));
|
|
2431
|
+
const end = to <= 0 ? committedLineCount(repoPath, file, rev) ?? Math.max(start, 1) : Math.max(start, Math.floor(to));
|
|
2432
|
+
let raw;
|
|
2433
|
+
try {
|
|
2434
|
+
raw = execFileSync3(
|
|
2435
|
+
"git",
|
|
2436
|
+
[
|
|
2437
|
+
"log",
|
|
2438
|
+
// argv array, never a shell string: `file` is caller-supplied and can
|
|
2439
|
+
// legitimately contain spaces, quotes or a leading dash.
|
|
2440
|
+
`-L${start},${end}:${file}`,
|
|
2441
|
+
`--format=${COMMIT_MARK}%H${FIELD_SEP2}%aI${FIELD_SEP2}%an${FIELD_SEP2}%s`,
|
|
2442
|
+
// Anchors the walk at the revision the caller's line numbers came
|
|
2443
|
+
// from. Omitted, git starts at HEAD, which is right for a working-copy
|
|
2444
|
+
// selection and wrong for one taken from an old diff.
|
|
2445
|
+
...rev ? [rev] : []
|
|
2446
|
+
],
|
|
2447
|
+
{
|
|
2448
|
+
cwd: repoPath,
|
|
2449
|
+
maxBuffer: 1024 * 1024 * 64,
|
|
2450
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
2451
|
+
}
|
|
2452
|
+
).toString("utf-8");
|
|
2453
|
+
} catch {
|
|
2454
|
+
return { commits: [], historyMayBeTruncated: false };
|
|
2455
|
+
}
|
|
2456
|
+
const commits = parseLineLog(raw);
|
|
2457
|
+
return {
|
|
2458
|
+
commits,
|
|
2459
|
+
historyMayBeTruncated: detectTruncatedHistory(commits, repoPath)
|
|
2460
|
+
};
|
|
2461
|
+
}
|
|
2462
|
+
var COMMIT_MARK, FIELD_SEP2, MIN_LINE, MOVED_LINE_MIN_LENGTH, MOVED_LINE_FRACTION;
|
|
2463
|
+
var init_trace_lines = __esm({
|
|
2464
|
+
"../../packages/ingest-core/src/trace-lines.ts"() {
|
|
2465
|
+
"use strict";
|
|
2466
|
+
COMMIT_MARK = "@@EVREX-COMMIT@@";
|
|
2467
|
+
FIELD_SEP2 = "";
|
|
2468
|
+
MIN_LINE = 1;
|
|
2469
|
+
MOVED_LINE_MIN_LENGTH = 6;
|
|
2470
|
+
MOVED_LINE_FRACTION = 0.5;
|
|
2471
|
+
}
|
|
2472
|
+
});
|
|
2473
|
+
|
|
2474
|
+
// ../../packages/ingest-core/src/trace-target.ts
|
|
2475
|
+
function parseTraceTarget(raw) {
|
|
2476
|
+
const trimmed = raw.trim();
|
|
2477
|
+
if (!trimmed) return null;
|
|
2478
|
+
const match = /^(.*?):(\d+)(?:\s*[-\u2013:]\s*(\d+))?$/.exec(trimmed);
|
|
2479
|
+
if (!match) return null;
|
|
2480
|
+
const [, file, fromText, toText] = match;
|
|
2481
|
+
if (!file) return null;
|
|
2482
|
+
const from = Number(fromText);
|
|
2483
|
+
const to = toText ? Number(toText) : from;
|
|
2484
|
+
if (!Number.isFinite(from) || from < 1) return null;
|
|
2485
|
+
if (!Number.isFinite(to) || to < 1) return null;
|
|
2486
|
+
return { file, from: Math.min(from, to), to: Math.max(from, to) };
|
|
2487
|
+
}
|
|
2488
|
+
var init_trace_target = __esm({
|
|
2489
|
+
"../../packages/ingest-core/src/trace-target.ts"() {
|
|
2490
|
+
"use strict";
|
|
2491
|
+
}
|
|
2492
|
+
});
|
|
703
2493
|
|
|
704
2494
|
// ../../packages/ingest-core/src/gemini-sessions.ts
|
|
705
|
-
import { readFileSync as
|
|
706
|
-
var SYNTHETIC = /^\s*<session_context>/;
|
|
2495
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
707
2496
|
function textOf(content) {
|
|
708
2497
|
if (typeof content === "string") return content;
|
|
709
2498
|
if (!Array.isArray(content)) return "";
|
|
@@ -714,7 +2503,7 @@ function textOf(content) {
|
|
|
714
2503
|
function parseGeminiSessionFile(filePath, repoPath, repoId) {
|
|
715
2504
|
let raw;
|
|
716
2505
|
try {
|
|
717
|
-
raw =
|
|
2506
|
+
raw = readFileSync4(filePath, "utf-8");
|
|
718
2507
|
} catch {
|
|
719
2508
|
return null;
|
|
720
2509
|
}
|
|
@@ -760,6 +2549,7 @@ function parseGeminiSessionFile(filePath, repoPath, repoId) {
|
|
|
760
2549
|
// as a user message. Attributing that to the person is the same bug this
|
|
761
2550
|
// repo already fixed once for Claude Code's tool results.
|
|
762
2551
|
isSyntheticInput: role === "user" && SYNTHETIC.test(text),
|
|
2552
|
+
isToolError: false,
|
|
763
2553
|
// Gemini records a model per message but no token counts in the
|
|
764
2554
|
// transcript; unknown rather than zero.
|
|
765
2555
|
usage: { ...EMPTY_USAGE, model: record.model ?? null }
|
|
@@ -783,12 +2573,24 @@ function parseGeminiSessionFile(filePath, repoPath, repoId) {
|
|
|
783
2573
|
redactionCount: redactionCount + rawRedactions,
|
|
784
2574
|
turns,
|
|
785
2575
|
rawContent,
|
|
2576
|
+
branch: null,
|
|
2577
|
+
parentSessionId: null,
|
|
2578
|
+
subagent: null,
|
|
786
2579
|
rawFormat: "jsonl"
|
|
787
2580
|
};
|
|
788
2581
|
}
|
|
2582
|
+
var SYNTHETIC;
|
|
2583
|
+
var init_gemini_sessions = __esm({
|
|
2584
|
+
"../../packages/ingest-core/src/gemini-sessions.ts"() {
|
|
2585
|
+
"use strict";
|
|
2586
|
+
init_derive_uuid();
|
|
2587
|
+
init_redact();
|
|
2588
|
+
init_types();
|
|
2589
|
+
SYNTHETIC = /^\s*<session_context>/;
|
|
2590
|
+
}
|
|
2591
|
+
});
|
|
789
2592
|
|
|
790
2593
|
// ../../packages/ingest-core/src/transcript-parsers.ts
|
|
791
|
-
var PARSERS = [parseSessionFile, parseCodexSessionFile, parseGeminiSessionFile];
|
|
792
2594
|
function parseTranscriptFile(path, repoPath, repoId) {
|
|
793
2595
|
for (const parse of PARSERS) {
|
|
794
2596
|
const parsed = parse(path, repoPath, repoId);
|
|
@@ -796,21 +2598,598 @@ function parseTranscriptFile(path, repoPath, repoId) {
|
|
|
796
2598
|
}
|
|
797
2599
|
return null;
|
|
798
2600
|
}
|
|
2601
|
+
var PARSERS;
|
|
2602
|
+
var init_transcript_parsers = __esm({
|
|
2603
|
+
"../../packages/ingest-core/src/transcript-parsers.ts"() {
|
|
2604
|
+
"use strict";
|
|
2605
|
+
init_claude_sessions();
|
|
2606
|
+
init_codex_sessions();
|
|
2607
|
+
init_gemini_sessions();
|
|
2608
|
+
PARSERS = [parseSessionFile, parseCodexSessionFile, parseGeminiSessionFile];
|
|
2609
|
+
}
|
|
2610
|
+
});
|
|
2611
|
+
|
|
2612
|
+
// ../../packages/ingest-core/src/slack-threads.ts
|
|
2613
|
+
function displayName(users, id) {
|
|
2614
|
+
if (!id) return "unknown";
|
|
2615
|
+
const user = users.get(id);
|
|
2616
|
+
return user?.profile?.real_name ?? user?.real_name ?? user?.profile?.display_name ?? user?.name ?? id;
|
|
2617
|
+
}
|
|
2618
|
+
function slackTimestamp(ts) {
|
|
2619
|
+
const seconds = Number(ts);
|
|
2620
|
+
if (!Number.isFinite(seconds) || seconds <= 0) return null;
|
|
2621
|
+
return new Date(seconds * 1e3).toISOString();
|
|
2622
|
+
}
|
|
2623
|
+
function readableText(text, users) {
|
|
2624
|
+
return text.replace(/<@([A-Z0-9]+)(?:\|[^>]*)?>/g, (_, id) => `@${displayName(users, id)}`).replace(/<#[A-Z0-9]+\|([^>]*)>/g, (_, name) => `#${name}`).replace(/<(https?:[^>|]+)\|([^>]*)>/g, (_, __, label) => label).replace(/<(https?:[^>]+)>/g, (_, url) => url).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
2625
|
+
}
|
|
2626
|
+
function isHumanMessage(message) {
|
|
2627
|
+
if (message.type && message.type !== "message") return false;
|
|
2628
|
+
if (message.subtype && MACHINE_SUBTYPES.has(message.subtype)) return false;
|
|
2629
|
+
if (message.bot_id && !message.user) return false;
|
|
2630
|
+
return Boolean(message.text?.trim());
|
|
2631
|
+
}
|
|
2632
|
+
function groupIntoThreads(messages) {
|
|
2633
|
+
const threads = /* @__PURE__ */ new Map();
|
|
2634
|
+
for (const message of messages) {
|
|
2635
|
+
if (!isHumanMessage(message)) continue;
|
|
2636
|
+
const key = message.thread_ts ?? message.ts;
|
|
2637
|
+
if (!key) continue;
|
|
2638
|
+
threads.set(key, [...threads.get(key) ?? [], message]);
|
|
2639
|
+
}
|
|
2640
|
+
return [...threads.values()].map(
|
|
2641
|
+
(thread) => [...thread].sort((a, b) => Number(a.ts ?? 0) - Number(b.ts ?? 0))
|
|
2642
|
+
);
|
|
2643
|
+
}
|
|
2644
|
+
function parseSlackExport(messages, options) {
|
|
2645
|
+
const users = /* @__PURE__ */ new Map();
|
|
2646
|
+
for (const user of options.users ?? []) if (user.id) users.set(user.id, user);
|
|
2647
|
+
const out = [];
|
|
2648
|
+
for (const thread of groupIntoThreads(messages)) {
|
|
2649
|
+
const root = thread[0];
|
|
2650
|
+
const rootTs = root?.thread_ts ?? root?.ts;
|
|
2651
|
+
const startedAt = slackTimestamp(rootTs ?? void 0);
|
|
2652
|
+
if (!rootTs || !startedAt) continue;
|
|
2653
|
+
const turns = [];
|
|
2654
|
+
let redactionCount = 0;
|
|
2655
|
+
for (const message of thread) {
|
|
2656
|
+
const ts = slackTimestamp(message.ts);
|
|
2657
|
+
if (!ts) continue;
|
|
2658
|
+
const { text, count } = redactSecrets(
|
|
2659
|
+
readableText(message.text ?? "", users)
|
|
2660
|
+
);
|
|
2661
|
+
if (!text.trim()) continue;
|
|
2662
|
+
redactionCount += count;
|
|
2663
|
+
turns.push({
|
|
2664
|
+
// A uuid, because that is what the columns these land in are. The
|
|
2665
|
+
// readable `slack:<channel>:<ts>` key is kept on `sourceFile`.
|
|
2666
|
+
id: deriveSlackTurnId(options.channel, message.ts),
|
|
2667
|
+
sessionId: deriveSlackSessionId(options.channel, rootTs),
|
|
2668
|
+
// Everything here was typed by a person. There is no assistant side,
|
|
2669
|
+
// and marking any of it otherwise would let it be read as recovered
|
|
2670
|
+
// agent reasoning.
|
|
2671
|
+
role: "user",
|
|
2672
|
+
ts,
|
|
2673
|
+
text: `${displayName(users, message.user)}: ${text}`,
|
|
2674
|
+
filesTouched: [],
|
|
2675
|
+
editedLines: [],
|
|
2676
|
+
parentUuid: null,
|
|
2677
|
+
isSidechain: false,
|
|
2678
|
+
redacted: count > 0,
|
|
2679
|
+
isSyntheticInput: false,
|
|
2680
|
+
isToolError: false,
|
|
2681
|
+
// Neither source reports what a turn cost, so it is unknown rather than free.
|
|
2682
|
+
usage: { ...EMPTY_USAGE }
|
|
2683
|
+
});
|
|
2684
|
+
}
|
|
2685
|
+
const size = turns.reduce((total, turn) => total + turn.text.length, 0);
|
|
2686
|
+
if (turns.length < MIN_THREAD_MESSAGES) continue;
|
|
2687
|
+
const floor = turns.length === 1 ? MIN_SOLO_CHARS : MIN_THREAD_CHARS;
|
|
2688
|
+
if (size < floor) continue;
|
|
2689
|
+
out.push({
|
|
2690
|
+
session: {
|
|
2691
|
+
id: deriveSlackSessionId(options.channel, rootTs),
|
|
2692
|
+
agentKind: "slack",
|
|
2693
|
+
repoId: options.repoId,
|
|
2694
|
+
cwd: options.cwd,
|
|
2695
|
+
startedAt,
|
|
2696
|
+
endedAt: turns[turns.length - 1]?.ts ?? startedAt,
|
|
2697
|
+
turnCount: turns.length,
|
|
2698
|
+
// The first message is what the thread is about, near enough, and it
|
|
2699
|
+
// is what Slack itself shows in a thread list.
|
|
2700
|
+
aiTitle: (turns[0]?.text ?? "").slice(0, 120),
|
|
2701
|
+
author: displayName(users, root?.user),
|
|
2702
|
+
sourceFile: `slack/${options.channel}/${rootTs}.json`,
|
|
2703
|
+
redactionCount,
|
|
2704
|
+
// The thread exactly as Slack gave it, redacted like every other
|
|
2705
|
+
// archived transcript: a teammate re-materialising this must not
|
|
2706
|
+
// receive a key the author pasted into a channel.
|
|
2707
|
+
// A Slack thread runs no commands; nothing here can place a commit.
|
|
2708
|
+
committedSubjects: [],
|
|
2709
|
+
rawContent: JSON.stringify(
|
|
2710
|
+
thread.map((message) => ({
|
|
2711
|
+
...message,
|
|
2712
|
+
text: redactSecrets(message.text ?? "").text
|
|
2713
|
+
})),
|
|
2714
|
+
null,
|
|
2715
|
+
2
|
|
2716
|
+
),
|
|
2717
|
+
branch: null,
|
|
2718
|
+
parentSessionId: null,
|
|
2719
|
+
subagent: null,
|
|
2720
|
+
rawFormat: "json",
|
|
2721
|
+
turns
|
|
2722
|
+
},
|
|
2723
|
+
turns
|
|
2724
|
+
});
|
|
2725
|
+
}
|
|
2726
|
+
return out;
|
|
2727
|
+
}
|
|
2728
|
+
var MIN_THREAD_MESSAGES, MIN_THREAD_CHARS, MIN_SOLO_CHARS, MACHINE_SUBTYPES;
|
|
2729
|
+
var init_slack_threads = __esm({
|
|
2730
|
+
"../../packages/ingest-core/src/slack-threads.ts"() {
|
|
2731
|
+
"use strict";
|
|
2732
|
+
init_redact();
|
|
2733
|
+
init_derive_uuid();
|
|
2734
|
+
init_types();
|
|
2735
|
+
MIN_THREAD_MESSAGES = 1;
|
|
2736
|
+
MIN_THREAD_CHARS = 80;
|
|
2737
|
+
MIN_SOLO_CHARS = 200;
|
|
2738
|
+
MACHINE_SUBTYPES = /* @__PURE__ */ new Set([
|
|
2739
|
+
"channel_join",
|
|
2740
|
+
"channel_leave",
|
|
2741
|
+
"channel_topic",
|
|
2742
|
+
"channel_purpose",
|
|
2743
|
+
"channel_name",
|
|
2744
|
+
"channel_archive",
|
|
2745
|
+
"channel_unarchive",
|
|
2746
|
+
"bot_message",
|
|
2747
|
+
"thread_broadcast_join"
|
|
2748
|
+
]);
|
|
2749
|
+
}
|
|
2750
|
+
});
|
|
2751
|
+
|
|
2752
|
+
// ../../packages/ingest-core/src/slack-client.ts
|
|
2753
|
+
async function call(method, params, options, attempt = 0) {
|
|
2754
|
+
const doFetch = options.fetchImpl ?? fetch;
|
|
2755
|
+
const sleep = options.sleep ?? wait;
|
|
2756
|
+
const query = new URLSearchParams(params).toString();
|
|
2757
|
+
const response = options.post ? await doFetch(`${API}/${method}`, {
|
|
2758
|
+
method: "POST",
|
|
2759
|
+
headers: {
|
|
2760
|
+
Authorization: `Bearer ${options.token}`,
|
|
2761
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
2762
|
+
},
|
|
2763
|
+
body: query
|
|
2764
|
+
}) : await doFetch(`${API}/${method}?${query}`, {
|
|
2765
|
+
headers: { Authorization: `Bearer ${options.token}` }
|
|
2766
|
+
});
|
|
2767
|
+
if (response.status === 429 && attempt < 5) {
|
|
2768
|
+
const header = response.headers.get("retry-after");
|
|
2769
|
+
const seconds = header ? Number(header) : NaN;
|
|
2770
|
+
await sleep((Number.isFinite(seconds) ? seconds : 30) * 1e3);
|
|
2771
|
+
return call(method, params, options, attempt + 1);
|
|
2772
|
+
}
|
|
2773
|
+
const body = await response.json();
|
|
2774
|
+
if (!body.ok) {
|
|
2775
|
+
const code = body.error ?? `http_${response.status}`;
|
|
2776
|
+
throw new SlackError(code, PERMANENT.has(code));
|
|
2777
|
+
}
|
|
2778
|
+
return body;
|
|
2779
|
+
}
|
|
2780
|
+
async function paginate(method, params, pick, options) {
|
|
2781
|
+
const out = [];
|
|
2782
|
+
let cursor;
|
|
2783
|
+
do {
|
|
2784
|
+
const body = await call(
|
|
2785
|
+
method,
|
|
2786
|
+
{ ...params, limit: String(PAGE_SIZE), ...cursor ? { cursor } : {} },
|
|
2787
|
+
options
|
|
2788
|
+
);
|
|
2789
|
+
out.push(...pick(body) ?? []);
|
|
2790
|
+
cursor = body.response_metadata?.next_cursor || void 0;
|
|
2791
|
+
} while (cursor);
|
|
2792
|
+
return out;
|
|
2793
|
+
}
|
|
2794
|
+
async function listChannels(options) {
|
|
2795
|
+
const channels = await paginate(
|
|
2796
|
+
"conversations.list",
|
|
2797
|
+
{ types: "public_channel,private_channel", exclude_archived: "true" },
|
|
2798
|
+
(body) => body.channels,
|
|
2799
|
+
options
|
|
2800
|
+
);
|
|
2801
|
+
return channels.filter(
|
|
2802
|
+
(channel) => !channel.is_private || channel.is_member !== false
|
|
2803
|
+
);
|
|
2804
|
+
}
|
|
2805
|
+
async function joinChannel(channel, options) {
|
|
2806
|
+
await call("conversations.join", { channel }, { ...options, post: true });
|
|
2807
|
+
}
|
|
2808
|
+
async function postMessage(channel, text, options) {
|
|
2809
|
+
await call(
|
|
2810
|
+
"chat.postMessage",
|
|
2811
|
+
{ channel, text, unfurl_links: "false", unfurl_media: "false" },
|
|
2812
|
+
{ ...options, post: true }
|
|
2813
|
+
);
|
|
2814
|
+
}
|
|
2815
|
+
async function listUsers(options) {
|
|
2816
|
+
return paginate("users.list", {}, (body) => body.members, options);
|
|
2817
|
+
}
|
|
2818
|
+
async function fetchChannelMessages(channel, options) {
|
|
2819
|
+
const top = await paginate(
|
|
2820
|
+
"conversations.history",
|
|
2821
|
+
{ channel, ...options.oldest ? { oldest: options.oldest } : {} },
|
|
2822
|
+
(body) => body.messages,
|
|
2823
|
+
options
|
|
2824
|
+
);
|
|
2825
|
+
const all = [];
|
|
2826
|
+
for (const message of top) {
|
|
2827
|
+
all.push(message);
|
|
2828
|
+
const replyCount = message.reply_count ?? 0;
|
|
2829
|
+
if (replyCount > 0 && message.ts) {
|
|
2830
|
+
options.onProgress?.(`reading a thread with ${replyCount} replies`);
|
|
2831
|
+
const replies = await paginate(
|
|
2832
|
+
"conversations.replies",
|
|
2833
|
+
{ channel, ts: message.ts },
|
|
2834
|
+
(body) => body.messages,
|
|
2835
|
+
options
|
|
2836
|
+
);
|
|
2837
|
+
all.push(...replies.filter((reply) => reply.ts !== message.ts));
|
|
2838
|
+
}
|
|
2839
|
+
}
|
|
2840
|
+
return all;
|
|
2841
|
+
}
|
|
2842
|
+
var API, PAGE_SIZE, PERMANENT, SlackError, wait;
|
|
2843
|
+
var init_slack_client = __esm({
|
|
2844
|
+
"../../packages/ingest-core/src/slack-client.ts"() {
|
|
2845
|
+
"use strict";
|
|
2846
|
+
API = "https://slack.com/api";
|
|
2847
|
+
PAGE_SIZE = 200;
|
|
2848
|
+
PERMANENT = /* @__PURE__ */ new Set([
|
|
2849
|
+
"invalid_auth",
|
|
2850
|
+
"account_inactive",
|
|
2851
|
+
"token_revoked",
|
|
2852
|
+
"missing_scope",
|
|
2853
|
+
"not_allowed_token_type"
|
|
2854
|
+
]);
|
|
2855
|
+
SlackError = class extends Error {
|
|
2856
|
+
constructor(slackCode, permanent) {
|
|
2857
|
+
super(`Slack returned ${slackCode}`);
|
|
2858
|
+
this.slackCode = slackCode;
|
|
2859
|
+
this.permanent = permanent;
|
|
2860
|
+
this.name = "SlackError";
|
|
2861
|
+
}
|
|
2862
|
+
};
|
|
2863
|
+
wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
2864
|
+
}
|
|
2865
|
+
});
|
|
2866
|
+
|
|
2867
|
+
// ../../packages/ingest-core/src/line-survival.ts
|
|
2868
|
+
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
2869
|
+
function git2(repoPath, args) {
|
|
2870
|
+
return execFileSync4("git", args, {
|
|
2871
|
+
cwd: repoPath,
|
|
2872
|
+
maxBuffer: 1024 * 1024 * 64,
|
|
2873
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
2874
|
+
}).toString("utf-8");
|
|
2875
|
+
}
|
|
2876
|
+
function addedByFile(repoPath, sha) {
|
|
2877
|
+
const out = /* @__PURE__ */ new Map();
|
|
2878
|
+
const raw = git2(repoPath, ["diff-tree", "--root", "--no-commit-id", "--numstat", "-r", "-M", sha]);
|
|
2879
|
+
for (const line of raw.split("\n")) {
|
|
2880
|
+
const [added, , path] = line.split(" ");
|
|
2881
|
+
if (!path || added === "-") continue;
|
|
2882
|
+
out.set(path, Number(added) || 0);
|
|
2883
|
+
}
|
|
2884
|
+
return out;
|
|
2885
|
+
}
|
|
2886
|
+
function survivingIn(repoPath, sha, path) {
|
|
2887
|
+
let raw;
|
|
2888
|
+
try {
|
|
2889
|
+
raw = git2(repoPath, ["blame", "-w", "-M", "--line-porcelain", "HEAD", "--", path]);
|
|
2890
|
+
} catch {
|
|
2891
|
+
return 0;
|
|
2892
|
+
}
|
|
2893
|
+
let n = 0;
|
|
2894
|
+
for (const line of raw.split("\n")) {
|
|
2895
|
+
if (line.length > 41 && line.charCodeAt(0) !== 9 && line.startsWith(sha.slice(0, 40)) && line[40] === " ") {
|
|
2896
|
+
n += 1;
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2899
|
+
return n;
|
|
2900
|
+
}
|
|
2901
|
+
function measureLineSurvival(repoPath, sha, now = /* @__PURE__ */ new Date()) {
|
|
2902
|
+
const full = git2(repoPath, ["rev-parse", sha]).trim();
|
|
2903
|
+
const added = addedByFile(repoPath, full);
|
|
2904
|
+
let addedLines = 0;
|
|
2905
|
+
let survivingLines = 0;
|
|
2906
|
+
for (const [path, n] of added) {
|
|
2907
|
+
addedLines += n;
|
|
2908
|
+
if (n > 0) survivingLines += survivingIn(repoPath, full, path);
|
|
2909
|
+
}
|
|
2910
|
+
return { sha: full, addedLines, survivingLines: Math.min(survivingLines, addedLines), measuredAt: now.toISOString() };
|
|
2911
|
+
}
|
|
2912
|
+
function commitsOlderThan(repoPath, minAgeDays, limit, now = /* @__PURE__ */ new Date()) {
|
|
2913
|
+
const before = new Date(now.getTime() - minAgeDays * 864e5).toISOString();
|
|
2914
|
+
const raw = git2(repoPath, ["log", `--before=${before}`, `--max-count=${limit}`, "--format=%H%x09%cI", "HEAD"]);
|
|
2915
|
+
return raw.split("\n").filter(Boolean).map((line) => {
|
|
2916
|
+
const [sha, at] = line.split(" ");
|
|
2917
|
+
return { sha, at };
|
|
2918
|
+
});
|
|
2919
|
+
}
|
|
2920
|
+
var init_line_survival = __esm({
|
|
2921
|
+
"../../packages/ingest-core/src/line-survival.ts"() {
|
|
2922
|
+
"use strict";
|
|
2923
|
+
}
|
|
2924
|
+
});
|
|
2925
|
+
|
|
2926
|
+
// ../../packages/ingest-core/src/index.ts
|
|
2927
|
+
var src_exports = {};
|
|
2928
|
+
__export(src_exports, {
|
|
2929
|
+
CONVERSATION_KINDS: () => CONVERSATION_KINDS,
|
|
2930
|
+
EMPTY_USAGE: () => EMPTY_USAGE,
|
|
2931
|
+
EVREX_SESSION_TRAILER_KEY: () => EVREX_SESSION_TRAILER_KEY,
|
|
2932
|
+
JIRA_FIELDS: () => JIRA_FIELDS,
|
|
2933
|
+
JIRA_SEARCH_PATH: () => JIRA_SEARCH_PATH,
|
|
2934
|
+
LINEAR_AUTHORIZE_URL: () => LINEAR_AUTHORIZE_URL,
|
|
2935
|
+
LINEAR_GRAPHQL_URL: () => LINEAR_GRAPHQL_URL,
|
|
2936
|
+
LINEAR_ISSUES_QUERY: () => LINEAR_ISSUES_QUERY,
|
|
2937
|
+
LINEAR_READ_SCOPE: () => LINEAR_READ_SCOPE,
|
|
2938
|
+
LINEAR_TOKEN_URL: () => LINEAR_TOKEN_URL,
|
|
2939
|
+
MIN_MEANINGFUL_LINE_LENGTH: () => MIN_MEANINGFUL_LINE_LENGTH,
|
|
2940
|
+
PARSER_VERSION: () => PARSER_VERSION,
|
|
2941
|
+
PROVENANCE_TRAILERS: () => PROVENANCE_TRAILERS,
|
|
2942
|
+
REFERENCE_KINDS: () => REFERENCE_KINDS,
|
|
2943
|
+
SOURCE_KINDS: () => SOURCE_KINDS,
|
|
2944
|
+
STATED_TRAILERS: () => STATED_TRAILERS,
|
|
2945
|
+
SlackError: () => SlackError,
|
|
2946
|
+
advanceCursor: () => advanceCursor,
|
|
2947
|
+
agentTrailersOf: () => agentTrailersOf,
|
|
2948
|
+
buildCopilotSession: () => buildCopilotSession,
|
|
2949
|
+
buildOpenCodeSession: () => buildOpenCodeSession,
|
|
2950
|
+
changedSessions: () => changedSessions,
|
|
2951
|
+
codexSessionsDir: () => codexSessionsDir,
|
|
2952
|
+
collectRepoData: () => collectRepoData,
|
|
2953
|
+
commitsOlderThan: () => commitsOlderThan,
|
|
2954
|
+
committedSubjects: () => committedSubjects,
|
|
2955
|
+
copilotSessionsDir: () => copilotSessionsDir,
|
|
2956
|
+
cursorStateDbPath: () => cursorStateDbPath,
|
|
2957
|
+
deriveCodexTurnId: () => deriveCodexTurnId,
|
|
2958
|
+
deriveCursorSessionId: () => deriveCursorSessionId,
|
|
2959
|
+
deriveOpenCodeSessionId: () => deriveOpenCodeSessionId,
|
|
2960
|
+
deriveRepoId: () => deriveRepoId,
|
|
2961
|
+
deriveSlackSessionId: () => deriveSlackSessionId,
|
|
2962
|
+
deriveSlackTurnId: () => deriveSlackTurnId,
|
|
2963
|
+
deriveTicketId: () => deriveTicketId,
|
|
2964
|
+
deriveUuid: () => deriveUuid,
|
|
2965
|
+
detectTruncatedHistory: () => detectTruncatedHistory,
|
|
2966
|
+
fetchChannelMessages: () => fetchChannelMessages,
|
|
2967
|
+
fetchJiraIssues: () => fetchJiraIssues,
|
|
2968
|
+
fetchLinearIssues: () => fetchLinearIssues,
|
|
2969
|
+
findCodexSessionFiles: () => findCodexSessionFiles,
|
|
2970
|
+
findCopilotSessionDirs: () => findCopilotSessionDirs,
|
|
2971
|
+
findSessionFiles: () => findSessionFiles,
|
|
2972
|
+
getGitUserName: () => getGitUserName,
|
|
2973
|
+
groupIntoThreads: () => groupIntoThreads,
|
|
2974
|
+
isHumanMessage: () => isHumanMessage,
|
|
2975
|
+
isReferenceKind: () => isReferenceKind,
|
|
2976
|
+
jiraBasicAuth: () => jiraBasicAuth,
|
|
2977
|
+
jiraIssueToTicket: () => jiraIssueToTicket,
|
|
2978
|
+
joinChannel: () => joinChannel,
|
|
2979
|
+
lacksFileAttribution: () => lacksFileAttribution,
|
|
2980
|
+
linearAuthHeader: () => linearAuthHeader,
|
|
2981
|
+
linearIssueToTicket: () => linearIssueToTicket,
|
|
2982
|
+
listChannels: () => listChannels,
|
|
2983
|
+
listUsers: () => listUsers,
|
|
2984
|
+
loadComposers: () => loadComposers,
|
|
2985
|
+
matchCommitToSession: () => matchCommitToSession,
|
|
2986
|
+
measureLineSurvival: () => measureLineSurvival,
|
|
2987
|
+
normalizeRepoRemote: () => normalizeRepoRemote,
|
|
2988
|
+
opencodeDbPath: () => opencodeDbPath,
|
|
2989
|
+
parseAllCodexSessions: () => parseAllCodexSessions,
|
|
2990
|
+
parseAllCopilotSessions: () => parseAllCopilotSessions,
|
|
2991
|
+
parseAllCursorSessions: () => parseAllCursorSessions,
|
|
2992
|
+
parseAllOpenCodeSessions: () => parseAllOpenCodeSessions,
|
|
2993
|
+
parseAllSessions: () => parseAllSessions,
|
|
2994
|
+
parseCodexSessionFile: () => parseCodexSessionFile,
|
|
2995
|
+
parseCopilotSessionDir: () => parseCopilotSessionDir,
|
|
2996
|
+
parseCopilotWorkspace: () => parseCopilotWorkspace,
|
|
2997
|
+
parseCursorComposer: () => parseCursorComposer,
|
|
2998
|
+
parseCursorConversation: () => parseCursorConversation,
|
|
2999
|
+
parseGeminiSessionFile: () => parseGeminiSessionFile,
|
|
3000
|
+
parseGitLog: () => parseGitLog,
|
|
3001
|
+
parseLineLog: () => parseLineLog,
|
|
3002
|
+
parseSessionFile: () => parseSessionFile,
|
|
3003
|
+
parseSlackExport: () => parseSlackExport,
|
|
3004
|
+
parseTraceTarget: () => parseTraceTarget,
|
|
3005
|
+
parseTrailers: () => parseTrailers,
|
|
3006
|
+
parseTranscriptFile: () => parseTranscriptFile,
|
|
3007
|
+
parseUnifiedDiffHunks: () => parseUnifiedDiffHunks,
|
|
3008
|
+
planRead: () => planRead,
|
|
3009
|
+
postMessage: () => postMessage,
|
|
3010
|
+
proposeLinks: () => proposeLinks,
|
|
3011
|
+
provenanceTrailerFor: () => provenanceTrailerFor,
|
|
3012
|
+
readableText: () => readableText,
|
|
3013
|
+
redactJsonValue: () => redactJsonValue,
|
|
3014
|
+
redactRawTranscript: () => redactRawTranscript,
|
|
3015
|
+
redactSecrets: () => redactSecrets,
|
|
3016
|
+
repoIdFromPath: () => repoIdFromPath,
|
|
3017
|
+
repoIdFromRemote: () => repoIdFromRemote,
|
|
3018
|
+
repoIdFromRootCommit: () => repoIdFromRootCommit,
|
|
3019
|
+
repoNameFromId: () => repoNameFromId,
|
|
3020
|
+
sessionSignature: () => sessionSignature,
|
|
3021
|
+
slackTimestamp: () => slackTimestamp,
|
|
3022
|
+
splitCompleteLines: () => splitCompleteLines,
|
|
3023
|
+
statedInsightsOf: () => statedInsightsOf,
|
|
3024
|
+
stripNulls: () => stripNulls,
|
|
3025
|
+
stripNullsDeep: () => stripNullsDeep,
|
|
3026
|
+
ticketReferencesIn: () => ticketReferencesIn,
|
|
3027
|
+
traceLines: () => traceLines,
|
|
3028
|
+
trailersFromMessage: () => trailersFromMessage
|
|
3029
|
+
});
|
|
3030
|
+
import { userInfo } from "node:os";
|
|
3031
|
+
function resolveFallbackAuthor(repoPath, commits) {
|
|
3032
|
+
const configured = getGitUserName(repoPath);
|
|
3033
|
+
if (configured) return configured;
|
|
3034
|
+
const counts = /* @__PURE__ */ new Map();
|
|
3035
|
+
for (const c of commits) {
|
|
3036
|
+
if (!c.author) continue;
|
|
3037
|
+
counts.set(c.author, (counts.get(c.author) ?? 0) + 1);
|
|
3038
|
+
}
|
|
3039
|
+
const mostFrequent = [...counts.entries()].sort((a, b) => b[1] - a[1])[0]?.[0];
|
|
3040
|
+
if (mostFrequent) return mostFrequent;
|
|
3041
|
+
try {
|
|
3042
|
+
return userInfo().username || null;
|
|
3043
|
+
} catch {
|
|
3044
|
+
return null;
|
|
3045
|
+
}
|
|
3046
|
+
}
|
|
3047
|
+
function collectRepoData(repoPath, options = {}) {
|
|
3048
|
+
const repoId = deriveRepoId(repoPath);
|
|
3049
|
+
const commits = parseGitLog(repoPath, repoId, options.knownCommits);
|
|
3050
|
+
const author = resolveFallbackAuthor(repoPath, commits);
|
|
3051
|
+
const claude = parseAllSessions(repoPath, repoId, options.cursors ?? {});
|
|
3052
|
+
const sessions = [
|
|
3053
|
+
...claude.sessions,
|
|
3054
|
+
...parseAllCursorSessions(repoPath, repoId),
|
|
3055
|
+
...parseAllCodexSessions(repoPath, repoId),
|
|
3056
|
+
...parseAllOpenCodeSessions(repoPath, repoId),
|
|
3057
|
+
...parseAllCopilotSessions(repoPath, repoId)
|
|
3058
|
+
].map((s) => ({
|
|
3059
|
+
...s,
|
|
3060
|
+
author: s.author ?? author
|
|
3061
|
+
}));
|
|
3062
|
+
const totalRedactions = sessions.reduce((sum, s) => sum + s.redactionCount, 0);
|
|
3063
|
+
return {
|
|
3064
|
+
sessions: stripNullsDeep(sessions),
|
|
3065
|
+
commits: stripNullsDeep(commits),
|
|
3066
|
+
totalRedactions,
|
|
3067
|
+
cursors: claude.cursors,
|
|
3068
|
+
skippedTranscripts: claude.skipped
|
|
3069
|
+
};
|
|
3070
|
+
}
|
|
3071
|
+
var init_src = __esm({
|
|
3072
|
+
"../../packages/ingest-core/src/index.ts"() {
|
|
3073
|
+
"use strict";
|
|
3074
|
+
init_git_history();
|
|
3075
|
+
init_claude_sessions();
|
|
3076
|
+
init_cursor_sessions();
|
|
3077
|
+
init_opencode_sessions();
|
|
3078
|
+
init_copilot_sessions();
|
|
3079
|
+
init_codex_sessions();
|
|
3080
|
+
init_sanitize();
|
|
3081
|
+
init_types();
|
|
3082
|
+
init_sanitize();
|
|
3083
|
+
init_incremental();
|
|
3084
|
+
init_redact();
|
|
3085
|
+
init_tickets();
|
|
3086
|
+
init_linear();
|
|
3087
|
+
init_jira();
|
|
3088
|
+
init_claude_sessions();
|
|
3089
|
+
init_copilot_sessions();
|
|
3090
|
+
init_opencode_sessions();
|
|
3091
|
+
init_cursor_sessions();
|
|
3092
|
+
init_codex_sessions();
|
|
3093
|
+
init_git_history();
|
|
3094
|
+
init_diff_parser();
|
|
3095
|
+
init_trace_lines();
|
|
3096
|
+
init_trace_target();
|
|
3097
|
+
init_transcript_parsers();
|
|
3098
|
+
init_gemini_sessions();
|
|
3099
|
+
init_slack_threads();
|
|
3100
|
+
init_slack_client();
|
|
3101
|
+
init_subject_linking();
|
|
3102
|
+
init_derive_uuid();
|
|
3103
|
+
init_line_survival();
|
|
3104
|
+
}
|
|
3105
|
+
});
|
|
3106
|
+
|
|
3107
|
+
// src/client.ts
|
|
3108
|
+
var client_exports = {};
|
|
3109
|
+
__export(client_exports, {
|
|
3110
|
+
evrexApi: () => evrexApi
|
|
3111
|
+
});
|
|
3112
|
+
function headers() {
|
|
3113
|
+
const base = { "Content-Type": "application/json" };
|
|
3114
|
+
if (EVREX_TOKEN) base.Authorization = `Bearer ${EVREX_TOKEN}`;
|
|
3115
|
+
return base;
|
|
3116
|
+
}
|
|
3117
|
+
function describeFailure(method, path, status, statusText) {
|
|
3118
|
+
if (status === 401 || status === 403) {
|
|
3119
|
+
return EVREX_TOKEN ? `${method} ${path} -> ${status}: the EVREX_TOKEN this server was started with was rejected. It may be revoked or expired.` : `${method} ${path} -> ${status}: this evrex backend requires a credential, but no EVREX_TOKEN is set for this MCP server.`;
|
|
3120
|
+
}
|
|
3121
|
+
return `${method} ${path} -> ${status} ${statusText}`;
|
|
3122
|
+
}
|
|
3123
|
+
async function request(method, path, { body, absentIsAnswer } = {}) {
|
|
3124
|
+
const res = await fetch(`${API_BASE_URL}${path}`, {
|
|
3125
|
+
method,
|
|
3126
|
+
headers: headers(),
|
|
3127
|
+
...body === void 0 ? {} : { body: JSON.stringify(body) }
|
|
3128
|
+
});
|
|
3129
|
+
if (res.status === 404 && absentIsAnswer) return null;
|
|
3130
|
+
if (!res.ok) throw new Error(describeFailure(method, path, res.status, res.statusText));
|
|
3131
|
+
return await res.json();
|
|
3132
|
+
}
|
|
3133
|
+
var DEFAULT_API_BASE_URL, API_BASE_URL, EVREX_TOKEN, get, getOrNull, post, evrexApi;
|
|
3134
|
+
var init_client = __esm({
|
|
3135
|
+
"src/client.ts"() {
|
|
3136
|
+
"use strict";
|
|
3137
|
+
DEFAULT_API_BASE_URL = "https://api.evrex.ai";
|
|
3138
|
+
API_BASE_URL = (process.env.EVREX_API_BASE_URL ?? process.env.EVREX_API_URL ?? DEFAULT_API_BASE_URL).replace(/\/+$/, "");
|
|
3139
|
+
EVREX_TOKEN = process.env.EVREX_TOKEN ?? process.env.EVREX_API_TOKEN ?? null;
|
|
3140
|
+
get = (path) => request("GET", path);
|
|
3141
|
+
getOrNull = (path) => request("GET", path, { absentIsAnswer: true });
|
|
3142
|
+
post = (path, body) => request("POST", path, { body });
|
|
3143
|
+
evrexApi = {
|
|
3144
|
+
baseUrl: API_BASE_URL,
|
|
3145
|
+
repos: () => get("/repos"),
|
|
3146
|
+
commits: (repoPath) => get(`/commits?repoPath=${encodeURIComponent(repoPath)}`),
|
|
3147
|
+
// Abbreviated shas resolve server-side, so a value pasted from `git log`
|
|
3148
|
+
// works here (apps/backend/src/reads/reads.service.ts#resolveSha).
|
|
3149
|
+
commit: (sha) => getOrNull(`/commits/${encodeURIComponent(sha)}`),
|
|
3150
|
+
sessions: (repoPath) => get(`/sessions?repoPath=${encodeURIComponent(repoPath)}`),
|
|
3151
|
+
session: (id) => getOrNull(`/sessions/${encodeURIComponent(id)}`),
|
|
3152
|
+
// The paginated transcript — see reads.service.ts#getSessionTurns. `ts, id`
|
|
3153
|
+
// ordering server-side makes offsets stable across requests.
|
|
3154
|
+
sessionTurns: (id, offset, limit) => getOrNull(
|
|
3155
|
+
`/sessions/${encodeURIComponent(id)}/turns?offset=${offset}&limit=${limit}`
|
|
3156
|
+
),
|
|
3157
|
+
ask: (repoPath, text, filePaths) => post("/ask", { repoPath, text, filePaths }),
|
|
3158
|
+
// Evidence-only retrieval (BM25 + embedding), no LLM synthesis call — see
|
|
3159
|
+
// apps/backend/src/query/query.service.ts#search. Used by evrex_search,
|
|
3160
|
+
// which wants ranked hits fast, not a synthesized paragraph.
|
|
3161
|
+
search: (repoPath, text, filePaths) => post("/search", { repoPath, text, filePaths }),
|
|
3162
|
+
// Everything that happened in a repo, newest first, bounded by days — the
|
|
3163
|
+
// same query the desktop Timeline screen makes. Sessions and commits
|
|
3164
|
+
// interleaved, each with the handle evrex_expand takes.
|
|
3165
|
+
feedback: (body) => post("/feedback", body),
|
|
3166
|
+
timeline: (repoPath, days) => get(
|
|
3167
|
+
`/timeline?repoPath=${encodeURIComponent(repoPath)}&days=${encodeURIComponent(String(days))}`
|
|
3168
|
+
)
|
|
3169
|
+
};
|
|
3170
|
+
}
|
|
3171
|
+
});
|
|
3172
|
+
|
|
3173
|
+
// src/capture.ts
|
|
3174
|
+
init_src();
|
|
3175
|
+
import { statSync as statSync5 } from "node:fs";
|
|
3176
|
+
import { homedir as homedir7, hostname } from "node:os";
|
|
3177
|
+
import { dirname as dirname3, join as join7 } from "node:path";
|
|
799
3178
|
|
|
800
3179
|
// src/capture-state.ts
|
|
801
|
-
import { existsSync as
|
|
802
|
-
import { homedir } from "node:os";
|
|
803
|
-
import { dirname, join } from "node:path";
|
|
3180
|
+
import { existsSync as existsSync6, mkdirSync, readFileSync as readFileSync5, renameSync, writeFileSync } from "node:fs";
|
|
3181
|
+
import { homedir as homedir6 } from "node:os";
|
|
3182
|
+
import { dirname, join as join6 } from "node:path";
|
|
804
3183
|
var EMPTY = { sessions: {} };
|
|
805
|
-
function stateDir(home =
|
|
806
|
-
return
|
|
3184
|
+
function stateDir(home = homedir6()) {
|
|
3185
|
+
return join6(home, ".evrex");
|
|
807
3186
|
}
|
|
808
|
-
function statePath(home =
|
|
809
|
-
return
|
|
3187
|
+
function statePath(home = homedir6()) {
|
|
3188
|
+
return join6(stateDir(home), "capture-state.json");
|
|
810
3189
|
}
|
|
811
3190
|
function readState(path) {
|
|
812
3191
|
try {
|
|
813
|
-
const parsed = JSON.parse(
|
|
3192
|
+
const parsed = JSON.parse(readFileSync5(path, "utf8"));
|
|
814
3193
|
if (!parsed || typeof parsed !== "object" || typeof parsed.sessions !== "object") {
|
|
815
3194
|
return { sessions: {} };
|
|
816
3195
|
}
|
|
@@ -829,17 +3208,17 @@ function writeState(path, state) {
|
|
|
829
3208
|
}
|
|
830
3209
|
}
|
|
831
3210
|
function orphaned(state, exceptSessionId) {
|
|
832
|
-
return Object.entries(state.sessions).filter(([id, s]) => !s.archived && id !== exceptSessionId).map(([, s]) => s).filter((s) =>
|
|
3211
|
+
return Object.entries(state.sessions).filter(([id, s]) => !s.archived && id !== exceptSessionId).map(([, s]) => s).filter((s) => existsSync6(s.transcriptPath));
|
|
833
3212
|
}
|
|
834
3213
|
|
|
835
3214
|
// src/spool.ts
|
|
836
|
-
import { appendFileSync, existsSync as
|
|
3215
|
+
import { appendFileSync, existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as readFileSync6, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
837
3216
|
import { dirname as dirname2 } from "node:path";
|
|
838
3217
|
var MAX_SPOOL_BYTES = 32 * 1024 * 1024;
|
|
839
3218
|
function append(spoolPath, post2) {
|
|
840
3219
|
try {
|
|
841
3220
|
mkdirSync2(dirname2(spoolPath), { recursive: true });
|
|
842
|
-
if (
|
|
3221
|
+
if (existsSync7(spoolPath) && sizeOf(spoolPath) >= MAX_SPOOL_BYTES) return;
|
|
843
3222
|
appendFileSync(spoolPath, `${JSON.stringify(post2)}
|
|
844
3223
|
`, { mode: 384 });
|
|
845
3224
|
} catch {
|
|
@@ -847,7 +3226,7 @@ function append(spoolPath, post2) {
|
|
|
847
3226
|
}
|
|
848
3227
|
function read(spoolPath) {
|
|
849
3228
|
try {
|
|
850
|
-
return
|
|
3229
|
+
return readFileSync6(spoolPath, "utf8").split("\n").filter((line) => line.trim().length > 0).flatMap((line) => {
|
|
851
3230
|
try {
|
|
852
3231
|
return [JSON.parse(line)];
|
|
853
3232
|
} catch {
|
|
@@ -876,7 +3255,7 @@ function keep(spoolPath, remaining) {
|
|
|
876
3255
|
}
|
|
877
3256
|
function sizeOf(path) {
|
|
878
3257
|
try {
|
|
879
|
-
return
|
|
3258
|
+
return readFileSync6(path).byteLength;
|
|
880
3259
|
} catch {
|
|
881
3260
|
return 0;
|
|
882
3261
|
}
|
|
@@ -884,7 +3263,10 @@ function sizeOf(path) {
|
|
|
884
3263
|
|
|
885
3264
|
// src/capture.ts
|
|
886
3265
|
var DEADLINE_MS = 6e3;
|
|
887
|
-
var
|
|
3266
|
+
var MAX_TRANSCRIPT_BYTES2 = 256 * 1024 * 1024;
|
|
3267
|
+
function subagentTranscriptPath(parentTranscriptPath, sessionId, agentId) {
|
|
3268
|
+
return join7(dirname3(parentTranscriptPath), sessionId, "subagents", `agent-${agentId}.jsonl`);
|
|
3269
|
+
}
|
|
888
3270
|
function readStdin() {
|
|
889
3271
|
return new Promise((resolve) => {
|
|
890
3272
|
let raw = "";
|
|
@@ -902,8 +3284,8 @@ function withDeadline(work, ms) {
|
|
|
902
3284
|
}
|
|
903
3285
|
function parseTranscript(path, cwd) {
|
|
904
3286
|
try {
|
|
905
|
-
const stat =
|
|
906
|
-
if (!stat.isFile() || stat.size >
|
|
3287
|
+
const stat = statSync5(path);
|
|
3288
|
+
if (!stat.isFile() || stat.size > MAX_TRANSCRIPT_BYTES2) return null;
|
|
907
3289
|
return parseTranscriptFile(path, cwd, deriveRepoId(cwd));
|
|
908
3290
|
} catch {
|
|
909
3291
|
return null;
|
|
@@ -930,9 +3312,11 @@ async function deliver(spoolPath, path, body, deps) {
|
|
|
930
3312
|
}
|
|
931
3313
|
async function handleEvent(event, deps) {
|
|
932
3314
|
const name = event.hook_event_name;
|
|
3315
|
+
if (!name) return null;
|
|
3316
|
+
if (event.conversation_id && !event.session_id) return handleCursorEvent(event, name, deps);
|
|
933
3317
|
const sessionId = event.session_id;
|
|
934
|
-
if (!
|
|
935
|
-
const spoolPath =
|
|
3318
|
+
if (!sessionId) return null;
|
|
3319
|
+
const spoolPath = join7(stateDir(deps.home), "spool.jsonl");
|
|
936
3320
|
const drained = await drainSpool(spoolPath, deps);
|
|
937
3321
|
const path = statePath(deps.home);
|
|
938
3322
|
const state = readState(path) ?? EMPTY;
|
|
@@ -948,6 +3332,13 @@ async function handleEvent(event, deps) {
|
|
|
948
3332
|
if (!transcriptPath) {
|
|
949
3333
|
return { event: name, turnsSent: 0, archived: false, spoolDrained: drained.sent };
|
|
950
3334
|
}
|
|
3335
|
+
if (name === "SubagentStop" && event.agent_id) {
|
|
3336
|
+
const childPath = subagentTranscriptPath(transcriptPath, sessionId, event.agent_id);
|
|
3337
|
+
const child = parseTranscript(childPath, event.cwd ?? process.cwd());
|
|
3338
|
+
if (child) {
|
|
3339
|
+
await deliver(spoolPath, "/ingest/sessions", { sessions: [child] }, deps);
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
951
3342
|
const parsed = parseTranscript(transcriptPath, event.cwd ?? process.cwd());
|
|
952
3343
|
const sentThrough = prior?.sentThrough ?? 0;
|
|
953
3344
|
let turnsSent = 0;
|
|
@@ -979,6 +3370,39 @@ async function handleEvent(event, deps) {
|
|
|
979
3370
|
writeState(path, state);
|
|
980
3371
|
return { event: name, turnsSent, archived, spoolDrained: drained.sent };
|
|
981
3372
|
}
|
|
3373
|
+
var CURSOR_ENDINGS = /* @__PURE__ */ new Set(["stop", "sessionEnd"]);
|
|
3374
|
+
async function handleCursorEvent(event, name, deps) {
|
|
3375
|
+
if (!CURSOR_ENDINGS.has(name)) return { event: name, turnsSent: 0, archived: false, spoolDrained: 0 };
|
|
3376
|
+
const cwd = event.workspace_roots?.[0] ?? event.cwd ?? process.cwd();
|
|
3377
|
+
const { parseCursorConversation: parseCursorConversation2 } = await Promise.resolve().then(() => (init_src(), src_exports));
|
|
3378
|
+
let parsed;
|
|
3379
|
+
try {
|
|
3380
|
+
parsed = parseCursorConversation2(event.conversation_id, cwd);
|
|
3381
|
+
} catch {
|
|
3382
|
+
return null;
|
|
3383
|
+
}
|
|
3384
|
+
if (!parsed) return null;
|
|
3385
|
+
const spoolPath = join7(stateDir(deps.home), "spool.jsonl");
|
|
3386
|
+
const drained = await drainSpool(spoolPath, deps);
|
|
3387
|
+
const path = statePath(deps.home);
|
|
3388
|
+
const state = readState(path) ?? EMPTY;
|
|
3389
|
+
const prior = state.sessions[parsed.id];
|
|
3390
|
+
const sentThrough = prior?.sentThrough ?? 0;
|
|
3391
|
+
let turnsSent = 0;
|
|
3392
|
+
if (parsed.turnCount > sentThrough) {
|
|
3393
|
+
const ok = await deliver(spoolPath, "/ingest/sessions", { sessions: [parsed] }, deps);
|
|
3394
|
+
turnsSent = ok ? parsed.turnCount - sentThrough : 0;
|
|
3395
|
+
}
|
|
3396
|
+
state.sessions[parsed.id] = {
|
|
3397
|
+
transcriptPath: `cursor://${event.conversation_id}`,
|
|
3398
|
+
sentThrough: parsed.turnCount,
|
|
3399
|
+
startedAt: prior?.startedAt ?? deps.now().toISOString(),
|
|
3400
|
+
// No transcript file to archive; the store is the archive.
|
|
3401
|
+
archived: true
|
|
3402
|
+
};
|
|
3403
|
+
writeState(path, state);
|
|
3404
|
+
return { event: name, turnsSent, archived: false, spoolDrained: drained.sent };
|
|
3405
|
+
}
|
|
982
3406
|
async function main() {
|
|
983
3407
|
const raw = await withDeadline(readStdin(), DEADLINE_MS);
|
|
984
3408
|
if (!raw) return;
|
|
@@ -991,7 +3415,7 @@ async function main() {
|
|
|
991
3415
|
const { evrexApi: evrexApi2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
992
3416
|
const deps = {
|
|
993
3417
|
now: () => /* @__PURE__ */ new Date(),
|
|
994
|
-
home:
|
|
3418
|
+
home: homedir7(),
|
|
995
3419
|
machine: hostname(),
|
|
996
3420
|
post: async (path, body) => {
|
|
997
3421
|
try {
|
|
@@ -1018,5 +3442,6 @@ main().then(
|
|
|
1018
3442
|
export {
|
|
1019
3443
|
drainSpool,
|
|
1020
3444
|
handleEvent,
|
|
1021
|
-
parseTranscript
|
|
3445
|
+
parseTranscript,
|
|
3446
|
+
subagentTranscriptPath
|
|
1022
3447
|
};
|