evrex-mcp 0.6.0 → 0.8.0

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