evrex-mcp 0.8.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -63,7 +63,7 @@ var init_client = __esm({
63
63
  // Evidence-only retrieval (BM25 + embedding), no LLM synthesis call — see
64
64
  // apps/backend/src/query/query.service.ts#search. Used by evrex_search,
65
65
  // which wants ranked hits fast, not a synthesized paragraph.
66
- search: (repoPath, text, filePaths) => post("/search", { repoPath, text, filePaths }),
66
+ search: (repoPath, text, filePaths, boost) => post("/search", { repoPath, text, filePaths, boost }),
67
67
  // Everything that happened in a repo, newest first, bounded by days — the
68
68
  // same query the desktop Timeline screen makes. Sessions and commits
69
69
  // interleaved, each with the handle evrex_expand takes.
@@ -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
 
@@ -525,6 +536,29 @@ async function synthesizeAnswer(question, evidence, client2, options = {}) {
525
536
  };
526
537
  }
527
538
 
539
+ // ../../packages/llm-core/src/retrieval-boost.ts
540
+ var CORPUS = `The corpus is an engineering-memory index built from a team's coding-agent conversations and git history, retrieved with Postgres full-text OR-matching plus BM25 (exact word stems match; meanings do not). Searchable documents:
541
+ - Conversation turns: prose by the person and the agent, plus tool-call lines like "[tool_call: Edit] apps/backend/src/query/query.service.ts" \u2014 so file paths, function names, error strings and identifiers appear verbatim.
542
+ - Extracted insights: short titled records of decisions made, approaches REJECTED (with the reason), constraints, and procedures.
543
+ - Commit messages: conventional-commit subjects and explanatory bodies.`;
544
+ var EXPAND_SYSTEM = `You expand a search query into terms that will match an engineering record.
545
+
546
+ ${CORPUS}
547
+
548
+ Given a person's question, produce the search terms most likely to appear VERBATIM in that corpus but absent from the question itself: the identifiers an engineer would have typed (function, constant, env var, file names), the error text the failure would have printed, and the words the same idea takes in commit-message register ("fix the flaky test" \u2192 "flaky", "deflake", "retry", "timeout"). Prefer specific over general \u2014 "RLS" and "row level security" over "database permissions". Never invent identifiers you are not confident exist in this kind of codebase; a wrong specific term matches nothing and costs nothing, but fill the list with your best evidence-seeking guesses.
549
+
550
+ Respond with JSON only: {"terms": ["...", ...]} \u2014 3 to 8 terms, each 1 to 4 words, no duplicates of the question's own words.`;
551
+ var RERANK_SYSTEM = `You order search results for an engineer's question about their own project's history.
552
+
553
+ ${CORPUS}
554
+
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:
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.
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.
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.
559
+
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.`;
561
+
528
562
  // ../../packages/llm-core/src/cli-client.ts
529
563
  import { execFile } from "node:child_process";
530
564
  var TIMEOUT_MS = 12e4;
@@ -1635,6 +1669,28 @@ function proceduresBlock(procedures) {
1635
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}
1636
1670
  ${p.steps.slice(0, 12).map((step, i) => ` ${i + 1}. ${step}`).join("\n")}`).join("\n");
1637
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
+ }
1638
1694
  async function evrexExpand(handles) {
1639
1695
  if (handles.length === 0) return "No handles given. Pass ids from evrex_search, e.g. commit:069a0b5.";
1640
1696
  const parts = [];
@@ -1688,6 +1744,8 @@ Open in Evrex: evrex://open?handle=commit:${id}`);
1688
1744
  }
1689
1745
  const procedures = proceduresBlock(session.procedures ?? []);
1690
1746
  if (procedures) block.push(procedures);
1747
+ const fromSubagents = subagentsBlock(session.subagents ?? []);
1748
+ if (fromSubagents) block.push(fromSubagents);
1691
1749
  parts.push(block.join("\n"));
1692
1750
  }
1693
1751
  parts.push(
@@ -1806,6 +1864,8 @@ ${truncate(commit.body, MAX_EXCERPT)}` : ""}`
1806
1864
  "CONSTRAINTS:\n" + session.constraints.slice(0, MAX_ITEMS).map((c) => `- [${c.source}] ${c.text}`).join("\n")
1807
1865
  );
1808
1866
  }
1867
+ const fromSubagents = subagentsBlock(session.subagents ?? []);
1868
+ if (fromSubagents) parts.push(fromSubagents);
1809
1869
  parts.push(`OUTCOME: ${truncate(session.outcome, MAX_EXCERPT)}`);
1810
1870
  }
1811
1871
  } else if (!commit.agentTrailers?.length) {
@@ -1962,10 +2022,22 @@ async function runEnrolment() {
1962
2022
  });
1963
2023
  process.exit(result.ok ? 0 : 1);
1964
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
+ }
1965
2035
  async function main() {
1966
2036
  if (process.argv[2] === "enrol" || process.argv[2] === "enroll") {
1967
2037
  await runEnrolment();
1968
2038
  }
2039
+ const subcommand = SUBCOMMANDS[process.argv[2] ?? ""];
2040
+ if (subcommand) await runSubcommand(subcommand);
1969
2041
  assertConfigured();
1970
2042
  const transport = new StdioServerTransport();
1971
2043
  await server.connect(transport);
package/dist/pretool.js CHANGED
@@ -49,7 +49,7 @@ var evrexApi = {
49
49
  // Evidence-only retrieval (BM25 + embedding), no LLM synthesis call — see
50
50
  // apps/backend/src/query/query.service.ts#search. Used by evrex_search,
51
51
  // which wants ranked hits fast, not a synthesized paragraph.
52
- search: (repoPath, text, filePaths) => post("/search", { repoPath, text, filePaths }),
52
+ search: (repoPath, text, filePaths, boost) => post("/search", { repoPath, text, filePaths, boost }),
53
53
  // Everything that happened in a repo, newest first, bounded by days — the
54
54
  // same query the desktop Timeline screen makes. Sessions and commits
55
55
  // interleaved, each with the handle evrex_expand takes.
@@ -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
@@ -63,7 +63,7 @@ var init_client = __esm({
63
63
  // Evidence-only retrieval (BM25 + embedding), no LLM synthesis call — see
64
64
  // apps/backend/src/query/query.service.ts#search. Used by evrex_search,
65
65
  // which wants ranked hits fast, not a synthesized paragraph.
66
- search: (repoPath, text, filePaths) => post("/search", { repoPath, text, filePaths }),
66
+ search: (repoPath, text, filePaths, boost) => post("/search", { repoPath, text, filePaths, boost }),
67
67
  // Everything that happened in a repo, newest first, bounded by days — the
68
68
  // same query the desktop Timeline screen makes. Sessions and commits
69
69
  // interleaved, each with the handle evrex_expand takes.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "evrex-mcp",
3
- "version": "0.8.1",
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
+ }