evrex-mcp 0.7.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -1
- package/dist/account.js +210 -0
- package/dist/capture.js +2604 -166
- package/dist/continuity.js +83 -10
- package/dist/hook.js +106 -11
- package/dist/import.js +2336 -350
- package/dist/index.js +597 -88
- package/dist/pretool.js +67 -16
- package/dist/tickets.js +24 -1
- package/package.json +4 -2
package/dist/import.js
CHANGED
|
@@ -9,257 +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
|
-
// The paginated transcript — see reads.service.ts#getSessionTurns. `ts, id`
|
|
58
|
-
// ordering server-side makes offsets stable across requests.
|
|
59
|
-
sessionTurns: (id, offset, limit) => getOrNull(
|
|
60
|
-
`/sessions/${encodeURIComponent(id)}/turns?offset=${offset}&limit=${limit}`
|
|
61
|
-
),
|
|
62
|
-
ask: (repoPath, text, filePaths) => post("/ask", { repoPath, text, filePaths }),
|
|
63
|
-
// Evidence-only retrieval (BM25 + embedding), no LLM synthesis call — see
|
|
64
|
-
// apps/backend/src/query/query.service.ts#search. Used by evrex_search,
|
|
65
|
-
// which wants ranked hits fast, not a synthesized paragraph.
|
|
66
|
-
search: (repoPath, text, filePaths) => post("/search", { repoPath, text, filePaths })
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
// src/credential-store.ts
|
|
72
|
-
var credential_store_exports = {};
|
|
73
|
-
__export(credential_store_exports, {
|
|
74
|
-
CredentialStore: () => CredentialStore,
|
|
75
|
-
NoKeychainError: () => NoKeychainError,
|
|
76
|
-
systemRunner: () => systemRunner
|
|
77
|
-
});
|
|
78
|
-
import { spawn } from "node:child_process";
|
|
79
|
-
var SERVICE, ACCOUNT, systemRunner, NoKeychainError, CredentialStore, WINDOWS_PATH, WINDOWS_STORE, WINDOWS_RETRIEVE, WINDOWS_REMOVE;
|
|
80
|
-
var init_credential_store = __esm({
|
|
81
|
-
"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"() {
|
|
82
26
|
"use strict";
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
CredentialStore = class {
|
|
115
|
-
constructor(runner = systemRunner) {
|
|
116
|
-
this.runner = runner;
|
|
117
|
-
}
|
|
118
|
-
async available() {
|
|
119
|
-
switch (this.runner.platform) {
|
|
120
|
-
case "darwin":
|
|
121
|
-
return (await this.runner.run("security", ["help"])).code !== 127;
|
|
122
|
-
case "win32":
|
|
123
|
-
return (await this.runner.run("powershell", ["-Command", "$PSVersionTable.PSVersion.Major"])).code !== 127;
|
|
124
|
-
default:
|
|
125
|
-
return (await this.runner.run("secret-tool", ["--version"])).code !== 127;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
/**
|
|
129
|
-
* Replaces any existing credential rather than adding a second one. A
|
|
130
|
-
* machine that re-enrols after expiry must end up with exactly one entry, or
|
|
131
|
-
* the next read is a coin flip between the live credential and a dead one.
|
|
132
|
-
*/
|
|
133
|
-
async store(secret) {
|
|
134
|
-
if (!await this.available()) throw new NoKeychainError(this.runner.platform);
|
|
135
|
-
switch (this.runner.platform) {
|
|
136
|
-
case "darwin": {
|
|
137
|
-
const result = await this.runner.run(
|
|
138
|
-
"security",
|
|
139
|
-
["add-generic-password", "-a", ACCOUNT, "-s", SERVICE, "-U", "-w"],
|
|
140
|
-
`${secret}
|
|
141
|
-
${secret}
|
|
142
|
-
`
|
|
143
|
-
);
|
|
144
|
-
if (result.code !== 0) throw new Error(`Keychain write failed: ${result.stderr.trim()}`);
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
case "win32": {
|
|
148
|
-
const result = await this.runner.run(
|
|
149
|
-
"powershell",
|
|
150
|
-
["-NoProfile", "-Command", WINDOWS_STORE],
|
|
151
|
-
secret
|
|
152
|
-
);
|
|
153
|
-
if (result.code !== 0) throw new Error(`DPAPI write failed: ${result.stderr.trim()}`);
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
default: {
|
|
157
|
-
const result = await this.runner.run(
|
|
158
|
-
"secret-tool",
|
|
159
|
-
["store", "--label=evrex capture credential", "service", SERVICE, "account", ACCOUNT],
|
|
160
|
-
secret
|
|
161
|
-
);
|
|
162
|
-
if (result.code !== 0) throw new Error(`secret-tool write failed: ${result.stderr.trim()}`);
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
/** Null when there is nothing stored, which is the normal pre-enrolment state. */
|
|
168
|
-
async retrieve() {
|
|
169
|
-
if (!await this.available()) throw new NoKeychainError(this.runner.platform);
|
|
170
|
-
switch (this.runner.platform) {
|
|
171
|
-
case "darwin": {
|
|
172
|
-
const r = await this.runner.run("security", [
|
|
173
|
-
"find-generic-password",
|
|
174
|
-
"-a",
|
|
175
|
-
ACCOUNT,
|
|
176
|
-
"-s",
|
|
177
|
-
SERVICE,
|
|
178
|
-
"-w"
|
|
179
|
-
]);
|
|
180
|
-
return r.code === 0 ? r.stdout.trim() || null : null;
|
|
181
|
-
}
|
|
182
|
-
case "win32": {
|
|
183
|
-
const r = await this.runner.run("powershell", ["-NoProfile", "-Command", WINDOWS_RETRIEVE]);
|
|
184
|
-
return r.code === 0 ? r.stdout.trim() || null : null;
|
|
185
|
-
}
|
|
186
|
-
default: {
|
|
187
|
-
const r = await this.runner.run("secret-tool", [
|
|
188
|
-
"lookup",
|
|
189
|
-
"service",
|
|
190
|
-
SERVICE,
|
|
191
|
-
"account",
|
|
192
|
-
ACCOUNT
|
|
193
|
-
]);
|
|
194
|
-
return r.code === 0 ? r.stdout.trim() || null : null;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
/** Idempotent: removing a credential that is not there is not an error. */
|
|
199
|
-
async remove() {
|
|
200
|
-
if (!await this.available()) return;
|
|
201
|
-
switch (this.runner.platform) {
|
|
202
|
-
case "darwin":
|
|
203
|
-
await this.runner.run("security", [
|
|
204
|
-
"delete-generic-password",
|
|
205
|
-
"-a",
|
|
206
|
-
ACCOUNT,
|
|
207
|
-
"-s",
|
|
208
|
-
SERVICE
|
|
209
|
-
]);
|
|
210
|
-
return;
|
|
211
|
-
case "win32":
|
|
212
|
-
await this.runner.run("powershell", ["-NoProfile", "-Command", WINDOWS_REMOVE]);
|
|
213
|
-
return;
|
|
214
|
-
default:
|
|
215
|
-
await this.runner.run("secret-tool", [
|
|
216
|
-
"clear",
|
|
217
|
-
"service",
|
|
218
|
-
SERVICE,
|
|
219
|
-
"account",
|
|
220
|
-
ACCOUNT
|
|
221
|
-
]);
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
}
|
|
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
|
|
225
58
|
};
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
$p = "${WINDOWS_PATH}"
|
|
244
|
-
if (Test-Path $p) { Remove-Item $p -Force }
|
|
245
|
-
`.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
|
+
];
|
|
246
76
|
}
|
|
247
77
|
});
|
|
248
78
|
|
|
249
|
-
// src/import.ts
|
|
250
|
-
import { realpathSync } from "node:fs";
|
|
251
|
-
import { homedir as homedir5 } from "node:os";
|
|
252
|
-
import { fileURLToPath } from "node:url";
|
|
253
|
-
import { resolve } from "node:path";
|
|
254
|
-
|
|
255
|
-
// ../../packages/ingest-core/src/index.ts
|
|
256
|
-
import { userInfo } from "node:os";
|
|
257
|
-
|
|
258
79
|
// ../../packages/ingest-core/src/git-history.ts
|
|
259
80
|
import { execFileSync } from "node:child_process";
|
|
260
|
-
var DIFF_CAP = 2e4;
|
|
261
|
-
var FIELD_SEP = "";
|
|
262
|
-
var EVREX_SESSION_TRAILER_KEY = "Evrex-Session";
|
|
263
81
|
function git(repoPath, args, input) {
|
|
264
82
|
return execFileSync("git", args, {
|
|
265
83
|
cwd: repoPath,
|
|
@@ -308,6 +126,10 @@ function repoIdFromRootCommit(sha) {
|
|
|
308
126
|
function repoIdFromPath(repoPath) {
|
|
309
127
|
return `path:${repoPath}`;
|
|
310
128
|
}
|
|
129
|
+
function repoNameFromId(repoId) {
|
|
130
|
+
const withoutPrefix = repoId.replace(/^(remote|root|path):/, "");
|
|
131
|
+
return withoutPrefix.split("/").filter(Boolean).pop() ?? repoId;
|
|
132
|
+
}
|
|
311
133
|
function firstRemoteUrl(repoPath) {
|
|
312
134
|
try {
|
|
313
135
|
const origin = gitQuiet(repoPath, ["remote", "get-url", "origin"]).trim();
|
|
@@ -358,7 +180,7 @@ function commitMeta(repoPath, sha) {
|
|
|
358
180
|
const line = git(repoPath, [
|
|
359
181
|
"show",
|
|
360
182
|
"-s",
|
|
361
|
-
`--format=%H${FIELD_SEP}%an${FIELD_SEP}%ae${FIELD_SEP}%aI`,
|
|
183
|
+
`--format=%H${FIELD_SEP}%an${FIELD_SEP}%ae${FIELD_SEP}%aI${FIELD_SEP}%P`,
|
|
362
184
|
sha
|
|
363
185
|
]).trim();
|
|
364
186
|
const parts = line.split(FIELD_SEP);
|
|
@@ -368,28 +190,66 @@ function commitMeta(repoPath, sha) {
|
|
|
368
190
|
author: parts[1] ?? "",
|
|
369
191
|
authorEmail: parts[2] ?? "",
|
|
370
192
|
ts: parts[3] ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
193
|
+
parents: (parts[4] ?? "").split(" ").filter(Boolean),
|
|
371
194
|
message
|
|
372
195
|
};
|
|
373
196
|
}
|
|
374
|
-
function
|
|
197
|
+
function parseTrailers(repoPath, message) {
|
|
375
198
|
try {
|
|
376
199
|
const out = git(
|
|
377
200
|
repoPath,
|
|
378
201
|
["interpret-trailers", "--parse", "--no-divider"],
|
|
379
202
|
message
|
|
380
203
|
).trim();
|
|
204
|
+
const trailers = [];
|
|
381
205
|
for (const line of out.split("\n")) {
|
|
382
206
|
const idx = line.indexOf(":");
|
|
383
207
|
if (idx === -1) continue;
|
|
384
|
-
const
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
}
|
|
208
|
+
const key = line.slice(0, idx).trim();
|
|
209
|
+
const value = line.slice(idx + 1).trim();
|
|
210
|
+
if (key && value) trailers.push({ key, value });
|
|
388
211
|
}
|
|
389
|
-
return
|
|
212
|
+
return trailers;
|
|
390
213
|
} catch {
|
|
391
|
-
return
|
|
214
|
+
return [];
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function trailersFromMessage(message) {
|
|
218
|
+
const paragraphs = message.replace(/\r\n/g, "\n").trim().split(/\n{2,}/);
|
|
219
|
+
const last = paragraphs[paragraphs.length - 1] ?? "";
|
|
220
|
+
if (paragraphs.length < 2) return [];
|
|
221
|
+
const lines = last.split("\n");
|
|
222
|
+
const trailers = [];
|
|
223
|
+
for (const line of lines) {
|
|
224
|
+
const m = /^([A-Za-z][A-Za-z0-9-]*):\s+(.+?)\s*$/.exec(line);
|
|
225
|
+
if (!m) return [];
|
|
226
|
+
trailers.push({ key: m[1], value: m[2] });
|
|
227
|
+
}
|
|
228
|
+
return trailers;
|
|
229
|
+
}
|
|
230
|
+
function statedInsightsOf(trailers) {
|
|
231
|
+
const out = [];
|
|
232
|
+
for (const t of trailers) {
|
|
233
|
+
const known = STATED_TRAILERS.find((s) => s.key.toLowerCase() === t.key.toLowerCase());
|
|
234
|
+
if (known && t.value.trim()) out.push({ kind: known.kind, text: t.value.trim() });
|
|
235
|
+
}
|
|
236
|
+
return out;
|
|
237
|
+
}
|
|
238
|
+
function trailerValue(trailers, key) {
|
|
239
|
+
const k = key.toLowerCase();
|
|
240
|
+
return trailers.find((t) => t.key.toLowerCase() === k)?.value ?? null;
|
|
241
|
+
}
|
|
242
|
+
function originOf(trailers) {
|
|
243
|
+
const v = trailerValue(trailers, EVREX_ORIGIN_TRAILER_KEY);
|
|
244
|
+
return v?.trim().toLowerCase() === "human" ? "human" : null;
|
|
245
|
+
}
|
|
246
|
+
function agentTrailersOf(trailers) {
|
|
247
|
+
const out = [];
|
|
248
|
+
for (const t of trailers) {
|
|
249
|
+
const known = provenanceTrailerFor(t.key);
|
|
250
|
+
if (known) out.push({ key: known.key, value: t.value, agent: known.agent });
|
|
392
251
|
}
|
|
252
|
+
return out;
|
|
393
253
|
}
|
|
394
254
|
function commitBranch(repoPath, sha) {
|
|
395
255
|
try {
|
|
@@ -455,6 +315,7 @@ function parseGitLog(repoPath, repoId, known) {
|
|
|
455
315
|
const id = repoId ?? deriveRepoId(repoPath);
|
|
456
316
|
return shas.map((sha) => {
|
|
457
317
|
const meta = commitMeta(repoPath, sha);
|
|
318
|
+
const trailers = parseTrailers(repoPath, meta.message);
|
|
458
319
|
return {
|
|
459
320
|
sha: meta.sha,
|
|
460
321
|
repoId: id,
|
|
@@ -463,15 +324,27 @@ function parseGitLog(repoPath, repoId, known) {
|
|
|
463
324
|
authorEmail: meta.authorEmail,
|
|
464
325
|
ts: meta.ts,
|
|
465
326
|
message: meta.message,
|
|
327
|
+
parents: meta.parents,
|
|
466
328
|
branch: commitBranch(repoPath, sha),
|
|
467
|
-
evrexSessionTrailer:
|
|
329
|
+
evrexSessionTrailer: trailerValue(trailers, EVREX_SESSION_TRAILER_KEY),
|
|
330
|
+
origin: originOf(trailers),
|
|
331
|
+
agentTrailers: agentTrailersOf(trailers),
|
|
332
|
+
statedInsights: statedInsightsOf(trailers),
|
|
468
333
|
files: commitFiles(repoPath, sha)
|
|
469
334
|
};
|
|
470
335
|
});
|
|
471
336
|
}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
337
|
+
var DIFF_CAP, FIELD_SEP, EVREX_SESSION_TRAILER_KEY, EVREX_ORIGIN_TRAILER_KEY;
|
|
338
|
+
var init_git_history = __esm({
|
|
339
|
+
"../../packages/ingest-core/src/git-history.ts"() {
|
|
340
|
+
"use strict";
|
|
341
|
+
init_types();
|
|
342
|
+
DIFF_CAP = 2e4;
|
|
343
|
+
FIELD_SEP = "";
|
|
344
|
+
EVREX_SESSION_TRAILER_KEY = "Evrex-Session";
|
|
345
|
+
EVREX_ORIGIN_TRAILER_KEY = "Evrex-Origin";
|
|
346
|
+
}
|
|
347
|
+
});
|
|
475
348
|
|
|
476
349
|
// ../../packages/ingest-core/src/incremental.ts
|
|
477
350
|
import { statSync } from "node:fs";
|
|
@@ -507,28 +380,60 @@ function advanceCursor(path, consumedTo) {
|
|
|
507
380
|
}
|
|
508
381
|
return { offset: consumedTo, size, modifiedAt, parserVersion: PARSER_VERSION };
|
|
509
382
|
}
|
|
510
|
-
|
|
383
|
+
function splitCompleteLines(chunk) {
|
|
384
|
+
const lastNewline = chunk.lastIndexOf("\n");
|
|
385
|
+
if (lastNewline === -1) return { lines: [], consumedBytes: 0 };
|
|
386
|
+
const complete = chunk.slice(0, lastNewline);
|
|
387
|
+
return {
|
|
388
|
+
lines: complete.split("\n").filter((l) => l.trim().length > 0),
|
|
389
|
+
consumedBytes: Buffer.byteLength(complete, "utf-8") + 1
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
function sessionSignature(session) {
|
|
393
|
+
const last = session.turns[session.turns.length - 1]?.ts ?? session.endedAt ?? "";
|
|
394
|
+
return `v${PARSER_VERSION}:${session.turnCount}:${last}`;
|
|
395
|
+
}
|
|
396
|
+
function changedSessions(sessions, known) {
|
|
397
|
+
const signatures = {};
|
|
398
|
+
const changed = [];
|
|
399
|
+
for (const session of sessions) {
|
|
400
|
+
const signature = sessionSignature(session);
|
|
401
|
+
signatures[session.id] = signature;
|
|
402
|
+
if (known[session.id] !== signature) changed.push(session);
|
|
403
|
+
}
|
|
404
|
+
return { changed, signatures };
|
|
405
|
+
}
|
|
406
|
+
var PARSER_VERSION;
|
|
407
|
+
var init_incremental = __esm({
|
|
408
|
+
"../../packages/ingest-core/src/incremental.ts"() {
|
|
409
|
+
"use strict";
|
|
410
|
+
PARSER_VERSION = 4;
|
|
411
|
+
}
|
|
412
|
+
});
|
|
511
413
|
|
|
512
|
-
// ../../packages/ingest-core/src/
|
|
513
|
-
import {
|
|
514
|
-
|
|
414
|
+
// ../../packages/ingest-core/src/derive-uuid.ts
|
|
415
|
+
import { createHash } from "node:crypto";
|
|
416
|
+
function deriveUuid(name) {
|
|
417
|
+
const h = createHash("sha1").update(name).digest("hex");
|
|
418
|
+
const variant = (parseInt(h.slice(16, 17) || "0", 16) & 3 | 8).toString(16);
|
|
419
|
+
const s = h.slice(0, 12) + // time-low + time-mid
|
|
420
|
+
"5" + // version 5 (name-based, SHA-1)
|
|
421
|
+
h.slice(13, 16) + variant + h.slice(17, 32);
|
|
422
|
+
return `${s.slice(0, 8)}-${s.slice(8, 12)}-${s.slice(12, 16)}-${s.slice(16, 20)}-${s.slice(20, 32)}`;
|
|
423
|
+
}
|
|
424
|
+
function deriveSlackSessionId(channel, threadTs) {
|
|
425
|
+
return deriveUuid(`evrex-slack-session ${channel} ${threadTs}`);
|
|
426
|
+
}
|
|
427
|
+
function deriveSlackTurnId(channel, messageTs) {
|
|
428
|
+
return deriveUuid(`evrex-slack-turn ${channel} ${messageTs}`);
|
|
429
|
+
}
|
|
430
|
+
var init_derive_uuid = __esm({
|
|
431
|
+
"../../packages/ingest-core/src/derive-uuid.ts"() {
|
|
432
|
+
"use strict";
|
|
433
|
+
}
|
|
434
|
+
});
|
|
515
435
|
|
|
516
436
|
// ../../packages/ingest-core/src/redact.ts
|
|
517
|
-
var PATTERNS = [
|
|
518
|
-
{ type: "private_key", regex: /-----BEGIN(?: [A-Z]+)? PRIVATE KEY-----[\s\S]*?-----END(?: [A-Z]+)? PRIVATE KEY-----/g },
|
|
519
|
-
{ type: "aws_access_key", regex: /AKIA[0-9A-Z]{16}/g },
|
|
520
|
-
{ type: "github_token", regex: /gh[pousr]_[A-Za-z0-9]{36,}/g },
|
|
521
|
-
{ type: "slack_token", regex: /xox[baprs]-[A-Za-z0-9-]{10,}/g },
|
|
522
|
-
{ type: "stripe_key", regex: /sk_(live|test)_[A-Za-z0-9]{24,}/g },
|
|
523
|
-
{ type: "openai_key", regex: /sk-[A-Za-z0-9]{20,}/g },
|
|
524
|
-
{ type: "anthropic_key", regex: /sk-ant-[A-Za-z0-9-_]{20,}/g },
|
|
525
|
-
{ type: "jwt", regex: /eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g },
|
|
526
|
-
{ type: "bearer_token", regex: /Bearer\s+[A-Za-z0-9\-_.]{20,}/g },
|
|
527
|
-
{
|
|
528
|
-
type: "env_secret",
|
|
529
|
-
regex: /\b([A-Z0-9_]*(?:_KEY|_SECRET|_TOKEN|_PASSWORD|PASSWD)[A-Z0-9_]*)\s*[:=]\s*["']?[^\s"'\n]{6,}["']?/g
|
|
530
|
-
}
|
|
531
|
-
];
|
|
532
437
|
function redactJsonValue(value) {
|
|
533
438
|
let count = 0;
|
|
534
439
|
const walk = (v) => {
|
|
@@ -549,7 +454,31 @@ function redactJsonValue(value) {
|
|
|
549
454
|
};
|
|
550
455
|
return { value: walk(value), count };
|
|
551
456
|
}
|
|
552
|
-
|
|
457
|
+
function redactRawTranscript(content, format) {
|
|
458
|
+
let count = 0;
|
|
459
|
+
if (format === "jsonl") {
|
|
460
|
+
const out = content.split("\n").map((line) => {
|
|
461
|
+
if (!line.trim()) return line;
|
|
462
|
+
try {
|
|
463
|
+
const r = redactJsonValue(JSON.parse(line));
|
|
464
|
+
count += r.count;
|
|
465
|
+
return JSON.stringify(r.value);
|
|
466
|
+
} catch {
|
|
467
|
+
const r = redactSecrets(line);
|
|
468
|
+
count += r.count;
|
|
469
|
+
return r.text;
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
return { content: out.join("\n"), count };
|
|
473
|
+
}
|
|
474
|
+
try {
|
|
475
|
+
const r = redactJsonValue(JSON.parse(content));
|
|
476
|
+
return { content: JSON.stringify(r.value), count: r.count };
|
|
477
|
+
} catch {
|
|
478
|
+
const r = redactSecrets(content);
|
|
479
|
+
return { content: r.text, count: r.count };
|
|
480
|
+
}
|
|
481
|
+
}
|
|
553
482
|
function redactSecrets(input) {
|
|
554
483
|
let text = input;
|
|
555
484
|
let count = 0;
|
|
@@ -568,8 +497,74 @@ function redactSecrets(input) {
|
|
|
568
497
|
}
|
|
569
498
|
return { text, count };
|
|
570
499
|
}
|
|
500
|
+
var PATTERNS, PRIVATE_BLOCK;
|
|
501
|
+
var init_redact = __esm({
|
|
502
|
+
"../../packages/ingest-core/src/redact.ts"() {
|
|
503
|
+
"use strict";
|
|
504
|
+
PATTERNS = [
|
|
505
|
+
{ type: "private_key", regex: /-----BEGIN(?: [A-Z]+)? PRIVATE KEY-----[\s\S]*?-----END(?: [A-Z]+)? PRIVATE KEY-----/g },
|
|
506
|
+
{ type: "aws_access_key", regex: /AKIA[0-9A-Z]{16}/g },
|
|
507
|
+
{ type: "github_token", regex: /gh[pousr]_[A-Za-z0-9]{36,}/g },
|
|
508
|
+
{ type: "slack_token", regex: /xox[baprs]-[A-Za-z0-9-]{10,}/g },
|
|
509
|
+
{ type: "stripe_key", regex: /sk_(live|test)_[A-Za-z0-9]{24,}/g },
|
|
510
|
+
{ type: "openai_key", regex: /sk-[A-Za-z0-9]{20,}/g },
|
|
511
|
+
{ type: "anthropic_key", regex: /sk-ant-[A-Za-z0-9-_]{20,}/g },
|
|
512
|
+
{ type: "jwt", regex: /eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g },
|
|
513
|
+
{ type: "bearer_token", regex: /Bearer\s+[A-Za-z0-9\-_.]{20,}/g },
|
|
514
|
+
{
|
|
515
|
+
type: "env_secret",
|
|
516
|
+
regex: /\b([A-Z0-9_]*(?:_KEY|_SECRET|_TOKEN|_PASSWORD|PASSWD)[A-Z0-9_]*)\s*[:=]\s*["']?[^\s"'\n]{6,}["']?/g
|
|
517
|
+
}
|
|
518
|
+
];
|
|
519
|
+
PRIVATE_BLOCK = /<private>[\s\S]*?(?:<\/private>|$)/gi;
|
|
520
|
+
}
|
|
521
|
+
});
|
|
571
522
|
|
|
572
523
|
// ../../packages/ingest-core/src/subject-linking.ts
|
|
524
|
+
function matchCommitToSession(commit, sessions) {
|
|
525
|
+
const subject = commit.subject.trim();
|
|
526
|
+
if (subject.length === 0) {
|
|
527
|
+
return { sha: commit.sha, sessionId: null, reason: "absent" };
|
|
528
|
+
}
|
|
529
|
+
const ran = sessions.filter((s) => s.committedSubjects.includes(subject));
|
|
530
|
+
if (ran.length === 0) {
|
|
531
|
+
return { sha: commit.sha, sessionId: null, reason: "absent" };
|
|
532
|
+
}
|
|
533
|
+
if (ran.length === 1) {
|
|
534
|
+
return {
|
|
535
|
+
sha: commit.sha,
|
|
536
|
+
sessionId: ran[0].sessionId,
|
|
537
|
+
evidence: "ran-the-commit",
|
|
538
|
+
confidence: INFERRED_CONFIDENCE
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
const oneLineage = ran.every((s) => s.startedAt === ran[0].startedAt);
|
|
542
|
+
if (!oneLineage) {
|
|
543
|
+
return { sha: commit.sha, sessionId: null, reason: "ambiguous" };
|
|
544
|
+
}
|
|
545
|
+
const original = [...ran].sort((a, b) => a.endedAt - b.endedAt)[0];
|
|
546
|
+
return {
|
|
547
|
+
sha: commit.sha,
|
|
548
|
+
sessionId: original.sessionId,
|
|
549
|
+
evidence: "ran-the-commit-then-lineage",
|
|
550
|
+
confidence: INFERRED_CONFIDENCE
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
function proposeLinks(commits, sessions) {
|
|
554
|
+
const out = [];
|
|
555
|
+
for (const commit of commits) {
|
|
556
|
+
const match = matchCommitToSession(commit, sessions);
|
|
557
|
+
if (match.sessionId !== null) {
|
|
558
|
+
out.push({
|
|
559
|
+
sha: match.sha,
|
|
560
|
+
sessionId: match.sessionId,
|
|
561
|
+
confidence: match.confidence,
|
|
562
|
+
evidence: match.evidence
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
return out;
|
|
567
|
+
}
|
|
573
568
|
function committedSubjects(command) {
|
|
574
569
|
const out = [];
|
|
575
570
|
const invocation = /\bgit\s+(?:-C\s+\S+\s+)?commit\b/g;
|
|
@@ -589,30 +584,18 @@ function committedSubjects(command) {
|
|
|
589
584
|
}
|
|
590
585
|
return out;
|
|
591
586
|
}
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
"slack"
|
|
600
|
-
];
|
|
601
|
-
var REFERENCE_KINDS = ["linear", "jira", "confluence"];
|
|
602
|
-
var SOURCE_KINDS = [
|
|
603
|
-
...CONVERSATION_KINDS,
|
|
604
|
-
...REFERENCE_KINDS
|
|
605
|
-
];
|
|
606
|
-
var EMPTY_USAGE = {
|
|
607
|
-
inputTokens: null,
|
|
608
|
-
outputTokens: null,
|
|
609
|
-
cacheReadTokens: null,
|
|
610
|
-
cacheWriteTokens: null,
|
|
611
|
-
model: null
|
|
612
|
-
};
|
|
587
|
+
var INFERRED_CONFIDENCE;
|
|
588
|
+
var init_subject_linking = __esm({
|
|
589
|
+
"../../packages/ingest-core/src/subject-linking.ts"() {
|
|
590
|
+
"use strict";
|
|
591
|
+
INFERRED_CONFIDENCE = 0.9;
|
|
592
|
+
}
|
|
593
|
+
});
|
|
613
594
|
|
|
614
595
|
// ../../packages/ingest-core/src/claude-sessions.ts
|
|
615
|
-
|
|
596
|
+
import { closeSync, existsSync, openSync, readdirSync, readFileSync, readSync, statSync as statSync2 } from "node:fs";
|
|
597
|
+
import { homedir } from "node:os";
|
|
598
|
+
import { join } from "node:path";
|
|
616
599
|
function meaningfulLines(lines) {
|
|
617
600
|
return lines.map((l) => l.trim()).filter((l) => l.length >= MIN_MEANINGFUL_LINE_LENGTH);
|
|
618
601
|
}
|
|
@@ -639,20 +622,56 @@ function extractEditedLines(toolUseResult) {
|
|
|
639
622
|
if (meaningfulAdded.length === 0 && meaningfulRemoved.length === 0) return null;
|
|
640
623
|
return { path: r.filePath, added: meaningfulAdded, removed: meaningfulRemoved };
|
|
641
624
|
}
|
|
642
|
-
var MAX_TURN_TEXT_LENGTH = 4e3;
|
|
643
625
|
function slugifyCwd(repoPath) {
|
|
644
|
-
return repoPath.replace(
|
|
626
|
+
return repoPath.replace(/[^A-Za-z0-9]/g, "-");
|
|
645
627
|
}
|
|
646
628
|
function claudeProjectsDir() {
|
|
647
629
|
return join(homedir(), ".claude", "projects");
|
|
648
630
|
}
|
|
631
|
+
function xcodeAssistantProjectsDir() {
|
|
632
|
+
return join(
|
|
633
|
+
homedir(),
|
|
634
|
+
"Library",
|
|
635
|
+
"Developer",
|
|
636
|
+
"Xcode",
|
|
637
|
+
"CodingAssistant",
|
|
638
|
+
"ClaudeAgentConfig",
|
|
639
|
+
"projects"
|
|
640
|
+
);
|
|
641
|
+
}
|
|
649
642
|
function findSessionFiles(repoPath) {
|
|
650
|
-
const
|
|
651
|
-
|
|
652
|
-
|
|
643
|
+
const roots = [
|
|
644
|
+
{ dir: claudeProjectsDir(), agentKind: "claude-code" },
|
|
645
|
+
{ dir: xcodeAssistantProjectsDir(), agentKind: "claude-xcode" }
|
|
646
|
+
];
|
|
647
|
+
const out = [];
|
|
648
|
+
for (const { dir, agentKind } of roots) {
|
|
649
|
+
const slug = join(dir, slugifyCwd(repoPath));
|
|
650
|
+
if (!existsSync(slug)) continue;
|
|
651
|
+
for (const name of readdirSync(slug)) {
|
|
652
|
+
if (name.endsWith(".jsonl")) out.push({ file: join(slug, name), agentKind });
|
|
653
|
+
const subagents = join(slug, name, "subagents");
|
|
654
|
+
if (!name.endsWith(".jsonl") && existsSync(subagents)) {
|
|
655
|
+
for (const child of readdirSync(subagents)) {
|
|
656
|
+
if (child.endsWith(".jsonl")) out.push({ file: join(subagents, child), agentKind });
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
return out;
|
|
662
|
+
}
|
|
663
|
+
function editFromXcodeInput(input) {
|
|
664
|
+
const filePath = input.filePath;
|
|
665
|
+
if (typeof filePath !== "string" || !filePath) return null;
|
|
666
|
+
const oldLines = typeof input.oldString === "string" ? input.oldString.split("\n") : [];
|
|
667
|
+
const newLines = typeof input.newString === "string" ? input.newString.split("\n") : [];
|
|
668
|
+
const oldSet = new Set(oldLines);
|
|
669
|
+
const newSet = new Set(newLines);
|
|
670
|
+
const added = meaningfulLines(newLines.filter((l) => !oldSet.has(l)));
|
|
671
|
+
const removed = meaningfulLines(oldLines.filter((l) => !newSet.has(l)));
|
|
672
|
+
if (added.length === 0 && removed.length === 0) return null;
|
|
673
|
+
return { path: filePath, added, removed };
|
|
653
674
|
}
|
|
654
|
-
var FILE_PATH_TOOLS = /* @__PURE__ */ new Set(["Read", "Edit", "Write", "NotebookEdit"]);
|
|
655
|
-
var PATH_TOKEN_RE = /(?:[.~]?\/)?(?:[\w-]+\/)+[\w.-]+/g;
|
|
656
675
|
function extractPathsFromText(text) {
|
|
657
676
|
const matches = text.match(PATH_TOKEN_RE) ?? [];
|
|
658
677
|
return [...new Set(matches)].filter((p) => p.length > 3 && p.length < 300);
|
|
@@ -667,6 +686,7 @@ function blockText(content) {
|
|
|
667
686
|
function extractFromAssistantContent(content) {
|
|
668
687
|
const textParts = [];
|
|
669
688
|
const filesTouched = [];
|
|
689
|
+
const editedLines = [];
|
|
670
690
|
for (const block of content) {
|
|
671
691
|
if (block.type === "text" && block.text) {
|
|
672
692
|
textParts.push(block.text);
|
|
@@ -676,7 +696,15 @@ function extractFromAssistantContent(content) {
|
|
|
676
696
|
} else if (block.type === "tool_use") {
|
|
677
697
|
const name = block.name ?? "tool";
|
|
678
698
|
const input = block.input ?? {};
|
|
679
|
-
if (
|
|
699
|
+
if (XCODE_EDIT_TOOL.test(name) && typeof input.filePath === "string") {
|
|
700
|
+
textParts.push(`[tool_call: ${name}] ${input.filePath}`);
|
|
701
|
+
filesTouched.push({ path: input.filePath, source: "tool_path", tool: name });
|
|
702
|
+
const edit = editFromXcodeInput(input);
|
|
703
|
+
if (edit) editedLines.push(edit);
|
|
704
|
+
} else if (XCODE_PATH_TOOLS.test(name) && typeof input.filePath === "string") {
|
|
705
|
+
textParts.push(`[tool_call: ${name}] ${input.filePath}`);
|
|
706
|
+
filesTouched.push({ path: input.filePath, source: "tool_path", tool: name });
|
|
707
|
+
} else if (FILE_PATH_TOOLS.has(name) && typeof input.file_path === "string") {
|
|
680
708
|
textParts.push(`[tool_call: ${name}] ${input.file_path}`);
|
|
681
709
|
filesTouched.push({ path: input.file_path, source: "tool_path", tool: name });
|
|
682
710
|
} else if (name === "Bash" && typeof input.command === "string") {
|
|
@@ -690,9 +718,8 @@ function extractFromAssistantContent(content) {
|
|
|
690
718
|
}
|
|
691
719
|
}
|
|
692
720
|
}
|
|
693
|
-
return { text: textParts.join("\n"), filesTouched };
|
|
721
|
+
return { text: textParts.join("\n"), filesTouched, editedLines };
|
|
694
722
|
}
|
|
695
|
-
var SYNTHETIC_CONTENT_RE = /^\s*(<task-notification>|<system-reminder>|\[SYSTEM NOTIFICATION)/;
|
|
696
723
|
function extractFromUserContent(content) {
|
|
697
724
|
if (typeof content === "string") {
|
|
698
725
|
return {
|
|
@@ -704,9 +731,11 @@ function extractFromUserContent(content) {
|
|
|
704
731
|
if (Array.isArray(content)) {
|
|
705
732
|
const toolParts = [];
|
|
706
733
|
const textParts = [];
|
|
734
|
+
let isToolError = false;
|
|
707
735
|
for (const block of content) {
|
|
708
736
|
if (block.type === "tool_result") {
|
|
709
737
|
toolParts.push(blockText(block.content));
|
|
738
|
+
if (block.is_error === true) isToolError = true;
|
|
710
739
|
} else if (block.type === "text" && typeof block.text === "string") {
|
|
711
740
|
textParts.push(block.text);
|
|
712
741
|
}
|
|
@@ -715,7 +744,8 @@ function extractFromUserContent(content) {
|
|
|
715
744
|
return {
|
|
716
745
|
text: toolParts.join("\n"),
|
|
717
746
|
filesTouched: [],
|
|
718
|
-
isSyntheticInput: true
|
|
747
|
+
isSyntheticInput: true,
|
|
748
|
+
isToolError
|
|
719
749
|
};
|
|
720
750
|
}
|
|
721
751
|
const text = textParts.join("\n");
|
|
@@ -751,8 +781,35 @@ function usageOnce(record, billed) {
|
|
|
751
781
|
model: typeof message?.model === "string" ? message.model : null
|
|
752
782
|
};
|
|
753
783
|
}
|
|
754
|
-
function
|
|
755
|
-
const
|
|
784
|
+
function readTranscript(filePath) {
|
|
785
|
+
const size = statSync2(filePath).size;
|
|
786
|
+
if (size <= MAX_TRANSCRIPT_BYTES) {
|
|
787
|
+
return { text: readFileSync(filePath, "utf-8"), capped: false };
|
|
788
|
+
}
|
|
789
|
+
const fd = openSync(filePath, "r");
|
|
790
|
+
try {
|
|
791
|
+
const buf = Buffer.alloc(MAX_TRANSCRIPT_BYTES);
|
|
792
|
+
readSync(fd, buf, 0, MAX_TRANSCRIPT_BYTES, size - MAX_TRANSCRIPT_BYTES);
|
|
793
|
+
const text = buf.toString("utf-8");
|
|
794
|
+
return { text: text.slice(text.indexOf("\n") + 1), capped: true };
|
|
795
|
+
} finally {
|
|
796
|
+
closeSync(fd);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
function readSubagentMeta(transcriptPath) {
|
|
800
|
+
const metaPath = transcriptPath.replace(/\.jsonl$/, ".meta.json");
|
|
801
|
+
try {
|
|
802
|
+
const raw = JSON.parse(readFileSync(metaPath, "utf-8"));
|
|
803
|
+
return {
|
|
804
|
+
agentType: typeof raw.agentType === "string" ? raw.agentType : null,
|
|
805
|
+
description: typeof raw.description === "string" ? raw.description : null
|
|
806
|
+
};
|
|
807
|
+
} catch {
|
|
808
|
+
return null;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
function parseSessionFile(filePath, repoPath, repoId = deriveRepoId(repoPath), agentKind = "claude-code") {
|
|
812
|
+
const { text: raw, capped } = readTranscript(filePath);
|
|
756
813
|
const lines = raw.split("\n").filter((l) => l.trim().length > 0);
|
|
757
814
|
const turns = [];
|
|
758
815
|
const billedMessages = /* @__PURE__ */ new Set();
|
|
@@ -760,6 +817,9 @@ function parseSessionFile(filePath, repoPath, repoId = deriveRepoId(repoPath)) {
|
|
|
760
817
|
const cwd = repoPath;
|
|
761
818
|
let aiTitle = null;
|
|
762
819
|
let totalRedactions = 0;
|
|
820
|
+
let branch = null;
|
|
821
|
+
let agentId = null;
|
|
822
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
763
823
|
for (const line of lines) {
|
|
764
824
|
let record;
|
|
765
825
|
try {
|
|
@@ -775,23 +835,30 @@ function parseSessionFile(filePath, repoPath, repoId = deriveRepoId(repoPath)) {
|
|
|
775
835
|
const id = record.uuid;
|
|
776
836
|
const ts = record.timestamp;
|
|
777
837
|
if (!id || !ts) continue;
|
|
838
|
+
if (seenIds.has(id)) continue;
|
|
839
|
+
seenIds.add(id);
|
|
778
840
|
sessionId ??= record.sessionId ?? record.session_id ?? null;
|
|
841
|
+
if (typeof record.gitBranch === "string" && record.gitBranch) branch = record.gitBranch;
|
|
842
|
+
if (typeof record.agentId === "string" && record.agentId) agentId ??= record.agentId;
|
|
779
843
|
let text = "";
|
|
780
844
|
let filesTouched = [];
|
|
781
845
|
let editedLines = [];
|
|
782
846
|
let isSyntheticInput = false;
|
|
847
|
+
let isToolError = false;
|
|
783
848
|
if (record.type === "assistant") {
|
|
784
849
|
const content = record.message?.content;
|
|
785
850
|
if (Array.isArray(content)) {
|
|
786
851
|
const extracted = extractFromAssistantContent(content);
|
|
787
852
|
text = extracted.text;
|
|
788
853
|
filesTouched = extracted.filesTouched;
|
|
854
|
+
editedLines = extracted.editedLines;
|
|
789
855
|
}
|
|
790
856
|
} else {
|
|
791
857
|
const extracted = extractFromUserContent(record.message?.content);
|
|
792
858
|
text = extracted.text;
|
|
793
859
|
filesTouched = extracted.filesTouched;
|
|
794
860
|
isSyntheticInput = extracted.isSyntheticInput;
|
|
861
|
+
isToolError = extracted.isToolError ?? false;
|
|
795
862
|
const edited = extractEditedLines(record.toolUseResult);
|
|
796
863
|
if (edited) editedLines = [edited];
|
|
797
864
|
}
|
|
@@ -809,12 +876,21 @@ function parseSessionFile(filePath, repoPath, repoId = deriveRepoId(repoPath)) {
|
|
|
809
876
|
isSidechain: Boolean(record.isSidechain),
|
|
810
877
|
redacted: redacted.count > 0,
|
|
811
878
|
isSyntheticInput,
|
|
879
|
+
isToolError,
|
|
812
880
|
usage: usageOnce(record, billedMessages)
|
|
813
881
|
});
|
|
814
882
|
}
|
|
815
883
|
if (!sessionId || turns.length === 0) return null;
|
|
884
|
+
const parentSessionId = agentId ? sessionId : null;
|
|
885
|
+
if (agentId) {
|
|
886
|
+
sessionId = deriveUuid(`evrex-subagent\0${parentSessionId}\0${agentId}`);
|
|
887
|
+
for (const t of turns) t.sessionId = sessionId;
|
|
888
|
+
}
|
|
889
|
+
const meta = agentId ? readSubagentMeta(filePath) : null;
|
|
890
|
+
const subagent = agentId ? { agentId, agentType: meta?.agentType ?? null, description: meta?.description ?? null } : null;
|
|
891
|
+
if (agentId && !aiTitle && meta?.description) aiTitle = meta.description;
|
|
816
892
|
const sortedTs = turns.map((t) => t.ts).sort();
|
|
817
|
-
const rawContent = lines.map((line) => {
|
|
893
|
+
const rawContent = capped ? "" : lines.map((line) => {
|
|
818
894
|
try {
|
|
819
895
|
return JSON.stringify(redactJsonValue(JSON.parse(line)).value);
|
|
820
896
|
} catch {
|
|
@@ -826,7 +902,7 @@ function parseSessionFile(filePath, repoPath, repoId = deriveRepoId(repoPath)) {
|
|
|
826
902
|
}
|
|
827
903
|
return {
|
|
828
904
|
id: sessionId,
|
|
829
|
-
agentKind
|
|
905
|
+
agentKind,
|
|
830
906
|
repoId,
|
|
831
907
|
cwd,
|
|
832
908
|
startedAt: sortedTs[0] ?? null,
|
|
@@ -838,6 +914,9 @@ function parseSessionFile(filePath, repoPath, repoId = deriveRepoId(repoPath)) {
|
|
|
838
914
|
sourceFile: filePath,
|
|
839
915
|
redactionCount: totalRedactions,
|
|
840
916
|
committedSubjects: collectCommittedSubjects(lines),
|
|
917
|
+
branch,
|
|
918
|
+
parentSessionId,
|
|
919
|
+
subagent,
|
|
841
920
|
rawContent,
|
|
842
921
|
rawFormat: "jsonl",
|
|
843
922
|
turns
|
|
@@ -869,40 +948,45 @@ function parseAllSessions(repoPath, repoId, cursors = {}) {
|
|
|
869
948
|
const next = { ...cursors };
|
|
870
949
|
const sessions = [];
|
|
871
950
|
let skipped = 0;
|
|
872
|
-
for (const file of findSessionFiles(repoPath)) {
|
|
951
|
+
for (const { file, agentKind } of findSessionFiles(repoPath)) {
|
|
873
952
|
const plan = planRead(file, cursors[file]);
|
|
874
953
|
if (plan.reason === "unchanged") {
|
|
875
954
|
skipped++;
|
|
876
955
|
continue;
|
|
877
956
|
}
|
|
878
|
-
const parsed = parseSessionFile(file, repoPath, id);
|
|
957
|
+
const parsed = parseSessionFile(file, repoPath, id, agentKind);
|
|
879
958
|
if (!parsed) continue;
|
|
880
959
|
sessions.push(parsed);
|
|
881
960
|
next[file] = advanceCursor(file, statSync2(file).size);
|
|
882
961
|
}
|
|
883
962
|
return { sessions, cursors: next, skipped };
|
|
884
963
|
}
|
|
964
|
+
var MIN_MEANINGFUL_LINE_LENGTH, MAX_TURN_TEXT_LENGTH, FILE_PATH_TOOLS, XCODE_EDIT_TOOL, XCODE_PATH_TOOLS, PATH_TOKEN_RE, SYNTHETIC_CONTENT_RE, MAX_TRANSCRIPT_BYTES;
|
|
965
|
+
var init_claude_sessions = __esm({
|
|
966
|
+
"../../packages/ingest-core/src/claude-sessions.ts"() {
|
|
967
|
+
"use strict";
|
|
968
|
+
init_incremental();
|
|
969
|
+
init_git_history();
|
|
970
|
+
init_derive_uuid();
|
|
971
|
+
init_redact();
|
|
972
|
+
init_subject_linking();
|
|
973
|
+
init_types();
|
|
974
|
+
MIN_MEANINGFUL_LINE_LENGTH = 6;
|
|
975
|
+
MAX_TURN_TEXT_LENGTH = 4e3;
|
|
976
|
+
FILE_PATH_TOOLS = /* @__PURE__ */ new Set(["Read", "Edit", "Write", "NotebookEdit"]);
|
|
977
|
+
XCODE_EDIT_TOOL = /XcodeUpdate$/;
|
|
978
|
+
XCODE_PATH_TOOLS = /Xcode(Read|Grep|Glob|RefreshCodeIssuesInFile)$/;
|
|
979
|
+
PATH_TOKEN_RE = /(?:[.~]?\/)?(?:[\w-]+\/)+[\w.-]+/g;
|
|
980
|
+
SYNTHETIC_CONTENT_RE = /^\s*(<task-notification>|<system-reminder>|\[SYSTEM NOTIFICATION)/;
|
|
981
|
+
MAX_TRANSCRIPT_BYTES = 256 * 1024 * 1024;
|
|
982
|
+
}
|
|
983
|
+
});
|
|
885
984
|
|
|
886
985
|
// ../../packages/ingest-core/src/cursor-sessions.ts
|
|
887
986
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
888
987
|
import { existsSync as existsSync2 } from "node:fs";
|
|
889
988
|
import { homedir as homedir2 } from "node:os";
|
|
890
989
|
import { join as join2, sep } from "node:path";
|
|
891
|
-
|
|
892
|
-
// ../../packages/ingest-core/src/derive-uuid.ts
|
|
893
|
-
import { createHash } from "node:crypto";
|
|
894
|
-
function deriveUuid(name) {
|
|
895
|
-
const h = createHash("sha1").update(name).digest("hex");
|
|
896
|
-
const variant = (parseInt(h.slice(16, 17) || "0", 16) & 3 | 8).toString(16);
|
|
897
|
-
const s = h.slice(0, 12) + // time-low + time-mid
|
|
898
|
-
"5" + // version 5 (name-based, SHA-1)
|
|
899
|
-
h.slice(13, 16) + variant + h.slice(17, 32);
|
|
900
|
-
return `${s.slice(0, 8)}-${s.slice(8, 12)}-${s.slice(12, 16)}-${s.slice(16, 20)}-${s.slice(20, 32)}`;
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
// ../../packages/ingest-core/src/cursor-sessions.ts
|
|
904
|
-
var MAX_TURN_TEXT_LENGTH2 = 4e3;
|
|
905
|
-
var PATH_TOKEN_RE2 = /(?:[.~]?\/)?(?:[\w-]+\/)+[\w.-]+/g;
|
|
906
990
|
function extractPathsFromText2(text) {
|
|
907
991
|
const matches = text.match(PATH_TOKEN_RE2) ?? [];
|
|
908
992
|
return [...new Set(matches)].filter((p) => p.length > 3 && p.length < 300);
|
|
@@ -1119,6 +1203,7 @@ function parseCursorComposer(dbPath, composer, repoPath, options = { scoped: fal
|
|
|
1119
1203
|
// wire-format ambiguity where a tool result also arrives as a
|
|
1120
1204
|
// `role: "user"` record. No synthetic-input misattribution risk here.
|
|
1121
1205
|
isSyntheticInput: false,
|
|
1206
|
+
isToolError: false,
|
|
1122
1207
|
// Neither source reports what a turn cost, so it is unknown rather than free.
|
|
1123
1208
|
usage: { ...EMPTY_USAGE }
|
|
1124
1209
|
}
|
|
@@ -1176,6 +1261,9 @@ function parseCursorComposer(dbPath, composer, repoPath, options = { scoped: fal
|
|
|
1176
1261
|
...new Set(turns.flatMap((t) => committedSubjects(t.text)))
|
|
1177
1262
|
],
|
|
1178
1263
|
rawContent,
|
|
1264
|
+
branch: null,
|
|
1265
|
+
parentSessionId: null,
|
|
1266
|
+
subagent: null,
|
|
1179
1267
|
rawFormat: "json",
|
|
1180
1268
|
turns
|
|
1181
1269
|
};
|
|
@@ -1184,6 +1272,12 @@ function workspaceMatchesRepo(workspacePath, repoPath) {
|
|
|
1184
1272
|
if (!workspacePath) return false;
|
|
1185
1273
|
return workspacePath === repoPath || workspacePath.startsWith(`${repoPath}${sep}`);
|
|
1186
1274
|
}
|
|
1275
|
+
function parseCursorConversation(composerId, repoPath, repoId, dbPath = cursorStateDbPath()) {
|
|
1276
|
+
if (!existsSync2(dbPath)) return null;
|
|
1277
|
+
const composer = loadComposers(dbPath).find((c) => c.composerId === composerId);
|
|
1278
|
+
if (!composer) return null;
|
|
1279
|
+
return parseCursorComposer(dbPath, composer, repoPath, { scoped: false }, repoId);
|
|
1280
|
+
}
|
|
1187
1281
|
function parseAllCursorSessions(repoPath, repoId) {
|
|
1188
1282
|
const dbPath = cursorStateDbPath();
|
|
1189
1283
|
if (!existsSync2(dbPath)) return [];
|
|
@@ -1214,15 +1308,497 @@ function parseAllCursorSessions(repoPath, repoId) {
|
|
|
1214
1308
|
}
|
|
1215
1309
|
return sessions;
|
|
1216
1310
|
}
|
|
1311
|
+
var MAX_TURN_TEXT_LENGTH2, PATH_TOKEN_RE2;
|
|
1312
|
+
var init_cursor_sessions = __esm({
|
|
1313
|
+
"../../packages/ingest-core/src/cursor-sessions.ts"() {
|
|
1314
|
+
"use strict";
|
|
1315
|
+
init_git_history();
|
|
1316
|
+
init_redact();
|
|
1317
|
+
init_derive_uuid();
|
|
1318
|
+
init_subject_linking();
|
|
1319
|
+
init_types();
|
|
1320
|
+
MAX_TURN_TEXT_LENGTH2 = 4e3;
|
|
1321
|
+
PATH_TOKEN_RE2 = /(?:[.~]?\/)?(?:[\w-]+\/)+[\w.-]+/g;
|
|
1322
|
+
}
|
|
1323
|
+
});
|
|
1217
1324
|
|
|
1218
|
-
// ../../packages/ingest-core/src/
|
|
1219
|
-
import {
|
|
1220
|
-
import { existsSync as existsSync3, readFileSync as readFileSync2, readdirSync as readdirSync2, statSync as statSync3 } from "node:fs";
|
|
1325
|
+
// ../../packages/ingest-core/src/opencode-sessions.ts
|
|
1326
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
1221
1327
|
import { homedir as homedir3 } from "node:os";
|
|
1222
1328
|
import { join as join3 } from "node:path";
|
|
1223
|
-
|
|
1329
|
+
function opencodeDbPath(env = process.env, home = homedir3()) {
|
|
1330
|
+
const data = env.XDG_DATA_HOME || join3(home, ".local", "share");
|
|
1331
|
+
return join3(data, "opencode", "opencode.db");
|
|
1332
|
+
}
|
|
1333
|
+
function q(value) {
|
|
1334
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
1335
|
+
}
|
|
1336
|
+
function parseJson(raw) {
|
|
1337
|
+
try {
|
|
1338
|
+
return JSON.parse(raw);
|
|
1339
|
+
} catch {
|
|
1340
|
+
return null;
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
function deriveOpenCodeSessionId(sessionId) {
|
|
1344
|
+
return deriveUuid(`evrex-opencode\0${sessionId}`);
|
|
1345
|
+
}
|
|
1346
|
+
function deriveTurnId(partOrMessageId, suffix = "") {
|
|
1347
|
+
return deriveUuid(`evrex-opencode-turn\0${partOrMessageId}${suffix}`);
|
|
1348
|
+
}
|
|
1349
|
+
function filesFromTool2(tool, input) {
|
|
1350
|
+
const out = [];
|
|
1351
|
+
for (const key of ["filePath", "path", "file"]) {
|
|
1352
|
+
const v = input[key];
|
|
1353
|
+
if (typeof v === "string" && v.length > 0) out.push({ path: v, source: "tool_path", tool });
|
|
1354
|
+
}
|
|
1355
|
+
const command = input.command;
|
|
1356
|
+
if (tool === "bash" && typeof command === "string") {
|
|
1357
|
+
for (const p of extractPathsFromText(command)) out.push({ path: p, source: "tool_bash", tool });
|
|
1358
|
+
}
|
|
1359
|
+
return out;
|
|
1360
|
+
}
|
|
1361
|
+
function usageOf(message) {
|
|
1362
|
+
const t = message.tokens;
|
|
1363
|
+
if (!t) return { ...EMPTY_USAGE, model: message.modelID ?? null };
|
|
1364
|
+
return {
|
|
1365
|
+
inputTokens: t.input ?? null,
|
|
1366
|
+
outputTokens: t.output ?? null,
|
|
1367
|
+
cacheReadTokens: t.cache?.read ?? null,
|
|
1368
|
+
cacheWriteTokens: t.cache?.write ?? null,
|
|
1369
|
+
model: message.modelID ?? null
|
|
1370
|
+
};
|
|
1371
|
+
}
|
|
1372
|
+
function buildOpenCodeSession(session, messages, parts, repoId, sourceFile) {
|
|
1373
|
+
const id = deriveOpenCodeSessionId(session.id);
|
|
1374
|
+
const partsByMessage = /* @__PURE__ */ new Map();
|
|
1375
|
+
for (const p of parts) {
|
|
1376
|
+
const list = partsByMessage.get(p.message_id) ?? [];
|
|
1377
|
+
list.push(p);
|
|
1378
|
+
partsByMessage.set(p.message_id, list);
|
|
1379
|
+
}
|
|
1380
|
+
const turns = [];
|
|
1381
|
+
let redactionCount = 0;
|
|
1382
|
+
let previous = null;
|
|
1383
|
+
const subjects = [];
|
|
1384
|
+
const push = (turn) => {
|
|
1385
|
+
const redacted = redactSecrets(turn.text);
|
|
1386
|
+
redactionCount += redacted.count;
|
|
1387
|
+
turns.push({
|
|
1388
|
+
...turn,
|
|
1389
|
+
text: redacted.text,
|
|
1390
|
+
sessionId: id,
|
|
1391
|
+
parentUuid: previous,
|
|
1392
|
+
isSidechain: false,
|
|
1393
|
+
redacted: redacted.count > 0,
|
|
1394
|
+
editedLines: []
|
|
1395
|
+
});
|
|
1396
|
+
previous = turn.id;
|
|
1397
|
+
};
|
|
1398
|
+
const at = (ms) => new Date(ms).toISOString();
|
|
1399
|
+
for (const m of [...messages].sort((a, b) => a.time_created - b.time_created)) {
|
|
1400
|
+
const data = parseJson(m.data);
|
|
1401
|
+
if (!data?.role) continue;
|
|
1402
|
+
const mparts = (partsByMessage.get(m.id) ?? []).sort((a, b) => a.time_created - b.time_created);
|
|
1403
|
+
if (data.role === "user") {
|
|
1404
|
+
const text = mparts.map((p) => parseJson(p.data)).filter((p) => !!p && p.type === "text" && typeof p.text === "string").map((p) => p.text).join("\n").trim();
|
|
1405
|
+
if (!text) continue;
|
|
1406
|
+
push({
|
|
1407
|
+
id: deriveTurnId(m.id),
|
|
1408
|
+
role: "user",
|
|
1409
|
+
ts: at(data.time?.created ?? m.time_created),
|
|
1410
|
+
text,
|
|
1411
|
+
filesTouched: extractPathsFromText(text).map((path) => ({ path, source: "prose" })),
|
|
1412
|
+
usage: EMPTY_USAGE,
|
|
1413
|
+
isSyntheticInput: false,
|
|
1414
|
+
isToolError: false
|
|
1415
|
+
});
|
|
1416
|
+
continue;
|
|
1417
|
+
}
|
|
1418
|
+
const usage = usageOf(data);
|
|
1419
|
+
let usageGiven = false;
|
|
1420
|
+
const take = () => {
|
|
1421
|
+
if (usageGiven) return EMPTY_USAGE;
|
|
1422
|
+
usageGiven = true;
|
|
1423
|
+
return usage;
|
|
1424
|
+
};
|
|
1425
|
+
for (const p of mparts) {
|
|
1426
|
+
const part = parseJson(p.data);
|
|
1427
|
+
if (!part) continue;
|
|
1428
|
+
if (part.type === "text" && typeof part.text === "string" && part.text.trim()) {
|
|
1429
|
+
push({
|
|
1430
|
+
id: deriveTurnId(p.id),
|
|
1431
|
+
role: "assistant",
|
|
1432
|
+
ts: at(p.time_created),
|
|
1433
|
+
text: part.text.trim(),
|
|
1434
|
+
filesTouched: [],
|
|
1435
|
+
usage: take(),
|
|
1436
|
+
isSyntheticInput: false,
|
|
1437
|
+
isToolError: false
|
|
1438
|
+
});
|
|
1439
|
+
} else if (part.type === "tool" && typeof part.tool === "string") {
|
|
1440
|
+
const input = part.state?.input ?? {};
|
|
1441
|
+
const args = JSON.stringify(input);
|
|
1442
|
+
const files = filesFromTool2(part.tool, input);
|
|
1443
|
+
if (part.tool === "bash" && typeof input.command === "string") {
|
|
1444
|
+
subjects.push(...committedSubjects(input.command));
|
|
1445
|
+
}
|
|
1446
|
+
push({
|
|
1447
|
+
id: deriveTurnId(p.id, ":call"),
|
|
1448
|
+
role: "assistant",
|
|
1449
|
+
ts: at(p.time_created),
|
|
1450
|
+
text: `[tool: ${part.tool}] ${args.length > 600 ? `${args.slice(0, 600)}\u2026` : args}`,
|
|
1451
|
+
filesTouched: files,
|
|
1452
|
+
usage: take(),
|
|
1453
|
+
isSyntheticInput: false,
|
|
1454
|
+
isToolError: false
|
|
1455
|
+
});
|
|
1456
|
+
const failed = part.state?.status === "error";
|
|
1457
|
+
const output = failed ? part.state?.error ?? part.state?.output ?? "" : part.state?.output ?? "";
|
|
1458
|
+
push({
|
|
1459
|
+
id: deriveTurnId(p.id, ":result"),
|
|
1460
|
+
role: "user",
|
|
1461
|
+
ts: at(p.time_created),
|
|
1462
|
+
text: output.length > MAX_OUTPUT_CHARS ? `${output.slice(0, MAX_OUTPUT_CHARS)}\u2026` : output,
|
|
1463
|
+
filesTouched: [],
|
|
1464
|
+
usage: EMPTY_USAGE,
|
|
1465
|
+
isSyntheticInput: true,
|
|
1466
|
+
isToolError: failed
|
|
1467
|
+
});
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
if (turns.length === 0) return null;
|
|
1472
|
+
return {
|
|
1473
|
+
id,
|
|
1474
|
+
agentKind: "opencode",
|
|
1475
|
+
repoId,
|
|
1476
|
+
cwd: session.directory,
|
|
1477
|
+
startedAt: new Date(session.time_created).toISOString(),
|
|
1478
|
+
endedAt: new Date(session.time_updated).toISOString(),
|
|
1479
|
+
turnCount: turns.length,
|
|
1480
|
+
aiTitle: session.title || null,
|
|
1481
|
+
author: null,
|
|
1482
|
+
sourceFile,
|
|
1483
|
+
redactionCount,
|
|
1484
|
+
committedSubjects: [...new Set(subjects)],
|
|
1485
|
+
parentSessionId: session.parent_id ? deriveOpenCodeSessionId(session.parent_id) : null,
|
|
1486
|
+
subagent: session.parent_id ? { agentId: session.id, agentType: session.agent ?? null, description: session.title || null } : null,
|
|
1487
|
+
branch: null,
|
|
1488
|
+
// The archive is the rows themselves, so a reader later has what the
|
|
1489
|
+
// store had — minus the reasoning parts, which OpenCode itself hides.
|
|
1490
|
+
rawContent: JSON.stringify({
|
|
1491
|
+
session,
|
|
1492
|
+
messages,
|
|
1493
|
+
parts: parts.filter((p) => !p.data.includes('"type":"reasoning"'))
|
|
1494
|
+
}),
|
|
1495
|
+
rawFormat: "json",
|
|
1496
|
+
turns
|
|
1497
|
+
};
|
|
1498
|
+
}
|
|
1499
|
+
function parseAllOpenCodeSessions(repoPath, repoId = deriveRepoId(repoPath), dbPath = opencodeDbPath()) {
|
|
1500
|
+
if (!existsSync3(dbPath)) return [];
|
|
1501
|
+
const sessions = sqliteJson(
|
|
1502
|
+
dbPath,
|
|
1503
|
+
`SELECT s.id, s.parent_id, s.directory, s.title, s.agent, s.model, s.time_created, s.time_updated, p.worktree
|
|
1504
|
+
FROM session s LEFT JOIN project p ON p.id = s.project_id
|
|
1505
|
+
WHERE s.time_archived IS NULL`
|
|
1506
|
+
).filter((s) => workspaceMatchesRepo(s.directory, repoPath) || workspaceMatchesRepo(s.worktree, repoPath));
|
|
1507
|
+
const out = [];
|
|
1508
|
+
for (const s of sessions) {
|
|
1509
|
+
const messages = sqliteJson(
|
|
1510
|
+
dbPath,
|
|
1511
|
+
`SELECT id, session_id, time_created, data FROM message WHERE session_id = ${q(s.id)} ORDER BY time_created, id`
|
|
1512
|
+
);
|
|
1513
|
+
const parts = sqliteJson(
|
|
1514
|
+
dbPath,
|
|
1515
|
+
`SELECT id, message_id, time_created, data FROM part WHERE session_id = ${q(s.id)} ORDER BY time_created, id`
|
|
1516
|
+
);
|
|
1517
|
+
const parsed = buildOpenCodeSession(s, messages, parts, repoId, `${dbPath}#${s.id}`);
|
|
1518
|
+
if (parsed) out.push(parsed);
|
|
1519
|
+
}
|
|
1520
|
+
return out;
|
|
1521
|
+
}
|
|
1522
|
+
var MAX_OUTPUT_CHARS;
|
|
1523
|
+
var init_opencode_sessions = __esm({
|
|
1524
|
+
"../../packages/ingest-core/src/opencode-sessions.ts"() {
|
|
1525
|
+
"use strict";
|
|
1526
|
+
init_claude_sessions();
|
|
1527
|
+
init_cursor_sessions();
|
|
1528
|
+
init_derive_uuid();
|
|
1529
|
+
init_git_history();
|
|
1530
|
+
init_redact();
|
|
1531
|
+
init_subject_linking();
|
|
1532
|
+
init_types();
|
|
1533
|
+
MAX_OUTPUT_CHARS = 2e3;
|
|
1534
|
+
}
|
|
1535
|
+
});
|
|
1536
|
+
|
|
1537
|
+
// ../../packages/ingest-core/src/copilot-sessions.ts
|
|
1538
|
+
import { existsSync as existsSync4, readFileSync as readFileSync2, readdirSync as readdirSync2, statSync as statSync3 } from "node:fs";
|
|
1539
|
+
import { homedir as homedir4 } from "node:os";
|
|
1540
|
+
import { join as join4 } from "node:path";
|
|
1541
|
+
function copilotSessionsDir(home = homedir4()) {
|
|
1542
|
+
return join4(home, ".copilot", "session-state");
|
|
1543
|
+
}
|
|
1544
|
+
function parseCopilotWorkspace(text) {
|
|
1545
|
+
const map = /* @__PURE__ */ new Map();
|
|
1546
|
+
for (const line of text.split("\n")) {
|
|
1547
|
+
const m = /^([a-z_]+):\s*(.*)$/.exec(line);
|
|
1548
|
+
if (!m) continue;
|
|
1549
|
+
let value = m[2].trim();
|
|
1550
|
+
if (value.length >= 2 && value.startsWith("'") && value.endsWith("'")) {
|
|
1551
|
+
value = value.slice(1, -1).replace(/''/g, "'");
|
|
1552
|
+
} else if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
|
|
1553
|
+
try {
|
|
1554
|
+
value = JSON.parse(value);
|
|
1555
|
+
} catch {
|
|
1556
|
+
value = value.slice(1, -1);
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
map.set(m[1], value);
|
|
1560
|
+
}
|
|
1561
|
+
const get2 = (k) => {
|
|
1562
|
+
const v = map.get(k);
|
|
1563
|
+
return v === void 0 || v === "" || v === "null" ? null : v;
|
|
1564
|
+
};
|
|
1565
|
+
return {
|
|
1566
|
+
id: get2("id"),
|
|
1567
|
+
cwd: get2("cwd"),
|
|
1568
|
+
gitRoot: get2("git_root"),
|
|
1569
|
+
branch: get2("branch"),
|
|
1570
|
+
name: get2("name"),
|
|
1571
|
+
createdAt: get2("created_at"),
|
|
1572
|
+
updatedAt: get2("updated_at")
|
|
1573
|
+
};
|
|
1574
|
+
}
|
|
1575
|
+
function findCopilotSessionDirs(root = copilotSessionsDir()) {
|
|
1576
|
+
if (!existsSync4(root)) return [];
|
|
1577
|
+
return readdirSync2(root).map((name) => join4(root, name)).filter((dir) => {
|
|
1578
|
+
try {
|
|
1579
|
+
return statSync3(dir).isDirectory() && existsSync4(join4(dir, "events.jsonl"));
|
|
1580
|
+
} catch {
|
|
1581
|
+
return false;
|
|
1582
|
+
}
|
|
1583
|
+
}).sort((a, b) => statSync3(b).mtimeMs - statSync3(a).mtimeMs);
|
|
1584
|
+
}
|
|
1585
|
+
function deriveTurnId2(key) {
|
|
1586
|
+
return deriveUuid(`evrex-copilot-turn ${key}`);
|
|
1587
|
+
}
|
|
1588
|
+
function filesFromTool3(tool, args) {
|
|
1589
|
+
const out = [];
|
|
1590
|
+
for (const key of ["path", "filePath", "file"]) {
|
|
1591
|
+
const v = args[key];
|
|
1592
|
+
if (typeof v === "string" && v.length > 0) out.push({ path: v, source: "tool_path", tool });
|
|
1593
|
+
}
|
|
1594
|
+
if (tool === "bash" && typeof args.command === "string") {
|
|
1595
|
+
for (const p of extractPathsFromText(args.command)) out.push({ path: p, source: "tool_bash", tool });
|
|
1596
|
+
}
|
|
1597
|
+
return out;
|
|
1598
|
+
}
|
|
1599
|
+
function shutdownUsage(data, model) {
|
|
1600
|
+
const details = data.tokenDetails;
|
|
1601
|
+
if (!details) return { ...EMPTY_USAGE, model };
|
|
1602
|
+
const n = (k) => typeof details[k]?.tokenCount === "number" ? details[k].tokenCount : null;
|
|
1603
|
+
return {
|
|
1604
|
+
inputTokens: n("input"),
|
|
1605
|
+
outputTokens: n("output"),
|
|
1606
|
+
cacheReadTokens: n("cache_read"),
|
|
1607
|
+
cacheWriteTokens: n("cache_write"),
|
|
1608
|
+
model
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
function buildCopilotSession(eventsText, workspace, repoId, sourceFile) {
|
|
1612
|
+
const events = [];
|
|
1613
|
+
const archived = [];
|
|
1614
|
+
for (const line of eventsText.split("\n")) {
|
|
1615
|
+
if (!line.trim()) continue;
|
|
1616
|
+
let e;
|
|
1617
|
+
try {
|
|
1618
|
+
e = JSON.parse(line);
|
|
1619
|
+
} catch {
|
|
1620
|
+
continue;
|
|
1621
|
+
}
|
|
1622
|
+
events.push(e);
|
|
1623
|
+
if (e.data && Object.keys(e.data).some((k) => REASONING_KEYS.has(k))) {
|
|
1624
|
+
const data = Object.fromEntries(Object.entries(e.data).filter(([k]) => !REASONING_KEYS.has(k)));
|
|
1625
|
+
archived.push(JSON.stringify({ ...e, data }));
|
|
1626
|
+
} else {
|
|
1627
|
+
archived.push(line);
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
const start = events.find((e) => e.type === "session.start");
|
|
1631
|
+
const startData = start?.data ?? {};
|
|
1632
|
+
const sessionId = workspace.id ?? startData.sessionId ?? null;
|
|
1633
|
+
if (!sessionId) return null;
|
|
1634
|
+
const cwd = workspace.cwd ?? startData.context?.cwd ?? null;
|
|
1635
|
+
if (!cwd) return null;
|
|
1636
|
+
const turns = [];
|
|
1637
|
+
let redactionCount = 0;
|
|
1638
|
+
let previous = null;
|
|
1639
|
+
const subjects = [];
|
|
1640
|
+
let lastModel = null;
|
|
1641
|
+
const push = (turn) => {
|
|
1642
|
+
const redacted = redactSecrets(turn.text);
|
|
1643
|
+
redactionCount += redacted.count;
|
|
1644
|
+
turns.push({
|
|
1645
|
+
...turn,
|
|
1646
|
+
text: redacted.text,
|
|
1647
|
+
sessionId,
|
|
1648
|
+
parentUuid: previous,
|
|
1649
|
+
isSidechain: false,
|
|
1650
|
+
redacted: redacted.count > 0,
|
|
1651
|
+
editedLines: []
|
|
1652
|
+
});
|
|
1653
|
+
previous = turn.id;
|
|
1654
|
+
};
|
|
1655
|
+
const toolNames = /* @__PURE__ */ new Map();
|
|
1656
|
+
for (const e of events) {
|
|
1657
|
+
const d = e.data ?? {};
|
|
1658
|
+
const ts = e.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
|
|
1659
|
+
const id = e.id ?? deriveTurnId2(`${sessionId}:${turns.length}`);
|
|
1660
|
+
switch (e.type) {
|
|
1661
|
+
case "user.message": {
|
|
1662
|
+
const text = typeof d.content === "string" ? d.content.trim() : "";
|
|
1663
|
+
if (!text) break;
|
|
1664
|
+
push({
|
|
1665
|
+
id,
|
|
1666
|
+
role: "user",
|
|
1667
|
+
ts,
|
|
1668
|
+
text,
|
|
1669
|
+
filesTouched: extractPathsFromText(text).map((path) => ({ path, source: "prose" })),
|
|
1670
|
+
usage: EMPTY_USAGE,
|
|
1671
|
+
isSyntheticInput: false,
|
|
1672
|
+
isToolError: false
|
|
1673
|
+
});
|
|
1674
|
+
break;
|
|
1675
|
+
}
|
|
1676
|
+
case "assistant.message": {
|
|
1677
|
+
if (typeof d.model === "string") lastModel = d.model;
|
|
1678
|
+
const text = typeof d.content === "string" ? d.content.trim() : "";
|
|
1679
|
+
if (!text) break;
|
|
1680
|
+
push({ id, role: "assistant", ts, text, filesTouched: [], usage: EMPTY_USAGE, isSyntheticInput: false, isToolError: false });
|
|
1681
|
+
break;
|
|
1682
|
+
}
|
|
1683
|
+
case "tool.execution_start": {
|
|
1684
|
+
const tool = typeof d.toolName === "string" ? d.toolName : "tool";
|
|
1685
|
+
const callId = typeof d.toolCallId === "string" ? d.toolCallId : id;
|
|
1686
|
+
const args = d.arguments && typeof d.arguments === "object" ? d.arguments : {};
|
|
1687
|
+
toolNames.set(callId, tool);
|
|
1688
|
+
if (tool === "bash" && typeof args.command === "string") subjects.push(...committedSubjects(args.command));
|
|
1689
|
+
const rendered = JSON.stringify(args);
|
|
1690
|
+
push({
|
|
1691
|
+
id: deriveTurnId2(`${callId}:call`),
|
|
1692
|
+
role: "assistant",
|
|
1693
|
+
ts,
|
|
1694
|
+
text: `[tool: ${tool}] ${rendered.length > 600 ? `${rendered.slice(0, 600)}\u2026` : rendered}`,
|
|
1695
|
+
filesTouched: filesFromTool3(tool, args),
|
|
1696
|
+
usage: EMPTY_USAGE,
|
|
1697
|
+
isSyntheticInput: false,
|
|
1698
|
+
isToolError: false
|
|
1699
|
+
});
|
|
1700
|
+
break;
|
|
1701
|
+
}
|
|
1702
|
+
case "tool.execution_complete": {
|
|
1703
|
+
const callId = typeof d.toolCallId === "string" ? d.toolCallId : id;
|
|
1704
|
+
const failed = d.success === false;
|
|
1705
|
+
const result = d.result ?? {};
|
|
1706
|
+
const error = d.error ?? {};
|
|
1707
|
+
const output = (failed ? error.message ?? result.content ?? "Tool call failed" : result.content ?? "").trim();
|
|
1708
|
+
push({
|
|
1709
|
+
id: deriveTurnId2(`${callId}:result`),
|
|
1710
|
+
role: "user",
|
|
1711
|
+
ts,
|
|
1712
|
+
text: output.length > MAX_OUTPUT_CHARS2 ? `${output.slice(0, MAX_OUTPUT_CHARS2)}\u2026` : output,
|
|
1713
|
+
filesTouched: [],
|
|
1714
|
+
usage: EMPTY_USAGE,
|
|
1715
|
+
isSyntheticInput: true,
|
|
1716
|
+
isToolError: failed
|
|
1717
|
+
});
|
|
1718
|
+
break;
|
|
1719
|
+
}
|
|
1720
|
+
case "session.shutdown": {
|
|
1721
|
+
const usage = shutdownUsage(d, typeof d.currentModel === "string" ? d.currentModel : lastModel);
|
|
1722
|
+
for (let i = turns.length - 1; i >= 0; i--) {
|
|
1723
|
+
if (turns[i].role === "assistant") {
|
|
1724
|
+
turns[i].usage = usage;
|
|
1725
|
+
break;
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
break;
|
|
1729
|
+
}
|
|
1730
|
+
default:
|
|
1731
|
+
break;
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
if (turns.length === 0) return null;
|
|
1735
|
+
const first = turns[0].ts;
|
|
1736
|
+
const last = turns[turns.length - 1].ts;
|
|
1737
|
+
return {
|
|
1738
|
+
id: sessionId,
|
|
1739
|
+
agentKind: "copilot",
|
|
1740
|
+
repoId,
|
|
1741
|
+
cwd,
|
|
1742
|
+
startedAt: workspace.createdAt ?? startData.startTime ?? first,
|
|
1743
|
+
endedAt: events[events.length - 1]?.timestamp ?? workspace.updatedAt ?? last,
|
|
1744
|
+
turnCount: turns.length,
|
|
1745
|
+
aiTitle: workspace.name,
|
|
1746
|
+
author: null,
|
|
1747
|
+
sourceFile,
|
|
1748
|
+
redactionCount,
|
|
1749
|
+
committedSubjects: [...new Set(subjects)],
|
|
1750
|
+
parentSessionId: null,
|
|
1751
|
+
subagent: null,
|
|
1752
|
+
branch: workspace.branch,
|
|
1753
|
+
rawContent: archived.join("\n"),
|
|
1754
|
+
rawFormat: "jsonl",
|
|
1755
|
+
turns
|
|
1756
|
+
};
|
|
1757
|
+
}
|
|
1758
|
+
function parseCopilotSessionDir(dir, repoId) {
|
|
1759
|
+
const eventsPath = join4(dir, "events.jsonl");
|
|
1760
|
+
if (!existsSync4(eventsPath)) return null;
|
|
1761
|
+
const workspacePath = join4(dir, "workspace.yaml");
|
|
1762
|
+
const workspace = existsSync4(workspacePath) ? parseCopilotWorkspace(readFileSync2(workspacePath, "utf-8")) : parseCopilotWorkspace("");
|
|
1763
|
+
return buildCopilotSession(readFileSync2(eventsPath, "utf-8"), workspace, repoId, eventsPath);
|
|
1764
|
+
}
|
|
1765
|
+
function parseAllCopilotSessions(repoPath, repoId = deriveRepoId(repoPath), root = copilotSessionsDir()) {
|
|
1766
|
+
const out = [];
|
|
1767
|
+
for (const dir of findCopilotSessionDirs(root)) {
|
|
1768
|
+
const workspacePath = join4(dir, "workspace.yaml");
|
|
1769
|
+
const workspace = existsSync4(workspacePath) ? parseCopilotWorkspace(readFileSync2(workspacePath, "utf-8")) : parseCopilotWorkspace("");
|
|
1770
|
+
const known = workspace.gitRoot ?? workspace.cwd;
|
|
1771
|
+
if (known && !workspaceMatchesRepo(known, repoPath)) continue;
|
|
1772
|
+
const parsed = buildCopilotSession(readFileSync2(join4(dir, "events.jsonl"), "utf-8"), workspace, repoId, join4(dir, "events.jsonl"));
|
|
1773
|
+
if (!parsed) continue;
|
|
1774
|
+
if (!known && !workspaceMatchesRepo(parsed.cwd, repoPath)) continue;
|
|
1775
|
+
out.push(parsed);
|
|
1776
|
+
}
|
|
1777
|
+
return out;
|
|
1778
|
+
}
|
|
1779
|
+
var MAX_OUTPUT_CHARS2, REASONING_KEYS;
|
|
1780
|
+
var init_copilot_sessions = __esm({
|
|
1781
|
+
"../../packages/ingest-core/src/copilot-sessions.ts"() {
|
|
1782
|
+
"use strict";
|
|
1783
|
+
init_claude_sessions();
|
|
1784
|
+
init_cursor_sessions();
|
|
1785
|
+
init_derive_uuid();
|
|
1786
|
+
init_git_history();
|
|
1787
|
+
init_redact();
|
|
1788
|
+
init_subject_linking();
|
|
1789
|
+
init_types();
|
|
1790
|
+
MAX_OUTPUT_CHARS2 = 2e3;
|
|
1791
|
+
REASONING_KEYS = /* @__PURE__ */ new Set(["reasoningOpaque", "reasoningText", "reasoningBlocks"]);
|
|
1792
|
+
}
|
|
1793
|
+
});
|
|
1794
|
+
|
|
1795
|
+
// ../../packages/ingest-core/src/codex-sessions.ts
|
|
1796
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
1797
|
+
import { existsSync as existsSync5, readFileSync as readFileSync3, readdirSync as readdirSync3, statSync as statSync4 } from "node:fs";
|
|
1798
|
+
import { homedir as homedir5 } from "node:os";
|
|
1799
|
+
import { join as join5 } from "node:path";
|
|
1224
1800
|
function codexSessionsDir() {
|
|
1225
|
-
return
|
|
1801
|
+
return join5(homedir5(), CODEX_DIR, "sessions");
|
|
1226
1802
|
}
|
|
1227
1803
|
function deriveUuid2(name) {
|
|
1228
1804
|
const h = createHash2("sha1").update(name).digest("hex");
|
|
@@ -1238,15 +1814,15 @@ function findCodexSessionFiles(root = codexSessionsDir()) {
|
|
|
1238
1814
|
const walk = (dir) => {
|
|
1239
1815
|
let entries;
|
|
1240
1816
|
try {
|
|
1241
|
-
entries =
|
|
1817
|
+
entries = readdirSync3(dir);
|
|
1242
1818
|
} catch {
|
|
1243
1819
|
return;
|
|
1244
1820
|
}
|
|
1245
1821
|
for (const entry of entries) {
|
|
1246
|
-
const full =
|
|
1822
|
+
const full = join5(dir, entry);
|
|
1247
1823
|
let isDir = false;
|
|
1248
1824
|
try {
|
|
1249
|
-
isDir =
|
|
1825
|
+
isDir = statSync4(full).isDirectory();
|
|
1250
1826
|
} catch {
|
|
1251
1827
|
continue;
|
|
1252
1828
|
}
|
|
@@ -1254,7 +1830,7 @@ function findCodexSessionFiles(root = codexSessionsDir()) {
|
|
|
1254
1830
|
else if (entry.endsWith(".jsonl")) found.push(full);
|
|
1255
1831
|
}
|
|
1256
1832
|
};
|
|
1257
|
-
if (
|
|
1833
|
+
if (existsSync5(root)) walk(root);
|
|
1258
1834
|
return found.sort();
|
|
1259
1835
|
}
|
|
1260
1836
|
function parseLines(raw) {
|
|
@@ -1359,7 +1935,7 @@ function filesFromToolCalls(lines) {
|
|
|
1359
1935
|
function parseCodexSessionFile(filePath, repoPath, repoId) {
|
|
1360
1936
|
let raw;
|
|
1361
1937
|
try {
|
|
1362
|
-
raw =
|
|
1938
|
+
raw = readFileSync3(filePath, "utf-8");
|
|
1363
1939
|
} catch {
|
|
1364
1940
|
return null;
|
|
1365
1941
|
}
|
|
@@ -1392,6 +1968,7 @@ function parseCodexSessionFile(filePath, repoPath, repoId) {
|
|
|
1392
1968
|
isSidechain: false,
|
|
1393
1969
|
redacted: count > 0,
|
|
1394
1970
|
isSyntheticInput: false,
|
|
1971
|
+
isToolError: false,
|
|
1395
1972
|
usage: event.usage
|
|
1396
1973
|
};
|
|
1397
1974
|
});
|
|
@@ -1401,6 +1978,9 @@ function parseCodexSessionFile(filePath, repoPath, repoId) {
|
|
|
1401
1978
|
return {
|
|
1402
1979
|
id: sessionId,
|
|
1403
1980
|
agentKind: "codex",
|
|
1981
|
+
branch: null,
|
|
1982
|
+
parentSessionId: null,
|
|
1983
|
+
subagent: null,
|
|
1404
1984
|
// Codex records the remote itself, so identity survives a moved or deleted
|
|
1405
1985
|
// checkout. Falls back to the caller's derivation when it is absent.
|
|
1406
1986
|
repoId: repoIdFromMeta(meta) ?? repoId,
|
|
@@ -1419,9 +1999,9 @@ function parseCodexSessionFile(filePath, repoPath, repoId) {
|
|
|
1419
1999
|
};
|
|
1420
2000
|
}
|
|
1421
2001
|
function repoIdFromMeta(meta) {
|
|
1422
|
-
const
|
|
1423
|
-
if (typeof
|
|
1424
|
-
const url = asString(
|
|
2002
|
+
const git3 = meta.git;
|
|
2003
|
+
if (typeof git3 !== "object" || git3 === null) return null;
|
|
2004
|
+
const url = asString(git3.repository_url);
|
|
1425
2005
|
if (!url) return null;
|
|
1426
2006
|
const normalized = normalizeRepoRemote(url);
|
|
1427
2007
|
return normalized ? `remote:${normalized}` : null;
|
|
@@ -1444,9 +2024,20 @@ function parseAllCodexSessions(repoPath, repoId) {
|
|
|
1444
2024
|
}
|
|
1445
2025
|
return sessions;
|
|
1446
2026
|
}
|
|
2027
|
+
var CODEX_DIR;
|
|
2028
|
+
var init_codex_sessions = __esm({
|
|
2029
|
+
"../../packages/ingest-core/src/codex-sessions.ts"() {
|
|
2030
|
+
"use strict";
|
|
2031
|
+
init_claude_sessions();
|
|
2032
|
+
init_git_history();
|
|
2033
|
+
init_redact();
|
|
2034
|
+
init_subject_linking();
|
|
2035
|
+
init_types();
|
|
2036
|
+
CODEX_DIR = ".codex";
|
|
2037
|
+
}
|
|
2038
|
+
});
|
|
1447
2039
|
|
|
1448
2040
|
// ../../packages/ingest-core/src/sanitize.ts
|
|
1449
|
-
var NUL = String.fromCharCode(0);
|
|
1450
2041
|
function stripNulls(value) {
|
|
1451
2042
|
return value.includes(NUL) ? value.split(NUL).join("") : value;
|
|
1452
2043
|
}
|
|
@@ -1463,8 +2054,993 @@ function stripNullsDeep(value) {
|
|
|
1463
2054
|
}
|
|
1464
2055
|
return value;
|
|
1465
2056
|
}
|
|
2057
|
+
var NUL;
|
|
2058
|
+
var init_sanitize = __esm({
|
|
2059
|
+
"../../packages/ingest-core/src/sanitize.ts"() {
|
|
2060
|
+
"use strict";
|
|
2061
|
+
NUL = String.fromCharCode(0);
|
|
2062
|
+
}
|
|
2063
|
+
});
|
|
2064
|
+
|
|
2065
|
+
// ../../packages/ingest-core/src/tickets.ts
|
|
2066
|
+
function deriveTicketId(args) {
|
|
2067
|
+
return deriveUuid(
|
|
2068
|
+
`evrex-ticket ${args.orgId} ${args.kind} ${args.workspace} ${args.externalId}`
|
|
2069
|
+
);
|
|
2070
|
+
}
|
|
2071
|
+
function ticketReferencesIn(text) {
|
|
2072
|
+
return [...new Set([...text.matchAll(TICKET_REFERENCE)].map((m) => m[0]))];
|
|
2073
|
+
}
|
|
2074
|
+
var TICKET_REFERENCE;
|
|
2075
|
+
var init_tickets = __esm({
|
|
2076
|
+
"../../packages/ingest-core/src/tickets.ts"() {
|
|
2077
|
+
"use strict";
|
|
2078
|
+
init_derive_uuid();
|
|
2079
|
+
TICKET_REFERENCE = /(?<![A-Za-z0-9_-])([A-Z][A-Z0-9]{1,9})-(\d{1,6})(?![A-Za-z0-9_-])/g;
|
|
2080
|
+
}
|
|
2081
|
+
});
|
|
2082
|
+
|
|
2083
|
+
// ../../packages/ingest-core/src/linear.ts
|
|
2084
|
+
function linearIssueToTicket(issue) {
|
|
2085
|
+
if (!issue.identifier) return null;
|
|
2086
|
+
const attributes = {
|
|
2087
|
+
title: issue.title ?? issue.identifier,
|
|
2088
|
+
status: issue.state?.name ?? null,
|
|
2089
|
+
statusCategory: issue.state?.type ? STATE_TYPE[issue.state.type] ?? null : null,
|
|
2090
|
+
assignee: issue.assignee?.displayName ?? null,
|
|
2091
|
+
assigneeId: issue.assignee?.id ?? null,
|
|
2092
|
+
team: issue.team?.key ?? null,
|
|
2093
|
+
project: issue.project?.name ?? null,
|
|
2094
|
+
// Linear has one kind of issue; the field exists so a Jira row and a
|
|
2095
|
+
// Linear row are the same shape, which is what U5 exists to prove.
|
|
2096
|
+
issueType: null,
|
|
2097
|
+
// Null rather than "No priority" when the field is absent: unset and
|
|
2098
|
+
// explicitly-not-prioritised are different claims.
|
|
2099
|
+
priority: typeof issue.priority === "number" ? PRIORITY[issue.priority] ?? null : null,
|
|
2100
|
+
// Linear records completion as a state, not as a separate resolution.
|
|
2101
|
+
resolution: null
|
|
2102
|
+
};
|
|
2103
|
+
return {
|
|
2104
|
+
externalId: issue.identifier,
|
|
2105
|
+
kind: "linear",
|
|
2106
|
+
url: issue.url ?? `https://linear.app/issue/${issue.identifier}`,
|
|
2107
|
+
createdAt: issue.createdAt ?? null,
|
|
2108
|
+
updatedAt: issue.updatedAt ?? null,
|
|
2109
|
+
attributes
|
|
2110
|
+
};
|
|
2111
|
+
}
|
|
2112
|
+
function linearAuthHeader(auth) {
|
|
2113
|
+
if (auth.accessToken) return `Bearer ${auth.accessToken}`;
|
|
2114
|
+
if (auth.apiKey) return auth.apiKey;
|
|
2115
|
+
throw new Error(
|
|
2116
|
+
"Linear needs either an API key or an OAuth access token; got neither."
|
|
2117
|
+
);
|
|
2118
|
+
}
|
|
2119
|
+
async function fetchLinearIssues(options) {
|
|
2120
|
+
const call2 = options.fetch ?? globalThis.fetch;
|
|
2121
|
+
const pageSize = options.pageSize ?? 50;
|
|
2122
|
+
const limit = options.limit ?? Infinity;
|
|
2123
|
+
const tickets = [];
|
|
2124
|
+
let after = null;
|
|
2125
|
+
while (tickets.length < limit) {
|
|
2126
|
+
const response = await call2(LINEAR_GRAPHQL_URL, {
|
|
2127
|
+
method: "POST",
|
|
2128
|
+
headers: {
|
|
2129
|
+
Authorization: linearAuthHeader(options.auth),
|
|
2130
|
+
"Content-Type": "application/json"
|
|
2131
|
+
},
|
|
2132
|
+
body: JSON.stringify({
|
|
2133
|
+
query: LINEAR_ISSUES_QUERY,
|
|
2134
|
+
variables: {
|
|
2135
|
+
first: Math.min(pageSize, limit - tickets.length),
|
|
2136
|
+
after
|
|
2137
|
+
}
|
|
2138
|
+
})
|
|
2139
|
+
});
|
|
2140
|
+
if (!response.ok) {
|
|
2141
|
+
throw new Error(
|
|
2142
|
+
`Linear returned ${response.status} ${response.statusText}`
|
|
2143
|
+
);
|
|
2144
|
+
}
|
|
2145
|
+
const body = await response.json();
|
|
2146
|
+
if (body.errors?.length) {
|
|
2147
|
+
throw new Error(
|
|
2148
|
+
`Linear rejected the query: ${body.errors.map((e) => e.message).join("; ")}`
|
|
2149
|
+
);
|
|
2150
|
+
}
|
|
2151
|
+
const page = body.data?.issues;
|
|
2152
|
+
for (const node of page?.nodes ?? []) {
|
|
2153
|
+
const ticket = linearIssueToTicket(node);
|
|
2154
|
+
if (ticket) tickets.push(ticket);
|
|
2155
|
+
}
|
|
2156
|
+
if (!page?.pageInfo?.hasNextPage || !page.pageInfo.endCursor) break;
|
|
2157
|
+
after = page.pageInfo.endCursor;
|
|
2158
|
+
}
|
|
2159
|
+
return tickets;
|
|
2160
|
+
}
|
|
2161
|
+
var LINEAR_GRAPHQL_URL, LINEAR_AUTHORIZE_URL, LINEAR_TOKEN_URL, LINEAR_READ_SCOPE, PRIORITY, STATE_TYPE, LINEAR_ISSUES_QUERY;
|
|
2162
|
+
var init_linear = __esm({
|
|
2163
|
+
"../../packages/ingest-core/src/linear.ts"() {
|
|
2164
|
+
"use strict";
|
|
2165
|
+
LINEAR_GRAPHQL_URL = "https://api.linear.app/graphql";
|
|
2166
|
+
LINEAR_AUTHORIZE_URL = "https://linear.app/oauth/authorize";
|
|
2167
|
+
LINEAR_TOKEN_URL = "https://api.linear.app/oauth/token";
|
|
2168
|
+
LINEAR_READ_SCOPE = "read";
|
|
2169
|
+
PRIORITY = {
|
|
2170
|
+
0: "No priority",
|
|
2171
|
+
1: "Urgent",
|
|
2172
|
+
2: "High",
|
|
2173
|
+
3: "Medium",
|
|
2174
|
+
4: "Low"
|
|
2175
|
+
};
|
|
2176
|
+
STATE_TYPE = {
|
|
2177
|
+
triage: "triage",
|
|
2178
|
+
backlog: "backlog",
|
|
2179
|
+
unstarted: "todo",
|
|
2180
|
+
started: "in-progress",
|
|
2181
|
+
completed: "done",
|
|
2182
|
+
canceled: "cancelled"
|
|
2183
|
+
};
|
|
2184
|
+
LINEAR_ISSUES_QUERY = `
|
|
2185
|
+
query EvrexIssues($first: Int!, $after: String) {
|
|
2186
|
+
issues(first: $first, after: $after, orderBy: updatedAt) {
|
|
2187
|
+
nodes {
|
|
2188
|
+
identifier
|
|
2189
|
+
title
|
|
2190
|
+
url
|
|
2191
|
+
priority
|
|
2192
|
+
createdAt
|
|
2193
|
+
updatedAt
|
|
2194
|
+
state { name type }
|
|
2195
|
+
assignee { id displayName }
|
|
2196
|
+
team { key name }
|
|
2197
|
+
project { name }
|
|
2198
|
+
}
|
|
2199
|
+
pageInfo { hasNextPage endCursor }
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
`;
|
|
2203
|
+
}
|
|
2204
|
+
});
|
|
2205
|
+
|
|
2206
|
+
// ../../packages/ingest-core/src/jira.ts
|
|
2207
|
+
function jiraIssueToTicket(issue, siteUrl) {
|
|
2208
|
+
if (!issue.key) return null;
|
|
2209
|
+
const f = issue.fields ?? {};
|
|
2210
|
+
const attributes = {
|
|
2211
|
+
title: f.summary ?? issue.key,
|
|
2212
|
+
status: f.status?.name ?? null,
|
|
2213
|
+
statusCategory: f.status?.statusCategory?.key ? STATUS_CATEGORY[f.status.statusCategory.key] ?? null : null,
|
|
2214
|
+
// Null when the assignee's privacy settings withhold it, which is not the
|
|
2215
|
+
// same claim as an unassigned ticket — `assigneeId` distinguishes them.
|
|
2216
|
+
assignee: f.assignee?.displayName ?? null,
|
|
2217
|
+
assigneeId: f.assignee?.accountId ?? null,
|
|
2218
|
+
// Jira has no team on an issue; the project is the closest equivalent and
|
|
2219
|
+
// is reported as itself rather than smuggled into `team`.
|
|
2220
|
+
team: null,
|
|
2221
|
+
project: f.project?.key ?? null,
|
|
2222
|
+
issueType: f.issuetype?.name ?? null,
|
|
2223
|
+
priority: f.priority?.name ?? null,
|
|
2224
|
+
resolution: f.resolution?.name ?? null
|
|
2225
|
+
};
|
|
2226
|
+
return {
|
|
2227
|
+
externalId: issue.key,
|
|
2228
|
+
kind: "jira",
|
|
2229
|
+
url: `${siteUrl.replace(/\/+$/, "")}/browse/${issue.key}`,
|
|
2230
|
+
createdAt: f.created ?? null,
|
|
2231
|
+
updatedAt: f.updated ?? null,
|
|
2232
|
+
attributes
|
|
2233
|
+
};
|
|
2234
|
+
}
|
|
2235
|
+
function jiraBasicAuth(email, apiToken) {
|
|
2236
|
+
return `Basic ${Buffer.from(`${email}:${apiToken}`).toString("base64")}`;
|
|
2237
|
+
}
|
|
2238
|
+
async function fetchJiraIssues(options) {
|
|
2239
|
+
const call2 = options.fetch ?? globalThis.fetch;
|
|
2240
|
+
const site = options.siteUrl.replace(/\/+$/, "");
|
|
2241
|
+
const pageSize = options.pageSize ?? 100;
|
|
2242
|
+
const limit = options.limit ?? Infinity;
|
|
2243
|
+
const jql = options.jql ?? "ORDER BY updated DESC";
|
|
2244
|
+
const tickets = [];
|
|
2245
|
+
let nextPageToken = null;
|
|
2246
|
+
while (tickets.length < limit) {
|
|
2247
|
+
const response = await call2(`${site}${JIRA_SEARCH_PATH}`, {
|
|
2248
|
+
method: "POST",
|
|
2249
|
+
headers: {
|
|
2250
|
+
Authorization: jiraBasicAuth(options.email, options.apiToken),
|
|
2251
|
+
"Content-Type": "application/json",
|
|
2252
|
+
Accept: "application/json"
|
|
2253
|
+
},
|
|
2254
|
+
body: JSON.stringify({
|
|
2255
|
+
jql,
|
|
2256
|
+
fields: [...JIRA_FIELDS],
|
|
2257
|
+
maxResults: Math.min(pageSize, limit - tickets.length),
|
|
2258
|
+
...nextPageToken ? { nextPageToken } : {}
|
|
2259
|
+
})
|
|
2260
|
+
});
|
|
2261
|
+
if (!response.ok) {
|
|
2262
|
+
const hint = response.status === 410 ? ` \u2014 that is what the removed /rest/api/3/search returns; this client uses ${JIRA_SEARCH_PATH}` : "";
|
|
2263
|
+
throw new Error(
|
|
2264
|
+
`Jira returned ${response.status} ${response.statusText}${hint}`
|
|
2265
|
+
);
|
|
2266
|
+
}
|
|
2267
|
+
const body = await response.json();
|
|
2268
|
+
for (const issue of body.issues ?? []) {
|
|
2269
|
+
const ticket = jiraIssueToTicket(issue, site);
|
|
2270
|
+
if (ticket) tickets.push(ticket);
|
|
2271
|
+
}
|
|
2272
|
+
if (!body.nextPageToken) break;
|
|
2273
|
+
nextPageToken = body.nextPageToken;
|
|
2274
|
+
}
|
|
2275
|
+
return tickets;
|
|
2276
|
+
}
|
|
2277
|
+
var JIRA_SEARCH_PATH, JIRA_FIELDS, STATUS_CATEGORY;
|
|
2278
|
+
var init_jira = __esm({
|
|
2279
|
+
"../../packages/ingest-core/src/jira.ts"() {
|
|
2280
|
+
"use strict";
|
|
2281
|
+
JIRA_SEARCH_PATH = "/rest/api/3/search/jql";
|
|
2282
|
+
JIRA_FIELDS = [
|
|
2283
|
+
"summary",
|
|
2284
|
+
"status",
|
|
2285
|
+
"assignee",
|
|
2286
|
+
"resolution",
|
|
2287
|
+
"created",
|
|
2288
|
+
"updated",
|
|
2289
|
+
"project",
|
|
2290
|
+
"issuetype",
|
|
2291
|
+
"priority"
|
|
2292
|
+
];
|
|
2293
|
+
STATUS_CATEGORY = {
|
|
2294
|
+
new: "todo",
|
|
2295
|
+
indeterminate: "in-progress",
|
|
2296
|
+
done: "done"
|
|
2297
|
+
};
|
|
2298
|
+
}
|
|
2299
|
+
});
|
|
2300
|
+
|
|
2301
|
+
// ../../packages/ingest-core/src/diff-parser.ts
|
|
2302
|
+
function parseUnifiedDiffHunks(diffText) {
|
|
2303
|
+
const hunks = [];
|
|
2304
|
+
let current = null;
|
|
2305
|
+
for (const line of diffText.split("\n")) {
|
|
2306
|
+
if (HUNK_HEADER_RE.test(line)) {
|
|
2307
|
+
current = { header: line, lines: [] };
|
|
2308
|
+
hunks.push(current);
|
|
2309
|
+
continue;
|
|
2310
|
+
}
|
|
2311
|
+
if (!current) continue;
|
|
2312
|
+
if (line.startsWith("+++") || line.startsWith("---")) continue;
|
|
2313
|
+
if (line.startsWith("+")) {
|
|
2314
|
+
current.lines.push({ kind: "add", text: line.slice(1) });
|
|
2315
|
+
} else if (line.startsWith("-")) {
|
|
2316
|
+
current.lines.push({ kind: "del", text: line.slice(1) });
|
|
2317
|
+
} else if (line.startsWith(" ")) {
|
|
2318
|
+
current.lines.push({ kind: "ctx", text: line.slice(1) });
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
return hunks;
|
|
2322
|
+
}
|
|
2323
|
+
var HUNK_HEADER_RE;
|
|
2324
|
+
var init_diff_parser = __esm({
|
|
2325
|
+
"../../packages/ingest-core/src/diff-parser.ts"() {
|
|
2326
|
+
"use strict";
|
|
2327
|
+
HUNK_HEADER_RE = /^@@ .+? @@.*$/;
|
|
2328
|
+
}
|
|
2329
|
+
});
|
|
2330
|
+
|
|
2331
|
+
// ../../packages/ingest-core/src/trace-lines.ts
|
|
2332
|
+
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
2333
|
+
function isHunkHeader(line) {
|
|
2334
|
+
return line.startsWith("@@");
|
|
2335
|
+
}
|
|
2336
|
+
function parseLineLog(raw) {
|
|
2337
|
+
const commits = [];
|
|
2338
|
+
let current = null;
|
|
2339
|
+
let hunk = null;
|
|
2340
|
+
const closeHunk = () => {
|
|
2341
|
+
if (current && hunk) current.hunks.push(hunk);
|
|
2342
|
+
hunk = null;
|
|
2343
|
+
};
|
|
2344
|
+
for (const line of raw.split("\n")) {
|
|
2345
|
+
if (line.startsWith(COMMIT_MARK)) {
|
|
2346
|
+
closeHunk();
|
|
2347
|
+
const [sha, at, author, ...rest] = line.slice(COMMIT_MARK.length).split(FIELD_SEP2);
|
|
2348
|
+
if (!sha || !at) {
|
|
2349
|
+
current = null;
|
|
2350
|
+
continue;
|
|
2351
|
+
}
|
|
2352
|
+
current = {
|
|
2353
|
+
sha,
|
|
2354
|
+
at,
|
|
2355
|
+
author: author ?? "",
|
|
2356
|
+
subject: rest.join(FIELD_SEP2),
|
|
2357
|
+
hunks: [],
|
|
2358
|
+
createdFile: false
|
|
2359
|
+
};
|
|
2360
|
+
commits.push(current);
|
|
2361
|
+
continue;
|
|
2362
|
+
}
|
|
2363
|
+
if (!current) continue;
|
|
2364
|
+
if (line.startsWith("--- ")) {
|
|
2365
|
+
if (line.trim() === "--- /dev/null") current.createdFile = true;
|
|
2366
|
+
continue;
|
|
2367
|
+
}
|
|
2368
|
+
if (line.startsWith("+++ ") || line.startsWith("diff --git ")) continue;
|
|
2369
|
+
if (isHunkHeader(line)) {
|
|
2370
|
+
closeHunk();
|
|
2371
|
+
hunk = { header: line, lines: [] };
|
|
2372
|
+
continue;
|
|
2373
|
+
}
|
|
2374
|
+
if (hunk && (line.startsWith(" ") || line.startsWith("+") || line.startsWith("-"))) {
|
|
2375
|
+
hunk.lines.push(line);
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
closeHunk();
|
|
2379
|
+
return commits;
|
|
2380
|
+
}
|
|
2381
|
+
function detectTruncatedHistory(commits, repoPath) {
|
|
2382
|
+
const oldest = commits[commits.length - 1];
|
|
2383
|
+
if (!oldest) return false;
|
|
2384
|
+
const added = oldest.hunks.flatMap((h) => h.lines).filter((l) => l.startsWith("+")).map((l) => l.slice(1).trim()).filter((l) => l.length >= MOVED_LINE_MIN_LENGTH);
|
|
2385
|
+
const body = oldest.hunks.flatMap((h) => h.lines);
|
|
2386
|
+
if (body.length === 0 || !body.every((l) => l.startsWith("+"))) return false;
|
|
2387
|
+
if (added.length === 0 || !repoPath) return false;
|
|
2388
|
+
const removedElsewhere = deletedLinesElsewhere(repoPath, oldest.sha);
|
|
2389
|
+
if (removedElsewhere.size === 0) return false;
|
|
2390
|
+
const matched = added.filter((l) => removedElsewhere.has(l)).length;
|
|
2391
|
+
return matched / added.length >= MOVED_LINE_FRACTION;
|
|
2392
|
+
}
|
|
2393
|
+
function deletedLinesElsewhere(repoPath, sha) {
|
|
2394
|
+
const out = /* @__PURE__ */ new Set();
|
|
2395
|
+
let raw;
|
|
2396
|
+
try {
|
|
2397
|
+
raw = execFileSync3(
|
|
2398
|
+
"git",
|
|
2399
|
+
[
|
|
2400
|
+
"show",
|
|
2401
|
+
"--format=",
|
|
2402
|
+
"--unified=0",
|
|
2403
|
+
"--no-color",
|
|
2404
|
+
// Without this git collapses a delete-plus-identical-add into
|
|
2405
|
+
// "rename from/to" with no line content at all — so the very case
|
|
2406
|
+
// this function exists to detect would produce nothing to match.
|
|
2407
|
+
"--no-renames",
|
|
2408
|
+
sha
|
|
2409
|
+
],
|
|
2410
|
+
{
|
|
2411
|
+
cwd: repoPath,
|
|
2412
|
+
maxBuffer: 1024 * 1024 * 64,
|
|
2413
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
2414
|
+
}
|
|
2415
|
+
).toString("utf-8");
|
|
2416
|
+
} catch {
|
|
2417
|
+
return out;
|
|
2418
|
+
}
|
|
2419
|
+
for (const line of raw.split("\n")) {
|
|
2420
|
+
if (!line.startsWith("-") || line.startsWith("---")) continue;
|
|
2421
|
+
const text = line.slice(1).trim();
|
|
2422
|
+
if (text.length >= MOVED_LINE_MIN_LENGTH) out.add(text);
|
|
2423
|
+
}
|
|
2424
|
+
return out;
|
|
2425
|
+
}
|
|
2426
|
+
function committedLineCount(repoPath, file, rev = "HEAD") {
|
|
2427
|
+
try {
|
|
2428
|
+
const out = execFileSync3("git", ["show", `${rev}:${file}`], {
|
|
2429
|
+
cwd: repoPath,
|
|
2430
|
+
maxBuffer: 1024 * 1024 * 64,
|
|
2431
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
2432
|
+
}).toString("utf-8");
|
|
2433
|
+
const lines = out.split("\n");
|
|
2434
|
+
const count = lines[lines.length - 1] === "" ? lines.length - 1 : lines.length;
|
|
2435
|
+
return count > 0 ? count : null;
|
|
2436
|
+
} catch {
|
|
2437
|
+
return null;
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
function traceLines(repoPath, file, from, to, rev) {
|
|
2441
|
+
const start = Math.max(MIN_LINE, Math.floor(from));
|
|
2442
|
+
const end = to <= 0 ? committedLineCount(repoPath, file, rev) ?? Math.max(start, 1) : Math.max(start, Math.floor(to));
|
|
2443
|
+
let raw;
|
|
2444
|
+
try {
|
|
2445
|
+
raw = execFileSync3(
|
|
2446
|
+
"git",
|
|
2447
|
+
[
|
|
2448
|
+
"log",
|
|
2449
|
+
// argv array, never a shell string: `file` is caller-supplied and can
|
|
2450
|
+
// legitimately contain spaces, quotes or a leading dash.
|
|
2451
|
+
`-L${start},${end}:${file}`,
|
|
2452
|
+
`--format=${COMMIT_MARK}%H${FIELD_SEP2}%aI${FIELD_SEP2}%an${FIELD_SEP2}%s`,
|
|
2453
|
+
// Anchors the walk at the revision the caller's line numbers came
|
|
2454
|
+
// from. Omitted, git starts at HEAD, which is right for a working-copy
|
|
2455
|
+
// selection and wrong for one taken from an old diff.
|
|
2456
|
+
...rev ? [rev] : []
|
|
2457
|
+
],
|
|
2458
|
+
{
|
|
2459
|
+
cwd: repoPath,
|
|
2460
|
+
maxBuffer: 1024 * 1024 * 64,
|
|
2461
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
2462
|
+
}
|
|
2463
|
+
).toString("utf-8");
|
|
2464
|
+
} catch {
|
|
2465
|
+
return { commits: [], historyMayBeTruncated: false };
|
|
2466
|
+
}
|
|
2467
|
+
const commits = parseLineLog(raw);
|
|
2468
|
+
return {
|
|
2469
|
+
commits,
|
|
2470
|
+
historyMayBeTruncated: detectTruncatedHistory(commits, repoPath)
|
|
2471
|
+
};
|
|
2472
|
+
}
|
|
2473
|
+
var COMMIT_MARK, FIELD_SEP2, MIN_LINE, MOVED_LINE_MIN_LENGTH, MOVED_LINE_FRACTION;
|
|
2474
|
+
var init_trace_lines = __esm({
|
|
2475
|
+
"../../packages/ingest-core/src/trace-lines.ts"() {
|
|
2476
|
+
"use strict";
|
|
2477
|
+
COMMIT_MARK = "@@EVREX-COMMIT@@";
|
|
2478
|
+
FIELD_SEP2 = "";
|
|
2479
|
+
MIN_LINE = 1;
|
|
2480
|
+
MOVED_LINE_MIN_LENGTH = 6;
|
|
2481
|
+
MOVED_LINE_FRACTION = 0.5;
|
|
2482
|
+
}
|
|
2483
|
+
});
|
|
2484
|
+
|
|
2485
|
+
// ../../packages/ingest-core/src/trace-target.ts
|
|
2486
|
+
function parseTraceTarget(raw) {
|
|
2487
|
+
const trimmed = raw.trim();
|
|
2488
|
+
if (!trimmed) return null;
|
|
2489
|
+
const match = /^(.*?):(\d+)(?:\s*[-\u2013:]\s*(\d+))?$/.exec(trimmed);
|
|
2490
|
+
if (!match) return null;
|
|
2491
|
+
const [, file, fromText, toText] = match;
|
|
2492
|
+
if (!file) return null;
|
|
2493
|
+
const from = Number(fromText);
|
|
2494
|
+
const to = toText ? Number(toText) : from;
|
|
2495
|
+
if (!Number.isFinite(from) || from < 1) return null;
|
|
2496
|
+
if (!Number.isFinite(to) || to < 1) return null;
|
|
2497
|
+
return { file, from: Math.min(from, to), to: Math.max(from, to) };
|
|
2498
|
+
}
|
|
2499
|
+
var init_trace_target = __esm({
|
|
2500
|
+
"../../packages/ingest-core/src/trace-target.ts"() {
|
|
2501
|
+
"use strict";
|
|
2502
|
+
}
|
|
2503
|
+
});
|
|
2504
|
+
|
|
2505
|
+
// ../../packages/ingest-core/src/gemini-sessions.ts
|
|
2506
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
2507
|
+
function textOf(content) {
|
|
2508
|
+
if (typeof content === "string") return content;
|
|
2509
|
+
if (!Array.isArray(content)) return "";
|
|
2510
|
+
return content.map(
|
|
2511
|
+
(part) => part && typeof part === "object" && typeof part.text === "string" ? part.text : ""
|
|
2512
|
+
).filter(Boolean).join("\n");
|
|
2513
|
+
}
|
|
2514
|
+
function parseGeminiSessionFile(filePath, repoPath, repoId) {
|
|
2515
|
+
let raw;
|
|
2516
|
+
try {
|
|
2517
|
+
raw = readFileSync4(filePath, "utf-8");
|
|
2518
|
+
} catch {
|
|
2519
|
+
return null;
|
|
2520
|
+
}
|
|
2521
|
+
const records = [];
|
|
2522
|
+
for (const line of raw.split("\n")) {
|
|
2523
|
+
if (!line.trim()) continue;
|
|
2524
|
+
try {
|
|
2525
|
+
records.push(JSON.parse(line));
|
|
2526
|
+
} catch {
|
|
2527
|
+
}
|
|
2528
|
+
}
|
|
2529
|
+
const meta = records.find((r) => r.sessionId && r.startTime);
|
|
2530
|
+
if (!meta?.sessionId) return null;
|
|
2531
|
+
let messages = [];
|
|
2532
|
+
for (const record of records) {
|
|
2533
|
+
const next = record.$set?.messages ?? record.messages;
|
|
2534
|
+
if (Array.isArray(next)) messages = next;
|
|
2535
|
+
}
|
|
2536
|
+
let redactionCount = 0;
|
|
2537
|
+
const turns = [];
|
|
2538
|
+
messages.forEach((record, index) => {
|
|
2539
|
+
const role = record.type === "user" ? "user" : record.type === "gemini" || record.type === "model" ? "assistant" : null;
|
|
2540
|
+
if (!role) return;
|
|
2541
|
+
const text = textOf(record.content ?? record.displayContent);
|
|
2542
|
+
if (!text.trim()) return;
|
|
2543
|
+
const { text: safe, count } = redactSecrets(text);
|
|
2544
|
+
redactionCount += count;
|
|
2545
|
+
turns.push({
|
|
2546
|
+
// Gemini's own record id when it has one, so a re-read lands on the same
|
|
2547
|
+
// row; a derived id keyed on position otherwise, which is stable for an
|
|
2548
|
+
// append-only file.
|
|
2549
|
+
id: record.id ? deriveUuid(`evrex-gemini-turn ${meta.sessionId} ${record.id}`) : deriveUuid(`evrex-gemini-turn ${meta.sessionId} ${index}`),
|
|
2550
|
+
sessionId: deriveUuid(`evrex-gemini-session ${meta.sessionId}`),
|
|
2551
|
+
role,
|
|
2552
|
+
ts: record.timestamp ?? meta.startTime ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
2553
|
+
text: safe,
|
|
2554
|
+
filesTouched: [],
|
|
2555
|
+
editedLines: [],
|
|
2556
|
+
parentUuid: null,
|
|
2557
|
+
isSidechain: false,
|
|
2558
|
+
redacted: count > 0,
|
|
2559
|
+
// Gemini opens every session with a `<session_context>` block delivered
|
|
2560
|
+
// as a user message. Attributing that to the person is the same bug this
|
|
2561
|
+
// repo already fixed once for Claude Code's tool results.
|
|
2562
|
+
isSyntheticInput: role === "user" && SYNTHETIC.test(text),
|
|
2563
|
+
isToolError: false,
|
|
2564
|
+
// Gemini records a model per message but no token counts in the
|
|
2565
|
+
// transcript; unknown rather than zero.
|
|
2566
|
+
usage: { ...EMPTY_USAGE, model: record.model ?? null }
|
|
2567
|
+
});
|
|
2568
|
+
});
|
|
2569
|
+
if (turns.length === 0) return null;
|
|
2570
|
+
const { text: rawContent, count: rawRedactions } = redactSecrets(raw);
|
|
2571
|
+
const firstUser = turns.find((t) => t.role === "user" && !t.isSyntheticInput);
|
|
2572
|
+
return {
|
|
2573
|
+
id: deriveUuid(`evrex-gemini-session ${meta.sessionId}`),
|
|
2574
|
+
agentKind: "gemini",
|
|
2575
|
+
repoId,
|
|
2576
|
+
cwd: meta.directories?.[0] ?? repoPath,
|
|
2577
|
+
startedAt: meta.startTime ?? turns[0]?.ts ?? null,
|
|
2578
|
+
endedAt: turns[turns.length - 1]?.ts ?? null,
|
|
2579
|
+
turnCount: turns.length,
|
|
2580
|
+
committedSubjects: [],
|
|
2581
|
+
aiTitle: firstUser ? firstUser.text.slice(0, 120).trim() : null,
|
|
2582
|
+
author: null,
|
|
2583
|
+
sourceFile: filePath,
|
|
2584
|
+
redactionCount: redactionCount + rawRedactions,
|
|
2585
|
+
turns,
|
|
2586
|
+
rawContent,
|
|
2587
|
+
branch: null,
|
|
2588
|
+
parentSessionId: null,
|
|
2589
|
+
subagent: null,
|
|
2590
|
+
rawFormat: "jsonl"
|
|
2591
|
+
};
|
|
2592
|
+
}
|
|
2593
|
+
var SYNTHETIC;
|
|
2594
|
+
var init_gemini_sessions = __esm({
|
|
2595
|
+
"../../packages/ingest-core/src/gemini-sessions.ts"() {
|
|
2596
|
+
"use strict";
|
|
2597
|
+
init_derive_uuid();
|
|
2598
|
+
init_redact();
|
|
2599
|
+
init_types();
|
|
2600
|
+
SYNTHETIC = /^\s*<session_context>/;
|
|
2601
|
+
}
|
|
2602
|
+
});
|
|
2603
|
+
|
|
2604
|
+
// ../../packages/ingest-core/src/transcript-parsers.ts
|
|
2605
|
+
function parseTranscriptFile(path, repoPath, repoId) {
|
|
2606
|
+
for (const parse of PARSERS) {
|
|
2607
|
+
const parsed = parse(path, repoPath, repoId);
|
|
2608
|
+
if (parsed) return parsed;
|
|
2609
|
+
}
|
|
2610
|
+
return null;
|
|
2611
|
+
}
|
|
2612
|
+
var PARSERS;
|
|
2613
|
+
var init_transcript_parsers = __esm({
|
|
2614
|
+
"../../packages/ingest-core/src/transcript-parsers.ts"() {
|
|
2615
|
+
"use strict";
|
|
2616
|
+
init_claude_sessions();
|
|
2617
|
+
init_codex_sessions();
|
|
2618
|
+
init_gemini_sessions();
|
|
2619
|
+
PARSERS = [parseSessionFile, parseCodexSessionFile, parseGeminiSessionFile];
|
|
2620
|
+
}
|
|
2621
|
+
});
|
|
2622
|
+
|
|
2623
|
+
// ../../packages/ingest-core/src/slack-threads.ts
|
|
2624
|
+
function displayName(users, id) {
|
|
2625
|
+
if (!id) return "unknown";
|
|
2626
|
+
const user = users.get(id);
|
|
2627
|
+
return user?.profile?.real_name ?? user?.real_name ?? user?.profile?.display_name ?? user?.name ?? id;
|
|
2628
|
+
}
|
|
2629
|
+
function slackTimestamp(ts) {
|
|
2630
|
+
const seconds = Number(ts);
|
|
2631
|
+
if (!Number.isFinite(seconds) || seconds <= 0) return null;
|
|
2632
|
+
return new Date(seconds * 1e3).toISOString();
|
|
2633
|
+
}
|
|
2634
|
+
function readableText(text, users) {
|
|
2635
|
+
return text.replace(/<@([A-Z0-9]+)(?:\|[^>]*)?>/g, (_, id) => `@${displayName(users, id)}`).replace(/<#[A-Z0-9]+\|([^>]*)>/g, (_, name) => `#${name}`).replace(/<(https?:[^>|]+)\|([^>]*)>/g, (_, __, label) => label).replace(/<(https?:[^>]+)>/g, (_, url) => url).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
2636
|
+
}
|
|
2637
|
+
function isHumanMessage(message) {
|
|
2638
|
+
if (message.type && message.type !== "message") return false;
|
|
2639
|
+
if (message.subtype && MACHINE_SUBTYPES.has(message.subtype)) return false;
|
|
2640
|
+
if (message.bot_id && !message.user) return false;
|
|
2641
|
+
return Boolean(message.text?.trim());
|
|
2642
|
+
}
|
|
2643
|
+
function groupIntoThreads(messages) {
|
|
2644
|
+
const threads = /* @__PURE__ */ new Map();
|
|
2645
|
+
for (const message of messages) {
|
|
2646
|
+
if (!isHumanMessage(message)) continue;
|
|
2647
|
+
const key = message.thread_ts ?? message.ts;
|
|
2648
|
+
if (!key) continue;
|
|
2649
|
+
threads.set(key, [...threads.get(key) ?? [], message]);
|
|
2650
|
+
}
|
|
2651
|
+
return [...threads.values()].map(
|
|
2652
|
+
(thread) => [...thread].sort((a, b) => Number(a.ts ?? 0) - Number(b.ts ?? 0))
|
|
2653
|
+
);
|
|
2654
|
+
}
|
|
2655
|
+
function parseSlackExport(messages, options) {
|
|
2656
|
+
const users = /* @__PURE__ */ new Map();
|
|
2657
|
+
for (const user of options.users ?? []) if (user.id) users.set(user.id, user);
|
|
2658
|
+
const out = [];
|
|
2659
|
+
for (const thread of groupIntoThreads(messages)) {
|
|
2660
|
+
const root = thread[0];
|
|
2661
|
+
const rootTs = root?.thread_ts ?? root?.ts;
|
|
2662
|
+
const startedAt = slackTimestamp(rootTs ?? void 0);
|
|
2663
|
+
if (!rootTs || !startedAt) continue;
|
|
2664
|
+
const turns = [];
|
|
2665
|
+
let redactionCount = 0;
|
|
2666
|
+
for (const message of thread) {
|
|
2667
|
+
const ts = slackTimestamp(message.ts);
|
|
2668
|
+
if (!ts) continue;
|
|
2669
|
+
const { text, count } = redactSecrets(
|
|
2670
|
+
readableText(message.text ?? "", users)
|
|
2671
|
+
);
|
|
2672
|
+
if (!text.trim()) continue;
|
|
2673
|
+
redactionCount += count;
|
|
2674
|
+
turns.push({
|
|
2675
|
+
// A uuid, because that is what the columns these land in are. The
|
|
2676
|
+
// readable `slack:<channel>:<ts>` key is kept on `sourceFile`.
|
|
2677
|
+
id: deriveSlackTurnId(options.channel, message.ts),
|
|
2678
|
+
sessionId: deriveSlackSessionId(options.channel, rootTs),
|
|
2679
|
+
// Everything here was typed by a person. There is no assistant side,
|
|
2680
|
+
// and marking any of it otherwise would let it be read as recovered
|
|
2681
|
+
// agent reasoning.
|
|
2682
|
+
role: "user",
|
|
2683
|
+
ts,
|
|
2684
|
+
text: `${displayName(users, message.user)}: ${text}`,
|
|
2685
|
+
filesTouched: [],
|
|
2686
|
+
editedLines: [],
|
|
2687
|
+
parentUuid: null,
|
|
2688
|
+
isSidechain: false,
|
|
2689
|
+
redacted: count > 0,
|
|
2690
|
+
isSyntheticInput: false,
|
|
2691
|
+
isToolError: false,
|
|
2692
|
+
// Neither source reports what a turn cost, so it is unknown rather than free.
|
|
2693
|
+
usage: { ...EMPTY_USAGE }
|
|
2694
|
+
});
|
|
2695
|
+
}
|
|
2696
|
+
const size = turns.reduce((total, turn) => total + turn.text.length, 0);
|
|
2697
|
+
if (turns.length < MIN_THREAD_MESSAGES) continue;
|
|
2698
|
+
const floor = turns.length === 1 ? MIN_SOLO_CHARS : MIN_THREAD_CHARS;
|
|
2699
|
+
if (size < floor) continue;
|
|
2700
|
+
out.push({
|
|
2701
|
+
session: {
|
|
2702
|
+
id: deriveSlackSessionId(options.channel, rootTs),
|
|
2703
|
+
agentKind: "slack",
|
|
2704
|
+
repoId: options.repoId,
|
|
2705
|
+
cwd: options.cwd,
|
|
2706
|
+
startedAt,
|
|
2707
|
+
endedAt: turns[turns.length - 1]?.ts ?? startedAt,
|
|
2708
|
+
turnCount: turns.length,
|
|
2709
|
+
// The first message is what the thread is about, near enough, and it
|
|
2710
|
+
// is what Slack itself shows in a thread list.
|
|
2711
|
+
aiTitle: (turns[0]?.text ?? "").slice(0, 120),
|
|
2712
|
+
author: displayName(users, root?.user),
|
|
2713
|
+
sourceFile: `slack/${options.channel}/${rootTs}.json`,
|
|
2714
|
+
redactionCount,
|
|
2715
|
+
// The thread exactly as Slack gave it, redacted like every other
|
|
2716
|
+
// archived transcript: a teammate re-materialising this must not
|
|
2717
|
+
// receive a key the author pasted into a channel.
|
|
2718
|
+
// A Slack thread runs no commands; nothing here can place a commit.
|
|
2719
|
+
committedSubjects: [],
|
|
2720
|
+
rawContent: JSON.stringify(
|
|
2721
|
+
thread.map((message) => ({
|
|
2722
|
+
...message,
|
|
2723
|
+
text: redactSecrets(message.text ?? "").text
|
|
2724
|
+
})),
|
|
2725
|
+
null,
|
|
2726
|
+
2
|
|
2727
|
+
),
|
|
2728
|
+
branch: null,
|
|
2729
|
+
parentSessionId: null,
|
|
2730
|
+
subagent: null,
|
|
2731
|
+
rawFormat: "json",
|
|
2732
|
+
turns
|
|
2733
|
+
},
|
|
2734
|
+
turns
|
|
2735
|
+
});
|
|
2736
|
+
}
|
|
2737
|
+
return out;
|
|
2738
|
+
}
|
|
2739
|
+
var MIN_THREAD_MESSAGES, MIN_THREAD_CHARS, MIN_SOLO_CHARS, MACHINE_SUBTYPES;
|
|
2740
|
+
var init_slack_threads = __esm({
|
|
2741
|
+
"../../packages/ingest-core/src/slack-threads.ts"() {
|
|
2742
|
+
"use strict";
|
|
2743
|
+
init_redact();
|
|
2744
|
+
init_derive_uuid();
|
|
2745
|
+
init_types();
|
|
2746
|
+
MIN_THREAD_MESSAGES = 1;
|
|
2747
|
+
MIN_THREAD_CHARS = 80;
|
|
2748
|
+
MIN_SOLO_CHARS = 200;
|
|
2749
|
+
MACHINE_SUBTYPES = /* @__PURE__ */ new Set([
|
|
2750
|
+
"channel_join",
|
|
2751
|
+
"channel_leave",
|
|
2752
|
+
"channel_topic",
|
|
2753
|
+
"channel_purpose",
|
|
2754
|
+
"channel_name",
|
|
2755
|
+
"channel_archive",
|
|
2756
|
+
"channel_unarchive",
|
|
2757
|
+
"bot_message",
|
|
2758
|
+
"thread_broadcast_join"
|
|
2759
|
+
]);
|
|
2760
|
+
}
|
|
2761
|
+
});
|
|
2762
|
+
|
|
2763
|
+
// ../../packages/ingest-core/src/slack-client.ts
|
|
2764
|
+
async function call(method, params, options, attempt = 0) {
|
|
2765
|
+
const doFetch = options.fetchImpl ?? fetch;
|
|
2766
|
+
const sleep = options.sleep ?? wait;
|
|
2767
|
+
const query = new URLSearchParams(params).toString();
|
|
2768
|
+
const response = options.post ? await doFetch(`${API}/${method}`, {
|
|
2769
|
+
method: "POST",
|
|
2770
|
+
headers: {
|
|
2771
|
+
Authorization: `Bearer ${options.token}`,
|
|
2772
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
2773
|
+
},
|
|
2774
|
+
body: query
|
|
2775
|
+
}) : await doFetch(`${API}/${method}?${query}`, {
|
|
2776
|
+
headers: { Authorization: `Bearer ${options.token}` }
|
|
2777
|
+
});
|
|
2778
|
+
if (response.status === 429 && attempt < 5) {
|
|
2779
|
+
const header = response.headers.get("retry-after");
|
|
2780
|
+
const seconds = header ? Number(header) : NaN;
|
|
2781
|
+
await sleep((Number.isFinite(seconds) ? seconds : 30) * 1e3);
|
|
2782
|
+
return call(method, params, options, attempt + 1);
|
|
2783
|
+
}
|
|
2784
|
+
const body = await response.json();
|
|
2785
|
+
if (!body.ok) {
|
|
2786
|
+
const code = body.error ?? `http_${response.status}`;
|
|
2787
|
+
throw new SlackError(code, PERMANENT.has(code));
|
|
2788
|
+
}
|
|
2789
|
+
return body;
|
|
2790
|
+
}
|
|
2791
|
+
async function paginate(method, params, pick, options) {
|
|
2792
|
+
const out = [];
|
|
2793
|
+
let cursor;
|
|
2794
|
+
do {
|
|
2795
|
+
const body = await call(
|
|
2796
|
+
method,
|
|
2797
|
+
{ ...params, limit: String(PAGE_SIZE), ...cursor ? { cursor } : {} },
|
|
2798
|
+
options
|
|
2799
|
+
);
|
|
2800
|
+
out.push(...pick(body) ?? []);
|
|
2801
|
+
cursor = body.response_metadata?.next_cursor || void 0;
|
|
2802
|
+
} while (cursor);
|
|
2803
|
+
return out;
|
|
2804
|
+
}
|
|
2805
|
+
async function listChannels(options) {
|
|
2806
|
+
const channels = await paginate(
|
|
2807
|
+
"conversations.list",
|
|
2808
|
+
{ types: "public_channel,private_channel", exclude_archived: "true" },
|
|
2809
|
+
(body) => body.channels,
|
|
2810
|
+
options
|
|
2811
|
+
);
|
|
2812
|
+
return channels.filter(
|
|
2813
|
+
(channel) => !channel.is_private || channel.is_member !== false
|
|
2814
|
+
);
|
|
2815
|
+
}
|
|
2816
|
+
async function joinChannel(channel, options) {
|
|
2817
|
+
await call("conversations.join", { channel }, { ...options, post: true });
|
|
2818
|
+
}
|
|
2819
|
+
async function postMessage(channel, text, options) {
|
|
2820
|
+
await call(
|
|
2821
|
+
"chat.postMessage",
|
|
2822
|
+
{ channel, text, unfurl_links: "false", unfurl_media: "false" },
|
|
2823
|
+
{ ...options, post: true }
|
|
2824
|
+
);
|
|
2825
|
+
}
|
|
2826
|
+
async function listUsers(options) {
|
|
2827
|
+
return paginate("users.list", {}, (body) => body.members, options);
|
|
2828
|
+
}
|
|
2829
|
+
async function fetchChannelMessages(channel, options) {
|
|
2830
|
+
const top = await paginate(
|
|
2831
|
+
"conversations.history",
|
|
2832
|
+
{ channel, ...options.oldest ? { oldest: options.oldest } : {} },
|
|
2833
|
+
(body) => body.messages,
|
|
2834
|
+
options
|
|
2835
|
+
);
|
|
2836
|
+
const all = [];
|
|
2837
|
+
for (const message of top) {
|
|
2838
|
+
all.push(message);
|
|
2839
|
+
const replyCount = message.reply_count ?? 0;
|
|
2840
|
+
if (replyCount > 0 && message.ts) {
|
|
2841
|
+
options.onProgress?.(`reading a thread with ${replyCount} replies`);
|
|
2842
|
+
const replies = await paginate(
|
|
2843
|
+
"conversations.replies",
|
|
2844
|
+
{ channel, ts: message.ts },
|
|
2845
|
+
(body) => body.messages,
|
|
2846
|
+
options
|
|
2847
|
+
);
|
|
2848
|
+
all.push(...replies.filter((reply) => reply.ts !== message.ts));
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2851
|
+
return all;
|
|
2852
|
+
}
|
|
2853
|
+
var API, PAGE_SIZE, PERMANENT, SlackError, wait;
|
|
2854
|
+
var init_slack_client = __esm({
|
|
2855
|
+
"../../packages/ingest-core/src/slack-client.ts"() {
|
|
2856
|
+
"use strict";
|
|
2857
|
+
API = "https://slack.com/api";
|
|
2858
|
+
PAGE_SIZE = 200;
|
|
2859
|
+
PERMANENT = /* @__PURE__ */ new Set([
|
|
2860
|
+
"invalid_auth",
|
|
2861
|
+
"account_inactive",
|
|
2862
|
+
"token_revoked",
|
|
2863
|
+
"missing_scope",
|
|
2864
|
+
"not_allowed_token_type"
|
|
2865
|
+
]);
|
|
2866
|
+
SlackError = class extends Error {
|
|
2867
|
+
constructor(slackCode, permanent) {
|
|
2868
|
+
super(`Slack returned ${slackCode}`);
|
|
2869
|
+
this.slackCode = slackCode;
|
|
2870
|
+
this.permanent = permanent;
|
|
2871
|
+
this.name = "SlackError";
|
|
2872
|
+
}
|
|
2873
|
+
};
|
|
2874
|
+
wait = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
2875
|
+
}
|
|
2876
|
+
});
|
|
2877
|
+
|
|
2878
|
+
// ../../packages/ingest-core/src/line-survival.ts
|
|
2879
|
+
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
2880
|
+
function git2(repoPath, args) {
|
|
2881
|
+
return execFileSync4("git", args, {
|
|
2882
|
+
cwd: repoPath,
|
|
2883
|
+
maxBuffer: 1024 * 1024 * 64,
|
|
2884
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
2885
|
+
}).toString("utf-8");
|
|
2886
|
+
}
|
|
2887
|
+
function addedByFile(repoPath, sha) {
|
|
2888
|
+
const out = /* @__PURE__ */ new Map();
|
|
2889
|
+
const raw = git2(repoPath, ["diff-tree", "--root", "--no-commit-id", "--numstat", "-r", "-M", sha]);
|
|
2890
|
+
for (const line of raw.split("\n")) {
|
|
2891
|
+
const [added, , path] = line.split(" ");
|
|
2892
|
+
if (!path || added === "-") continue;
|
|
2893
|
+
out.set(path, Number(added) || 0);
|
|
2894
|
+
}
|
|
2895
|
+
return out;
|
|
2896
|
+
}
|
|
2897
|
+
function survivingIn(repoPath, sha, path) {
|
|
2898
|
+
let raw;
|
|
2899
|
+
try {
|
|
2900
|
+
raw = git2(repoPath, ["blame", "-w", "-M", "--line-porcelain", "HEAD", "--", path]);
|
|
2901
|
+
} catch {
|
|
2902
|
+
return 0;
|
|
2903
|
+
}
|
|
2904
|
+
let n = 0;
|
|
2905
|
+
for (const line of raw.split("\n")) {
|
|
2906
|
+
if (line.length > 41 && line.charCodeAt(0) !== 9 && line.startsWith(sha.slice(0, 40)) && line[40] === " ") {
|
|
2907
|
+
n += 1;
|
|
2908
|
+
}
|
|
2909
|
+
}
|
|
2910
|
+
return n;
|
|
2911
|
+
}
|
|
2912
|
+
function measureLineSurvival(repoPath, sha, now = /* @__PURE__ */ new Date()) {
|
|
2913
|
+
const full = git2(repoPath, ["rev-parse", sha]).trim();
|
|
2914
|
+
const added = addedByFile(repoPath, full);
|
|
2915
|
+
let addedLines = 0;
|
|
2916
|
+
let survivingLines = 0;
|
|
2917
|
+
for (const [path, n] of added) {
|
|
2918
|
+
addedLines += n;
|
|
2919
|
+
if (n > 0) survivingLines += survivingIn(repoPath, full, path);
|
|
2920
|
+
}
|
|
2921
|
+
return { sha: full, addedLines, survivingLines: Math.min(survivingLines, addedLines), measuredAt: now.toISOString() };
|
|
2922
|
+
}
|
|
2923
|
+
function commitsOlderThan(repoPath, minAgeDays, limit, now = /* @__PURE__ */ new Date()) {
|
|
2924
|
+
const before = new Date(now.getTime() - minAgeDays * 864e5).toISOString();
|
|
2925
|
+
const raw = git2(repoPath, ["log", `--before=${before}`, `--max-count=${limit}`, "--format=%H%x09%cI", "HEAD"]);
|
|
2926
|
+
return raw.split("\n").filter(Boolean).map((line) => {
|
|
2927
|
+
const [sha, at] = line.split(" ");
|
|
2928
|
+
return { sha, at };
|
|
2929
|
+
});
|
|
2930
|
+
}
|
|
2931
|
+
var init_line_survival = __esm({
|
|
2932
|
+
"../../packages/ingest-core/src/line-survival.ts"() {
|
|
2933
|
+
"use strict";
|
|
2934
|
+
}
|
|
2935
|
+
});
|
|
1466
2936
|
|
|
1467
2937
|
// ../../packages/ingest-core/src/index.ts
|
|
2938
|
+
var src_exports = {};
|
|
2939
|
+
__export(src_exports, {
|
|
2940
|
+
CONVERSATION_KINDS: () => CONVERSATION_KINDS,
|
|
2941
|
+
EMPTY_USAGE: () => EMPTY_USAGE,
|
|
2942
|
+
EVREX_ORIGIN_TRAILER_KEY: () => EVREX_ORIGIN_TRAILER_KEY,
|
|
2943
|
+
EVREX_SESSION_TRAILER_KEY: () => EVREX_SESSION_TRAILER_KEY,
|
|
2944
|
+
JIRA_FIELDS: () => JIRA_FIELDS,
|
|
2945
|
+
JIRA_SEARCH_PATH: () => JIRA_SEARCH_PATH,
|
|
2946
|
+
LINEAR_AUTHORIZE_URL: () => LINEAR_AUTHORIZE_URL,
|
|
2947
|
+
LINEAR_GRAPHQL_URL: () => LINEAR_GRAPHQL_URL,
|
|
2948
|
+
LINEAR_ISSUES_QUERY: () => LINEAR_ISSUES_QUERY,
|
|
2949
|
+
LINEAR_READ_SCOPE: () => LINEAR_READ_SCOPE,
|
|
2950
|
+
LINEAR_TOKEN_URL: () => LINEAR_TOKEN_URL,
|
|
2951
|
+
MIN_MEANINGFUL_LINE_LENGTH: () => MIN_MEANINGFUL_LINE_LENGTH,
|
|
2952
|
+
PARSER_VERSION: () => PARSER_VERSION,
|
|
2953
|
+
PROVENANCE_TRAILERS: () => PROVENANCE_TRAILERS,
|
|
2954
|
+
REFERENCE_KINDS: () => REFERENCE_KINDS,
|
|
2955
|
+
SOURCE_KINDS: () => SOURCE_KINDS,
|
|
2956
|
+
STATED_TRAILERS: () => STATED_TRAILERS,
|
|
2957
|
+
SlackError: () => SlackError,
|
|
2958
|
+
advanceCursor: () => advanceCursor,
|
|
2959
|
+
agentTrailersOf: () => agentTrailersOf,
|
|
2960
|
+
buildCopilotSession: () => buildCopilotSession,
|
|
2961
|
+
buildOpenCodeSession: () => buildOpenCodeSession,
|
|
2962
|
+
changedSessions: () => changedSessions,
|
|
2963
|
+
codexSessionsDir: () => codexSessionsDir,
|
|
2964
|
+
collectRepoData: () => collectRepoData,
|
|
2965
|
+
commitsOlderThan: () => commitsOlderThan,
|
|
2966
|
+
committedSubjects: () => committedSubjects,
|
|
2967
|
+
copilotSessionsDir: () => copilotSessionsDir,
|
|
2968
|
+
cursorStateDbPath: () => cursorStateDbPath,
|
|
2969
|
+
deriveCodexTurnId: () => deriveCodexTurnId,
|
|
2970
|
+
deriveCursorSessionId: () => deriveCursorSessionId,
|
|
2971
|
+
deriveOpenCodeSessionId: () => deriveOpenCodeSessionId,
|
|
2972
|
+
deriveRepoId: () => deriveRepoId,
|
|
2973
|
+
deriveSlackSessionId: () => deriveSlackSessionId,
|
|
2974
|
+
deriveSlackTurnId: () => deriveSlackTurnId,
|
|
2975
|
+
deriveTicketId: () => deriveTicketId,
|
|
2976
|
+
deriveUuid: () => deriveUuid,
|
|
2977
|
+
detectTruncatedHistory: () => detectTruncatedHistory,
|
|
2978
|
+
fetchChannelMessages: () => fetchChannelMessages,
|
|
2979
|
+
fetchJiraIssues: () => fetchJiraIssues,
|
|
2980
|
+
fetchLinearIssues: () => fetchLinearIssues,
|
|
2981
|
+
findCodexSessionFiles: () => findCodexSessionFiles,
|
|
2982
|
+
findCopilotSessionDirs: () => findCopilotSessionDirs,
|
|
2983
|
+
findSessionFiles: () => findSessionFiles,
|
|
2984
|
+
getGitUserName: () => getGitUserName,
|
|
2985
|
+
groupIntoThreads: () => groupIntoThreads,
|
|
2986
|
+
isHumanMessage: () => isHumanMessage,
|
|
2987
|
+
isReferenceKind: () => isReferenceKind,
|
|
2988
|
+
jiraBasicAuth: () => jiraBasicAuth,
|
|
2989
|
+
jiraIssueToTicket: () => jiraIssueToTicket,
|
|
2990
|
+
joinChannel: () => joinChannel,
|
|
2991
|
+
lacksFileAttribution: () => lacksFileAttribution,
|
|
2992
|
+
linearAuthHeader: () => linearAuthHeader,
|
|
2993
|
+
linearIssueToTicket: () => linearIssueToTicket,
|
|
2994
|
+
listChannels: () => listChannels,
|
|
2995
|
+
listUsers: () => listUsers,
|
|
2996
|
+
loadComposers: () => loadComposers,
|
|
2997
|
+
matchCommitToSession: () => matchCommitToSession,
|
|
2998
|
+
measureLineSurvival: () => measureLineSurvival,
|
|
2999
|
+
normalizeRepoRemote: () => normalizeRepoRemote,
|
|
3000
|
+
opencodeDbPath: () => opencodeDbPath,
|
|
3001
|
+
originOf: () => originOf,
|
|
3002
|
+
parseAllCodexSessions: () => parseAllCodexSessions,
|
|
3003
|
+
parseAllCopilotSessions: () => parseAllCopilotSessions,
|
|
3004
|
+
parseAllCursorSessions: () => parseAllCursorSessions,
|
|
3005
|
+
parseAllOpenCodeSessions: () => parseAllOpenCodeSessions,
|
|
3006
|
+
parseAllSessions: () => parseAllSessions,
|
|
3007
|
+
parseCodexSessionFile: () => parseCodexSessionFile,
|
|
3008
|
+
parseCopilotSessionDir: () => parseCopilotSessionDir,
|
|
3009
|
+
parseCopilotWorkspace: () => parseCopilotWorkspace,
|
|
3010
|
+
parseCursorComposer: () => parseCursorComposer,
|
|
3011
|
+
parseCursorConversation: () => parseCursorConversation,
|
|
3012
|
+
parseGeminiSessionFile: () => parseGeminiSessionFile,
|
|
3013
|
+
parseGitLog: () => parseGitLog,
|
|
3014
|
+
parseLineLog: () => parseLineLog,
|
|
3015
|
+
parseSessionFile: () => parseSessionFile,
|
|
3016
|
+
parseSlackExport: () => parseSlackExport,
|
|
3017
|
+
parseTraceTarget: () => parseTraceTarget,
|
|
3018
|
+
parseTrailers: () => parseTrailers,
|
|
3019
|
+
parseTranscriptFile: () => parseTranscriptFile,
|
|
3020
|
+
parseUnifiedDiffHunks: () => parseUnifiedDiffHunks,
|
|
3021
|
+
planRead: () => planRead,
|
|
3022
|
+
postMessage: () => postMessage,
|
|
3023
|
+
proposeLinks: () => proposeLinks,
|
|
3024
|
+
provenanceTrailerFor: () => provenanceTrailerFor,
|
|
3025
|
+
readableText: () => readableText,
|
|
3026
|
+
redactJsonValue: () => redactJsonValue,
|
|
3027
|
+
redactRawTranscript: () => redactRawTranscript,
|
|
3028
|
+
redactSecrets: () => redactSecrets,
|
|
3029
|
+
repoIdFromPath: () => repoIdFromPath,
|
|
3030
|
+
repoIdFromRemote: () => repoIdFromRemote,
|
|
3031
|
+
repoIdFromRootCommit: () => repoIdFromRootCommit,
|
|
3032
|
+
repoNameFromId: () => repoNameFromId,
|
|
3033
|
+
sessionSignature: () => sessionSignature,
|
|
3034
|
+
slackTimestamp: () => slackTimestamp,
|
|
3035
|
+
splitCompleteLines: () => splitCompleteLines,
|
|
3036
|
+
statedInsightsOf: () => statedInsightsOf,
|
|
3037
|
+
stripNulls: () => stripNulls,
|
|
3038
|
+
stripNullsDeep: () => stripNullsDeep,
|
|
3039
|
+
ticketReferencesIn: () => ticketReferencesIn,
|
|
3040
|
+
traceLines: () => traceLines,
|
|
3041
|
+
trailersFromMessage: () => trailersFromMessage
|
|
3042
|
+
});
|
|
3043
|
+
import { userInfo } from "node:os";
|
|
1468
3044
|
function resolveFallbackAuthor(repoPath, commits) {
|
|
1469
3045
|
const configured = getGitUserName(repoPath);
|
|
1470
3046
|
if (configured) return configured;
|
|
@@ -1489,7 +3065,9 @@ function collectRepoData(repoPath, options = {}) {
|
|
|
1489
3065
|
const sessions = [
|
|
1490
3066
|
...claude.sessions,
|
|
1491
3067
|
...parseAllCursorSessions(repoPath, repoId),
|
|
1492
|
-
...parseAllCodexSessions(repoPath, repoId)
|
|
3068
|
+
...parseAllCodexSessions(repoPath, repoId),
|
|
3069
|
+
...parseAllOpenCodeSessions(repoPath, repoId),
|
|
3070
|
+
...parseAllCopilotSessions(repoPath, repoId)
|
|
1493
3071
|
].map((s) => ({
|
|
1494
3072
|
...s,
|
|
1495
3073
|
author: s.author ?? author
|
|
@@ -1503,6 +3081,292 @@ function collectRepoData(repoPath, options = {}) {
|
|
|
1503
3081
|
skippedTranscripts: claude.skipped
|
|
1504
3082
|
};
|
|
1505
3083
|
}
|
|
3084
|
+
var init_src = __esm({
|
|
3085
|
+
"../../packages/ingest-core/src/index.ts"() {
|
|
3086
|
+
"use strict";
|
|
3087
|
+
init_git_history();
|
|
3088
|
+
init_claude_sessions();
|
|
3089
|
+
init_cursor_sessions();
|
|
3090
|
+
init_opencode_sessions();
|
|
3091
|
+
init_copilot_sessions();
|
|
3092
|
+
init_codex_sessions();
|
|
3093
|
+
init_sanitize();
|
|
3094
|
+
init_types();
|
|
3095
|
+
init_sanitize();
|
|
3096
|
+
init_incremental();
|
|
3097
|
+
init_redact();
|
|
3098
|
+
init_tickets();
|
|
3099
|
+
init_linear();
|
|
3100
|
+
init_jira();
|
|
3101
|
+
init_claude_sessions();
|
|
3102
|
+
init_copilot_sessions();
|
|
3103
|
+
init_opencode_sessions();
|
|
3104
|
+
init_cursor_sessions();
|
|
3105
|
+
init_codex_sessions();
|
|
3106
|
+
init_git_history();
|
|
3107
|
+
init_diff_parser();
|
|
3108
|
+
init_trace_lines();
|
|
3109
|
+
init_trace_target();
|
|
3110
|
+
init_transcript_parsers();
|
|
3111
|
+
init_gemini_sessions();
|
|
3112
|
+
init_slack_threads();
|
|
3113
|
+
init_slack_client();
|
|
3114
|
+
init_subject_linking();
|
|
3115
|
+
init_derive_uuid();
|
|
3116
|
+
init_line_survival();
|
|
3117
|
+
}
|
|
3118
|
+
});
|
|
3119
|
+
|
|
3120
|
+
// src/client.ts
|
|
3121
|
+
var client_exports = {};
|
|
3122
|
+
__export(client_exports, {
|
|
3123
|
+
evrexApi: () => evrexApi
|
|
3124
|
+
});
|
|
3125
|
+
function headers() {
|
|
3126
|
+
const base = { "Content-Type": "application/json" };
|
|
3127
|
+
if (EVREX_TOKEN) base.Authorization = `Bearer ${EVREX_TOKEN}`;
|
|
3128
|
+
return base;
|
|
3129
|
+
}
|
|
3130
|
+
function describeFailure(method, path, status, statusText) {
|
|
3131
|
+
if (status === 401 || status === 403) {
|
|
3132
|
+
return EVREX_TOKEN ? `${method} ${path} -> ${status}: the EVREX_TOKEN this server was started with was rejected. It may be revoked or expired.` : `${method} ${path} -> ${status}: this evrex backend requires a credential, but no EVREX_TOKEN is set for this MCP server.`;
|
|
3133
|
+
}
|
|
3134
|
+
return `${method} ${path} -> ${status} ${statusText}`;
|
|
3135
|
+
}
|
|
3136
|
+
async function request(method, path, { body, absentIsAnswer } = {}) {
|
|
3137
|
+
const res = await fetch(`${API_BASE_URL}${path}`, {
|
|
3138
|
+
method,
|
|
3139
|
+
headers: headers(),
|
|
3140
|
+
...body === void 0 ? {} : { body: JSON.stringify(body) }
|
|
3141
|
+
});
|
|
3142
|
+
if (res.status === 404 && absentIsAnswer) return null;
|
|
3143
|
+
if (!res.ok) throw new Error(describeFailure(method, path, res.status, res.statusText));
|
|
3144
|
+
return await res.json();
|
|
3145
|
+
}
|
|
3146
|
+
var DEFAULT_API_BASE_URL, API_BASE_URL, EVREX_TOKEN, get, getOrNull, post, evrexApi;
|
|
3147
|
+
var init_client = __esm({
|
|
3148
|
+
"src/client.ts"() {
|
|
3149
|
+
"use strict";
|
|
3150
|
+
DEFAULT_API_BASE_URL = "https://api.evrex.ai";
|
|
3151
|
+
API_BASE_URL = (process.env.EVREX_API_BASE_URL ?? process.env.EVREX_API_URL ?? DEFAULT_API_BASE_URL).replace(/\/+$/, "");
|
|
3152
|
+
EVREX_TOKEN = process.env.EVREX_TOKEN ?? process.env.EVREX_API_TOKEN ?? null;
|
|
3153
|
+
get = (path) => request("GET", path);
|
|
3154
|
+
getOrNull = (path) => request("GET", path, { absentIsAnswer: true });
|
|
3155
|
+
post = (path, body) => request("POST", path, { body });
|
|
3156
|
+
evrexApi = {
|
|
3157
|
+
baseUrl: API_BASE_URL,
|
|
3158
|
+
repos: () => get("/repos"),
|
|
3159
|
+
commits: (repoPath) => get(`/commits?repoPath=${encodeURIComponent(repoPath)}`),
|
|
3160
|
+
// Abbreviated shas resolve server-side, so a value pasted from `git log`
|
|
3161
|
+
// works here (apps/backend/src/reads/reads.service.ts#resolveSha).
|
|
3162
|
+
commit: (sha) => getOrNull(`/commits/${encodeURIComponent(sha)}`),
|
|
3163
|
+
sessions: (repoPath) => get(`/sessions?repoPath=${encodeURIComponent(repoPath)}`),
|
|
3164
|
+
session: (id) => getOrNull(`/sessions/${encodeURIComponent(id)}`),
|
|
3165
|
+
// The paginated transcript — see reads.service.ts#getSessionTurns. `ts, id`
|
|
3166
|
+
// ordering server-side makes offsets stable across requests.
|
|
3167
|
+
sessionTurns: (id, offset, limit) => getOrNull(
|
|
3168
|
+
`/sessions/${encodeURIComponent(id)}/turns?offset=${offset}&limit=${limit}`
|
|
3169
|
+
),
|
|
3170
|
+
ask: (repoPath, text, filePaths) => post("/ask", { repoPath, text, filePaths }),
|
|
3171
|
+
// Evidence-only retrieval (BM25 + embedding), no LLM synthesis call — see
|
|
3172
|
+
// apps/backend/src/query/query.service.ts#search. Used by evrex_search,
|
|
3173
|
+
// which wants ranked hits fast, not a synthesized paragraph.
|
|
3174
|
+
search: (repoPath, text, filePaths) => post("/search", { repoPath, text, filePaths }),
|
|
3175
|
+
// Everything that happened in a repo, newest first, bounded by days — the
|
|
3176
|
+
// same query the desktop Timeline screen makes. Sessions and commits
|
|
3177
|
+
// interleaved, each with the handle evrex_expand takes.
|
|
3178
|
+
feedback: (body) => post("/feedback", body),
|
|
3179
|
+
timeline: (repoPath, days) => get(
|
|
3180
|
+
`/timeline?repoPath=${encodeURIComponent(repoPath)}&days=${encodeURIComponent(String(days))}`
|
|
3181
|
+
)
|
|
3182
|
+
};
|
|
3183
|
+
}
|
|
3184
|
+
});
|
|
3185
|
+
|
|
3186
|
+
// src/credential-store.ts
|
|
3187
|
+
var credential_store_exports = {};
|
|
3188
|
+
__export(credential_store_exports, {
|
|
3189
|
+
CredentialStore: () => CredentialStore,
|
|
3190
|
+
NoKeychainError: () => NoKeychainError,
|
|
3191
|
+
systemRunner: () => systemRunner
|
|
3192
|
+
});
|
|
3193
|
+
import { spawn } from "node:child_process";
|
|
3194
|
+
var SERVICE, ACCOUNT, systemRunner, NoKeychainError, CredentialStore, WINDOWS_PATH, WINDOWS_STORE, WINDOWS_RETRIEVE, WINDOWS_REMOVE;
|
|
3195
|
+
var init_credential_store = __esm({
|
|
3196
|
+
"src/credential-store.ts"() {
|
|
3197
|
+
"use strict";
|
|
3198
|
+
SERVICE = "evrex-capture";
|
|
3199
|
+
ACCOUNT = "evrex";
|
|
3200
|
+
systemRunner = {
|
|
3201
|
+
platform: process.platform,
|
|
3202
|
+
run(command, args, stdin) {
|
|
3203
|
+
return new Promise((resolve2) => {
|
|
3204
|
+
const child = spawn(command, args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
3205
|
+
let stdout = "";
|
|
3206
|
+
let stderr = "";
|
|
3207
|
+
child.stdout.on("data", (d) => stdout += d.toString());
|
|
3208
|
+
child.stderr.on("data", (d) => stderr += d.toString());
|
|
3209
|
+
child.on("error", () => resolve2({ code: 127, stdout: "", stderr: "" }));
|
|
3210
|
+
child.on("close", (code) => resolve2({ code: code ?? 1, stdout, stderr }));
|
|
3211
|
+
if (stdin !== void 0) child.stdin.write(stdin);
|
|
3212
|
+
child.stdin.end();
|
|
3213
|
+
});
|
|
3214
|
+
}
|
|
3215
|
+
};
|
|
3216
|
+
NoKeychainError = class extends Error {
|
|
3217
|
+
constructor(platform) {
|
|
3218
|
+
super(
|
|
3219
|
+
`evrex could not find a credential store on this machine (${platform}).
|
|
3220
|
+
macOS needs \`security\`, which ships with the system.
|
|
3221
|
+
Linux needs \`secret-tool\` \u2014 install libsecret-tools (Debian/Ubuntu)
|
|
3222
|
+
or libsecret (Fedora/Arch), and make sure a keyring daemon is running.
|
|
3223
|
+
Windows needs PowerShell.
|
|
3224
|
+
evrex will not fall back to writing the credential in a plain file.`
|
|
3225
|
+
);
|
|
3226
|
+
this.name = "NoKeychainError";
|
|
3227
|
+
}
|
|
3228
|
+
};
|
|
3229
|
+
CredentialStore = class {
|
|
3230
|
+
constructor(runner = systemRunner) {
|
|
3231
|
+
this.runner = runner;
|
|
3232
|
+
}
|
|
3233
|
+
async available() {
|
|
3234
|
+
switch (this.runner.platform) {
|
|
3235
|
+
case "darwin":
|
|
3236
|
+
return (await this.runner.run("security", ["help"])).code !== 127;
|
|
3237
|
+
case "win32":
|
|
3238
|
+
return (await this.runner.run("powershell", ["-Command", "$PSVersionTable.PSVersion.Major"])).code !== 127;
|
|
3239
|
+
default:
|
|
3240
|
+
return (await this.runner.run("secret-tool", ["--version"])).code !== 127;
|
|
3241
|
+
}
|
|
3242
|
+
}
|
|
3243
|
+
/**
|
|
3244
|
+
* Replaces any existing credential rather than adding a second one. A
|
|
3245
|
+
* machine that re-enrols after expiry must end up with exactly one entry, or
|
|
3246
|
+
* the next read is a coin flip between the live credential and a dead one.
|
|
3247
|
+
*/
|
|
3248
|
+
async store(secret) {
|
|
3249
|
+
if (!await this.available()) throw new NoKeychainError(this.runner.platform);
|
|
3250
|
+
switch (this.runner.platform) {
|
|
3251
|
+
case "darwin": {
|
|
3252
|
+
const result = await this.runner.run(
|
|
3253
|
+
"security",
|
|
3254
|
+
["add-generic-password", "-a", ACCOUNT, "-s", SERVICE, "-U", "-w"],
|
|
3255
|
+
`${secret}
|
|
3256
|
+
${secret}
|
|
3257
|
+
`
|
|
3258
|
+
);
|
|
3259
|
+
if (result.code !== 0) throw new Error(`Keychain write failed: ${result.stderr.trim()}`);
|
|
3260
|
+
return;
|
|
3261
|
+
}
|
|
3262
|
+
case "win32": {
|
|
3263
|
+
const result = await this.runner.run(
|
|
3264
|
+
"powershell",
|
|
3265
|
+
["-NoProfile", "-Command", WINDOWS_STORE],
|
|
3266
|
+
secret
|
|
3267
|
+
);
|
|
3268
|
+
if (result.code !== 0) throw new Error(`DPAPI write failed: ${result.stderr.trim()}`);
|
|
3269
|
+
return;
|
|
3270
|
+
}
|
|
3271
|
+
default: {
|
|
3272
|
+
const result = await this.runner.run(
|
|
3273
|
+
"secret-tool",
|
|
3274
|
+
["store", "--label=evrex capture credential", "service", SERVICE, "account", ACCOUNT],
|
|
3275
|
+
secret
|
|
3276
|
+
);
|
|
3277
|
+
if (result.code !== 0) throw new Error(`secret-tool write failed: ${result.stderr.trim()}`);
|
|
3278
|
+
return;
|
|
3279
|
+
}
|
|
3280
|
+
}
|
|
3281
|
+
}
|
|
3282
|
+
/** Null when there is nothing stored, which is the normal pre-enrolment state. */
|
|
3283
|
+
async retrieve() {
|
|
3284
|
+
if (!await this.available()) throw new NoKeychainError(this.runner.platform);
|
|
3285
|
+
switch (this.runner.platform) {
|
|
3286
|
+
case "darwin": {
|
|
3287
|
+
const r = await this.runner.run("security", [
|
|
3288
|
+
"find-generic-password",
|
|
3289
|
+
"-a",
|
|
3290
|
+
ACCOUNT,
|
|
3291
|
+
"-s",
|
|
3292
|
+
SERVICE,
|
|
3293
|
+
"-w"
|
|
3294
|
+
]);
|
|
3295
|
+
return r.code === 0 ? r.stdout.trim() || null : null;
|
|
3296
|
+
}
|
|
3297
|
+
case "win32": {
|
|
3298
|
+
const r = await this.runner.run("powershell", ["-NoProfile", "-Command", WINDOWS_RETRIEVE]);
|
|
3299
|
+
return r.code === 0 ? r.stdout.trim() || null : null;
|
|
3300
|
+
}
|
|
3301
|
+
default: {
|
|
3302
|
+
const r = await this.runner.run("secret-tool", [
|
|
3303
|
+
"lookup",
|
|
3304
|
+
"service",
|
|
3305
|
+
SERVICE,
|
|
3306
|
+
"account",
|
|
3307
|
+
ACCOUNT
|
|
3308
|
+
]);
|
|
3309
|
+
return r.code === 0 ? r.stdout.trim() || null : null;
|
|
3310
|
+
}
|
|
3311
|
+
}
|
|
3312
|
+
}
|
|
3313
|
+
/** Idempotent: removing a credential that is not there is not an error. */
|
|
3314
|
+
async remove() {
|
|
3315
|
+
if (!await this.available()) return;
|
|
3316
|
+
switch (this.runner.platform) {
|
|
3317
|
+
case "darwin":
|
|
3318
|
+
await this.runner.run("security", [
|
|
3319
|
+
"delete-generic-password",
|
|
3320
|
+
"-a",
|
|
3321
|
+
ACCOUNT,
|
|
3322
|
+
"-s",
|
|
3323
|
+
SERVICE
|
|
3324
|
+
]);
|
|
3325
|
+
return;
|
|
3326
|
+
case "win32":
|
|
3327
|
+
await this.runner.run("powershell", ["-NoProfile", "-Command", WINDOWS_REMOVE]);
|
|
3328
|
+
return;
|
|
3329
|
+
default:
|
|
3330
|
+
await this.runner.run("secret-tool", [
|
|
3331
|
+
"clear",
|
|
3332
|
+
"service",
|
|
3333
|
+
SERVICE,
|
|
3334
|
+
"account",
|
|
3335
|
+
ACCOUNT
|
|
3336
|
+
]);
|
|
3337
|
+
return;
|
|
3338
|
+
}
|
|
3339
|
+
}
|
|
3340
|
+
};
|
|
3341
|
+
WINDOWS_PATH = "$env:APPDATA\\evrex\\capture.cred";
|
|
3342
|
+
WINDOWS_STORE = `
|
|
3343
|
+
$ErrorActionPreference = 'Stop'
|
|
3344
|
+
$p = "${WINDOWS_PATH}"
|
|
3345
|
+
New-Item -ItemType Directory -Force -Path (Split-Path $p) | Out-Null
|
|
3346
|
+
$secret = [Console]::In.ReadToEnd().Trim()
|
|
3347
|
+
ConvertTo-SecureString $secret -AsPlainText -Force | ConvertFrom-SecureString | Set-Content -Path $p
|
|
3348
|
+
`.trim();
|
|
3349
|
+
WINDOWS_RETRIEVE = `
|
|
3350
|
+
$ErrorActionPreference = 'Stop'
|
|
3351
|
+
$p = "${WINDOWS_PATH}"
|
|
3352
|
+
if (-not (Test-Path $p)) { exit 1 }
|
|
3353
|
+
$sec = Get-Content $p | ConvertTo-SecureString
|
|
3354
|
+
[Runtime.InteropServices.Marshal]::PtrToStringAuto(
|
|
3355
|
+
[Runtime.InteropServices.Marshal]::SecureStringToBSTR($sec))
|
|
3356
|
+
`.trim();
|
|
3357
|
+
WINDOWS_REMOVE = `
|
|
3358
|
+
$p = "${WINDOWS_PATH}"
|
|
3359
|
+
if (Test-Path $p) { Remove-Item $p -Force }
|
|
3360
|
+
`.trim();
|
|
3361
|
+
}
|
|
3362
|
+
});
|
|
3363
|
+
|
|
3364
|
+
// src/import.ts
|
|
3365
|
+
init_src();
|
|
3366
|
+
import { realpathSync } from "node:fs";
|
|
3367
|
+
import { homedir as homedir7 } from "node:os";
|
|
3368
|
+
import { fileURLToPath } from "node:url";
|
|
3369
|
+
import { resolve } from "node:path";
|
|
1506
3370
|
|
|
1507
3371
|
// src/batch.ts
|
|
1508
3372
|
var MAX_BATCH_BYTES = 24 * 1024 * 1024;
|
|
@@ -1530,15 +3394,15 @@ function batchByBytes(items, maxBytes = MAX_BATCH_BYTES) {
|
|
|
1530
3394
|
}
|
|
1531
3395
|
|
|
1532
3396
|
// src/import-state.ts
|
|
1533
|
-
import { existsSync as
|
|
1534
|
-
import { dirname, join as
|
|
1535
|
-
import { homedir as
|
|
1536
|
-
function importStatePath(home =
|
|
1537
|
-
return
|
|
3397
|
+
import { existsSync as existsSync6, mkdirSync, readFileSync as readFileSync5, renameSync, writeFileSync } from "node:fs";
|
|
3398
|
+
import { dirname, join as join6 } from "node:path";
|
|
3399
|
+
import { homedir as homedir6 } from "node:os";
|
|
3400
|
+
function importStatePath(home = homedir6()) {
|
|
3401
|
+
return join6(home, ".evrex", "import-state.json");
|
|
1538
3402
|
}
|
|
1539
3403
|
function readImportState(path) {
|
|
1540
3404
|
try {
|
|
1541
|
-
const parsed = JSON.parse(
|
|
3405
|
+
const parsed = JSON.parse(readFileSync5(path, "utf8"));
|
|
1542
3406
|
if (!parsed || typeof parsed.repos !== "object") return { repos: {} };
|
|
1543
3407
|
return { repos: parsed.repos ?? {} };
|
|
1544
3408
|
} catch {
|
|
@@ -1588,17 +3452,59 @@ async function importRepo(repoPath, deps) {
|
|
|
1588
3452
|
result.commits += batch.length;
|
|
1589
3453
|
deps.log(` commits ${result.commits}/${data.commits.length}`);
|
|
1590
3454
|
}
|
|
3455
|
+
const wireSessions = data.sessions.map(
|
|
3456
|
+
({ rawContent: _raw, rawFormat: _fmt, ...rest }) => rest
|
|
3457
|
+
);
|
|
3458
|
+
const rawBySession = new Map(
|
|
3459
|
+
data.sessions.map((s) => [s.id, { rawContent: s.rawContent, rawFormat: s.rawFormat }])
|
|
3460
|
+
);
|
|
3461
|
+
async function archiveRaw(id) {
|
|
3462
|
+
const raw = rawBySession.get(id);
|
|
3463
|
+
if (!raw?.rawContent) return;
|
|
3464
|
+
await deps.post(`/ingest/sessions/${id}/transcript`, {
|
|
3465
|
+
rawContent: raw.rawContent,
|
|
3466
|
+
rawFormat: raw.rawFormat ?? "jsonl"
|
|
3467
|
+
});
|
|
3468
|
+
}
|
|
3469
|
+
async function postInPieces(d, session) {
|
|
3470
|
+
const turns = session.turns ?? [];
|
|
3471
|
+
if (turns.length <= 1) return false;
|
|
3472
|
+
const mid = Math.ceil(turns.length / 2);
|
|
3473
|
+
for (const slice of [turns.slice(0, mid), turns.slice(mid)]) {
|
|
3474
|
+
const part = { ...session, turns: slice };
|
|
3475
|
+
if (!await d.post("/ingest/sessions", { sessions: [part] })) {
|
|
3476
|
+
if (!await postInPieces(d, part)) return false;
|
|
3477
|
+
}
|
|
3478
|
+
}
|
|
3479
|
+
return true;
|
|
3480
|
+
}
|
|
1591
3481
|
const deliveredCursors = {};
|
|
1592
3482
|
if (!result.failed) {
|
|
1593
|
-
for (const batch of batchByBytes(
|
|
3483
|
+
for (const batch of batchByBytes(wireSessions)) {
|
|
1594
3484
|
result.batches += 1;
|
|
1595
3485
|
if (!await deps.post("/ingest/sessions", { sessions: batch })) {
|
|
3486
|
+
const tooBig = (x) => JSON.stringify(x).length >= MAX_BATCH_BYTES / 4;
|
|
3487
|
+
let salvaged = true;
|
|
3488
|
+
for (const one of batch) {
|
|
3489
|
+
const landed = batch.length > 1 && await deps.post("/ingest/sessions", { sessions: [one] }) ? true : tooBig(one) && await postInPieces(deps, one);
|
|
3490
|
+
if (!landed) {
|
|
3491
|
+
salvaged = false;
|
|
3492
|
+
break;
|
|
3493
|
+
}
|
|
3494
|
+
result.sessions += 1;
|
|
3495
|
+
const cursor = data.cursors[one.sourceFile];
|
|
3496
|
+
if (cursor !== void 0) deliveredCursors[one.sourceFile] = cursor;
|
|
3497
|
+
await archiveRaw(one.id);
|
|
3498
|
+
deps.log(` sessions ${result.sessions}/${data.sessions.length} (split upload)`);
|
|
3499
|
+
}
|
|
3500
|
+
if (salvaged) continue;
|
|
1596
3501
|
result.failed = true;
|
|
1597
3502
|
break;
|
|
1598
3503
|
}
|
|
1599
3504
|
for (const session of batch) {
|
|
1600
3505
|
const cursor = data.cursors[session.sourceFile];
|
|
1601
3506
|
if (cursor !== void 0) deliveredCursors[session.sourceFile] = cursor;
|
|
3507
|
+
await archiveRaw(session.id);
|
|
1602
3508
|
}
|
|
1603
3509
|
result.sessions += batch.length;
|
|
1604
3510
|
deps.log(` sessions ${result.sessions}/${data.sessions.length}`);
|
|
@@ -1612,7 +3518,87 @@ async function importRepo(repoPath, deps) {
|
|
|
1612
3518
|
writeImportState(statePath, state);
|
|
1613
3519
|
return result;
|
|
1614
3520
|
}
|
|
3521
|
+
async function durability(target, argv) {
|
|
3522
|
+
const { measureLineSurvival: measureLineSurvival2, commitsOlderThan: commitsOlderThan2, deriveRepoId: deriveRepoId2 } = await Promise.resolve().then(() => (init_src(), src_exports));
|
|
3523
|
+
const { evrexApi: evrexApi2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
3524
|
+
const flag = (name, fallback) => {
|
|
3525
|
+
const i = argv.indexOf(name);
|
|
3526
|
+
return i === -1 ? fallback : Number(argv[i + 1]) || fallback;
|
|
3527
|
+
};
|
|
3528
|
+
const minAgeDays = flag("--min-age-days", 30);
|
|
3529
|
+
const limit = flag("--limit", 300);
|
|
3530
|
+
const token = process.env.EVREX_TOKEN ?? await new (await Promise.resolve().then(() => (init_credential_store(), credential_store_exports))).CredentialStore().retrieve().catch(() => null);
|
|
3531
|
+
if (!token) {
|
|
3532
|
+
console.error("evrex: this machine is not enrolled.\n Run `npx -y evrex-mcp enrol` first, or set EVREX_TOKEN.");
|
|
3533
|
+
process.exit(1);
|
|
3534
|
+
}
|
|
3535
|
+
const repoId = deriveRepoId2(target);
|
|
3536
|
+
const commits = commitsOlderThan2(target, minAgeDays, limit);
|
|
3537
|
+
console.error(`Measuring ${commits.length} commits at least ${minAgeDays} days old in ${target}`);
|
|
3538
|
+
const measurements = [];
|
|
3539
|
+
for (const [i, c] of commits.entries()) {
|
|
3540
|
+
try {
|
|
3541
|
+
measurements.push(measureLineSurvival2(target, c.sha));
|
|
3542
|
+
} catch {
|
|
3543
|
+
}
|
|
3544
|
+
if ((i + 1) % 25 === 0) console.error(` ${i + 1}/${commits.length}`);
|
|
3545
|
+
}
|
|
3546
|
+
const res = await fetch(`${evrexApi2.baseUrl}/ingest/commits/durability`, {
|
|
3547
|
+
method: "POST",
|
|
3548
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
|
3549
|
+
body: JSON.stringify({ repoId, measurements })
|
|
3550
|
+
});
|
|
3551
|
+
if (!res.ok) {
|
|
3552
|
+
console.error(` /ingest/commits/durability -> ${res.status}`);
|
|
3553
|
+
process.exit(1);
|
|
3554
|
+
}
|
|
3555
|
+
const body = await res.json();
|
|
3556
|
+
const added = measurements.reduce((n, m) => n + m.addedLines, 0);
|
|
3557
|
+
const surviving = measurements.reduce((n, m) => n + m.survivingLines, 0);
|
|
3558
|
+
console.error(` ${body.updated} commits recorded \xB7 ${surviving}/${added} added lines still at HEAD (${added ? Math.round(100 * surviving / added) : 0}%)`);
|
|
3559
|
+
}
|
|
3560
|
+
async function parents(target) {
|
|
3561
|
+
const { deriveRepoId: deriveRepoId2 } = await Promise.resolve().then(() => (init_src(), src_exports));
|
|
3562
|
+
const { execFileSync: execFileSync5 } = await import("node:child_process");
|
|
3563
|
+
const { evrexApi: evrexApi2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
3564
|
+
const token = process.env.EVREX_TOKEN ?? await new (await Promise.resolve().then(() => (init_credential_store(), credential_store_exports))).CredentialStore().retrieve().catch(() => null);
|
|
3565
|
+
if (!token) {
|
|
3566
|
+
console.error("evrex: this machine is not enrolled.\n Run `npx -y evrex-mcp enrol` first, or set EVREX_TOKEN.");
|
|
3567
|
+
process.exit(1);
|
|
3568
|
+
}
|
|
3569
|
+
const repoId = deriveRepoId2(target);
|
|
3570
|
+
const rows = execFileSync5("git", ["rev-list", "--parents", "--all"], { cwd: target, maxBuffer: 64 * 1024 * 1024 }).toString("utf-8").split("\n").filter(Boolean).map((line) => {
|
|
3571
|
+
const [sha, ...parents2] = line.split(" ");
|
|
3572
|
+
return { sha, parents: parents2 };
|
|
3573
|
+
});
|
|
3574
|
+
console.error(`Reporting parents for ${rows.length} commits in ${target} (${rows.filter((r) => r.parents.length > 1).length} merges)`);
|
|
3575
|
+
let updated = 0;
|
|
3576
|
+
let inherited = 0;
|
|
3577
|
+
for (let i = 0; i < rows.length; i += 500) {
|
|
3578
|
+
const res = await fetch(`${evrexApi2.baseUrl}/ingest/commits/parents`, {
|
|
3579
|
+
method: "POST",
|
|
3580
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
|
3581
|
+
body: JSON.stringify({ repoId, commits: rows.slice(i, i + 500) })
|
|
3582
|
+
});
|
|
3583
|
+
if (!res.ok) {
|
|
3584
|
+
console.error(` /ingest/commits/parents -> ${res.status}`);
|
|
3585
|
+
process.exit(1);
|
|
3586
|
+
}
|
|
3587
|
+
const body = await res.json();
|
|
3588
|
+
updated += body.updated;
|
|
3589
|
+
inherited = body.inherited;
|
|
3590
|
+
}
|
|
3591
|
+
console.error(` ${updated} commits updated \xB7 merges now inherit ${inherited} link(s)`);
|
|
3592
|
+
}
|
|
1615
3593
|
async function main() {
|
|
3594
|
+
if (process.argv[2] === "parents") {
|
|
3595
|
+
await parents(resolve(process.argv[3] && !process.argv[3].startsWith("--") ? process.argv[3] : process.cwd()));
|
|
3596
|
+
return;
|
|
3597
|
+
}
|
|
3598
|
+
if (process.argv[2] === "durability") {
|
|
3599
|
+
await durability(resolve(process.argv[3] && !process.argv[3].startsWith("--") ? process.argv[3] : process.cwd()), process.argv.slice(3));
|
|
3600
|
+
return;
|
|
3601
|
+
}
|
|
1616
3602
|
const target = resolve(process.argv[3] ?? process.cwd());
|
|
1617
3603
|
const { evrexApi: evrexApi2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
1618
3604
|
const { CredentialStore: CredentialStore2 } = await Promise.resolve().then(() => (init_credential_store(), credential_store_exports));
|
|
@@ -1627,7 +3613,7 @@ async function main() {
|
|
|
1627
3613
|
const result = await importRepo(target, {
|
|
1628
3614
|
collect: collectRepoData,
|
|
1629
3615
|
deriveRepoId,
|
|
1630
|
-
home:
|
|
3616
|
+
home: homedir7(),
|
|
1631
3617
|
now: () => /* @__PURE__ */ new Date(),
|
|
1632
3618
|
log: (line) => console.error(line),
|
|
1633
3619
|
post: async (path, body) => {
|