evrex-mcp 0.8.0 → 0.8.2

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.
@@ -525,6 +525,29 @@ async function synthesizeAnswer(question, evidence, client2, options = {}) {
525
525
  };
526
526
  }
527
527
 
528
+ // ../../packages/llm-core/src/retrieval-boost.ts
529
+ 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:
530
+ - 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.
531
+ - Extracted insights: short titled records of decisions made, approaches REJECTED (with the reason), constraints, and procedures.
532
+ - Commit messages: conventional-commit subjects and explanatory bodies.`;
533
+ var EXPAND_SYSTEM = `You expand a search query into terms that will match an engineering record.
534
+
535
+ ${CORPUS}
536
+
537
+ 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.
538
+
539
+ Respond with JSON only: {"terms": ["...", ...]} \u2014 3 to 8 terms, each 1 to 4 words, no duplicates of the question's own words.`;
540
+ var RERANK_SYSTEM = `You order search results for an engineer's question about their own project's history.
541
+
542
+ ${CORPUS}
543
+
544
+ 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
+ - 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
+ - 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.
548
+
549
+ 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
+
528
551
  // ../../packages/llm-core/src/cli-client.ts
529
552
  import { execFile } from "node:child_process";
530
553
  var TIMEOUT_MS = 12e4;
@@ -646,6 +669,29 @@ ${request2.user}`;
646
669
  // ../../packages/llm-core/src/provider-clients.ts
647
670
  import Anthropic3 from "@anthropic-ai/sdk";
648
671
  var NOOP = { warn: () => void 0, error: () => void 0 };
672
+ var BATCH_POLL_MS = 5e3;
673
+ var BATCH_TIMEOUT_MS = 6 * 60 * 60 * 1e3;
674
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
675
+ function totalize(items, results) {
676
+ for (const item of items) if (!results.has(item.id)) results.set(item.id, null);
677
+ return results;
678
+ }
679
+ function totalizeIds(ids, results) {
680
+ for (const id of ids) if (!results.has(id)) results.set(id, null);
681
+ return results;
682
+ }
683
+ async function pollToCompletion(handle, resolve, sleepFn, onTimeout) {
684
+ const startedAt = Date.now();
685
+ for (; ; ) {
686
+ const resolved = await resolve(handle);
687
+ if (resolved) return resolved;
688
+ if (Date.now() - startedAt > BATCH_TIMEOUT_MS) {
689
+ onTimeout(handle.id);
690
+ return null;
691
+ }
692
+ await sleepFn(BATCH_POLL_MS);
693
+ }
694
+ }
649
695
  var realSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
650
696
  var MAX_RETRIES = 4;
651
697
  var RETRYABLE_STATUS = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
@@ -653,12 +699,12 @@ function isExhaustedStatus(status, retriesSpent) {
653
699
  if (status === 401 || status === 403) return true;
654
700
  return status === 429 && retriesSpent;
655
701
  }
