evrex-mcp 0.8.2 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/capture.js +257 -17
- package/dist/continuity.js +31 -3
- package/dist/hook.js +28 -1
- package/dist/import.js +69 -19
- package/dist/index.js +57 -6
- package/dist/pretool.js +28 -1
- package/dist/tickets.js +5 -3
- package/package.json +15 -16
package/dist/capture.js
CHANGED
|
@@ -432,7 +432,7 @@ var PARSER_VERSION;
|
|
|
432
432
|
var init_incremental = __esm({
|
|
433
433
|
"../../packages/ingest-core/src/incremental.ts"() {
|
|
434
434
|
"use strict";
|
|
435
|
-
PARSER_VERSION =
|
|
435
|
+
PARSER_VERSION = 9;
|
|
436
436
|
}
|
|
437
437
|
});
|
|
438
438
|
|
|
@@ -777,14 +777,30 @@ function findSessionFiles(repoPath) {
|
|
|
777
777
|
function editFromXcodeInput(input) {
|
|
778
778
|
const filePath = input.filePath;
|
|
779
779
|
if (typeof filePath !== "string" || !filePath) return null;
|
|
780
|
-
|
|
781
|
-
|
|
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") : [];
|
|
782
788
|
const oldSet = new Set(oldLines);
|
|
783
789
|
const newSet = new Set(newLines);
|
|
784
790
|
const added = meaningfulLines(newLines.filter((l) => !oldSet.has(l)));
|
|
785
791
|
const removed = meaningfulLines(oldLines.filter((l) => !newSet.has(l)));
|
|
786
792
|
if (added.length === 0 && removed.length === 0) return null;
|
|
787
|
-
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) };
|
|
788
804
|
}
|
|
789
805
|
function extractPathsFromText(text) {
|
|
790
806
|
const matches = text.match(PATH_TOKEN_RE) ?? [];
|
|
@@ -801,6 +817,7 @@ function extractFromAssistantContent(content) {
|
|
|
801
817
|
const textParts = [];
|
|
802
818
|
const filesTouched = [];
|
|
803
819
|
const editedLines = [];
|
|
820
|
+
const inputEdits = /* @__PURE__ */ new Map();
|
|
804
821
|
for (const block of content) {
|
|
805
822
|
if (block.type === "text" && block.text) {
|
|
806
823
|
textParts.push(block.text);
|
|
@@ -821,6 +838,10 @@ function extractFromAssistantContent(content) {
|
|
|
821
838
|
} else if (FILE_PATH_TOOLS.has(name) && typeof input.file_path === "string") {
|
|
822
839
|
textParts.push(`[tool_call: ${name}] ${input.file_path}`);
|
|
823
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
|
+
}
|
|
824
845
|
} else if (name === "Bash" && typeof input.command === "string") {
|
|
825
846
|
const desc = typeof input.description === "string" ? input.description : input.command;
|
|
826
847
|
textParts.push(`[tool_call: Bash] ${desc}`);
|
|
@@ -832,7 +853,7 @@ function extractFromAssistantContent(content) {
|
|
|
832
853
|
}
|
|
833
854
|
}
|
|
834
855
|
}
|
|
835
|
-
return { text: textParts.join("\n"), filesTouched, editedLines };
|
|
856
|
+
return { text: textParts.join("\n"), filesTouched, editedLines, inputEdits };
|
|
836
857
|
}
|
|
837
858
|
function extractFromUserContent(content) {
|
|
838
859
|
if (typeof content === "string") {
|
|
@@ -914,14 +935,23 @@ function readSubagentMeta(transcriptPath) {
|
|
|
914
935
|
const metaPath = transcriptPath.replace(/\.jsonl$/, ".meta.json");
|
|
915
936
|
try {
|
|
916
937
|
const raw = JSON.parse(readFileSync(metaPath, "utf-8"));
|
|
938
|
+
const text = (key) => typeof raw[key] === "string" && raw[key] ? raw[key] : null;
|
|
917
939
|
return {
|
|
918
|
-
agentType:
|
|
919
|
-
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")
|
|
920
947
|
};
|
|
921
948
|
} catch {
|
|
922
949
|
return null;
|
|
923
950
|
}
|
|
924
951
|
}
|
|
952
|
+
function subagentSessionId(rootSessionId, agentId) {
|
|
953
|
+
return deriveUuid(`evrex-subagent\0${rootSessionId}\0${agentId}`);
|
|
954
|
+
}
|
|
925
955
|
function parseSessionFile(filePath, repoPath, repoId = deriveRepoId(repoPath), agentKind = "claude-code") {
|
|
926
956
|
const { text: raw, capped } = readTranscript(filePath);
|
|
927
957
|
const lines = raw.split("\n").filter((l) => l.trim().length > 0);
|
|
@@ -934,6 +964,7 @@ function parseSessionFile(filePath, repoPath, repoId = deriveRepoId(repoPath), a
|
|
|
934
964
|
let branch = null;
|
|
935
965
|
let agentId = null;
|
|
936
966
|
const seenIds = /* @__PURE__ */ new Set();
|
|
967
|
+
const callEdits = /* @__PURE__ */ new Map();
|
|
937
968
|
for (const line of lines) {
|
|
938
969
|
let record;
|
|
939
970
|
try {
|
|
@@ -966,6 +997,7 @@ function parseSessionFile(filePath, repoPath, repoId = deriveRepoId(repoPath), a
|
|
|
966
997
|
text = extracted.text;
|
|
967
998
|
filesTouched = extracted.filesTouched;
|
|
968
999
|
editedLines = extracted.editedLines;
|
|
1000
|
+
for (const [toolUseId, edit] of extracted.inputEdits) callEdits.set(toolUseId, edit);
|
|
969
1001
|
}
|
|
970
1002
|
} else {
|
|
971
1003
|
const extracted = extractFromUserContent(record.message?.content);
|
|
@@ -975,6 +1007,13 @@ function parseSessionFile(filePath, repoPath, repoId = deriveRepoId(repoPath), a
|
|
|
975
1007
|
isToolError = extracted.isToolError ?? false;
|
|
976
1008
|
const edited = extractEditedLines(record.toolUseResult);
|
|
977
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
|
+
}
|
|
978
1017
|
}
|
|
979
1018
|
const redacted = redactSecrets(text);
|
|
980
1019
|
totalRedactions += redacted.count;
|
|
@@ -995,13 +1034,21 @@ function parseSessionFile(filePath, repoPath, repoId = deriveRepoId(repoPath), a
|
|
|
995
1034
|
});
|
|
996
1035
|
}
|
|
997
1036
|
if (!sessionId || turns.length === 0) return null;
|
|
998
|
-
const
|
|
999
|
-
|
|
1000
|
-
|
|
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);
|
|
1001
1042
|
for (const t of turns) t.sessionId = sessionId;
|
|
1002
1043
|
}
|
|
1003
|
-
const
|
|
1004
|
-
|
|
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;
|
|
1005
1052
|
if (agentId && !aiTitle && meta?.description) aiTitle = meta.description;
|
|
1006
1053
|
const sortedTs = turns.map((t) => t.ts).sort();
|
|
1007
1054
|
const commitEvidence = collectCommitEvidence(lines);
|
|
@@ -1109,7 +1156,7 @@ function parseAllSessions(repoPath, repoId, cursors = {}) {
|
|
|
1109
1156
|
}
|
|
1110
1157
|
return { sessions, cursors: next, skipped };
|
|
1111
1158
|
}
|
|
1112
|
-
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;
|
|
1113
1160
|
var init_claude_sessions = __esm({
|
|
1114
1161
|
"../../packages/ingest-core/src/claude-sessions.ts"() {
|
|
1115
1162
|
"use strict";
|
|
@@ -1121,9 +1168,10 @@ var init_claude_sessions = __esm({
|
|
|
1121
1168
|
init_types();
|
|
1122
1169
|
MIN_MEANINGFUL_LINE_LENGTH = 6;
|
|
1123
1170
|
MAX_TURN_TEXT_LENGTH = 4e3;
|
|
1124
|
-
FILE_PATH_TOOLS = /* @__PURE__ */ new Set(["Read", "Edit", "Write", "NotebookEdit"]);
|
|
1171
|
+
FILE_PATH_TOOLS = /* @__PURE__ */ new Set(["Read", "Edit", "Write", "MultiEdit", "NotebookEdit"]);
|
|
1125
1172
|
XCODE_EDIT_TOOL = /XcodeUpdate$/;
|
|
1126
1173
|
XCODE_PATH_TOOLS = /Xcode(Read|Grep|Glob|RefreshCodeIssuesInFile)$/;
|
|
1174
|
+
CLAUDE_EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit"]);
|
|
1127
1175
|
PATH_TOKEN_RE = /(?:[.~]?\/)?(?:[\w-]+\/)+[\w.-]+/g;
|
|
1128
1176
|
SYNTHETIC_CONTENT_RE = /^\s*(<task-notification>|<system-reminder>|\[SYSTEM NOTIFICATION)/;
|
|
1129
1177
|
MAX_TRANSCRIPT_BYTES = 256 * 1024 * 1024;
|
|
@@ -2702,7 +2750,7 @@ var init_trace_target = __esm({
|
|
|
2702
2750
|
|
|
2703
2751
|
// ../../packages/ingest-core/src/gemini-sessions.ts
|
|
2704
2752
|
import { readFileSync as readFileSync4 } from "node:fs";
|
|
2705
|
-
function
|
|
2753
|
+
function textOf2(content) {
|
|
2706
2754
|
if (typeof content === "string") return content;
|
|
2707
2755
|
if (!Array.isArray(content)) return "";
|
|
2708
2756
|
return content.map(
|
|
@@ -2736,7 +2784,7 @@ function parseGeminiSessionFile(filePath, repoPath, repoId) {
|
|
|
2736
2784
|
messages.forEach((record, index) => {
|
|
2737
2785
|
const role = record.type === "user" ? "user" : record.type === "gemini" || record.type === "model" ? "assistant" : null;
|
|
2738
2786
|
if (!role) return;
|
|
2739
|
-
const text =
|
|
2787
|
+
const text = textOf2(record.content ?? record.displayContent);
|
|
2740
2788
|
if (!text.trim()) return;
|
|
2741
2789
|
const { text: safe, count } = redactSecrets(text);
|
|
2742
2790
|
redactionCount += count;
|
|
@@ -3386,6 +3434,186 @@ var init_client = __esm({
|
|
|
3386
3434
|
}
|
|
3387
3435
|
});
|
|
3388
3436
|
|
|
3437
|
+
// src/credential-store.ts
|
|
3438
|
+
var credential_store_exports = {};
|
|
3439
|
+
__export(credential_store_exports, {
|
|
3440
|
+
CredentialStore: () => CredentialStore,
|
|
3441
|
+
NoKeychainError: () => NoKeychainError,
|
|
3442
|
+
systemRunner: () => systemRunner
|
|
3443
|
+
});
|
|
3444
|
+
import { spawn } from "node:child_process";
|
|
3445
|
+
var SERVICE, ACCOUNT, systemRunner, NoKeychainError, CredentialStore, WINDOWS_PATH, WINDOWS_STORE, WINDOWS_RETRIEVE, WINDOWS_REMOVE;
|
|
3446
|
+
var init_credential_store = __esm({
|
|
3447
|
+
"src/credential-store.ts"() {
|
|
3448
|
+
"use strict";
|
|
3449
|
+
SERVICE = "evrex-capture";
|
|
3450
|
+
ACCOUNT = "evrex";
|
|
3451
|
+
systemRunner = {
|
|
3452
|
+
platform: process.platform,
|
|
3453
|
+
run(command, args, stdin) {
|
|
3454
|
+
return new Promise((resolve) => {
|
|
3455
|
+
const child = spawn(command, args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
3456
|
+
let stdout = "";
|
|
3457
|
+
let stderr = "";
|
|
3458
|
+
child.stdout.on("data", (d) => stdout += d.toString());
|
|
3459
|
+
child.stderr.on("data", (d) => stderr += d.toString());
|
|
3460
|
+
child.on("error", () => resolve({ code: 127, stdout: "", stderr: "" }));
|
|
3461
|
+
child.on("close", (code) => resolve({ code: code ?? 1, stdout, stderr }));
|
|
3462
|
+
if (stdin !== void 0) child.stdin.write(stdin);
|
|
3463
|
+
child.stdin.end();
|
|
3464
|
+
});
|
|
3465
|
+
}
|
|
3466
|
+
};
|
|
3467
|
+
NoKeychainError = class extends Error {
|
|
3468
|
+
constructor(platform) {
|
|
3469
|
+
super(
|
|
3470
|
+
`evrex could not find a credential store on this machine (${platform}).
|
|
3471
|
+
macOS needs \`security\`, which ships with the system.
|
|
3472
|
+
Linux needs \`secret-tool\` \u2014 install libsecret-tools (Debian/Ubuntu)
|
|
3473
|
+
or libsecret (Fedora/Arch), and make sure a keyring daemon is running.
|
|
3474
|
+
Windows needs PowerShell.
|
|
3475
|
+
evrex will not fall back to writing the credential in a plain file.`
|
|
3476
|
+
);
|
|
3477
|
+
this.name = "NoKeychainError";
|
|
3478
|
+
}
|
|
3479
|
+
};
|
|
3480
|
+
CredentialStore = class {
|
|
3481
|
+
constructor(runner = systemRunner) {
|
|
3482
|
+
this.runner = runner;
|
|
3483
|
+
}
|
|
3484
|
+
async available() {
|
|
3485
|
+
switch (this.runner.platform) {
|
|
3486
|
+
case "darwin":
|
|
3487
|
+
return (await this.runner.run("security", ["help"])).code !== 127;
|
|
3488
|
+
case "win32":
|
|
3489
|
+
return (await this.runner.run("powershell", ["-Command", "$PSVersionTable.PSVersion.Major"])).code !== 127;
|
|
3490
|
+
default:
|
|
3491
|
+
return (await this.runner.run("secret-tool", ["--version"])).code !== 127;
|
|
3492
|
+
}
|
|
3493
|
+
}
|
|
3494
|
+
/**
|
|
3495
|
+
* Replaces any existing credential rather than adding a second one. A
|
|
3496
|
+
* machine that re-enrols after expiry must end up with exactly one entry, or
|
|
3497
|
+
* the next read is a coin flip between the live credential and a dead one.
|
|
3498
|
+
*/
|
|
3499
|
+
async store(secret) {
|
|
3500
|
+
if (!await this.available()) throw new NoKeychainError(this.runner.platform);
|
|
3501
|
+
switch (this.runner.platform) {
|
|
3502
|
+
case "darwin": {
|
|
3503
|
+
if (/["\\\r\n]/.test(secret)) {
|
|
3504
|
+
throw new Error("Keychain write refused: the credential has a character `security -i` cannot carry.");
|
|
3505
|
+
}
|
|
3506
|
+
const result = await this.runner.run(
|
|
3507
|
+
"security",
|
|
3508
|
+
["-i"],
|
|
3509
|
+
`add-generic-password -a "${ACCOUNT}" -s "${SERVICE}" -U -w "${secret}"
|
|
3510
|
+
`
|
|
3511
|
+
);
|
|
3512
|
+
if (result.code !== 0) throw new Error(`Keychain write failed: ${result.stderr.trim()}`);
|
|
3513
|
+
return;
|
|
3514
|
+
}
|
|
3515
|
+
case "win32": {
|
|
3516
|
+
const result = await this.runner.run(
|
|
3517
|
+
"powershell",
|
|
3518
|
+
["-NoProfile", "-Command", WINDOWS_STORE],
|
|
3519
|
+
secret
|
|
3520
|
+
);
|
|
3521
|
+
if (result.code !== 0) throw new Error(`DPAPI write failed: ${result.stderr.trim()}`);
|
|
3522
|
+
return;
|
|
3523
|
+
}
|
|
3524
|
+
default: {
|
|
3525
|
+
const result = await this.runner.run(
|
|
3526
|
+
"secret-tool",
|
|
3527
|
+
["store", "--label=evrex capture credential", "service", SERVICE, "account", ACCOUNT],
|
|
3528
|
+
secret
|
|
3529
|
+
);
|
|
3530
|
+
if (result.code !== 0) throw new Error(`secret-tool write failed: ${result.stderr.trim()}`);
|
|
3531
|
+
return;
|
|
3532
|
+
}
|
|
3533
|
+
}
|
|
3534
|
+
}
|
|
3535
|
+
/** Null when there is nothing stored, which is the normal pre-enrolment state. */
|
|
3536
|
+
async retrieve() {
|
|
3537
|
+
if (!await this.available()) throw new NoKeychainError(this.runner.platform);
|
|
3538
|
+
switch (this.runner.platform) {
|
|
3539
|
+
case "darwin": {
|
|
3540
|
+
const r = await this.runner.run("security", [
|
|
3541
|
+
"find-generic-password",
|
|
3542
|
+
"-a",
|
|
3543
|
+
ACCOUNT,
|
|
3544
|
+
"-s",
|
|
3545
|
+
SERVICE,
|
|
3546
|
+
"-w"
|
|
3547
|
+
]);
|
|
3548
|
+
return r.code === 0 ? r.stdout.trim() || null : null;
|
|
3549
|
+
}
|
|
3550
|
+
case "win32": {
|
|
3551
|
+
const r = await this.runner.run("powershell", ["-NoProfile", "-Command", WINDOWS_RETRIEVE]);
|
|
3552
|
+
return r.code === 0 ? r.stdout.trim() || null : null;
|
|
3553
|
+
}
|
|
3554
|
+
default: {
|
|
3555
|
+
const r = await this.runner.run("secret-tool", [
|
|
3556
|
+
"lookup",
|
|
3557
|
+
"service",
|
|
3558
|
+
SERVICE,
|
|
3559
|
+
"account",
|
|
3560
|
+
ACCOUNT
|
|
3561
|
+
]);
|
|
3562
|
+
return r.code === 0 ? r.stdout.trim() || null : null;
|
|
3563
|
+
}
|
|
3564
|
+
}
|
|
3565
|
+
}
|
|
3566
|
+
/** Idempotent: removing a credential that is not there is not an error. */
|
|
3567
|
+
async remove() {
|
|
3568
|
+
if (!await this.available()) return;
|
|
3569
|
+
switch (this.runner.platform) {
|
|
3570
|
+
case "darwin":
|
|
3571
|
+
await this.runner.run("security", [
|
|
3572
|
+
"delete-generic-password",
|
|
3573
|
+
"-a",
|
|
3574
|
+
ACCOUNT,
|
|
3575
|
+
"-s",
|
|
3576
|
+
SERVICE
|
|
3577
|
+
]);
|
|
3578
|
+
return;
|
|
3579
|
+
case "win32":
|
|
3580
|
+
await this.runner.run("powershell", ["-NoProfile", "-Command", WINDOWS_REMOVE]);
|
|
3581
|
+
return;
|
|
3582
|
+
default:
|
|
3583
|
+
await this.runner.run("secret-tool", [
|
|
3584
|
+
"clear",
|
|
3585
|
+
"service",
|
|
3586
|
+
SERVICE,
|
|
3587
|
+
"account",
|
|
3588
|
+
ACCOUNT
|
|
3589
|
+
]);
|
|
3590
|
+
return;
|
|
3591
|
+
}
|
|
3592
|
+
}
|
|
3593
|
+
};
|
|
3594
|
+
WINDOWS_PATH = "$env:APPDATA\\evrex\\capture.cred";
|
|
3595
|
+
WINDOWS_STORE = `
|
|
3596
|
+
$ErrorActionPreference = 'Stop'
|
|
3597
|
+
$p = "${WINDOWS_PATH}"
|
|
3598
|
+
New-Item -ItemType Directory -Force -Path (Split-Path $p) | Out-Null
|
|
3599
|
+
$secret = [Console]::In.ReadToEnd().Trim()
|
|
3600
|
+
ConvertTo-SecureString $secret -AsPlainText -Force | ConvertFrom-SecureString | Set-Content -Path $p
|
|
3601
|
+
`.trim();
|
|
3602
|
+
WINDOWS_RETRIEVE = `
|
|
3603
|
+
$ErrorActionPreference = 'Stop'
|
|
3604
|
+
$p = "${WINDOWS_PATH}"
|
|
3605
|
+
if (-not (Test-Path $p)) { exit 1 }
|
|
3606
|
+
$sec = Get-Content $p | ConvertTo-SecureString
|
|
3607
|
+
[Runtime.InteropServices.Marshal]::PtrToStringAuto(
|
|
3608
|
+
[Runtime.InteropServices.Marshal]::SecureStringToBSTR($sec))
|
|
3609
|
+
`.trim();
|
|
3610
|
+
WINDOWS_REMOVE = `
|
|
3611
|
+
$p = "${WINDOWS_PATH}"
|
|
3612
|
+
if (Test-Path $p) { Remove-Item $p -Force }
|
|
3613
|
+
`.trim();
|
|
3614
|
+
}
|
|
3615
|
+
});
|
|
3616
|
+
|
|
3389
3617
|
// src/capture.ts
|
|
3390
3618
|
init_src();
|
|
3391
3619
|
import { statSync as statSync5 } from "node:fs";
|
|
@@ -3619,6 +3847,11 @@ async function handleCursorEvent(event, name, deps) {
|
|
|
3619
3847
|
writeState(path, state);
|
|
3620
3848
|
return { event: name, turnsSent, archived: false, spoolDrained: drained.sent };
|
|
3621
3849
|
}
|
|
3850
|
+
async function captureToken(env, retrieve) {
|
|
3851
|
+
const explicit = env.EVREX_TOKEN ?? env.EVREX_API_TOKEN;
|
|
3852
|
+
if (explicit) return explicit;
|
|
3853
|
+
return retrieve().catch(() => null);
|
|
3854
|
+
}
|
|
3622
3855
|
async function main() {
|
|
3623
3856
|
const raw = await withDeadline(readStdin(), DEADLINE_MS);
|
|
3624
3857
|
if (!raw) return;
|
|
@@ -3629,17 +3862,23 @@ async function main() {
|
|
|
3629
3862
|
return;
|
|
3630
3863
|
}
|
|
3631
3864
|
const { evrexApi: evrexApi2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
3865
|
+
let token;
|
|
3866
|
+
const credential = () => token ??= captureToken(process.env, async () => {
|
|
3867
|
+
const { CredentialStore: CredentialStore2 } = await Promise.resolve().then(() => (init_credential_store(), credential_store_exports));
|
|
3868
|
+
return new CredentialStore2().retrieve();
|
|
3869
|
+
});
|
|
3632
3870
|
const deps = {
|
|
3633
3871
|
now: () => /* @__PURE__ */ new Date(),
|
|
3634
3872
|
home: homedir7(),
|
|
3635
3873
|
machine: hostname(),
|
|
3636
3874
|
post: async (path, body) => {
|
|
3637
3875
|
try {
|
|
3876
|
+
const bearer = await credential();
|
|
3638
3877
|
const res = await fetch(`${evrexApi2.baseUrl}${path}`, {
|
|
3639
3878
|
method: "POST",
|
|
3640
3879
|
headers: {
|
|
3641
3880
|
"content-type": "application/json",
|
|
3642
|
-
...
|
|
3881
|
+
...bearer ? { authorization: `Bearer ${bearer}` } : {}
|
|
3643
3882
|
},
|
|
3644
3883
|
body: JSON.stringify(body)
|
|
3645
3884
|
});
|
|
@@ -3656,6 +3895,7 @@ main().then(
|
|
|
3656
3895
|
() => process.exit(0)
|
|
3657
3896
|
);
|
|
3658
3897
|
export {
|
|
3898
|
+
captureToken,
|
|
3659
3899
|
drainSpool,
|
|
3660
3900
|
handleEvent,
|
|
3661
3901
|
parseTranscript,
|
package/dist/continuity.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/continuity.ts
|
|
4
|
-
import { execFileSync } from "node:child_process";
|
|
4
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
5
5
|
import { closeSync, openSync, readSync, statSync } from "node:fs";
|
|
6
6
|
import { pathToFileURL } from "node:url";
|
|
7
7
|
|
|
@@ -62,6 +62,7 @@ var evrexApi = {
|
|
|
62
62
|
};
|
|
63
63
|
|
|
64
64
|
// src/hook-runtime.ts
|
|
65
|
+
import { execFileSync } from "node:child_process";
|
|
65
66
|
function readStdin() {
|
|
66
67
|
return new Promise((resolve) => {
|
|
67
68
|
let raw = "";
|
|
@@ -87,7 +88,34 @@ function repoMatchForCwd(repos, cwd) {
|
|
|
87
88
|
}
|
|
88
89
|
}
|
|
89
90
|
}
|
|
90
|
-
return best;
|
|
91
|
+
if (best) return best;
|
|
92
|
+
const hop = worktreeHop(cwd);
|
|
93
|
+
if (!hop) return null;
|
|
94
|
+
for (const repo of repos) {
|
|
95
|
+
for (const path of repo.localPaths ?? []) {
|
|
96
|
+
if (!path) continue;
|
|
97
|
+
if (hop.main === path || hop.main.startsWith(`${path}/`)) {
|
|
98
|
+
return { id: repo.id, root: hop.top };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
function worktreeHop(cwd) {
|
|
105
|
+
try {
|
|
106
|
+
const raw = execFileSync(
|
|
107
|
+
"git",
|
|
108
|
+
["-C", cwd, "rev-parse", "--git-common-dir", "--show-toplevel"],
|
|
109
|
+
{ stdio: ["ignore", "pipe", "ignore"], timeout: 1500 }
|
|
110
|
+
).toString("utf-8").trim().split("\n");
|
|
111
|
+
const [gitDir, top] = [raw[0] ?? "", raw[1] ?? ""];
|
|
112
|
+
if (!gitDir.startsWith("/") || !gitDir.endsWith("/.git") || !top) return null;
|
|
113
|
+
const main2 = gitDir.slice(0, -"/.git".length);
|
|
114
|
+
if (!main2 || main2 === top) return null;
|
|
115
|
+
return { main: main2, top };
|
|
116
|
+
} catch {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
91
119
|
}
|
|
92
120
|
function repoForCwd(repos, cwd) {
|
|
93
121
|
return repoMatchForCwd(repos, cwd)?.id ?? null;
|
|
@@ -258,7 +286,7 @@ function ago(iso, now = /* @__PURE__ */ new Date()) {
|
|
|
258
286
|
}
|
|
259
287
|
function currentBranch(cwd) {
|
|
260
288
|
try {
|
|
261
|
-
const out =
|
|
289
|
+
const out = execFileSync2("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
262
290
|
cwd,
|
|
263
291
|
stdio: ["ignore", "pipe", "ignore"],
|
|
264
292
|
timeout: 1e3
|
package/dist/hook.js
CHANGED
|
@@ -86,7 +86,34 @@ function repoMatchForCwd(repos, cwd) {
|
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
|
-
return best;
|
|
89
|
+
if (best) return best;
|
|
90
|
+
const hop = worktreeHop(cwd);
|
|
91
|
+
if (!hop) return null;
|
|
92
|
+
for (const repo of repos) {
|
|
93
|
+
for (const path of repo.localPaths ?? []) {
|
|
94
|
+
if (!path) continue;
|
|
95
|
+
if (hop.main === path || hop.main.startsWith(`${path}/`)) {
|
|
96
|
+
return { id: repo.id, root: hop.top };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
function worktreeHop(cwd) {
|
|
103
|
+
try {
|
|
104
|
+
const raw = execFileSync(
|
|
105
|
+
"git",
|
|
106
|
+
["-C", cwd, "rev-parse", "--git-common-dir", "--show-toplevel"],
|
|
107
|
+
{ stdio: ["ignore", "pipe", "ignore"], timeout: 1500 }
|
|
108
|
+
).toString("utf-8").trim().split("\n");
|
|
109
|
+
const [gitDir, top] = [raw[0] ?? "", raw[1] ?? ""];
|
|
110
|
+
if (!gitDir.startsWith("/") || !gitDir.endsWith("/.git") || !top) return null;
|
|
111
|
+
const main2 = gitDir.slice(0, -"/.git".length);
|
|
112
|
+
if (!main2 || main2 === top) return null;
|
|
113
|
+
return { main: main2, top };
|
|
114
|
+
} catch {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
90
117
|
}
|
|
91
118
|
function repoForCwd(repos, cwd) {
|
|
92
119
|
return repoMatchForCwd(repos, cwd)?.id ?? null;
|
package/dist/import.js
CHANGED
|
@@ -432,7 +432,7 @@ var PARSER_VERSION;
|
|
|
432
432
|
var init_incremental = __esm({
|
|
433
433
|
"../../packages/ingest-core/src/incremental.ts"() {
|
|
434
434
|
"use strict";
|
|
435
|
-
PARSER_VERSION =
|
|
435
|
+
PARSER_VERSION = 9;
|
|
436
436
|
}
|
|
437
437
|
});
|
|
438
438
|
|
|
@@ -777,14 +777,30 @@ function findSessionFiles(repoPath) {
|
|
|
777
777
|
function editFromXcodeInput(input) {
|
|
778
778
|
const filePath = input.filePath;
|
|
779
779
|
if (typeof filePath !== "string" || !filePath) return null;
|
|
780
|
-
|
|
781
|
-
|
|
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") : [];
|
|
782
788
|
const oldSet = new Set(oldLines);
|
|
783
789
|
const newSet = new Set(newLines);
|
|
784
790
|
const added = meaningfulLines(newLines.filter((l) => !oldSet.has(l)));
|
|
785
791
|
const removed = meaningfulLines(oldLines.filter((l) => !newSet.has(l)));
|
|
786
792
|
if (added.length === 0 && removed.length === 0) return null;
|
|
787
|
-
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) };
|
|
788
804
|
}
|
|
789
805
|
function extractPathsFromText(text) {
|
|
790
806
|
const matches = text.match(PATH_TOKEN_RE) ?? [];
|
|
@@ -801,6 +817,7 @@ function extractFromAssistantContent(content) {
|
|
|
801
817
|
const textParts = [];
|
|
802
818
|
const filesTouched = [];
|
|
803
819
|
const editedLines = [];
|
|
820
|
+
const inputEdits = /* @__PURE__ */ new Map();
|
|
804
821
|
for (const block of content) {
|
|
805
822
|
if (block.type === "text" && block.text) {
|
|
806
823
|
textParts.push(block.text);
|
|
@@ -821,6 +838,10 @@ function extractFromAssistantContent(content) {
|
|
|
821
838
|
} else if (FILE_PATH_TOOLS.has(name) && typeof input.file_path === "string") {
|
|
822
839
|
textParts.push(`[tool_call: ${name}] ${input.file_path}`);
|
|
823
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
|
+
}
|
|
824
845
|
} else if (name === "Bash" && typeof input.command === "string") {
|
|
825
846
|
const desc = typeof input.description === "string" ? input.description : input.command;
|
|
826
847
|
textParts.push(`[tool_call: Bash] ${desc}`);
|
|
@@ -832,7 +853,7 @@ function extractFromAssistantContent(content) {
|
|
|
832
853
|
}
|
|
833
854
|
}
|
|
834
855
|
}
|
|
835
|
-
return { text: textParts.join("\n"), filesTouched, editedLines };
|
|
856
|
+
return { text: textParts.join("\n"), filesTouched, editedLines, inputEdits };
|
|
836
857
|
}
|
|
837
858
|
function extractFromUserContent(content) {
|
|
838
859
|
if (typeof content === "string") {
|
|
@@ -914,14 +935,23 @@ function readSubagentMeta(transcriptPath) {
|
|
|
914
935
|
const metaPath = transcriptPath.replace(/\.jsonl$/, ".meta.json");
|
|
915
936
|
try {
|
|
916
937
|
const raw = JSON.parse(readFileSync(metaPath, "utf-8"));
|
|
938
|
+
const text = (key) => typeof raw[key] === "string" && raw[key] ? raw[key] : null;
|
|
917
939
|
return {
|
|
918
|
-
agentType:
|
|
919
|
-
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")
|
|
920
947
|
};
|
|
921
948
|
} catch {
|
|
922
949
|
return null;
|
|
923
950
|
}
|
|
924
951
|
}
|
|
952
|
+
function subagentSessionId(rootSessionId, agentId) {
|
|
953
|
+
return deriveUuid(`evrex-subagent\0${rootSessionId}\0${agentId}`);
|
|
954
|
+
}
|
|
925
955
|
function parseSessionFile(filePath, repoPath, repoId = deriveRepoId(repoPath), agentKind = "claude-code") {
|
|
926
956
|
const { text: raw, capped } = readTranscript(filePath);
|
|
927
957
|
const lines = raw.split("\n").filter((l) => l.trim().length > 0);
|
|
@@ -934,6 +964,7 @@ function parseSessionFile(filePath, repoPath, repoId = deriveRepoId(repoPath), a
|
|
|
934
964
|
let branch = null;
|
|
935
965
|
let agentId = null;
|
|
936
966
|
const seenIds = /* @__PURE__ */ new Set();
|
|
967
|
+
const callEdits = /* @__PURE__ */ new Map();
|
|
937
968
|
for (const line of lines) {
|
|
938
969
|
let record;
|
|
939
970
|
try {
|
|
@@ -966,6 +997,7 @@ function parseSessionFile(filePath, repoPath, repoId = deriveRepoId(repoPath), a
|
|
|
966
997
|
text = extracted.text;
|
|
967
998
|
filesTouched = extracted.filesTouched;
|
|
968
999
|
editedLines = extracted.editedLines;
|
|
1000
|
+
for (const [toolUseId, edit] of extracted.inputEdits) callEdits.set(toolUseId, edit);
|
|
969
1001
|
}
|
|
970
1002
|
} else {
|
|
971
1003
|
const extracted = extractFromUserContent(record.message?.content);
|
|
@@ -975,6 +1007,13 @@ function parseSessionFile(filePath, repoPath, repoId = deriveRepoId(repoPath), a
|
|
|
975
1007
|
isToolError = extracted.isToolError ?? false;
|
|
976
1008
|
const edited = extractEditedLines(record.toolUseResult);
|
|
977
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
|
+
}
|
|
978
1017
|
}
|
|
979
1018
|
const redacted = redactSecrets(text);
|
|
980
1019
|
totalRedactions += redacted.count;
|
|
@@ -995,13 +1034,21 @@ function parseSessionFile(filePath, repoPath, repoId = deriveRepoId(repoPath), a
|
|
|
995
1034
|
});
|
|
996
1035
|
}
|
|
997
1036
|
if (!sessionId || turns.length === 0) return null;
|
|
998
|
-
const
|
|
999
|
-
|
|
1000
|
-
|
|
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);
|
|
1001
1042
|
for (const t of turns) t.sessionId = sessionId;
|
|
1002
1043
|
}
|
|
1003
|
-
const
|
|
1004
|
-
|
|
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;
|
|
1005
1052
|
if (agentId && !aiTitle && meta?.description) aiTitle = meta.description;
|
|
1006
1053
|
const sortedTs = turns.map((t) => t.ts).sort();
|
|
1007
1054
|
const commitEvidence = collectCommitEvidence(lines);
|
|
@@ -1109,7 +1156,7 @@ function parseAllSessions(repoPath, repoId, cursors = {}) {
|
|
|
1109
1156
|
}
|
|
1110
1157
|
return { sessions, cursors: next, skipped };
|
|
1111
1158
|
}
|
|
1112
|
-
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;
|
|
1113
1160
|
var init_claude_sessions = __esm({
|
|
1114
1161
|
"../../packages/ingest-core/src/claude-sessions.ts"() {
|
|
1115
1162
|
"use strict";
|
|
@@ -1121,9 +1168,10 @@ var init_claude_sessions = __esm({
|
|
|
1121
1168
|
init_types();
|
|
1122
1169
|
MIN_MEANINGFUL_LINE_LENGTH = 6;
|
|
1123
1170
|
MAX_TURN_TEXT_LENGTH = 4e3;
|
|
1124
|
-
FILE_PATH_TOOLS = /* @__PURE__ */ new Set(["Read", "Edit", "Write", "NotebookEdit"]);
|
|
1171
|
+
FILE_PATH_TOOLS = /* @__PURE__ */ new Set(["Read", "Edit", "Write", "MultiEdit", "NotebookEdit"]);
|
|
1125
1172
|
XCODE_EDIT_TOOL = /XcodeUpdate$/;
|
|
1126
1173
|
XCODE_PATH_TOOLS = /Xcode(Read|Grep|Glob|RefreshCodeIssuesInFile)$/;
|
|
1174
|
+
CLAUDE_EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit"]);
|
|
1127
1175
|
PATH_TOKEN_RE = /(?:[.~]?\/)?(?:[\w-]+\/)+[\w.-]+/g;
|
|
1128
1176
|
SYNTHETIC_CONTENT_RE = /^\s*(<task-notification>|<system-reminder>|\[SYSTEM NOTIFICATION)/;
|
|
1129
1177
|
MAX_TRANSCRIPT_BYTES = 256 * 1024 * 1024;
|
|
@@ -2702,7 +2750,7 @@ var init_trace_target = __esm({
|
|
|
2702
2750
|
|
|
2703
2751
|
// ../../packages/ingest-core/src/gemini-sessions.ts
|
|
2704
2752
|
import { readFileSync as readFileSync4 } from "node:fs";
|
|
2705
|
-
function
|
|
2753
|
+
function textOf2(content) {
|
|
2706
2754
|
if (typeof content === "string") return content;
|
|
2707
2755
|
if (!Array.isArray(content)) return "";
|
|
2708
2756
|
return content.map(
|
|
@@ -2736,7 +2784,7 @@ function parseGeminiSessionFile(filePath, repoPath, repoId) {
|
|
|
2736
2784
|
messages.forEach((record, index) => {
|
|
2737
2785
|
const role = record.type === "user" ? "user" : record.type === "gemini" || record.type === "model" ? "assistant" : null;
|
|
2738
2786
|
if (!role) return;
|
|
2739
|
-
const text =
|
|
2787
|
+
const text = textOf2(record.content ?? record.displayContent);
|
|
2740
2788
|
if (!text.trim()) return;
|
|
2741
2789
|
const { text: safe, count } = redactSecrets(text);
|
|
2742
2790
|
redactionCount += count;
|
|
@@ -3452,11 +3500,13 @@ evrex will not fall back to writing the credential in a plain file.`
|
|
|
3452
3500
|
if (!await this.available()) throw new NoKeychainError(this.runner.platform);
|
|
3453
3501
|
switch (this.runner.platform) {
|
|
3454
3502
|
case "darwin": {
|
|
3503
|
+
if (/["\\\r\n]/.test(secret)) {
|
|
3504
|
+
throw new Error("Keychain write refused: the credential has a character `security -i` cannot carry.");
|
|
3505
|
+
}
|
|
3455
3506
|
const result = await this.runner.run(
|
|
3456
3507
|
"security",
|
|
3457
|
-
["
|
|
3458
|
-
|
|
3459
|
-
${secret}
|
|
3508
|
+
["-i"],
|
|
3509
|
+
`add-generic-password -a "${ACCOUNT}" -s "${SERVICE}" -U -w "${secret}"
|
|
3460
3510
|
`
|
|
3461
3511
|
);
|
|
3462
3512
|
if (result.code !== 0) throw new Error(`Keychain write failed: ${result.stderr.trim()}`);
|
package/dist/index.js
CHANGED
|
@@ -141,11 +141,13 @@ evrex will not fall back to writing the credential in a plain file.`
|
|
|
141
141
|
if (!await this.available()) throw new NoKeychainError(this.runner.platform);
|
|
142
142
|
switch (this.runner.platform) {
|
|
143
143
|
case "darwin": {
|
|
144
|
+
if (/["\\\r\n]/.test(secret)) {
|
|
145
|
+
throw new Error("Keychain write refused: the credential has a character `security -i` cannot carry.");
|
|
146
|
+
}
|
|
144
147
|
const result = await this.runner.run(
|
|
145
148
|
"security",
|
|
146
|
-
["
|
|
147
|
-
|
|
148
|
-
${secret}
|
|
149
|
+
["-i"],
|
|
150
|
+
`add-generic-password -a "${ACCOUNT}" -s "${SERVICE}" -U -w "${secret}"
|
|
149
151
|
`
|
|
150
152
|
);
|
|
151
153
|
if (result.code !== 0) throw new Error(`Keychain write failed: ${result.stderr.trim()}`);
|
|
@@ -354,6 +356,17 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
354
356
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
355
357
|
import { z } from "zod";
|
|
356
358
|
|
|
359
|
+
// src/subcommands.ts
|
|
360
|
+
var SUBCOMMANDS = {
|
|
361
|
+
"evrex-hook": "hook.js",
|
|
362
|
+
"evrex-capture": "capture.js",
|
|
363
|
+
"evrex-import": "import.js",
|
|
364
|
+
"evrex-tickets": "tickets.js",
|
|
365
|
+
"evrex-pretool": "pretool.js",
|
|
366
|
+
"evrex-continuity": "continuity.js",
|
|
367
|
+
"evrex-account": "account.js"
|
|
368
|
+
};
|
|
369
|
+
|
|
357
370
|
// src/tools.ts
|
|
358
371
|
init_client();
|
|
359
372
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
@@ -453,7 +466,7 @@ var EXTRACTION_SYSTEM_PROMPT = [
|
|
|
453
466
|
// ../../packages/llm-core/src/synthesis.ts
|
|
454
467
|
var MIN_SENTENCES = 1;
|
|
455
468
|
var MAX_SENTENCES = 4;
|
|
456
|
-
var SYNTHESIS_SYSTEM_PROMPT = `You answer questions about why code changed, using ONLY the evidence provided below. Never state a claim that is not directly supported by the evidence text. Respond with a JSON object: {"sentences": string[], "sentenceEvidence": number[][], "noRecordedReasoning": boolean}. Write 2-4 sentences when the evidence supports an answer; each sentence's entry in "sentenceEvidence" lists only the evidence indices (0-based) that directly support it. If the evidence is too thin or unrelated to actually answer the question, set "noRecordedReasoning": true and return empty arrays for "sentences" and "sentenceEvidence" \u2014 do not speculate or hedge.`;
|
|
469
|
+
var SYNTHESIS_SYSTEM_PROMPT = `You answer questions about why code changed, using ONLY the evidence provided below. The evidence spans the project's whole history and the system it describes keeps changing: when items disagree, the most recently dated evidence describes the CURRENT state \u2014 answer as of the newest evidence, frame older accounts as how it used to work, and never present a superseded design as current. Never state a claim that is not directly supported by the evidence text. Respond with a JSON object: {"sentences": string[], "sentenceEvidence": number[][], "noRecordedReasoning": boolean}. Write 2-4 sentences when the evidence supports an answer; each sentence's entry in "sentenceEvidence" lists only the evidence indices (0-based) that directly support it. If the evidence is too thin or unrelated to actually answer the question, set "noRecordedReasoning": true and return empty arrays for "sentences" and "sentenceEvidence" \u2014 do not speculate or hedge.`;
|
|
457
470
|
var SYNTHESIS_SCHEMA = {
|
|
458
471
|
type: "object",
|
|
459
472
|
additionalProperties: false,
|
|
@@ -469,7 +482,7 @@ var SYNTHESIS_SCHEMA = {
|
|
|
469
482
|
};
|
|
470
483
|
function buildSynthesisPrompt(question, evidence) {
|
|
471
484
|
const list = evidence.map(
|
|
472
|
-
(e, i) => `[${i}] (${e.kind}, confidence ${(e.confidence ?? 1).toFixed(2)}): ${e.excerpt}`
|
|
485
|
+
(e, i) => `[${i}] (${e.kind}${e.at ? `, ${e.at.slice(0, 10)}` : ""}, confidence ${(e.confidence ?? 1).toFixed(2)}): ${e.excerpt}`
|
|
473
486
|
).join("\n");
|
|
474
487
|
return `Question: ${question}
|
|
475
488
|
|
|
@@ -544,7 +557,7 @@ ${CORPUS}
|
|
|
544
557
|
You are given the question and a numbered list of candidate results that lexical retrieval fetched. Order the candidate ids best-first by how much each would actually help ANSWER the question:
|
|
545
558
|
- Recorded reasoning outranks mechanical record: a decision, a rejected approach with its reason, a constraint, or an explanation beats a log line or a file listing that merely shares words with the question.
|
|
546
559
|
- A candidate about the question's subject beats one that only quotes its vocabulary; watch for results that repeat the question's words while being about something else entirely.
|
|
547
|
-
-
|
|
560
|
+
- Match recency to the question's intent: for "why does X exist / why was this decided", the origin of the idea is the answer; for "how does X work / what is the current pipeline", the system has usually changed since its founding \u2014 prefer the most recent account of the mechanism and treat older ones as history. Among near-duplicates prefer the more complete one.
|
|
548
561
|
|
|
549
562
|
Respond with JSON only: {"ranked": ["id", ...]} \u2014 every listed id at most once, ids from the list only, best first. Include every id you consider relevant; omit only what would waste the reader's attention.`;
|
|
550
563
|
|
|
@@ -1658,6 +1671,28 @@ function proceduresBlock(procedures) {
|
|
|
1658
1671
|
return "PROCEDURES (how this was done here, as steps that worked once \u2014 check they still apply before following them):\n" + procedures.slice(0, MAX_ITEMS).map((p) => `- ${p.title}
|
|
1659
1672
|
${p.steps.slice(0, 12).map((step, i) => ` ${i + 1}. ${step}`).join("\n")}`).join("\n");
|
|
1660
1673
|
}
|
|
1674
|
+
function subagentsBlock(subagents) {
|
|
1675
|
+
const budget = MAX_ITEMS * 2;
|
|
1676
|
+
const lines = [];
|
|
1677
|
+
let printed = 0;
|
|
1678
|
+
let total = 0;
|
|
1679
|
+
for (const s of subagents) {
|
|
1680
|
+
const items = [
|
|
1681
|
+
...s.rejected.map((r) => ` - rejected: ${r.title} \u2014 ${r.reason}`),
|
|
1682
|
+
...s.constraints.map((c) => ` - constraint [${c.source}]: ${c.text}`)
|
|
1683
|
+
];
|
|
1684
|
+
total += items.length;
|
|
1685
|
+
const shown = items.slice(0, budget - printed);
|
|
1686
|
+
if (shown.length === 0) continue;
|
|
1687
|
+
const what = [s.agentType, s.description].filter(Boolean).join(": ");
|
|
1688
|
+
lines.push(`- session:${s.id}${what ? ` (${what})` : ""}`, ...shown);
|
|
1689
|
+
printed += shown.length;
|
|
1690
|
+
}
|
|
1691
|
+
if (lines.length === 0) return "";
|
|
1692
|
+
if (total > printed) lines.push(` \u2026 ${total - printed} more in their own records (expand the sub-agent handles).`);
|
|
1693
|
+
return `FROM SUB-AGENTS (what the agents this session ran ruled out and were bound by):
|
|
1694
|
+
${lines.join("\n")}`;
|
|
1695
|
+
}
|
|
1661
1696
|
async function evrexExpand(handles) {
|
|
1662
1697
|
if (handles.length === 0) return "No handles given. Pass ids from evrex_search, e.g. commit:069a0b5.";
|
|
1663
1698
|
const parts = [];
|
|
@@ -1711,6 +1746,8 @@ Open in Evrex: evrex://open?handle=commit:${id}`);
|
|
|
1711
1746
|
}
|
|
1712
1747
|
const procedures = proceduresBlock(session.procedures ?? []);
|
|
1713
1748
|
if (procedures) block.push(procedures);
|
|
1749
|
+
const fromSubagents = subagentsBlock(session.subagents ?? []);
|
|
1750
|
+
if (fromSubagents) block.push(fromSubagents);
|
|
1714
1751
|
parts.push(block.join("\n"));
|
|
1715
1752
|
}
|
|
1716
1753
|
parts.push(
|
|
@@ -1829,6 +1866,8 @@ ${truncate(commit.body, MAX_EXCERPT)}` : ""}`
|
|
|
1829
1866
|
"CONSTRAINTS:\n" + session.constraints.slice(0, MAX_ITEMS).map((c) => `- [${c.source}] ${c.text}`).join("\n")
|
|
1830
1867
|
);
|
|
1831
1868
|
}
|
|
1869
|
+
const fromSubagents = subagentsBlock(session.subagents ?? []);
|
|
1870
|
+
if (fromSubagents) parts.push(fromSubagents);
|
|
1832
1871
|
parts.push(`OUTCOME: ${truncate(session.outcome, MAX_EXCERPT)}`);
|
|
1833
1872
|
}
|
|
1834
1873
|
} else if (!commit.agentTrailers?.length) {
|
|
@@ -1985,10 +2024,22 @@ async function runEnrolment() {
|
|
|
1985
2024
|
});
|
|
1986
2025
|
process.exit(result.ok ? 0 : 1);
|
|
1987
2026
|
}
|
|
2027
|
+
async function runSubcommand(file) {
|
|
2028
|
+
const { spawnSync } = await import("node:child_process");
|
|
2029
|
+
const { fileURLToPath } = await import("node:url");
|
|
2030
|
+
const result = spawnSync(
|
|
2031
|
+
process.execPath,
|
|
2032
|
+
[fileURLToPath(new URL(`./${file}`, import.meta.url)), ...process.argv.slice(3)],
|
|
2033
|
+
{ stdio: "inherit" }
|
|
2034
|
+
);
|
|
2035
|
+
process.exit(result.status ?? 1);
|
|
2036
|
+
}
|
|
1988
2037
|
async function main() {
|
|
1989
2038
|
if (process.argv[2] === "enrol" || process.argv[2] === "enroll") {
|
|
1990
2039
|
await runEnrolment();
|
|
1991
2040
|
}
|
|
2041
|
+
const subcommand = SUBCOMMANDS[process.argv[2] ?? ""];
|
|
2042
|
+
if (subcommand) await runSubcommand(subcommand);
|
|
1992
2043
|
assertConfigured();
|
|
1993
2044
|
const transport = new StdioServerTransport();
|
|
1994
2045
|
await server.connect(transport);
|
package/dist/pretool.js
CHANGED
|
@@ -86,7 +86,34 @@ function repoMatchForCwd(repos, cwd) {
|
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
|
-
return best;
|
|
89
|
+
if (best) return best;
|
|
90
|
+
const hop = worktreeHop(cwd);
|
|
91
|
+
if (!hop) return null;
|
|
92
|
+
for (const repo of repos) {
|
|
93
|
+
for (const path of repo.localPaths ?? []) {
|
|
94
|
+
if (!path) continue;
|
|
95
|
+
if (hop.main === path || hop.main.startsWith(`${path}/`)) {
|
|
96
|
+
return { id: repo.id, root: hop.top };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
function worktreeHop(cwd) {
|
|
103
|
+
try {
|
|
104
|
+
const raw = execFileSync(
|
|
105
|
+
"git",
|
|
106
|
+
["-C", cwd, "rev-parse", "--git-common-dir", "--show-toplevel"],
|
|
107
|
+
{ stdio: ["ignore", "pipe", "ignore"], timeout: 1500 }
|
|
108
|
+
).toString("utf-8").trim().split("\n");
|
|
109
|
+
const [gitDir, top] = [raw[0] ?? "", raw[1] ?? ""];
|
|
110
|
+
if (!gitDir.startsWith("/") || !gitDir.endsWith("/.git") || !top) return null;
|
|
111
|
+
const main2 = gitDir.slice(0, -"/.git".length);
|
|
112
|
+
if (!main2 || main2 === top) return null;
|
|
113
|
+
return { main: main2, top };
|
|
114
|
+
} catch {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
90
117
|
}
|
|
91
118
|
function toRepoRelative(filePath, root) {
|
|
92
119
|
if (!root || !filePath.startsWith("/")) return filePath;
|
package/dist/tickets.js
CHANGED
|
@@ -141,11 +141,13 @@ evrex will not fall back to writing the credential in a plain file.`
|
|
|
141
141
|
if (!await this.available()) throw new NoKeychainError(this.runner.platform);
|
|
142
142
|
switch (this.runner.platform) {
|
|
143
143
|
case "darwin": {
|
|
144
|
+
if (/["\\\r\n]/.test(secret)) {
|
|
145
|
+
throw new Error("Keychain write refused: the credential has a character `security -i` cannot carry.");
|
|
146
|
+
}
|
|
144
147
|
const result = await this.runner.run(
|
|
145
148
|
"security",
|
|
146
|
-
["
|
|
147
|
-
|
|
148
|
-
${secret}
|
|
149
|
+
["-i"],
|
|
150
|
+
`add-generic-password -a "${ACCOUNT}" -s "${SERVICE}" -U -w "${secret}"
|
|
149
151
|
`
|
|
150
152
|
);
|
|
151
153
|
if (result.code !== 0) throw new Error(`Keychain write failed: ${result.stderr.trim()}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "evrex-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "MCP server that gives coding agents the recorded reasoning behind a repo: prior decisions, hard constraints, and approaches already rejected.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
@@ -36,28 +36,27 @@
|
|
|
36
36
|
"engines": {
|
|
37
37
|
"node": ">=20"
|
|
38
38
|
},
|
|
39
|
-
"scripts": {
|
|
40
|
-
"build": "tsc -p tsconfig.json",
|
|
41
|
-
"bundle": "node scripts/bundle.mjs",
|
|
42
|
-
"dev": "tsc -p tsconfig.json --watch",
|
|
43
|
-
"lint": "eslint .",
|
|
44
|
-
"check-types": "tsc --noEmit",
|
|
45
|
-
"test": "tsc -p tsconfig.json && node --test dist/*.test.js",
|
|
46
|
-
"prepack": "pnpm run bundle"
|
|
47
|
-
},
|
|
48
39
|
"dependencies": {
|
|
49
40
|
"@anthropic-ai/sdk": "^0.72.0",
|
|
50
41
|
"@modelcontextprotocol/sdk": "^1.20.1",
|
|
51
42
|
"zod": "^3.24.1"
|
|
52
43
|
},
|
|
53
44
|
"devDependencies": {
|
|
54
|
-
"@repo/eslint-config": "workspace:*",
|
|
55
|
-
"@repo/ingest-core": "workspace:*",
|
|
56
|
-
"@repo/llm-core": "workspace:*",
|
|
57
|
-
"@repo/typescript-config": "workspace:*",
|
|
58
45
|
"@types/node": "^24.0.0",
|
|
59
46
|
"esbuild": "^0.25.12",
|
|
60
47
|
"eslint": "^9.39.1",
|
|
61
|
-
"typescript": "^5.9.2"
|
|
48
|
+
"typescript": "^5.9.2",
|
|
49
|
+
"@repo/eslint-config": "0.0.0",
|
|
50
|
+
"@repo/ingest-core": "0.0.0",
|
|
51
|
+
"@repo/llm-core": "0.0.0",
|
|
52
|
+
"@repo/typescript-config": "0.0.0"
|
|
53
|
+
},
|
|
54
|
+
"scripts": {
|
|
55
|
+
"build": "tsc -p tsconfig.json",
|
|
56
|
+
"bundle": "node scripts/bundle.mjs",
|
|
57
|
+
"dev": "tsc -p tsconfig.json --watch",
|
|
58
|
+
"lint": "eslint .",
|
|
59
|
+
"check-types": "tsc --noEmit",
|
|
60
|
+
"test": "tsc -p tsconfig.json && node --test dist/*.test.js"
|
|
62
61
|
}
|
|
63
|
-
}
|
|
62
|
+
}
|