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