@twarc_net/groundtruth 0.1.0 → 0.3.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 +136 -2
- package/dist/cli.js +1120 -178
- package/dist/index.d.ts +139 -8
- package/dist/index.js +849 -24
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -75,21 +75,34 @@ function extractClaims(summary) {
|
|
|
75
75
|
seen.add(key);
|
|
76
76
|
claims.push({ kind, target, polarity, source: source.trim() });
|
|
77
77
|
};
|
|
78
|
-
for (const clause of splitClauses(summary)) {
|
|
78
|
+
for (const clause of splitClauses(stripCodeFences(summary))) {
|
|
79
79
|
if (isIntent(clause)) continue;
|
|
80
80
|
const polarity = detectPolarity(clause);
|
|
81
81
|
const hasVerb = ADD_VERBS.test(clause) || REMOVE_VERBS.test(clause) || MODIFY_VERBS.test(clause);
|
|
82
82
|
let concrete = false;
|
|
83
|
+
const renamed = matchRename(clause);
|
|
84
|
+
if (renamed) {
|
|
85
|
+
add("symbol", renamed.from, "remove", clause);
|
|
86
|
+
add("symbol", renamed.to, "add", clause);
|
|
87
|
+
concrete = true;
|
|
88
|
+
}
|
|
83
89
|
for (const tok of backtickTokens(clause)) {
|
|
84
|
-
|
|
85
|
-
if (kind === "file") {
|
|
90
|
+
if (looksLikePath(tok)) {
|
|
86
91
|
add("file", tok, polarity, clause);
|
|
87
92
|
concrete = true;
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (tok.includes(" ")) {
|
|
96
|
+
const first = (tok.split(/\s+/)[0] ?? "").toLowerCase();
|
|
97
|
+
if (COMMAND_WORDS.has(first)) {
|
|
98
|
+
add("command", tok, polarity, clause);
|
|
99
|
+
concrete = true;
|
|
100
|
+
}
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const sym = symbolName(tok);
|
|
104
|
+
if (sym) {
|
|
105
|
+
add("symbol", sym, polarity, clause);
|
|
93
106
|
concrete = true;
|
|
94
107
|
}
|
|
95
108
|
}
|
|
@@ -248,22 +261,31 @@ var SPECIAL_FILES = /* @__PURE__ */ new Set([
|
|
|
248
261
|
".dockerignore",
|
|
249
262
|
".editorconfig"
|
|
250
263
|
]);
|
|
251
|
-
function
|
|
252
|
-
|
|
253
|
-
if (
|
|
254
|
-
const first = tok.split(/\s+/)[0] ?? "";
|
|
255
|
-
return COMMAND_WORDS.has(first.toLowerCase()) ? "command" : null;
|
|
256
|
-
}
|
|
264
|
+
function symbolName(tok) {
|
|
265
|
+
const jsx = /^<\/?\s*([A-Za-z][\w.]*)\b[^>]*>$/.exec(tok);
|
|
266
|
+
if (jsx?.[1]) return jsx[1];
|
|
257
267
|
const id = stripCall(tok);
|
|
258
|
-
|
|
259
|
-
|
|
268
|
+
return /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$/.test(id) ? id : null;
|
|
269
|
+
}
|
|
270
|
+
function matchRename(clause) {
|
|
271
|
+
if (!/\brenam(?:e|ed|es|ing)\b/i.test(clause)) return null;
|
|
272
|
+
const m = /`([^`]+)`\s*(?:to|into|->|→|=>)\s*`([^`]+)`/i.exec(clause);
|
|
273
|
+
if (!m?.[1] || !m[2]) return null;
|
|
274
|
+
const from = symbolName(m[1]);
|
|
275
|
+
const to = symbolName(m[2]);
|
|
276
|
+
return from && to ? { from, to } : null;
|
|
277
|
+
}
|
|
278
|
+
function stripCodeFences(text) {
|
|
279
|
+
return text.replace(/```[\s\S]*?```/g, " ").replace(/~~~[\s\S]*?~~~/g, " ");
|
|
260
280
|
}
|
|
261
281
|
function looksLikePath(tok) {
|
|
262
282
|
if (tok.includes(" ")) return false;
|
|
283
|
+
if (/[<>]/.test(tok)) return false;
|
|
263
284
|
if (SPECIAL_FILES.has(tok)) return true;
|
|
264
285
|
if (/^https?:\/\//i.test(tok)) return false;
|
|
265
|
-
if (tok.includes("/")) return true;
|
|
266
286
|
const ext = extOf(tok);
|
|
287
|
+
if (tok.startsWith("/")) return ext !== null && CODE_EXTENSIONS.has(ext);
|
|
288
|
+
if (tok.includes("/")) return true;
|
|
267
289
|
return ext !== null && CODE_EXTENSIONS.has(ext);
|
|
268
290
|
}
|
|
269
291
|
function extOf(tok) {
|
|
@@ -316,10 +338,104 @@ function escapeRe(s) {
|
|
|
316
338
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
317
339
|
}
|
|
318
340
|
|
|
341
|
+
// src/config.ts
|
|
342
|
+
import { existsSync, readFileSync as readFileSync2 } from "fs";
|
|
343
|
+
import { join } from "path";
|
|
344
|
+
var RC_FILE = ".groundtruthrc.json";
|
|
345
|
+
var VALID_FAIL_LEVELS = /* @__PURE__ */ new Set(["unsupported", "unverifiable"]);
|
|
346
|
+
var VALID_KINDS = /* @__PURE__ */ new Set([
|
|
347
|
+
"file",
|
|
348
|
+
"symbol",
|
|
349
|
+
"test",
|
|
350
|
+
"dependency",
|
|
351
|
+
"command",
|
|
352
|
+
"action"
|
|
353
|
+
]);
|
|
354
|
+
var VALID_OUTPUTS = /* @__PURE__ */ new Set(["terminal", "json", "markdown"]);
|
|
355
|
+
function loadConfig(cwd) {
|
|
356
|
+
const pkg = readJson(join(cwd, "package.json"));
|
|
357
|
+
const fromPkg = pkg && isRecord2(pkg.groundtruth) ? pkg.groundtruth : void 0;
|
|
358
|
+
const fromRc = readJson(join(cwd, RC_FILE));
|
|
359
|
+
return { ...sanitize(fromPkg), ...sanitize(fromRc) };
|
|
360
|
+
}
|
|
361
|
+
function applyConfig(claims, config) {
|
|
362
|
+
const ignoreKinds = new Set(config.ignoreKinds ?? []);
|
|
363
|
+
const matchers = (config.ignore ?? []).map(toMatcher);
|
|
364
|
+
return claims.filter((claim) => {
|
|
365
|
+
if (ignoreKinds.has(claim.kind)) return false;
|
|
366
|
+
return !matchers.some((match) => match(claim.target));
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
function toMatcher(pattern) {
|
|
370
|
+
const p = pattern.toLowerCase();
|
|
371
|
+
if (p.includes("*")) {
|
|
372
|
+
const re = new RegExp(`^${p.split("*").map(escapeRe2).join(".*")}$`);
|
|
373
|
+
return (value) => re.test(value.toLowerCase());
|
|
374
|
+
}
|
|
375
|
+
return (value) => value.toLowerCase().includes(p);
|
|
376
|
+
}
|
|
377
|
+
function failingCount(report, config) {
|
|
378
|
+
const levels = config.failOn ?? ["unsupported"];
|
|
379
|
+
let n = 0;
|
|
380
|
+
if (levels.includes("unsupported")) n += report.summary.unsupported;
|
|
381
|
+
if (levels.includes("unverifiable")) n += report.summary.unverifiable;
|
|
382
|
+
return n;
|
|
383
|
+
}
|
|
384
|
+
function sanitize(input) {
|
|
385
|
+
if (!isRecord2(input)) return {};
|
|
386
|
+
const out = {};
|
|
387
|
+
if (typeof input.strict === "boolean") out.strict = input.strict;
|
|
388
|
+
if (typeof input.shadow === "boolean") out.shadow = input.shadow;
|
|
389
|
+
if (isStringArray(input.failOn)) {
|
|
390
|
+
out.failOn = input.failOn.filter((l) => VALID_FAIL_LEVELS.has(l));
|
|
391
|
+
}
|
|
392
|
+
if (isStringArray(input.ignore)) out.ignore = input.ignore;
|
|
393
|
+
if (isStringArray(input.ignoreKinds)) {
|
|
394
|
+
out.ignoreKinds = input.ignoreKinds.filter((k) => VALID_KINDS.has(k));
|
|
395
|
+
}
|
|
396
|
+
if (typeof input.output === "string" && VALID_OUTPUTS.has(input.output)) {
|
|
397
|
+
out.output = input.output;
|
|
398
|
+
}
|
|
399
|
+
return out;
|
|
400
|
+
}
|
|
401
|
+
function readJson(path) {
|
|
402
|
+
if (!existsSync(path)) return void 0;
|
|
403
|
+
try {
|
|
404
|
+
const parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
405
|
+
return isRecord2(parsed) ? parsed : void 0;
|
|
406
|
+
} catch {
|
|
407
|
+
return void 0;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
function isRecord2(v) {
|
|
411
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
412
|
+
}
|
|
413
|
+
function isStringArray(v) {
|
|
414
|
+
return Array.isArray(v) && v.every((item) => typeof item === "string");
|
|
415
|
+
}
|
|
416
|
+
function escapeRe2(s) {
|
|
417
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
418
|
+
}
|
|
419
|
+
|
|
319
420
|
// src/git.ts
|
|
320
421
|
import { execFileSync } from "child_process";
|
|
321
|
-
function collectGitEvidence(cwd) {
|
|
422
|
+
function collectGitEvidence(cwd, opts = {}) {
|
|
322
423
|
const ev = emptyEvidence();
|
|
424
|
+
if (opts.staged) {
|
|
425
|
+
const diff2 = git(["diff", "--cached", "--no-color", "--unified=0"], cwd);
|
|
426
|
+
if (diff2 !== null) parseDiff(diff2, ev);
|
|
427
|
+
const names = git(["diff", "--cached", "--name-status"], cwd);
|
|
428
|
+
if (names !== null) parseNameStatus(names, ev);
|
|
429
|
+
return ev;
|
|
430
|
+
}
|
|
431
|
+
if (opts.base) {
|
|
432
|
+
const range = `${opts.base}...HEAD`;
|
|
433
|
+
const diff2 = git(["diff", range, "--no-color", "--unified=0"], cwd);
|
|
434
|
+
if (diff2 !== null) parseDiff(diff2, ev);
|
|
435
|
+
const names = git(["diff", "--name-status", range], cwd);
|
|
436
|
+
if (names !== null) parseNameStatus(names, ev);
|
|
437
|
+
return ev;
|
|
438
|
+
}
|
|
323
439
|
const diff = git(["diff", "HEAD", "--no-color", "--unified=0"], cwd);
|
|
324
440
|
if (diff !== null) parseDiff(diff, ev);
|
|
325
441
|
const status = git(["status", "--porcelain"], cwd);
|
|
@@ -357,6 +473,17 @@ function stripDiffPath(raw) {
|
|
|
357
473
|
if (t === "/dev/null") return "";
|
|
358
474
|
return t.replace(/^[ab]\//, "");
|
|
359
475
|
}
|
|
476
|
+
function parseNameStatus(out, ev) {
|
|
477
|
+
for (const line of out.split("\n")) {
|
|
478
|
+
if (!line.trim()) continue;
|
|
479
|
+
const parts = line.split(" ");
|
|
480
|
+
const code = parts[0] ?? "";
|
|
481
|
+
const path = (parts.length > 2 ? parts[parts.length - 1] : parts[1]) ?? "";
|
|
482
|
+
if (!path) continue;
|
|
483
|
+
pushUnique(ev.touchedFiles, path);
|
|
484
|
+
if (code.startsWith("A")) pushUnique(ev.createdFiles, path);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
360
487
|
function parseStatus(status, ev) {
|
|
361
488
|
for (const line of status.split("\n")) {
|
|
362
489
|
if (!line.trim()) continue;
|
|
@@ -375,10 +502,10 @@ function pushUnique(arr, value) {
|
|
|
375
502
|
}
|
|
376
503
|
|
|
377
504
|
// src/evidence.ts
|
|
378
|
-
function buildEvidence(toolUses, cwd) {
|
|
505
|
+
function buildEvidence(toolUses, cwd, git2 = {}) {
|
|
379
506
|
const ev = emptyEvidence();
|
|
380
507
|
collectToolEvidence(toolUses, ev);
|
|
381
|
-
if (cwd) mergeEvidence(ev, collectGitEvidence(cwd));
|
|
508
|
+
if (cwd) mergeEvidence(ev, collectGitEvidence(cwd, git2));
|
|
382
509
|
return ev;
|
|
383
510
|
}
|
|
384
511
|
function emptyEvidence() {
|
|
@@ -410,7 +537,7 @@ ${str(input.old_string)}`;
|
|
|
410
537
|
if (file) addFile(ev, file, false);
|
|
411
538
|
const edits = Array.isArray(input.edits) ? input.edits : [];
|
|
412
539
|
for (const edit of edits) {
|
|
413
|
-
if (!
|
|
540
|
+
if (!isRecord3(edit)) continue;
|
|
414
541
|
ev.addedText += `
|
|
415
542
|
${str(edit.new_string)}`;
|
|
416
543
|
ev.removedText += `
|
|
@@ -458,7 +585,7 @@ function pushUnique2(arr, value) {
|
|
|
458
585
|
function str(v) {
|
|
459
586
|
return typeof v === "string" ? v : "";
|
|
460
587
|
}
|
|
461
|
-
function
|
|
588
|
+
function isRecord3(v) {
|
|
462
589
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
463
590
|
}
|
|
464
591
|
|
|
@@ -556,8 +683,20 @@ function fileMatches(claimPath, actual) {
|
|
|
556
683
|
if (b.endsWith(`/${a}`) || a.endsWith(`/${b}`)) return true;
|
|
557
684
|
if (!a.includes("/") && base(b) === a) return true;
|
|
558
685
|
if (!b.includes("/") && base(a) === b) return true;
|
|
686
|
+
if (!hasExt(a)) {
|
|
687
|
+
const bNoExt = stripExt(b);
|
|
688
|
+
if (a === bNoExt) return true;
|
|
689
|
+
if (bNoExt.endsWith(`/${a}`)) return true;
|
|
690
|
+
if (!a.includes("/") && base(bNoExt) === a) return true;
|
|
691
|
+
}
|
|
559
692
|
return false;
|
|
560
693
|
}
|
|
694
|
+
function hasExt(path) {
|
|
695
|
+
return /\.[A-Za-z0-9]+$/.test(base(path));
|
|
696
|
+
}
|
|
697
|
+
function stripExt(path) {
|
|
698
|
+
return path.replace(/\.[A-Za-z0-9]+$/, "");
|
|
699
|
+
}
|
|
561
700
|
function identifierPresent(haystack, id) {
|
|
562
701
|
if (!haystack) return false;
|
|
563
702
|
const esc = id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -807,23 +946,709 @@ function truncate(s, max) {
|
|
|
807
946
|
// src/pipeline.ts
|
|
808
947
|
function runPipeline(input) {
|
|
809
948
|
const turn = input.turn ?? (input.transcriptPath ? parseTranscriptFile(input.transcriptPath) : { summary: "", toolUses: [] });
|
|
810
|
-
const
|
|
811
|
-
const
|
|
949
|
+
const config = input.config ?? (input.cwd ? loadConfig(input.cwd) : {});
|
|
950
|
+
const evidence = buildEvidence(turn.toolUses, input.cwd, {
|
|
951
|
+
base: input.base,
|
|
952
|
+
staged: input.staged
|
|
953
|
+
});
|
|
954
|
+
const claims = applyConfig(extractClaims(turn.summary), config);
|
|
812
955
|
const verdicts = verifyClaims(claims, evidence);
|
|
813
956
|
return buildReport(verdicts);
|
|
814
957
|
}
|
|
958
|
+
|
|
959
|
+
// src/adapters/index.ts
|
|
960
|
+
import { createHash } from "crypto";
|
|
961
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync as readdirSync3, statSync as statSync3 } from "fs";
|
|
962
|
+
import { homedir as homedir2 } from "os";
|
|
963
|
+
import { join as join4 } from "path";
|
|
964
|
+
|
|
965
|
+
// src/locate.ts
|
|
966
|
+
import { existsSync as existsSync2, readdirSync, statSync } from "fs";
|
|
967
|
+
import { homedir } from "os";
|
|
968
|
+
import { join as join2, resolve } from "path";
|
|
969
|
+
function findLatestTranscript(cwd) {
|
|
970
|
+
const dir = projectDir(cwd);
|
|
971
|
+
if (!existsSync2(dir)) return null;
|
|
972
|
+
let newest = null;
|
|
973
|
+
for (const name of readdirSync(dir)) {
|
|
974
|
+
if (!name.endsWith(".jsonl")) continue;
|
|
975
|
+
const path = join2(dir, name);
|
|
976
|
+
try {
|
|
977
|
+
const mtime = statSync(path).mtimeMs;
|
|
978
|
+
if (!newest || mtime > newest.mtime) newest = { path, mtime };
|
|
979
|
+
} catch {
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
return newest?.path ?? null;
|
|
983
|
+
}
|
|
984
|
+
function projectDir(cwd) {
|
|
985
|
+
const encoded = resolve(cwd).replace(/[^A-Za-z0-9]/g, "-");
|
|
986
|
+
return join2(homedir(), ".claude", "projects", encoded);
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// src/adapters/turn.ts
|
|
990
|
+
function assembleTurn(events) {
|
|
991
|
+
let start = -1;
|
|
992
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
993
|
+
if (events[i]?.role === "user") {
|
|
994
|
+
start = i;
|
|
995
|
+
break;
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
let summary = "";
|
|
999
|
+
const toolUses = [];
|
|
1000
|
+
for (const e of events.slice(start + 1)) {
|
|
1001
|
+
if (e.role === "assistant" && e.text && e.text.trim()) summary = e.text.trim();
|
|
1002
|
+
if (e.tool) toolUses.push(e.tool);
|
|
1003
|
+
}
|
|
1004
|
+
return { summary, toolUses };
|
|
1005
|
+
}
|
|
1006
|
+
function parseJsonlLines(raw) {
|
|
1007
|
+
const out = [];
|
|
1008
|
+
for (const line of raw.split("\n")) {
|
|
1009
|
+
const trimmed = line.trim();
|
|
1010
|
+
if (!trimmed) continue;
|
|
1011
|
+
try {
|
|
1012
|
+
out.push(JSON.parse(trimmed));
|
|
1013
|
+
} catch {
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
return out;
|
|
1017
|
+
}
|
|
1018
|
+
function parseApplyPatch(patch) {
|
|
1019
|
+
const tools = [];
|
|
1020
|
+
let file = null;
|
|
1021
|
+
let op = null;
|
|
1022
|
+
let added = [];
|
|
1023
|
+
let removed = [];
|
|
1024
|
+
const flush = () => {
|
|
1025
|
+
if (!file || !op) return;
|
|
1026
|
+
if (op === "add") {
|
|
1027
|
+
tools.push({ name: "Write", input: { file_path: file, content: added.join("\n") } });
|
|
1028
|
+
} else {
|
|
1029
|
+
tools.push({
|
|
1030
|
+
name: "Edit",
|
|
1031
|
+
input: { file_path: file, new_string: added.join("\n"), old_string: removed.join("\n") }
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
added = [];
|
|
1035
|
+
removed = [];
|
|
1036
|
+
};
|
|
1037
|
+
for (const line of patch.split("\n")) {
|
|
1038
|
+
const add = /^\*\*\* Add File: (.+)$/.exec(line);
|
|
1039
|
+
const upd = /^\*\*\* Update File: (.+)$/.exec(line);
|
|
1040
|
+
if (add?.[1]) {
|
|
1041
|
+
flush();
|
|
1042
|
+
file = add[1].trim();
|
|
1043
|
+
op = "add";
|
|
1044
|
+
} else if (upd?.[1]) {
|
|
1045
|
+
flush();
|
|
1046
|
+
file = upd[1].trim();
|
|
1047
|
+
op = "update";
|
|
1048
|
+
} else if (/^\*\*\* (End Patch|Begin Patch|Delete File:)/.test(line)) {
|
|
1049
|
+
flush();
|
|
1050
|
+
if (line.startsWith("*** End Patch")) {
|
|
1051
|
+
file = null;
|
|
1052
|
+
op = null;
|
|
1053
|
+
}
|
|
1054
|
+
} else if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
1055
|
+
added.push(line.slice(1));
|
|
1056
|
+
} else if (line.startsWith("-") && !line.startsWith("---")) {
|
|
1057
|
+
removed.push(line.slice(1));
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
flush();
|
|
1061
|
+
return tools;
|
|
1062
|
+
}
|
|
1063
|
+
function isRecord4(v) {
|
|
1064
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1065
|
+
}
|
|
1066
|
+
function str2(v) {
|
|
1067
|
+
return typeof v === "string" ? v : "";
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
// src/adapters/aider.ts
|
|
1071
|
+
function parseAider(raw) {
|
|
1072
|
+
const sessions = raw.split(/^# aider chat started at .*$/m);
|
|
1073
|
+
const last = sessions[sessions.length - 1] ?? raw;
|
|
1074
|
+
const events = [];
|
|
1075
|
+
let assistant = [];
|
|
1076
|
+
let user = [];
|
|
1077
|
+
const flushAssistant = () => {
|
|
1078
|
+
const text = assistant.join("\n").trim();
|
|
1079
|
+
if (text) {
|
|
1080
|
+
events.push({ role: "assistant", text });
|
|
1081
|
+
for (const tool of parseSearchReplace(text)) events.push({ role: "assistant", tool });
|
|
1082
|
+
}
|
|
1083
|
+
assistant = [];
|
|
1084
|
+
};
|
|
1085
|
+
const flushUser = () => {
|
|
1086
|
+
const text = user.join("\n").trim();
|
|
1087
|
+
if (text) events.push({ role: "user", text });
|
|
1088
|
+
user = [];
|
|
1089
|
+
};
|
|
1090
|
+
for (const line of last.split("\n")) {
|
|
1091
|
+
if (line.startsWith("#### ")) {
|
|
1092
|
+
flushAssistant();
|
|
1093
|
+
user.push(line.slice(5));
|
|
1094
|
+
} else if (line.startsWith("> ")) {
|
|
1095
|
+
} else {
|
|
1096
|
+
flushUser();
|
|
1097
|
+
assistant.push(line);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
flushUser();
|
|
1101
|
+
flushAssistant();
|
|
1102
|
+
return assembleTurn(events);
|
|
1103
|
+
}
|
|
1104
|
+
function parseSearchReplace(text) {
|
|
1105
|
+
const tools = [];
|
|
1106
|
+
const lines = text.split("\n");
|
|
1107
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1108
|
+
if (!/^<{5,9} SEARCH\s*$/.test(lines[i] ?? "")) continue;
|
|
1109
|
+
let path = "";
|
|
1110
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
1111
|
+
const l = (lines[j] ?? "").trim();
|
|
1112
|
+
if (!l || l.startsWith("```")) continue;
|
|
1113
|
+
path = l;
|
|
1114
|
+
break;
|
|
1115
|
+
}
|
|
1116
|
+
const added = [];
|
|
1117
|
+
let inReplace = false;
|
|
1118
|
+
let k = i + 1;
|
|
1119
|
+
for (; k < lines.length; k++) {
|
|
1120
|
+
const l = lines[k] ?? "";
|
|
1121
|
+
if (/^={5,9}\s*$/.test(l)) {
|
|
1122
|
+
inReplace = true;
|
|
1123
|
+
continue;
|
|
1124
|
+
}
|
|
1125
|
+
if (/^>{5,9} REPLACE\s*$/.test(l)) break;
|
|
1126
|
+
if (inReplace) added.push(l);
|
|
1127
|
+
}
|
|
1128
|
+
if (path)
|
|
1129
|
+
tools.push({ name: "Edit", input: { file_path: path, new_string: added.join("\n") } });
|
|
1130
|
+
i = k;
|
|
1131
|
+
}
|
|
1132
|
+
return tools;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
// src/adapters/codex.ts
|
|
1136
|
+
function parseCodex(raw) {
|
|
1137
|
+
const events = [];
|
|
1138
|
+
for (const line of parseJsonlLines(raw)) {
|
|
1139
|
+
if (!isRecord4(line) || line.type !== "response_item" || !isRecord4(line.payload)) continue;
|
|
1140
|
+
const p = line.payload;
|
|
1141
|
+
switch (p.type) {
|
|
1142
|
+
case "message": {
|
|
1143
|
+
events.push({ role: p.role === "user" ? "user" : "assistant", text: textOf(p.content) });
|
|
1144
|
+
break;
|
|
1145
|
+
}
|
|
1146
|
+
case "function_call": {
|
|
1147
|
+
for (const tool of fromFunctionCall(str2(p.name), str2(p.arguments))) {
|
|
1148
|
+
events.push({ role: "assistant", tool });
|
|
1149
|
+
}
|
|
1150
|
+
break;
|
|
1151
|
+
}
|
|
1152
|
+
case "custom_tool_call": {
|
|
1153
|
+
if (str2(p.name) === "apply_patch") {
|
|
1154
|
+
for (const tool of parseApplyPatch(str2(p.input)))
|
|
1155
|
+
events.push({ role: "assistant", tool });
|
|
1156
|
+
}
|
|
1157
|
+
break;
|
|
1158
|
+
}
|
|
1159
|
+
case "local_shell_call": {
|
|
1160
|
+
const cmd = isRecord4(p.action) ? commandToString(p.action.command) : "";
|
|
1161
|
+
if (cmd)
|
|
1162
|
+
events.push({ role: "assistant", tool: { name: "Bash", input: { command: cmd } } });
|
|
1163
|
+
break;
|
|
1164
|
+
}
|
|
1165
|
+
default:
|
|
1166
|
+
break;
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
return assembleTurn(events);
|
|
1170
|
+
}
|
|
1171
|
+
function textOf(content) {
|
|
1172
|
+
if (typeof content === "string") return content;
|
|
1173
|
+
if (!Array.isArray(content)) return "";
|
|
1174
|
+
return content.filter(isRecord4).map((b) => typeof b.text === "string" ? b.text : "").join("\n").trim();
|
|
1175
|
+
}
|
|
1176
|
+
function fromFunctionCall(name, argsJson) {
|
|
1177
|
+
if (name === "apply_patch") {
|
|
1178
|
+
const parsed = safeJson(argsJson);
|
|
1179
|
+
const patch = isRecord4(parsed) && typeof parsed.input === "string" ? parsed.input : argsJson;
|
|
1180
|
+
return parseApplyPatch(patch);
|
|
1181
|
+
}
|
|
1182
|
+
if (name === "shell" || name === "shell_command" || name === "bash") {
|
|
1183
|
+
const parsed = safeJson(argsJson);
|
|
1184
|
+
const cmd = isRecord4(parsed) ? commandToString(parsed.command) : "";
|
|
1185
|
+
if (!cmd) return [];
|
|
1186
|
+
if (cmd.includes("apply_patch") && cmd.includes("*** Begin Patch")) {
|
|
1187
|
+
const m = /\*\*\* Begin Patch[\s\S]*?\*\*\* End Patch/.exec(cmd);
|
|
1188
|
+
if (m) return parseApplyPatch(m[0]);
|
|
1189
|
+
}
|
|
1190
|
+
return [{ name: "Bash", input: { command: cmd } }];
|
|
1191
|
+
}
|
|
1192
|
+
return [];
|
|
1193
|
+
}
|
|
1194
|
+
function commandToString(command) {
|
|
1195
|
+
if (typeof command === "string") return command;
|
|
1196
|
+
if (Array.isArray(command)) return command.filter((c2) => typeof c2 === "string").join(" ");
|
|
1197
|
+
return "";
|
|
1198
|
+
}
|
|
1199
|
+
function safeJson(s) {
|
|
1200
|
+
try {
|
|
1201
|
+
return JSON.parse(s);
|
|
1202
|
+
} catch {
|
|
1203
|
+
return null;
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
// src/adapters/cursor.ts
|
|
1208
|
+
function parseCursor(raw) {
|
|
1209
|
+
const events = [];
|
|
1210
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1211
|
+
let resultText = "";
|
|
1212
|
+
for (const line of parseJsonlLines(raw)) {
|
|
1213
|
+
if (!isRecord4(line)) continue;
|
|
1214
|
+
switch (line.type) {
|
|
1215
|
+
case "user":
|
|
1216
|
+
events.push({ role: "user", text: messageText(line.message) });
|
|
1217
|
+
break;
|
|
1218
|
+
case "assistant":
|
|
1219
|
+
events.push({ role: "assistant", text: messageText(line.message) });
|
|
1220
|
+
break;
|
|
1221
|
+
case "tool_call": {
|
|
1222
|
+
const id = str2(line.call_id);
|
|
1223
|
+
if (id && seen.has(id)) break;
|
|
1224
|
+
if (id) seen.add(id);
|
|
1225
|
+
const tool = fromToolCall(line.tool_call);
|
|
1226
|
+
if (tool) events.push({ role: "assistant", tool });
|
|
1227
|
+
break;
|
|
1228
|
+
}
|
|
1229
|
+
case "result":
|
|
1230
|
+
resultText = str2(line.result);
|
|
1231
|
+
break;
|
|
1232
|
+
default:
|
|
1233
|
+
break;
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
if (resultText.trim()) events.push({ role: "assistant", text: resultText });
|
|
1237
|
+
return assembleTurn(events);
|
|
1238
|
+
}
|
|
1239
|
+
function messageText(message) {
|
|
1240
|
+
if (!isRecord4(message)) return "";
|
|
1241
|
+
const content = message.content;
|
|
1242
|
+
if (typeof content === "string") return content;
|
|
1243
|
+
if (!Array.isArray(content)) return "";
|
|
1244
|
+
return content.filter(isRecord4).map((b) => typeof b.text === "string" ? b.text : "").join("\n").trim();
|
|
1245
|
+
}
|
|
1246
|
+
function fromToolCall(tc) {
|
|
1247
|
+
if (!isRecord4(tc)) return null;
|
|
1248
|
+
if (isRecord4(tc.writeToolCall)) {
|
|
1249
|
+
const a = argsOf(tc.writeToolCall);
|
|
1250
|
+
return { name: "Write", input: { file_path: str2(a.path), content: str2(a.fileText) } };
|
|
1251
|
+
}
|
|
1252
|
+
if (isRecord4(tc.editToolCall)) {
|
|
1253
|
+
const a = argsOf(tc.editToolCall);
|
|
1254
|
+
return {
|
|
1255
|
+
name: "Edit",
|
|
1256
|
+
input: {
|
|
1257
|
+
file_path: str2(a.path),
|
|
1258
|
+
new_string: str2(a.fileText ?? a.newString),
|
|
1259
|
+
old_string: str2(a.oldString)
|
|
1260
|
+
}
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1263
|
+
if (isRecord4(tc.shellToolCall)) {
|
|
1264
|
+
const a = argsOf(tc.shellToolCall);
|
|
1265
|
+
return { name: "Bash", input: { command: str2(a.command) } };
|
|
1266
|
+
}
|
|
1267
|
+
return null;
|
|
1268
|
+
}
|
|
1269
|
+
function argsOf(x) {
|
|
1270
|
+
return isRecord4(x.args) ? x.args : {};
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
// src/adapters/gemini.ts
|
|
1274
|
+
function parseGemini(raw) {
|
|
1275
|
+
const events = [];
|
|
1276
|
+
for (const rec of records(raw)) {
|
|
1277
|
+
if (!isRecord4(rec)) continue;
|
|
1278
|
+
if (rec.type === "user") {
|
|
1279
|
+
events.push({ role: "user", text: textOf2(rec.content) });
|
|
1280
|
+
} else if (rec.type === "gemini") {
|
|
1281
|
+
events.push({ role: "assistant", text: textOf2(rec.content) });
|
|
1282
|
+
const calls = Array.isArray(rec.toolCalls) ? rec.toolCalls : [];
|
|
1283
|
+
for (const call of calls) {
|
|
1284
|
+
const tool = fromToolCall2(call);
|
|
1285
|
+
if (tool) events.push({ role: "assistant", tool });
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
return assembleTurn(events);
|
|
1290
|
+
}
|
|
1291
|
+
function records(raw) {
|
|
1292
|
+
const trimmed = raw.trim();
|
|
1293
|
+
if (trimmed.startsWith("{")) {
|
|
1294
|
+
try {
|
|
1295
|
+
const obj = JSON.parse(trimmed);
|
|
1296
|
+
if (isRecord4(obj) && Array.isArray(obj.messages)) return obj.messages;
|
|
1297
|
+
} catch {
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
return parseJsonlLines(raw);
|
|
1301
|
+
}
|
|
1302
|
+
function textOf2(content) {
|
|
1303
|
+
if (typeof content === "string") return content;
|
|
1304
|
+
if (!Array.isArray(content)) return "";
|
|
1305
|
+
return content.filter(isRecord4).map((b) => typeof b.text === "string" ? b.text : "").join("\n").trim();
|
|
1306
|
+
}
|
|
1307
|
+
function fromToolCall2(call) {
|
|
1308
|
+
if (!isRecord4(call)) return null;
|
|
1309
|
+
const name = str2(call.name);
|
|
1310
|
+
const args = isRecord4(call.args) ? call.args : {};
|
|
1311
|
+
if (name === "write_file") {
|
|
1312
|
+
return { name: "Write", input: { file_path: str2(args.file_path), content: str2(args.content) } };
|
|
1313
|
+
}
|
|
1314
|
+
if (name === "replace") {
|
|
1315
|
+
return {
|
|
1316
|
+
name: "Edit",
|
|
1317
|
+
input: {
|
|
1318
|
+
file_path: str2(args.file_path),
|
|
1319
|
+
new_string: str2(args.new_string),
|
|
1320
|
+
old_string: str2(args.old_string)
|
|
1321
|
+
}
|
|
1322
|
+
};
|
|
1323
|
+
}
|
|
1324
|
+
if (name === "run_shell_command") {
|
|
1325
|
+
return { name: "Bash", input: { command: str2(args.command) } };
|
|
1326
|
+
}
|
|
1327
|
+
return null;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
// src/adapters/opencode.ts
|
|
1331
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
1332
|
+
import { dirname, join as join3 } from "path";
|
|
1333
|
+
function parseOpenCode(input) {
|
|
1334
|
+
const storageRoot = resolveStorageRoot(input);
|
|
1335
|
+
const messageRoot = join3(storageRoot, "message");
|
|
1336
|
+
if (!existsSync3(messageRoot)) return { summary: "", toolUses: [] };
|
|
1337
|
+
const session = latestSession(messageRoot);
|
|
1338
|
+
if (!session) return { summary: "", toolUses: [] };
|
|
1339
|
+
const messages = loadMessages(join3(messageRoot, session)).sort((a, b) => a.created - b.created);
|
|
1340
|
+
const events = [];
|
|
1341
|
+
for (const msg of messages) {
|
|
1342
|
+
const parts = loadParts(join3(storageRoot, "part", msg.id));
|
|
1343
|
+
if (msg.role === "user") {
|
|
1344
|
+
const text = parts.filter((p) => p.type === "text").map((p) => str2(p.text)).join("\n").trim();
|
|
1345
|
+
events.push({ role: "user", text });
|
|
1346
|
+
continue;
|
|
1347
|
+
}
|
|
1348
|
+
for (const part of parts) {
|
|
1349
|
+
if (part.type === "text" && str2(part.text).trim()) {
|
|
1350
|
+
events.push({ role: "assistant", text: str2(part.text) });
|
|
1351
|
+
} else if (part.type === "tool") {
|
|
1352
|
+
const tool = fromToolPart(part);
|
|
1353
|
+
if (tool) events.push({ role: "assistant", tool });
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
return assembleTurn(events);
|
|
1358
|
+
}
|
|
1359
|
+
function resolveStorageRoot(p) {
|
|
1360
|
+
try {
|
|
1361
|
+
if (statSync2(p).isFile()) return dirname(dirname(dirname(p)));
|
|
1362
|
+
} catch {
|
|
1363
|
+
}
|
|
1364
|
+
return p;
|
|
1365
|
+
}
|
|
1366
|
+
function latestSession(messageRoot) {
|
|
1367
|
+
let best = null;
|
|
1368
|
+
for (const session of listDirs(messageRoot)) {
|
|
1369
|
+
for (const file of listJson(join3(messageRoot, session))) {
|
|
1370
|
+
try {
|
|
1371
|
+
const mtime = statSync2(join3(messageRoot, session, file)).mtimeMs;
|
|
1372
|
+
if (!best || mtime > best.mtime) best = { session, mtime };
|
|
1373
|
+
} catch {
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
return best?.session ?? null;
|
|
1378
|
+
}
|
|
1379
|
+
function loadMessages(dir) {
|
|
1380
|
+
const out = [];
|
|
1381
|
+
for (const file of listJson(dir)) {
|
|
1382
|
+
const obj = readJson2(join3(dir, file));
|
|
1383
|
+
if (!isRecord4(obj)) continue;
|
|
1384
|
+
const id = str2(obj.id) || file.replace(/\.json$/, "");
|
|
1385
|
+
const role = str2(obj.role);
|
|
1386
|
+
const created = isRecord4(obj.time) && typeof obj.time.created === "number" ? obj.time.created : 0;
|
|
1387
|
+
out.push({ id, role, created });
|
|
1388
|
+
}
|
|
1389
|
+
return out;
|
|
1390
|
+
}
|
|
1391
|
+
function loadParts(dir) {
|
|
1392
|
+
if (!existsSync3(dir)) return [];
|
|
1393
|
+
const out = [];
|
|
1394
|
+
for (const file of listJson(dir)) {
|
|
1395
|
+
const obj = readJson2(join3(dir, file));
|
|
1396
|
+
if (isRecord4(obj) && typeof obj.type === "string") {
|
|
1397
|
+
out.push({
|
|
1398
|
+
type: obj.type,
|
|
1399
|
+
text: typeof obj.text === "string" ? obj.text : void 0,
|
|
1400
|
+
tool: typeof obj.tool === "string" ? obj.tool : void 0,
|
|
1401
|
+
state: obj.state
|
|
1402
|
+
});
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
return out;
|
|
1406
|
+
}
|
|
1407
|
+
function fromToolPart(part) {
|
|
1408
|
+
const tool = str2(part.tool);
|
|
1409
|
+
const state = isRecord4(part.state) ? part.state : {};
|
|
1410
|
+
const input = isRecord4(state.input) ? state.input : {};
|
|
1411
|
+
if (tool === "write") {
|
|
1412
|
+
return {
|
|
1413
|
+
name: "Write",
|
|
1414
|
+
input: { file_path: str2(input.filePath), content: str2(input.content) }
|
|
1415
|
+
};
|
|
1416
|
+
}
|
|
1417
|
+
if (tool === "edit") {
|
|
1418
|
+
return {
|
|
1419
|
+
name: "Edit",
|
|
1420
|
+
input: {
|
|
1421
|
+
file_path: str2(input.filePath),
|
|
1422
|
+
new_string: str2(input.newString),
|
|
1423
|
+
old_string: str2(input.oldString)
|
|
1424
|
+
}
|
|
1425
|
+
};
|
|
1426
|
+
}
|
|
1427
|
+
if (tool === "bash") {
|
|
1428
|
+
return { name: "Bash", input: { command: str2(input.command) } };
|
|
1429
|
+
}
|
|
1430
|
+
return null;
|
|
1431
|
+
}
|
|
1432
|
+
function listDirs(dir) {
|
|
1433
|
+
try {
|
|
1434
|
+
return readdirSync2(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
1435
|
+
} catch {
|
|
1436
|
+
return [];
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
function listJson(dir) {
|
|
1440
|
+
try {
|
|
1441
|
+
return readdirSync2(dir).filter((n) => n.endsWith(".json"));
|
|
1442
|
+
} catch {
|
|
1443
|
+
return [];
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
function readJson2(path) {
|
|
1447
|
+
try {
|
|
1448
|
+
return JSON.parse(readFileSync3(path, "utf8"));
|
|
1449
|
+
} catch {
|
|
1450
|
+
return null;
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
// src/adapters/index.ts
|
|
1455
|
+
var claude = {
|
|
1456
|
+
name: "claude",
|
|
1457
|
+
locate: (cwd) => findLatestTranscript(cwd),
|
|
1458
|
+
parse: (path) => parseTranscriptFile(path)
|
|
1459
|
+
};
|
|
1460
|
+
var codex = {
|
|
1461
|
+
name: "codex",
|
|
1462
|
+
locate() {
|
|
1463
|
+
const home = process.env.CODEX_HOME ?? join4(homedir2(), ".codex");
|
|
1464
|
+
return newestMatch(
|
|
1465
|
+
join4(home, "sessions"),
|
|
1466
|
+
(n) => n.startsWith("rollout-") && n.endsWith(".jsonl")
|
|
1467
|
+
);
|
|
1468
|
+
},
|
|
1469
|
+
parse: (path) => parseCodex(readFileSync4(path, "utf8"))
|
|
1470
|
+
};
|
|
1471
|
+
var gemini = {
|
|
1472
|
+
name: "gemini",
|
|
1473
|
+
locate(cwd) {
|
|
1474
|
+
const home = process.env.GEMINI_DIR ?? join4(homedir2(), ".gemini");
|
|
1475
|
+
const hash = createHash("sha256").update(cwd).digest("hex");
|
|
1476
|
+
const scoped = newestMatch(
|
|
1477
|
+
join4(home, "tmp", hash, "chats"),
|
|
1478
|
+
(n) => n.endsWith(".jsonl") || n.endsWith(".json")
|
|
1479
|
+
);
|
|
1480
|
+
if (scoped) return scoped;
|
|
1481
|
+
return newestMatch(join4(home, "tmp"), (n) => n.startsWith("session") && /\.jsonl?$/.test(n));
|
|
1482
|
+
},
|
|
1483
|
+
parse: (path) => parseGemini(readFileSync4(path, "utf8"))
|
|
1484
|
+
};
|
|
1485
|
+
var cursor = {
|
|
1486
|
+
name: "cursor",
|
|
1487
|
+
locate() {
|
|
1488
|
+
return newestMatch(join4(homedir2(), ".cursor", "projects"), (n) => n.endsWith(".jsonl"));
|
|
1489
|
+
},
|
|
1490
|
+
parse: (path) => parseCursor(readFileSync4(path, "utf8"))
|
|
1491
|
+
};
|
|
1492
|
+
var opencode = {
|
|
1493
|
+
name: "opencode",
|
|
1494
|
+
locate() {
|
|
1495
|
+
const base2 = process.env.XDG_DATA_HOME ? join4(process.env.XDG_DATA_HOME, "opencode") : join4(homedir2(), ".local", "share", "opencode");
|
|
1496
|
+
return newestMatch(join4(base2, "storage", "message"), (n) => n.endsWith(".json"));
|
|
1497
|
+
},
|
|
1498
|
+
parse: (path) => parseOpenCode(path)
|
|
1499
|
+
};
|
|
1500
|
+
var aider = {
|
|
1501
|
+
name: "aider",
|
|
1502
|
+
locate(cwd) {
|
|
1503
|
+
const file = join4(cwd, ".aider.chat.history.md");
|
|
1504
|
+
return existsSync4(file) ? file : null;
|
|
1505
|
+
},
|
|
1506
|
+
parse: (path) => parseAider(readFileSync4(path, "utf8"))
|
|
1507
|
+
};
|
|
1508
|
+
var ADAPTERS = {
|
|
1509
|
+
claude,
|
|
1510
|
+
codex,
|
|
1511
|
+
gemini,
|
|
1512
|
+
cursor,
|
|
1513
|
+
opencode,
|
|
1514
|
+
aider
|
|
1515
|
+
};
|
|
1516
|
+
var AGENT_NAMES = Object.keys(ADAPTERS);
|
|
1517
|
+
function getAdapter(name) {
|
|
1518
|
+
return ADAPTERS[name] ?? null;
|
|
1519
|
+
}
|
|
1520
|
+
function autoDetect(cwd) {
|
|
1521
|
+
let best = null;
|
|
1522
|
+
for (const adapter of Object.values(ADAPTERS)) {
|
|
1523
|
+
const path = adapter.locate(cwd);
|
|
1524
|
+
if (!path) continue;
|
|
1525
|
+
try {
|
|
1526
|
+
const mtime = statSync3(path).mtimeMs;
|
|
1527
|
+
if (!best || mtime > best.mtime) best = { adapter, path, mtime };
|
|
1528
|
+
} catch {
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
return best ? { adapter: best.adapter, path: best.path } : null;
|
|
1532
|
+
}
|
|
1533
|
+
function newestMatch(dir, test, depth = 6) {
|
|
1534
|
+
const found = walk(dir, test, depth);
|
|
1535
|
+
return found?.path ?? null;
|
|
1536
|
+
}
|
|
1537
|
+
function walk(dir, test, depth) {
|
|
1538
|
+
if (depth < 0 || !existsSync4(dir)) return null;
|
|
1539
|
+
let entries;
|
|
1540
|
+
try {
|
|
1541
|
+
entries = readdirSync3(dir, { withFileTypes: true });
|
|
1542
|
+
} catch {
|
|
1543
|
+
return null;
|
|
1544
|
+
}
|
|
1545
|
+
let best = null;
|
|
1546
|
+
for (const entry of entries) {
|
|
1547
|
+
const full = join4(dir, entry.name);
|
|
1548
|
+
try {
|
|
1549
|
+
if (entry.isDirectory()) {
|
|
1550
|
+
const sub = walk(full, test, depth - 1);
|
|
1551
|
+
if (sub && (!best || sub.mtime > best.mtime)) best = sub;
|
|
1552
|
+
} else if (entry.isFile() && test(entry.name)) {
|
|
1553
|
+
const mtime = statSync3(full).mtimeMs;
|
|
1554
|
+
if (!best || mtime > best.mtime) best = { path: full, mtime };
|
|
1555
|
+
}
|
|
1556
|
+
} catch {
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
return best;
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
// src/ledger.ts
|
|
1563
|
+
import { appendFileSync, existsSync as existsSync5, mkdirSync, readFileSync as readFileSync5 } from "fs";
|
|
1564
|
+
import { homedir as homedir3 } from "os";
|
|
1565
|
+
import { dirname as dirname2, join as join5 } from "path";
|
|
1566
|
+
function ledgerPath() {
|
|
1567
|
+
return process.env.GROUNDTRUTH_LEDGER ?? join5(homedir3(), ".groundtruth", "ledger.jsonl");
|
|
1568
|
+
}
|
|
1569
|
+
function recordRun(report, cwd, session) {
|
|
1570
|
+
if (report.summary.total === 0) return;
|
|
1571
|
+
const entry = {
|
|
1572
|
+
t: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1573
|
+
cwd,
|
|
1574
|
+
v: report.summary.verified,
|
|
1575
|
+
u: report.summary.unsupported,
|
|
1576
|
+
r: report.summary.unverifiable
|
|
1577
|
+
};
|
|
1578
|
+
if (session) entry.session = session;
|
|
1579
|
+
try {
|
|
1580
|
+
const path = ledgerPath();
|
|
1581
|
+
mkdirSync(dirname2(path), { recursive: true });
|
|
1582
|
+
appendFileSync(path, `${JSON.stringify(entry)}
|
|
1583
|
+
`, "utf8");
|
|
1584
|
+
} catch {
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
function readLedger() {
|
|
1588
|
+
const path = ledgerPath();
|
|
1589
|
+
if (!existsSync5(path)) return [];
|
|
1590
|
+
const out = [];
|
|
1591
|
+
try {
|
|
1592
|
+
for (const line of readFileSync5(path, "utf8").split("\n")) {
|
|
1593
|
+
const trimmed = line.trim();
|
|
1594
|
+
if (!trimmed) continue;
|
|
1595
|
+
try {
|
|
1596
|
+
const parsed = JSON.parse(trimmed);
|
|
1597
|
+
if (isEntry(parsed)) out.push(parsed);
|
|
1598
|
+
} catch {
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
} catch {
|
|
1602
|
+
}
|
|
1603
|
+
return out;
|
|
1604
|
+
}
|
|
1605
|
+
function summarize(entries, opts = {}) {
|
|
1606
|
+
const cutoff = opts.sinceDays !== void 0 ? Date.now() - opts.sinceDays * 864e5 : 0;
|
|
1607
|
+
const sum = { runs: 0, verified: 0, unsupported: 0, unverifiable: 0 };
|
|
1608
|
+
for (const e of entries) {
|
|
1609
|
+
if (opts.cwd && e.cwd !== opts.cwd) continue;
|
|
1610
|
+
if (opts.session && e.session !== opts.session) continue;
|
|
1611
|
+
if (cutoff && Date.parse(e.t) < cutoff) continue;
|
|
1612
|
+
sum.runs += 1;
|
|
1613
|
+
sum.verified += e.v;
|
|
1614
|
+
sum.unsupported += e.u;
|
|
1615
|
+
sum.unverifiable += e.r;
|
|
1616
|
+
}
|
|
1617
|
+
return sum;
|
|
1618
|
+
}
|
|
1619
|
+
function isEntry(v) {
|
|
1620
|
+
if (typeof v !== "object" || v === null) return false;
|
|
1621
|
+
const e = v;
|
|
1622
|
+
return typeof e.t === "string" && typeof e.cwd === "string" && typeof e.v === "number" && typeof e.u === "number" && typeof e.r === "number";
|
|
1623
|
+
}
|
|
815
1624
|
export {
|
|
1625
|
+
ADAPTERS,
|
|
1626
|
+
AGENT_NAMES,
|
|
1627
|
+
applyConfig,
|
|
1628
|
+
autoDetect,
|
|
816
1629
|
buildEvidence,
|
|
817
1630
|
buildReport,
|
|
818
1631
|
collectGitEvidence,
|
|
819
1632
|
emptyEvidence,
|
|
820
1633
|
extractClaims,
|
|
1634
|
+
failingCount,
|
|
1635
|
+
getAdapter,
|
|
1636
|
+
ledgerPath,
|
|
1637
|
+
loadConfig,
|
|
821
1638
|
mergeEvidence,
|
|
1639
|
+
parseAider,
|
|
1640
|
+
parseCodex,
|
|
1641
|
+
parseCursor,
|
|
1642
|
+
parseGemini,
|
|
1643
|
+
parseOpenCode,
|
|
822
1644
|
parseTranscript,
|
|
823
1645
|
parseTranscriptFile,
|
|
1646
|
+
readLedger,
|
|
1647
|
+
recordRun,
|
|
824
1648
|
renderJson,
|
|
825
1649
|
renderMarkdown,
|
|
826
1650
|
renderTerminal,
|
|
827
1651
|
runPipeline,
|
|
1652
|
+
summarize,
|
|
828
1653
|
verifyClaims
|
|
829
1654
|
};
|