evrex-mcp 0.6.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 +2600 -165
- package/dist/continuity.js +343 -0
- package/dist/hook.js +160 -24
- package/dist/import.js +2290 -344
- package/dist/index.js +432 -20
- package/dist/pretool.js +341 -0
- package/dist/tickets.js +29 -1
- package/package.json +22 -17
package/dist/capture.js
CHANGED
|
@@ -9,67 +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
|
-
|
|
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
|
|
62
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
|
+
];
|
|
63
76
|
}
|
|
64
77
|
});
|
|
65
78
|
|
|
66
|
-
// src/capture.ts
|
|
67
|
-
import { statSync as statSync3 } from "node:fs";
|
|
68
|
-
import { homedir as homedir2, hostname } from "node:os";
|
|
69
|
-
import { join as join2 } from "node:path";
|
|
70
|
-
|
|
71
79
|
// ../../packages/ingest-core/src/git-history.ts
|
|
72
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
|
+
}
|
|
73
88
|
function gitQuiet(repoPath, args) {
|
|
74
89
|
return execFileSync("git", args, {
|
|
75
90
|
cwd: repoPath,
|
|
@@ -111,6 +126,10 @@ function repoIdFromRootCommit(sha) {
|
|
|
111
126
|
function repoIdFromPath(repoPath) {
|
|
112
127
|
return `path:${repoPath}`;
|
|
113
128
|
}
|
|
129
|
+
function repoNameFromId(repoId) {
|
|
130
|
+
const withoutPrefix = repoId.replace(/^(remote|root|path):/, "");
|
|
131
|
+
return withoutPrefix.split("/").filter(Boolean).pop() ?? repoId;
|
|
132
|
+
}
|
|
114
133
|
function firstRemoteUrl(repoPath) {
|
|
115
134
|
try {
|
|
116
135
|
const origin = gitQuiet(repoPath, ["remote", "get-url", "origin"]).trim();
|
|
@@ -145,26 +164,268 @@ function deriveRepoId(repoPath) {
|
|
|
145
164
|
if (root) return repoIdFromRootCommit(root);
|
|
146
165
|
return repoIdFromPath(repoPath);
|
|
147
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
|
+
});
|
|
148
340
|
|
|
149
|
-
// ../../packages/ingest-core/src/
|
|
150
|
-
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
|
+
});
|
|
151
427
|
|
|
152
428
|
// ../../packages/ingest-core/src/redact.ts
|
|
153
|
-
var PATTERNS = [
|
|
154
|
-
{ type: "private_key", regex: /-----BEGIN(?: [A-Z]+)? PRIVATE KEY-----[\s\S]*?-----END(?: [A-Z]+)? PRIVATE KEY-----/g },
|
|
155
|
-
{ type: "aws_access_key", regex: /AKIA[0-9A-Z]{16}/g },
|
|
156
|
-
{ type: "github_token", regex: /gh[pousr]_[A-Za-z0-9]{36,}/g },
|
|
157
|
-
{ type: "slack_token", regex: /xox[baprs]-[A-Za-z0-9-]{10,}/g },
|
|
158
|
-
{ type: "stripe_key", regex: /sk_(live|test)_[A-Za-z0-9]{24,}/g },
|
|
159
|
-
{ type: "openai_key", regex: /sk-[A-Za-z0-9]{20,}/g },
|
|
160
|
-
{ type: "anthropic_key", regex: /sk-ant-[A-Za-z0-9-_]{20,}/g },
|
|
161
|
-
{ type: "jwt", regex: /eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g },
|
|
162
|
-
{ type: "bearer_token", regex: /Bearer\s+[A-Za-z0-9\-_.]{20,}/g },
|
|
163
|
-
{
|
|
164
|
-
type: "env_secret",
|
|
165
|
-
regex: /\b([A-Z0-9_]*(?:_KEY|_SECRET|_TOKEN|_PASSWORD|PASSWD)[A-Z0-9_]*)\s*[:=]\s*["']?[^\s"'\n]{6,}["']?/g
|
|
166
|
-
}
|
|
167
|
-
];
|
|
168
429
|
function redactJsonValue(value) {
|
|
169
430
|
let count = 0;
|
|
170
431
|
const walk = (v) => {
|
|
@@ -185,9 +446,38 @@ function redactJsonValue(value) {
|
|
|
185
446
|
};
|
|
186
447
|
return { value: walk(value), count };
|
|
187
448
|
}
|
|
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
|
+
}
|
|
188
474
|
function redactSecrets(input) {
|
|
189
475
|
let text = input;
|
|
190
476
|
let count = 0;
|
|
477
|
+
text = text.replace(PRIVATE_BLOCK, () => {
|
|
478
|
+
count += 1;
|
|
479
|
+
return "[REDACTED:private]";
|
|
480
|
+
});
|
|
191
481
|
for (const { type, regex } of PATTERNS) {
|
|
192
482
|
text = text.replace(regex, (match, group1) => {
|
|
193
483
|
count += 1;
|
|
@@ -199,8 +489,74 @@ function redactSecrets(input) {
|
|
|
199
489
|
}
|
|
200
490
|
return { text, count };
|
|
201
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
|
+
});
|
|
202
514
|
|
|
203
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
|
+
}
|
|
204
560
|
function committedSubjects(command) {
|
|
205
561
|
const out = [];
|
|
206
562
|
const invocation = /\bgit\s+(?:-C\s+\S+\s+)?commit\b/g;
|
|
@@ -220,30 +576,18 @@ function committedSubjects(command) {
|
|
|
220
576
|
}
|
|
221
577
|
return out;
|
|
222
578
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
"slack"
|
|
231
|
-
];
|
|
232
|
-
var REFERENCE_KINDS = ["linear", "jira", "confluence"];
|
|
233
|
-
var SOURCE_KINDS = [
|
|
234
|
-
...CONVERSATION_KINDS,
|
|
235
|
-
...REFERENCE_KINDS
|
|
236
|
-
];
|
|
237
|
-
var EMPTY_USAGE = {
|
|
238
|
-
inputTokens: null,
|
|
239
|
-
outputTokens: null,
|
|
240
|
-
cacheReadTokens: null,
|
|
241
|
-
cacheWriteTokens: null,
|
|
242
|
-
model: null
|
|
243
|
-
};
|
|
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
|
+
});
|
|
244
586
|
|
|
245
587
|
// ../../packages/ingest-core/src/claude-sessions.ts
|
|
246
|
-
|
|
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";
|
|
247
591
|
function meaningfulLines(lines) {
|
|
248
592
|
return lines.map((l) => l.trim()).filter((l) => l.length >= MIN_MEANINGFUL_LINE_LENGTH);
|
|
249
593
|
}
|
|
@@ -270,9 +614,56 @@ function extractEditedLines(toolUseResult) {
|
|
|
270
614
|
if (meaningfulAdded.length === 0 && meaningfulRemoved.length === 0) return null;
|
|
271
615
|
return { path: r.filePath, added: meaningfulAdded, removed: meaningfulRemoved };
|
|
272
616
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
+
}
|
|
276
667
|
function extractPathsFromText(text) {
|
|
277
668
|
const matches = text.match(PATH_TOKEN_RE) ?? [];
|
|
278
669
|
return [...new Set(matches)].filter((p) => p.length > 3 && p.length < 300);
|
|
@@ -287,6 +678,7 @@ function blockText(content) {
|
|
|
287
678
|
function extractFromAssistantContent(content) {
|
|
288
679
|
const textParts = [];
|
|
289
680
|
const filesTouched = [];
|
|
681
|
+
const editedLines = [];
|
|
290
682
|
for (const block of content) {
|
|
291
683
|
if (block.type === "text" && block.text) {
|
|
292
684
|
textParts.push(block.text);
|
|
@@ -296,7 +688,15 @@ function extractFromAssistantContent(content) {
|
|
|
296
688
|
} else if (block.type === "tool_use") {
|
|
297
689
|
const name = block.name ?? "tool";
|
|
298
690
|
const input = block.input ?? {};
|
|
299
|
-
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") {
|
|
300
700
|
textParts.push(`[tool_call: ${name}] ${input.file_path}`);
|
|
301
701
|
filesTouched.push({ path: input.file_path, source: "tool_path", tool: name });
|
|
302
702
|
} else if (name === "Bash" && typeof input.command === "string") {
|
|
@@ -310,9 +710,8 @@ function extractFromAssistantContent(content) {
|
|
|
310
710
|
}
|
|
311
711
|
}
|
|
312
712
|
}
|
|
313
|
-
return { text: textParts.join("\n"), filesTouched };
|
|
713
|
+
return { text: textParts.join("\n"), filesTouched, editedLines };
|
|
314
714
|
}
|
|
315
|
-
var SYNTHETIC_CONTENT_RE = /^\s*(<task-notification>|<system-reminder>|\[SYSTEM NOTIFICATION)/;
|
|
316
715
|
function extractFromUserContent(content) {
|
|
317
716
|
if (typeof content === "string") {
|
|
318
717
|
return {
|
|
@@ -324,9 +723,11 @@ function extractFromUserContent(content) {
|
|
|
324
723
|
if (Array.isArray(content)) {
|
|
325
724
|
const toolParts = [];
|
|
326
725
|
const textParts = [];
|
|
726
|
+
let isToolError = false;
|
|
327
727
|
for (const block of content) {
|
|
328
728
|
if (block.type === "tool_result") {
|
|
329
729
|
toolParts.push(blockText(block.content));
|
|
730
|
+
if (block.is_error === true) isToolError = true;
|
|
330
731
|
} else if (block.type === "text" && typeof block.text === "string") {
|
|
331
732
|
textParts.push(block.text);
|
|
332
733
|
}
|
|
@@ -335,7 +736,8 @@ function extractFromUserContent(content) {
|
|
|
335
736
|
return {
|
|
336
737
|
text: toolParts.join("\n"),
|
|
337
738
|
filesTouched: [],
|
|
338
|
-
isSyntheticInput: true
|
|
739
|
+
isSyntheticInput: true,
|
|
740
|
+
isToolError
|
|
339
741
|
};
|
|
340
742
|
}
|
|
341
743
|
const text = textParts.join("\n");
|
|
@@ -371,8 +773,35 @@ function usageOnce(record, billed) {
|
|
|
371
773
|
model: typeof message?.model === "string" ? message.model : null
|
|
372
774
|
};
|
|
373
775
|
}
|
|
374
|
-
function
|
|
375
|
-
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);
|
|
376
805
|
const lines = raw.split("\n").filter((l) => l.trim().length > 0);
|
|
377
806
|
const turns = [];
|
|
378
807
|
const billedMessages = /* @__PURE__ */ new Set();
|
|
@@ -380,6 +809,8 @@ function parseSessionFile(filePath, repoPath, repoId = deriveRepoId(repoPath)) {
|
|
|
380
809
|
const cwd = repoPath;
|
|
381
810
|
let aiTitle = null;
|
|
382
811
|
let totalRedactions = 0;
|
|
812
|
+
let branch = null;
|
|
813
|
+
let agentId = null;
|
|
383
814
|
for (const line of lines) {
|
|
384
815
|
let record;
|
|
385
816
|
try {
|
|
@@ -396,22 +827,27 @@ function parseSessionFile(filePath, repoPath, repoId = deriveRepoId(repoPath)) {
|
|
|
396
827
|
const ts = record.timestamp;
|
|
397
828
|
if (!id || !ts) continue;
|
|
398
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;
|
|
399
832
|
let text = "";
|
|
400
833
|
let filesTouched = [];
|
|
401
834
|
let editedLines = [];
|
|
402
835
|
let isSyntheticInput = false;
|
|
836
|
+
let isToolError = false;
|
|
403
837
|
if (record.type === "assistant") {
|
|
404
838
|
const content = record.message?.content;
|
|
405
839
|
if (Array.isArray(content)) {
|
|
406
840
|
const extracted = extractFromAssistantContent(content);
|
|
407
841
|
text = extracted.text;
|
|
408
842
|
filesTouched = extracted.filesTouched;
|
|
843
|
+
editedLines = extracted.editedLines;
|
|
409
844
|
}
|
|
410
845
|
} else {
|
|
411
846
|
const extracted = extractFromUserContent(record.message?.content);
|
|
412
847
|
text = extracted.text;
|
|
413
848
|
filesTouched = extracted.filesTouched;
|
|
414
849
|
isSyntheticInput = extracted.isSyntheticInput;
|
|
850
|
+
isToolError = extracted.isToolError ?? false;
|
|
415
851
|
const edited = extractEditedLines(record.toolUseResult);
|
|
416
852
|
if (edited) editedLines = [edited];
|
|
417
853
|
}
|
|
@@ -429,12 +865,21 @@ function parseSessionFile(filePath, repoPath, repoId = deriveRepoId(repoPath)) {
|
|
|
429
865
|
isSidechain: Boolean(record.isSidechain),
|
|
430
866
|
redacted: redacted.count > 0,
|
|
431
867
|
isSyntheticInput,
|
|
868
|
+
isToolError,
|
|
432
869
|
usage: usageOnce(record, billedMessages)
|
|
433
870
|
});
|
|
434
871
|
}
|
|
435
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;
|
|
436
881
|
const sortedTs = turns.map((t) => t.ts).sort();
|
|
437
|
-
const rawContent = lines.map((line) => {
|
|
882
|
+
const rawContent = capped ? "" : lines.map((line) => {
|
|
438
883
|
try {
|
|
439
884
|
return JSON.stringify(redactJsonValue(JSON.parse(line)).value);
|
|
440
885
|
} catch {
|
|
@@ -446,7 +891,7 @@ function parseSessionFile(filePath, repoPath, repoId = deriveRepoId(repoPath)) {
|
|
|
446
891
|
}
|
|
447
892
|
return {
|
|
448
893
|
id: sessionId,
|
|
449
|
-
agentKind
|
|
894
|
+
agentKind,
|
|
450
895
|
repoId,
|
|
451
896
|
cwd,
|
|
452
897
|
startedAt: sortedTs[0] ?? null,
|
|
@@ -458,6 +903,9 @@ function parseSessionFile(filePath, repoPath, repoId = deriveRepoId(repoPath)) {
|
|
|
458
903
|
sourceFile: filePath,
|
|
459
904
|
redactionCount: totalRedactions,
|
|
460
905
|
committedSubjects: collectCommittedSubjects(lines),
|
|
906
|
+
branch,
|
|
907
|
+
parentSessionId,
|
|
908
|
+
subagent,
|
|
461
909
|
rawContent,
|
|
462
910
|
rawFormat: "jsonl",
|
|
463
911
|
turns
|
|
@@ -484,51 +932,917 @@ function collectCommittedSubjects(lines) {
|
|
|
484
932
|
}
|
|
485
933
|
return [...subjects];
|
|
486
934
|
}
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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 };
|
|
497
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
|
+
});
|
|
498
973
|
|
|
499
|
-
// ../../packages/ingest-core/src/
|
|
500
|
-
import {
|
|
501
|
-
import { existsSync as existsSync2
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
const
|
|
506
|
-
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);
|
|
507
982
|
}
|
|
508
|
-
function
|
|
509
|
-
|
|
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");
|
|
510
993
|
}
|
|
511
|
-
function
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
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;
|
|
515
1084
|
try {
|
|
516
|
-
|
|
1085
|
+
records[key] = value ? JSON.parse(value) : null;
|
|
517
1086
|
} catch {
|
|
518
|
-
|
|
1087
|
+
records[key] = value;
|
|
519
1088
|
}
|
|
520
1089
|
}
|
|
521
|
-
return
|
|
522
|
-
}
|
|
523
|
-
function payloadType(line) {
|
|
524
|
-
const t = line.payload?.type;
|
|
525
|
-
return typeof t === "string" ? t : void 0;
|
|
1090
|
+
return records;
|
|
526
1091
|
}
|
|
527
|
-
function
|
|
528
|
-
|
|
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
|
+
}
|
|
529
1100
|
}
|
|
530
|
-
function
|
|
531
|
-
const
|
|
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) {
|
|
1842
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
1843
|
+
}
|
|
1844
|
+
function conversationEvents(lines) {
|
|
1845
|
+
const out = [];
|
|
532
1846
|
let pending = null;
|
|
533
1847
|
let model = null;
|
|
534
1848
|
lines.forEach((line, index) => {
|
|
@@ -610,7 +1924,7 @@ function filesFromToolCalls(lines) {
|
|
|
610
1924
|
function parseCodexSessionFile(filePath, repoPath, repoId) {
|
|
611
1925
|
let raw;
|
|
612
1926
|
try {
|
|
613
|
-
raw =
|
|
1927
|
+
raw = readFileSync3(filePath, "utf-8");
|
|
614
1928
|
} catch {
|
|
615
1929
|
return null;
|
|
616
1930
|
}
|
|
@@ -643,6 +1957,7 @@ function parseCodexSessionFile(filePath, repoPath, repoId) {
|
|
|
643
1957
|
isSidechain: false,
|
|
644
1958
|
redacted: count > 0,
|
|
645
1959
|
isSyntheticInput: false,
|
|
1960
|
+
isToolError: false,
|
|
646
1961
|
usage: event.usage
|
|
647
1962
|
};
|
|
648
1963
|
});
|
|
@@ -652,6 +1967,9 @@ function parseCodexSessionFile(filePath, repoPath, repoId) {
|
|
|
652
1967
|
return {
|
|
653
1968
|
id: sessionId,
|
|
654
1969
|
agentKind: "codex",
|
|
1970
|
+
branch: null,
|
|
1971
|
+
parentSessionId: null,
|
|
1972
|
+
subagent: null,
|
|
655
1973
|
// Codex records the remote itself, so identity survives a moved or deleted
|
|
656
1974
|
// checkout. Falls back to the caller's derivation when it is absent.
|
|
657
1975
|
repoId: repoIdFromMeta(meta) ?? repoId,
|
|
@@ -670,9 +1988,9 @@ function parseCodexSessionFile(filePath, repoPath, repoId) {
|
|
|
670
1988
|
};
|
|
671
1989
|
}
|
|
672
1990
|
function repoIdFromMeta(meta) {
|
|
673
|
-
const
|
|
674
|
-
if (typeof
|
|
675
|
-
const url = asString(
|
|
1991
|
+
const git3 = meta.git;
|
|
1992
|
+
if (typeof git3 !== "object" || git3 === null) return null;
|
|
1993
|
+
const url = asString(git3.repository_url);
|
|
676
1994
|
if (!url) return null;
|
|
677
1995
|
const normalized = normalizeRepoRemote(url);
|
|
678
1996
|
return normalized ? `remote:${normalized}` : null;
|
|
@@ -687,13 +2005,494 @@ function redactRollout(raw) {
|
|
|
687
2005
|
});
|
|
688
2006
|
return { content: lines.join("\n"), count };
|
|
689
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
|
+
});
|
|
690
2028
|
|
|
691
2029
|
// ../../packages/ingest-core/src/sanitize.ts
|
|
692
|
-
|
|
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
|
+
});
|
|
693
2493
|
|
|
694
2494
|
// ../../packages/ingest-core/src/gemini-sessions.ts
|
|
695
|
-
import { readFileSync as
|
|
696
|
-
var SYNTHETIC = /^\s*<session_context>/;
|
|
2495
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
697
2496
|
function textOf(content) {
|
|
698
2497
|
if (typeof content === "string") return content;
|
|
699
2498
|
if (!Array.isArray(content)) return "";
|
|
@@ -704,7 +2503,7 @@ function textOf(content) {
|
|
|
704
2503
|
function parseGeminiSessionFile(filePath, repoPath, repoId) {
|
|
705
2504
|
let raw;
|
|
706
2505
|
try {
|
|
707
|
-
raw =
|
|
2506
|
+
raw = readFileSync4(filePath, "utf-8");
|
|
708
2507
|
} catch {
|
|
709
2508
|
return null;
|
|
710
2509
|
}
|
|
@@ -750,6 +2549,7 @@ function parseGeminiSessionFile(filePath, repoPath, repoId) {
|
|
|
750
2549
|
// as a user message. Attributing that to the person is the same bug this
|
|
751
2550
|
// repo already fixed once for Claude Code's tool results.
|
|
752
2551
|
isSyntheticInput: role === "user" && SYNTHETIC.test(text),
|
|
2552
|
+
isToolError: false,
|
|
753
2553
|
// Gemini records a model per message but no token counts in the
|
|
754
2554
|
// transcript; unknown rather than zero.
|
|
755
2555
|
usage: { ...EMPTY_USAGE, model: record.model ?? null }
|
|
@@ -773,12 +2573,24 @@ function parseGeminiSessionFile(filePath, repoPath, repoId) {
|
|
|
773
2573
|
redactionCount: redactionCount + rawRedactions,
|
|
774
2574
|
turns,
|
|
775
2575
|
rawContent,
|
|
2576
|
+
branch: null,
|
|
2577
|
+
parentSessionId: null,
|
|
2578
|
+
subagent: null,
|
|
776
2579
|
rawFormat: "jsonl"
|
|
777
2580
|
};
|
|
778
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
|
+
});
|
|
779
2592
|
|
|
780
2593
|
// ../../packages/ingest-core/src/transcript-parsers.ts
|
|
781
|
-
var PARSERS = [parseSessionFile, parseCodexSessionFile, parseGeminiSessionFile];
|
|
782
2594
|
function parseTranscriptFile(path, repoPath, repoId) {
|
|
783
2595
|
for (const parse of PARSERS) {
|
|
784
2596
|
const parsed = parse(path, repoPath, repoId);
|
|
@@ -786,21 +2598,598 @@ function parseTranscriptFile(path, repoPath, repoId) {
|
|
|
786
2598
|
}
|
|
787
2599
|
return null;
|
|
788
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";
|
|
789
3178
|
|
|
790
3179
|
// src/capture-state.ts
|
|
791
|
-
import { existsSync as
|
|
792
|
-
import { homedir } from "node:os";
|
|
793
|
-
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";
|
|
794
3183
|
var EMPTY = { sessions: {} };
|
|
795
|
-
function stateDir(home =
|
|
796
|
-
return
|
|
3184
|
+
function stateDir(home = homedir6()) {
|
|
3185
|
+
return join6(home, ".evrex");
|
|
797
3186
|
}
|
|
798
|
-
function statePath(home =
|
|
799
|
-
return
|
|
3187
|
+
function statePath(home = homedir6()) {
|
|
3188
|
+
return join6(stateDir(home), "capture-state.json");
|
|
800
3189
|
}
|
|
801
3190
|
function readState(path) {
|
|
802
3191
|
try {
|
|
803
|
-
const parsed = JSON.parse(
|
|
3192
|
+
const parsed = JSON.parse(readFileSync5(path, "utf8"));
|
|
804
3193
|
if (!parsed || typeof parsed !== "object" || typeof parsed.sessions !== "object") {
|
|
805
3194
|
return { sessions: {} };
|
|
806
3195
|
}
|
|
@@ -819,17 +3208,17 @@ function writeState(path, state) {
|
|
|
819
3208
|
}
|
|
820
3209
|
}
|
|
821
3210
|
function orphaned(state, exceptSessionId) {
|
|
822
|
-
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));
|
|
823
3212
|
}
|
|
824
3213
|
|
|
825
3214
|
// src/spool.ts
|
|
826
|
-
import { appendFileSync, existsSync as
|
|
3215
|
+
import { appendFileSync, existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as readFileSync6, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
827
3216
|
import { dirname as dirname2 } from "node:path";
|
|
828
3217
|
var MAX_SPOOL_BYTES = 32 * 1024 * 1024;
|
|
829
3218
|
function append(spoolPath, post2) {
|
|
830
3219
|
try {
|
|
831
3220
|
mkdirSync2(dirname2(spoolPath), { recursive: true });
|
|
832
|
-
if (
|
|
3221
|
+
if (existsSync7(spoolPath) && sizeOf(spoolPath) >= MAX_SPOOL_BYTES) return;
|
|
833
3222
|
appendFileSync(spoolPath, `${JSON.stringify(post2)}
|
|
834
3223
|
`, { mode: 384 });
|
|
835
3224
|
} catch {
|
|
@@ -837,7 +3226,7 @@ function append(spoolPath, post2) {
|
|
|
837
3226
|
}
|
|
838
3227
|
function read(spoolPath) {
|
|
839
3228
|
try {
|
|
840
|
-
return
|
|
3229
|
+
return readFileSync6(spoolPath, "utf8").split("\n").filter((line) => line.trim().length > 0).flatMap((line) => {
|
|
841
3230
|
try {
|
|
842
3231
|
return [JSON.parse(line)];
|
|
843
3232
|
} catch {
|
|
@@ -866,7 +3255,7 @@ function keep(spoolPath, remaining) {
|
|
|
866
3255
|
}
|
|
867
3256
|
function sizeOf(path) {
|
|
868
3257
|
try {
|
|
869
|
-
return
|
|
3258
|
+
return readFileSync6(path).byteLength;
|
|
870
3259
|
} catch {
|
|
871
3260
|
return 0;
|
|
872
3261
|
}
|
|
@@ -874,7 +3263,10 @@ function sizeOf(path) {
|
|
|
874
3263
|
|
|
875
3264
|
// src/capture.ts
|
|
876
3265
|
var DEADLINE_MS = 6e3;
|
|
877
|
-
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
|
+
}
|
|
878
3270
|
function readStdin() {
|
|
879
3271
|
return new Promise((resolve) => {
|
|
880
3272
|
let raw = "";
|
|
@@ -892,8 +3284,8 @@ function withDeadline(work, ms) {
|
|
|
892
3284
|
}
|
|
893
3285
|
function parseTranscript(path, cwd) {
|
|
894
3286
|
try {
|
|
895
|
-
const stat =
|
|
896
|
-
if (!stat.isFile() || stat.size >
|
|
3287
|
+
const stat = statSync5(path);
|
|
3288
|
+
if (!stat.isFile() || stat.size > MAX_TRANSCRIPT_BYTES2) return null;
|
|
897
3289
|
return parseTranscriptFile(path, cwd, deriveRepoId(cwd));
|
|
898
3290
|
} catch {
|
|
899
3291
|
return null;
|
|
@@ -920,9 +3312,11 @@ async function deliver(spoolPath, path, body, deps) {
|
|
|
920
3312
|
}
|
|
921
3313
|
async function handleEvent(event, deps) {
|
|
922
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);
|
|
923
3317
|
const sessionId = event.session_id;
|
|
924
|
-
if (!
|
|
925
|
-
const spoolPath =
|
|
3318
|
+
if (!sessionId) return null;
|
|
3319
|
+
const spoolPath = join7(stateDir(deps.home), "spool.jsonl");
|
|
926
3320
|
const drained = await drainSpool(spoolPath, deps);
|
|
927
3321
|
const path = statePath(deps.home);
|
|
928
3322
|
const state = readState(path) ?? EMPTY;
|
|
@@ -938,6 +3332,13 @@ async function handleEvent(event, deps) {
|
|
|
938
3332
|
if (!transcriptPath) {
|
|
939
3333
|
return { event: name, turnsSent: 0, archived: false, spoolDrained: drained.sent };
|
|
940
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
|
+
}
|
|
941
3342
|
const parsed = parseTranscript(transcriptPath, event.cwd ?? process.cwd());
|
|
942
3343
|
const sentThrough = prior?.sentThrough ?? 0;
|
|
943
3344
|
let turnsSent = 0;
|
|
@@ -969,6 +3370,39 @@ async function handleEvent(event, deps) {
|
|
|
969
3370
|
writeState(path, state);
|
|
970
3371
|
return { event: name, turnsSent, archived, spoolDrained: drained.sent };
|
|
971
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
|
+
}
|
|
972
3406
|
async function main() {
|
|
973
3407
|
const raw = await withDeadline(readStdin(), DEADLINE_MS);
|
|
974
3408
|
if (!raw) return;
|
|
@@ -981,7 +3415,7 @@ async function main() {
|
|
|
981
3415
|
const { evrexApi: evrexApi2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
982
3416
|
const deps = {
|
|
983
3417
|
now: () => /* @__PURE__ */ new Date(),
|
|
984
|
-
home:
|
|
3418
|
+
home: homedir7(),
|
|
985
3419
|
machine: hostname(),
|
|
986
3420
|
post: async (path, body) => {
|
|
987
3421
|
try {
|
|
@@ -1008,5 +3442,6 @@ main().then(
|
|
|
1008
3442
|
export {
|
|
1009
3443
|
drainSpool,
|
|
1010
3444
|
handleEvent,
|
|
1011
|
-
parseTranscript
|
|
3445
|
+
parseTranscript,
|
|
3446
|
+
subagentTranscriptPath
|
|
1012
3447
|
};
|