evrex-mcp 0.8.2 → 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/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 = 7;
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
- const oldLines = typeof input.oldString === "string" ? input.oldString.split("\n") : [];
781
- const newLines = typeof input.newString === "string" ? input.newString.split("\n") : [];
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: filePath, added, removed };
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: typeof raw.agentType === "string" ? raw.agentType : null,
919
- description: typeof raw.description === "string" ? raw.description : null
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 parentSessionId = agentId ? sessionId : null;
999
- if (agentId) {
1000
- sessionId = deriveUuid(`evrex-subagent\0${parentSessionId}\0${agentId}`);
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 meta = agentId ? readSubagentMeta(filePath) : null;
1004
- const subagent = agentId ? { agentId, agentType: meta?.agentType ?? null, description: meta?.description ?? null } : null;
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 textOf(content) {
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 = textOf(record.content ?? record.displayContent);
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,184 @@ 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
+ const result = await this.runner.run(
3504
+ "security",
3505
+ ["add-generic-password", "-a", ACCOUNT, "-s", SERVICE, "-U", "-w"],
3506
+ `${secret}
3507
+ ${secret}
3508
+ `
3509
+ );
3510
+ if (result.code !== 0) throw new Error(`Keychain write failed: ${result.stderr.trim()}`);
3511
+ return;
3512
+ }
3513
+ case "win32": {
3514
+ const result = await this.runner.run(
3515
+ "powershell",
3516
+ ["-NoProfile", "-Command", WINDOWS_STORE],
3517
+ secret
3518
+ );
3519
+ if (result.code !== 0) throw new Error(`DPAPI write failed: ${result.stderr.trim()}`);
3520
+ return;
3521
+ }
3522
+ default: {
3523
+ const result = await this.runner.run(
3524
+ "secret-tool",
3525
+ ["store", "--label=evrex capture credential", "service", SERVICE, "account", ACCOUNT],
3526
+ secret
3527
+ );
3528
+ if (result.code !== 0) throw new Error(`secret-tool write failed: ${result.stderr.trim()}`);
3529
+ return;
3530
+ }
3531
+ }
3532
+ }
3533
+ /** Null when there is nothing stored, which is the normal pre-enrolment state. */
3534
+ async retrieve() {
3535
+ if (!await this.available()) throw new NoKeychainError(this.runner.platform);
3536
+ switch (this.runner.platform) {
3537
+ case "darwin": {
3538
+ const r = await this.runner.run("security", [
3539
+ "find-generic-password",
3540
+ "-a",
3541
+ ACCOUNT,
3542
+ "-s",
3543
+ SERVICE,
3544
+ "-w"
3545
+ ]);
3546
+ return r.code === 0 ? r.stdout.trim() || null : null;
3547
+ }
3548
+ case "win32": {
3549
+ const r = await this.runner.run("powershell", ["-NoProfile", "-Command", WINDOWS_RETRIEVE]);
3550
+ return r.code === 0 ? r.stdout.trim() || null : null;
3551
+ }
3552
+ default: {
3553
+ const r = await this.runner.run("secret-tool", [
3554
+ "lookup",
3555
+ "service",
3556
+ SERVICE,
3557
+ "account",
3558
+ ACCOUNT
3559
+ ]);
3560
+ return r.code === 0 ? r.stdout.trim() || null : null;
3561
+ }
3562
+ }
3563
+ }
3564
+ /** Idempotent: removing a credential that is not there is not an error. */
3565
+ async remove() {
3566
+ if (!await this.available()) return;
3567
+ switch (this.runner.platform) {
3568
+ case "darwin":
3569
+ await this.runner.run("security", [
3570
+ "delete-generic-password",
3571
+ "-a",
3572
+ ACCOUNT,
3573
+ "-s",
3574
+ SERVICE
3575
+ ]);
3576
+ return;
3577
+ case "win32":
3578
+ await this.runner.run("powershell", ["-NoProfile", "-Command", WINDOWS_REMOVE]);
3579
+ return;
3580
+ default:
3581
+ await this.runner.run("secret-tool", [
3582
+ "clear",
3583
+ "service",
3584
+ SERVICE,
3585
+ "account",
3586
+ ACCOUNT
3587
+ ]);
3588
+ return;
3589
+ }
3590
+ }
3591
+ };
3592
+ WINDOWS_PATH = "$env:APPDATA\\evrex\\capture.cred";
3593
+ WINDOWS_STORE = `
3594
+ $ErrorActionPreference = 'Stop'
3595
+ $p = "${WINDOWS_PATH}"
3596
+ New-Item -ItemType Directory -Force -Path (Split-Path $p) | Out-Null
3597
+ $secret = [Console]::In.ReadToEnd().Trim()
3598
+ ConvertTo-SecureString $secret -AsPlainText -Force | ConvertFrom-SecureString | Set-Content -Path $p
3599
+ `.trim();
3600
+ WINDOWS_RETRIEVE = `
3601
+ $ErrorActionPreference = 'Stop'
3602
+ $p = "${WINDOWS_PATH}"
3603
+ if (-not (Test-Path $p)) { exit 1 }
3604
+ $sec = Get-Content $p | ConvertTo-SecureString
3605
+ [Runtime.InteropServices.Marshal]::PtrToStringAuto(
3606
+ [Runtime.InteropServices.Marshal]::SecureStringToBSTR($sec))
3607
+ `.trim();
3608
+ WINDOWS_REMOVE = `
3609
+ $p = "${WINDOWS_PATH}"
3610
+ if (Test-Path $p) { Remove-Item $p -Force }
3611
+ `.trim();
3612
+ }
3613
+ });
3614
+
3389
3615
  // src/capture.ts