656
- async function waitBeforeRetry(response, attempt, sleep) {
702
+ async function waitBeforeRetry(response, attempt, sleep2) {
657
703
  const header = response.headers.get("retry-after") ?? response.headers.get("x-ratelimit-reset-tokens");
658
704
  const seconds = header ? parseDuration(header) : null;
659
705
  const backoff = Math.min(8 * 2 ** attempt, 60);
660
706
  const wait = Math.min(seconds ?? backoff, MAX_RETRY_WAIT_SECONDS);
661
- await sleep(wait * 1e3);
707
+ await sleep2(wait * 1e3);
662
708
  }
663
709
  var MAX_RETRY_WAIT_SECONDS = 90;
664
710
  function parseDuration(value) {
@@ -678,7 +724,7 @@ function parseJsonBody(text) {
678
724
  return null;
679
725
  }
680
726
  }
681
- var AnthropicClient = class {
727
+ var AnthropicClient = class _AnthropicClient {
682
728
  constructor(key, model, logger) {
683
729
  this.model = model;
684
730
  this.logger = logger;
@@ -687,37 +733,47 @@ var AnthropicClient = class {
687
733
  provider = "anthropic";
688
734
  exhausted = false;
689
735
  sdk;
736
+ // The one place a ModelRequest becomes Anthropic's message params, so the
737
+ // single and batch paths cannot drift on the cache breakpoint or the
738
+ // json_schema wiring.
739
+ messageParams(request2) {
740
+ return {
741
+ model: request2.model ?? this.model,
742
+ max_tokens: request2.maxTokens ?? DEFAULT_MAX_TOKENS,
743
+ // The breakpoint sits at the end of the system prompt, which is the
744
+ // only part of these requests that repeats. Rendering order is
745
+ // tools -> system -> messages, so a marker here caches everything
746
+ // ahead of the transcript.
747
+ //
748
+ // Deliberately NOT on the user block. That block is a different
749
+ // transcript chunk on every call, so a breakpoint there would write a
750
+ // fresh cache entry per request and read none of them back — paying
751
+ // the write premium for nothing, which is worse than not caching.
752
+ system: [
753
+ {
754
+ type: "text",
755
+ text: request2.system,
756
+ cache_control: { type: "ephemeral" }
757
+ }
758
+ ],
759
+ messages: [{ role: "user", content: request2.user }],
760
+ ...request2.schema ? {
761
+ output_config: {
762
+ format: { type: "json_schema", schema: request2.schema }
763
+ }
764
+ } : {}
765
+ };
766
+ }
767
+ static messageText(message) {
768
+ return message.content.filter((b) => b.type === "text").map((b) => b.text).join("");
769
+ }
690
770
  async completeJson(request2) {
691
771
  try {
692
- const message = await this.sdk.messages.create({
693
- model: request2.model ?? this.model,
694
- max_tokens: request2.maxTokens ?? DEFAULT_MAX_TOKENS,
695
- // The breakpoint sits at the end of the system prompt, which is the
696
- // only part of these requests that repeats. Rendering order is
697
- // tools -> system -> messages, so a marker here caches everything
698
- // ahead of the transcript.
699
- //
700
- // Deliberately NOT on the user block. That block is a different
701
- // transcript chunk on every call, so a breakpoint there would write a
702
- // fresh cache entry per request and read none of them back — paying
703
- // the write premium for nothing, which is worse than not caching.
704
- system: [
705
- {
706
- type: "text",
707
- text: request2.system,
708
- cache_control: { type: "ephemeral" }
709
- }
710
- ],
711
- messages: [{ role: "user", content: request2.user }],
712
- ...request2.schema ? {
713
- output_config: {
714
- format: { type: "json_schema", schema: request2.schema }
715
- }
716
- } : {}
717
- });
772
+ const message = await this.sdk.messages.create(
773
+ this.messageParams(request2)
774
+ );
718
775
  this.reportCacheUsage(message.usage);
719
- const text = message.content.filter((b) => b.type === "text").map((b) => b.text).join("");
720
- return parseJsonBody(text);
776
+ return parseJsonBody(_AnthropicClient.messageText(message));
721
777
  } catch (err) {
722
778
  const status = err?.status;
723
779
  if (typeof status === "number" && isExhaustedStatus(status, true)) {
@@ -727,6 +783,58 @@ var AnthropicClient = class {
727
783
  return null;
728
784
  }
729
785
  }
786
+ async submitBatch(items) {
787
+ if (items.length === 0) return null;
788
+ try {
789
+ const created = await this.sdk.messages.batches.create({
790
+ requests: items.map((item) => ({
791
+ custom_id: item.id,
792
+ params: this.messageParams(item.request)
793
+ }))
794
+ });
795
+ return { provider: "anthropic", id: created.id, itemIds: items.map((i) => i.id) };
796
+ } catch (err) {
797
+ const status = err?.status;
798
+ if (typeof status === "number" && isExhaustedStatus(status, true)) {
799
+ this.exhausted = true;
800
+ }
801
+ this.logger.warn(`anthropic batch submit failed: ${describeError(err)}`);
802
+ return null;
803
+ }
804
+ }
805
+ async resolveBatch(handle) {
806
+ try {
807
+ const batch = await this.sdk.messages.batches.retrieve(handle.id);
808
+ if (batch.processing_status !== "ended") return null;
809
+ const results = /* @__PURE__ */ new Map();
810
+ for await (const entry of await this.sdk.messages.batches.results(
811
+ handle.id
812
+ )) {
813
+ results.set(
814
+ entry.custom_id,
815
+ entry.result.type === "succeeded" ? parseJsonBody(_AnthropicClient.messageText(entry.result.message)) : null
816
+ );
817
+ }
818
+ return totalizeIds(handle.itemIds, results);
819
+ } catch (err) {
820
+ this.logger.warn(`anthropic batch resolve failed: ${describeError(err)}`);
821
+ return totalizeIds(handle.itemIds, /* @__PURE__ */ new Map());
822
+ }
823
+ }
824
+ async completeJsonBatch(items) {
825
+ if (items.length === 0) return /* @__PURE__ */ new Map();
826
+ const handle = await this.submitBatch(items);
827
+ if (!handle) return totalize(items, /* @__PURE__ */ new Map());
828
+ const resolved = await pollToCompletion(
829
+ handle,
830
+ (h) => this.resolveBatch(h),
831
+ sleep,
832
+ (id) => this.logger.warn(
833
+ `anthropic batch ${id} did not finish within the cap; the sessions it covers stay unextracted until the next pass`
834
+ )
835
+ );
836
+ return resolved ?? totalize(items, /* @__PURE__ */ new Map());
837
+ }
730
838
  /**
731
839
  * Says out loud whether the cache was actually used.
732
840
  *
@@ -753,12 +861,12 @@ var AnthropicClient = class {
753
861
  );
754
862
  }
755
863
  };
756
- var GoogleClient = class {
757
- constructor(key, model, logger, sleep = realSleep) {
864
+ var GoogleClient = class _GoogleClient {
865
+ constructor(key, model, logger, sleep2 = realSleep) {
758
866
  this.key = key;
759
867
  this.model = model;
760
868
  this.logger = logger;
761
- this.sleep = sleep;
869
+ this.sleep = sleep2;
762
870
  }
763
871
  provider = "google";
764
872
  exhausted = false;
@@ -801,56 +909,129 @@ ${JSON.stringify(request2.schema)}` : "";
801
909
  return null;
802
910
  }
803
911
  }
912
+ static GEMINI_BASE = "https://generativelanguage.googleapis.com/v1beta";
913
+ async submitBatch(items) {
914
+ if (items.length === 0) return null;
915
+ const model = items[0].request.model ?? this.model;
916
+ try {
917
+ const create = await fetch(
918
+ `${_GoogleClient.GEMINI_BASE}/models/${encodeURIComponent(model)}:batchGenerateContent`,
919
+ {
920
+ method: "POST",
921
+ headers: {
922
+ "Content-Type": "application/json",
923
+ "x-goog-api-key": this.key
924
+ },
925
+ body: JSON.stringify({
926
+ batch: {
927
+ display_name: "evrex-extraction",
928
+ input_config: {
929
+ requests: {
930
+ requests: items.map((item) => {
931
+ const shape = item.request.schema ? `
932
+
933
+ Respond with JSON matching exactly this schema:
934
+ ${JSON.stringify(item.request.schema)}` : "";
935
+ return {
936
+ request: {
937
+ systemInstruction: {
938
+ parts: [{ text: item.request.system + shape }]
939
+ },
940
+ contents: [
941
+ { role: "user", parts: [{ text: item.request.user }] }
942
+ ],
943
+ generationConfig: { responseMimeType: "application/json" }
944
+ },
945
+ metadata: { key: item.id }
946
+ };
947
+ })
948
+ }
949
+ }
950
+ }
951
+ })
952
+ }
953
+ );
954
+ if (!create.ok) {
955
+ if (isExhaustedStatus(create.status, true)) this.exhausted = true;
956
+ this.logger.warn(
957
+ `google batch submit failed: ${create.status} ${(await create.text()).slice(0, 200)}`
958
+ );
959
+ return null;
960
+ }
961
+ const name = (await create.json()).name;
962
+ if (!name) {
963
+ this.logger.warn("google batch: no job name returned");
964
+ return null;
965
+ }
966
+ return { provider: "google", id: name, itemIds: items.map((i) => i.id) };
967
+ } catch (err) {
968
+ this.logger.warn(`google batch submit failed: ${describeError(err)}`);
969
+ return null;
970
+ }
971
+ }
972
+ async resolveBatch(handle) {
973
+ try {
974
+ const poll = await fetch(`${_GoogleClient.GEMINI_BASE}/${handle.id}`, {
975
+ headers: { "x-goog-api-key": this.key }
976
+ });
977
+ if (!poll.ok) {
978
+ this.logger.warn(`google batch poll failed: ${poll.status}`);
979
+ return totalizeIds(handle.itemIds, /* @__PURE__ */ new Map());
980
+ }
981
+ const job = await poll.json();
982
+ const state = job.state ?? "";
983
+ if (state === "JOB_STATE_FAILED" || state === "JOB_STATE_CANCELLED" || state === "JOB_STATE_EXPIRED") {
984
+ this.logger.warn(`google batch ${handle.id} ended ${state}`);
985
+ return totalizeIds(handle.itemIds, /* @__PURE__ */ new Map());
986
+ }
987
+ if (state !== "JOB_STATE_SUCCEEDED") return null;
988
+ const inlined = job.response?.inlinedResponses ?? [];
989
+ const results = /* @__PURE__ */ new Map();
990
+ handle.itemIds.forEach((id, i) => {
991
+ const text = inlined[i]?.response?.candidates?.[0]?.content?.parts?.map((p) => p.text ?? "").join("");
992
+ results.set(id, text ? parseJsonBody(text) : null);
993
+ });
994
+ return totalizeIds(handle.itemIds, results);
995
+ } catch (err) {
996
+ this.logger.warn(`google batch resolve failed: ${describeError(err)}`);
997
+ return totalizeIds(handle.itemIds, /* @__PURE__ */ new Map());
998
+ }
999
+ }
1000
+ async completeJsonBatch(items) {
1001
+ if (items.length === 0) return /* @__PURE__ */ new Map();
1002
+ const handle = await this.submitBatch(items);
1003
+ if (!handle) return totalize(items, /* @__PURE__ */ new Map());
1004
+ const resolved = await pollToCompletion(
1005
+ handle,
1006
+ (h) => this.resolveBatch(h),
1007
+ this.sleep,
1008
+ (id) => this.logger.warn(
1009
+ `google batch ${id} did not finish within the cap; leaving its sessions for a later pass`
1010
+ )
1011
+ );
1012
+ return resolved ?? totalize(items, /* @__PURE__ */ new Map());
1013
+ }
804
1014
  };
805
1015
  var OpenAiCompatibleClient = class {
806
- constructor(provider, baseUrl, key, model, logger, sleep = realSleep) {
1016
+ constructor(provider, baseUrl, key, model, logger, sleep2 = realSleep) {
807
1017
  this.provider = provider;
808
1018
  this.baseUrl = baseUrl;
809
1019
  this.key = key;
810
1020
  this.model = model;
811
1021
  this.logger = logger;
812
- this.sleep = sleep;
1022
+ this.sleep = sleep2;
1023
+ if (this.provider === "openai") {
1024
+ this.completeJsonBatch = (items) => this.runBatch(items);
1025
+ }
813
1026
  }
814
1027
  exhausted = false;
1028
+ completeJsonBatch;
815
1029
  async completeJson(request2, attempt = 0) {
816
- const strict = this.provider === "openai";
817
- const shape = request2.schema && !strict ? `
818
-
819
- Respond with JSON matching exactly this schema:
820
- ${JSON.stringify(request2.schema)}` : "";
821
1030
  try {
822
1031
  const res = await fetch(`${this.baseUrl}/chat/completions`, {
823
1032
  method: "POST",
824
- headers: {
825
- "Content-Type": "application/json",
826
- // A local server does not want one, and some reject an empty bearer
827
- // token outright.
828
- ...this.key ? { Authorization: `Bearer ${this.key}` } : {}
829
- },
830
- body: JSON.stringify({
831
- model: request2.model ?? this.model,
832
- messages: [
833
- { role: "system", content: request2.system + shape },
834
- { role: "user", content: request2.user }
835
- ],
836
- ...request2.schema && strict ? {
837
- response_format: {
838
- type: "json_schema",
839
- json_schema: {
840
- name: "evrex_result",
841
- strict: true,
842
- schema: request2.schema
843
- }
844
- }
845
- } : { response_format: { type: "json_object" } },
846
- // Extraction is a reading task with a right answer, not a writing
847
- // task, and a default sampling temperature makes it a different
848
- // answer each run: one model scored 53% and then 20% on the same ten
849
- // sessions before this was pinned. Indexing twice must not produce
850
- // two different histories.
851
- temperature: 0,
852
- stream: false
853
- })
1033
+ headers: this.authHeaders(),
1034
+ body: JSON.stringify(this.chatBody(request2))
854
1035
  });
855
1036
  if (RETRYABLE_STATUS.has(res.status) && attempt < MAX_RETRIES) {
856
1037
  await waitBeforeRetry(res, attempt, this.sleep);
@@ -871,6 +1052,147 @@ ${JSON.stringify(request2.schema)}` : "";
871
1052
  return null;
872
1053
  }
873
1054
  }
1055
+ authHeaders() {
1056
+ return {
1057
+ "Content-Type": "application/json",
1058
+ // A local server does not want one, and some reject an empty bearer token.
1059
+ ...this.key ? { Authorization: `Bearer ${this.key}` } : {}
1060
+ };
1061
+ }
1062
+ // One place a ModelRequest becomes the chat body, so completeJson and the
1063
+ // batch path cannot drift on response_format or the pinned temperature.
1064
+ chatBody(request2) {
1065
+ const strict = this.provider === "openai";
1066
+ const shape = request2.schema && !strict ? `
1067
+
1068
+ Respond with JSON matching exactly this schema:
1069
+ ${JSON.stringify(request2.schema)}` : "";
1070
+ return {
1071
+ model: request2.model ?? this.model,
1072
+ messages: [
1073
+ { role: "system", content: request2.system + shape },
1074
+ { role: "user", content: request2.user }
1075
+ ],
1076
+ ...request2.schema && strict ? {
1077
+ response_format: {
1078
+ type: "json_schema",
1079
+ json_schema: {
1080
+ name: "evrex_result",
1081
+ strict: true,
1082
+ schema: request2.schema
1083
+ }
1084
+ }
1085
+ } : { response_format: { type: "json_object" } },
1086
+ // A reading task with a right answer: temperature pinned so indexing
1087
+ // twice is the same history.
1088
+ temperature: 0,
1089
+ stream: false
1090
+ };
1091
+ }
1092
+ async runBatch(items) {
1093
+ const results = /* @__PURE__ */ new Map();
1094
+ if (items.length === 0) return results;
1095
+ const auth = this.key ? { Authorization: `Bearer ${this.key}` } : {};
1096
+ try {
1097
+ const jsonl = items.map(
1098
+ (item) => JSON.stringify({
1099
+ custom_id: item.id,
1100
+ method: "POST",
1101
+ url: "/v1/chat/completions",
1102
+ body: this.chatBody(item.request)
1103
+ })
1104
+ ).join("\n");
1105
+ const form = new FormData();
1106
+ form.append("purpose", "batch");
1107
+ form.append(
1108
+ "file",
1109
+ new Blob([jsonl], { type: "application/jsonl" }),
1110
+ "evrex-batch.jsonl"
1111
+ );
1112
+ const upload = await fetch(`${this.baseUrl}/files`, {
1113
+ method: "POST",
1114
+ headers: auth,
1115
+ body: form
1116
+ });
1117
+ if (!upload.ok) {
1118
+ if (isExhaustedStatus(upload.status, true)) this.exhausted = true;
1119
+ this.logger.warn(
1120
+ `openai batch upload failed: ${upload.status} ${(await upload.text()).slice(0, 200)}`
1121
+ );
1122
+ return totalize(items, results);
1123
+ }
1124
+ const fileId = (await upload.json()).id;
1125
+ if (!fileId) return totalize(items, results);
1126
+ const created = await fetch(`${this.baseUrl}/batches`, {
1127
+ method: "POST",
1128
+ headers: this.authHeaders(),
1129
+ body: JSON.stringify({
1130
+ input_file_id: fileId,
1131
+ endpoint: "/v1/chat/completions",
1132
+ completion_window: "24h"
1133
+ })
1134
+ });
1135
+ if (!created.ok) {
1136
+ this.logger.warn(
1137
+ `openai batch create failed: ${created.status} ${(await created.text()).slice(0, 200)}`
1138
+ );
1139
+ return totalize(items, results);
1140
+ }
1141
+ const batchId = (await created.json()).id;
1142
+ if (!batchId) return totalize(items, results);
1143
+ let status = "";
1144
+ let outputFileId;
1145
+ const startedAt = Date.now();
1146
+ while (status !== "completed") {
1147
+ if (Date.now() - startedAt > BATCH_TIMEOUT_MS) {
1148
+ this.logger.warn(
1149
+ `openai batch ${batchId} did not finish within the cap; leaving its sessions for a later pass`
1150
+ );
1151
+ return totalize(items, results);
1152
+ }
1153
+ await this.sleep(BATCH_POLL_MS);
1154
+ const poll = await fetch(`${this.baseUrl}/batches/${batchId}`, {
1155
+ headers: auth
1156
+ });
1157
+ if (!poll.ok) {
1158
+ this.logger.warn(`openai batch poll failed: ${poll.status}`);
1159
+ return totalize(items, results);
1160
+ }
1161
+ const job = await poll.json();
1162
+ status = job.status ?? "";
1163
+ outputFileId = job.output_file_id;
1164
+ if (status === "failed" || status === "expired" || status === "cancelled") {
1165
+ this.logger.warn(`openai batch ${batchId} ended ${status}`);
1166
+ return totalize(items, results);
1167
+ }
1168
+ }
1169
+ if (!outputFileId) return totalize(items, results);
1170
+ const out = await fetch(`${this.baseUrl}/files/${outputFileId}/content`, {
1171
+ headers: auth
1172
+ });
1173
+ if (!out.ok) {
1174
+ this.logger.warn(`openai batch results fetch failed: ${out.status}`);
1175
+ return totalize(items, results);
1176
+ }
1177
+ for (const line of (await out.text()).split("\n")) {
1178
+ if (!line.trim()) continue;
1179
+ try {
1180
+ const row = JSON.parse(line);
1181
+ const content = row.response?.body?.choices?.[0]?.message?.content;
1182
+ if (row.custom_id) {
1183
+ results.set(
1184
+ row.custom_id,
1185
+ typeof content === "string" ? parseJsonBody(content) : null
1186
+ );
1187
+ }
1188
+ } catch {
1189
+ }
1190
+ }
1191
+ } catch (err) {
1192
+ this.logger.warn(`openai batch failed: ${describeError(err)}`);
1193
+ }
1194
+ return totalize(items, results);
1195
+ }
874
1196
  };
875
1197
  function describeError(err) {
876
1198
  return err instanceof Error ? err.message : String(err);
@@ -882,9 +1204,9 @@ function createModelClient(key, options = {}) {
882
1204
  const model = options.model ?? DEFAULT_MODELS[provider];
883
1205
  const trimmed = key.trim();
884
1206
  if (provider === "anthropic") return new AnthropicClient(trimmed, model, logger);
885
- const sleep = options.sleep ?? realSleep;
1207
+ const sleep2 = options.sleep ?? realSleep;
886
1208
  if (provider === "google")
887
- return new GoogleClient(trimmed, model, logger, sleep);
1209
+ return new GoogleClient(trimmed, model, logger, sleep2);
888
1210
  const cli = CLI_SPECS[provider];
889
1211
  if (cli) return new CliClient(cli, model, logger);
890
1212
  if (provider === "local") {
@@ -894,7 +1216,7 @@ function createModelClient(key, options = {}) {
894
1216
  null,
895
1217
  model,
896
1218
  logger,
897
- sleep
1219
+ sleep2
898
1220
  );
899
1221
  }
900
1222
  const baseUrl = options.baseUrl ?? OPENAI_COMPATIBLE_BASE_URLS[provider];
@@ -905,7 +1227,7 @@ function createModelClient(key, options = {}) {
905
1227
  trimmed,
906
1228
  model,
907
1229
  logger,
908
- sleep
1230
+ sleep2
909
1231
  );
910
1232
  }
911
1233
 
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.
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.0",
3
+ "version": "0.8.2",
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,27 +36,28 @@
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
+ },
39
48
  "dependencies": {
40
49
  "@anthropic-ai/sdk": "^0.72.0",
41
50
  "@modelcontextprotocol/sdk": "^1.20.1",
42
51
  "zod": "^3.24.1"
43
52
  },
44
53
  "devDependencies": {
54
+ "@repo/eslint-config": "workspace:*",
55
+ "@repo/ingest-core": "workspace:*",
56
+ "@repo/llm-core": "workspace:*",
57
+ "@repo/typescript-config": "workspace:*",
45
58
  "@types/node": "^24.0.0",
46
59
  "esbuild": "^0.25.12",
47
60
  "eslint": "^9.39.1",
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"
61
+ "typescript": "^5.9.2"
61
62
  }
62
- }
63
+ }