evrex-mcp 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/capture.js CHANGED
@@ -9,72 +9,82 @@ var __export = (target, all) => {
9
9
  __defProp(target, name, { get: all[name], enumerable: true });
10
10
  };
11
11
 
12
- // src/client.ts
13
- var client_exports = {};
14
- __export(client_exports, {
15
- evrexApi: () => evrexApi
16
- });
17
- function headers() {
18
- const base = { "Content-Type": "application/json" };
19
- if (EVREX_TOKEN) base.Authorization = `Bearer ${EVREX_TOKEN}`;
20
- return base;
12
+ // ../../packages/ingest-core/src/types.ts
13
+ function isReferenceKind(kind) {
14
+ return REFERENCE_KINDS.includes(kind ?? "");
21
15
  }
22
- function describeFailure(method, path, status, statusText) {
23
- if (status === 401 || status === 403) {
24
- return EVREX_TOKEN ? `${method} ${path} -> ${status}: the EVREX_TOKEN this server was started with was rejected. It may be revoked or expired.` : `${method} ${path} -> ${status}: this evrex backend requires a credential, but no EVREX_TOKEN is set for this MCP server.`;
25
- }
26
- return `${method} ${path} -> ${status} ${statusText}`;
16
+ function lacksFileAttribution(kind) {
17
+ return kind === "slack" || isReferenceKind(kind);
27
18
  }
28
- async function request(method, path, { body, absentIsAnswer } = {}) {
29
- const res = await fetch(`${API_BASE_URL}${path}`, {
30
- method,
31
- headers: headers(),
32
- ...body === void 0 ? {} : { body: JSON.stringify(body) }
33
- });
34
- if (res.status === 404 && absentIsAnswer) return null;
35
- if (!res.ok) throw new Error(describeFailure(method, path, res.status, res.statusText));
36
- return await res.json();
19
+ function provenanceTrailerFor(key) {
20
+ const k = key.toLowerCase();
21
+ return PROVENANCE_TRAILERS.find((t) => t.key.toLowerCase() === k);
37
22
  }
38
- var DEFAULT_API_BASE_URL, API_BASE_URL, EVREX_TOKEN, get, getOrNull, post, evrexApi;
39
- var init_client = __esm({
40
- "src/client.ts"() {
23
+ var CONVERSATION_KINDS, REFERENCE_KINDS, SOURCE_KINDS, EMPTY_USAGE, PROVENANCE_TRAILERS, STATED_TRAILERS;
24
+ var init_types = __esm({
25
+ "../../packages/ingest-core/src/types.ts"() {
41
26
  "use strict";
42
- DEFAULT_API_BASE_URL = "https://api.evrex.ai";
43
- API_BASE_URL = (process.env.EVREX_API_BASE_URL ?? process.env.EVREX_API_URL ?? DEFAULT_API_BASE_URL).replace(/\/+$/, "");
44
- EVREX_TOKEN = process.env.EVREX_TOKEN ?? process.env.EVREX_API_TOKEN ?? null;
45
- get = (path) => request("GET", path);
46
- getOrNull = (path) => request("GET", path, { absentIsAnswer: true });
47
- post = (path, body) => request("POST", path, { body });
48
- evrexApi = {
49
- baseUrl: API_BASE_URL,
50
- repos: () => get("/repos"),
51
- commits: (repoPath) => get(`/commits?repoPath=${encodeURIComponent(repoPath)}`),
52
- // Abbreviated shas resolve server-side, so a value pasted from `git log`
53
- // works here (apps/backend/src/reads/reads.service.ts#resolveSha).
54
- commit: (sha) => getOrNull(`/commits/${encodeURIComponent(sha)}`),
55
- sessions: (repoPath) => get(`/sessions?repoPath=${encodeURIComponent(repoPath)}`),
56
- session: (id) => getOrNull(`/sessions/${encodeURIComponent(id)}`),
57
- // The paginated transcript see reads.service.ts#getSessionTurns. `ts, id`
58
- // ordering server-side makes offsets stable across requests.
59
- sessionTurns: (id, offset, limit) => getOrNull(
60
- `/sessions/${encodeURIComponent(id)}/turns?offset=${offset}&limit=${limit}`
61
- ),
62
- ask: (repoPath, text, filePaths) => post("/ask", { repoPath, text, filePaths }),
63
- // Evidence-only retrieval (BM25 + embedding), no LLM synthesis call — see
64
- // apps/backend/src/query/query.service.ts#search. Used by evrex_search,
65
- // which wants ranked hits fast, not a synthesized paragraph.
66
- search: (repoPath, text, filePaths) => post("/search", { repoPath, text, filePaths })
27
+ CONVERSATION_KINDS = [
28
+ "claude-code",
29
+ // Xcode's coding assistant the Claude Agent SDK embedded, writing the
30
+ // identical transcript format under ~/Library/Developer/Xcode. A separate
31
+ // kind because a conversation in an IDE panel is not a CLI session, and the
32
+ // Slack mislabel already taught this list what an absent entry costs.
33
+ "claude-xcode",
34
+ "cursor",
35
+ "codex",
36
+ "gemini",
37
+ // OpenCode keeps its sessions in a SQLite store rather than files; the
38
+ // parser reads the store, so this kind arrives through the same importer
39
+ // as Cursor's.
40
+ "opencode",
41
+ // GitHub Copilot CLI writes an events file per session under ~/.copilot;
42
+ // the coding agent on github.com is a different thing and arrives as a
43
+ // reference through the Agent-Logs-Url trailer, not as this kind.
44
+ "copilot",
45
+ "slack"
46
+ ];
47
+ REFERENCE_KINDS = ["linear", "jira", "confluence"];
48
+ SOURCE_KINDS = [
49
+ ...CONVERSATION_KINDS,
50
+ ...REFERENCE_KINDS
51
+ ];
52
+ EMPTY_USAGE = {
53
+ inputTokens: null,
54
+ outputTokens: null,
55
+ cacheReadTokens: null,
56
+ cacheWriteTokens: null,
57
+ model: null
67
58
  };
59
+ PROVENANCE_TRAILERS = [
60
+ // GitHub's Copilot coding agent stamps every commit it makes with a link to
61
+ // its session logs (changelog, 2026-03-20). Copilot exposes no lifecycle
62
+ // hooks evrex could capture through, so this is the only way one of its
63
+ // commits ever gets a session behind it.
64
+ { key: "Agent-Logs-Url", agent: "copilot", label: "GitHub Copilot coding agent" },
65
+ // Entire's Checkpoints CLI: a checkpoint id, resolvable on entire.io or in
66
+ // the repo's own `refs/entire/checkpoints/` refs.
67
+ { key: "Entire-Checkpoint", agent: "entire", label: "Entire checkpoint" },
68
+ // AgentsRoom's Commit Context: the whole conversation, as an unlisted gist.
69
+ { key: "Agent-Conversation", agent: "agentsroom", label: "AgentsRoom conversation" }
70
+ ];
71
+ STATED_TRAILERS = [
72
+ { key: "Evrex-Rejected", kind: "rejected" },
73
+ { key: "Evrex-Constraint", kind: "constraint" },
74
+ { key: "Evrex-Decision", kind: "decision" }
75
+ ];
68
76
  }
69
77
  });
70
78
 
71
- // src/capture.ts
72
- import { statSync as statSync3 } from "node:fs";
73
- import { homedir as homedir2, hostname } from "node:os";
74
- import { join as join2 } from "node:path";
75
-
76
79
  // ../../packages/ingest-core/src/git-history.ts
77
80
  import { execFileSync } from "node:child_process";
81
+ function git(repoPath, args, input) {
82
+ return execFileSync("git", args, {
83
+ cwd: repoPath,
84
+ maxBuffer: 1024 * 1024 * 64,
85
+ input
86
+ }).toString("utf-8");
87
+ }
78
88
  function gitQuiet(repoPath, args) {
79
89
  return execFileSync("git", args, {
80
90
  cwd: repoPath,
@@ -116,6 +126,10 @@ function repoIdFromRootCommit(sha) {
116
126
  function repoIdFromPath(repoPath) {
117
127
  return `path:${repoPath}`;
118
128
  }
129
+ function repoNameFromId(repoId) {
130
+ const withoutPrefix = repoId.replace(/^(remote|root|path):/, "");
131
+ return withoutPrefix.split("/").filter(Boolean).pop() ?? repoId;
132
+ }
119
133
  function firstRemoteUrl(repoPath) {
120
134
  try {
121
135
  const origin = gitQuiet(repoPath, ["remote", "get-url", "origin"]).trim();
@@ -150,26 +164,268 @@ function deriveRepoId(repoPath) {
150
164
  if (root) return repoIdFromRootCommit(root);
151
165
  return repoIdFromPath(repoPath);
152
166
  }
167
+ function getGitUserName(repoPath) {
168
+ try {
169
+ const name = git(repoPath, ["config", "user.name"]).trim();
170
+ return name || null;
171
+ } catch {
172
+ return null;
173
+ }
174
+ }
175
+ function listShas(repoPath) {
176
+ const out = git(repoPath, ["rev-list", "--reverse", "HEAD"]).trim();
177
+ return out.length ? out.split("\n") : [];
178
+ }
179
+ function commitMeta(repoPath, sha) {
180
+ const line = git(repoPath, [
181
+ "show",
182
+ "-s",
183
+ `--format=%H${FIELD_SEP}%an${FIELD_SEP}%ae${FIELD_SEP}%aI`,
184
+ sha
185
+ ]).trim();
186
+ const parts = line.split(FIELD_SEP);
187
+ const message = git(repoPath, ["show", "-s", "--format=%B", sha]).replace(/\n+$/, "");
188
+ return {
189
+ sha: parts[0] ?? sha,
190
+ author: parts[1] ?? "",
191
+ authorEmail: parts[2] ?? "",
192
+ ts: parts[3] ?? (/* @__PURE__ */ new Date()).toISOString(),
193
+ message
194
+ };
195
+ }
196
+ function parseTrailers(repoPath, message) {
197
+ try {
198
+ const out = git(
199
+ repoPath,
200
+ ["interpret-trailers", "--parse", "--no-divider"],
201
+ message
202
+ ).trim();
203
+ const trailers = [];
204
+ for (const line of out.split("\n")) {
205
+ const idx = line.indexOf(":");
206
+ if (idx === -1) continue;
207
+ const key = line.slice(0, idx).trim();
208
+ const value = line.slice(idx + 1).trim();
209
+ if (key && value) trailers.push({ key, value });
210
+ }
211
+ return trailers;
212
+ } catch {
213
+ return [];
214
+ }
215
+ }
216
+ function trailersFromMessage(message) {
217
+ const paragraphs = message.replace(/\r\n/g, "\n").trim().split(/\n{2,}/);
218
+ const last = paragraphs[paragraphs.length - 1] ?? "";
219
+ if (paragraphs.length < 2) return [];
220
+ const lines = last.split("\n");
221
+ const trailers = [];
222
+ for (const line of lines) {
223
+ const m = /^([A-Za-z][A-Za-z0-9-]*):\s+(.+?)\s*$/.exec(line);
224
+ if (!m) return [];
225
+ trailers.push({ key: m[1], value: m[2] });
226
+ }
227
+ return trailers;
228
+ }
229
+ function statedInsightsOf(trailers) {
230
+ const out = [];
231
+ for (const t of trailers) {
232
+ const known = STATED_TRAILERS.find((s) => s.key.toLowerCase() === t.key.toLowerCase());
233
+ if (known && t.value.trim()) out.push({ kind: known.kind, text: t.value.trim() });
234
+ }
235
+ return out;
236
+ }
237
+ function trailerValue(trailers, key) {
238
+ const k = key.toLowerCase();
239
+ return trailers.find((t) => t.key.toLowerCase() === k)?.value ?? null;
240
+ }
241
+ function agentTrailersOf(trailers) {
242
+ const out = [];
243
+ for (const t of trailers) {
244
+ const known = provenanceTrailerFor(t.key);
245
+ if (known) out.push({ key: known.key, value: t.value, agent: known.agent });
246
+ }
247
+ return out;
248
+ }
249
+ function commitBranch(repoPath, sha) {
250
+ try {
251
+ const out = git(repoPath, ["branch", "--contains", sha, "--format=%(refname:short)"]).trim();
252
+ const first = out.split("\n").find((b) => b.length > 0);
253
+ return first ?? null;
254
+ } catch {
255
+ return null;
256
+ }
257
+ }
258
+ function numstat(repoPath, sha) {
259
+ const out = git(repoPath, ["show", "--numstat", "--format=", sha]).trim();
260
+ const map = /* @__PURE__ */ new Map();
261
+ if (!out) return map;
262
+ for (const line of out.split("\n")) {
263
+ const [ins, del, path] = line.split(" ");
264
+ if (!path) continue;
265
+ map.set(path, {
266
+ insertions: !ins || ins === "-" ? 0 : Number.parseInt(ins, 10) || 0,
267
+ deletions: !del || del === "-" ? 0 : Number.parseInt(del, 10) || 0
268
+ });
269
+ }
270
+ return map;
271
+ }
272
+ function parseDiffBlocks(diff) {
273
+ const map = /* @__PURE__ */ new Map();
274
+ if (!diff.trim()) return map;
275
+ const blocks = diff.split(/^diff --git /m).filter(Boolean);
276
+ for (const block of blocks) {
277
+ const full = `diff --git ${block}`;
278
+ const pathMatch = block.match(/^a\/(.+?) b\/(.+?)\n/);
279
+ const path = pathMatch?.[2] ?? block.match(/^\S+/)?.[0] ?? "unknown";
280
+ const hunkHeaders = [...full.matchAll(/^@@ .+? @@.*$/gm)].map((m) => m[0]);
281
+ map.set(path, {
282
+ hunkHeaders,
283
+ diffText: full.length > DIFF_CAP ? full.slice(0, DIFF_CAP) : full
284
+ });
285
+ }
286
+ return map;
287
+ }
288
+ function commitFiles(repoPath, sha) {
289
+ const stats = numstat(repoPath, sha);
290
+ const diff = git(repoPath, ["show", "--unified=0", "--format=", sha]);
291
+ const blocks = parseDiffBlocks(diff);
292
+ const paths = /* @__PURE__ */ new Set([...stats.keys(), ...blocks.keys()]);
293
+ const files = [];
294
+ for (const path of paths) {
295
+ const stat = stats.get(path) ?? { insertions: 0, deletions: 0 };
296
+ const block = blocks.get(path) ?? { hunkHeaders: [], diffText: "" };
297
+ files.push({
298
+ path,
299
+ insertions: stat.insertions,
300
+ deletions: stat.deletions,
301
+ hunkHeaders: block.hunkHeaders,
302
+ diffText: block.diffText,
303
+ diffTruncated: block.diffText.length >= DIFF_CAP
304
+ });
305
+ }
306
+ return files;
307
+ }
308
+ function parseGitLog(repoPath, repoId, known) {
309
+ const shas = listShas(repoPath).filter((sha) => !known?.has(sha));
310
+ const id = repoId ?? deriveRepoId(repoPath);
311
+ return shas.map((sha) => {
312
+ const meta = commitMeta(repoPath, sha);
313
+ const trailers = parseTrailers(repoPath, meta.message);
314
+ return {
315
+ sha: meta.sha,
316
+ repoId: id,
317
+ repoPath,
318
+ author: meta.author,
319
+ authorEmail: meta.authorEmail,
320
+ ts: meta.ts,
321
+ message: meta.message,
322
+ branch: commitBranch(repoPath, sha),
323
+ evrexSessionTrailer: trailerValue(trailers, EVREX_SESSION_TRAILER_KEY),
324
+ agentTrailers: agentTrailersOf(trailers),
325
+ statedInsights: statedInsightsOf(trailers),
326
+ files: commitFiles(repoPath, sha)
327
+ };
328
+ });
329
+ }
330
+ var DIFF_CAP, FIELD_SEP, EVREX_SESSION_TRAILER_KEY;
331
+ var init_git_history = __esm({
332
+ "../../packages/ingest-core/src/git-history.ts"() {
333
+ "use strict";
334
+ init_types();
335
+ DIFF_CAP = 2e4;
336
+ FIELD_SEP = "";
337
+ EVREX_SESSION_TRAILER_KEY = "Evrex-Session";
338
+ }
339
+ });
153
340
 
154
- // ../../packages/ingest-core/src/claude-sessions.ts
155
- 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
+ });
156
427
 
157
428
  // ../../packages/ingest-core/src/redact.ts
158
- var PATTERNS = [
159
- { type: "private_key", regex: /-----BEGIN(?: [A-Z]+)? PRIVATE KEY-----[\s\S]*?-----END(?: [A-Z]+)? PRIVATE KEY-----/g },
160
- { type: "aws_access_key", regex: /AKIA[0-9A-Z]{16}/g },
161
- { type: "github_token", regex: /gh[pousr]_[A-Za-z0-9]{36,}/g },
162
- { type: "slack_token", regex: /xox[baprs]-[A-Za-z0-9-]{10,}/g },
163
- { type: "stripe_key", regex: /sk_(live|test)_[A-Za-z0-9]{24,}/g },
164
- { type: "openai_key", regex: /sk-[A-Za-z0-9]{20,}/g },
165
- { type: "anthropic_key", regex: /sk-ant-[A-Za-z0-9-_]{20,}/g },
166
- { type: "jwt", regex: /eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g },
167
- { type: "bearer_token", regex: /Bearer\s+[A-Za-z0-9\-_.]{20,}/g },
168
- {
169
- type: "env_secret",
170
- regex: /\b([A-Z0-9_]*(?:_KEY|_SECRET|_TOKEN|_PASSWORD|PASSWD)[A-Z0-9_]*)\s*[:=]\s*["']?[^\s"'\n]{6,}["']?/g
171
- }
172
- ];
173
429
  function redactJsonValue(value) {
174
430
  let count = 0;
175
431
  const walk = (v) => {
@@ -190,7 +446,31 @@ function redactJsonValue(value) {
190
446
  };
191
447
  return { value: walk(value), count };
192
448
  }
193
- var PRIVATE_BLOCK = /<private>[\s\S]*?(?:<\/private>|$)/gi;
449
+ function redactRawTranscript(content, format) {
450
+ let count = 0;
451
+ if (format === "jsonl") {
452
+ const out = content.split("\n").map((line) => {
453
+ if (!line.trim()) return line;
454
+ try {
455
+ const r = redactJsonValue(JSON.parse(line));
456
+ count += r.count;
457
+ return JSON.stringify(r.value);
458
+ } catch {
459
+ const r = redactSecrets(line);
460
+ count += r.count;
461
+ return r.text;
462
+ }
463
+ });
464
+ return { content: out.join("\n"), count };
465
+ }
466
+ try {
467
+ const r = redactJsonValue(JSON.parse(content));
468
+ return { content: JSON.stringify(r.value), count: r.count };
469
+ } catch {
470
+ const r = redactSecrets(content);
471
+ return { content: r.text, count: r.count };
472
+ }
473
+ }
194
474
  function redactSecrets(input) {
195
475
  let text = input;
196
476
  let count = 0;
@@ -209,8 +489,74 @@ function redactSecrets(input) {
209
489
  }
210
490
  return { text, count };
211
491
  }
492
+ var PATTERNS, PRIVATE_BLOCK;
493
+ var init_redact = __esm({
494
+ "../../packages/ingest-core/src/redact.ts"() {
495
+ "use strict";
496
+ PATTERNS = [
497
+ { type: "private_key", regex: /-----BEGIN(?: [A-Z]+)? PRIVATE KEY-----[\s\S]*?-----END(?: [A-Z]+)? PRIVATE KEY-----/g },
498
+ { type: "aws_access_key", regex: /AKIA[0-9A-Z]{16}/g },
499
+ { type: "github_token", regex: /gh[pousr]_[A-Za-z0-9]{36,}/g },
500
+ { type: "slack_token", regex: /xox[baprs]-[A-Za-z0-9-]{10,}/g },
501
+ { type: "stripe_key", regex: /sk_(live|test)_[A-Za-z0-9]{24,}/g },
502
+ { type: "openai_key", regex: /sk-[A-Za-z0-9]{20,}/g },
503
+ { type: "anthropic_key", regex: /sk-ant-[A-Za-z0-9-_]{20,}/g },
504
+ { type: "jwt", regex: /eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g },
505
+ { type: "bearer_token", regex: /Bearer\s+[A-Za-z0-9\-_.]{20,}/g },
506
+ {
507
+ type: "env_secret",
508
+ regex: /\b([A-Z0-9_]*(?:_KEY|_SECRET|_TOKEN|_PASSWORD|PASSWD)[A-Z0-9_]*)\s*[:=]\s*["']?[^\s"'\n]{6,}["']?/g
509
+ }
510
+ ];
511
+ PRIVATE_BLOCK = /<private>[\s\S]*?(?:<\/private>|$)/gi;
512
+ }
513
+ });
212
514
 
213
515
  // ../../packages/ingest-core/src/subject-linking.ts
516
+ function matchCommitToSession(commit, sessions) {
517
+ const subject = commit.subject.trim();
518
+ if (subject.length === 0) {
519
+ return { sha: commit.sha, sessionId: null, reason: "absent" };
520
+ }
521
+ const ran = sessions.filter((s) => s.committedSubjects.includes(subject));
522
+ if (ran.length === 0) {
523
+ return { sha: commit.sha, sessionId: null, reason: "absent" };
524
+ }
525
+ if (ran.length === 1) {
526
+ return {
527
+ sha: commit.sha,
528
+ sessionId: ran[0].sessionId,
529
+ evidence: "ran-the-commit",
530
+ confidence: INFERRED_CONFIDENCE
531
+ };
532
+ }
533
+ const oneLineage = ran.every((s) => s.startedAt === ran[0].startedAt);
534
+ if (!oneLineage) {
535
+ return { sha: commit.sha, sessionId: null, reason: "ambiguous" };
536
+ }
537
+ const original = [...ran].sort((a, b) => a.endedAt - b.endedAt)[0];
538
+ return {
539
+ sha: commit.sha,
540
+ sessionId: original.sessionId,
541
+ evidence: "ran-the-commit-then-lineage",
542
+ confidence: INFERRED_CONFIDENCE
543
+ };
544
+ }
545
+ function proposeLinks(commits, sessions) {
546
+ const out = [];
547
+ for (const commit of commits) {
548
+ const match = matchCommitToSession(commit, sessions);
549
+ if (match.sessionId !== null) {
550
+ out.push({
551
+ sha: match.sha,
552
+ sessionId: match.sessionId,
553
+ confidence: match.confidence,
554
+ evidence: match.evidence
555
+ });
556
+ }
557
+ }
558
+ return out;
559
+ }
214
560
  function committedSubjects(command) {
215
561
  const out = [];
216
562
  const invocation = /\bgit\s+(?:-C\s+\S+\s+)?commit\b/g;
@@ -230,30 +576,18 @@ function committedSubjects(command) {
230
576
  }
231
577
  return out;
232
578
  }
233
-
234
- // ../../packages/ingest-core/src/types.ts
235
- var CONVERSATION_KINDS = [
236
- "claude-code",
237
- "cursor",
238
- "codex",
239
- "gemini",
240
- "slack"
241
- ];
242
- var REFERENCE_KINDS = ["linear", "jira", "confluence"];
243
- var SOURCE_KINDS = [
244
- ...CONVERSATION_KINDS,
245
- ...REFERENCE_KINDS
246
- ];
247
- var EMPTY_USAGE = {
248
- inputTokens: null,
249
- outputTokens: null,
250
- cacheReadTokens: null,
251
- cacheWriteTokens: null,
252
- model: null
253
- };
579
+ var INFERRED_CONFIDENCE;
580
+ var init_subject_linking = __esm({
581
+ "../../packages/ingest-core/src/subject-linking.ts"() {
582
+ "use strict";
583
+ INFERRED_CONFIDENCE = 0.9;
584
+ }
585
+ });
254
586
 
255
587
  // ../../packages/ingest-core/src/claude-sessions.ts
256
- 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";
257
591
  function meaningfulLines(lines) {
258
592
  return lines.map((l) => l.trim()).filter((l) => l.length >= MIN_MEANINGFUL_LINE_LENGTH);
259
593
  }
@@ -280,9 +614,56 @@ function extractEditedLines(toolUseResult) {
280
614
  if (meaningfulAdded.length === 0 && meaningfulRemoved.length === 0) return null;
281
615
  return { path: r.filePath, added: meaningfulAdded, removed: meaningfulRemoved };
282
616
  }
283
- var MAX_TURN_TEXT_LENGTH = 4e3;
284
- var FILE_PATH_TOOLS = /* @__PURE__ */ new Set(["Read", "Edit", "Write", "NotebookEdit"]);
285
- var PATH_TOKEN_RE = /(?:[.~]?\/)?(?:[\w-]+\/)+[\w.-]+/g;
617
+ function slugifyCwd(repoPath) {
618
+ return repoPath.replace(/[^A-Za-z0-9]/g, "-");
619
+ }
620
+ function claudeProjectsDir() {
621
+ return join(homedir(), ".claude", "projects");
622
+ }
623
+ function xcodeAssistantProjectsDir() {
624
+ return join(
625
+ homedir(),
626
+ "Library",
627
+ "Developer",
628
+ "Xcode",
629
+ "CodingAssistant",
630
+ "ClaudeAgentConfig",
631
+ "projects"
632
+ );
633
+ }
634
+ function findSessionFiles(repoPath) {
635
+ const roots = [
636
+ { dir: claudeProjectsDir(), agentKind: "claude-code" },
637
+ { dir: xcodeAssistantProjectsDir(), agentKind: "claude-xcode" }
638
+ ];
639
+ const out = [];
640
+ for (const { dir, agentKind } of roots) {
641
+ const slug = join(dir, slugifyCwd(repoPath));
642
+ if (!existsSync(slug)) continue;
643
+ for (const name of readdirSync(slug)) {
644
+ if (name.endsWith(".jsonl")) out.push({ file: join(slug, name), agentKind });
645
+ const subagents = join(slug, name, "subagents");
646
+ if (!name.endsWith(".jsonl") && existsSync(subagents)) {
647
+ for (const child of readdirSync(subagents)) {
648
+ if (child.endsWith(".jsonl")) out.push({ file: join(subagents, child), agentKind });
649
+ }
650
+ }
651
+ }
652
+ }
653
+ return out;
654
+ }
655
+ function editFromXcodeInput(input) {
656
+ const filePath = input.filePath;
657
+ if (typeof filePath !== "string" || !filePath) return null;
658
+ const oldLines = typeof input.oldString === "string" ? input.oldString.split("\n") : [];
659
+ const newLines = typeof input.newString === "string" ? input.newString.split("\n") : [];
660
+ const oldSet = new Set(oldLines);
661
+ const newSet = new Set(newLines);
662
+ const added = meaningfulLines(newLines.filter((l) => !oldSet.has(l)));
663
+ const removed = meaningfulLines(oldLines.filter((l) => !newSet.has(l)));
664
+ if (added.length === 0 && removed.length === 0) return null;
665
+ return { path: filePath, added, removed };
666
+ }
286
667
  function extractPathsFromText(text) {
287
668
  const matches = text.match(PATH_TOKEN_RE) ?? [];
288
669
  return [...new Set(matches)].filter((p) => p.length > 3 && p.length < 300);
@@ -297,6 +678,7 @@ function blockText(content) {
297
678
  function extractFromAssistantContent(content) {
298
679
  const textParts = [];
299
680
  const filesTouched = [];
681
+ const editedLines = [];
300
682
  for (const block of content) {
301
683
  if (block.type === "text" && block.text) {
302
684
  textParts.push(block.text);
@@ -306,7 +688,15 @@ function extractFromAssistantContent(content) {
306
688
  } else if (block.type === "tool_use") {
307
689
  const name = block.name ?? "tool";
308
690
  const input = block.input ?? {};
309
- if (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") {
310
700
  textParts.push(`[tool_call: ${name}] ${input.file_path}`);
311
701
  filesTouched.push({ path: input.file_path, source: "tool_path", tool: name });
312
702
  } else if (name === "Bash" && typeof input.command === "string") {
@@ -320,9 +710,8 @@ function extractFromAssistantContent(content) {
320
710
  }
321
711
  }
322
712
  }
323
- return { text: textParts.join("\n"), filesTouched };
713
+ return { text: textParts.join("\n"), filesTouched, editedLines };
324
714
  }
325
- var SYNTHETIC_CONTENT_RE = /^\s*(<task-notification>|<system-reminder>|\[SYSTEM NOTIFICATION)/;
326
715
  function extractFromUserContent(content) {
327
716
  if (typeof content === "string") {
328
717
  return {
@@ -334,9 +723,11 @@ function extractFromUserContent(content) {
334
723
  if (Array.isArray(content)) {
335
724
  const toolParts = [];
336
725
  const textParts = [];
726
+ let isToolError = false;
337
727
  for (const block of content) {
338
728
  if (block.type === "tool_result") {
339
729
  toolParts.push(blockText(block.content));
730
+ if (block.is_error === true) isToolError = true;
340
731
  } else if (block.type === "text" && typeof block.text === "string") {
341
732
  textParts.push(block.text);
342
733
  }
@@ -345,7 +736,8 @@ function extractFromUserContent(content) {
345
736
  return {
346
737
  text: toolParts.join("\n"),
347
738
  filesTouched: [],
348
- isSyntheticInput: true
739
+ isSyntheticInput: true,
740
+ isToolError
349
741
  };
350
742
  }
351
743
  const text = textParts.join("\n");
@@ -381,8 +773,35 @@ function usageOnce(record, billed) {
381
773
  model: typeof message?.model === "string" ? message.model : null
382
774
  };
383
775
  }
384
- function parseSessionFile(filePath, repoPath, repoId = deriveRepoId(repoPath)) {
385
- 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);
386
805
  const lines = raw.split("\n").filter((l) => l.trim().length > 0);
387
806
  const turns = [];
388
807
  const billedMessages = /* @__PURE__ */ new Set();
@@ -390,6 +809,8 @@ function parseSessionFile(filePath, repoPath, repoId = deriveRepoId(repoPath)) {
390
809
  const cwd = repoPath;
391
810
  let aiTitle = null;
392
811
  let totalRedactions = 0;
812
+ let branch = null;
813
+ let agentId = null;
393
814
  for (const line of lines) {
394
815
  let record;
395
816
  try {
@@ -406,22 +827,27 @@ function parseSessionFile(filePath, repoPath, repoId = deriveRepoId(repoPath)) {
406
827
  const ts = record.timestamp;
407
828
  if (!id || !ts) continue;
408
829
  sessionId ??= record.sessionId ?? record.session_id ?? null;
830
+ if (typeof record.gitBranch === "string" && record.gitBranch) branch = record.gitBranch;
831
+ if (typeof record.agentId === "string" && record.agentId) agentId ??= record.agentId;
409
832
  let text = "";
410
833
  let filesTouched = [];
411
834
  let editedLines = [];
412
835
  let isSyntheticInput = false;
836
+ let isToolError = false;
413
837
  if (record.type === "assistant") {
414
838
  const content = record.message?.content;
415
839
  if (Array.isArray(content)) {
416
840
  const extracted = extractFromAssistantContent(content);
417
841
  text = extracted.text;
418
842
  filesTouched = extracted.filesTouched;
843
+ editedLines = extracted.editedLines;
419
844
  }
420
845
  } else {
421
846
  const extracted = extractFromUserContent(record.message?.content);
422
847
  text = extracted.text;
423
848
  filesTouched = extracted.filesTouched;
424
849
  isSyntheticInput = extracted.isSyntheticInput;
850
+ isToolError = extracted.isToolError ?? false;
425
851
  const edited = extractEditedLines(record.toolUseResult);
426
852
  if (edited) editedLines = [edited];
427
853
  }
@@ -439,12 +865,21 @@ function parseSessionFile(filePath, repoPath, repoId = deriveRepoId(repoPath)) {
439
865
  isSidechain: Boolean(record.isSidechain),
440
866
  redacted: redacted.count > 0,
441
867
  isSyntheticInput,
868
+ isToolError,
442
869
  usage: usageOnce(record, billedMessages)
443
870
  });
444
871
  }
445
872
  if (!sessionId || turns.length === 0) return null;
873
+ const parentSessionId = agentId ? sessionId : null;
874
+ if (agentId) {
875
+ sessionId = deriveUuid(`evrex-subagent\0${parentSessionId}\0${agentId}`);
876
+ for (const t of turns) t.sessionId = sessionId;
877
+ }
878
+ const meta = agentId ? readSubagentMeta(filePath) : null;
879
+ const subagent = agentId ? { agentId, agentType: meta?.agentType ?? null, description: meta?.description ?? null } : null;
880
+ if (agentId && !aiTitle && meta?.description) aiTitle = meta.description;
446
881
  const sortedTs = turns.map((t) => t.ts).sort();
447
- const rawContent = lines.map((line) => {
882
+ const rawContent = capped ? "" : lines.map((line) => {
448
883
  try {
449
884
  return JSON.stringify(redactJsonValue(JSON.parse(line)).value);
450
885
  } catch {
@@ -456,7 +891,7 @@ function parseSessionFile(filePath, repoPath, repoId = deriveRepoId(repoPath)) {
456
891
  }
457
892
  return {
458
893
  id: sessionId,
459
- agentKind: "claude-code",
894
+ agentKind,
460
895
  repoId,
461
896
  cwd,
462
897
  startedAt: sortedTs[0] ?? null,
@@ -468,6 +903,9 @@ function parseSessionFile(filePath, repoPath, repoId = deriveRepoId(repoPath)) {
468
903
  sourceFile: filePath,
469
904
  redactionCount: totalRedactions,
470
905
  committedSubjects: collectCommittedSubjects(lines),
906
+ branch,
907
+ parentSessionId,
908
+ subagent,
471
909
  rawContent,
472
910
  rawFormat: "jsonl",
473
911
  turns
@@ -494,47 +932,913 @@ function collectCommittedSubjects(lines) {
494
932
  }
495
933
  return [...subjects];
496
934
  }
497
-
498
- // ../../packages/ingest-core/src/derive-uuid.ts
499
- import { createHash } from "node:crypto";
500
- function deriveUuid(name) {
501
- const h = createHash("sha1").update(name).digest("hex");
502
- const variant = (parseInt(h.slice(16, 17) || "0", 16) & 3 | 8).toString(16);
503
- const s = h.slice(0, 12) + // time-low + time-mid
504
- "5" + // version 5 (name-based, SHA-1)
505
- h.slice(13, 16) + variant + h.slice(17, 32);
506
- return `${s.slice(0, 8)}-${s.slice(8, 12)}-${s.slice(12, 16)}-${s.slice(16, 20)}-${s.slice(20, 32)}`;
935
+ function parseAllSessions(repoPath, repoId, cursors = {}) {
936
+ const id = repoId ?? deriveRepoId(repoPath);
937
+ const next = { ...cursors };
938
+ const sessions = [];
939
+ let skipped = 0;
940
+ for (const { file, agentKind } of findSessionFiles(repoPath)) {
941
+ const plan = planRead(file, cursors[file]);
942
+ if (plan.reason === "unchanged") {
943
+ skipped++;
944
+ continue;
945
+ }
946
+ const parsed = parseSessionFile(file, repoPath, id, agentKind);
947
+ if (!parsed) continue;
948
+ sessions.push(parsed);
949
+ next[file] = advanceCursor(file, statSync2(file).size);
950
+ }
951
+ return { sessions, cursors: next, skipped };
507
952
  }
953
+ var MIN_MEANINGFUL_LINE_LENGTH, MAX_TURN_TEXT_LENGTH, FILE_PATH_TOOLS, XCODE_EDIT_TOOL, XCODE_PATH_TOOLS, PATH_TOKEN_RE, SYNTHETIC_CONTENT_RE, MAX_TRANSCRIPT_BYTES;
954
+ var init_claude_sessions = __esm({
955
+ "../../packages/ingest-core/src/claude-sessions.ts"() {
956
+ "use strict";
957
+ init_incremental();
958
+ init_git_history();
959
+ init_derive_uuid();
960
+ init_redact();
961
+ init_subject_linking();
962
+ init_types();
963
+ MIN_MEANINGFUL_LINE_LENGTH = 6;
964
+ MAX_TURN_TEXT_LENGTH = 4e3;
965
+ FILE_PATH_TOOLS = /* @__PURE__ */ new Set(["Read", "Edit", "Write", "NotebookEdit"]);
966
+ XCODE_EDIT_TOOL = /XcodeUpdate$/;
967
+ XCODE_PATH_TOOLS = /Xcode(Read|Grep|Glob|RefreshCodeIssuesInFile)$/;
968
+ PATH_TOKEN_RE = /(?:[.~]?\/)?(?:[\w-]+\/)+[\w.-]+/g;
969
+ SYNTHETIC_CONTENT_RE = /^\s*(<task-notification>|<system-reminder>|\[SYSTEM NOTIFICATION)/;
970
+ MAX_TRANSCRIPT_BYTES = 256 * 1024 * 1024;
971
+ }
972
+ });
508
973
 
509
- // ../../packages/ingest-core/src/codex-sessions.ts
510
- import { createHash as createHash2 } from "node:crypto";
511
- import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync as readdirSync2, statSync as statSync2 } from "node:fs";
512
- function deriveUuid2(name) {
513
- const h = createHash2("sha1").update(name).digest("hex");
514
- const variant = (parseInt(h.slice(16, 17) || "0", 16) & 3 | 8).toString(16);
515
- const s = h.slice(0, 12) + "5" + h.slice(13, 16) + variant + h.slice(17, 32);
516
- return `${s.slice(0, 8)}-${s.slice(8, 12)}-${s.slice(12, 16)}-${s.slice(16, 20)}-${s.slice(20, 32)}`;
974
+ // ../../packages/ingest-core/src/cursor-sessions.ts
975
+ import { execFileSync as execFileSync2 } from "node:child_process";
976
+ import { existsSync as existsSync2 } from "node:fs";
977
+ import { homedir as homedir2 } from "node:os";
978
+ import { join as join2, sep } from "node:path";
979
+ function extractPathsFromText2(text) {
980
+ const matches = text.match(PATH_TOKEN_RE2) ?? [];
981
+ return [...new Set(matches)].filter((p) => p.length > 3 && p.length < 300);
517
982
  }
518
- function deriveCodexTurnId(sessionId, ordinal) {
519
- 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");
520
993
  }
521
- function parseLines(raw) {
522
- const out = [];
523
- for (const line of raw.split("\n")) {
524
- 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;
525
1084
  try {
526
- out.push(JSON.parse(line));
1085
+ records[key] = value ? JSON.parse(value) : null;
527
1086
  } catch {
528
- continue;
1087
+ records[key] = value;
529
1088
  }
530
1089
  }
531
- return out;
1090
+ return records;
532
1091
  }
533
- function payloadType(line) {
534
- const t = line.payload?.type;
535
- return typeof t === "string" ? t : void 0;
1092
+ function parseHeaders(headers2) {
1093
+ if (!headers2) return [];
1094
+ try {
1095
+ const parsed = JSON.parse(headers2);
1096
+ return Array.isArray(parsed) ? parsed.filter((h) => h && h.bubbleId) : [];
1097
+ } catch {
1098
+ return [];
1099
+ }
536
1100
  }
537
- function asString(value) {
1101
+ function filesFromTool(toolName, rawArgs) {
1102
+ const files = [];
1103
+ let label = `[tool_call: ${toolName}]`;
1104
+ if (!rawArgs) return { label, files };
1105
+ let args = {};
1106
+ try {
1107
+ args = JSON.parse(rawArgs);
1108
+ } catch {
1109
+ return { label, files };
1110
+ }
1111
+ const filePath = typeof args.target_file === "string" && args.target_file || typeof args.path === "string" && args.path || typeof args.relative_workspace_path === "string" && args.relative_workspace_path || null;
1112
+ const command = typeof args.command === "string" ? args.command : null;
1113
+ if (filePath) {
1114
+ label = `[tool_call: ${toolName}] ${filePath}`;
1115
+ files.push({ path: filePath, source: "tool_path", tool: toolName });
1116
+ } else if (command) {
1117
+ label = `[tool_call: ${toolName}] ${command}`;
1118
+ for (const p of extractPathsFromText2(command)) {
1119
+ files.push({ path: p, source: "tool_bash", tool: toolName });
1120
+ }
1121
+ }
1122
+ return { label, files };
1123
+ }
1124
+ function toIso(value, fallbackMs) {
1125
+ if (typeof value === "string" && value) {
1126
+ const t = Date.parse(value);
1127
+ if (!Number.isNaN(t)) return new Date(t).toISOString();
1128
+ }
1129
+ if (typeof value === "number" && Number.isFinite(value)) {
1130
+ return new Date(value).toISOString();
1131
+ }
1132
+ return new Date(fallbackMs).toISOString();
1133
+ }
1134
+ function parseCursorComposer(dbPath, composer, repoPath, options = { scoped: false }, repoId = deriveRepoId(repoPath)) {
1135
+ const composerId = composer.composerId;
1136
+ if (!composerId) return null;
1137
+ const headers2 = parseHeaders(composer.headers);
1138
+ if (headers2.length === 0) return null;
1139
+ const bubbles = loadBubbles(dbPath, composerId);
1140
+ const composerCreatedMs = typeof composer.createdAt === "number" ? composer.createdAt : Date.now();
1141
+ const referencing = options.scoped ? bubbleIdsReferencingRepo(dbPath, composerId, repoPath) : null;
1142
+ const built = [];
1143
+ for (const header of headers2) {
1144
+ const bubble = bubbles.get(header.bubbleId);
1145
+ if (!bubble) continue;
1146
+ const role = header.type === 1 ? "user" : "assistant";
1147
+ const textParts = [];
1148
+ const filesTouched = [];
1149
+ if (bubble.text && bubble.text.trim()) {
1150
+ textParts.push(bubble.text);
1151
+ for (const p of extractPathsFromText2(bubble.text)) {
1152
+ filesTouched.push({ path: p, source: "prose" });
1153
+ }
1154
+ }
1155
+ if (role === "assistant" && bubble.thinking && bubble.thinking.trim()) {
1156
+ textParts.push(bubble.thinking);
1157
+ }
1158
+ if (bubble.toolName) {
1159
+ const { label, files } = filesFromTool(bubble.toolName, bubble.rawArgs);
1160
+ textParts.push(label);
1161
+ filesTouched.push(...files);
1162
+ }
1163
+ const text = textParts.join("\n");
1164
+ if (!text.trim() && filesTouched.length === 0) continue;
1165
+ const redacted = redactSecrets(text);
1166
+ built.push({
1167
+ bubbleId: header.bubbleId,
1168
+ role,
1169
+ referencesRepo: referencing ? referencing.has(header.bubbleId) : true,
1170
+ redactionCount: redacted.count,
1171
+ turn: {
1172
+ id: header.bubbleId,
1173
+ // rewritten to the per-repo derived id below
1174
+ sessionId: composerId,
1175
+ // rewritten to the per-repo derived id below
1176
+ role,
1177
+ ts: toIso(bubble.createdAt, composerCreatedMs),
1178
+ text: redacted.text.slice(0, MAX_TURN_TEXT_LENGTH2),
1179
+ filesTouched,
1180
+ // Cursor's bubble format doesn't expose a structuredPatch-equivalent
1181
+ // the way Claude Code's toolUseResult does — content-overlap scoring
1182
+ // (see scoreContentOverlap in the linker) simply has no signal here
1183
+ // yet, same tier as any other not-yet-supported evidence source.
1184
+ editedLines: [],
1185
+ // Cursor has no parent-pointer chain; header order is authoritative, so
1186
+ // parentUuid is left null (the backend/linker never require it).
1187
+ parentUuid: null,
1188
+ isSidechain: false,
1189
+ redacted: redacted.count > 0,
1190
+ // Cursor's "user" bubbles (header.type === 1, see `role` above) are a
1191
+ // structural field distinct from tool output — not the Claude Code
1192
+ // wire-format ambiguity where a tool result also arrives as a
1193
+ // `role: "user"` record. No synthetic-input misattribution risk here.
1194
+ isSyntheticInput: false,
1195
+ isToolError: false,
1196
+ // Neither source reports what a turn cost, so it is unknown rather than free.
1197
+ usage: { ...EMPTY_USAGE }
1198
+ }
1199
+ });
1200
+ }
1201
+ let kept;
1202
+ if (referencing) {
1203
+ const keep2 = /* @__PURE__ */ new Set();
1204
+ for (let i = 0; i < built.length; i++) {
1205
+ const cur = built[i];
1206
+ if (!cur || !cur.referencesRepo) continue;
1207
+ keep2.add(i);
1208
+ const prev = built[i - 1];
1209
+ if (prev && prev.role === "user") keep2.add(i - 1);
1210
+ }
1211
+ kept = built.filter((_, i) => keep2.has(i));
1212
+ } else {
1213
+ kept = built;
1214
+ }
1215
+ if (kept.length === 0) return null;
1216
+ const sessionId = deriveCursorSessionId(composerId, repoPath);
1217
+ const turns = kept.map((b) => ({
1218
+ ...b.turn,
1219
+ id: deriveCursorTurnId(b.bubbleId, repoPath),
1220
+ sessionId
1221
+ }));
1222
+ const totalRedactions = kept.reduce((sum, b) => sum + b.redactionCount, 0);
1223
+ const sortedTs = turns.map((t) => t.ts).sort();
1224
+ if (totalRedactions > 0) {
1225
+ console.log(`[ingest-core] redacted ${totalRedactions} potential secret(s) in cursor session ${composerId}`);
1226
+ }
1227
+ const rawRecords = loadRawRecords(dbPath, composerId, kept.map((b) => b.bubbleId));
1228
+ const rawContent = JSON.stringify(
1229
+ redactJsonValue({ format: "cursor-composer", composerId, records: rawRecords }).value
1230
+ );
1231
+ return {
1232
+ id: sessionId,
1233
+ agentKind: "cursor",
1234
+ repoId,
1235
+ cwd: repoPath,
1236
+ startedAt: sortedTs[0] ?? null,
1237
+ endedAt: sortedTs[sortedTs.length - 1] ?? null,
1238
+ turnCount: turns.length,
1239
+ aiTitle: composer.name && composer.name.trim() ? composer.name : null,
1240
+ author: null,
1241
+ // filled in by the caller — see collectRepoData
1242
+ // Includes repoPath so a shared composer's per-repo copies have distinct,
1243
+ // traceable source refs (the raw composerId alone is no longer unique).
1244
+ sourceFile: `${dbPath}#${composerId}#${repoPath}`,
1245
+ redactionCount: totalRedactions,
1246
+ // Read back off the turns rather than the raw records: unlike Claude Code,
1247
+ // a Cursor tool call's label keeps the command itself, so the invocation is
1248
+ // still there to read.
1249
+ committedSubjects: [
1250
+ ...new Set(turns.flatMap((t) => committedSubjects(t.text)))
1251
+ ],
1252
+ rawContent,
1253
+ branch: null,
1254
+ parentSessionId: null,
1255
+ subagent: null,
1256
+ rawFormat: "json",
1257
+ turns
1258
+ };
1259
+ }
1260
+ function workspaceMatchesRepo(workspacePath, repoPath) {
1261
+ if (!workspacePath) return false;
1262
+ return workspacePath === repoPath || workspacePath.startsWith(`${repoPath}${sep}`);
1263
+ }
1264
+ function parseCursorConversation(composerId, repoPath, repoId, dbPath = cursorStateDbPath()) {
1265
+ if (!existsSync2(dbPath)) return null;
1266
+ const composer = loadComposers(dbPath).find((c) => c.composerId === composerId);
1267
+ if (!composer) return null;
1268
+ return parseCursorComposer(dbPath, composer, repoPath, { scoped: false }, repoId);
1269
+ }
1270
+ function parseAllCursorSessions(repoPath, repoId) {
1271
+ const dbPath = cursorStateDbPath();
1272
+ if (!existsSync2(dbPath)) return [];
1273
+ const composers = loadComposers(dbPath);
1274
+ if (composers.length === 0) return [];
1275
+ const byId = /* @__PURE__ */ new Map();
1276
+ for (const c of composers) {
1277
+ if (c.composerId) byId.set(c.composerId, c);
1278
+ }
1279
+ const workspaceDedicated = /* @__PURE__ */ new Set();
1280
+ for (const c of composers) {
1281
+ if (c.composerId && workspaceMatchesRepo(c.workspacePath, repoPath)) {
1282
+ workspaceDedicated.add(c.composerId);
1283
+ }
1284
+ }
1285
+ const attributed = new Set(workspaceDedicated);
1286
+ for (const id of composerIdsReferencingRepo(dbPath, repoPath)) {
1287
+ attributed.add(id);
1288
+ }
1289
+ const resolvedRepoId = repoId ?? deriveRepoId(repoPath);
1290
+ const sessions = [];
1291
+ for (const id of attributed) {
1292
+ const composer = byId.get(id);
1293
+ if (!composer) continue;
1294
+ const scoped = !workspaceDedicated.has(id);
1295
+ const parsed = parseCursorComposer(dbPath, composer, repoPath, { scoped }, resolvedRepoId);
1296
+ if (parsed) sessions.push(parsed);
1297
+ }
1298
+ return sessions;
1299
+ }
1300
+ var MAX_TURN_TEXT_LENGTH2, PATH_TOKEN_RE2;
1301
+ var init_cursor_sessions = __esm({
1302
+ "../../packages/ingest-core/src/cursor-sessions.ts"() {
1303
+ "use strict";
1304
+ init_git_history();
1305
+ init_redact();
1306
+ init_derive_uuid();
1307
+ init_subject_linking();
1308
+ init_types();
1309
+ MAX_TURN_TEXT_LENGTH2 = 4e3;
1310
+ PATH_TOKEN_RE2 = /(?:[.~]?\/)?(?:[\w-]+\/)+[\w.-]+/g;
1311
+ }
1312
+ });
1313
+
1314
+ // ../../packages/ingest-core/src/opencode-sessions.ts
1315
+ import { existsSync as existsSync3 } from "node:fs";
1316
+ import { homedir as homedir3 } from "node:os";
1317
+ import { join as join3 } from "node:path";
1318
+ function opencodeDbPath(env = process.env, home = homedir3()) {
1319
+ const data = env.XDG_DATA_HOME || join3(home, ".local", "share");
1320
+ return join3(data, "opencode", "opencode.db");
1321
+ }
1322
+ function q(value) {
1323
+ return `'${value.replace(/'/g, "''")}'`;
1324
+ }
1325
+ function parseJson(raw) {
1326
+ try {
1327
+ return JSON.parse(raw);
1328
+ } catch {
1329
+ return null;
1330
+ }
1331
+ }
1332
+ function deriveOpenCodeSessionId(sessionId) {
1333
+ return deriveUuid(`evrex-opencode\0${sessionId}`);
1334
+ }
1335
+ function deriveTurnId(partOrMessageId, suffix = "") {
1336
+ return deriveUuid(`evrex-opencode-turn\0${partOrMessageId}${suffix}`);
1337
+ }
1338
+ function filesFromTool2(tool, input) {
1339
+ const out = [];
1340
+ for (const key of ["filePath", "path", "file"]) {
1341
+ const v = input[key];
1342
+ if (typeof v === "string" && v.length > 0) out.push({ path: v, source: "tool_path", tool });
1343
+ }
1344
+ const command = input.command;
1345
+ if (tool === "bash" && typeof command === "string") {
1346
+ for (const p of extractPathsFromText(command)) out.push({ path: p, source: "tool_bash", tool });
1347
+ }
1348
+ return out;
1349
+ }
1350
+ function usageOf(message) {
1351
+ const t = message.tokens;
1352
+ if (!t) return { ...EMPTY_USAGE, model: message.modelID ?? null };
1353
+ return {
1354
+ inputTokens: t.input ?? null,
1355
+ outputTokens: t.output ?? null,
1356
+ cacheReadTokens: t.cache?.read ?? null,
1357
+ cacheWriteTokens: t.cache?.write ?? null,
1358
+ model: message.modelID ?? null
1359
+ };
1360
+ }
1361
+ function buildOpenCodeSession(session, messages, parts, repoId, sourceFile) {
1362
+ const id = deriveOpenCodeSessionId(session.id);
1363
+ const partsByMessage = /* @__PURE__ */ new Map();
1364
+ for (const p of parts) {
1365
+ const list = partsByMessage.get(p.message_id) ?? [];
1366
+ list.push(p);
1367
+ partsByMessage.set(p.message_id, list);
1368
+ }
1369
+ const turns = [];
1370
+ let redactionCount = 0;
1371
+ let previous = null;
1372
+ const subjects = [];
1373
+ const push = (turn) => {
1374
+ const redacted = redactSecrets(turn.text);
1375
+ redactionCount += redacted.count;
1376
+ turns.push({
1377
+ ...turn,
1378
+ text: redacted.text,
1379
+ sessionId: id,
1380
+ parentUuid: previous,
1381
+ isSidechain: false,
1382
+ redacted: redacted.count > 0,
1383
+ editedLines: []
1384
+ });
1385
+ previous = turn.id;
1386
+ };
1387
+ const at = (ms) => new Date(ms).toISOString();
1388
+ for (const m of [...messages].sort((a, b) => a.time_created - b.time_created)) {
1389
+ const data = parseJson(m.data);
1390
+ if (!data?.role) continue;
1391
+ const mparts = (partsByMessage.get(m.id) ?? []).sort((a, b) => a.time_created - b.time_created);
1392
+ if (data.role === "user") {
1393
+ const text = mparts.map((p) => parseJson(p.data)).filter((p) => !!p && p.type === "text" && typeof p.text === "string").map((p) => p.text).join("\n").trim();
1394
+ if (!text) continue;
1395
+ push({
1396
+ id: deriveTurnId(m.id),
1397
+ role: "user",
1398
+ ts: at(data.time?.created ?? m.time_created),
1399
+ text,
1400
+ filesTouched: extractPathsFromText(text).map((path) => ({ path, source: "prose" })),
1401
+ usage: EMPTY_USAGE,
1402
+ isSyntheticInput: false,
1403
+ isToolError: false
1404
+ });
1405
+ continue;
1406
+ }
1407
+ const usage = usageOf(data);
1408
+ let usageGiven = false;
1409
+ const take = () => {
1410
+ if (usageGiven) return EMPTY_USAGE;
1411
+ usageGiven = true;
1412
+ return usage;
1413
+ };
1414
+ for (const p of mparts) {
1415
+ const part = parseJson(p.data);
1416
+ if (!part) continue;
1417
+ if (part.type === "text" && typeof part.text === "string" && part.text.trim()) {
1418
+ push({
1419
+ id: deriveTurnId(p.id),
1420
+ role: "assistant",
1421
+ ts: at(p.time_created),
1422
+ text: part.text.trim(),
1423
+ filesTouched: [],
1424
+ usage: take(),
1425
+ isSyntheticInput: false,
1426
+ isToolError: false
1427
+ });
1428
+ } else if (part.type === "tool" && typeof part.tool === "string") {
1429
+ const input = part.state?.input ?? {};
1430
+ const args = JSON.stringify(input);
1431
+ const files = filesFromTool2(part.tool, input);
1432
+ if (part.tool === "bash" && typeof input.command === "string") {
1433
+ subjects.push(...committedSubjects(input.command));
1434
+ }
1435
+ push({
1436
+ id: deriveTurnId(p.id, ":call"),
1437
+ role: "assistant",
1438
+ ts: at(p.time_created),
1439
+ text: `[tool: ${part.tool}] ${args.length > 600 ? `${args.slice(0, 600)}\u2026` : args}`,
1440
+ filesTouched: files,
1441
+ usage: take(),
1442
+ isSyntheticInput: false,
1443
+ isToolError: false
1444
+ });
1445
+ const failed = part.state?.status === "error";
1446
+ const output = failed ? part.state?.error ?? part.state?.output ?? "" : part.state?.output ?? "";
1447
+ push({
1448
+ id: deriveTurnId(p.id, ":result"),
1449
+ role: "user",
1450
+ ts: at(p.time_created),
1451
+ text: output.length > MAX_OUTPUT_CHARS ? `${output.slice(0, MAX_OUTPUT_CHARS)}\u2026` : output,
1452
+ filesTouched: [],
1453
+ usage: EMPTY_USAGE,
1454
+ isSyntheticInput: true,
1455
+ isToolError: failed
1456
+ });
1457
+ }
1458
+ }
1459
+ }
1460
+ if (turns.length === 0) return null;
1461
+ return {
1462
+ id,
1463
+ agentKind: "opencode",
1464
+ repoId,
1465
+ cwd: session.directory,
1466
+ startedAt: new Date(session.time_created).toISOString(),
1467
+ endedAt: new Date(session.time_updated).toISOString(),
1468
+ turnCount: turns.length,
1469
+ aiTitle: session.title || null,
1470
+ author: null,
1471
+ sourceFile,
1472
+ redactionCount,
1473
+ committedSubjects: [...new Set(subjects)],
1474
+ parentSessionId: session.parent_id ? deriveOpenCodeSessionId(session.parent_id) : null,
1475
+ subagent: session.parent_id ? { agentId: session.id, agentType: session.agent ?? null, description: session.title || null } : null,
1476
+ branch: null,
1477
+ // The archive is the rows themselves, so a reader later has what the
1478
+ // store had — minus the reasoning parts, which OpenCode itself hides.
1479
+ rawContent: JSON.stringify({
1480
+ session,
1481
+ messages,
1482
+ parts: parts.filter((p) => !p.data.includes('"type":"reasoning"'))
1483
+ }),
1484
+ rawFormat: "json",
1485
+ turns
1486
+ };
1487
+ }
1488
+ function parseAllOpenCodeSessions(repoPath, repoId = deriveRepoId(repoPath), dbPath = opencodeDbPath()) {
1489
+ if (!existsSync3(dbPath)) return [];
1490
+ const sessions = sqliteJson(
1491
+ dbPath,
1492
+ `SELECT s.id, s.parent_id, s.directory, s.title, s.agent, s.model, s.time_created, s.time_updated, p.worktree
1493
+ FROM session s LEFT JOIN project p ON p.id = s.project_id
1494
+ WHERE s.time_archived IS NULL`
1495
+ ).filter((s) => workspaceMatchesRepo(s.directory, repoPath) || workspaceMatchesRepo(s.worktree, repoPath));
1496
+ const out = [];
1497
+ for (const s of sessions) {
1498
+ const messages = sqliteJson(
1499
+ dbPath,
1500
+ `SELECT id, session_id, time_created, data FROM message WHERE session_id = ${q(s.id)} ORDER BY time_created, id`
1501
+ );
1502
+ const parts = sqliteJson(
1503
+ dbPath,
1504
+ `SELECT id, message_id, time_created, data FROM part WHERE session_id = ${q(s.id)} ORDER BY time_created, id`
1505
+ );
1506
+ const parsed = buildOpenCodeSession(s, messages, parts, repoId, `${dbPath}#${s.id}`);
1507
+ if (parsed) out.push(parsed);
1508
+ }
1509
+ return out;
1510
+ }
1511
+ var MAX_OUTPUT_CHARS;
1512
+ var init_opencode_sessions = __esm({
1513
+ "../../packages/ingest-core/src/opencode-sessions.ts"() {
1514
+ "use strict";
1515
+ init_claude_sessions();
1516
+ init_cursor_sessions();
1517
+ init_derive_uuid();
1518
+ init_git_history();
1519
+ init_redact();
1520
+ init_subject_linking();
1521
+ init_types();
1522
+ MAX_OUTPUT_CHARS = 2e3;
1523
+ }
1524
+ });
1525
+
1526
+ // ../../packages/ingest-core/src/copilot-sessions.ts
1527
+ import { existsSync as existsSync4, readFileSync as readFileSync2, readdirSync as readdirSync2, statSync as statSync3 } from "node:fs";
1528
+ import { homedir as homedir4 } from "node:os";
1529
+ import { join as join4 } from "node:path";
1530
+ function copilotSessionsDir(home = homedir4()) {
1531
+ return join4(home, ".copilot", "session-state");
1532
+ }
1533
+ function parseCopilotWorkspace(text) {
1534
+ const map = /* @__PURE__ */ new Map();
1535
+ for (const line of text.split("\n")) {
1536
+ const m = /^([a-z_]+):\s*(.*)$/.exec(line);
1537
+ if (!m) continue;
1538
+ let value = m[2].trim();
1539
+ if (value.length >= 2 && value.startsWith("'") && value.endsWith("'")) {
1540
+ value = value.slice(1, -1).replace(/''/g, "'");
1541
+ } else if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
1542
+ try {
1543
+ value = JSON.parse(value);
1544
+ } catch {
1545
+ value = value.slice(1, -1);
1546
+ }
1547
+ }
1548
+ map.set(m[1], value);
1549
+ }
1550
+ const get2 = (k) => {
1551
+ const v = map.get(k);
1552
+ return v === void 0 || v === "" || v === "null" ? null : v;
1553
+ };
1554
+ return {
1555
+ id: get2("id"),
1556
+ cwd: get2("cwd"),
1557
+ gitRoot: get2("git_root"),
1558
+ branch: get2("branch"),
1559
+ name: get2("name"),
1560
+ createdAt: get2("created_at"),
1561
+ updatedAt: get2("updated_at")
1562
+ };
1563
+ }
1564
+ function findCopilotSessionDirs(root = copilotSessionsDir()) {
1565
+ if (!existsSync4(root)) return [];
1566
+ return readdirSync2(root).map((name) => join4(root, name)).filter((dir) => {
1567
+ try {
1568
+ return statSync3(dir).isDirectory() && existsSync4(join4(dir, "events.jsonl"));
1569
+ } catch {
1570
+ return false;
1571
+ }
1572
+ }).sort((a, b) => statSync3(b).mtimeMs - statSync3(a).mtimeMs);
1573
+ }
1574
+ function deriveTurnId2(key) {
1575
+ return deriveUuid(`evrex-copilot-turn ${key}`);
1576
+ }
1577
+ function filesFromTool3(tool, args) {
1578
+ const out = [];
1579
+ for (const key of ["path", "filePath", "file"]) {
1580
+ const v = args[key];
1581
+ if (typeof v === "string" && v.length > 0) out.push({ path: v, source: "tool_path", tool });
1582
+ }
1583
+ if (tool === "bash" && typeof args.command === "string") {
1584
+ for (const p of extractPathsFromText(args.command)) out.push({ path: p, source: "tool_bash", tool });
1585
+ }
1586
+ return out;
1587
+ }
1588
+ function shutdownUsage(data, model) {
1589
+ const details = data.tokenDetails;
1590
+ if (!details) return { ...EMPTY_USAGE, model };
1591
+ const n = (k) => typeof details[k]?.tokenCount === "number" ? details[k].tokenCount : null;
1592
+ return {
1593
+ inputTokens: n("input"),
1594
+ outputTokens: n("output"),
1595
+ cacheReadTokens: n("cache_read"),
1596
+ cacheWriteTokens: n("cache_write"),
1597
+ model
1598
+ };
1599
+ }
1600
+ function buildCopilotSession(eventsText, workspace, repoId, sourceFile) {
1601
+ const events = [];
1602
+ const archived = [];
1603
+ for (const line of eventsText.split("\n")) {
1604
+ if (!line.trim()) continue;
1605
+ let e;
1606
+ try {
1607
+ e = JSON.parse(line);
1608
+ } catch {
1609
+ continue;
1610
+ }
1611
+ events.push(e);
1612
+ if (e.data && Object.keys(e.data).some((k) => REASONING_KEYS.has(k))) {
1613
+ const data = Object.fromEntries(Object.entries(e.data).filter(([k]) => !REASONING_KEYS.has(k)));
1614
+ archived.push(JSON.stringify({ ...e, data }));
1615
+ } else {
1616
+ archived.push(line);
1617
+ }
1618
+ }
1619
+ const start = events.find((e) => e.type === "session.start");
1620
+ const startData = start?.data ?? {};
1621
+ const sessionId = workspace.id ?? startData.sessionId ?? null;
1622
+ if (!sessionId) return null;
1623
+ const cwd = workspace.cwd ?? startData.context?.cwd ?? null;
1624
+ if (!cwd) return null;
1625
+ const turns = [];
1626
+ let redactionCount = 0;
1627
+ let previous = null;
1628
+ const subjects = [];
1629
+ let lastModel = null;
1630
+ const push = (turn) => {
1631
+ const redacted = redactSecrets(turn.text);
1632
+ redactionCount += redacted.count;
1633
+ turns.push({
1634
+ ...turn,
1635
+ text: redacted.text,
1636
+ sessionId,
1637
+ parentUuid: previous,
1638
+ isSidechain: false,
1639
+ redacted: redacted.count > 0,
1640
+ editedLines: []
1641
+ });
1642
+ previous = turn.id;
1643
+ };
1644
+ const toolNames = /* @__PURE__ */ new Map();
1645
+ for (const e of events) {
1646
+ const d = e.data ?? {};
1647
+ const ts = e.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
1648
+ const id = e.id ?? deriveTurnId2(`${sessionId}:${turns.length}`);
1649
+ switch (e.type) {
1650
+ case "user.message": {
1651
+ const text = typeof d.content === "string" ? d.content.trim() : "";
1652
+ if (!text) break;
1653
+ push({
1654
+ id,
1655
+ role: "user",
1656
+ ts,
1657
+ text,
1658
+ filesTouched: extractPathsFromText(text).map((path) => ({ path, source: "prose" })),
1659
+ usage: EMPTY_USAGE,
1660
+ isSyntheticInput: false,
1661
+ isToolError: false
1662
+ });
1663
+ break;
1664
+ }
1665
+ case "assistant.message": {
1666
+ if (typeof d.model === "string") lastModel = d.model;
1667
+ const text = typeof d.content === "string" ? d.content.trim() : "";
1668
+ if (!text) break;
1669
+ push({ id, role: "assistant", ts, text, filesTouched: [], usage: EMPTY_USAGE, isSyntheticInput: false, isToolError: false });
1670
+ break;
1671
+ }
1672
+ case "tool.execution_start": {
1673
+ const tool = typeof d.toolName === "string" ? d.toolName : "tool";
1674
+ const callId = typeof d.toolCallId === "string" ? d.toolCallId : id;
1675
+ const args = d.arguments && typeof d.arguments === "object" ? d.arguments : {};
1676
+ toolNames.set(callId, tool);
1677
+ if (tool === "bash" && typeof args.command === "string") subjects.push(...committedSubjects(args.command));
1678
+ const rendered = JSON.stringify(args);
1679
+ push({
1680
+ id: deriveTurnId2(`${callId}:call`),
1681
+ role: "assistant",
1682
+ ts,
1683
+ text: `[tool: ${tool}] ${rendered.length > 600 ? `${rendered.slice(0, 600)}\u2026` : rendered}`,
1684
+ filesTouched: filesFromTool3(tool, args),
1685
+ usage: EMPTY_USAGE,
1686
+ isSyntheticInput: false,
1687
+ isToolError: false
1688
+ });
1689
+ break;
1690
+ }
1691
+ case "tool.execution_complete": {
1692
+ const callId = typeof d.toolCallId === "string" ? d.toolCallId : id;
1693
+ const failed = d.success === false;
1694
+ const result = d.result ?? {};
1695
+ const error = d.error ?? {};
1696
+ const output = (failed ? error.message ?? result.content ?? "Tool call failed" : result.content ?? "").trim();
1697
+ push({
1698
+ id: deriveTurnId2(`${callId}:result`),
1699
+ role: "user",
1700
+ ts,
1701
+ text: output.length > MAX_OUTPUT_CHARS2 ? `${output.slice(0, MAX_OUTPUT_CHARS2)}\u2026` : output,
1702
+ filesTouched: [],
1703
+ usage: EMPTY_USAGE,
1704
+ isSyntheticInput: true,
1705
+ isToolError: failed
1706
+ });
1707
+ break;
1708
+ }
1709
+ case "session.shutdown": {
1710
+ const usage = shutdownUsage(d, typeof d.currentModel === "string" ? d.currentModel : lastModel);
1711
+ for (let i = turns.length - 1; i >= 0; i--) {
1712
+ if (turns[i].role === "assistant") {
1713
+ turns[i].usage = usage;
1714
+ break;
1715
+ }
1716
+ }
1717
+ break;
1718
+ }
1719
+ default:
1720
+ break;
1721
+ }
1722
+ }
1723
+ if (turns.length === 0) return null;
1724
+ const first = turns[0].ts;
1725
+ const last = turns[turns.length - 1].ts;
1726
+ return {
1727
+ id: sessionId,
1728
+ agentKind: "copilot",
1729
+ repoId,
1730
+ cwd,
1731
+ startedAt: workspace.createdAt ?? startData.startTime ?? first,
1732
+ endedAt: events[events.length - 1]?.timestamp ?? workspace.updatedAt ?? last,
1733
+ turnCount: turns.length,
1734
+ aiTitle: workspace.name,
1735
+ author: null,
1736
+ sourceFile,
1737
+ redactionCount,
1738
+ committedSubjects: [...new Set(subjects)],
1739
+ parentSessionId: null,
1740
+ subagent: null,
1741
+ branch: workspace.branch,
1742
+ rawContent: archived.join("\n"),
1743
+ rawFormat: "jsonl",
1744
+ turns
1745
+ };
1746
+ }
1747
+ function parseCopilotSessionDir(dir, repoId) {
1748
+ const eventsPath = join4(dir, "events.jsonl");
1749
+ if (!existsSync4(eventsPath)) return null;
1750
+ const workspacePath = join4(dir, "workspace.yaml");
1751
+ const workspace = existsSync4(workspacePath) ? parseCopilotWorkspace(readFileSync2(workspacePath, "utf-8")) : parseCopilotWorkspace("");
1752
+ return buildCopilotSession(readFileSync2(eventsPath, "utf-8"), workspace, repoId, eventsPath);
1753
+ }
1754
+ function parseAllCopilotSessions(repoPath, repoId = deriveRepoId(repoPath), root = copilotSessionsDir()) {
1755
+ const out = [];
1756
+ for (const dir of findCopilotSessionDirs(root)) {
1757
+ const workspacePath = join4(dir, "workspace.yaml");
1758
+ const workspace = existsSync4(workspacePath) ? parseCopilotWorkspace(readFileSync2(workspacePath, "utf-8")) : parseCopilotWorkspace("");
1759
+ const known = workspace.gitRoot ?? workspace.cwd;
1760
+ if (known && !workspaceMatchesRepo(known, repoPath)) continue;
1761
+ const parsed = buildCopilotSession(readFileSync2(join4(dir, "events.jsonl"), "utf-8"), workspace, repoId, join4(dir, "events.jsonl"));
1762
+ if (!parsed) continue;
1763
+ if (!known && !workspaceMatchesRepo(parsed.cwd, repoPath)) continue;
1764
+ out.push(parsed);
1765
+ }
1766
+ return out;
1767
+ }
1768
+ var MAX_OUTPUT_CHARS2, REASONING_KEYS;
1769
+ var init_copilot_sessions = __esm({
1770
+ "../../packages/ingest-core/src/copilot-sessions.ts"() {
1771
+ "use strict";
1772
+ init_claude_sessions();
1773
+ init_cursor_sessions();
1774
+ init_derive_uuid();
1775
+ init_git_history();
1776
+ init_redact();
1777
+ init_subject_linking();
1778
+ init_types();
1779
+ MAX_OUTPUT_CHARS2 = 2e3;
1780
+ REASONING_KEYS = /* @__PURE__ */ new Set(["reasoningOpaque", "reasoningText", "reasoningBlocks"]);
1781
+ }
1782
+ });
1783
+
1784
+ // ../../packages/ingest-core/src/codex-sessions.ts
1785
+ import { createHash as createHash2 } from "node:crypto";
1786
+ import { existsSync as existsSync5, readFileSync as readFileSync3, readdirSync as readdirSync3, statSync as statSync4 } from "node:fs";
1787
+ import { homedir as homedir5 } from "node:os";
1788
+ import { join as join5 } from "node:path";
1789
+ function codexSessionsDir() {
1790
+ return join5(homedir5(), CODEX_DIR, "sessions");
1791
+ }
1792
+ function deriveUuid2(name) {
1793
+ const h = createHash2("sha1").update(name).digest("hex");
1794
+ const variant = (parseInt(h.slice(16, 17) || "0", 16) & 3 | 8).toString(16);
1795
+ const s = h.slice(0, 12) + "5" + h.slice(13, 16) + variant + h.slice(17, 32);
1796
+ return `${s.slice(0, 8)}-${s.slice(8, 12)}-${s.slice(12, 16)}-${s.slice(16, 20)}-${s.slice(20, 32)}`;
1797
+ }
1798
+ function deriveCodexTurnId(sessionId, ordinal) {
1799
+ return deriveUuid2(`evrex-codex-turn\0${sessionId}\0${ordinal}`);
1800
+ }
1801
+ function findCodexSessionFiles(root = codexSessionsDir()) {
1802
+ const found = [];
1803
+ const walk = (dir) => {
1804
+ let entries;
1805
+ try {
1806
+ entries = readdirSync3(dir);
1807
+ } catch {
1808
+ return;
1809
+ }
1810
+ for (const entry of entries) {
1811
+ const full = join5(dir, entry);
1812
+ let isDir = false;
1813
+ try {
1814
+ isDir = statSync4(full).isDirectory();
1815
+ } catch {
1816
+ continue;
1817
+ }
1818
+ if (isDir) walk(full);
1819
+ else if (entry.endsWith(".jsonl")) found.push(full);
1820
+ }
1821
+ };
1822
+ if (existsSync5(root)) walk(root);
1823
+ return found.sort();
1824
+ }
1825
+ function parseLines(raw) {
1826
+ const out = [];
1827
+ for (const line of raw.split("\n")) {
1828
+ if (!line.trim()) continue;
1829
+ try {
1830
+ out.push(JSON.parse(line));
1831
+ } catch {
1832
+ continue;
1833
+ }
1834
+ }
1835
+ return out;
1836
+ }
1837
+ function payloadType(line) {
1838
+ const t = line.payload?.type;
1839
+ return typeof t === "string" ? t : void 0;
1840
+ }
1841
+ function asString(value) {
538
1842
  return typeof value === "string" && value.length > 0 ? value : null;
539
1843
  }
540
1844
  function conversationEvents(lines) {
@@ -620,7 +1924,7 @@ function filesFromToolCalls(lines) {
620
1924
  function parseCodexSessionFile(filePath, repoPath, repoId) {
621
1925
  let raw;
622
1926
  try {
623
- raw = readFileSync2(filePath, "utf-8");
1927
+ raw = readFileSync3(filePath, "utf-8");
624
1928
  } catch {
625
1929
  return null;
626
1930
  }
@@ -653,6 +1957,7 @@ function parseCodexSessionFile(filePath, repoPath, repoId) {
653
1957
  isSidechain: false,
654
1958
  redacted: count > 0,
655
1959
  isSyntheticInput: false,
1960
+ isToolError: false,
656
1961
  usage: event.usage
657
1962
  };
658
1963
  });
@@ -662,6 +1967,9 @@ function parseCodexSessionFile(filePath, repoPath, repoId) {
662
1967
  return {
663
1968
  id: sessionId,
664
1969
  agentKind: "codex",
1970
+ branch: null,
1971
+ parentSessionId: null,
1972
+ subagent: null,
665
1973
  // Codex records the remote itself, so identity survives a moved or deleted
666
1974
  // checkout. Falls back to the caller's derivation when it is absent.
667
1975
  repoId: repoIdFromMeta(meta) ?? repoId,
@@ -680,9 +1988,9 @@ function parseCodexSessionFile(filePath, repoPath, repoId) {
680
1988
  };
681
1989
  }
682
1990
  function repoIdFromMeta(meta) {
683
- const git = meta.git;
684
- if (typeof git !== "object" || git === null) return null;
685
- 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);
686
1994
  if (!url) return null;
687
1995
  const normalized = normalizeRepoRemote(url);
688
1996
  return normalized ? `remote:${normalized}` : null;
@@ -697,13 +2005,494 @@ function redactRollout(raw) {
697
2005
  });
698
2006
  return { content: lines.join("\n"), count };
699
2007
  }
2008
+ function parseAllCodexSessions(repoPath, repoId) {
2009
+ const sessions = [];
2010
+ for (const file of findCodexSessionFiles()) {
2011
+ const parsed = parseCodexSessionFile(file, repoPath, repoId);
2012
+ if (parsed) sessions.push(parsed);
2013
+ }
2014
+ return sessions;
2015
+ }
2016
+ var CODEX_DIR;
2017
+ var init_codex_sessions = __esm({
2018
+ "../../packages/ingest-core/src/codex-sessions.ts"() {
2019
+ "use strict";
2020
+ init_claude_sessions();
2021
+ init_git_history();
2022
+ init_redact();
2023
+ init_subject_linking();
2024
+ init_types();
2025
+ CODEX_DIR = ".codex";
2026
+ }
2027
+ });
700
2028
 
701
2029
  // ../../packages/ingest-core/src/sanitize.ts
702
- 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
+ });
703
2493
 
704
2494
  // ../../packages/ingest-core/src/gemini-sessions.ts
705
- import { readFileSync as readFileSync3 } from "node:fs";
706
- var SYNTHETIC = /^\s*<session_context>/;
2495
+ import { readFileSync as readFileSync4 } from "node:fs";
707
2496
  function textOf(content) {
708
2497
  if (typeof content === "string") return content;
709
2498
  if (!Array.isArray(content)) return "";
@@ -714,7 +2503,7 @@ function textOf(content) {
714
2503
  function parseGeminiSessionFile(filePath, repoPath, repoId) {
715
2504
  let raw;
716
2505
  try {
717
- raw = readFileSync3(filePath, "utf-8");
2506
+ raw = readFileSync4(filePath, "utf-8");
718
2507
  } catch {
719
2508
  return null;
720
2509
  }
@@ -760,6 +2549,7 @@ function parseGeminiSessionFile(filePath, repoPath, repoId) {
760
2549
  // as a user message. Attributing that to the person is the same bug this
761
2550
  // repo already fixed once for Claude Code's tool results.
762
2551
  isSyntheticInput: role === "user" && SYNTHETIC.test(text),
2552
+ isToolError: false,
763
2553
  // Gemini records a model per message but no token counts in the
764
2554
  // transcript; unknown rather than zero.
765
2555
  usage: { ...EMPTY_USAGE, model: record.model ?? null }
@@ -783,12 +2573,24 @@ function parseGeminiSessionFile(filePath, repoPath, repoId) {
783
2573
  redactionCount: redactionCount + rawRedactions,
784
2574
  turns,
785
2575
  rawContent,
2576
+ branch: null,
2577
+ parentSessionId: null,
2578
+ subagent: null,
786
2579
  rawFormat: "jsonl"
787
2580
  };
788
2581
  }
2582
+ var SYNTHETIC;
2583
+ var init_gemini_sessions = __esm({
2584
+ "../../packages/ingest-core/src/gemini-sessions.ts"() {
2585
+ "use strict";
2586
+ init_derive_uuid();
2587
+ init_redact();
2588
+ init_types();
2589
+ SYNTHETIC = /^\s*<session_context>/;
2590
+ }
2591
+ });
789
2592
 
790
2593
  // ../../packages/ingest-core/src/transcript-parsers.ts
791
- var PARSERS = [parseSessionFile, parseCodexSessionFile, parseGeminiSessionFile];
792
2594
  function parseTranscriptFile(path, repoPath, repoId) {
793
2595
  for (const parse of PARSERS) {
794
2596
  const parsed = parse(path, repoPath, repoId);
@@ -796,21 +2598,598 @@ function parseTranscriptFile(path, repoPath, repoId) {
796
2598
  }
797
2599
  return null;
798
2600
  }
2601
+ var PARSERS;
2602
+ var init_transcript_parsers = __esm({
2603
+ "../../packages/ingest-core/src/transcript-parsers.ts"() {
2604
+ "use strict";
2605
+ init_claude_sessions();
2606
+ init_codex_sessions();
2607
+ init_gemini_sessions();
2608
+ PARSERS = [parseSessionFile, parseCodexSessionFile, parseGeminiSessionFile];
2609
+ }
2610
+ });
2611
+
2612
+ // ../../packages/ingest-core/src/slack-threads.ts
2613
+ function displayName(users, id) {
2614
+ if (!id) return "unknown";
2615
+ const user = users.get(id);
2616
+ return user?.profile?.real_name ?? user?.real_name ?? user?.profile?.display_name ?? user?.name ?? id;
2617
+ }
2618
+ function slackTimestamp(ts) {
2619
+ const seconds = Number(ts);
2620
+ if (!Number.isFinite(seconds) || seconds <= 0) return null;
2621
+ return new Date(seconds * 1e3).toISOString();
2622
+ }
2623
+ function readableText(text, users) {
2624
+ return text.replace(/<@([A-Z0-9]+)(?:\|[^>]*)?>/g, (_, id) => `@${displayName(users, id)}`).replace(/<#[A-Z0-9]+\|([^>]*)>/g, (_, name) => `#${name}`).replace(/<(https?:[^>|]+)\|([^>]*)>/g, (_, __, label) => label).replace(/<(https?:[^>]+)>/g, (_, url) => url).replace(/&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";
799
3178
 
800
3179
  // src/capture-state.ts
801
- import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync4, renameSync, writeFileSync } from "node:fs";
802
- import { homedir } from "node:os";
803
- import { dirname, join } from "node:path";
3180
+ import { existsSync as existsSync6, mkdirSync, readFileSync as readFileSync5, renameSync, writeFileSync } from "node:fs";
3181
+ import { homedir as homedir6 } from "node:os";
3182
+ import { dirname, join as join6 } from "node:path";
804
3183
  var EMPTY = { sessions: {} };
805
- function stateDir(home = homedir()) {
806
- return join(home, ".evrex");
3184
+ function stateDir(home = homedir6()) {
3185
+ return join6(home, ".evrex");
807
3186
  }
808
- function statePath(home = homedir()) {
809
- return join(stateDir(home), "capture-state.json");
3187
+ function statePath(home = homedir6()) {
3188
+ return join6(stateDir(home), "capture-state.json");
810
3189
  }
811
3190
  function readState(path) {
812
3191
  try {
813
- const parsed = JSON.parse(readFileSync4(path, "utf8"));
3192
+ const parsed = JSON.parse(readFileSync5(path, "utf8"));
814
3193
  if (!parsed || typeof parsed !== "object" || typeof parsed.sessions !== "object") {
815
3194
  return { sessions: {} };
816
3195
  }
@@ -829,17 +3208,17 @@ function writeState(path, state) {
829
3208
  }
830
3209
  }
831
3210
  function orphaned(state, exceptSessionId) {
832
- return Object.entries(state.sessions).filter(([id, s]) => !s.archived && id !== exceptSessionId).map(([, s]) => s).filter((s) => existsSync3(s.transcriptPath));
3211
+ return Object.entries(state.sessions).filter(([id, s]) => !s.archived && id !== exceptSessionId).map(([, s]) => s).filter((s) => existsSync6(s.transcriptPath));
833
3212
  }
834
3213
 
835
3214
  // src/spool.ts
836
- import { appendFileSync, existsSync as 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";
837
3216
  import { dirname as dirname2 } from "node:path";
838
3217
  var MAX_SPOOL_BYTES = 32 * 1024 * 1024;
839
3218
  function append(spoolPath, post2) {
840
3219
  try {
841
3220
  mkdirSync2(dirname2(spoolPath), { recursive: true });
842
- if (existsSync4(spoolPath) && sizeOf(spoolPath) >= MAX_SPOOL_BYTES) return;
3221
+ if (existsSync7(spoolPath) && sizeOf(spoolPath) >= MAX_SPOOL_BYTES) return;
843
3222
  appendFileSync(spoolPath, `${JSON.stringify(post2)}
844
3223
  `, { mode: 384 });
845
3224
  } catch {
@@ -847,7 +3226,7 @@ function append(spoolPath, post2) {
847
3226
  }
848
3227
  function read(spoolPath) {
849
3228
  try {
850
- return 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) => {
851
3230
  try {
852
3231
  return [JSON.parse(line)];
853
3232
  } catch {
@@ -876,7 +3255,7 @@ function keep(spoolPath, remaining) {
876
3255
  }
877
3256
  function sizeOf(path) {
878
3257
  try {
879
- return readFileSync5(path).byteLength;
3258
+ return readFileSync6(path).byteLength;
880
3259
  } catch {
881
3260
  return 0;
882
3261
  }
@@ -884,7 +3263,10 @@ function sizeOf(path) {
884
3263
 
885
3264
  // src/capture.ts
886
3265
  var DEADLINE_MS = 6e3;
887
- var 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
+ }
888
3270
  function readStdin() {
889
3271
  return new Promise((resolve) => {
890
3272
  let raw = "";
@@ -902,8 +3284,8 @@ function withDeadline(work, ms) {
902
3284
  }
903
3285
  function parseTranscript(path, cwd) {
904
3286
  try {
905
- const stat = statSync3(path);
906
- 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;
907
3289
  return parseTranscriptFile(path, cwd, deriveRepoId(cwd));
908
3290
  } catch {
909
3291
  return null;
@@ -930,9 +3312,11 @@ async function deliver(spoolPath, path, body, deps) {
930
3312
  }
931
3313
  async function handleEvent(event, deps) {
932
3314
  const name = event.hook_event_name;
3315
+ if (!name) return null;
3316
+ if (event.conversation_id && !event.session_id) return handleCursorEvent(event, name, deps);
933
3317
  const sessionId = event.session_id;
934
- if (!name || !sessionId) return null;
935
- const spoolPath = join2(stateDir(deps.home), "spool.jsonl");
3318
+ if (!sessionId) return null;
3319
+ const spoolPath = join7(stateDir(deps.home), "spool.jsonl");
936
3320
  const drained = await drainSpool(spoolPath, deps);
937
3321
  const path = statePath(deps.home);
938
3322
  const state = readState(path) ?? EMPTY;
@@ -948,6 +3332,13 @@ async function handleEvent(event, deps) {
948
3332
  if (!transcriptPath) {
949
3333
  return { event: name, turnsSent: 0, archived: false, spoolDrained: drained.sent };
950
3334
  }
3335
+ if (name === "SubagentStop" && event.agent_id) {
3336
+ const childPath = subagentTranscriptPath(transcriptPath, sessionId, event.agent_id);
3337
+ const child = parseTranscript(childPath, event.cwd ?? process.cwd());
3338
+ if (child) {
3339
+ await deliver(spoolPath, "/ingest/sessions", { sessions: [child] }, deps);
3340
+ }
3341
+ }
951
3342
  const parsed = parseTranscript(transcriptPath, event.cwd ?? process.cwd());
952
3343
  const sentThrough = prior?.sentThrough ?? 0;
953
3344
  let turnsSent = 0;
@@ -979,6 +3370,39 @@ async function handleEvent(event, deps) {
979
3370
  writeState(path, state);
980
3371
  return { event: name, turnsSent, archived, spoolDrained: drained.sent };
981
3372
  }
3373
+ var CURSOR_ENDINGS = /* @__PURE__ */ new Set(["stop", "sessionEnd"]);
3374
+ async function handleCursorEvent(event, name, deps) {
3375
+ if (!CURSOR_ENDINGS.has(name)) return { event: name, turnsSent: 0, archived: false, spoolDrained: 0 };
3376
+ const cwd = event.workspace_roots?.[0] ?? event.cwd ?? process.cwd();
3377
+ const { parseCursorConversation: parseCursorConversation2 } = await Promise.resolve().then(() => (init_src(), src_exports));
3378
+ let parsed;
3379
+ try {
3380
+ parsed = parseCursorConversation2(event.conversation_id, cwd);
3381
+ } catch {
3382
+ return null;
3383
+ }
3384
+ if (!parsed) return null;
3385
+ const spoolPath = join7(stateDir(deps.home), "spool.jsonl");
3386
+ const drained = await drainSpool(spoolPath, deps);
3387
+ const path = statePath(deps.home);
3388
+ const state = readState(path) ?? EMPTY;
3389
+ const prior = state.sessions[parsed.id];
3390
+ const sentThrough = prior?.sentThrough ?? 0;
3391
+ let turnsSent = 0;
3392
+ if (parsed.turnCount > sentThrough) {
3393
+ const ok = await deliver(spoolPath, "/ingest/sessions", { sessions: [parsed] }, deps);
3394
+ turnsSent = ok ? parsed.turnCount - sentThrough : 0;
3395
+ }
3396
+ state.sessions[parsed.id] = {
3397
+ transcriptPath: `cursor://${event.conversation_id}`,
3398
+ sentThrough: parsed.turnCount,
3399
+ startedAt: prior?.startedAt ?? deps.now().toISOString(),
3400
+ // No transcript file to archive; the store is the archive.
3401
+ archived: true
3402
+ };
3403
+ writeState(path, state);
3404
+ return { event: name, turnsSent, archived: false, spoolDrained: drained.sent };
3405
+ }
982
3406
  async function main() {
983
3407
  const raw = await withDeadline(readStdin(), DEADLINE_MS);
984
3408
  if (!raw) return;
@@ -991,7 +3415,7 @@ async function main() {
991
3415
  const { evrexApi: evrexApi2 } = await Promise.resolve().then(() => (init_client(), client_exports));
992
3416
  const deps = {
993
3417
  now: () => /* @__PURE__ */ new Date(),
994
- home: homedir2(),
3418
+ home: homedir7(),
995
3419
  machine: hostname(),
996
3420
  post: async (path, body) => {
997
3421
  try {
@@ -1018,5 +3442,6 @@ main().then(
1018
3442
  export {
1019
3443
  drainSpool,
1020
3444
  handleEvent,
1021
- parseTranscript
3445
+ parseTranscript,
3446
+ subagentTranscriptPath
1022
3447
  };