3390
3616
  init_src();
3391
3617
  import { statSync as statSync5 } from "node:fs";
@@ -3619,6 +3845,11 @@ async function handleCursorEvent(event, name, deps) {
3619
3845
  writeState(path, state);
3620
3846
  return { event: name, turnsSent, archived: false, spoolDrained: drained.sent };
3621
3847
  }
3848
+ async function captureToken(env, retrieve) {
3849
+ const explicit = env.EVREX_TOKEN ?? env.EVREX_API_TOKEN;
3850
+ if (explicit) return explicit;
3851
+ return retrieve().catch(() => null);
3852
+ }
3622
3853
  async function main() {
3623
3854
  const raw = await withDeadline(readStdin(), DEADLINE_MS);
3624
3855
  if (!raw) return;
@@ -3629,17 +3860,23 @@ async function main() {
3629
3860
  return;
3630
3861
  }
3631
3862
  const { evrexApi: evrexApi2 } = await Promise.resolve().then(() => (init_client(), client_exports));
3863
+ let token;
3864
+ const credential = () => token ??= captureToken(process.env, async () => {
3865
+ const { CredentialStore: CredentialStore2 } = await Promise.resolve().then(() => (init_credential_store(), credential_store_exports));
3866
+ return new CredentialStore2().retrieve();
3867
+ });
3632
3868
  const deps = {
3633
3869
  now: () => /* @__PURE__ */ new Date(),
3634
3870
  home: homedir7(),
3635
3871
  machine: hostname(),
3636
3872
  post: async (path, body) => {
3637
3873
  try {
3874
+ const bearer = await credential();
3638
3875
  const res = await fetch(`${evrexApi2.baseUrl}${path}`, {
3639
3876
  method: "POST",
3640
3877
  headers: {
3641
3878
  "content-type": "application/json",
3642
- ...process.env.EVREX_TOKEN ? { authorization: `Bearer ${process.env.EVREX_TOKEN}` } : {}
3879
+ ...bearer ? { authorization: `Bearer ${bearer}` } : {}
3643
3880
  },
3644
3881
  body: JSON.stringify(body)
3645
3882
  });
@@ -3656,6 +3893,7 @@ main().then(
3656
3893
  () => process.exit(0)
3657
3894
  );
3658
3895
  export {
3896
+ captureToken,
3659
3897
  drainSpool,
3660
3898
  handleEvent,
3661
3899
  parseTranscript,
@@ -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 = execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
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 = 7;
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
- const oldLines = typeof input.oldString === "string" ? input.oldString.split("\n") : [];
781
- const newLines = typeof input.newString === "string" ? input.newString.split("\n") : [];
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: filePath, added, removed };
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: typeof raw.agentType === "string" ? raw.agentType : null,
919
- description: typeof raw.description === "string" ? raw.description : null
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 parentSessionId = agentId ? sessionId : null;
999
- if (agentId) {
1000
- sessionId = deriveUuid(`evrex-subagent\0${parentSessionId}\0${agentId}`);
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 meta = agentId ? readSubagentMeta(filePath) : null;
1004
- const subagent = agentId ? { agentId, agentType: meta?.agentType ?? null, description: meta?.description ?? null } : null;
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 textOf(content) {
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 = textOf(record.content ?? record.displayContent);
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;
package/dist/index.js CHANGED
@@ -354,6 +354,17 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
354
354
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
355
355
  import { z } from "zod";
356
356
 
357
+ // src/subcommands.ts
358
+ var SUBCOMMANDS = {
359
+ "evrex-hook": "hook.js",
360
+ "evrex-capture": "capture.js",
361
+ "evrex-import": "import.js",
362
+ "evrex-tickets": "tickets.js",
363
+ "evrex-pretool": "pretool.js",
364
+ "evrex-continuity": "continuity.js",
365
+ "evrex-account": "account.js"
366
+ };
367
+
357
368
  // src/tools.ts
358
369
  init_client();
359
370
  import { execFileSync as execFileSync2 } from "node:child_process";
@@ -453,7 +464,7 @@ var EXTRACTION_SYSTEM_PROMPT = [
453
464
  // ../../packages/llm-core/src/synthesis.ts
454
465
  var MIN_SENTENCES = 1;
455
466
  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.`;
467
+ 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
468
  var SYNTHESIS_SCHEMA = {
458
469
  type: "object",
459
470
  additionalProperties: false,
@@ -469,7 +480,7 @@ var SYNTHESIS_SCHEMA = {
469
480
  };
470
481
  function buildSynthesisPrompt(question, evidence) {
471
482
  const list = evidence.map(
472
- (e, i) => `[${i}] (${e.kind}, confidence ${(e.confidence ?? 1).toFixed(2)}): ${e.excerpt}`
483
+ (e, i) => `[${i}] (${e.kind}${e.at ? `, ${e.at.slice(0, 10)}` : ""}, confidence ${(e.confidence ?? 1).toFixed(2)}): ${e.excerpt}`
473
484
  ).join("\n");
474
485
  return `Question: ${question}
475
486
 
@@ -544,7 +555,7 @@ ${CORPUS}
544
555
  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
556
  - 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
557
  - 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
- - Prefer the origin of an idea over a later mention of it; among near-duplicates prefer the more complete one; use the date only to break ties.
558
+ - 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
559
 
549
560
  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
561
 
@@ -1658,6 +1669,28 @@ function proceduresBlock(procedures) {
1658
1669
  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
1670
  ${p.steps.slice(0, 12).map((step, i) => ` ${i + 1}. ${step}`).join("\n")}`).join("\n");
1660
1671
  }
1672
+ function subagentsBlock(subagents) {
1673
+ const budget = MAX_ITEMS * 2;
1674
+ const lines = [];
1675
+ let printed = 0;
1676
+ let total = 0;
1677
+ for (const s of subagents) {
1678
+ const items = [
1679
+ ...s.rejected.map((r) => ` - rejected: ${r.title} \u2014 ${r.reason}`),
1680
+ ...s.constraints.map((c) => ` - constraint [${c.source}]: ${c.text}`)
1681
+ ];
1682
+ total += items.length;
1683
+ const shown = items.slice(0, budget - printed);
1684
+ if (shown.length === 0) continue;
1685
+ const what = [s.agentType, s.description].filter(Boolean).join(": ");
1686
+ lines.push(`- session:${s.id}${what ? ` (${what})` : ""}`, ...shown);
1687
+ printed += shown.length;
1688
+ }
1689
+ if (lines.length === 0) return "";
1690
+ if (total > printed) lines.push(` \u2026 ${total - printed} more in their own records (expand the sub-agent handles).`);
1691
+ return `FROM SUB-AGENTS (what the agents this session ran ruled out and were bound by):
1692
+ ${lines.join("\n")}`;
1693
+ }
1661
1694
  async function evrexExpand(handles) {
1662
1695
  if (handles.length === 0) return "No handles given. Pass ids from evrex_search, e.g. commit:069a0b5.";
1663
1696
  const parts = [];
@@ -1711,6 +1744,8 @@ Open in Evrex: evrex://open?handle=commit:${id}`);
1711
1744
  }
1712
1745
  const procedures = proceduresBlock(session.procedures ?? []);
1713
1746
  if (procedures) block.push(procedures);
1747
+ const fromSubagents = subagentsBlock(session.subagents ?? []);
1748
+ if (fromSubagents) block.push(fromSubagents);
1714
1749
  parts.push(block.join("\n"));
1715
1750
  }
1716
1751
  parts.push(
@@ -1829,6 +1864,8 @@ ${truncate(commit.body, MAX_EXCERPT)}` : ""}`
1829
1864
  "CONSTRAINTS:\n" + session.constraints.slice(0, MAX_ITEMS).map((c) => `- [${c.source}] ${c.text}`).join("\n")
1830
1865
  );
1831
1866
  }
1867
+ const fromSubagents = subagentsBlock(session.subagents ?? []);
1868
+ if (fromSubagents) parts.push(fromSubagents);
1832
1869
  parts.push(`OUTCOME: ${truncate(session.outcome, MAX_EXCERPT)}`);
1833
1870
  }
1834
1871
  } else if (!commit.agentTrailers?.length) {
@@ -1985,10 +2022,22 @@ async function runEnrolment() {
1985
2022
  });
1986
2023
  process.exit(result.ok ? 0 : 1);
1987
2024
  }
2025
+ async function runSubcommand(file) {
2026
+ const { spawnSync } = await import("node:child_process");
2027
+ const { fileURLToPath } = await import("node:url");
2028
+ const result = spawnSync(
2029
+ process.execPath,
2030
+ [fileURLToPath(new URL(`./${file}`, import.meta.url)), ...process.argv.slice(3)],
2031
+ { stdio: "inherit" }
2032
+ );
2033
+ process.exit(result.status ?? 1);
2034
+ }
1988
2035
  async function main() {
1989
2036
  if (process.argv[2] === "enrol" || process.argv[2] === "enroll") {
1990
2037
  await runEnrolment();
1991
2038
  }
2039
+ const subcommand = SUBCOMMANDS[process.argv[2] ?? ""];
2040
+ if (subcommand) await runSubcommand(subcommand);
1992
2041
  assertConfigured();
1993
2042
  const transport = new StdioServerTransport();
1994
2043
  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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "evrex-mcp",
3
- "version": "0.8.2",
3
+ "version": "0.9.0",
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/typescript-config": "0.0.0",
52
+ "@repo/llm-core": "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
+ }