evrex-mcp 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -1
- package/dist/account.js +210 -0
- package/dist/capture.js +2600 -165
- package/dist/continuity.js +343 -0
- package/dist/hook.js +160 -24
- package/dist/import.js +2290 -344
- package/dist/index.js +432 -20
- package/dist/pretool.js +341 -0
- package/dist/tickets.js +29 -1
- package/package.json +22 -17
package/dist/import.js
CHANGED
|
@@ -9,252 +9,75 @@ var __export = (target, all) => {
|
|
|
9
9
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
-
// src/
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
evrexApi: () => evrexApi
|
|
16
|
-
});
|
|
17
|
-
function headers() {
|
|
18
|
-
const base = { "Content-Type": "application/json" };
|
|
19
|
-
if (EVREX_TOKEN) base.Authorization = `Bearer ${EVREX_TOKEN}`;
|
|
20
|
-
return base;
|
|
12
|
+
// ../../packages/ingest-core/src/types.ts
|
|
13
|
+
function isReferenceKind(kind) {
|
|
14
|
+
return REFERENCE_KINDS.includes(kind ?? "");
|
|
21
15
|
}
|
|
22
|
-
function
|
|
23
|
-
|
|
24
|
-
return EVREX_TOKEN ? `${method} ${path} -> ${status}: the EVREX_TOKEN this server was started with was rejected. It may be revoked or expired.` : `${method} ${path} -> ${status}: this evrex backend requires a credential, but no EVREX_TOKEN is set for this MCP server.`;
|
|
25
|
-
}
|
|
26
|
-
return `${method} ${path} -> ${status} ${statusText}`;
|
|
16
|
+
function lacksFileAttribution(kind) {
|
|
17
|
+
return kind === "slack" || isReferenceKind(kind);
|
|
27
18
|
}
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
headers: headers(),
|
|
32
|
-
...body === void 0 ? {} : { body: JSON.stringify(body) }
|
|
33
|
-
});
|
|
34
|
-
if (res.status === 404 && absentIsAnswer) return null;
|
|
35
|
-
if (!res.ok) throw new Error(describeFailure(method, path, res.status, res.statusText));
|
|
36
|
-
return await res.json();
|
|
19
|
+
function provenanceTrailerFor(key) {
|
|
20
|
+
const k = key.toLowerCase();
|
|
21
|
+
return PROVENANCE_TRAILERS.find((t) => t.key.toLowerCase() === k);
|
|
37
22
|
}
|
|
38
|
-
var
|
|
39
|
-
var
|
|
40
|
-
"src/
|
|
41
|
-
"use strict";
|
|
42
|
-
DEFAULT_API_BASE_URL = "https://api.evrex.ai";
|
|
43
|
-
API_BASE_URL = (process.env.EVREX_API_BASE_URL ?? process.env.EVREX_API_URL ?? DEFAULT_API_BASE_URL).replace(/\/+$/, "");
|
|
44
|
-
EVREX_TOKEN = process.env.EVREX_TOKEN ?? process.env.EVREX_API_TOKEN ?? null;
|
|
45
|
-
get = (path) => request("GET", path);
|
|
46
|
-
getOrNull = (path) => request("GET", path, { absentIsAnswer: true });
|
|
47
|
-
post = (path, body) => request("POST", path, { body });
|
|
48
|
-
evrexApi = {
|
|
49
|
-
baseUrl: API_BASE_URL,
|
|
50
|
-
repos: () => get("/repos"),
|
|
51
|
-
commits: (repoPath) => get(`/commits?repoPath=${encodeURIComponent(repoPath)}`),
|
|
52
|
-
// Abbreviated shas resolve server-side, so a value pasted from `git log`
|
|
53
|
-
// works here (apps/backend/src/reads/reads.service.ts#resolveSha).
|
|
54
|
-
commit: (sha) => getOrNull(`/commits/${encodeURIComponent(sha)}`),
|
|
55
|
-
sessions: (repoPath) => get(`/sessions?repoPath=${encodeURIComponent(repoPath)}`),
|
|
56
|
-
session: (id) => getOrNull(`/sessions/${encodeURIComponent(id)}`),
|
|
57
|
-
ask: (repoPath, text, filePaths) => post("/ask", { repoPath, text, filePaths }),
|
|
58
|
-
// Evidence-only retrieval (BM25 + embedding), no LLM synthesis call — see
|
|
59
|
-
// apps/backend/src/query/query.service.ts#search. Used by evrex_search,
|
|
60
|
-
// which wants ranked hits fast, not a synthesized paragraph.
|
|
61
|
-
search: (repoPath, text, filePaths) => post("/search", { repoPath, text, filePaths })
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
// src/credential-store.ts
|
|
67
|
-
var credential_store_exports = {};
|
|
68
|
-
__export(credential_store_exports, {
|
|
69
|
-
CredentialStore: () => CredentialStore,
|
|
70
|
-
NoKeychainError: () => NoKeychainError,
|
|
71
|
-
systemRunner: () => systemRunner
|
|
72
|
-
});
|
|
73
|
-
import { spawn } from "node:child_process";
|
|
74
|
-
var SERVICE, ACCOUNT, systemRunner, NoKeychainError, CredentialStore, WINDOWS_PATH, WINDOWS_STORE, WINDOWS_RETRIEVE, WINDOWS_REMOVE;
|
|
75
|
-
var init_credential_store = __esm({
|
|
76
|
-
"src/credential-store.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"() {
|
|
77
26
|
"use strict";
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
CredentialStore = class {
|
|
110
|
-
constructor(runner = systemRunner) {
|
|
111
|
-
this.runner = runner;
|
|
112
|
-
}
|
|
113
|
-
async available() {
|
|
114
|
-
switch (this.runner.platform) {
|
|
115
|
-
case "darwin":
|
|
116
|
-
return (await this.runner.run("security", ["help"])).code !== 127;
|
|
117
|
-
case "win32":
|
|
118
|
-
return (await this.runner.run("powershell", ["-Command", "$PSVersionTable.PSVersion.Major"])).code !== 127;
|
|
119
|
-
default:
|
|
120
|
-
return (await this.runner.run("secret-tool", ["--version"])).code !== 127;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
/**
|
|
124
|
-
* Replaces any existing credential rather than adding a second one. A
|
|
125
|
-
* machine that re-enrols after expiry must end up with exactly one entry, or
|
|
126
|
-
* the next read is a coin flip between the live credential and a dead one.
|
|
127
|
-
*/
|
|
128
|
-
async store(secret) {
|
|
129
|
-
if (!await this.available()) throw new NoKeychainError(this.runner.platform);
|
|
130
|
-
switch (this.runner.platform) {
|
|
131
|
-
case "darwin": {
|
|
132
|
-
const result = await this.runner.run(
|
|
133
|
-
"security",
|
|
134
|
-
["add-generic-password", "-a", ACCOUNT, "-s", SERVICE, "-U", "-w"],
|
|
135
|
-
`${secret}
|
|
136
|
-
${secret}
|
|
137
|
-
`
|
|
138
|
-
);
|
|
139
|
-
if (result.code !== 0) throw new Error(`Keychain write failed: ${result.stderr.trim()}`);
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
case "win32": {
|
|
143
|
-
const result = await this.runner.run(
|
|
144
|
-
"powershell",
|
|
145
|
-
["-NoProfile", "-Command", WINDOWS_STORE],
|
|
146
|
-
secret
|
|
147
|
-
);
|
|
148
|
-
if (result.code !== 0) throw new Error(`DPAPI write failed: ${result.stderr.trim()}`);
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
default: {
|
|
152
|
-
const result = await this.runner.run(
|
|
153
|
-
"secret-tool",
|
|
154
|
-
["store", "--label=evrex capture credential", "service", SERVICE, "account", ACCOUNT],
|
|
155
|
-
secret
|
|
156
|
-
);
|
|
157
|
-
if (result.code !== 0) throw new Error(`secret-tool write failed: ${result.stderr.trim()}`);
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
/** Null when there is nothing stored, which is the normal pre-enrolment state. */
|
|
163
|
-
async retrieve() {
|
|
164
|
-
if (!await this.available()) throw new NoKeychainError(this.runner.platform);
|
|
165
|
-
switch (this.runner.platform) {
|
|
166
|
-
case "darwin": {
|
|
167
|
-
const r = await this.runner.run("security", [
|
|
168
|
-
"find-generic-password",
|
|
169
|
-
"-a",
|
|
170
|
-
ACCOUNT,
|
|
171
|
-
"-s",
|
|
172
|
-
SERVICE,
|
|
173
|
-
"-w"
|
|
174
|
-
]);
|
|
175
|
-
return r.code === 0 ? r.stdout.trim() || null : null;
|
|
176
|
-
}
|
|
177
|
-
case "win32": {
|
|
178
|
-
const r = await this.runner.run("powershell", ["-NoProfile", "-Command", WINDOWS_RETRIEVE]);
|
|
179
|
-
return r.code === 0 ? r.stdout.trim() || null : null;
|
|
180
|
-
}
|
|
181
|
-
default: {
|
|
182
|
-
const r = await this.runner.run("secret-tool", [
|
|
183
|
-
"lookup",
|
|
184
|
-
"service",
|
|
185
|
-
SERVICE,
|
|
186
|
-
"account",
|
|
187
|
-
ACCOUNT
|
|
188
|
-
]);
|
|
189
|
-
return r.code === 0 ? r.stdout.trim() || null : null;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
/** Idempotent: removing a credential that is not there is not an error. */
|
|
194
|
-
async remove() {
|
|
195
|
-
if (!await this.available()) return;
|
|
196
|
-
switch (this.runner.platform) {
|
|
197
|
-
case "darwin":
|
|
198
|
-
await this.runner.run("security", [
|
|
199
|
-
"delete-generic-password",
|
|
200
|
-
"-a",
|
|
201
|
-
ACCOUNT,
|
|
202
|
-
"-s",
|
|
203
|
-
SERVICE
|
|
204
|
-
]);
|
|
205
|
-
return;
|
|
206
|
-
case "win32":
|
|
207
|
-
await this.runner.run("powershell", ["-NoProfile", "-Command", WINDOWS_REMOVE]);
|
|
208
|
-
return;
|
|
209
|
-
default:
|
|
210
|
-
await this.runner.run("secret-tool", [
|
|
211
|
-
"clear",
|
|
212
|
-
"service",
|
|
213
|
-
SERVICE,
|
|
214
|
-
"account",
|
|
215
|
-
ACCOUNT
|
|
216
|
-
]);
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
219
|
-
}
|
|
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
|
|
220
58
|
};
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
$p = "${WINDOWS_PATH}"
|
|
239
|
-
if (Test-Path $p) { Remove-Item $p -Force }
|
|
240
|
-
`.trim();
|
|
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
|
+
];
|
|
241
76
|
}
|
|
242
77
|
});
|
|
243
78
|
|
|
244
|
-
// src/import.ts
|
|
245
|
-
import { realpathSync } from "node:fs";
|
|
246
|
-
import { homedir as homedir5 } from "node:os";
|
|
247
|
-
import { fileURLToPath } from "node:url";
|
|
248
|
-
import { resolve } from "node:path";
|
|
249
|
-
|
|
250
|
-
// ../../packages/ingest-core/src/index.ts
|
|
251
|
-
import { userInfo } from "node:os";
|
|
252
|
-
|
|
253
79
|
// ../../packages/ingest-core/src/git-history.ts
|
|
254
80
|
import { execFileSync } from "node:child_process";
|
|
255
|
-
var DIFF_CAP = 2e4;
|
|
256
|
-
var FIELD_SEP = "";
|
|
257
|
-
var EVREX_SESSION_TRAILER_KEY = "Evrex-Session";
|
|
258
81
|
function git(repoPath, args, input) {
|
|
259
82
|
return execFileSync("git", args, {
|
|
260
83
|
cwd: repoPath,
|
|
@@ -303,6 +126,10 @@ function repoIdFromRootCommit(sha) {
|
|
|
303
126
|
function repoIdFromPath(repoPath) {
|
|
304
127
|
return `path:${repoPath}`;
|
|
305
128
|
}
|
|
129
|
+
function repoNameFromId(repoId) {
|
|
130
|
+
const withoutPrefix = repoId.replace(/^(remote|root|path):/, "");
|
|
131
|
+
return withoutPrefix.split("/").filter(Boolean).pop() ?? repoId;
|
|
132
|
+
}
|
|
306
133
|
function firstRemoteUrl(repoPath) {
|
|
307
134
|
try {
|
|
308
135
|
const origin = gitQuiet(repoPath, ["remote", "get-url", "origin"]).trim();
|
|
@@ -366,25 +193,58 @@ function commitMeta(repoPath, sha) {
|
|
|
366
193
|
message
|
|
367
194
|
};
|
|
368
195
|
}
|
|
369
|
-
function
|
|
196
|
+
function parseTrailers(repoPath, message) {
|
|
370
197
|
try {
|
|
371
198
|
const out = git(
|
|
372
199
|
repoPath,
|
|
373
200
|
["interpret-trailers", "--parse", "--no-divider"],
|
|
374
201
|
message
|
|
375
202
|
).trim();
|
|
203
|
+
const trailers = [];
|
|
376
204
|
for (const line of out.split("\n")) {
|
|
377
205
|
const idx = line.indexOf(":");
|
|
378
206
|
if (idx === -1) continue;
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
}
|
|
207
|
+
const key = line.slice(0, idx).trim();
|
|
208
|
+
const value = line.slice(idx + 1).trim();
|
|
209
|
+
if (key && value) trailers.push({ key, value });
|
|
383
210
|
}
|
|
384
|
-
return
|
|
211
|
+
return trailers;
|
|
385
212
|
} catch {
|
|
386
|
-
return
|
|
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 });
|
|
387
246
|
}
|
|
247
|
+
return out;
|
|
388
248
|
}
|
|
389
249
|
function commitBranch(repoPath, sha) {
|
|
390
250
|
try {
|
|
@@ -450,6 +310,7 @@ function parseGitLog(repoPath, repoId, known) {
|
|
|
450
310
|
const id = repoId ?? deriveRepoId(repoPath);
|
|
451
311
|
return shas.map((sha) => {
|
|
452
312
|
const meta = commitMeta(repoPath, sha);
|
|
313
|
+
const trailers = parseTrailers(repoPath, meta.message);
|
|
453
314
|
return {
|
|
454
315
|
sha: meta.sha,
|
|
455
316
|
repoId: id,
|
|
@@ -459,14 +320,23 @@ function parseGitLog(repoPath, repoId, known) {
|
|
|
459
320
|
ts: meta.ts,
|
|
460
321
|
message: meta.message,
|
|
461
322
|
branch: commitBranch(repoPath, sha),
|
|
462
|
-
evrexSessionTrailer:
|
|
323
|
+
evrexSessionTrailer: trailerValue(trailers, EVREX_SESSION_TRAILER_KEY),
|
|
324
|
+
agentTrailers: agentTrailersOf(trailers),
|
|
325
|
+
statedInsights: statedInsightsOf(trailers),
|
|
463
326
|
files: commitFiles(repoPath, sha)
|
|
464
327
|
};
|
|
465
328
|
});
|
|
466
329
|
}
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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
|
+
});
|
|
470
340
|
|
|
471
341
|
// ../../packages/ingest-core/src/incremental.ts
|
|
472
342
|
import { statSync } from "node:fs";
|
|
@@ -502,28 +372,60 @@ function advanceCursor(path, consumedTo) {
|
|
|
502
372
|
}
|
|
503
373
|
return { offset: consumedTo, size, modifiedAt, parserVersion: PARSER_VERSION };
|
|
504
374
|
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
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
|
+
});
|
|
510
427
|
|
|
511
428
|
// ../../packages/ingest-core/src/redact.ts
|
|
512
|
-
var PATTERNS = [
|
|
513
|
-
{ type: "private_key", regex: /-----BEGIN(?: [A-Z]+)? PRIVATE KEY-----[\s\S]*?-----END(?: [A-Z]+)? PRIVATE KEY-----/g },
|
|
514
|
-
{ type: "aws_access_key", regex: /AKIA[0-9A-Z]{16}/g },
|
|
515
|
-
{ type: "github_token", regex: /gh[pousr]_[A-Za-z0-9]{36,}/g },
|
|
516
|
-
{ type: "slack_token", regex: /xox[baprs]-[A-Za-z0-9-]{10,}/g },
|
|
517
|
-
{ type: "stripe_key", regex: /sk_(live|test)_[A-Za-z0-9]{24,}/g },
|
|
518
|
-
{ type: "openai_key", regex: /sk-[A-Za-z0-9]{20,}/g },
|
|
519
|
-
{ type: "anthropic_key", regex: /sk-ant-[A-Za-z0-9-_]{20,}/g },
|
|
520
|
-
{ type: "jwt", regex: /eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g },
|
|
521
|
-
{ type: "bearer_token", regex: /Bearer\s+[A-Za-z0-9\-_.]{20,}/g },
|
|
522
|
-
{
|
|
523
|
-
type: "env_secret",
|
|
524
|
-
regex: /\b([A-Z0-9_]*(?:_KEY|_SECRET|_TOKEN|_PASSWORD|PASSWD)[A-Z0-9_]*)\s*[:=]\s*["']?[^\s"'\n]{6,}["']?/g
|
|
525
|
-
}
|
|
526
|
-
];
|
|
527
429
|
function redactJsonValue(value) {
|
|
528
430
|
let count = 0;
|
|
529
431
|
const walk = (v) => {
|
|
@@ -544,9 +446,38 @@ function redactJsonValue(value) {
|
|
|
544
446
|
};
|
|
545
447
|
return { value: walk(value), count };
|
|
546
448
|
}
|
|
449
|
+
function redactRawTranscript(content, format) {
|
|
450
|
+
let count = 0;
|
|
451
|
+
if (format === "jsonl") {
|
|
452
|
+
const out = content.split("\n").map((line) => {
|
|
453
|
+
if (!line.trim()) return line;
|
|
454
|
+
try {
|
|
455
|
+
const r = redactJsonValue(JSON.parse(line));
|
|
456
|
+
count += r.count;
|
|
457
|
+
return JSON.stringify(r.value);
|
|
458
|
+
} catch {
|
|
459
|
+
const r = redactSecrets(line);
|
|
460
|
+
count += r.count;
|
|
461
|
+
return r.text;
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
return { content: out.join("\n"), count };
|
|
465
|
+
}
|
|
466
|
+
try {
|
|
467
|
+
const r = redactJsonValue(JSON.parse(content));
|
|
468
|
+
return { content: JSON.stringify(r.value), count: r.count };
|
|
469
|
+
} catch {
|
|
470
|
+
const r = redactSecrets(content);
|
|
471
|
+
return { content: r.text, count: r.count };
|
|
472
|
+
}
|
|
473
|
+
}
|
|
547
474
|
function redactSecrets(input) {
|
|
548
475
|
let text = input;
|
|
549
476
|
let count = 0;
|
|
477
|
+
text = text.replace(PRIVATE_BLOCK, () => {
|
|
478
|
+
count += 1;
|
|
479
|
+
return "[REDACTED:private]";
|
|
480
|
+
});
|
|
550
481
|
for (const { type, regex } of PATTERNS) {
|
|
551
482
|
text = text.replace(regex, (match, group1) => {
|
|
552
483
|
count += 1;
|
|
@@ -558,8 +489,74 @@ function redactSecrets(input) {
|
|
|
558
489
|
}
|
|
559
490
|
return { text, count };
|
|
560
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
|
+
});
|
|
561
514
|
|
|
562
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
|
+
}
|
|
563
560
|
function committedSubjects(command) {
|
|
564
561
|
const out = [];
|
|
565
562
|
const invocation = /\bgit\s+(?:-C\s+\S+\s+)?commit\b/g;
|
|
@@ -579,30 +576,18 @@ function committedSubjects(command) {
|
|
|
579
576
|
}
|
|
580
577
|
return out;
|
|
581
578
|
}
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
"slack"
|
|
590
|
-
];
|
|
591
|
-
var REFERENCE_KINDS = ["linear", "jira", "confluence"];
|
|
592
|
-
var SOURCE_KINDS = [
|
|
593
|
-
...CONVERSATION_KINDS,
|
|
594
|
-
...REFERENCE_KINDS
|
|
595
|
-
];
|
|
596
|
-
var EMPTY_USAGE = {
|
|
597
|
-
inputTokens: null,
|
|
598
|
-
outputTokens: null,
|
|
599
|
-
cacheReadTokens: null,
|
|
600
|
-
cacheWriteTokens: null,
|
|
601
|
-
model: null
|
|
602
|
-
};
|
|
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
|
+
});
|
|
603
586
|
|
|
604
587
|
// ../../packages/ingest-core/src/claude-sessions.ts
|
|
605
|
-
|
|
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";
|
|
606
591
|
function meaningfulLines(lines) {
|
|
607
592
|
return lines.map((l) => l.trim()).filter((l) => l.length >= MIN_MEANINGFUL_LINE_LENGTH);
|
|
608
593
|
}
|
|
@@ -629,20 +614,56 @@ function extractEditedLines(toolUseResult) {
|
|
|
629
614
|
if (meaningfulAdded.length === 0 && meaningfulRemoved.length === 0) return null;
|
|
630
615
|
return { path: r.filePath, added: meaningfulAdded, removed: meaningfulRemoved };
|
|
631
616
|
}
|
|
632
|
-
var MAX_TURN_TEXT_LENGTH = 4e3;
|
|
633
617
|
function slugifyCwd(repoPath) {
|
|
634
|
-
return repoPath.replace(
|
|
618
|
+
return repoPath.replace(/[^A-Za-z0-9]/g, "-");
|
|
635
619
|
}
|
|
636
620
|
function claudeProjectsDir() {
|
|
637
621
|
return join(homedir(), ".claude", "projects");
|
|
638
622
|
}
|
|
623
|
+
function xcodeAssistantProjectsDir() {
|
|
624
|
+
return join(
|
|
625
|
+
homedir(),
|
|
626
|
+
"Library",
|
|
627
|
+
"Developer",
|
|
628
|
+
"Xcode",
|
|
629
|
+
"CodingAssistant",
|
|
630
|
+
"ClaudeAgentConfig",
|
|
631
|
+
"projects"
|
|
632
|
+
);
|
|
633
|
+
}
|
|
639
634
|
function findSessionFiles(repoPath) {
|
|
640
|
-
const
|
|
641
|
-
|
|
642
|
-
|
|
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 };
|
|
643
666
|
}
|
|
644
|
-
var FILE_PATH_TOOLS = /* @__PURE__ */ new Set(["Read", "Edit", "Write", "NotebookEdit"]);
|
|
645
|
-
var PATH_TOKEN_RE = /(?:[.~]?\/)?(?:[\w-]+\/)+[\w.-]+/g;
|
|
646
667
|
function extractPathsFromText(text) {
|
|
647
668
|
const matches = text.match(PATH_TOKEN_RE) ?? [];
|
|
648
669
|
return [...new Set(matches)].filter((p) => p.length > 3 && p.length < 300);
|
|
@@ -657,6 +678,7 @@ function blockText(content) {
|
|
|
657
678
|
function extractFromAssistantContent(content) {
|
|
658
679
|
const textParts = [];
|
|
659
680
|
const filesTouched = [];
|
|
681
|
+
const editedLines = [];
|
|
660
682
|
for (const block of content) {
|
|
661
683
|
if (block.type === "text" && block.text) {
|
|
662
684
|
textParts.push(block.text);
|
|
@@ -666,7 +688,15 @@ function extractFromAssistantContent(content) {
|
|
|
666
688
|
} else if (block.type === "tool_use") {
|
|
667
689
|
const name = block.name ?? "tool";
|
|
668
690
|
const input = block.input ?? {};
|
|
669
|
-
if (
|
|
691
|
+
if (XCODE_EDIT_TOOL.test(name) && typeof input.filePath === "string") {
|
|
692
|
+
textParts.push(`[tool_call: ${name}] ${input.filePath}`);
|
|
693
|
+
filesTouched.push({ path: input.filePath, source: "tool_path", tool: name });
|
|
694
|
+
const edit = editFromXcodeInput(input);
|
|
695
|
+
if (edit) editedLines.push(edit);
|
|
696
|
+
} else if (XCODE_PATH_TOOLS.test(name) && typeof input.filePath === "string") {
|
|
697
|
+
textParts.push(`[tool_call: ${name}] ${input.filePath}`);
|
|
698
|
+
filesTouched.push({ path: input.filePath, source: "tool_path", tool: name });
|
|
699
|
+
} else if (FILE_PATH_TOOLS.has(name) && typeof input.file_path === "string") {
|
|
670
700
|
textParts.push(`[tool_call: ${name}] ${input.file_path}`);
|
|
671
701
|
filesTouched.push({ path: input.file_path, source: "tool_path", tool: name });
|
|
672
702
|
} else if (name === "Bash" && typeof input.command === "string") {
|
|
@@ -680,9 +710,8 @@ function extractFromAssistantContent(content) {
|
|
|
680
710
|
}
|
|
681
711
|
}
|
|
682
712
|
}
|
|
683
|
-
return { text: textParts.join("\n"), filesTouched };
|
|
713
|
+
return { text: textParts.join("\n"), filesTouched, editedLines };
|
|
684
714
|
}
|
|
685
|
-
var SYNTHETIC_CONTENT_RE = /^\s*(<task-notification>|<system-reminder>|\[SYSTEM NOTIFICATION)/;
|
|
686
715
|
function extractFromUserContent(content) {
|
|
687
716
|
if (typeof content === "string") {
|
|
688
717
|
return {
|
|
@@ -694,9 +723,11 @@ function extractFromUserContent(content) {
|
|
|
694
723
|
if (Array.isArray(content)) {
|
|
695
724
|
const toolParts = [];
|
|
696
725
|
const textParts = [];
|
|
726
|
+
let isToolError = false;
|
|
697
727
|
for (const block of content) {
|
|
698
728
|
if (block.type === "tool_result") {
|
|
699
729
|
toolParts.push(blockText(block.content));
|
|
730
|
+
if (block.is_error === true) isToolError = true;
|
|
700
731
|
} else if (block.type === "text" && typeof block.text === "string") {
|
|
701
732
|
textParts.push(block.text);
|
|
702
733
|
}
|
|
@@ -705,7 +736,8 @@ function extractFromUserContent(content) {
|
|
|
705
736
|
return {
|
|
706
737
|
text: toolParts.join("\n"),
|
|
707
738
|
filesTouched: [],
|
|
708
|
-
isSyntheticInput: true
|
|
739
|
+
isSyntheticInput: true,
|
|
740
|
+
isToolError
|
|
709
741
|
};
|
|
710
742
|
}
|
|
711
743
|
const text = textParts.join("\n");
|
|
@@ -741,8 +773,35 @@ function usageOnce(record, billed) {
|
|
|
741
773
|
model: typeof message?.model === "string" ? message.model : null
|
|
742
774
|
};
|
|
743
775
|
}
|
|
744
|
-
function
|
|
745
|
-
const
|
|
776
|
+
function readTranscript(filePath) {
|
|
777
|
+
const size = statSync2(filePath).size;
|
|
778
|
+
if (size <= MAX_TRANSCRIPT_BYTES) {
|
|
779
|
+
return { text: readFileSync(filePath, "utf-8"), capped: false };
|
|
780
|
+
}
|
|
781
|
+
const fd = openSync(filePath, "r");
|
|
782
|
+
try {
|
|
783
|
+
const buf = Buffer.alloc(MAX_TRANSCRIPT_BYTES);
|
|
784
|
+
readSync(fd, buf, 0, MAX_TRANSCRIPT_BYTES, size - MAX_TRANSCRIPT_BYTES);
|
|
785
|
+
const text = buf.toString("utf-8");
|
|
786
|
+
return { text: text.slice(text.indexOf("\n") + 1), capped: true };
|
|
787
|
+
} finally {
|
|
788
|
+
closeSync(fd);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
function readSubagentMeta(transcriptPath) {
|
|
792
|
+
const metaPath = transcriptPath.replace(/\.jsonl$/, ".meta.json");
|
|
793
|
+
try {
|
|
794
|
+
const raw = JSON.parse(readFileSync(metaPath, "utf-8"));
|
|
795
|
+
return {
|
|
796
|
+
agentType: typeof raw.agentType === "string" ? raw.agentType : null,
|
|
797
|
+
description: typeof raw.description === "string" ? raw.description : null
|
|
798
|
+
};
|
|
799
|
+
} catch {
|
|
800
|
+
return null;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
function parseSessionFile(filePath, repoPath, repoId = deriveRepoId(repoPath), agentKind = "claude-code") {
|
|
804
|
+
const { text: raw, capped } = readTranscript(filePath);
|
|
746
805
|
const lines = raw.split("\n").filter((l) => l.trim().length > 0);
|
|
747
806
|
const turns = [];
|
|
748
807
|
const billedMessages = /* @__PURE__ */ new Set();
|
|
@@ -750,6 +809,8 @@ function parseSessionFile(filePath, repoPath, repoId = deriveRepoId(repoPath)) {
|
|
|
750
809
|
const cwd = repoPath;
|
|
751
810
|
let aiTitle = null;
|
|
752
811
|
let totalRedactions = 0;
|
|
812
|
+
let branch = null;
|
|
813
|
+
let agentId = null;
|
|
753
814
|
for (const line of lines) {
|
|
754
815
|
let record;
|
|
755
816
|
try {
|
|
@@ -766,22 +827,27 @@ function parseSessionFile(filePath, repoPath, repoId = deriveRepoId(repoPath)) {
|
|
|
766
827
|
const ts = record.timestamp;
|
|
767
828
|
if (!id || !ts) continue;
|
|
768
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;
|
|
769
832
|
let text = "";
|
|
770
833
|
let filesTouched = [];
|
|
771
834
|
let editedLines = [];
|
|
772
835
|
let isSyntheticInput = false;
|
|
836
|
+
let isToolError = false;
|
|
773
837
|
if (record.type === "assistant") {
|
|
774
838
|
const content = record.message?.content;
|
|
775
839
|
if (Array.isArray(content)) {
|
|
776
840
|
const extracted = extractFromAssistantContent(content);
|
|
777
841
|
text = extracted.text;
|
|
778
842
|
filesTouched = extracted.filesTouched;
|
|
843
|
+
editedLines = extracted.editedLines;
|
|
779
844
|
}
|
|
780
845
|
} else {
|
|
781
846
|
const extracted = extractFromUserContent(record.message?.content);
|
|
782
847
|
text = extracted.text;
|
|
783
848
|
filesTouched = extracted.filesTouched;
|
|
784
849
|
isSyntheticInput = extracted.isSyntheticInput;
|
|
850
|
+
isToolError = extracted.isToolError ?? false;
|
|
785
851
|
const edited = extractEditedLines(record.toolUseResult);
|
|
786
852
|
if (edited) editedLines = [edited];
|
|
787
853
|
}
|
|
@@ -799,12 +865,21 @@ function parseSessionFile(filePath, repoPath, repoId = deriveRepoId(repoPath)) {
|
|
|
799
865
|
isSidechain: Boolean(record.isSidechain),
|
|
800
866
|
redacted: redacted.count > 0,
|
|
801
867
|
isSyntheticInput,
|
|
868
|
+
isToolError,
|
|
802
869
|
usage: usageOnce(record, billedMessages)
|
|
803
870
|
});
|
|
804
871
|
}
|
|
805
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;
|
|
806
881
|
const sortedTs = turns.map((t) => t.ts).sort();
|
|
807
|
-
const rawContent = lines.map((line) => {
|
|
882
|
+
const rawContent = capped ? "" : lines.map((line) => {
|
|
808
883
|
try {
|
|
809
884
|
return JSON.stringify(redactJsonValue(JSON.parse(line)).value);
|
|
810
885
|
} catch {
|
|
@@ -816,7 +891,7 @@ function parseSessionFile(filePath, repoPath, repoId = deriveRepoId(repoPath)) {
|
|
|
816
891
|
}
|
|
817
892
|
return {
|
|
818
893
|
id: sessionId,
|
|
819
|
-
agentKind
|
|
894
|
+
agentKind,
|
|
820
895
|
repoId,
|
|
821
896
|
cwd,
|
|
822
897
|
startedAt: sortedTs[0] ?? null,
|
|
@@ -828,6 +903,9 @@ function parseSessionFile(filePath, repoPath, repoId = deriveRepoId(repoPath)) {
|
|
|
828
903
|
sourceFile: filePath,
|
|
829
904
|
redactionCount: totalRedactions,
|
|
830
905
|
committedSubjects: collectCommittedSubjects(lines),
|
|
906
|
+
branch,
|
|
907
|
+
parentSessionId,
|
|
908
|
+
subagent,
|
|
831
909
|
rawContent,
|
|
832
910
|
rawFormat: "jsonl",
|
|
833
911
|
turns
|
|
@@ -859,40 +937,45 @@ function parseAllSessions(repoPath, repoId, cursors = {}) {
|
|
|
859
937
|
const next = { ...cursors };
|
|
860
938
|
const sessions = [];
|
|
861
939
|
let skipped = 0;
|
|
862
|
-
for (const file of findSessionFiles(repoPath)) {
|
|
940
|
+
for (const { file, agentKind } of findSessionFiles(repoPath)) {
|
|
863
941
|
const plan = planRead(file, cursors[file]);
|
|
864
942
|
if (plan.reason === "unchanged") {
|
|
865
943
|
skipped++;
|
|
866
944
|
continue;
|
|
867
945
|
}
|
|
868
|
-
const parsed = parseSessionFile(file, repoPath, id);
|
|
946
|
+
const parsed = parseSessionFile(file, repoPath, id, agentKind);
|
|
869
947
|
if (!parsed) continue;
|
|
870
948
|
sessions.push(parsed);
|
|
871
949
|
next[file] = advanceCursor(file, statSync2(file).size);
|
|
872
950
|
}
|
|
873
951
|
return { sessions, cursors: next, skipped };
|
|
874
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
|
+
});
|
|
875
973
|
|
|
876
974
|
// ../../packages/ingest-core/src/cursor-sessions.ts
|
|
877
975
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
878
976
|
import { existsSync as existsSync2 } from "node:fs";
|
|
879
977
|
import { homedir as homedir2 } from "node:os";
|
|
880
978
|
import { join as join2, sep } from "node:path";
|
|
881
|
-
|
|
882
|
-
// ../../packages/ingest-core/src/derive-uuid.ts
|
|
883
|
-
import { createHash } from "node:crypto";
|
|
884
|
-
function deriveUuid(name) {
|
|
885
|
-
const h = createHash("sha1").update(name).digest("hex");
|
|
886
|
-
const variant = (parseInt(h.slice(16, 17) || "0", 16) & 3 | 8).toString(16);
|
|
887
|
-
const s = h.slice(0, 12) + // time-low + time-mid
|
|
888
|
-
"5" + // version 5 (name-based, SHA-1)
|
|
889
|
-
h.slice(13, 16) + variant + h.slice(17, 32);
|
|
890
|
-
return `${s.slice(0, 8)}-${s.slice(8, 12)}-${s.slice(12, 16)}-${s.slice(16, 20)}-${s.slice(20, 32)}`;
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
// ../../packages/ingest-core/src/cursor-sessions.ts
|
|
894
|
-
var MAX_TURN_TEXT_LENGTH2 = 4e3;
|
|
895
|
-
var PATH_TOKEN_RE2 = /(?:[.~]?\/)?(?:[\w-]+\/)+[\w.-]+/g;
|
|
896
979
|
function extractPathsFromText2(text) {
|
|
897
980
|
const matches = text.match(PATH_TOKEN_RE2) ?? [];
|
|
898
981
|
return [...new Set(matches)].filter((p) => p.length > 3 && p.length < 300);
|
|
@@ -1109,6 +1192,7 @@ function parseCursorComposer(dbPath, composer, repoPath, options = { scoped: fal
|
|
|
1109
1192
|
// wire-format ambiguity where a tool result also arrives as a
|
|
1110
1193
|
// `role: "user"` record. No synthetic-input misattribution risk here.
|
|
1111
1194
|
isSyntheticInput: false,
|
|
1195
|
+
isToolError: false,
|
|
1112
1196
|
// Neither source reports what a turn cost, so it is unknown rather than free.
|
|
1113
1197
|
usage: { ...EMPTY_USAGE }
|
|
1114
1198
|
}
|
|
@@ -1166,6 +1250,9 @@ function parseCursorComposer(dbPath, composer, repoPath, options = { scoped: fal
|
|
|
1166
1250
|
...new Set(turns.flatMap((t) => committedSubjects(t.text)))
|
|
1167
1251
|
],
|
|
1168
1252
|
rawContent,
|
|
1253
|
+
branch: null,
|
|
1254
|
+
parentSessionId: null,
|
|
1255
|
+
subagent: null,
|
|
1169
1256
|
rawFormat: "json",
|
|
1170
1257
|
turns
|
|
1171
1258
|
};
|
|
@@ -1174,6 +1261,12 @@ function workspaceMatchesRepo(workspacePath, repoPath) {
|
|
|
1174
1261
|
if (!workspacePath) return false;
|
|
1175
1262
|
return workspacePath === repoPath || workspacePath.startsWith(`${repoPath}${sep}`);
|
|
1176
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
|
+
}
|
|
1177
1270
|
function parseAllCursorSessions(repoPath, repoId) {
|
|
1178
1271
|
const dbPath = cursorStateDbPath();
|
|
1179
1272
|
if (!existsSync2(dbPath)) return [];
|
|
@@ -1204,15 +1297,497 @@ function parseAllCursorSessions(repoPath, repoId) {
|
|
|
1204
1297
|
}
|
|
1205
1298
|
return sessions;
|
|
1206
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
|
+
});
|
|
1207
1313
|
|
|
1208
|
-
// ../../packages/ingest-core/src/
|
|
1209
|
-
import {
|
|
1210
|
-
import { existsSync as existsSync3, readFileSync as readFileSync2, readdirSync as readdirSync2, statSync as statSync3 } from "node:fs";
|
|
1314
|
+
// ../../packages/ingest-core/src/opencode-sessions.ts
|
|
1315
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
1211
1316
|
import { homedir as homedir3 } from "node:os";
|
|
1212
1317
|
import { join as join3 } from "node:path";
|
|
1213
|
-
|
|
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";
|
|
1214
1789
|
function codexSessionsDir() {
|
|
1215
|
-
return
|
|
1790
|
+
return join5(homedir5(), CODEX_DIR, "sessions");
|
|
1216
1791
|
}
|
|
1217
1792
|
function deriveUuid2(name) {
|
|
1218
1793
|
const h = createHash2("sha1").update(name).digest("hex");
|
|
@@ -1228,15 +1803,15 @@ function findCodexSessionFiles(root = codexSessionsDir()) {
|
|
|
1228
1803
|
const walk = (dir) => {
|
|
1229
1804
|
let entries;
|
|
1230
1805
|
try {
|
|
1231
|
-
entries =
|
|
1806
|
+
entries = readdirSync3(dir);
|
|
1232
1807
|
} catch {
|
|
1233
1808
|
return;
|
|
1234
1809
|
}
|
|
1235
1810
|
for (const entry of entries) {
|
|
1236
|
-
const full =
|
|
1811
|
+
const full = join5(dir, entry);
|
|
1237
1812
|
let isDir = false;
|
|
1238
1813
|
try {
|
|
1239
|
-
isDir =
|
|
1814
|
+
isDir = statSync4(full).isDirectory();
|
|
1240
1815
|
} catch {
|
|
1241
1816
|
continue;
|
|
1242
1817
|
}
|
|
@@ -1244,7 +1819,7 @@ function findCodexSessionFiles(root = codexSessionsDir()) {
|
|
|
1244
1819
|
else if (entry.endsWith(".jsonl")) found.push(full);
|
|
1245
1820
|
}
|
|
1246
1821
|
};
|
|
1247
|
-
if (
|
|
1822
|
+
if (existsSync5(root)) walk(root);
|
|
1248
1823
|
return found.sort();
|
|
1249
1824
|
}
|
|
1250
1825
|
function parseLines(raw) {
|
|
@@ -1349,7 +1924,7 @@ function filesFromToolCalls(lines) {
|
|
|
1349
1924
|
function parseCodexSessionFile(filePath, repoPath, repoId) {
|
|
1350
1925
|
let raw;
|
|
1351
1926
|
try {
|
|
1352
|
-
raw =
|
|
1927
|
+
raw = readFileSync3(filePath, "utf-8");
|
|
1353
1928
|
} catch {
|
|
1354
1929
|
return null;
|
|
1355
1930
|
}
|
|
@@ -1382,6 +1957,7 @@ function parseCodexSessionFile(filePath, repoPath, repoId) {
|
|
|
1382
1957
|
isSidechain: false,
|
|
1383
1958
|
redacted: count > 0,
|
|
1384
1959
|
isSyntheticInput: false,
|
|
1960
|
+
isToolError: false,
|
|
1385
1961
|
usage: event.usage
|
|
1386
1962
|
};
|
|
1387
1963
|
});
|
|
@@ -1391,6 +1967,9 @@ function parseCodexSessionFile(filePath, repoPath, repoId) {
|
|
|
1391
1967
|
return {
|
|
1392
1968
|
id: sessionId,
|
|
1393
1969
|
agentKind: "codex",
|
|
1970
|
+
branch: null,
|
|
1971
|
+
parentSessionId: null,
|
|
1972
|
+
subagent: null,
|
|
1394
1973
|
// Codex records the remote itself, so identity survives a moved or deleted
|
|
1395
1974
|
// checkout. Falls back to the caller's derivation when it is absent.
|
|
1396
1975
|
repoId: repoIdFromMeta(meta) ?? repoId,
|
|
@@ -1409,9 +1988,9 @@ function parseCodexSessionFile(filePath, repoPath, repoId) {
|
|
|
1409
1988
|
};
|
|
1410
1989
|
}
|
|
1411
1990
|
function repoIdFromMeta(meta) {
|
|
1412
|
-
const
|
|
1413
|
-
if (typeof
|
|
1414
|
-
const url = asString(
|
|
1991
|
+
const git3 = meta.git;
|
|
1992
|
+
if (typeof git3 !== "object" || git3 === null) return null;
|
|
1993
|
+
const url = asString(git3.repository_url);
|
|
1415
1994
|
if (!url) return null;
|
|
1416
1995
|
const normalized = normalizeRepoRemote(url);
|
|
1417
1996
|
return normalized ? `remote:${normalized}` : null;
|
|
@@ -1434,9 +2013,20 @@ function parseAllCodexSessions(repoPath, repoId) {
|
|
|
1434
2013
|
}
|
|
1435
2014
|
return sessions;
|
|
1436
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
|
+
});
|
|
1437
2028
|
|
|
1438
2029
|
// ../../packages/ingest-core/src/sanitize.ts
|
|
1439
|
-
var NUL = String.fromCharCode(0);
|
|
1440
2030
|
function stripNulls(value) {
|
|
1441
2031
|
return value.includes(NUL) ? value.split(NUL).join("") : value;
|
|
1442
2032
|
}
|
|
@@ -1453,8 +2043,991 @@ function stripNullsDeep(value) {
|
|
|
1453
2043
|
}
|
|
1454
2044
|
return value;
|
|
1455
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
|
+
});
|
|
2493
|
+
|
|
2494
|
+
// ../../packages/ingest-core/src/gemini-sessions.ts
|
|
2495
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
2496
|
+
function textOf(content) {
|
|
2497
|
+
if (typeof content === "string") return content;
|
|
2498
|
+
if (!Array.isArray(content)) return "";
|
|
2499
|
+
return content.map(
|
|
2500
|
+
(part) => part && typeof part === "object" && typeof part.text === "string" ? part.text : ""
|
|
2501
|
+
).filter(Boolean).join("\n");
|
|
2502
|
+
}
|
|
2503
|
+
function parseGeminiSessionFile(filePath, repoPath, repoId) {
|
|
2504
|
+
let raw;
|
|
2505
|
+
try {
|
|
2506
|
+
raw = readFileSync4(filePath, "utf-8");
|
|
2507
|
+
} catch {
|
|
2508
|
+
return null;
|
|
2509
|
+
}
|
|
2510
|
+
const records = [];
|
|
2511
|
+
for (const line of raw.split("\n")) {
|
|
2512
|
+
if (!line.trim()) continue;
|
|
2513
|
+
try {
|
|
2514
|
+
records.push(JSON.parse(line));
|
|
2515
|
+
} catch {
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
const meta = records.find((r) => r.sessionId && r.startTime);
|
|
2519
|
+
if (!meta?.sessionId) return null;
|
|
2520
|
+
let messages = [];
|
|
2521
|
+
for (const record of records) {
|
|
2522
|
+
const next = record.$set?.messages ?? record.messages;
|
|
2523
|
+
if (Array.isArray(next)) messages = next;
|
|
2524
|
+
}
|
|
2525
|
+
let redactionCount = 0;
|
|
2526
|
+
const turns = [];
|
|
2527
|
+
messages.forEach((record, index) => {
|
|
2528
|
+
const role = record.type === "user" ? "user" : record.type === "gemini" || record.type === "model" ? "assistant" : null;
|
|
2529
|
+
if (!role) return;
|
|
2530
|
+
const text = textOf(record.content ?? record.displayContent);
|
|
2531
|
+
if (!text.trim()) return;
|
|
2532
|
+
const { text: safe, count } = redactSecrets(text);
|
|
2533
|
+
redactionCount += count;
|
|
2534
|
+
turns.push({
|
|
2535
|
+
// Gemini's own record id when it has one, so a re-read lands on the same
|
|
2536
|
+
// row; a derived id keyed on position otherwise, which is stable for an
|
|
2537
|
+
// append-only file.
|
|
2538
|
+
id: record.id ? deriveUuid(`evrex-gemini-turn ${meta.sessionId} ${record.id}`) : deriveUuid(`evrex-gemini-turn ${meta.sessionId} ${index}`),
|
|
2539
|
+
sessionId: deriveUuid(`evrex-gemini-session ${meta.sessionId}`),
|
|
2540
|
+
role,
|
|
2541
|
+
ts: record.timestamp ?? meta.startTime ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
2542
|
+
text: safe,
|
|
2543
|
+
filesTouched: [],
|
|
2544
|
+
editedLines: [],
|
|
2545
|
+
parentUuid: null,
|
|
2546
|
+
isSidechain: false,
|
|
2547
|
+
redacted: count > 0,
|
|
2548
|
+
// Gemini opens every session with a `<session_context>` block delivered
|
|
2549
|
+
// as a user message. Attributing that to the person is the same bug this
|
|
2550
|
+
// repo already fixed once for Claude Code's tool results.
|
|
2551
|
+
isSyntheticInput: role === "user" && SYNTHETIC.test(text),
|
|
2552
|
+
isToolError: false,
|
|
2553
|
+
// Gemini records a model per message but no token counts in the
|
|
2554
|
+
// transcript; unknown rather than zero.
|
|
2555
|
+
usage: { ...EMPTY_USAGE, model: record.model ?? null }
|
|
2556
|
+
});
|
|
2557
|
+
});
|
|
2558
|
+
if (turns.length === 0) return null;
|
|
2559
|
+
const { text: rawContent, count: rawRedactions } = redactSecrets(raw);
|
|
2560
|
+
const firstUser = turns.find((t) => t.role === "user" && !t.isSyntheticInput);
|
|
2561
|
+
return {
|
|
2562
|
+
id: deriveUuid(`evrex-gemini-session ${meta.sessionId}`),
|
|
2563
|
+
agentKind: "gemini",
|
|
2564
|
+
repoId,
|
|
2565
|
+
cwd: meta.directories?.[0] ?? repoPath,
|
|
2566
|
+
startedAt: meta.startTime ?? turns[0]?.ts ?? null,
|
|
2567
|
+
endedAt: turns[turns.length - 1]?.ts ?? null,
|
|
2568
|
+
turnCount: turns.length,
|
|
2569
|
+
committedSubjects: [],
|
|
2570
|
+
aiTitle: firstUser ? firstUser.text.slice(0, 120).trim() : null,
|
|
2571
|
+
author: null,
|
|
2572
|
+
sourceFile: filePath,
|
|
2573
|
+
redactionCount: redactionCount + rawRedactions,
|
|
2574
|
+
turns,
|
|
2575
|
+
rawContent,
|
|
2576
|
+
branch: null,
|
|
2577
|
+
parentSessionId: null,
|
|
2578
|
+
subagent: null,
|
|
2579
|
+
rawFormat: "jsonl"
|
|
2580
|
+
};
|
|
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
|
+
});
|
|
2592
|
+
|
|
2593
|
+
// ../../packages/ingest-core/src/transcript-parsers.ts
|
|
2594
|
+
function parseTranscriptFile(path, repoPath, repoId) {
|
|
2595
|
+
for (const parse of PARSERS) {
|
|
2596
|
+
const parsed = parse(path, repoPath, repoId);
|
|
2597
|
+
if (parsed) return parsed;
|
|
2598
|
+
}
|
|
2599
|
+
return null;
|
|
2600
|
+
}
|
|
2601
|
+
var PARSERS;
|
|
2602
|
+
var init_transcript_parsers = __esm({
|
|
2603
|
+
"../../packages/ingest-core/src/transcript-parsers.ts"() {
|
|
2604
|
+
"use strict";
|
|
2605
|
+
init_claude_sessions();
|
|
2606
|
+
init_codex_sessions();
|
|
2607
|
+
init_gemini_sessions();
|
|
2608
|
+
PARSERS = [parseSessionFile, parseCodexSessionFile, parseGeminiSessionFile];
|
|
2609
|
+
}
|
|
2610
|
+
});
|
|
2611
|
+
|
|
2612
|
+
// ../../packages/ingest-core/src/slack-threads.ts
|
|
2613
|
+
function displayName(users, id) {
|
|
2614
|
+
if (!id) return "unknown";
|
|
2615
|
+
const user = users.get(id);
|
|
2616
|
+
return user?.profile?.real_name ?? user?.real_name ?? user?.profile?.display_name ?? user?.name ?? id;
|
|
2617
|
+
}
|
|
2618
|
+
function slackTimestamp(ts) {
|
|
2619
|
+
const seconds = Number(ts);
|
|
2620
|
+
if (!Number.isFinite(seconds) || seconds <= 0) return null;
|
|
2621
|
+
return new Date(seconds * 1e3).toISOString();
|
|
2622
|
+
}
|
|
2623
|
+
function readableText(text, users) {
|
|
2624
|
+
return text.replace(/<@([A-Z0-9]+)(?:\|[^>]*)?>/g, (_, id) => `@${displayName(users, id)}`).replace(/<#[A-Z0-9]+\|([^>]*)>/g, (_, name) => `#${name}`).replace(/<(https?:[^>|]+)\|([^>]*)>/g, (_, __, label) => label).replace(/<(https?:[^>]+)>/g, (_, url) => url).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
2625
|
+
}
|
|
2626
|
+
function isHumanMessage(message) {
|
|
2627
|
+
if (message.type && message.type !== "message") return false;
|
|
2628
|
+
if (message.subtype && MACHINE_SUBTYPES.has(message.subtype)) return false;
|
|
2629
|
+
if (message.bot_id && !message.user) return false;
|
|
2630
|
+
return Boolean(message.text?.trim());
|
|
2631
|
+
}
|
|
2632
|
+
function groupIntoThreads(messages) {
|
|
2633
|
+
const threads = /* @__PURE__ */ new Map();
|
|
2634
|
+
for (const message of messages) {
|
|
2635
|
+
if (!isHumanMessage(message)) continue;
|
|
2636
|
+
const key = message.thread_ts ?? message.ts;
|
|
2637
|
+
if (!key) continue;
|
|
2638
|
+
threads.set(key, [...threads.get(key) ?? [], message]);
|
|
2639
|
+
}
|
|
2640
|
+
return [...threads.values()].map(
|
|
2641
|
+
(thread) => [...thread].sort((a, b) => Number(a.ts ?? 0) - Number(b.ts ?? 0))
|
|
2642
|
+
);
|
|
2643
|
+
}
|
|
2644
|
+
function parseSlackExport(messages, options) {
|
|
2645
|
+
const users = /* @__PURE__ */ new Map();
|
|
2646
|
+
for (const user of options.users ?? []) if (user.id) users.set(user.id, user);
|
|
2647
|
+
const out = [];
|
|
2648
|
+
for (const thread of groupIntoThreads(messages)) {
|
|
2649
|
+
const root = thread[0];
|
|
2650
|
+
const rootTs = root?.thread_ts ?? root?.ts;
|
|
2651
|
+
const startedAt = slackTimestamp(rootTs ?? void 0);
|
|
2652
|
+
if (!rootTs || !startedAt) continue;
|
|
2653
|
+
const turns = [];
|
|
2654
|
+
let redactionCount = 0;
|
|
2655
|
+
for (const message of thread) {
|
|
2656
|
+
const ts = slackTimestamp(message.ts);
|
|
2657
|
+
if (!ts) continue;
|
|
2658
|
+
const { text, count } = redactSecrets(
|
|
2659
|
+
readableText(message.text ?? "", users)
|
|
2660
|
+
);
|
|
2661
|
+
if (!text.trim()) continue;
|
|
2662
|
+
redactionCount += count;
|
|
2663
|
+
turns.push({
|
|
2664
|
+
// A uuid, because that is what the columns these land in are. The
|
|
2665
|
+
// readable `slack:<channel>:<ts>` key is kept on `sourceFile`.
|
|
2666
|
+
id: deriveSlackTurnId(options.channel, message.ts),
|
|
2667
|
+
sessionId: deriveSlackSessionId(options.channel, rootTs),
|
|
2668
|
+
// Everything here was typed by a person. There is no assistant side,
|
|
2669
|
+
// and marking any of it otherwise would let it be read as recovered
|
|
2670
|
+
// agent reasoning.
|
|
2671
|
+
role: "user",
|
|
2672
|
+
ts,
|
|
2673
|
+
text: `${displayName(users, message.user)}: ${text}`,
|
|
2674
|
+
filesTouched: [],
|
|
2675
|
+
editedLines: [],
|
|
2676
|
+
parentUuid: null,
|
|
2677
|
+
isSidechain: false,
|
|
2678
|
+
redacted: count > 0,
|
|
2679
|
+
isSyntheticInput: false,
|
|
2680
|
+
isToolError: false,
|
|
2681
|
+
// Neither source reports what a turn cost, so it is unknown rather than free.
|
|
2682
|
+
usage: { ...EMPTY_USAGE }
|
|
2683
|
+
});
|
|
2684
|
+
}
|
|
2685
|
+
const size = turns.reduce((total, turn) => total + turn.text.length, 0);
|
|
2686
|
+
if (turns.length < MIN_THREAD_MESSAGES) continue;
|
|
2687
|
+
const floor = turns.length === 1 ? MIN_SOLO_CHARS : MIN_THREAD_CHARS;
|
|
2688
|
+
if (size < floor) continue;
|
|
2689
|
+
out.push({
|
|
2690
|
+
session: {
|
|
2691
|
+
id: deriveSlackSessionId(options.channel, rootTs),
|
|
2692
|
+
agentKind: "slack",
|
|
2693
|
+
repoId: options.repoId,
|
|
2694
|
+
cwd: options.cwd,
|
|
2695
|
+
startedAt,
|
|
2696
|
+
endedAt: turns[turns.length - 1]?.ts ?? startedAt,
|
|
2697
|
+
turnCount: turns.length,
|
|
2698
|
+
// The first message is what the thread is about, near enough, and it
|
|
2699
|
+
// is what Slack itself shows in a thread list.
|
|
2700
|
+
aiTitle: (turns[0]?.text ?? "").slice(0, 120),
|
|
2701
|
+
author: displayName(users, root?.user),
|
|
2702
|
+
sourceFile: `slack/${options.channel}/${rootTs}.json`,
|
|
2703
|
+
redactionCount,
|
|
2704
|
+
// The thread exactly as Slack gave it, redacted like every other
|
|
2705
|
+
// archived transcript: a teammate re-materialising this must not
|
|
2706
|
+
// receive a key the author pasted into a channel.
|
|
2707
|
+
// A Slack thread runs no commands; nothing here can place a commit.
|
|
2708
|
+
committedSubjects: [],
|
|
2709
|
+
rawContent: JSON.stringify(
|
|
2710
|
+
thread.map((message) => ({
|
|
2711
|
+
...message,
|
|
2712
|
+
text: redactSecrets(message.text ?? "").text
|
|
2713
|
+
})),
|
|
2714
|
+
null,
|
|
2715
|
+
2
|
|
2716
|
+
),
|
|
2717
|
+
branch: null,
|
|
2718
|
+
parentSessionId: null,
|
|
2719
|
+
subagent: null,
|
|
2720
|
+
rawFormat: "json",
|
|
2721
|
+
turns
|
|
2722
|
+
},
|
|
2723
|
+
turns
|
|
2724
|
+
});
|
|
2725
|
+
}
|
|
2726
|
+
return out;
|
|
2727
|
+
}
|
|
2728
|
+
var MIN_THREAD_MESSAGES, MIN_THREAD_CHARS, MIN_SOLO_CHARS, MACHINE_SUBTYPES;
|
|
2729
|
+
var init_slack_threads = __esm({
|
|
2730
|
+
"../../packages/ingest-core/src/slack-threads.ts"() {
|
|
2731
|
+
"use strict";
|
|
2732
|
+
init_redact();
|
|
2733
|
+
init_derive_uuid();
|
|
2734
|
+
init_types();
|
|
2735
|
+
MIN_THREAD_MESSAGES = 1;
|
|
2736
|
+
MIN_THREAD_CHARS = 80;
|
|
2737
|
+
MIN_SOLO_CHARS = 200;
|
|
2738
|
+
MACHINE_SUBTYPES = /* @__PURE__ */ new Set([
|
|
2739
|
+
"channel_join",
|
|
2740
|
+
"channel_leave",
|
|
2741
|
+
"channel_topic",
|
|
2742
|
+
"channel_purpose",
|
|
2743
|
+
"channel_name",
|
|
2744
|
+
"channel_archive",
|
|
2745
|
+
"channel_unarchive",
|
|
2746
|
+
"bot_message",
|
|
2747
|
+
"thread_broadcast_join"
|
|
2748
|
+
]);
|
|
2749
|
+
}
|
|
2750
|
+
});
|
|
2751
|
+
|
|
2752
|
+
// ../../packages/ingest-core/src/slack-client.ts
|
|
2753
|
+
async function call(method, params, options, attempt = 0) {
|
|
2754
|
+
const doFetch = options.fetchImpl ?? fetch;
|
|
2755
|
+
const sleep = options.sleep ?? wait;
|
|
2756
|
+
const query = new URLSearchParams(params).toString();
|
|
2757
|
+
const response = options.post ? await doFetch(`${API}/${method}`, {
|
|
2758
|
+
method: "POST",
|
|
2759
|
+
headers: {
|
|
2760
|
+
Authorization: `Bearer ${options.token}`,
|
|
2761
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
2762
|
+
},
|
|
2763
|
+
body: query
|
|
2764
|
+
}) : await doFetch(`${API}/${method}?${query}`, {
|
|
2765
|
+
headers: { Authorization: `Bearer ${options.token}` }
|
|
2766
|
+
});
|
|
2767
|
+
if (response.status === 429 && attempt < 5) {
|
|
2768
|
+
const header = response.headers.get("retry-after");
|
|
2769
|
+
const seconds = header ? Number(header) : NaN;
|
|
2770
|
+
await sleep((Number.isFinite(seconds) ? seconds : 30) * 1e3);
|
|
2771
|
+
return call(method, params, options, attempt + 1);
|
|
2772
|
+
}
|
|
2773
|
+
const body = await response.json();
|
|
2774
|
+
if (!body.ok) {
|
|
2775
|
+
const code = body.error ?? `http_${response.status}`;
|
|
2776
|
+
throw new SlackError(code, PERMANENT.has(code));
|
|
2777
|
+
}
|
|
2778
|
+
return body;
|
|
2779
|
+
}
|
|
2780
|
+
async function paginate(method, params, pick, options) {
|
|
2781
|
+
const out = [];
|
|
2782
|
+
let cursor;
|
|
2783
|
+
do {
|
|
2784
|
+
const body = await call(
|
|
2785
|
+
method,
|
|
2786
|
+
{ ...params, limit: String(PAGE_SIZE), ...cursor ? { cursor } : {} },
|
|
2787
|
+
options
|
|
2788
|
+
);
|
|
2789
|
+
out.push(...pick(body) ?? []);
|
|
2790
|
+
cursor = body.response_metadata?.next_cursor || void 0;
|
|
2791
|
+
} while (cursor);
|
|
2792
|
+
return out;
|
|
2793
|
+
}
|
|
2794
|
+
async function listChannels(options) {
|
|
2795
|
+
const channels = await paginate(
|
|
2796
|
+
"conversations.list",
|
|
2797
|
+
{ types: "public_channel,private_channel", exclude_archived: "true" },
|
|
2798
|
+
(body) => body.channels,
|
|
2799
|
+
options
|
|
2800
|
+
);
|
|
2801
|
+
return channels.filter(
|
|
2802
|
+
(channel) => !channel.is_private || channel.is_member !== false
|
|
2803
|
+
);
|
|
2804
|
+
}
|
|
2805
|
+
async function joinChannel(channel, options) {
|
|
2806
|
+
await call("conversations.join", { channel }, { ...options, post: true });
|
|
2807
|
+
}
|
|
2808
|
+
async function postMessage(channel, text, options) {
|
|
2809
|
+
await call(
|
|
2810
|
+
"chat.postMessage",
|
|
2811
|
+
{ channel, text, unfurl_links: "false", unfurl_media: "false" },
|
|
2812
|
+
{ ...options, post: true }
|
|
2813
|
+
);
|
|
2814
|
+
}
|
|
2815
|
+
async function listUsers(options) {
|
|
2816
|
+
return paginate("users.list", {}, (body) => body.members, options);
|
|
2817
|
+
}
|
|
2818
|
+
async function fetchChannelMessages(channel, options) {
|
|
2819
|
+
const top = await paginate(
|
|
2820
|
+
"conversations.history",
|
|
2821
|
+
{ channel, ...options.oldest ? { oldest: options.oldest } : {} },
|
|
2822
|
+
(body) => body.messages,
|
|
2823
|
+
options
|
|
2824
|
+
);
|
|
2825
|
+
const all = [];
|
|
2826
|
+
for (const message of top) {
|
|
2827
|
+
all.push(message);
|
|
2828
|
+
const replyCount = message.reply_count ?? 0;
|
|
2829
|
+
if (replyCount > 0 && message.ts) {
|
|
2830
|
+
options.onProgress?.(`reading a thread with ${replyCount} replies`);
|
|
2831
|
+
const replies = await paginate(
|
|
2832
|
+
"conversations.replies",
|
|
2833
|
+
{ channel, ts: message.ts },
|
|
2834
|
+
(body) => body.messages,
|
|
2835
|
+
options
|
|
2836
|
+
);
|
|
2837
|
+
all.push(...replies.filter((reply) => reply.ts !== message.ts));
|
|
2838
|
+
}
|
|
2839
|
+
}
|
|
2840
|
+
return all;
|
|
2841
|
+
}
|
|
2842
|
+
var API, PAGE_SIZE, PERMANENT, SlackError, wait;
|
|
2843
|
+
var init_slack_client = __esm({
|
|
2844
|
+
"../../packages/ingest-core/src/slack-client.ts"() {
|
|
2845
|
+
"use strict";
|
|
2846
|
+
API = "https://slack.com/api";
|
|
2847
|
+
PAGE_SIZE = 200;
|
|
2848
|
+
PERMANENT = /* @__PURE__ */ new Set([
|
|
2849
|
+
"invalid_auth",
|
|
2850
|
+
"account_inactive",
|
|
2851
|
+
"token_revoked",
|
|
2852
|
+
"missing_scope",
|
|
2853
|
+
"not_allowed_token_type"
|
|
2854
|
+
]);
|
|
2855
|
+
SlackError = class extends Error {
|
|
2856
|
+
constructor(slackCode, permanent) {
|
|
2857
|
+
super(`Slack returned ${slackCode}`);
|
|
2858
|
+
this.slackCode = slackCode;
|
|
2859
|
+
this.permanent = permanent;
|
|
2860
|
+
this.name = "SlackError";
|
|
2861
|
+
}
|
|
2862
|
+
};
|
|
2863
|
+
wait = (ms) => new Promise((resolve2) => setTimeout(resolve2, 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
|
+
});
|
|
1456
2925
|
|
|
1457
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";
|
|
1458
3031
|
function resolveFallbackAuthor(repoPath, commits) {
|
|
1459
3032
|
const configured = getGitUserName(repoPath);
|
|
1460
3033
|
if (configured) return configured;
|
|
@@ -1479,7 +3052,9 @@ function collectRepoData(repoPath, options = {}) {
|
|
|
1479
3052
|
const sessions = [
|
|
1480
3053
|
...claude.sessions,
|
|
1481
3054
|
...parseAllCursorSessions(repoPath, repoId),
|
|
1482
|
-
...parseAllCodexSessions(repoPath, repoId)
|
|
3055
|
+
...parseAllCodexSessions(repoPath, repoId),
|
|
3056
|
+
...parseAllOpenCodeSessions(repoPath, repoId),
|
|
3057
|
+
...parseAllCopilotSessions(repoPath, repoId)
|
|
1483
3058
|
].map((s) => ({
|
|
1484
3059
|
...s,
|
|
1485
3060
|
author: s.author ?? author
|
|
@@ -1493,6 +3068,292 @@ function collectRepoData(repoPath, options = {}) {
|
|
|
1493
3068
|
skippedTranscripts: claude.skipped
|
|
1494
3069
|
};
|
|
1495
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/credential-store.ts
|
|
3174
|
+
var credential_store_exports = {};
|
|
3175
|
+
__export(credential_store_exports, {
|
|
3176
|
+
CredentialStore: () => CredentialStore,
|
|
3177
|
+
NoKeychainError: () => NoKeychainError,
|
|
3178
|
+
systemRunner: () => systemRunner
|
|
3179
|
+
});
|
|
3180
|
+
import { spawn } from "node:child_process";
|
|
3181
|
+
var SERVICE, ACCOUNT, systemRunner, NoKeychainError, CredentialStore, WINDOWS_PATH, WINDOWS_STORE, WINDOWS_RETRIEVE, WINDOWS_REMOVE;
|
|
3182
|
+
var init_credential_store = __esm({
|
|
3183
|
+
"src/credential-store.ts"() {
|
|
3184
|
+
"use strict";
|
|
3185
|
+
SERVICE = "evrex-capture";
|
|
3186
|
+
ACCOUNT = "evrex";
|
|
3187
|
+
systemRunner = {
|
|
3188
|
+
platform: process.platform,
|
|
3189
|
+
run(command, args, stdin) {
|
|
3190
|
+
return new Promise((resolve2) => {
|
|
3191
|
+
const child = spawn(command, args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
3192
|
+
let stdout = "";
|
|
3193
|
+
let stderr = "";
|
|
3194
|
+
child.stdout.on("data", (d) => stdout += d.toString());
|
|
3195
|
+
child.stderr.on("data", (d) => stderr += d.toString());
|
|
3196
|
+
child.on("error", () => resolve2({ code: 127, stdout: "", stderr: "" }));
|
|
3197
|
+
child.on("close", (code) => resolve2({ code: code ?? 1, stdout, stderr }));
|
|
3198
|
+
if (stdin !== void 0) child.stdin.write(stdin);
|
|
3199
|
+
child.stdin.end();
|
|
3200
|
+
});
|
|
3201
|
+
}
|
|
3202
|
+
};
|
|
3203
|
+
NoKeychainError = class extends Error {
|
|
3204
|
+
constructor(platform) {
|
|
3205
|
+
super(
|
|
3206
|
+
`evrex could not find a credential store on this machine (${platform}).
|
|
3207
|
+
macOS needs \`security\`, which ships with the system.
|
|
3208
|
+
Linux needs \`secret-tool\` \u2014 install libsecret-tools (Debian/Ubuntu)
|
|
3209
|
+
or libsecret (Fedora/Arch), and make sure a keyring daemon is running.
|
|
3210
|
+
Windows needs PowerShell.
|
|
3211
|
+
evrex will not fall back to writing the credential in a plain file.`
|
|
3212
|
+
);
|
|
3213
|
+
this.name = "NoKeychainError";
|
|
3214
|
+
}
|
|
3215
|
+
};
|
|
3216
|
+
CredentialStore = class {
|
|
3217
|
+
constructor(runner = systemRunner) {
|
|
3218
|
+
this.runner = runner;
|
|
3219
|
+
}
|
|
3220
|
+
async available() {
|
|
3221
|
+
switch (this.runner.platform) {
|
|
3222
|
+
case "darwin":
|
|
3223
|
+
return (await this.runner.run("security", ["help"])).code !== 127;
|
|
3224
|
+
case "win32":
|
|
3225
|
+
return (await this.runner.run("powershell", ["-Command", "$PSVersionTable.PSVersion.Major"])).code !== 127;
|
|
3226
|
+
default:
|
|
3227
|
+
return (await this.runner.run("secret-tool", ["--version"])).code !== 127;
|
|
3228
|
+
}
|
|
3229
|
+
}
|
|
3230
|
+
/**
|
|
3231
|
+
* Replaces any existing credential rather than adding a second one. A
|
|
3232
|
+
* machine that re-enrols after expiry must end up with exactly one entry, or
|
|
3233
|
+
* the next read is a coin flip between the live credential and a dead one.
|
|
3234
|
+
*/
|
|
3235
|
+
async store(secret) {
|
|
3236
|
+
if (!await this.available()) throw new NoKeychainError(this.runner.platform);
|
|
3237
|
+
switch (this.runner.platform) {
|
|
3238
|
+
case "darwin": {
|
|
3239
|
+
const result = await this.runner.run(
|
|
3240
|
+
"security",
|
|
3241
|
+
["add-generic-password", "-a", ACCOUNT, "-s", SERVICE, "-U", "-w"],
|
|
3242
|
+
`${secret}
|
|
3243
|
+
${secret}
|
|
3244
|
+
`
|
|
3245
|
+
);
|
|
3246
|
+
if (result.code !== 0) throw new Error(`Keychain write failed: ${result.stderr.trim()}`);
|
|
3247
|
+
return;
|
|
3248
|
+
}
|
|
3249
|
+
case "win32": {
|
|
3250
|
+
const result = await this.runner.run(
|
|
3251
|
+
"powershell",
|
|
3252
|
+
["-NoProfile", "-Command", WINDOWS_STORE],
|
|
3253
|
+
secret
|
|
3254
|
+
);
|
|
3255
|
+
if (result.code !== 0) throw new Error(`DPAPI write failed: ${result.stderr.trim()}`);
|
|
3256
|
+
return;
|
|
3257
|
+
}
|
|
3258
|
+
default: {
|
|
3259
|
+
const result = await this.runner.run(
|
|
3260
|
+
"secret-tool",
|
|
3261
|
+
["store", "--label=evrex capture credential", "service", SERVICE, "account", ACCOUNT],
|
|
3262
|
+
secret
|
|
3263
|
+
);
|
|
3264
|
+
if (result.code !== 0) throw new Error(`secret-tool write failed: ${result.stderr.trim()}`);
|
|
3265
|
+
return;
|
|
3266
|
+
}
|
|
3267
|
+
}
|
|
3268
|
+
}
|
|
3269
|
+
/** Null when there is nothing stored, which is the normal pre-enrolment state. */
|
|
3270
|
+
async retrieve() {
|
|
3271
|
+
if (!await this.available()) throw new NoKeychainError(this.runner.platform);
|
|
3272
|
+
switch (this.runner.platform) {
|
|
3273
|
+
case "darwin": {
|
|
3274
|
+
const r = await this.runner.run("security", [
|
|
3275
|
+
"find-generic-password",
|
|
3276
|
+
"-a",
|
|
3277
|
+
ACCOUNT,
|
|
3278
|
+
"-s",
|
|
3279
|
+
SERVICE,
|
|
3280
|
+
"-w"
|
|
3281
|
+
]);
|
|
3282
|
+
return r.code === 0 ? r.stdout.trim() || null : null;
|
|
3283
|
+
}
|
|
3284
|
+
case "win32": {
|
|
3285
|
+
const r = await this.runner.run("powershell", ["-NoProfile", "-Command", WINDOWS_RETRIEVE]);
|
|
3286
|
+
return r.code === 0 ? r.stdout.trim() || null : null;
|
|
3287
|
+
}
|
|
3288
|
+
default: {
|
|
3289
|
+
const r = await this.runner.run("secret-tool", [
|
|
3290
|
+
"lookup",
|
|
3291
|
+
"service",
|
|
3292
|
+
SERVICE,
|
|
3293
|
+
"account",
|
|
3294
|
+
ACCOUNT
|
|
3295
|
+
]);
|
|
3296
|
+
return r.code === 0 ? r.stdout.trim() || null : null;
|
|
3297
|
+
}
|
|
3298
|
+
}
|
|
3299
|
+
}
|
|
3300
|
+
/** Idempotent: removing a credential that is not there is not an error. */
|
|
3301
|
+
async remove() {
|
|
3302
|
+
if (!await this.available()) return;
|
|
3303
|
+
switch (this.runner.platform) {
|
|
3304
|
+
case "darwin":
|
|
3305
|
+
await this.runner.run("security", [
|
|
3306
|
+
"delete-generic-password",
|
|
3307
|
+
"-a",
|
|
3308
|
+
ACCOUNT,
|
|
3309
|
+
"-s",
|
|
3310
|
+
SERVICE
|
|
3311
|
+
]);
|
|
3312
|
+
return;
|
|
3313
|
+
case "win32":
|
|
3314
|
+
await this.runner.run("powershell", ["-NoProfile", "-Command", WINDOWS_REMOVE]);
|
|
3315
|
+
return;
|
|
3316
|
+
default:
|
|
3317
|
+
await this.runner.run("secret-tool", [
|
|
3318
|
+
"clear",
|
|
3319
|
+
"service",
|
|
3320
|
+
SERVICE,
|
|
3321
|
+
"account",
|
|
3322
|
+
ACCOUNT
|
|
3323
|
+
]);
|
|
3324
|
+
return;
|
|
3325
|
+
}
|
|
3326
|
+
}
|
|
3327
|
+
};
|
|
3328
|
+
WINDOWS_PATH = "$env:APPDATA\\evrex\\capture.cred";
|
|
3329
|
+
WINDOWS_STORE = `
|
|
3330
|
+
$ErrorActionPreference = 'Stop'
|
|
3331
|
+
$p = "${WINDOWS_PATH}"
|
|
3332
|
+
New-Item -ItemType Directory -Force -Path (Split-Path $p) | Out-Null
|
|
3333
|
+
$secret = [Console]::In.ReadToEnd().Trim()
|
|
3334
|
+
ConvertTo-SecureString $secret -AsPlainText -Force | ConvertFrom-SecureString | Set-Content -Path $p
|
|
3335
|
+
`.trim();
|
|
3336
|
+
WINDOWS_RETRIEVE = `
|
|
3337
|
+
$ErrorActionPreference = 'Stop'
|
|
3338
|
+
$p = "${WINDOWS_PATH}"
|
|
3339
|
+
if (-not (Test-Path $p)) { exit 1 }
|
|
3340
|
+
$sec = Get-Content $p | ConvertTo-SecureString
|
|
3341
|
+
[Runtime.InteropServices.Marshal]::PtrToStringAuto(
|
|
3342
|
+
[Runtime.InteropServices.Marshal]::SecureStringToBSTR($sec))
|
|
3343
|
+
`.trim();
|
|
3344
|
+
WINDOWS_REMOVE = `
|
|
3345
|
+
$p = "${WINDOWS_PATH}"
|
|
3346
|
+
if (Test-Path $p) { Remove-Item $p -Force }
|
|
3347
|
+
`.trim();
|
|
3348
|
+
}
|
|
3349
|
+
});
|
|
3350
|
+
|
|
3351
|
+
// src/import.ts
|
|
3352
|
+
init_src();
|
|
3353
|
+
import { realpathSync } from "node:fs";
|
|
3354
|
+
import { homedir as homedir7 } from "node:os";
|
|
3355
|
+
import { fileURLToPath } from "node:url";
|
|
3356
|
+
import { resolve } from "node:path";
|
|
1496
3357
|
|
|
1497
3358
|
// src/batch.ts
|
|
1498
3359
|
var MAX_BATCH_BYTES = 24 * 1024 * 1024;
|
|
@@ -1520,15 +3381,15 @@ function batchByBytes(items, maxBytes = MAX_BATCH_BYTES) {
|
|
|
1520
3381
|
}
|
|
1521
3382
|
|
|
1522
3383
|
// src/import-state.ts
|
|
1523
|
-
import { existsSync as
|
|
1524
|
-
import { dirname, join as
|
|
1525
|
-
import { homedir as
|
|
1526
|
-
function importStatePath(home =
|
|
1527
|
-
return
|
|
3384
|
+
import { existsSync as existsSync6, mkdirSync, readFileSync as readFileSync5, renameSync, writeFileSync } from "node:fs";
|
|
3385
|
+
import { dirname, join as join6 } from "node:path";
|
|
3386
|
+
import { homedir as homedir6 } from "node:os";
|
|
3387
|
+
function importStatePath(home = homedir6()) {
|
|
3388
|
+
return join6(home, ".evrex", "import-state.json");
|
|
1528
3389
|
}
|
|
1529
3390
|
function readImportState(path) {
|
|
1530
3391
|
try {
|
|
1531
|
-
const parsed = JSON.parse(
|
|
3392
|
+
const parsed = JSON.parse(readFileSync5(path, "utf8"));
|
|
1532
3393
|
if (!parsed || typeof parsed.repos !== "object") return { repos: {} };
|
|
1533
3394
|
return { repos: parsed.repos ?? {} };
|
|
1534
3395
|
} catch {
|
|
@@ -1578,17 +3439,59 @@ async function importRepo(repoPath, deps) {
|
|
|
1578
3439
|
result.commits += batch.length;
|
|
1579
3440
|
deps.log(` commits ${result.commits}/${data.commits.length}`);
|
|
1580
3441
|
}
|
|
3442
|
+
const wireSessions = data.sessions.map(
|
|
3443
|
+
({ rawContent: _raw, rawFormat: _fmt, ...rest }) => rest
|
|
3444
|
+
);
|
|
3445
|
+
const rawBySession = new Map(
|
|
3446
|
+
data.sessions.map((s) => [s.id, { rawContent: s.rawContent, rawFormat: s.rawFormat }])
|
|
3447
|
+
);
|
|
3448
|
+
async function archiveRaw(id) {
|
|
3449
|
+
const raw = rawBySession.get(id);
|
|
3450
|
+
if (!raw?.rawContent) return;
|
|
3451
|
+
await deps.post(`/ingest/sessions/${id}/transcript`, {
|
|
3452
|
+
rawContent: raw.rawContent,
|
|
3453
|
+
rawFormat: raw.rawFormat ?? "jsonl"
|
|
3454
|
+
});
|
|
3455
|
+
}
|
|
3456
|
+
async function postInPieces(d, session) {
|
|
3457
|
+
const turns = session.turns ?? [];
|
|
3458
|
+
if (turns.length <= 1) return false;
|
|
3459
|
+
const mid = Math.ceil(turns.length / 2);
|
|
3460
|
+
for (const slice of [turns.slice(0, mid), turns.slice(mid)]) {
|
|
3461
|
+
const part = { ...session, turns: slice };
|
|
3462
|
+
if (!await d.post("/ingest/sessions", { sessions: [part] })) {
|
|
3463
|
+
if (!await postInPieces(d, part)) return false;
|
|
3464
|
+
}
|
|
3465
|
+
}
|
|
3466
|
+
return true;
|
|
3467
|
+
}
|
|
1581
3468
|
const deliveredCursors = {};
|
|
1582
3469
|
if (!result.failed) {
|
|
1583
|
-
for (const batch of batchByBytes(
|
|
3470
|
+
for (const batch of batchByBytes(wireSessions)) {
|
|
1584
3471
|
result.batches += 1;
|
|
1585
3472
|
if (!await deps.post("/ingest/sessions", { sessions: batch })) {
|
|
3473
|
+
const tooBig = (x) => JSON.stringify(x).length >= MAX_BATCH_BYTES / 4;
|
|
3474
|
+
let salvaged = true;
|
|
3475
|
+
for (const one of batch) {
|
|
3476
|
+
const landed = batch.length > 1 && await deps.post("/ingest/sessions", { sessions: [one] }) ? true : tooBig(one) && await postInPieces(deps, one);
|
|
3477
|
+
if (!landed) {
|
|
3478
|
+
salvaged = false;
|
|
3479
|
+
break;
|
|
3480
|
+
}
|
|
3481
|
+
result.sessions += 1;
|
|
3482
|
+
const cursor = data.cursors[one.sourceFile];
|
|
3483
|
+
if (cursor !== void 0) deliveredCursors[one.sourceFile] = cursor;
|
|
3484
|
+
await archiveRaw(one.id);
|
|
3485
|
+
deps.log(` sessions ${result.sessions}/${data.sessions.length} (split upload)`);
|
|
3486
|
+
}
|
|
3487
|
+
if (salvaged) continue;
|
|
1586
3488
|
result.failed = true;
|
|
1587
3489
|
break;
|
|
1588
3490
|
}
|
|
1589
3491
|
for (const session of batch) {
|
|
1590
3492
|
const cursor = data.cursors[session.sourceFile];
|
|
1591
3493
|
if (cursor !== void 0) deliveredCursors[session.sourceFile] = cursor;
|
|
3494
|
+
await archiveRaw(session.id);
|
|
1592
3495
|
}
|
|
1593
3496
|
result.sessions += batch.length;
|
|
1594
3497
|
deps.log(` sessions ${result.sessions}/${data.sessions.length}`);
|
|
@@ -1602,7 +3505,50 @@ async function importRepo(repoPath, deps) {
|
|
|
1602
3505
|
writeImportState(statePath, state);
|
|
1603
3506
|
return result;
|
|
1604
3507
|
}
|
|
3508
|
+
async function durability(target, argv) {
|
|
3509
|
+
const { measureLineSurvival: measureLineSurvival2, commitsOlderThan: commitsOlderThan2, deriveRepoId: deriveRepoId2 } = await Promise.resolve().then(() => (init_src(), src_exports));
|
|
3510
|
+
const { evrexApi: evrexApi2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
3511
|
+
const flag = (name, fallback) => {
|
|
3512
|
+
const i = argv.indexOf(name);
|
|
3513
|
+
return i === -1 ? fallback : Number(argv[i + 1]) || fallback;
|
|
3514
|
+
};
|
|
3515
|
+
const minAgeDays = flag("--min-age-days", 30);
|
|
3516
|
+
const limit = flag("--limit", 300);
|
|
3517
|
+
const token = process.env.EVREX_TOKEN ?? await new (await Promise.resolve().then(() => (init_credential_store(), credential_store_exports))).CredentialStore().retrieve().catch(() => null);
|
|
3518
|
+
if (!token) {
|
|
3519
|
+
console.error("evrex: this machine is not enrolled.\n Run `npx -y evrex-mcp enrol` first, or set EVREX_TOKEN.");
|
|
3520
|
+
process.exit(1);
|
|
3521
|
+
}
|
|
3522
|
+
const repoId = deriveRepoId2(target);
|
|
3523
|
+
const commits = commitsOlderThan2(target, minAgeDays, limit);
|
|
3524
|
+
console.error(`Measuring ${commits.length} commits at least ${minAgeDays} days old in ${target}`);
|
|
3525
|
+
const measurements = [];
|
|
3526
|
+
for (const [i, c] of commits.entries()) {
|
|
3527
|
+
try {
|
|
3528
|
+
measurements.push(measureLineSurvival2(target, c.sha));
|
|
3529
|
+
} catch {
|
|
3530
|
+
}
|
|
3531
|
+
if ((i + 1) % 25 === 0) console.error(` ${i + 1}/${commits.length}`);
|
|
3532
|
+
}
|
|
3533
|
+
const res = await fetch(`${evrexApi2.baseUrl}/ingest/commits/durability`, {
|
|
3534
|
+
method: "POST",
|
|
3535
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
|
3536
|
+
body: JSON.stringify({ repoId, measurements })
|
|
3537
|
+
});
|
|
3538
|
+
if (!res.ok) {
|
|
3539
|
+
console.error(` /ingest/commits/durability -> ${res.status}`);
|
|
3540
|
+
process.exit(1);
|
|
3541
|
+
}
|
|
3542
|
+
const body = await res.json();
|
|
3543
|
+
const added = measurements.reduce((n, m) => n + m.addedLines, 0);
|
|
3544
|
+
const surviving = measurements.reduce((n, m) => n + m.survivingLines, 0);
|
|
3545
|
+
console.error(` ${body.updated} commits recorded \xB7 ${surviving}/${added} added lines still at HEAD (${added ? Math.round(100 * surviving / added) : 0}%)`);
|
|
3546
|
+
}
|
|
1605
3547
|
async function main() {
|
|
3548
|
+
if (process.argv[2] === "durability") {
|
|
3549
|
+
await durability(resolve(process.argv[3] && !process.argv[3].startsWith("--") ? process.argv[3] : process.cwd()), process.argv.slice(3));
|
|
3550
|
+
return;
|
|
3551
|
+
}
|
|
1606
3552
|
const target = resolve(process.argv[3] ?? process.cwd());
|
|
1607
3553
|
const { evrexApi: evrexApi2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
1608
3554
|
const { CredentialStore: CredentialStore2 } = await Promise.resolve().then(() => (init_credential_store(), credential_store_exports));
|
|
@@ -1617,7 +3563,7 @@ async function main() {
|
|
|
1617
3563
|
const result = await importRepo(target, {
|
|
1618
3564
|
collect: collectRepoData,
|
|
1619
3565
|
deriveRepoId,
|
|
1620
|
-
home:
|
|
3566
|
+
home: homedir7(),
|
|
1621
3567
|
now: () => /* @__PURE__ */ new Date(),
|
|
1622
3568
|
log: (line) => console.error(line),
|
|
1623
3569
|
post: async (path, body) => {
|