evrex-mcp 0.8.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/account.js +1 -1
- package/dist/capture.js +482 -41
- package/dist/continuity.js +32 -4
- package/dist/hook.js +33 -3
- package/dist/import.js +291 -40
- package/dist/index.js +75 -3
- package/dist/pretool.js +29 -2
- package/dist/tickets.js +1 -1
- package/package.json +15 -16
package/dist/import.js
CHANGED
|
@@ -184,14 +184,34 @@ function commitMeta(repoPath, sha) {
|
|
|
184
184
|
sha
|
|
185
185
|
]).trim();
|
|
186
186
|
const parts = line.split(FIELD_SEP);
|
|
187
|
-
const
|
|
187
|
+
const body = git(repoPath, ["show", "-s", "--notes", `--format=%B${FIELD_SEP}%N`, sha]);
|
|
188
|
+
const sep2 = body.indexOf(FIELD_SEP);
|
|
189
|
+
let message = (sep2 === -1 ? body : body.slice(0, sep2)).replace(/\n+$/, "");
|
|
190
|
+
const note = sep2 === -1 ? "" : body.slice(sep2 + FIELD_SEP.length);
|
|
191
|
+
const noted = /^Evrex-Session:\s*(\S+)\s*$/m.exec(note);
|
|
192
|
+
if (noted && !/^Evrex-Session:/m.test(message)) {
|
|
193
|
+
message = `${message}
|
|
194
|
+
|
|
195
|
+
${noted[0].trim()}`;
|
|
196
|
+
}
|
|
188
197
|
return {
|
|
189
198
|
sha: parts[0] ?? sha,
|
|
190
199
|
author: parts[1] ?? "",
|
|
191
200
|
authorEmail: parts[2] ?? "",
|
|
192
201
|
ts: parts[3] ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
193
202
|
parents: (parts[4] ?? "").split(" ").filter(Boolean),
|
|
194
|
-
message
|
|
203
|
+
message,
|
|
204
|
+
// The note's session, exposed on its own as well as folded above. When
|
|
205
|
+
// the message ALREADY carries a trailer, the fold declines (never rewrite
|
|
206
|
+
// what was stamped at commit time) — but the parse must still prefer the
|
|
207
|
+
// note: it exists only because an operator pressed "verify" on a commit
|
|
208
|
+
// whose message trailer names a session no ingest can resolve. Commit
|
|
209
|
+
// 843dabb3 is the standing example — its at-commit-time trailer names a
|
|
210
|
+
// session with no transcript anywhere, so the folding guard alone left it
|
|
211
|
+
// permanently un-verifiable: every promote re-ingested the dangling id,
|
|
212
|
+
// bestLink demoted it below the resolvable match, and the button offered
|
|
213
|
+
// the same commit again.
|
|
214
|
+
notedSession: noted?.[1] ?? null
|
|
195
215
|
};
|
|
196
216
|
}
|
|
197
217
|
function parseTrailers(repoPath, message) {
|
|
@@ -312,6 +332,9 @@ function commitFiles(repoPath, sha) {
|
|
|
312
332
|
}
|
|
313
333
|
function parseGitLog(repoPath, repoId, known) {
|
|
314
334
|
const shas = listShas(repoPath).filter((sha) => !known?.has(sha));
|
|
335
|
+
return parseCommitsBySha(repoPath, shas, repoId);
|
|
336
|
+
}
|
|
337
|
+
function parseCommitsBySha(repoPath, shas, repoId) {
|
|
315
338
|
const id = repoId ?? deriveRepoId(repoPath);
|
|
316
339
|
return shas.map((sha) => {
|
|
317
340
|
const meta = commitMeta(repoPath, sha);
|
|
@@ -326,7 +349,9 @@ function parseGitLog(repoPath, repoId, known) {
|
|
|
326
349
|
message: meta.message,
|
|
327
350
|
parents: meta.parents,
|
|
328
351
|
branch: commitBranch(repoPath, sha),
|
|
329
|
-
|
|
352
|
+
// A note is the operator's explicit later correction, so where both
|
|
353
|
+
// exist it names the session — see commitMeta's notedSession.
|
|
354
|
+
evrexSessionTrailer: meta.notedSession ?? trailerValue(trailers, EVREX_SESSION_TRAILER_KEY),
|
|
330
355
|
origin: originOf(trailers),
|
|
331
356
|
agentTrailers: agentTrailersOf(trailers),
|
|
332
357
|
statedInsights: statedInsightsOf(trailers),
|
|
@@ -407,7 +432,7 @@ var PARSER_VERSION;
|
|
|
407
432
|
var init_incremental = __esm({
|
|
408
433
|
"../../packages/ingest-core/src/incremental.ts"() {
|
|
409
434
|
"use strict";
|
|
410
|
-
PARSER_VERSION =
|
|
435
|
+
PARSER_VERSION = 9;
|
|
411
436
|
}
|
|
412
437
|
});
|
|
413
438
|
|
|
@@ -522,11 +547,54 @@ var init_redact = __esm({
|
|
|
522
547
|
|
|
523
548
|
// ../../packages/ingest-core/src/subject-linking.ts
|
|
524
549
|
function matchCommitToSession(commit, sessions) {
|
|
550
|
+
const saw = sessions.filter(
|
|
551
|
+
(s) => (s.committedShas ?? []).some(
|
|
552
|
+
(short) => short.length >= 7 && commit.sha.startsWith(short)
|
|
553
|
+
)
|
|
554
|
+
);
|
|
555
|
+
if (saw.length > 0) {
|
|
556
|
+
const oneLineage2 = saw.every((s) => s.startedAt === saw[0].startedAt);
|
|
557
|
+
if (!oneLineage2) {
|
|
558
|
+
return { sha: commit.sha, sessionId: null, reason: "ambiguous" };
|
|
559
|
+
}
|
|
560
|
+
const original2 = [...saw].sort((a, b) => a.endedAt - b.endedAt)[0];
|
|
561
|
+
return {
|
|
562
|
+
sha: commit.sha,
|
|
563
|
+
sessionId: original2.sessionId,
|
|
564
|
+
evidence: "saw-the-sha",
|
|
565
|
+
confidence: SHA_CONFIDENCE
|
|
566
|
+
};
|
|
567
|
+
}
|
|
525
568
|
const subject = commit.subject.trim();
|
|
569
|
+
const prMatch = /\(#(\d+)\)$/.exec(subject) ?? /^Merge pull request #(\d+)\b/.exec(subject);
|
|
570
|
+
if (prMatch) {
|
|
571
|
+
const pr = Number(prMatch[1]);
|
|
572
|
+
const mergedIt = sessions.filter(
|
|
573
|
+
(s) => (s.mergedPrNumbers ?? []).includes(pr)
|
|
574
|
+
);
|
|
575
|
+
if (mergedIt.length > 0) {
|
|
576
|
+
const oneLineage2 = mergedIt.every(
|
|
577
|
+
(s) => s.startedAt === mergedIt[0].startedAt
|
|
578
|
+
);
|
|
579
|
+
if (!oneLineage2) {
|
|
580
|
+
return { sha: commit.sha, sessionId: null, reason: "ambiguous" };
|
|
581
|
+
}
|
|
582
|
+
const original2 = [...mergedIt].sort((a, b) => a.endedAt - b.endedAt)[0];
|
|
583
|
+
return {
|
|
584
|
+
sha: commit.sha,
|
|
585
|
+
sessionId: original2.sessionId,
|
|
586
|
+
evidence: "merged-the-pr",
|
|
587
|
+
confidence: SHA_CONFIDENCE
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
}
|
|
526
591
|
if (subject.length === 0) {
|
|
527
592
|
return { sha: commit.sha, sessionId: null, reason: "absent" };
|
|
528
593
|
}
|
|
529
|
-
const
|
|
594
|
+
const unsquashed = subject.replace(/\s+\(#\d+\)$/, "");
|
|
595
|
+
const ran = sessions.filter(
|
|
596
|
+
(s) => s.committedSubjects.includes(subject) || unsquashed !== subject && s.committedSubjects.includes(unsquashed)
|
|
597
|
+
);
|
|
530
598
|
if (ran.length === 0) {
|
|
531
599
|
return { sha: commit.sha, sessionId: null, reason: "absent" };
|
|
532
600
|
}
|
|
@@ -580,15 +648,61 @@ function committedSubjects(command) {
|
|
|
580
648
|
if (inline) {
|
|
581
649
|
const subject = inline[2].split("\n")[0].trim();
|
|
582
650
|
if (subject) out.push(subject);
|
|
651
|
+
continue;
|
|
652
|
+
}
|
|
653
|
+
const marginal = /^[^\n]*?-am\s*(['"])([\s\S]*?)\1/.exec(rest) ?? /^[^\n]*?--message[= ]\s*(['"])([\s\S]*?)\1/.exec(rest) ?? /^[^\n]*?-m(['"])([\s\S]*?)\1/.exec(rest);
|
|
654
|
+
if (marginal) {
|
|
655
|
+
const subject = marginal[2].split("\n")[0].trim();
|
|
656
|
+
if (subject) out.push(subject);
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
659
|
+
const fromFile = /^[^\n]*?(?:-F|--file)[= ]\s*(\S+)/.exec(rest);
|
|
660
|
+
if (fromFile) {
|
|
661
|
+
const file = fromFile[1].replace(/^["']|["']$/g, "");
|
|
662
|
+
const escaped = file.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
663
|
+
const wrote = new RegExp(
|
|
664
|
+
`(?:cat\\s*>>?|tee\\s+(?:-a\\s+)?)\\s*["']?${escaped}["']?[^\\n]*<<-?\\s*(['"]?)(\\w+)\\1[^\\n]*\\r?\\n([^\\n]*)`
|
|
665
|
+
).exec(command.slice(0, found.index));
|
|
666
|
+
if (wrote) {
|
|
667
|
+
const subject = wrote[3].trim();
|
|
668
|
+
if (subject) out.push(subject);
|
|
669
|
+
}
|
|
583
670
|
}
|
|
584
671
|
}
|
|
585
672
|
return out;
|
|
586
673
|
}
|
|
587
|
-
|
|
674
|
+
function mergeResponseShas(output) {
|
|
675
|
+
const out = /* @__PURE__ */ new Set();
|
|
676
|
+
const line = /"sha"\s*:\s*"([0-9a-f]{40})"/g;
|
|
677
|
+
for (let m = line.exec(output); m !== null; m = line.exec(output)) {
|
|
678
|
+
out.add(m[1]);
|
|
679
|
+
}
|
|
680
|
+
return [...out];
|
|
681
|
+
}
|
|
682
|
+
function commitShaOutputs(output) {
|
|
683
|
+
const out = /* @__PURE__ */ new Set();
|
|
684
|
+
const line = /\[[\w./+-]+ (?:\(root-commit\) )?([0-9a-f]{7,12})\]/g;
|
|
685
|
+
for (let m = line.exec(output); m !== null; m = line.exec(output)) {
|
|
686
|
+
out.add(m[1]);
|
|
687
|
+
}
|
|
688
|
+
return [...out];
|
|
689
|
+
}
|
|
690
|
+
function mergedPrFromCommand(command) {
|
|
691
|
+
const api = /\/pulls\/(\d+)\/merge\b/.exec(command);
|
|
692
|
+
if (api) return Number(api[1]);
|
|
693
|
+
const gh = /\bgh\s+pr\s+merge\s+(?:.*?\/pull\/(\d+)\b|(\d+)\b)/.exec(command);
|
|
694
|
+
if (gh) return Number(gh[1] ?? gh[2]);
|
|
695
|
+
return null;
|
|
696
|
+
}
|
|
697
|
+
function mergeSucceeded(output) {
|
|
698
|
+
return /"merged"\s*:\s*true/.test(output) || /(Squashed and |Rebased and )?[Mm]erged pull request #\d+/.test(output);
|
|
699
|
+
}
|
|
700
|
+
var INFERRED_CONFIDENCE, SHA_CONFIDENCE;
|
|
588
701
|
var init_subject_linking = __esm({
|
|
589
702
|
"../../packages/ingest-core/src/subject-linking.ts"() {
|
|
590
703
|
"use strict";
|
|
591
704
|
INFERRED_CONFIDENCE = 0.9;
|
|
705
|
+
SHA_CONFIDENCE = 0.95;
|
|
592
706
|
}
|
|
593
707
|
});
|
|
594
708
|
|
|
@@ -663,14 +777,30 @@ function findSessionFiles(repoPath) {
|
|
|
663
777
|
function editFromXcodeInput(input) {
|
|
664
778
|
const filePath = input.filePath;
|
|
665
779
|
if (typeof filePath !== "string" || !filePath) return null;
|
|
666
|
-
|
|
667
|
-
|
|
780
|
+
return editFromStrings(filePath, textOf(input.oldString), textOf(input.newString));
|
|
781
|
+
}
|
|
782
|
+
function textOf(value) {
|
|
783
|
+
return typeof value === "string" ? value : "";
|
|
784
|
+
}
|
|
785
|
+
function editFromStrings(path, before, after) {
|
|
786
|
+
const oldLines = before ? before.split("\n") : [];
|
|
787
|
+
const newLines = after ? after.split("\n") : [];
|
|
668
788
|
const oldSet = new Set(oldLines);
|
|
669
789
|
const newSet = new Set(newLines);
|
|
670
790
|
const added = meaningfulLines(newLines.filter((l) => !oldSet.has(l)));
|
|
671
791
|
const removed = meaningfulLines(oldLines.filter((l) => !newSet.has(l)));
|
|
672
792
|
if (added.length === 0 && removed.length === 0) return null;
|
|
673
|
-
return { path
|
|
793
|
+
return { path, added, removed };
|
|
794
|
+
}
|
|
795
|
+
function editFromClaudeInput(name, input) {
|
|
796
|
+
const path = input.file_path;
|
|
797
|
+
if (typeof path !== "string" || !path) return null;
|
|
798
|
+
if (name === "Write") return editFromStrings(path, "", textOf(input.content));
|
|
799
|
+
if (name === "Edit") return editFromStrings(path, textOf(input.old_string), textOf(input.new_string));
|
|
800
|
+
if (name !== "MultiEdit" || !Array.isArray(input.edits)) return null;
|
|
801
|
+
const parts = input.edits.map((e) => editFromStrings(path, textOf(e?.old_string), textOf(e?.new_string))).filter((e) => e !== null);
|
|
802
|
+
if (parts.length === 0) return null;
|
|
803
|
+
return { path, added: parts.flatMap((p) => p.added), removed: parts.flatMap((p) => p.removed) };
|
|
674
804
|
}
|
|
675
805
|
function extractPathsFromText(text) {
|
|
676
806
|
const matches = text.match(PATH_TOKEN_RE) ?? [];
|
|
@@ -687,6 +817,7 @@ function extractFromAssistantContent(content) {
|
|
|
687
817
|
const textParts = [];
|
|
688
818
|
const filesTouched = [];
|
|
689
819
|
const editedLines = [];
|
|
820
|
+
const inputEdits = /* @__PURE__ */ new Map();
|
|
690
821
|
for (const block of content) {
|
|
691
822
|
if (block.type === "text" && block.text) {
|
|
692
823
|
textParts.push(block.text);
|
|
@@ -707,6 +838,10 @@ function extractFromAssistantContent(content) {
|
|
|
707
838
|
} else if (FILE_PATH_TOOLS.has(name) && typeof input.file_path === "string") {
|
|
708
839
|
textParts.push(`[tool_call: ${name}] ${input.file_path}`);
|
|
709
840
|
filesTouched.push({ path: input.file_path, source: "tool_path", tool: name });
|
|
841
|
+
if (CLAUDE_EDIT_TOOLS.has(name) && block.id) {
|
|
842
|
+
const edit = editFromClaudeInput(name, input);
|
|
843
|
+
if (edit) inputEdits.set(block.id, edit);
|
|
844
|
+
}
|
|
710
845
|
} else if (name === "Bash" && typeof input.command === "string") {
|
|
711
846
|
const desc = typeof input.description === "string" ? input.description : input.command;
|
|
712
847
|
textParts.push(`[tool_call: Bash] ${desc}`);
|
|
@@ -718,7 +853,7 @@ function extractFromAssistantContent(content) {
|
|
|
718
853
|
}
|
|
719
854
|
}
|
|
720
855
|
}
|
|
721
|
-
return { text: textParts.join("\n"), filesTouched, editedLines };
|
|
856
|
+
return { text: textParts.join("\n"), filesTouched, editedLines, inputEdits };
|
|
722
857
|
}
|
|
723
858
|
function extractFromUserContent(content) {
|
|
724
859
|
if (typeof content === "string") {
|
|
@@ -800,14 +935,23 @@ function readSubagentMeta(transcriptPath) {
|
|
|
800
935
|
const metaPath = transcriptPath.replace(/\.jsonl$/, ".meta.json");
|
|
801
936
|
try {
|
|
802
937
|
const raw = JSON.parse(readFileSync(metaPath, "utf-8"));
|
|
938
|
+
const text = (key) => typeof raw[key] === "string" && raw[key] ? raw[key] : null;
|
|
803
939
|
return {
|
|
804
|
-
agentType:
|
|
805
|
-
description:
|
|
940
|
+
agentType: text("agentType"),
|
|
941
|
+
description: text("description"),
|
|
942
|
+
parentAgentId: text("parentAgentId"),
|
|
943
|
+
// The upload's DTO takes an integer >= 0; one malformed sidecar must not
|
|
944
|
+
// 400 the whole batch it travels in.
|
|
945
|
+
spawnDepth: Number.isInteger(raw.spawnDepth) && raw.spawnDepth >= 0 ? raw.spawnDepth : null,
|
|
946
|
+
toolUseId: text("toolUseId")
|
|
806
947
|
};
|
|
807
948
|
} catch {
|
|
808
949
|
return null;
|
|
809
950
|
}
|
|
810
951
|
}
|
|
952
|
+
function subagentSessionId(rootSessionId, agentId) {
|
|
953
|
+
return deriveUuid(`evrex-subagent\0${rootSessionId}\0${agentId}`);
|
|
954
|
+
}
|
|
811
955
|
function parseSessionFile(filePath, repoPath, repoId = deriveRepoId(repoPath), agentKind = "claude-code") {
|
|
812
956
|
const { text: raw, capped } = readTranscript(filePath);
|
|
813
957
|
const lines = raw.split("\n").filter((l) => l.trim().length > 0);
|
|
@@ -820,6 +964,7 @@ function parseSessionFile(filePath, repoPath, repoId = deriveRepoId(repoPath), a
|
|
|
820
964
|
let branch = null;
|
|
821
965
|
let agentId = null;
|
|
822
966
|
const seenIds = /* @__PURE__ */ new Set();
|
|
967
|
+
const callEdits = /* @__PURE__ */ new Map();
|
|
823
968
|
for (const line of lines) {
|
|
824
969
|
let record;
|
|
825
970
|
try {
|
|
@@ -852,6 +997,7 @@ function parseSessionFile(filePath, repoPath, repoId = deriveRepoId(repoPath), a
|
|
|
852
997
|
text = extracted.text;
|
|
853
998
|
filesTouched = extracted.filesTouched;
|
|
854
999
|
editedLines = extracted.editedLines;
|
|
1000
|
+
for (const [toolUseId, edit] of extracted.inputEdits) callEdits.set(toolUseId, edit);
|
|
855
1001
|
}
|
|
856
1002
|
} else {
|
|
857
1003
|
const extracted = extractFromUserContent(record.message?.content);
|
|
@@ -861,6 +1007,13 @@ function parseSessionFile(filePath, repoPath, repoId = deriveRepoId(repoPath), a
|
|
|
861
1007
|
isToolError = extracted.isToolError ?? false;
|
|
862
1008
|
const edited = extractEditedLines(record.toolUseResult);
|
|
863
1009
|
if (edited) editedLines = [edited];
|
|
1010
|
+
const content = record.message?.content;
|
|
1011
|
+
for (const block of Array.isArray(content) ? content : []) {
|
|
1012
|
+
if (block.type !== "tool_result" || !block.tool_use_id) continue;
|
|
1013
|
+
const fromCall = callEdits.get(block.tool_use_id);
|
|
1014
|
+
callEdits.delete(block.tool_use_id);
|
|
1015
|
+
if (fromCall && !edited && block.is_error !== true) editedLines.push(fromCall);
|
|
1016
|
+
}
|
|
864
1017
|
}
|
|
865
1018
|
const redacted = redactSecrets(text);
|
|
866
1019
|
totalRedactions += redacted.count;
|
|
@@ -881,15 +1034,24 @@ function parseSessionFile(filePath, repoPath, repoId = deriveRepoId(repoPath), a
|
|
|
881
1034
|
});
|
|
882
1035
|
}
|
|
883
1036
|
if (!sessionId || turns.length === 0) return null;
|
|
884
|
-
const
|
|
885
|
-
|
|
886
|
-
|
|
1037
|
+
const rootSessionId = agentId ? sessionId : null;
|
|
1038
|
+
const meta = agentId ? readSubagentMeta(filePath) : null;
|
|
1039
|
+
const parentSessionId = agentId && rootSessionId ? meta?.parentAgentId ? subagentSessionId(rootSessionId, meta.parentAgentId) : rootSessionId : null;
|
|
1040
|
+
if (agentId && rootSessionId) {
|
|
1041
|
+
sessionId = subagentSessionId(rootSessionId, agentId);
|
|
887
1042
|
for (const t of turns) t.sessionId = sessionId;
|
|
888
1043
|
}
|
|
889
|
-
const
|
|
890
|
-
|
|
1044
|
+
const subagent = agentId ? {
|
|
1045
|
+
agentId,
|
|
1046
|
+
agentType: meta?.agentType ?? null,
|
|
1047
|
+
description: meta?.description ?? null,
|
|
1048
|
+
rootSessionId,
|
|
1049
|
+
spawnDepth: meta?.spawnDepth ?? null,
|
|
1050
|
+
toolUseId: meta?.toolUseId ?? null
|
|
1051
|
+
} : null;
|
|
891
1052
|
if (agentId && !aiTitle && meta?.description) aiTitle = meta.description;
|
|
892
1053
|
const sortedTs = turns.map((t) => t.ts).sort();
|
|
1054
|
+
const commitEvidence = collectCommitEvidence(lines);
|
|
893
1055
|
const rawContent = capped ? "" : lines.map((line) => {
|
|
894
1056
|
try {
|
|
895
1057
|
return JSON.stringify(redactJsonValue(JSON.parse(line)).value);
|
|
@@ -913,7 +1075,9 @@ function parseSessionFile(filePath, repoPath, repoId = deriveRepoId(repoPath), a
|
|
|
913
1075
|
// filled in by the caller — see getGitUserName in git-history.ts
|
|
914
1076
|
sourceFile: filePath,
|
|
915
1077
|
redactionCount: totalRedactions,
|
|
916
|
-
committedSubjects:
|
|
1078
|
+
committedSubjects: commitEvidence.subjects,
|
|
1079
|
+
committedShas: commitEvidence.shas,
|
|
1080
|
+
mergedPrNumbers: commitEvidence.prNumbers,
|
|
917
1081
|
branch,
|
|
918
1082
|
parentSessionId,
|
|
919
1083
|
subagent,
|
|
@@ -922,10 +1086,14 @@ function parseSessionFile(filePath, repoPath, repoId = deriveRepoId(repoPath), a
|
|
|
922
1086
|
turns
|
|
923
1087
|
};
|
|
924
1088
|
}
|
|
925
|
-
function
|
|
1089
|
+
function collectCommitEvidence(lines) {
|
|
926
1090
|
const subjects = /* @__PURE__ */ new Set();
|
|
1091
|
+
const shas = /* @__PURE__ */ new Set();
|
|
1092
|
+
const prNumbers = /* @__PURE__ */ new Set();
|
|
1093
|
+
const commitCallIds = /* @__PURE__ */ new Set();
|
|
1094
|
+
const mergeCallIds = /* @__PURE__ */ new Map();
|
|
927
1095
|
for (const line of lines) {
|
|
928
|
-
if (!line.includes("git commit")) continue;
|
|
1096
|
+
if (!line.includes("git commit") && !line.includes("/merge") && !line.includes("tool_use_id")) continue;
|
|
929
1097
|
let record;
|
|
930
1098
|
try {
|
|
931
1099
|
record = JSON.parse(line);
|
|
@@ -935,13 +1103,40 @@ function collectCommittedSubjects(lines) {
|
|
|
935
1103
|
const content = record?.message?.content;
|
|
936
1104
|
if (!Array.isArray(content)) continue;
|
|
937
1105
|
for (const block of content) {
|
|
938
|
-
if (block?.type
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
1106
|
+
if (block?.type === "tool_use") {
|
|
1107
|
+
const command = block?.input?.command;
|
|
1108
|
+
if (typeof command !== "string") continue;
|
|
1109
|
+
const found = committedSubjects(command);
|
|
1110
|
+
for (const subject of found) subjects.add(subject);
|
|
1111
|
+
if (/\bgit\s+(?:-C\s+\S+\s+)?commit\b/.test(command) && typeof block?.id === "string") {
|
|
1112
|
+
commitCallIds.add(block.id);
|
|
1113
|
+
}
|
|
1114
|
+
if ((/\/pulls\/\d+\/merge\b/.test(command) || /\bgh\s+pr\s+merge\b/.test(command)) && typeof block?.id === "string") {
|
|
1115
|
+
mergeCallIds.set(block.id, mergedPrFromCommand(command));
|
|
1116
|
+
}
|
|
1117
|
+
continue;
|
|
1118
|
+
}
|
|
1119
|
+
if (block?.type === "tool_result" && mergeCallIds.has(block?.tool_use_id)) {
|
|
1120
|
+
const raw = block?.content;
|
|
1121
|
+
const text = typeof raw === "string" ? raw : Array.isArray(raw) ? raw.map(
|
|
1122
|
+
(x) => typeof x?.text === "string" ? x.text : ""
|
|
1123
|
+
).join(" ") : "";
|
|
1124
|
+
for (const sha of mergeResponseShas(text)) shas.add(sha);
|
|
1125
|
+
if (mergeSucceeded(text)) {
|
|
1126
|
+
const pr = mergeCallIds.get(block?.tool_use_id);
|
|
1127
|
+
if (typeof pr === "number") prNumbers.add(pr);
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
if (block?.type === "tool_result" && commitCallIds.has(block?.tool_use_id)) {
|
|
1131
|
+
const raw = block?.content;
|
|
1132
|
+
const text = typeof raw === "string" ? raw : Array.isArray(raw) ? raw.map(
|
|
1133
|
+
(x) => typeof x?.text === "string" ? x.text : ""
|
|
1134
|
+
).join(" ") : "";
|
|
1135
|
+
for (const sha of commitShaOutputs(text)) shas.add(sha);
|
|
1136
|
+
}
|
|
942
1137
|
}
|
|
943
1138
|
}
|
|
944
|
-
return [...subjects];
|
|
1139
|
+
return { subjects: [...subjects], shas: [...shas], prNumbers: [...prNumbers] };
|
|
945
1140
|
}
|
|
946
1141
|
function parseAllSessions(repoPath, repoId, cursors = {}) {
|
|
947
1142
|
const id = repoId ?? deriveRepoId(repoPath);
|
|
@@ -961,7 +1156,7 @@ function parseAllSessions(repoPath, repoId, cursors = {}) {
|
|
|
961
1156
|
}
|
|
962
1157
|
return { sessions, cursors: next, skipped };
|
|
963
1158
|
}
|
|
964
|
-
var MIN_MEANINGFUL_LINE_LENGTH, MAX_TURN_TEXT_LENGTH, FILE_PATH_TOOLS, XCODE_EDIT_TOOL, XCODE_PATH_TOOLS, PATH_TOKEN_RE, SYNTHETIC_CONTENT_RE, MAX_TRANSCRIPT_BYTES;
|
|
1159
|
+
var MIN_MEANINGFUL_LINE_LENGTH, MAX_TURN_TEXT_LENGTH, FILE_PATH_TOOLS, XCODE_EDIT_TOOL, XCODE_PATH_TOOLS, CLAUDE_EDIT_TOOLS, PATH_TOKEN_RE, SYNTHETIC_CONTENT_RE, MAX_TRANSCRIPT_BYTES;
|
|
965
1160
|
var init_claude_sessions = __esm({
|
|
966
1161
|
"../../packages/ingest-core/src/claude-sessions.ts"() {
|
|
967
1162
|
"use strict";
|
|
@@ -973,9 +1168,10 @@ var init_claude_sessions = __esm({
|
|
|
973
1168
|
init_types();
|
|
974
1169
|
MIN_MEANINGFUL_LINE_LENGTH = 6;
|
|
975
1170
|
MAX_TURN_TEXT_LENGTH = 4e3;
|
|
976
|
-
FILE_PATH_TOOLS = /* @__PURE__ */ new Set(["Read", "Edit", "Write", "NotebookEdit"]);
|
|
1171
|
+
FILE_PATH_TOOLS = /* @__PURE__ */ new Set(["Read", "Edit", "Write", "MultiEdit", "NotebookEdit"]);
|
|
977
1172
|
XCODE_EDIT_TOOL = /XcodeUpdate$/;
|
|
978
1173
|
XCODE_PATH_TOOLS = /Xcode(Read|Grep|Glob|RefreshCodeIssuesInFile)$/;
|
|
1174
|
+
CLAUDE_EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit"]);
|
|
979
1175
|
PATH_TOKEN_RE = /(?:[.~]?\/)?(?:[\w-]+\/)+[\w.-]+/g;
|
|
980
1176
|
SYNTHETIC_CONTENT_RE = /^\s*(<task-notification>|<system-reminder>|\[SYSTEM NOTIFICATION)/;
|
|
981
1177
|
MAX_TRANSCRIPT_BYTES = 256 * 1024 * 1024;
|
|
@@ -1907,16 +2103,58 @@ function addUsage(into, next) {
|
|
|
1907
2103
|
model: into.model ?? next.model
|
|
1908
2104
|
};
|
|
1909
2105
|
}
|
|
1910
|
-
function
|
|
2106
|
+
function commitEvidenceFromToolCalls(lines) {
|
|
1911
2107
|
const subjects = /* @__PURE__ */ new Set();
|
|
2108
|
+
const shas = /* @__PURE__ */ new Set();
|
|
2109
|
+
const prNumbers = /* @__PURE__ */ new Set();
|
|
2110
|
+
const commitCallIds = /* @__PURE__ */ new Set();
|
|
2111
|
+
const mergeCallIds = /* @__PURE__ */ new Map();
|
|
1912
2112
|
for (const line of lines) {
|
|
1913
2113
|
if (line.type !== "response_item") continue;
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
2114
|
+
const kind = payloadType(line);
|
|
2115
|
+
if (kind === "custom_tool_call") {
|
|
2116
|
+
const input = asString(line.payload?.input);
|
|
2117
|
+
if (!input) continue;
|
|
2118
|
+
const cmd = execCmdFrom(input) ?? input;
|
|
2119
|
+
const callId = asString(line.payload?.call_id);
|
|
2120
|
+
for (const subject of committedSubjects(cmd)) subjects.add(subject);
|
|
2121
|
+
if (/\bgit\s+(?:-C\s+\S+\s+)?commit\b/.test(cmd) && callId) {
|
|
2122
|
+
commitCallIds.add(callId);
|
|
2123
|
+
}
|
|
2124
|
+
if ((/\/pulls\/\d+\/merge\b/.test(cmd) || /\bgh\s+pr\s+merge\b/.test(cmd)) && callId) {
|
|
2125
|
+
mergeCallIds.set(callId, mergedPrFromCommand(cmd));
|
|
2126
|
+
}
|
|
2127
|
+
continue;
|
|
2128
|
+
}
|
|
2129
|
+
if (kind === "custom_tool_call_output") {
|
|
2130
|
+
const callId = asString(line.payload?.call_id);
|
|
2131
|
+
if (!callId) continue;
|
|
2132
|
+
const raw = line.payload?.output;
|
|
2133
|
+
const text = Array.isArray(raw) ? raw.map(
|
|
2134
|
+
(x) => typeof x?.text === "string" ? x.text : ""
|
|
2135
|
+
).join(" ") : asString(raw) ?? "";
|
|
2136
|
+
if (commitCallIds.has(callId)) {
|
|
2137
|
+
for (const sha of commitShaOutputs(text)) shas.add(sha);
|
|
2138
|
+
}
|
|
2139
|
+
if (mergeCallIds.has(callId)) {
|
|
2140
|
+
for (const sha of mergeResponseShas(text)) shas.add(sha);
|
|
2141
|
+
if (mergeSucceeded(text)) {
|
|
2142
|
+
const pr = mergeCallIds.get(callId);
|
|
2143
|
+
if (typeof pr === "number") prNumbers.add(pr);
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
return { subjects: [...subjects], shas: [...shas], prNumbers: [...prNumbers] };
|
|
2149
|
+
}
|
|
2150
|
+
function execCmdFrom(input) {
|
|
2151
|
+
const m = /cmd\s*:\s*"((?:[^"\\]|\\.)*)"/.exec(input);
|
|
2152
|
+
if (!m) return null;
|
|
2153
|
+
try {
|
|
2154
|
+
return JSON.parse(`"${m[1]}"`);
|
|
2155
|
+
} catch {
|
|
2156
|
+
return null;
|
|
1918
2157
|
}
|
|
1919
|
-
return [...subjects];
|
|
1920
2158
|
}
|
|
1921
2159
|
function filesFromToolCalls(lines) {
|
|
1922
2160
|
const byPath = /* @__PURE__ */ new Map();
|
|
@@ -1975,20 +2213,28 @@ function parseCodexSessionFile(filePath, repoPath, repoId) {
|
|
|
1975
2213
|
const { content: rawContent, count: rawRedactions } = redactRollout(raw);
|
|
1976
2214
|
redactionCount += rawRedactions;
|
|
1977
2215
|
const firstUser = events.find((e) => e.role === "user");
|
|
2216
|
+
const codexCommitEvidence = commitEvidenceFromToolCalls(lines);
|
|
1978
2217
|
return {
|
|
1979
2218
|
id: sessionId,
|
|
1980
2219
|
agentKind: "codex",
|
|
1981
2220
|
branch: null,
|
|
1982
2221
|
parentSessionId: null,
|
|
1983
2222
|
subagent: null,
|
|
1984
|
-
//
|
|
1985
|
-
//
|
|
1986
|
-
|
|
2223
|
+
// The caller's derivation first: it reads the checkout's remote NOW,
|
|
2224
|
+
// which is the identity every other ingest path agrees on. The remote
|
|
2225
|
+
// recorded in session_meta is a historical fact — after the repo's
|
|
2226
|
+
// GitHub transfer, month-old rollouts kept re-minting the dead
|
|
2227
|
+
// jeffcheema id on every re-index, resurrecting a "third evrex-app"
|
|
2228
|
+
// as fast as migrations folded it. Meta only serves a rollout whose
|
|
2229
|
+
// checkout is gone, where a stale identity beats none.
|
|
2230
|
+
repoId: repoId.startsWith("path:") ? repoIdFromMeta(meta) ?? repoId : repoId,
|
|
1987
2231
|
cwd,
|
|
1988
2232
|
startedAt: asString(meta.timestamp) ?? events[0]?.ts ?? null,
|
|
1989
2233
|
endedAt: events[events.length - 1]?.ts ?? null,
|
|
1990
2234
|
turnCount: turns.length,
|
|
1991
|
-
committedSubjects:
|
|
2235
|
+
committedSubjects: codexCommitEvidence.subjects,
|
|
2236
|
+
committedShas: codexCommitEvidence.shas,
|
|
2237
|
+
mergedPrNumbers: codexCommitEvidence.prNumbers,
|
|
1992
2238
|
aiTitle: firstUser ? firstUser.text.slice(0, 120).trim() : null,
|
|
1993
2239
|
author: null,
|
|
1994
2240
|
sourceFile: filePath,
|
|
@@ -2504,7 +2750,7 @@ var init_trace_target = __esm({
|
|
|
2504
2750
|
|
|
2505
2751
|
// ../../packages/ingest-core/src/gemini-sessions.ts
|
|
2506
2752
|
import { readFileSync as readFileSync4 } from "node:fs";
|
|
2507
|
-
function
|
|
2753
|
+
function textOf2(content) {
|
|
2508
2754
|
if (typeof content === "string") return content;
|
|
2509
2755
|
if (!Array.isArray(content)) return "";
|
|
2510
2756
|
return content.map(
|
|
@@ -2538,7 +2784,7 @@ function parseGeminiSessionFile(filePath, repoPath, repoId) {
|
|
|
2538
2784
|
messages.forEach((record, index) => {
|
|
2539
2785
|
const role = record.type === "user" ? "user" : record.type === "gemini" || record.type === "model" ? "assistant" : null;
|
|
2540
2786
|
if (!role) return;
|
|
2541
|
-
const text =
|
|
2787
|
+
const text = textOf2(record.content ?? record.displayContent);
|
|
2542
2788
|
if (!text.trim()) return;
|
|
2543
2789
|
const { text: safe, count } = redactSecrets(text);
|
|
2544
2790
|
redactionCount += count;
|
|
@@ -2962,6 +3208,7 @@ __export(src_exports, {
|
|
|
2962
3208
|
changedSessions: () => changedSessions,
|
|
2963
3209
|
codexSessionsDir: () => codexSessionsDir,
|
|
2964
3210
|
collectRepoData: () => collectRepoData,
|
|
3211
|
+
commitShaOutputs: () => commitShaOutputs,
|
|
2965
3212
|
commitsOlderThan: () => commitsOlderThan,
|
|
2966
3213
|
committedSubjects: () => committedSubjects,
|
|
2967
3214
|
copilotSessionsDir: () => copilotSessionsDir,
|
|
@@ -2996,6 +3243,9 @@ __export(src_exports, {
|
|
|
2996
3243
|
loadComposers: () => loadComposers,
|
|
2997
3244
|
matchCommitToSession: () => matchCommitToSession,
|
|
2998
3245
|
measureLineSurvival: () => measureLineSurvival,
|
|
3246
|
+
mergeResponseShas: () => mergeResponseShas,
|
|
3247
|
+
mergeSucceeded: () => mergeSucceeded,
|
|
3248
|
+
mergedPrFromCommand: () => mergedPrFromCommand,
|
|
2999
3249
|
normalizeRepoRemote: () => normalizeRepoRemote,
|
|
3000
3250
|
opencodeDbPath: () => opencodeDbPath,
|
|
3001
3251
|
originOf: () => originOf,
|
|
@@ -3005,6 +3255,7 @@ __export(src_exports, {
|
|
|
3005
3255
|
parseAllOpenCodeSessions: () => parseAllOpenCodeSessions,
|
|
3006
3256
|
parseAllSessions: () => parseAllSessions,
|
|
3007
3257
|
parseCodexSessionFile: () => parseCodexSessionFile,
|
|
3258
|
+
parseCommitsBySha: () => parseCommitsBySha,
|
|
3008
3259
|
parseCopilotSessionDir: () => parseCopilotSessionDir,
|
|
3009
3260
|
parseCopilotWorkspace: () => parseCopilotWorkspace,
|
|
3010
3261
|
parseCursorComposer: () => parseCursorComposer,
|
|
@@ -3171,7 +3422,7 @@ var init_client = __esm({
|
|
|
3171
3422
|
// Evidence-only retrieval (BM25 + embedding), no LLM synthesis call — see
|
|
3172
3423
|
// apps/backend/src/query/query.service.ts#search. Used by evrex_search,
|
|
3173
3424
|
// which wants ranked hits fast, not a synthesized paragraph.
|
|
3174
|
-
search: (repoPath, text, filePaths) => post("/search", { repoPath, text, filePaths }),
|
|
3425
|
+
search: (repoPath, text, filePaths, boost) => post("/search", { repoPath, text, filePaths, boost }),
|
|
3175
3426
|
// Everything that happened in a repo, newest first, bounded by days — the
|
|
3176
3427
|
// same query the desktop Timeline screen makes. Sessions and commits
|
|
3177
3428
|
// interleaved, each with the handle evrex_expand takes.
|