evrex-mcp 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/capture.js +1012 -0
- package/dist/hook.js +134 -0
- package/dist/import.js +1667 -0
- package/dist/index.js +734 -109
- package/dist/tickets.js +636 -0
- package/package.json +11 -2
package/dist/import.js
ADDED
|
@@ -0,0 +1,1667 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// src/client.ts
|
|
13
|
+
var client_exports = {};
|
|
14
|
+
__export(client_exports, {
|
|
15
|
+
evrexApi: () => evrexApi
|
|
16
|
+
});
|
|
17
|
+
function headers() {
|
|
18
|
+
const base = { "Content-Type": "application/json" };
|
|
19
|
+
if (EVREX_TOKEN) base.Authorization = `Bearer ${EVREX_TOKEN}`;
|
|
20
|
+
return base;
|
|
21
|
+
}
|
|
22
|
+
function describeFailure(method, path, status, statusText) {
|
|
23
|
+
if (status === 401 || status === 403) {
|
|
24
|
+
return EVREX_TOKEN ? `${method} ${path} -> ${status}: the EVREX_TOKEN this server was started with was rejected. It may be revoked or expired.` : `${method} ${path} -> ${status}: this evrex backend requires a credential, but no EVREX_TOKEN is set for this MCP server.`;
|
|
25
|
+
}
|
|
26
|
+
return `${method} ${path} -> ${status} ${statusText}`;
|
|
27
|
+
}
|
|
28
|
+
async function request(method, path, { body, absentIsAnswer } = {}) {
|
|
29
|
+
const res = await fetch(`${API_BASE_URL}${path}`, {
|
|
30
|
+
method,
|
|
31
|
+
headers: headers(),
|
|
32
|
+
...body === void 0 ? {} : { body: JSON.stringify(body) }
|
|
33
|
+
});
|
|
34
|
+
if (res.status === 404 && absentIsAnswer) return null;
|
|
35
|
+
if (!res.ok) throw new Error(describeFailure(method, path, res.status, res.statusText));
|
|
36
|
+
return await res.json();
|
|
37
|
+
}
|
|
38
|
+
var DEFAULT_API_BASE_URL, API_BASE_URL, EVREX_TOKEN, get, getOrNull, post, evrexApi;
|
|
39
|
+
var init_client = __esm({
|
|
40
|
+
"src/client.ts"() {
|
|
41
|
+
"use strict";
|
|
42
|
+
DEFAULT_API_BASE_URL = "https://api.evrex.ai";
|
|
43
|
+
API_BASE_URL = (process.env.EVREX_API_BASE_URL ?? process.env.EVREX_API_URL ?? DEFAULT_API_BASE_URL).replace(/\/+$/, "");
|
|
44
|
+
EVREX_TOKEN = process.env.EVREX_TOKEN ?? process.env.EVREX_API_TOKEN ?? null;
|
|
45
|
+
get = (path) => request("GET", path);
|
|
46
|
+
getOrNull = (path) => request("GET", path, { absentIsAnswer: true });
|
|
47
|
+
post = (path, body) => request("POST", path, { body });
|
|
48
|
+
evrexApi = {
|
|
49
|
+
baseUrl: API_BASE_URL,
|
|
50
|
+
repos: () => get("/repos"),
|
|
51
|
+
commits: (repoPath) => get(`/commits?repoPath=${encodeURIComponent(repoPath)}`),
|
|
52
|
+
// Abbreviated shas resolve server-side, so a value pasted from `git log`
|
|
53
|
+
// works here (apps/backend/src/reads/reads.service.ts#resolveSha).
|
|
54
|
+
commit: (sha) => getOrNull(`/commits/${encodeURIComponent(sha)}`),
|
|
55
|
+
sessions: (repoPath) => get(`/sessions?repoPath=${encodeURIComponent(repoPath)}`),
|
|
56
|
+
session: (id) => getOrNull(`/sessions/${encodeURIComponent(id)}`),
|
|
57
|
+
ask: (repoPath, text, filePaths) => post("/ask", { repoPath, text, filePaths }),
|
|
58
|
+
// Evidence-only retrieval (BM25 + embedding), no LLM synthesis call — see
|
|
59
|
+
// apps/backend/src/query/query.service.ts#search. Used by evrex_search,
|
|
60
|
+
// which wants ranked hits fast, not a synthesized paragraph.
|
|
61
|
+
search: (repoPath, text, filePaths) => post("/search", { repoPath, text, filePaths })
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// src/credential-store.ts
|
|
67
|
+
var credential_store_exports = {};
|
|
68
|
+
__export(credential_store_exports, {
|
|
69
|
+
CredentialStore: () => CredentialStore,
|
|
70
|
+
NoKeychainError: () => NoKeychainError,
|
|
71
|
+
systemRunner: () => systemRunner
|
|
72
|
+
});
|
|
73
|
+
import { spawn } from "node:child_process";
|
|
74
|
+
var SERVICE, ACCOUNT, systemRunner, NoKeychainError, CredentialStore, WINDOWS_PATH, WINDOWS_STORE, WINDOWS_RETRIEVE, WINDOWS_REMOVE;
|
|
75
|
+
var init_credential_store = __esm({
|
|
76
|
+
"src/credential-store.ts"() {
|
|
77
|
+
"use strict";
|
|
78
|
+
SERVICE = "evrex-capture";
|
|
79
|
+
ACCOUNT = "evrex";
|
|
80
|
+
systemRunner = {
|
|
81
|
+
platform: process.platform,
|
|
82
|
+
run(command, args, stdin) {
|
|
83
|
+
return new Promise((resolve2) => {
|
|
84
|
+
const child = spawn(command, args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
85
|
+
let stdout = "";
|
|
86
|
+
let stderr = "";
|
|
87
|
+
child.stdout.on("data", (d) => stdout += d.toString());
|
|
88
|
+
child.stderr.on("data", (d) => stderr += d.toString());
|
|
89
|
+
child.on("error", () => resolve2({ code: 127, stdout: "", stderr: "" }));
|
|
90
|
+
child.on("close", (code) => resolve2({ code: code ?? 1, stdout, stderr }));
|
|
91
|
+
if (stdin !== void 0) child.stdin.write(stdin);
|
|
92
|
+
child.stdin.end();
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
NoKeychainError = class extends Error {
|
|
97
|
+
constructor(platform) {
|
|
98
|
+
super(
|
|
99
|
+
`evrex could not find a credential store on this machine (${platform}).
|
|
100
|
+
macOS needs \`security\`, which ships with the system.
|
|
101
|
+
Linux needs \`secret-tool\` \u2014 install libsecret-tools (Debian/Ubuntu)
|
|
102
|
+
or libsecret (Fedora/Arch), and make sure a keyring daemon is running.
|
|
103
|
+
Windows needs PowerShell.
|
|
104
|
+
evrex will not fall back to writing the credential in a plain file.`
|
|
105
|
+
);
|
|
106
|
+
this.name = "NoKeychainError";
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
CredentialStore = class {
|
|
110
|
+
constructor(runner = systemRunner) {
|
|
111
|
+
this.runner = runner;
|
|
112
|
+
}
|
|
113
|
+
async available() {
|
|
114
|
+
switch (this.runner.platform) {
|
|
115
|
+
case "darwin":
|
|
116
|
+
return (await this.runner.run("security", ["help"])).code !== 127;
|
|
117
|
+
case "win32":
|
|
118
|
+
return (await this.runner.run("powershell", ["-Command", "$PSVersionTable.PSVersion.Major"])).code !== 127;
|
|
119
|
+
default:
|
|
120
|
+
return (await this.runner.run("secret-tool", ["--version"])).code !== 127;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Replaces any existing credential rather than adding a second one. A
|
|
125
|
+
* machine that re-enrols after expiry must end up with exactly one entry, or
|
|
126
|
+
* the next read is a coin flip between the live credential and a dead one.
|
|
127
|
+
*/
|
|
128
|
+
async store(secret) {
|
|
129
|
+
if (!await this.available()) throw new NoKeychainError(this.runner.platform);
|
|
130
|
+
switch (this.runner.platform) {
|
|
131
|
+
case "darwin": {
|
|
132
|
+
const result = await this.runner.run(
|
|
133
|
+
"security",
|
|
134
|
+
["add-generic-password", "-a", ACCOUNT, "-s", SERVICE, "-U", "-w"],
|
|
135
|
+
`${secret}
|
|
136
|
+
${secret}
|
|
137
|
+
`
|
|
138
|
+
);
|
|
139
|
+
if (result.code !== 0) throw new Error(`Keychain write failed: ${result.stderr.trim()}`);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
case "win32": {
|
|
143
|
+
const result = await this.runner.run(
|
|
144
|
+
"powershell",
|
|
145
|
+
["-NoProfile", "-Command", WINDOWS_STORE],
|
|
146
|
+
secret
|
|
147
|
+
);
|
|
148
|
+
if (result.code !== 0) throw new Error(`DPAPI write failed: ${result.stderr.trim()}`);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
default: {
|
|
152
|
+
const result = await this.runner.run(
|
|
153
|
+
"secret-tool",
|
|
154
|
+
["store", "--label=evrex capture credential", "service", SERVICE, "account", ACCOUNT],
|
|
155
|
+
secret
|
|
156
|
+
);
|
|
157
|
+
if (result.code !== 0) throw new Error(`secret-tool write failed: ${result.stderr.trim()}`);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/** Null when there is nothing stored, which is the normal pre-enrolment state. */
|
|
163
|
+
async retrieve() {
|
|
164
|
+
if (!await this.available()) throw new NoKeychainError(this.runner.platform);
|
|
165
|
+
switch (this.runner.platform) {
|
|
166
|
+
case "darwin": {
|
|
167
|
+
const r = await this.runner.run("security", [
|
|
168
|
+
"find-generic-password",
|
|
169
|
+
"-a",
|
|
170
|
+
ACCOUNT,
|
|
171
|
+
"-s",
|
|
172
|
+
SERVICE,
|
|
173
|
+
"-w"
|
|
174
|
+
]);
|
|
175
|
+
return r.code === 0 ? r.stdout.trim() || null : null;
|
|
176
|
+
}
|
|
177
|
+
case "win32": {
|
|
178
|
+
const r = await this.runner.run("powershell", ["-NoProfile", "-Command", WINDOWS_RETRIEVE]);
|
|
179
|
+
return r.code === 0 ? r.stdout.trim() || null : null;
|
|
180
|
+
}
|
|
181
|
+
default: {
|
|
182
|
+
const r = await this.runner.run("secret-tool", [
|
|
183
|
+
"lookup",
|
|
184
|
+
"service",
|
|
185
|
+
SERVICE,
|
|
186
|
+
"account",
|
|
187
|
+
ACCOUNT
|
|
188
|
+
]);
|
|
189
|
+
return r.code === 0 ? r.stdout.trim() || null : null;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/** Idempotent: removing a credential that is not there is not an error. */
|
|
194
|
+
async remove() {
|
|
195
|
+
if (!await this.available()) return;
|
|
196
|
+
switch (this.runner.platform) {
|
|
197
|
+
case "darwin":
|
|
198
|
+
await this.runner.run("security", [
|
|
199
|
+
"delete-generic-password",
|
|
200
|
+
"-a",
|
|
201
|
+
ACCOUNT,
|
|
202
|
+
"-s",
|
|
203
|
+
SERVICE
|
|
204
|
+
]);
|
|
205
|
+
return;
|
|
206
|
+
case "win32":
|
|
207
|
+
await this.runner.run("powershell", ["-NoProfile", "-Command", WINDOWS_REMOVE]);
|
|
208
|
+
return;
|
|
209
|
+
default:
|
|
210
|
+
await this.runner.run("secret-tool", [
|
|
211
|
+
"clear",
|
|
212
|
+
"service",
|
|
213
|
+
SERVICE,
|
|
214
|
+
"account",
|
|
215
|
+
ACCOUNT
|
|
216
|
+
]);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
WINDOWS_PATH = "$env:APPDATA\\evrex\\capture.cred";
|
|
222
|
+
WINDOWS_STORE = `
|
|
223
|
+
$ErrorActionPreference = 'Stop'
|
|
224
|
+
$p = "${WINDOWS_PATH}"
|
|
225
|
+
New-Item -ItemType Directory -Force -Path (Split-Path $p) | Out-Null
|
|
226
|
+
$secret = [Console]::In.ReadToEnd().Trim()
|
|
227
|
+
ConvertTo-SecureString $secret -AsPlainText -Force | ConvertFrom-SecureString | Set-Content -Path $p
|
|
228
|
+
`.trim();
|
|
229
|
+
WINDOWS_RETRIEVE = `
|
|
230
|
+
$ErrorActionPreference = 'Stop'
|
|
231
|
+
$p = "${WINDOWS_PATH}"
|
|
232
|
+
if (-not (Test-Path $p)) { exit 1 }
|
|
233
|
+
$sec = Get-Content $p | ConvertTo-SecureString
|
|
234
|
+
[Runtime.InteropServices.Marshal]::PtrToStringAuto(
|
|
235
|
+
[Runtime.InteropServices.Marshal]::SecureStringToBSTR($sec))
|
|
236
|
+
`.trim();
|
|
237
|
+
WINDOWS_REMOVE = `
|
|
238
|
+
$p = "${WINDOWS_PATH}"
|
|
239
|
+
if (Test-Path $p) { Remove-Item $p -Force }
|
|
240
|
+
`.trim();
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// src/import.ts
|
|
245
|
+
import { realpathSync } from "node:fs";
|
|
246
|
+
import { homedir as homedir5 } from "node:os";
|
|
247
|
+
import { fileURLToPath } from "node:url";
|
|
248
|
+
import { resolve } from "node:path";
|
|
249
|
+
|
|
250
|
+
// ../../packages/ingest-core/src/index.ts
|
|
251
|
+
import { userInfo } from "node:os";
|
|
252
|
+
|
|
253
|
+
// ../../packages/ingest-core/src/git-history.ts
|
|
254
|
+
import { execFileSync } from "node:child_process";
|
|
255
|
+
var DIFF_CAP = 2e4;
|
|
256
|
+
var FIELD_SEP = "";
|
|
257
|
+
var EVREX_SESSION_TRAILER_KEY = "Evrex-Session";
|
|
258
|
+
function git(repoPath, args, input) {
|
|
259
|
+
return execFileSync("git", args, {
|
|
260
|
+
cwd: repoPath,
|
|
261
|
+
maxBuffer: 1024 * 1024 * 64,
|
|
262
|
+
input
|
|
263
|
+
}).toString("utf-8");
|
|
264
|
+
}
|
|
265
|
+
function gitQuiet(repoPath, args) {
|
|
266
|
+
return execFileSync("git", args, {
|
|
267
|
+
cwd: repoPath,
|
|
268
|
+
maxBuffer: 1024 * 1024 * 8,
|
|
269
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
270
|
+
}).toString("utf-8");
|
|
271
|
+
}
|
|
272
|
+
function normalizeRepoRemote(url) {
|
|
273
|
+
const trimmed = url.trim();
|
|
274
|
+
if (!trimmed) return null;
|
|
275
|
+
let host;
|
|
276
|
+
let path;
|
|
277
|
+
const scpLike = /^(?:[^/@]+@)?([^/:@]+\.[^/:@]+):(?!\/)(.+)$/.exec(trimmed);
|
|
278
|
+
if (scpLike?.[1] && scpLike[2]) {
|
|
279
|
+
host = scpLike[1];
|
|
280
|
+
path = scpLike[2];
|
|
281
|
+
} else {
|
|
282
|
+
let parsed;
|
|
283
|
+
try {
|
|
284
|
+
parsed = new URL(trimmed);
|
|
285
|
+
} catch {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
if (parsed.protocol === "file:" || !parsed.hostname) return null;
|
|
289
|
+
host = parsed.hostname;
|
|
290
|
+
path = parsed.pathname;
|
|
291
|
+
}
|
|
292
|
+
const cleanedPath = path.replace(/^\/+/, "").replace(/\/+$/, "").replace(/\.git$/i, "");
|
|
293
|
+
if (!cleanedPath) return null;
|
|
294
|
+
return `${host}/${cleanedPath}`.toLowerCase();
|
|
295
|
+
}
|
|
296
|
+
function repoIdFromRemote(url) {
|
|
297
|
+
const normalized = normalizeRepoRemote(url);
|
|
298
|
+
return normalized ? `remote:${normalized}` : null;
|
|
299
|
+
}
|
|
300
|
+
function repoIdFromRootCommit(sha) {
|
|
301
|
+
return `root:${sha}`;
|
|
302
|
+
}
|
|
303
|
+
function repoIdFromPath(repoPath) {
|
|
304
|
+
return `path:${repoPath}`;
|
|
305
|
+
}
|
|
306
|
+
function firstRemoteUrl(repoPath) {
|
|
307
|
+
try {
|
|
308
|
+
const origin = gitQuiet(repoPath, ["remote", "get-url", "origin"]).trim();
|
|
309
|
+
if (origin) return origin;
|
|
310
|
+
} catch {
|
|
311
|
+
}
|
|
312
|
+
try {
|
|
313
|
+
const names = gitQuiet(repoPath, ["remote"]).split("\n").map((n) => n.trim()).filter(Boolean).sort();
|
|
314
|
+
for (const name of names) {
|
|
315
|
+
const url = gitQuiet(repoPath, ["remote", "get-url", name]).trim();
|
|
316
|
+
if (url) return url;
|
|
317
|
+
}
|
|
318
|
+
} catch {
|
|
319
|
+
}
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
function rootCommitSha(repoPath) {
|
|
323
|
+
try {
|
|
324
|
+
const shas = gitQuiet(repoPath, ["rev-list", "--max-parents=0", "HEAD"]).split("\n").map((s) => s.trim()).filter(Boolean).sort();
|
|
325
|
+
return shas[0] ?? null;
|
|
326
|
+
} catch {
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
function deriveRepoId(repoPath) {
|
|
331
|
+
const remote = firstRemoteUrl(repoPath);
|
|
332
|
+
if (remote) {
|
|
333
|
+
const fromRemote = repoIdFromRemote(remote);
|
|
334
|
+
if (fromRemote) return fromRemote;
|
|
335
|
+
}
|
|
336
|
+
const root = rootCommitSha(repoPath);
|
|
337
|
+
if (root) return repoIdFromRootCommit(root);
|
|
338
|
+
return repoIdFromPath(repoPath);
|
|
339
|
+
}
|
|
340
|
+
function getGitUserName(repoPath) {
|
|
341
|
+
try {
|
|
342
|
+
const name = git(repoPath, ["config", "user.name"]).trim();
|
|
343
|
+
return name || null;
|
|
344
|
+
} catch {
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
function listShas(repoPath) {
|
|
349
|
+
const out = git(repoPath, ["rev-list", "--reverse", "HEAD"]).trim();
|
|
350
|
+
return out.length ? out.split("\n") : [];
|
|
351
|
+
}
|
|
352
|
+
function commitMeta(repoPath, sha) {
|
|
353
|
+
const line = git(repoPath, [
|
|
354
|
+
"show",
|
|
355
|
+
"-s",
|
|
356
|
+
`--format=%H${FIELD_SEP}%an${FIELD_SEP}%ae${FIELD_SEP}%aI`,
|
|
357
|
+
sha
|
|
358
|
+
]).trim();
|
|
359
|
+
const parts = line.split(FIELD_SEP);
|
|
360
|
+
const message = git(repoPath, ["show", "-s", "--format=%B", sha]).replace(/\n+$/, "");
|
|
361
|
+
return {
|
|
362
|
+
sha: parts[0] ?? sha,
|
|
363
|
+
author: parts[1] ?? "",
|
|
364
|
+
authorEmail: parts[2] ?? "",
|
|
365
|
+
ts: parts[3] ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
366
|
+
message
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
function extractTrailer(repoPath, message, key) {
|
|
370
|
+
try {
|
|
371
|
+
const out = git(
|
|
372
|
+
repoPath,
|
|
373
|
+
["interpret-trailers", "--parse", "--no-divider"],
|
|
374
|
+
message
|
|
375
|
+
).trim();
|
|
376
|
+
for (const line of out.split("\n")) {
|
|
377
|
+
const idx = line.indexOf(":");
|
|
378
|
+
if (idx === -1) continue;
|
|
379
|
+
const trailerKey = line.slice(0, idx).trim();
|
|
380
|
+
if (trailerKey.toLowerCase() === key.toLowerCase()) {
|
|
381
|
+
return line.slice(idx + 1).trim();
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return null;
|
|
385
|
+
} catch {
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
function commitBranch(repoPath, sha) {
|
|
390
|
+
try {
|
|
391
|
+
const out = git(repoPath, ["branch", "--contains", sha, "--format=%(refname:short)"]).trim();
|
|
392
|
+
const first = out.split("\n").find((b) => b.length > 0);
|
|
393
|
+
return first ?? null;
|
|
394
|
+
} catch {
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
function numstat(repoPath, sha) {
|
|
399
|
+
const out = git(repoPath, ["show", "--numstat", "--format=", sha]).trim();
|
|
400
|
+
const map = /* @__PURE__ */ new Map();
|
|
401
|
+
if (!out) return map;
|
|
402
|
+
for (const line of out.split("\n")) {
|
|
403
|
+
const [ins, del, path] = line.split(" ");
|
|
404
|
+
if (!path) continue;
|
|
405
|
+
map.set(path, {
|
|
406
|
+
insertions: !ins || ins === "-" ? 0 : Number.parseInt(ins, 10) || 0,
|
|
407
|
+
deletions: !del || del === "-" ? 0 : Number.parseInt(del, 10) || 0
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
return map;
|
|
411
|
+
}
|
|
412
|
+
function parseDiffBlocks(diff) {
|
|
413
|
+
const map = /* @__PURE__ */ new Map();
|
|
414
|
+
if (!diff.trim()) return map;
|
|
415
|
+
const blocks = diff.split(/^diff --git /m).filter(Boolean);
|
|
416
|
+
for (const block of blocks) {
|
|
417
|
+
const full = `diff --git ${block}`;
|
|
418
|
+
const pathMatch = block.match(/^a\/(.+?) b\/(.+?)\n/);
|
|
419
|
+
const path = pathMatch?.[2] ?? block.match(/^\S+/)?.[0] ?? "unknown";
|
|
420
|
+
const hunkHeaders = [...full.matchAll(/^@@ .+? @@.*$/gm)].map((m) => m[0]);
|
|
421
|
+
map.set(path, {
|
|
422
|
+
hunkHeaders,
|
|
423
|
+
diffText: full.length > DIFF_CAP ? full.slice(0, DIFF_CAP) : full
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
return map;
|
|
427
|
+
}
|
|
428
|
+
function commitFiles(repoPath, sha) {
|
|
429
|
+
const stats = numstat(repoPath, sha);
|
|
430
|
+
const diff = git(repoPath, ["show", "--unified=0", "--format=", sha]);
|
|
431
|
+
const blocks = parseDiffBlocks(diff);
|
|
432
|
+
const paths = /* @__PURE__ */ new Set([...stats.keys(), ...blocks.keys()]);
|
|
433
|
+
const files = [];
|
|
434
|
+
for (const path of paths) {
|
|
435
|
+
const stat = stats.get(path) ?? { insertions: 0, deletions: 0 };
|
|
436
|
+
const block = blocks.get(path) ?? { hunkHeaders: [], diffText: "" };
|
|
437
|
+
files.push({
|
|
438
|
+
path,
|
|
439
|
+
insertions: stat.insertions,
|
|
440
|
+
deletions: stat.deletions,
|
|
441
|
+
hunkHeaders: block.hunkHeaders,
|
|
442
|
+
diffText: block.diffText,
|
|
443
|
+
diffTruncated: block.diffText.length >= DIFF_CAP
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
return files;
|
|
447
|
+
}
|
|
448
|
+
function parseGitLog(repoPath, repoId, known) {
|
|
449
|
+
const shas = listShas(repoPath).filter((sha) => !known?.has(sha));
|
|
450
|
+
const id = repoId ?? deriveRepoId(repoPath);
|
|
451
|
+
return shas.map((sha) => {
|
|
452
|
+
const meta = commitMeta(repoPath, sha);
|
|
453
|
+
return {
|
|
454
|
+
sha: meta.sha,
|
|
455
|
+
repoId: id,
|
|
456
|
+
repoPath,
|
|
457
|
+
author: meta.author,
|
|
458
|
+
authorEmail: meta.authorEmail,
|
|
459
|
+
ts: meta.ts,
|
|
460
|
+
message: meta.message,
|
|
461
|
+
branch: commitBranch(repoPath, sha),
|
|
462
|
+
evrexSessionTrailer: extractTrailer(repoPath, meta.message, EVREX_SESSION_TRAILER_KEY),
|
|
463
|
+
files: commitFiles(repoPath, sha)
|
|
464
|
+
};
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// ../../packages/ingest-core/src/claude-sessions.ts
|
|
469
|
+
import { existsSync, readdirSync, readFileSync, statSync as statSync2 } from "node:fs";
|
|
470
|
+
|
|
471
|
+
// ../../packages/ingest-core/src/incremental.ts
|
|
472
|
+
import { statSync } from "node:fs";
|
|
473
|
+
function planRead(path, cursor) {
|
|
474
|
+
let size;
|
|
475
|
+
let modifiedAt;
|
|
476
|
+
if (cursor && (cursor.parserVersion ?? 0) < PARSER_VERSION) {
|
|
477
|
+
cursor = void 0;
|
|
478
|
+
}
|
|
479
|
+
try {
|
|
480
|
+
const stat = statSync(path);
|
|
481
|
+
size = stat.size;
|
|
482
|
+
modifiedAt = stat.mtimeMs;
|
|
483
|
+
} catch {
|
|
484
|
+
return { path, from: 0, reason: "unchanged" };
|
|
485
|
+
}
|
|
486
|
+
if (!cursor) return { path, from: 0, reason: "new" };
|
|
487
|
+
if (size < cursor.size) return { path, from: 0, reason: "rewritten" };
|
|
488
|
+
if (size === cursor.size && modifiedAt <= cursor.modifiedAt) {
|
|
489
|
+
return { path, from: cursor.offset, reason: "unchanged" };
|
|
490
|
+
}
|
|
491
|
+
if (size === cursor.offset) return { path, from: cursor.offset, reason: "unchanged" };
|
|
492
|
+
return { path, from: cursor.offset, reason: "appended" };
|
|
493
|
+
}
|
|
494
|
+
function advanceCursor(path, consumedTo) {
|
|
495
|
+
let size = consumedTo;
|
|
496
|
+
let modifiedAt = Date.now();
|
|
497
|
+
try {
|
|
498
|
+
const stat = statSync(path);
|
|
499
|
+
size = stat.size;
|
|
500
|
+
modifiedAt = stat.mtimeMs;
|
|
501
|
+
} catch {
|
|
502
|
+
}
|
|
503
|
+
return { offset: consumedTo, size, modifiedAt, parserVersion: PARSER_VERSION };
|
|
504
|
+
}
|
|
505
|
+
var PARSER_VERSION = 4;
|
|
506
|
+
|
|
507
|
+
// ../../packages/ingest-core/src/claude-sessions.ts
|
|
508
|
+
import { homedir } from "node:os";
|
|
509
|
+
import { join } from "node:path";
|
|
510
|
+
|
|
511
|
+
// ../../packages/ingest-core/src/redact.ts
|
|
512
|
+
var PATTERNS = [
|
|
513
|
+
{ type: "private_key", regex: /-----BEGIN(?: [A-Z]+)? PRIVATE KEY-----[\s\S]*?-----END(?: [A-Z]+)? PRIVATE KEY-----/g },
|
|
514
|
+
{ type: "aws_access_key", regex: /AKIA[0-9A-Z]{16}/g },
|
|
515
|
+
{ type: "github_token", regex: /gh[pousr]_[A-Za-z0-9]{36,}/g },
|
|
516
|
+
{ type: "slack_token", regex: /xox[baprs]-[A-Za-z0-9-]{10,}/g },
|
|
517
|
+
{ type: "stripe_key", regex: /sk_(live|test)_[A-Za-z0-9]{24,}/g },
|
|
518
|
+
{ type: "openai_key", regex: /sk-[A-Za-z0-9]{20,}/g },
|
|
519
|
+
{ type: "anthropic_key", regex: /sk-ant-[A-Za-z0-9-_]{20,}/g },
|
|
520
|
+
{ type: "jwt", regex: /eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g },
|
|
521
|
+
{ type: "bearer_token", regex: /Bearer\s+[A-Za-z0-9\-_.]{20,}/g },
|
|
522
|
+
{
|
|
523
|
+
type: "env_secret",
|
|
524
|
+
regex: /\b([A-Z0-9_]*(?:_KEY|_SECRET|_TOKEN|_PASSWORD|PASSWD)[A-Z0-9_]*)\s*[:=]\s*["']?[^\s"'\n]{6,}["']?/g
|
|
525
|
+
}
|
|
526
|
+
];
|
|
527
|
+
function redactJsonValue(value) {
|
|
528
|
+
let count = 0;
|
|
529
|
+
const walk = (v) => {
|
|
530
|
+
if (typeof v === "string") {
|
|
531
|
+
const r = redactSecrets(v);
|
|
532
|
+
count += r.count;
|
|
533
|
+
return r.text;
|
|
534
|
+
}
|
|
535
|
+
if (Array.isArray(v)) return v.map(walk);
|
|
536
|
+
if (v && typeof v === "object") {
|
|
537
|
+
const out = {};
|
|
538
|
+
for (const [k, val] of Object.entries(v)) {
|
|
539
|
+
out[k] = walk(val);
|
|
540
|
+
}
|
|
541
|
+
return out;
|
|
542
|
+
}
|
|
543
|
+
return v;
|
|
544
|
+
};
|
|
545
|
+
return { value: walk(value), count };
|
|
546
|
+
}
|
|
547
|
+
function redactSecrets(input) {
|
|
548
|
+
let text = input;
|
|
549
|
+
let count = 0;
|
|
550
|
+
for (const { type, regex } of PATTERNS) {
|
|
551
|
+
text = text.replace(regex, (match, group1) => {
|
|
552
|
+
count += 1;
|
|
553
|
+
if (type === "env_secret" && group1) {
|
|
554
|
+
return `${group1}=[REDACTED:${type}]`;
|
|
555
|
+
}
|
|
556
|
+
return `[REDACTED:${type}]`;
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
return { text, count };
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// ../../packages/ingest-core/src/subject-linking.ts
|
|
563
|
+
function committedSubjects(command) {
|
|
564
|
+
const out = [];
|
|
565
|
+
const invocation = /\bgit\s+(?:-C\s+\S+\s+)?commit\b/g;
|
|
566
|
+
for (let found = invocation.exec(command); found !== null; found = invocation.exec(command)) {
|
|
567
|
+
const rest = command.slice(found.index + found[0].length);
|
|
568
|
+
const heredoc = /^[^\n]*<<-?\s*(['"]?)(\w+)\1[^\n]*\r?\n([^\n]*)/.exec(rest);
|
|
569
|
+
if (heredoc) {
|
|
570
|
+
const subject = heredoc[3].trim();
|
|
571
|
+
if (subject) out.push(subject);
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
const inline = /^[^\n]*?-m\s+(['"])([\s\S]*?)\1/.exec(rest);
|
|
575
|
+
if (inline) {
|
|
576
|
+
const subject = inline[2].split("\n")[0].trim();
|
|
577
|
+
if (subject) out.push(subject);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
return out;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// ../../packages/ingest-core/src/types.ts
|
|
584
|
+
var CONVERSATION_KINDS = [
|
|
585
|
+
"claude-code",
|
|
586
|
+
"cursor",
|
|
587
|
+
"codex",
|
|
588
|
+
"gemini",
|
|
589
|
+
"slack"
|
|
590
|
+
];
|
|
591
|
+
var REFERENCE_KINDS = ["linear", "jira", "confluence"];
|
|
592
|
+
var SOURCE_KINDS = [
|
|
593
|
+
...CONVERSATION_KINDS,
|
|
594
|
+
...REFERENCE_KINDS
|
|
595
|
+
];
|
|
596
|
+
var EMPTY_USAGE = {
|
|
597
|
+
inputTokens: null,
|
|
598
|
+
outputTokens: null,
|
|
599
|
+
cacheReadTokens: null,
|
|
600
|
+
cacheWriteTokens: null,
|
|
601
|
+
model: null
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
// ../../packages/ingest-core/src/claude-sessions.ts
|
|
605
|
+
var MIN_MEANINGFUL_LINE_LENGTH = 6;
|
|
606
|
+
function meaningfulLines(lines) {
|
|
607
|
+
return lines.map((l) => l.trim()).filter((l) => l.length >= MIN_MEANINGFUL_LINE_LENGTH);
|
|
608
|
+
}
|
|
609
|
+
function extractEditedLines(toolUseResult) {
|
|
610
|
+
if (!toolUseResult || typeof toolUseResult !== "object") return null;
|
|
611
|
+
const r = toolUseResult;
|
|
612
|
+
if (!r.filePath) return null;
|
|
613
|
+
const added = [];
|
|
614
|
+
const removed = [];
|
|
615
|
+
if (Array.isArray(r.structuredPatch) && r.structuredPatch.length > 0) {
|
|
616
|
+
for (const hunk of r.structuredPatch) {
|
|
617
|
+
for (const line of hunk.lines ?? []) {
|
|
618
|
+
if (line.startsWith("+")) added.push(line.slice(1));
|
|
619
|
+
else if (line.startsWith("-")) removed.push(line.slice(1));
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
} else if (r.type === "create" && typeof r.content === "string") {
|
|
623
|
+
added.push(...r.content.split("\n"));
|
|
624
|
+
} else {
|
|
625
|
+
return null;
|
|
626
|
+
}
|
|
627
|
+
const meaningfulAdded = meaningfulLines(added);
|
|
628
|
+
const meaningfulRemoved = meaningfulLines(removed);
|
|
629
|
+
if (meaningfulAdded.length === 0 && meaningfulRemoved.length === 0) return null;
|
|
630
|
+
return { path: r.filePath, added: meaningfulAdded, removed: meaningfulRemoved };
|
|
631
|
+
}
|
|
632
|
+
var MAX_TURN_TEXT_LENGTH = 4e3;
|
|
633
|
+
function slugifyCwd(repoPath) {
|
|
634
|
+
return repoPath.replace(/\//g, "-");
|
|
635
|
+
}
|
|
636
|
+
function claudeProjectsDir() {
|
|
637
|
+
return join(homedir(), ".claude", "projects");
|
|
638
|
+
}
|
|
639
|
+
function findSessionFiles(repoPath) {
|
|
640
|
+
const dir = join(claudeProjectsDir(), slugifyCwd(repoPath));
|
|
641
|
+
if (!existsSync(dir)) return [];
|
|
642
|
+
return readdirSync(dir).filter((name) => name.endsWith(".jsonl")).map((name) => join(dir, name));
|
|
643
|
+
}
|
|
644
|
+
var FILE_PATH_TOOLS = /* @__PURE__ */ new Set(["Read", "Edit", "Write", "NotebookEdit"]);
|
|
645
|
+
var PATH_TOKEN_RE = /(?:[.~]?\/)?(?:[\w-]+\/)+[\w.-]+/g;
|
|
646
|
+
function extractPathsFromText(text) {
|
|
647
|
+
const matches = text.match(PATH_TOKEN_RE) ?? [];
|
|
648
|
+
return [...new Set(matches)].filter((p) => p.length > 3 && p.length < 300);
|
|
649
|
+
}
|
|
650
|
+
function blockText(content) {
|
|
651
|
+
if (typeof content === "string") return content;
|
|
652
|
+
if (Array.isArray(content)) {
|
|
653
|
+
return content.map((c) => typeof c === "string" ? c : c?.text ?? "").filter(Boolean).join("\n");
|
|
654
|
+
}
|
|
655
|
+
return "";
|
|
656
|
+
}
|
|
657
|
+
function extractFromAssistantContent(content) {
|
|
658
|
+
const textParts = [];
|
|
659
|
+
const filesTouched = [];
|
|
660
|
+
for (const block of content) {
|
|
661
|
+
if (block.type === "text" && block.text) {
|
|
662
|
+
textParts.push(block.text);
|
|
663
|
+
for (const p of extractPathsFromText(block.text)) {
|
|
664
|
+
filesTouched.push({ path: p, source: "prose" });
|
|
665
|
+
}
|
|
666
|
+
} else if (block.type === "tool_use") {
|
|
667
|
+
const name = block.name ?? "tool";
|
|
668
|
+
const input = block.input ?? {};
|
|
669
|
+
if (FILE_PATH_TOOLS.has(name) && typeof input.file_path === "string") {
|
|
670
|
+
textParts.push(`[tool_call: ${name}] ${input.file_path}`);
|
|
671
|
+
filesTouched.push({ path: input.file_path, source: "tool_path", tool: name });
|
|
672
|
+
} else if (name === "Bash" && typeof input.command === "string") {
|
|
673
|
+
const desc = typeof input.description === "string" ? input.description : input.command;
|
|
674
|
+
textParts.push(`[tool_call: Bash] ${desc}`);
|
|
675
|
+
for (const p of extractPathsFromText(input.command)) {
|
|
676
|
+
filesTouched.push({ path: p, source: "tool_bash", tool: "Bash" });
|
|
677
|
+
}
|
|
678
|
+
} else {
|
|
679
|
+
textParts.push(`[tool_call: ${name}]`);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
return { text: textParts.join("\n"), filesTouched };
|
|
684
|
+
}
|
|
685
|
+
var SYNTHETIC_CONTENT_RE = /^\s*(<task-notification>|<system-reminder>|\[SYSTEM NOTIFICATION)/;
|
|
686
|
+
function extractFromUserContent(content) {
|
|
687
|
+
if (typeof content === "string") {
|
|
688
|
+
return {
|
|
689
|
+
text: content,
|
|
690
|
+
filesTouched: extractPathsFromText(content).map((path) => ({ path, source: "prose" })),
|
|
691
|
+
isSyntheticInput: SYNTHETIC_CONTENT_RE.test(content)
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
if (Array.isArray(content)) {
|
|
695
|
+
const toolParts = [];
|
|
696
|
+
const textParts = [];
|
|
697
|
+
for (const block of content) {
|
|
698
|
+
if (block.type === "tool_result") {
|
|
699
|
+
toolParts.push(blockText(block.content));
|
|
700
|
+
} else if (block.type === "text" && typeof block.text === "string") {
|
|
701
|
+
textParts.push(block.text);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
if (toolParts.length > 0) {
|
|
705
|
+
return {
|
|
706
|
+
text: toolParts.join("\n"),
|
|
707
|
+
filesTouched: [],
|
|
708
|
+
isSyntheticInput: true
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
const text = textParts.join("\n");
|
|
712
|
+
return {
|
|
713
|
+
text,
|
|
714
|
+
// Same treatment the string branch gives prose, so a path a person
|
|
715
|
+
// names in a block-formatted message is found too.
|
|
716
|
+
filesTouched: extractPathsFromText(text).map((path) => ({
|
|
717
|
+
path,
|
|
718
|
+
source: "prose"
|
|
719
|
+
})),
|
|
720
|
+
isSyntheticInput: SYNTHETIC_CONTENT_RE.test(text)
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
return { text: "", filesTouched: [], isSyntheticInput: false };
|
|
724
|
+
}
|
|
725
|
+
function usageOnce(record, billed) {
|
|
726
|
+
const message = record.message;
|
|
727
|
+
const raw = message?.usage;
|
|
728
|
+
if (!raw || typeof raw !== "object") return { ...EMPTY_USAGE };
|
|
729
|
+
const messageId = message?.id;
|
|
730
|
+
if (!messageId || billed.has(messageId)) return { ...EMPTY_USAGE };
|
|
731
|
+
billed.add(messageId);
|
|
732
|
+
const num = (key) => {
|
|
733
|
+
const value = raw[key];
|
|
734
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
735
|
+
};
|
|
736
|
+
return {
|
|
737
|
+
inputTokens: num("input_tokens"),
|
|
738
|
+
outputTokens: num("output_tokens"),
|
|
739
|
+
cacheReadTokens: num("cache_read_input_tokens"),
|
|
740
|
+
cacheWriteTokens: num("cache_creation_input_tokens"),
|
|
741
|
+
model: typeof message?.model === "string" ? message.model : null
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
function parseSessionFile(filePath, repoPath, repoId = deriveRepoId(repoPath)) {
|
|
745
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
746
|
+
const lines = raw.split("\n").filter((l) => l.trim().length > 0);
|
|
747
|
+
const turns = [];
|
|
748
|
+
const billedMessages = /* @__PURE__ */ new Set();
|
|
749
|
+
let sessionId = null;
|
|
750
|
+
const cwd = repoPath;
|
|
751
|
+
let aiTitle = null;
|
|
752
|
+
let totalRedactions = 0;
|
|
753
|
+
for (const line of lines) {
|
|
754
|
+
let record;
|
|
755
|
+
try {
|
|
756
|
+
record = JSON.parse(line);
|
|
757
|
+
} catch {
|
|
758
|
+
continue;
|
|
759
|
+
}
|
|
760
|
+
if (record.type === "ai-title" && typeof record.aiTitle === "string") {
|
|
761
|
+
aiTitle = record.aiTitle;
|
|
762
|
+
continue;
|
|
763
|
+
}
|
|
764
|
+
if (record.type !== "user" && record.type !== "assistant") continue;
|
|
765
|
+
const id = record.uuid;
|
|
766
|
+
const ts = record.timestamp;
|
|
767
|
+
if (!id || !ts) continue;
|
|
768
|
+
sessionId ??= record.sessionId ?? record.session_id ?? null;
|
|
769
|
+
let text = "";
|
|
770
|
+
let filesTouched = [];
|
|
771
|
+
let editedLines = [];
|
|
772
|
+
let isSyntheticInput = false;
|
|
773
|
+
if (record.type === "assistant") {
|
|
774
|
+
const content = record.message?.content;
|
|
775
|
+
if (Array.isArray(content)) {
|
|
776
|
+
const extracted = extractFromAssistantContent(content);
|
|
777
|
+
text = extracted.text;
|
|
778
|
+
filesTouched = extracted.filesTouched;
|
|
779
|
+
}
|
|
780
|
+
} else {
|
|
781
|
+
const extracted = extractFromUserContent(record.message?.content);
|
|
782
|
+
text = extracted.text;
|
|
783
|
+
filesTouched = extracted.filesTouched;
|
|
784
|
+
isSyntheticInput = extracted.isSyntheticInput;
|
|
785
|
+
const edited = extractEditedLines(record.toolUseResult);
|
|
786
|
+
if (edited) editedLines = [edited];
|
|
787
|
+
}
|
|
788
|
+
const redacted = redactSecrets(text);
|
|
789
|
+
totalRedactions += redacted.count;
|
|
790
|
+
turns.push({
|
|
791
|
+
id,
|
|
792
|
+
sessionId: sessionId ?? "unknown",
|
|
793
|
+
role: record.type,
|
|
794
|
+
ts,
|
|
795
|
+
text: redacted.text.slice(0, MAX_TURN_TEXT_LENGTH),
|
|
796
|
+
filesTouched,
|
|
797
|
+
editedLines,
|
|
798
|
+
parentUuid: record.parentUuid ?? null,
|
|
799
|
+
isSidechain: Boolean(record.isSidechain),
|
|
800
|
+
redacted: redacted.count > 0,
|
|
801
|
+
isSyntheticInput,
|
|
802
|
+
usage: usageOnce(record, billedMessages)
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
if (!sessionId || turns.length === 0) return null;
|
|
806
|
+
const sortedTs = turns.map((t) => t.ts).sort();
|
|
807
|
+
const rawContent = lines.map((line) => {
|
|
808
|
+
try {
|
|
809
|
+
return JSON.stringify(redactJsonValue(JSON.parse(line)).value);
|
|
810
|
+
} catch {
|
|
811
|
+
return redactSecrets(line).text;
|
|
812
|
+
}
|
|
813
|
+
}).join("\n");
|
|
814
|
+
if (totalRedactions > 0) {
|
|
815
|
+
console.log(`[ingest-core] redacted ${totalRedactions} potential secret(s) in session ${sessionId}`);
|
|
816
|
+
}
|
|
817
|
+
return {
|
|
818
|
+
id: sessionId,
|
|
819
|
+
agentKind: "claude-code",
|
|
820
|
+
repoId,
|
|
821
|
+
cwd,
|
|
822
|
+
startedAt: sortedTs[0] ?? null,
|
|
823
|
+
endedAt: sortedTs[sortedTs.length - 1] ?? null,
|
|
824
|
+
turnCount: turns.length,
|
|
825
|
+
aiTitle,
|
|
826
|
+
author: null,
|
|
827
|
+
// filled in by the caller — see getGitUserName in git-history.ts
|
|
828
|
+
sourceFile: filePath,
|
|
829
|
+
redactionCount: totalRedactions,
|
|
830
|
+
committedSubjects: collectCommittedSubjects(lines),
|
|
831
|
+
rawContent,
|
|
832
|
+
rawFormat: "jsonl",
|
|
833
|
+
turns
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
function collectCommittedSubjects(lines) {
|
|
837
|
+
const subjects = /* @__PURE__ */ new Set();
|
|
838
|
+
for (const line of lines) {
|
|
839
|
+
if (!line.includes("git commit")) continue;
|
|
840
|
+
let record;
|
|
841
|
+
try {
|
|
842
|
+
record = JSON.parse(line);
|
|
843
|
+
} catch {
|
|
844
|
+
continue;
|
|
845
|
+
}
|
|
846
|
+
const content = record?.message?.content;
|
|
847
|
+
if (!Array.isArray(content)) continue;
|
|
848
|
+
for (const block of content) {
|
|
849
|
+
if (block?.type !== "tool_use") continue;
|
|
850
|
+
const command = block?.input?.command;
|
|
851
|
+
if (typeof command !== "string") continue;
|
|
852
|
+
for (const subject of committedSubjects(command)) subjects.add(subject);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
return [...subjects];
|
|
856
|
+
}
|
|
857
|
+
function parseAllSessions(repoPath, repoId, cursors = {}) {
|
|
858
|
+
const id = repoId ?? deriveRepoId(repoPath);
|
|
859
|
+
const next = { ...cursors };
|
|
860
|
+
const sessions = [];
|
|
861
|
+
let skipped = 0;
|
|
862
|
+
for (const file of findSessionFiles(repoPath)) {
|
|
863
|
+
const plan = planRead(file, cursors[file]);
|
|
864
|
+
if (plan.reason === "unchanged") {
|
|
865
|
+
skipped++;
|
|
866
|
+
continue;
|
|
867
|
+
}
|
|
868
|
+
const parsed = parseSessionFile(file, repoPath, id);
|
|
869
|
+
if (!parsed) continue;
|
|
870
|
+
sessions.push(parsed);
|
|
871
|
+
next[file] = advanceCursor(file, statSync2(file).size);
|
|
872
|
+
}
|
|
873
|
+
return { sessions, cursors: next, skipped };
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// ../../packages/ingest-core/src/cursor-sessions.ts
|
|
877
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
878
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
879
|
+
import { homedir as homedir2 } from "node:os";
|
|
880
|
+
import { join as join2, sep } from "node:path";
|
|
881
|
+
|
|
882
|
+
// ../../packages/ingest-core/src/derive-uuid.ts
|
|
883
|
+
import { createHash } from "node:crypto";
|
|
884
|
+
function deriveUuid(name) {
|
|
885
|
+
const h = createHash("sha1").update(name).digest("hex");
|
|
886
|
+
const variant = (parseInt(h.slice(16, 17) || "0", 16) & 3 | 8).toString(16);
|
|
887
|
+
const s = h.slice(0, 12) + // time-low + time-mid
|
|
888
|
+
"5" + // version 5 (name-based, SHA-1)
|
|
889
|
+
h.slice(13, 16) + variant + h.slice(17, 32);
|
|
890
|
+
return `${s.slice(0, 8)}-${s.slice(8, 12)}-${s.slice(12, 16)}-${s.slice(16, 20)}-${s.slice(20, 32)}`;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// ../../packages/ingest-core/src/cursor-sessions.ts
|
|
894
|
+
var MAX_TURN_TEXT_LENGTH2 = 4e3;
|
|
895
|
+
var PATH_TOKEN_RE2 = /(?:[.~]?\/)?(?:[\w-]+\/)+[\w.-]+/g;
|
|
896
|
+
function extractPathsFromText2(text) {
|
|
897
|
+
const matches = text.match(PATH_TOKEN_RE2) ?? [];
|
|
898
|
+
return [...new Set(matches)].filter((p) => p.length > 3 && p.length < 300);
|
|
899
|
+
}
|
|
900
|
+
function cursorStateDbPath() {
|
|
901
|
+
const home = homedir2();
|
|
902
|
+
if (process.platform === "darwin") {
|
|
903
|
+
return join2(home, "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb");
|
|
904
|
+
}
|
|
905
|
+
if (process.platform === "win32") {
|
|
906
|
+
const appData = process.env.APPDATA ?? join2(home, "AppData", "Roaming");
|
|
907
|
+
return join2(appData, "Cursor", "User", "globalStorage", "state.vscdb");
|
|
908
|
+
}
|
|
909
|
+
return join2(home, ".config", "Cursor", "User", "globalStorage", "state.vscdb");
|
|
910
|
+
}
|
|
911
|
+
function sqliteJson(dbPath, sql) {
|
|
912
|
+
try {
|
|
913
|
+
const out = execFileSync2("sqlite3", ["-readonly", "-json", dbPath, sql], {
|
|
914
|
+
maxBuffer: 1024 * 1024 * 256,
|
|
915
|
+
timeout: 6e4
|
|
916
|
+
}).toString("utf-8").trim();
|
|
917
|
+
if (!out) return [];
|
|
918
|
+
return JSON.parse(out);
|
|
919
|
+
} catch {
|
|
920
|
+
return [];
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
function sqlLiteral(value) {
|
|
924
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
925
|
+
}
|
|
926
|
+
function deriveCursorSessionId(composerId, repoPath) {
|
|
927
|
+
return deriveUuid(`evrex-cursor-session\0${composerId}\0${repoPath}`);
|
|
928
|
+
}
|
|
929
|
+
function deriveCursorTurnId(bubbleId, repoPath) {
|
|
930
|
+
return deriveUuid(`evrex-cursor-turn\0${bubbleId}\0${repoPath}`);
|
|
931
|
+
}
|
|
932
|
+
function loadComposers(dbPath) {
|
|
933
|
+
return sqliteJson(
|
|
934
|
+
dbPath,
|
|
935
|
+
`SELECT
|
|
936
|
+
json_extract(value,'$.composerId') AS composerId,
|
|
937
|
+
json_extract(value,'$.name') AS name,
|
|
938
|
+
json_extract(value,'$.createdAt') AS createdAt,
|
|
939
|
+
json_extract(value,'$.lastUpdatedAt') AS lastUpdatedAt,
|
|
940
|
+
json_extract(value,'$.workspaceIdentifier.uri.path') AS workspacePath,
|
|
941
|
+
json_extract(value,'$.fullConversationHeadersOnly') AS headers
|
|
942
|
+
FROM cursorDiskKV
|
|
943
|
+
WHERE key GLOB 'composerData:*'`
|
|
944
|
+
);
|
|
945
|
+
}
|
|
946
|
+
function composerIdsReferencingRepo(dbPath, repoPath) {
|
|
947
|
+
const needle = sqlLiteral(`%${repoPath}%`);
|
|
948
|
+
const rows = sqliteJson(
|
|
949
|
+
dbPath,
|
|
950
|
+
`SELECT DISTINCT substr(key, 10, 36) AS composerId
|
|
951
|
+
FROM cursorDiskKV
|
|
952
|
+
WHERE key GLOB 'bubbleId:*' AND value LIKE ${needle}`
|
|
953
|
+
);
|
|
954
|
+
return rows.map((r) => r.composerId).filter((id) => !!id);
|
|
955
|
+
}
|
|
956
|
+
function bubbleIdsReferencingRepo(dbPath, composerId, repoPath) {
|
|
957
|
+
const prefix = sqlLiteral(`bubbleId:${composerId}:%`);
|
|
958
|
+
const needle = sqlLiteral(`%${repoPath}%`);
|
|
959
|
+
const rows = sqliteJson(
|
|
960
|
+
dbPath,
|
|
961
|
+
`SELECT substr(key, ${10 + composerId.length + 1}) AS bubbleId
|
|
962
|
+
FROM cursorDiskKV
|
|
963
|
+
WHERE key LIKE ${prefix} AND value LIKE ${needle}`
|
|
964
|
+
);
|
|
965
|
+
return new Set(rows.map((r) => r.bubbleId).filter((id) => !!id));
|
|
966
|
+
}
|
|
967
|
+
function loadBubbles(dbPath, composerId) {
|
|
968
|
+
const prefix = sqlLiteral(`bubbleId:${composerId}:%`);
|
|
969
|
+
const rows = sqliteJson(
|
|
970
|
+
dbPath,
|
|
971
|
+
`SELECT
|
|
972
|
+
substr(key, ${10 + composerId.length + 1}) AS bubbleId,
|
|
973
|
+
json_extract(value,'$.type') AS type,
|
|
974
|
+
json_extract(value,'$.text') AS text,
|
|
975
|
+
json_extract(value,'$.thinking.text') AS thinking,
|
|
976
|
+
json_extract(value,'$.toolFormerData.name') AS toolName,
|
|
977
|
+
json_extract(value,'$.toolFormerData.rawArgs') AS rawArgs,
|
|
978
|
+
json_extract(value,'$.createdAt') AS createdAt
|
|
979
|
+
FROM cursorDiskKV
|
|
980
|
+
WHERE key LIKE ${prefix}`
|
|
981
|
+
);
|
|
982
|
+
const map = /* @__PURE__ */ new Map();
|
|
983
|
+
for (const r of rows) {
|
|
984
|
+
if (r.bubbleId) map.set(r.bubbleId, r);
|
|
985
|
+
}
|
|
986
|
+
return map;
|
|
987
|
+
}
|
|
988
|
+
function loadRawRecords(dbPath, composerId, bubbleIds) {
|
|
989
|
+
const keys = [
|
|
990
|
+
`composerData:${composerId}`,
|
|
991
|
+
...bubbleIds.map((b) => `bubbleId:${composerId}:${b}`)
|
|
992
|
+
];
|
|
993
|
+
const inList = keys.map((k) => sqlLiteral(k)).join(", ");
|
|
994
|
+
const rows = sqliteJson(
|
|
995
|
+
dbPath,
|
|
996
|
+
`SELECT key, value FROM cursorDiskKV WHERE key IN (${inList})`
|
|
997
|
+
);
|
|
998
|
+
const records = {};
|
|
999
|
+
for (const { key, value } of rows) {
|
|
1000
|
+
if (!key) continue;
|
|
1001
|
+
try {
|
|
1002
|
+
records[key] = value ? JSON.parse(value) : null;
|
|
1003
|
+
} catch {
|
|
1004
|
+
records[key] = value;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
return records;
|
|
1008
|
+
}
|
|
1009
|
+
function parseHeaders(headers2) {
|
|
1010
|
+
if (!headers2) return [];
|
|
1011
|
+
try {
|
|
1012
|
+
const parsed = JSON.parse(headers2);
|
|
1013
|
+
return Array.isArray(parsed) ? parsed.filter((h) => h && h.bubbleId) : [];
|
|
1014
|
+
} catch {
|
|
1015
|
+
return [];
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
function filesFromTool(toolName, rawArgs) {
|
|
1019
|
+
const files = [];
|
|
1020
|
+
let label = `[tool_call: ${toolName}]`;
|
|
1021
|
+
if (!rawArgs) return { label, files };
|
|
1022
|
+
let args = {};
|
|
1023
|
+
try {
|
|
1024
|
+
args = JSON.parse(rawArgs);
|
|
1025
|
+
} catch {
|
|
1026
|
+
return { label, files };
|
|
1027
|
+
}
|
|
1028
|
+
const filePath = typeof args.target_file === "string" && args.target_file || typeof args.path === "string" && args.path || typeof args.relative_workspace_path === "string" && args.relative_workspace_path || null;
|
|
1029
|
+
const command = typeof args.command === "string" ? args.command : null;
|
|
1030
|
+
if (filePath) {
|
|
1031
|
+
label = `[tool_call: ${toolName}] ${filePath}`;
|
|
1032
|
+
files.push({ path: filePath, source: "tool_path", tool: toolName });
|
|
1033
|
+
} else if (command) {
|
|
1034
|
+
label = `[tool_call: ${toolName}] ${command}`;
|
|
1035
|
+
for (const p of extractPathsFromText2(command)) {
|
|
1036
|
+
files.push({ path: p, source: "tool_bash", tool: toolName });
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
return { label, files };
|
|
1040
|
+
}
|
|
1041
|
+
function toIso(value, fallbackMs) {
|
|
1042
|
+
if (typeof value === "string" && value) {
|
|
1043
|
+
const t = Date.parse(value);
|
|
1044
|
+
if (!Number.isNaN(t)) return new Date(t).toISOString();
|
|
1045
|
+
}
|
|
1046
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
1047
|
+
return new Date(value).toISOString();
|
|
1048
|
+
}
|
|
1049
|
+
return new Date(fallbackMs).toISOString();
|
|
1050
|
+
}
|
|
1051
|
+
function parseCursorComposer(dbPath, composer, repoPath, options = { scoped: false }, repoId = deriveRepoId(repoPath)) {
|
|
1052
|
+
const composerId = composer.composerId;
|
|
1053
|
+
if (!composerId) return null;
|
|
1054
|
+
const headers2 = parseHeaders(composer.headers);
|
|
1055
|
+
if (headers2.length === 0) return null;
|
|
1056
|
+
const bubbles = loadBubbles(dbPath, composerId);
|
|
1057
|
+
const composerCreatedMs = typeof composer.createdAt === "number" ? composer.createdAt : Date.now();
|
|
1058
|
+
const referencing = options.scoped ? bubbleIdsReferencingRepo(dbPath, composerId, repoPath) : null;
|
|
1059
|
+
const built = [];
|
|
1060
|
+
for (const header of headers2) {
|
|
1061
|
+
const bubble = bubbles.get(header.bubbleId);
|
|
1062
|
+
if (!bubble) continue;
|
|
1063
|
+
const role = header.type === 1 ? "user" : "assistant";
|
|
1064
|
+
const textParts = [];
|
|
1065
|
+
const filesTouched = [];
|
|
1066
|
+
if (bubble.text && bubble.text.trim()) {
|
|
1067
|
+
textParts.push(bubble.text);
|
|
1068
|
+
for (const p of extractPathsFromText2(bubble.text)) {
|
|
1069
|
+
filesTouched.push({ path: p, source: "prose" });
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
if (role === "assistant" && bubble.thinking && bubble.thinking.trim()) {
|
|
1073
|
+
textParts.push(bubble.thinking);
|
|
1074
|
+
}
|
|
1075
|
+
if (bubble.toolName) {
|
|
1076
|
+
const { label, files } = filesFromTool(bubble.toolName, bubble.rawArgs);
|
|
1077
|
+
textParts.push(label);
|
|
1078
|
+
filesTouched.push(...files);
|
|
1079
|
+
}
|
|
1080
|
+
const text = textParts.join("\n");
|
|
1081
|
+
if (!text.trim() && filesTouched.length === 0) continue;
|
|
1082
|
+
const redacted = redactSecrets(text);
|
|
1083
|
+
built.push({
|
|
1084
|
+
bubbleId: header.bubbleId,
|
|
1085
|
+
role,
|
|
1086
|
+
referencesRepo: referencing ? referencing.has(header.bubbleId) : true,
|
|
1087
|
+
redactionCount: redacted.count,
|
|
1088
|
+
turn: {
|
|
1089
|
+
id: header.bubbleId,
|
|
1090
|
+
// rewritten to the per-repo derived id below
|
|
1091
|
+
sessionId: composerId,
|
|
1092
|
+
// rewritten to the per-repo derived id below
|
|
1093
|
+
role,
|
|
1094
|
+
ts: toIso(bubble.createdAt, composerCreatedMs),
|
|
1095
|
+
text: redacted.text.slice(0, MAX_TURN_TEXT_LENGTH2),
|
|
1096
|
+
filesTouched,
|
|
1097
|
+
// Cursor's bubble format doesn't expose a structuredPatch-equivalent
|
|
1098
|
+
// the way Claude Code's toolUseResult does — content-overlap scoring
|
|
1099
|
+
// (see scoreContentOverlap in the linker) simply has no signal here
|
|
1100
|
+
// yet, same tier as any other not-yet-supported evidence source.
|
|
1101
|
+
editedLines: [],
|
|
1102
|
+
// Cursor has no parent-pointer chain; header order is authoritative, so
|
|
1103
|
+
// parentUuid is left null (the backend/linker never require it).
|
|
1104
|
+
parentUuid: null,
|
|
1105
|
+
isSidechain: false,
|
|
1106
|
+
redacted: redacted.count > 0,
|
|
1107
|
+
// Cursor's "user" bubbles (header.type === 1, see `role` above) are a
|
|
1108
|
+
// structural field distinct from tool output — not the Claude Code
|
|
1109
|
+
// wire-format ambiguity where a tool result also arrives as a
|
|
1110
|
+
// `role: "user"` record. No synthetic-input misattribution risk here.
|
|
1111
|
+
isSyntheticInput: false,
|
|
1112
|
+
// Neither source reports what a turn cost, so it is unknown rather than free.
|
|
1113
|
+
usage: { ...EMPTY_USAGE }
|
|
1114
|
+
}
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
let kept;
|
|
1118
|
+
if (referencing) {
|
|
1119
|
+
const keep = /* @__PURE__ */ new Set();
|
|
1120
|
+
for (let i = 0; i < built.length; i++) {
|
|
1121
|
+
const cur = built[i];
|
|
1122
|
+
if (!cur || !cur.referencesRepo) continue;
|
|
1123
|
+
keep.add(i);
|
|
1124
|
+
const prev = built[i - 1];
|
|
1125
|
+
if (prev && prev.role === "user") keep.add(i - 1);
|
|
1126
|
+
}
|
|
1127
|
+
kept = built.filter((_, i) => keep.has(i));
|
|
1128
|
+
} else {
|
|
1129
|
+
kept = built;
|
|
1130
|
+
}
|
|
1131
|
+
if (kept.length === 0) return null;
|
|
1132
|
+
const sessionId = deriveCursorSessionId(composerId, repoPath);
|
|
1133
|
+
const turns = kept.map((b) => ({
|
|
1134
|
+
...b.turn,
|
|
1135
|
+
id: deriveCursorTurnId(b.bubbleId, repoPath),
|
|
1136
|
+
sessionId
|
|
1137
|
+
}));
|
|
1138
|
+
const totalRedactions = kept.reduce((sum, b) => sum + b.redactionCount, 0);
|
|
1139
|
+
const sortedTs = turns.map((t) => t.ts).sort();
|
|
1140
|
+
if (totalRedactions > 0) {
|
|
1141
|
+
console.log(`[ingest-core] redacted ${totalRedactions} potential secret(s) in cursor session ${composerId}`);
|
|
1142
|
+
}
|
|
1143
|
+
const rawRecords = loadRawRecords(dbPath, composerId, kept.map((b) => b.bubbleId));
|
|
1144
|
+
const rawContent = JSON.stringify(
|
|
1145
|
+
redactJsonValue({ format: "cursor-composer", composerId, records: rawRecords }).value
|
|
1146
|
+
);
|
|
1147
|
+
return {
|
|
1148
|
+
id: sessionId,
|
|
1149
|
+
agentKind: "cursor",
|
|
1150
|
+
repoId,
|
|
1151
|
+
cwd: repoPath,
|
|
1152
|
+
startedAt: sortedTs[0] ?? null,
|
|
1153
|
+
endedAt: sortedTs[sortedTs.length - 1] ?? null,
|
|
1154
|
+
turnCount: turns.length,
|
|
1155
|
+
aiTitle: composer.name && composer.name.trim() ? composer.name : null,
|
|
1156
|
+
author: null,
|
|
1157
|
+
// filled in by the caller — see collectRepoData
|
|
1158
|
+
// Includes repoPath so a shared composer's per-repo copies have distinct,
|
|
1159
|
+
// traceable source refs (the raw composerId alone is no longer unique).
|
|
1160
|
+
sourceFile: `${dbPath}#${composerId}#${repoPath}`,
|
|
1161
|
+
redactionCount: totalRedactions,
|
|
1162
|
+
// Read back off the turns rather than the raw records: unlike Claude Code,
|
|
1163
|
+
// a Cursor tool call's label keeps the command itself, so the invocation is
|
|
1164
|
+
// still there to read.
|
|
1165
|
+
committedSubjects: [
|
|
1166
|
+
...new Set(turns.flatMap((t) => committedSubjects(t.text)))
|
|
1167
|
+
],
|
|
1168
|
+
rawContent,
|
|
1169
|
+
rawFormat: "json",
|
|
1170
|
+
turns
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
1173
|
+
function workspaceMatchesRepo(workspacePath, repoPath) {
|
|
1174
|
+
if (!workspacePath) return false;
|
|
1175
|
+
return workspacePath === repoPath || workspacePath.startsWith(`${repoPath}${sep}`);
|
|
1176
|
+
}
|
|
1177
|
+
function parseAllCursorSessions(repoPath, repoId) {
|
|
1178
|
+
const dbPath = cursorStateDbPath();
|
|
1179
|
+
if (!existsSync2(dbPath)) return [];
|
|
1180
|
+
const composers = loadComposers(dbPath);
|
|
1181
|
+
if (composers.length === 0) return [];
|
|
1182
|
+
const byId = /* @__PURE__ */ new Map();
|
|
1183
|
+
for (const c of composers) {
|
|
1184
|
+
if (c.composerId) byId.set(c.composerId, c);
|
|
1185
|
+
}
|
|
1186
|
+
const workspaceDedicated = /* @__PURE__ */ new Set();
|
|
1187
|
+
for (const c of composers) {
|
|
1188
|
+
if (c.composerId && workspaceMatchesRepo(c.workspacePath, repoPath)) {
|
|
1189
|
+
workspaceDedicated.add(c.composerId);
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
const attributed = new Set(workspaceDedicated);
|
|
1193
|
+
for (const id of composerIdsReferencingRepo(dbPath, repoPath)) {
|
|
1194
|
+
attributed.add(id);
|
|
1195
|
+
}
|
|
1196
|
+
const resolvedRepoId = repoId ?? deriveRepoId(repoPath);
|
|
1197
|
+
const sessions = [];
|
|
1198
|
+
for (const id of attributed) {
|
|
1199
|
+
const composer = byId.get(id);
|
|
1200
|
+
if (!composer) continue;
|
|
1201
|
+
const scoped = !workspaceDedicated.has(id);
|
|
1202
|
+
const parsed = parseCursorComposer(dbPath, composer, repoPath, { scoped }, resolvedRepoId);
|
|
1203
|
+
if (parsed) sessions.push(parsed);
|
|
1204
|
+
}
|
|
1205
|
+
return sessions;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
// ../../packages/ingest-core/src/codex-sessions.ts
|
|
1209
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
1210
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2, readdirSync as readdirSync2, statSync as statSync3 } from "node:fs";
|
|
1211
|
+
import { homedir as homedir3 } from "node:os";
|
|
1212
|
+
import { join as join3 } from "node:path";
|
|
1213
|
+
var CODEX_DIR = ".codex";
|
|
1214
|
+
function codexSessionsDir() {
|
|
1215
|
+
return join3(homedir3(), CODEX_DIR, "sessions");
|
|
1216
|
+
}
|
|
1217
|
+
function deriveUuid2(name) {
|
|
1218
|
+
const h = createHash2("sha1").update(name).digest("hex");
|
|
1219
|
+
const variant = (parseInt(h.slice(16, 17) || "0", 16) & 3 | 8).toString(16);
|
|
1220
|
+
const s = h.slice(0, 12) + "5" + h.slice(13, 16) + variant + h.slice(17, 32);
|
|
1221
|
+
return `${s.slice(0, 8)}-${s.slice(8, 12)}-${s.slice(12, 16)}-${s.slice(16, 20)}-${s.slice(20, 32)}`;
|
|
1222
|
+
}
|
|
1223
|
+
function deriveCodexTurnId(sessionId, ordinal) {
|
|
1224
|
+
return deriveUuid2(`evrex-codex-turn\0${sessionId}\0${ordinal}`);
|
|
1225
|
+
}
|
|
1226
|
+
function findCodexSessionFiles(root = codexSessionsDir()) {
|
|
1227
|
+
const found = [];
|
|
1228
|
+
const walk = (dir) => {
|
|
1229
|
+
let entries;
|
|
1230
|
+
try {
|
|
1231
|
+
entries = readdirSync2(dir);
|
|
1232
|
+
} catch {
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
for (const entry of entries) {
|
|
1236
|
+
const full = join3(dir, entry);
|
|
1237
|
+
let isDir = false;
|
|
1238
|
+
try {
|
|
1239
|
+
isDir = statSync3(full).isDirectory();
|
|
1240
|
+
} catch {
|
|
1241
|
+
continue;
|
|
1242
|
+
}
|
|
1243
|
+
if (isDir) walk(full);
|
|
1244
|
+
else if (entry.endsWith(".jsonl")) found.push(full);
|
|
1245
|
+
}
|
|
1246
|
+
};
|
|
1247
|
+
if (existsSync3(root)) walk(root);
|
|
1248
|
+
return found.sort();
|
|
1249
|
+
}
|
|
1250
|
+
function parseLines(raw) {
|
|
1251
|
+
const out = [];
|
|
1252
|
+
for (const line of raw.split("\n")) {
|
|
1253
|
+
if (!line.trim()) continue;
|
|
1254
|
+
try {
|
|
1255
|
+
out.push(JSON.parse(line));
|
|
1256
|
+
} catch {
|
|
1257
|
+
continue;
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
return out;
|
|
1261
|
+
}
|
|
1262
|
+
function payloadType(line) {
|
|
1263
|
+
const t = line.payload?.type;
|
|
1264
|
+
return typeof t === "string" ? t : void 0;
|
|
1265
|
+
}
|
|
1266
|
+
function asString(value) {
|
|
1267
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
1268
|
+
}
|
|
1269
|
+
function conversationEvents(lines) {
|
|
1270
|
+
const out = [];
|
|
1271
|
+
let pending = null;
|
|
1272
|
+
let model = null;
|
|
1273
|
+
lines.forEach((line, index) => {
|
|
1274
|
+
if (line.type === "turn_context") {
|
|
1275
|
+
model = asString(line.payload?.model) ?? model;
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
if (line.type !== "event_msg") return;
|
|
1279
|
+
const kind = payloadType(line);
|
|
1280
|
+
if (kind === "token_count") {
|
|
1281
|
+
const delta = lastUsage(line.payload);
|
|
1282
|
+
if (delta) pending = addUsage(pending, delta);
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
if (kind !== "user_message" && kind !== "agent_message") return;
|
|
1286
|
+
const text = asString(line.payload?.message);
|
|
1287
|
+
if (!text) return;
|
|
1288
|
+
const role = kind === "user_message" ? "user" : "assistant";
|
|
1289
|
+
const usage = role === "assistant" && pending ? { ...pending, model: pending.model ?? model } : { ...EMPTY_USAGE };
|
|
1290
|
+
if (role === "assistant") pending = null;
|
|
1291
|
+
out.push({ role, text, ts: asString(line.timestamp), ordinal: index, usage });
|
|
1292
|
+
});
|
|
1293
|
+
return out;
|
|
1294
|
+
}
|
|
1295
|
+
function lastUsage(payload) {
|
|
1296
|
+
const info = payload?.info;
|
|
1297
|
+
const last = info?.last_token_usage;
|
|
1298
|
+
if (!last) return null;
|
|
1299
|
+
const num = (key) => {
|
|
1300
|
+
const value = last[key];
|
|
1301
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
1302
|
+
};
|
|
1303
|
+
const cached = num("cached_input_tokens");
|
|
1304
|
+
return {
|
|
1305
|
+
inputTokens: Math.max(0, num("input_tokens") - cached),
|
|
1306
|
+
outputTokens: num("output_tokens"),
|
|
1307
|
+
cacheReadTokens: cached,
|
|
1308
|
+
// Codex reports no cache-write figure; unknown rather than zero.
|
|
1309
|
+
cacheWriteTokens: null,
|
|
1310
|
+
model: null
|
|
1311
|
+
};
|
|
1312
|
+
}
|
|
1313
|
+
function addUsage(into, next) {
|
|
1314
|
+
if (!into) return { ...next };
|
|
1315
|
+
const sum = (a, b) => a === null && b === null ? null : (a ?? 0) + (b ?? 0);
|
|
1316
|
+
return {
|
|
1317
|
+
inputTokens: sum(into.inputTokens, next.inputTokens),
|
|
1318
|
+
outputTokens: sum(into.outputTokens, next.outputTokens),
|
|
1319
|
+
cacheReadTokens: sum(into.cacheReadTokens, next.cacheReadTokens),
|
|
1320
|
+
cacheWriteTokens: sum(into.cacheWriteTokens, next.cacheWriteTokens),
|
|
1321
|
+
model: into.model ?? next.model
|
|
1322
|
+
};
|
|
1323
|
+
}
|
|
1324
|
+
function committedSubjectsFromToolCalls(lines) {
|
|
1325
|
+
const subjects = /* @__PURE__ */ new Set();
|
|
1326
|
+
for (const line of lines) {
|
|
1327
|
+
if (line.type !== "response_item") continue;
|
|
1328
|
+
if (payloadType(line) !== "custom_tool_call") continue;
|
|
1329
|
+
const input = asString(line.payload?.input);
|
|
1330
|
+
if (!input) continue;
|
|
1331
|
+
for (const subject of committedSubjects(input)) subjects.add(subject);
|
|
1332
|
+
}
|
|
1333
|
+
return [...subjects];
|
|
1334
|
+
}
|
|
1335
|
+
function filesFromToolCalls(lines) {
|
|
1336
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
1337
|
+
for (const line of lines) {
|
|
1338
|
+
if (line.type !== "response_item") continue;
|
|
1339
|
+
if (payloadType(line) !== "custom_tool_call") continue;
|
|
1340
|
+
const input = asString(line.payload?.input);
|
|
1341
|
+
if (!input) continue;
|
|
1342
|
+
const tool = asString(line.payload?.name) ?? "exec";
|
|
1343
|
+
for (const path of extractPathsFromText(input)) {
|
|
1344
|
+
if (!byPath.has(path)) byPath.set(path, { path, source: "tool_bash", tool });
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
return [...byPath.values()];
|
|
1348
|
+
}
|
|
1349
|
+
function parseCodexSessionFile(filePath, repoPath, repoId) {
|
|
1350
|
+
let raw;
|
|
1351
|
+
try {
|
|
1352
|
+
raw = readFileSync2(filePath, "utf-8");
|
|
1353
|
+
} catch {
|
|
1354
|
+
return null;
|
|
1355
|
+
}
|
|
1356
|
+
const lines = parseLines(raw);
|
|
1357
|
+
const meta = lines.find((l) => l.type === "session_meta")?.payload;
|
|
1358
|
+
if (!meta) return null;
|
|
1359
|
+
const cwd = asString(meta.cwd);
|
|
1360
|
+
if (!cwd) return null;
|
|
1361
|
+
if (cwd.replace(/\/+$/, "") !== repoPath.replace(/\/+$/, "")) return null;
|
|
1362
|
+
const sessionId = asString(meta.session_id) ?? asString(meta.id);
|
|
1363
|
+
if (!sessionId) return null;
|
|
1364
|
+
const events = conversationEvents(lines);
|
|
1365
|
+
if (events.length === 0) return null;
|
|
1366
|
+
const filesTouched = filesFromToolCalls(lines);
|
|
1367
|
+
let redactionCount = 0;
|
|
1368
|
+
const turns = events.map((event, i) => {
|
|
1369
|
+
const { text, count } = redactSecrets(event.text);
|
|
1370
|
+
redactionCount += count;
|
|
1371
|
+
return {
|
|
1372
|
+
id: deriveCodexTurnId(sessionId, event.ordinal),
|
|
1373
|
+
sessionId,
|
|
1374
|
+
role: event.role,
|
|
1375
|
+
ts: event.ts ?? asString(meta.timestamp) ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1376
|
+
text,
|
|
1377
|
+
// Attributed to the assistant turns, which are the ones that ran the
|
|
1378
|
+
// tools; a user turn touched no files.
|
|
1379
|
+
filesTouched: event.role === "assistant" && i === events.length - 1 ? filesTouched : [],
|
|
1380
|
+
editedLines: [],
|
|
1381
|
+
parentUuid: null,
|
|
1382
|
+
isSidechain: false,
|
|
1383
|
+
redacted: count > 0,
|
|
1384
|
+
isSyntheticInput: false,
|
|
1385
|
+
usage: event.usage
|
|
1386
|
+
};
|
|
1387
|
+
});
|
|
1388
|
+
const { content: rawContent, count: rawRedactions } = redactRollout(raw);
|
|
1389
|
+
redactionCount += rawRedactions;
|
|
1390
|
+
const firstUser = events.find((e) => e.role === "user");
|
|
1391
|
+
return {
|
|
1392
|
+
id: sessionId,
|
|
1393
|
+
agentKind: "codex",
|
|
1394
|
+
// Codex records the remote itself, so identity survives a moved or deleted
|
|
1395
|
+
// checkout. Falls back to the caller's derivation when it is absent.
|
|
1396
|
+
repoId: repoIdFromMeta(meta) ?? repoId,
|
|
1397
|
+
cwd,
|
|
1398
|
+
startedAt: asString(meta.timestamp) ?? events[0]?.ts ?? null,
|
|
1399
|
+
endedAt: events[events.length - 1]?.ts ?? null,
|
|
1400
|
+
turnCount: turns.length,
|
|
1401
|
+
committedSubjects: committedSubjectsFromToolCalls(lines),
|
|
1402
|
+
aiTitle: firstUser ? firstUser.text.slice(0, 120).trim() : null,
|
|
1403
|
+
author: null,
|
|
1404
|
+
sourceFile: filePath,
|
|
1405
|
+
redactionCount,
|
|
1406
|
+
turns,
|
|
1407
|
+
rawContent,
|
|
1408
|
+
rawFormat: "jsonl"
|
|
1409
|
+
};
|
|
1410
|
+
}
|
|
1411
|
+
function repoIdFromMeta(meta) {
|
|
1412
|
+
const git2 = meta.git;
|
|
1413
|
+
if (typeof git2 !== "object" || git2 === null) return null;
|
|
1414
|
+
const url = asString(git2.repository_url);
|
|
1415
|
+
if (!url) return null;
|
|
1416
|
+
const normalized = normalizeRepoRemote(url);
|
|
1417
|
+
return normalized ? `remote:${normalized}` : null;
|
|
1418
|
+
}
|
|
1419
|
+
function redactRollout(raw) {
|
|
1420
|
+
let count = 0;
|
|
1421
|
+
const lines = raw.split("\n").map((line) => {
|
|
1422
|
+
if (!line.trim()) return line;
|
|
1423
|
+
const { text, count: n } = redactSecrets(line);
|
|
1424
|
+
count += n;
|
|
1425
|
+
return text;
|
|
1426
|
+
});
|
|
1427
|
+
return { content: lines.join("\n"), count };
|
|
1428
|
+
}
|
|
1429
|
+
function parseAllCodexSessions(repoPath, repoId) {
|
|
1430
|
+
const sessions = [];
|
|
1431
|
+
for (const file of findCodexSessionFiles()) {
|
|
1432
|
+
const parsed = parseCodexSessionFile(file, repoPath, repoId);
|
|
1433
|
+
if (parsed) sessions.push(parsed);
|
|
1434
|
+
}
|
|
1435
|
+
return sessions;
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
// ../../packages/ingest-core/src/sanitize.ts
|
|
1439
|
+
var NUL = String.fromCharCode(0);
|
|
1440
|
+
function stripNulls(value) {
|
|
1441
|
+
return value.includes(NUL) ? value.split(NUL).join("") : value;
|
|
1442
|
+
}
|
|
1443
|
+
function stripNullsDeep(value) {
|
|
1444
|
+
if (typeof value === "string") return stripNulls(value);
|
|
1445
|
+
if (Array.isArray(value)) return value.map((v) => stripNullsDeep(v));
|
|
1446
|
+
if (value && typeof value === "object") {
|
|
1447
|
+
if (value instanceof Date) return value;
|
|
1448
|
+
const out = {};
|
|
1449
|
+
for (const [k, v] of Object.entries(value)) {
|
|
1450
|
+
out[k] = stripNullsDeep(v);
|
|
1451
|
+
}
|
|
1452
|
+
return out;
|
|
1453
|
+
}
|
|
1454
|
+
return value;
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
// ../../packages/ingest-core/src/index.ts
|
|
1458
|
+
function resolveFallbackAuthor(repoPath, commits) {
|
|
1459
|
+
const configured = getGitUserName(repoPath);
|
|
1460
|
+
if (configured) return configured;
|
|
1461
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1462
|
+
for (const c of commits) {
|
|
1463
|
+
if (!c.author) continue;
|
|
1464
|
+
counts.set(c.author, (counts.get(c.author) ?? 0) + 1);
|
|
1465
|
+
}
|
|
1466
|
+
const mostFrequent = [...counts.entries()].sort((a, b) => b[1] - a[1])[0]?.[0];
|
|
1467
|
+
if (mostFrequent) return mostFrequent;
|
|
1468
|
+
try {
|
|
1469
|
+
return userInfo().username || null;
|
|
1470
|
+
} catch {
|
|
1471
|
+
return null;
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
function collectRepoData(repoPath, options = {}) {
|
|
1475
|
+
const repoId = deriveRepoId(repoPath);
|
|
1476
|
+
const commits = parseGitLog(repoPath, repoId, options.knownCommits);
|
|
1477
|
+
const author = resolveFallbackAuthor(repoPath, commits);
|
|
1478
|
+
const claude = parseAllSessions(repoPath, repoId, options.cursors ?? {});
|
|
1479
|
+
const sessions = [
|
|
1480
|
+
...claude.sessions,
|
|
1481
|
+
...parseAllCursorSessions(repoPath, repoId),
|
|
1482
|
+
...parseAllCodexSessions(repoPath, repoId)
|
|
1483
|
+
].map((s) => ({
|
|
1484
|
+
...s,
|
|
1485
|
+
author: s.author ?? author
|
|
1486
|
+
}));
|
|
1487
|
+
const totalRedactions = sessions.reduce((sum, s) => sum + s.redactionCount, 0);
|
|
1488
|
+
return {
|
|
1489
|
+
sessions: stripNullsDeep(sessions),
|
|
1490
|
+
commits: stripNullsDeep(commits),
|
|
1491
|
+
totalRedactions,
|
|
1492
|
+
cursors: claude.cursors,
|
|
1493
|
+
skippedTranscripts: claude.skipped
|
|
1494
|
+
};
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
// src/batch.ts
|
|
1498
|
+
var MAX_BATCH_BYTES = 24 * 1024 * 1024;
|
|
1499
|
+
function measure(items) {
|
|
1500
|
+
return items.map((item) => ({
|
|
1501
|
+
item,
|
|
1502
|
+
bytes: Buffer.byteLength(JSON.stringify(item), "utf8")
|
|
1503
|
+
}));
|
|
1504
|
+
}
|
|
1505
|
+
function batchByBytes(items, maxBytes = MAX_BATCH_BYTES) {
|
|
1506
|
+
const batches = [];
|
|
1507
|
+
let current = [];
|
|
1508
|
+
let size = 0;
|
|
1509
|
+
for (const { item, bytes } of measure(items)) {
|
|
1510
|
+
if (current.length > 0 && size + bytes > maxBytes) {
|
|
1511
|
+
batches.push(current);
|
|
1512
|
+
current = [];
|
|
1513
|
+
size = 0;
|
|
1514
|
+
}
|
|
1515
|
+
current.push(item);
|
|
1516
|
+
size += bytes;
|
|
1517
|
+
}
|
|
1518
|
+
if (current.length > 0) batches.push(current);
|
|
1519
|
+
return batches;
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
// src/import-state.ts
|
|
1523
|
+
import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync3, renameSync, writeFileSync } from "node:fs";
|
|
1524
|
+
import { dirname, join as join4 } from "node:path";
|
|
1525
|
+
import { homedir as homedir4 } from "node:os";
|
|
1526
|
+
function importStatePath(home = homedir4()) {
|
|
1527
|
+
return join4(home, ".evrex", "import-state.json");
|
|
1528
|
+
}
|
|
1529
|
+
function readImportState(path) {
|
|
1530
|
+
try {
|
|
1531
|
+
const parsed = JSON.parse(readFileSync3(path, "utf8"));
|
|
1532
|
+
if (!parsed || typeof parsed.repos !== "object") return { repos: {} };
|
|
1533
|
+
return { repos: parsed.repos ?? {} };
|
|
1534
|
+
} catch {
|
|
1535
|
+
return { repos: {} };
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
function writeImportState(path, state) {
|
|
1539
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
1540
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
1541
|
+
writeFileSync(tmp, JSON.stringify(state, null, 2), { mode: 384 });
|
|
1542
|
+
renameSync(tmp, path);
|
|
1543
|
+
}
|
|
1544
|
+
function stateFor(state, repoId) {
|
|
1545
|
+
return state.repos[repoId] ?? {
|
|
1546
|
+
cursors: {},
|
|
1547
|
+
knownCommits: [],
|
|
1548
|
+
lastImportedAt: (/* @__PURE__ */ new Date(0)).toISOString()
|
|
1549
|
+
};
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
// src/import.ts
|
|
1553
|
+
async function importRepo(repoPath, deps) {
|
|
1554
|
+
const statePath = importStatePath(deps.home);
|
|
1555
|
+
const state = readImportState(statePath);
|
|
1556
|
+
const repoId = deps.deriveRepoId(repoPath);
|
|
1557
|
+
const known = stateFor(state, repoId);
|
|
1558
|
+
const data = deps.collect(repoPath, {
|
|
1559
|
+
cursors: known.cursors,
|
|
1560
|
+
knownCommits: new Set(known.knownCommits)
|
|
1561
|
+
});
|
|
1562
|
+
const result = {
|
|
1563
|
+
sessions: 0,
|
|
1564
|
+
commits: 0,
|
|
1565
|
+
skippedTranscripts: data.skippedTranscripts,
|
|
1566
|
+
redactions: data.totalRedactions,
|
|
1567
|
+
batches: 0,
|
|
1568
|
+
failed: false
|
|
1569
|
+
};
|
|
1570
|
+
const deliveredCommits = [];
|
|
1571
|
+
for (const batch of batchByBytes(data.commits)) {
|
|
1572
|
+
result.batches += 1;
|
|
1573
|
+
if (!await deps.post("/ingest/commits", { commits: batch })) {
|
|
1574
|
+
result.failed = true;
|
|
1575
|
+
break;
|
|
1576
|
+
}
|
|
1577
|
+
deliveredCommits.push(...batch.map((c) => c.sha));
|
|
1578
|
+
result.commits += batch.length;
|
|
1579
|
+
deps.log(` commits ${result.commits}/${data.commits.length}`);
|
|
1580
|
+
}
|
|
1581
|
+
const deliveredCursors = {};
|
|
1582
|
+
if (!result.failed) {
|
|
1583
|
+
for (const batch of batchByBytes(data.sessions)) {
|
|
1584
|
+
result.batches += 1;
|
|
1585
|
+
if (!await deps.post("/ingest/sessions", { sessions: batch })) {
|
|
1586
|
+
result.failed = true;
|
|
1587
|
+
break;
|
|
1588
|
+
}
|
|
1589
|
+
for (const session of batch) {
|
|
1590
|
+
const cursor = data.cursors[session.sourceFile];
|
|
1591
|
+
if (cursor !== void 0) deliveredCursors[session.sourceFile] = cursor;
|
|
1592
|
+
}
|
|
1593
|
+
result.sessions += batch.length;
|
|
1594
|
+
deps.log(` sessions ${result.sessions}/${data.sessions.length}`);
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
state.repos[repoId] = {
|
|
1598
|
+
cursors: { ...known.cursors, ...deliveredCursors },
|
|
1599
|
+
knownCommits: [.../* @__PURE__ */ new Set([...known.knownCommits, ...deliveredCommits])],
|
|
1600
|
+
lastImportedAt: deps.now().toISOString()
|
|
1601
|
+
};
|
|
1602
|
+
writeImportState(statePath, state);
|
|
1603
|
+
return result;
|
|
1604
|
+
}
|
|
1605
|
+
async function main() {
|
|
1606
|
+
const target = resolve(process.argv[3] ?? process.cwd());
|
|
1607
|
+
const { evrexApi: evrexApi2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
1608
|
+
const { CredentialStore: CredentialStore2 } = await Promise.resolve().then(() => (init_credential_store(), credential_store_exports));
|
|
1609
|
+
const token = process.env.EVREX_TOKEN ?? await new CredentialStore2().retrieve().catch(() => null);
|
|
1610
|
+
if (!token) {
|
|
1611
|
+
console.error(
|
|
1612
|
+
"evrex: this machine is not enrolled.\n Run `npx -y evrex-mcp enrol` first, or set EVREX_TOKEN."
|
|
1613
|
+
);
|
|
1614
|
+
process.exit(1);
|
|
1615
|
+
}
|
|
1616
|
+
console.error(`Importing ${target}`);
|
|
1617
|
+
const result = await importRepo(target, {
|
|
1618
|
+
collect: collectRepoData,
|
|
1619
|
+
deriveRepoId,
|
|
1620
|
+
home: homedir5(),
|
|
1621
|
+
now: () => /* @__PURE__ */ new Date(),
|
|
1622
|
+
log: (line) => console.error(line),
|
|
1623
|
+
post: async (path, body) => {
|
|
1624
|
+
try {
|
|
1625
|
+
const res = await fetch(`${evrexApi2.baseUrl}${path}`, {
|
|
1626
|
+
method: "POST",
|
|
1627
|
+
headers: {
|
|
1628
|
+
"content-type": "application/json",
|
|
1629
|
+
authorization: `Bearer ${token}`
|
|
1630
|
+
},
|
|
1631
|
+
body: JSON.stringify(body)
|
|
1632
|
+
});
|
|
1633
|
+
if (!res.ok) console.error(` ${path} -> ${res.status}`);
|
|
1634
|
+
return res.ok;
|
|
1635
|
+
} catch (error) {
|
|
1636
|
+
console.error(` ${path} -> ${error.message}`);
|
|
1637
|
+
return false;
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
});
|
|
1641
|
+
console.error(
|
|
1642
|
+
`
|
|
1643
|
+
${result.commits} commits, ${result.sessions} sessions` + (result.skippedTranscripts ? `, ${result.skippedTranscripts} transcripts unchanged` : "") + (result.redactions ? `, ${result.redactions} secrets redacted` : "")
|
|
1644
|
+
);
|
|
1645
|
+
if (result.failed) {
|
|
1646
|
+
console.error(" Import stopped early. Run it again to resume.");
|
|
1647
|
+
}
|
|
1648
|
+
process.exit(result.failed ? 1 : 0);
|
|
1649
|
+
}
|
|
1650
|
+
function isMain() {
|
|
1651
|
+
const invoked = process.argv[1];
|
|
1652
|
+
if (!invoked) return false;
|
|
1653
|
+
try {
|
|
1654
|
+
return realpathSync(invoked) === fileURLToPath(import.meta.url);
|
|
1655
|
+
} catch {
|
|
1656
|
+
return false;
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
if (isMain()) {
|
|
1660
|
+
main().catch((error) => {
|
|
1661
|
+
console.error("evrex import failed:", error);
|
|
1662
|
+
process.exit(1);
|
|
1663
|
+
});
|
|
1664
|
+
}
|
|
1665
|
+
export {
|
|
1666
|
+
importRepo
|
|
1667
|
+
};
|