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/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/client.ts
13
- var client_exports = {};
14
- __export(client_exports, {
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 describeFailure(method, path, status, statusText) {
23
- if (status === 401 || status === 403) {
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
- async function request(method, path, { body, absentIsAnswer } = {}) {
29
- const res = await fetch(`${API_BASE_URL}${path}`, {
30
- method,
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 DEFAULT_API_BASE_URL, API_BASE_URL, EVREX_TOKEN, get, getOrNull, post, evrexApi;
39
- var init_client = __esm({
40
- "src/client.ts"() {
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
- DEFAULT_API_BASE_URL = "https://api.evrex.ai";
43
- API_BASE_URL = (process.env.EVREX_API_BASE_URL ?? process.env.EVREX_API_URL ?? DEFAULT_API_BASE_URL).replace(/\/+$/, "");
44
- EVREX_TOKEN = process.env.EVREX_TOKEN ?? process.env.EVREX_API_TOKEN ?? null;
45
- get = (path) => request("GET", path);
46
- getOrNull = (path) => request("GET", path, { absentIsAnswer: true });
47
- post = (path, body) => request("POST", path, { body });
48
- evrexApi = {
49
- baseUrl: API_BASE_URL,
50
- repos: () => get("/repos"),
51
- commits: (repoPath) => get(`/commits?repoPath=${encodeURIComponent(repoPath)}`),
52
- // Abbreviated shas resolve server-side, so a value pasted from `git log`
53
- // works here (apps/backend/src/reads/reads.service.ts#resolveSha).
54
- commit: (sha) => getOrNull(`/commits/${encodeURIComponent(sha)}`),
55
- sessions: (repoPath) => get(`/sessions?repoPath=${encodeURIComponent(repoPath)}`),
56
- session: (id) => getOrNull(`/sessions/${encodeURIComponent(id)}`),
57
- // The paginated transcript see reads.service.ts#getSessionTurns. `ts, id`
58
- // ordering server-side makes offsets stable across requests.
59
- sessionTurns: (id, offset, limit) => getOrNull(
60
- `/sessions/${encodeURIComponent(id)}/turns?offset=${offset}&limit=${limit}`
61
- ),
62
- ask: (repoPath, text, filePaths) => post("/ask", { repoPath, text, filePaths }),
63
- // Evidence-only retrieval (BM25 + embedding), no LLM synthesis call — see
64
- // apps/backend/src/query/query.service.ts#search. Used by evrex_search,
65
- // which wants ranked hits fast, not a synthesized paragraph.
66
- search: (repoPath, text, filePaths) => post("/search", { repoPath, text, filePaths })
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/claude-sessions.ts
155
- import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
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
- var PRIVATE_BLOCK = /<private>[\s\S]*?(?:<\/private>|$)/gi;
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
- // ../../packages/ingest-core/src/types.ts
235
- var CONVERSATION_KINDS = [
236
- "claude-code",
237
- "cursor",
238
- "codex",
239
- "gemini",
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
- var MIN_MEANINGFUL_LINE_LENGTH = 6;
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
- var MAX_TURN_TEXT_LENGTH = 4e3;
284
- var FILE_PATH_TOOLS = /* @__PURE__ */ new Set(["Read", "Edit", "Write", "NotebookEdit"]);
285
- var PATH_TOKEN_RE = /(?:[.~]?\/)?(?:[\w-]+\/)+[\w.-]+/g;
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 (FILE_PATH_TOOLS.has(name) && typeof input.file_path === "string") {
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 parseSessionFile(filePath, repoPath, repoId = deriveRepoId(repoPath)) {
385
- const raw = readFileSync(filePath, "utf-8");
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: "claude-code",
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
- // ../../packages/ingest-core/src/derive-uuid.ts
499
- import { createHash } from "node:crypto";
500
- function deriveUuid(name) {
501
- const h = createHash("sha1").update(name).digest("hex");
502
- const variant = (parseInt(h.slice(16, 17) || "0", 16) & 3 | 8).toString(16);
503
- const s = h.slice(0, 12) + // time-low + time-mid
504
- "5" + // version 5 (name-based, SHA-1)
505
- h.slice(13, 16) + variant + h.slice(17, 32);
506
- return `${s.slice(0, 8)}-${s.slice(8, 12)}-${s.slice(12, 16)}-${s.slice(16, 20)}-${s.slice(20, 32)}`;
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/codex-sessions.ts
510
- import { createHash as createHash2 } from "node:crypto";
511
- import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync as readdirSync2, statSync as statSync2 } from "node:fs";
512
- function deriveUuid2(name) {
513
- const h = createHash2("sha1").update(name).digest("hex");
514
- const variant = (parseInt(h.slice(16, 17) || "0", 16) & 3 | 8).toString(16);
515
- const s = h.slice(0, 12) + "5" + h.slice(13, 16) + variant + h.slice(17, 32);
516
- return `${s.slice(0, 8)}-${s.slice(8, 12)}-${s.slice(12, 16)}-${s.slice(16, 20)}-${s.slice(20, 32)}`;
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 deriveCodexTurnId(sessionId, ordinal) {
519
- return deriveUuid2(`evrex-codex-turn\0${sessionId}\0${ordinal}`);
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 parseLines(raw) {
522
- const out = [];
523
- for (const line of raw.split("\n")) {
524
- if (!line.trim()) continue;
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
- out.push(JSON.parse(line));
1096
+ records[key] = value ? JSON.parse(value) : null;
527
1097
  } catch {
528
- continue;
1098
+ records[key] = value;
529
1099
  }
530
1100
  }
531
- return out;
1101
+ return records;
532
1102
  }
533
- function payloadType(line) {
534
- const t = line.payload?.type;
535
- return typeof t === "string" ? t : void 0;
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 = readFileSync2(filePath, "utf-8");
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 git = meta.git;
684
- if (typeof git !== "object" || git === null) return null;
685
- const url = asString(git.repository_url);
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
- var NUL = String.fromCharCode(0);
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 readFileSync3 } from "node:fs";
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 = readFileSync3(filePath, "utf-8");
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(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/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 existsSync3, mkdirSync, readFileSync as readFileSync4, renameSync, writeFileSync } from "node:fs";
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 = homedir()) {
806
- return join(home, ".evrex");
3197
+ function stateDir(home = homedir6()) {
3198
+ return join6(home, ".evrex");
807
3199
  }
808
- function statePath(home = homedir()) {
809
- return join(stateDir(home), "capture-state.json");
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(readFileSync4(path, "utf8"));
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) => existsSync3(s.transcriptPath));
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 existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync5, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
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 (existsSync4(spoolPath) && sizeOf(spoolPath) >= MAX_SPOOL_BYTES) return;
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 readFileSync5(spoolPath, "utf8").split("\n").filter((line) => line.trim().length > 0).flatMap((line) => {
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 readFileSync5(path).byteLength;
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 MAX_TRANSCRIPT_BYTES = 256 * 1024 * 1024;
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 = statSync3(path);
906
- if (!stat.isFile() || stat.size > MAX_TRANSCRIPT_BYTES) return null;
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 (!name || !sessionId) return null;
935
- const spoolPath = join2(stateDir(deps.home), "spool.jsonl");
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: homedir2(),
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
  };