@prom.codes/context-mcp 0.2.2 → 0.3.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.
Files changed (2) hide show
  1. package/dist/bin.js +465 -5
  2. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -3275,15 +3275,30 @@ var EMBEDDING_DRIFT_CODE = "EMBEDDING_DRIFT";
3275
3275
  function hasLazyIdentity(embedder) {
3276
3276
  return typeof embedder.resolveIdentity === "function";
3277
3277
  }
3278
+ var EMBED_BODY_CHARS = 1500;
3279
+ var EMBED_TEXT_VERSION = 2;
3278
3280
  function chunkTextForSymbol(sym) {
3279
3281
  const container = sym.container === null ? "" : ` in ${sym.container}`;
3280
3282
  const exported = sym.exported ? "exported " : "";
3281
- return `${exported}${sym.kind} ${sym.name}${container} (${sym.language})`;
3283
+ const descriptor = `${exported}${sym.kind} ${sym.name}${container} (${sym.language})`;
3284
+ const body = sym.body?.trim();
3285
+ if (!body)
3286
+ return descriptor;
3287
+ const slice = body.length > EMBED_BODY_CHARS ? body.slice(0, EMBED_BODY_CHARS) : body;
3288
+ return `${descriptor}
3289
+ ${sym.filePath}
3290
+ ${slice}`;
3282
3291
  }
3283
3292
  async function embedWorkspace(storage, embedder, options = {}) {
3284
3293
  if (hasLazyIdentity(embedder)) {
3285
3294
  await embedder.resolveIdentity(options.signal);
3286
3295
  }
3296
+ {
3297
+ const existing = await storage.getEmbeddingMeta();
3298
+ if (existing !== null && (existing.embedTextVersion ?? 1) !== EMBED_TEXT_VERSION && existing.provider === embedder.name && existing.model === embedder.model && existing.dim === embedder.dimension) {
3299
+ await storage.clearEmbeddings();
3300
+ }
3301
+ }
3287
3302
  try {
3288
3303
  return await runEmbedPass(storage, embedder, options, false);
3289
3304
  } catch (err) {
@@ -3301,7 +3316,8 @@ async function runEmbedPass(storage, embedder, options, driftRecovered) {
3301
3316
  provider: embedder.name,
3302
3317
  model: embedder.model,
3303
3318
  dim: embedder.dimension,
3304
- region: embedder.region
3319
+ region: embedder.region,
3320
+ embedTextVersion: EMBED_TEXT_VERSION
3305
3321
  };
3306
3322
  await storage.setEmbeddingMeta(meta);
3307
3323
  const rows = [];
@@ -3391,6 +3407,13 @@ var EDGE_TYPES = [
3391
3407
  "co-change"
3392
3408
  ];
3393
3409
 
3410
+ // ../shared/dist/hyde-prompt.js
3411
+ function renderHydePrompt(query) {
3412
+ return `You are a code-retrieval assistant. Given the user query, emit a single hypothetical code snippet (no prose, no markdown fences) that would directly answer it. Use the most common language for the intent. Keep it \u2264 25 lines.
3413
+
3414
+ Query: ${query}`;
3415
+ }
3416
+
3394
3417
  // ../shared/dist/sensitive-paths.js
3395
3418
  var SENSITIVE_DIRECTORIES = /* @__PURE__ */ new Set([
3396
3419
  "secrets",
@@ -4113,6 +4136,7 @@ function rowToStoredSymbol(row) {
4113
4136
  language: row.language,
4114
4137
  container: row.container,
4115
4138
  exported: row.exported === 1,
4139
+ body: row.body ?? void 0,
4116
4140
  range: {
4117
4141
  start: { row: row.start_row, column: row.start_col },
4118
4142
  end: { row: row.end_row, column: row.end_col },
@@ -7531,6 +7555,344 @@ var OpenAICompatRerankProvider = class {
7531
7555
  }
7532
7556
  };
7533
7557
 
7558
+ // ../rewriter-gemini/dist/index.js
7559
+ var DEFAULT_MODEL4 = "gemini-2.5-flash";
7560
+ var DEFAULT_BASE_URL2 = "https://generativelanguage.googleapis.com/v1beta";
7561
+ var DEFAULT_RETRIES7 = 6;
7562
+ var DEFAULT_BACKOFF7 = 2e3;
7563
+ var DEFAULT_RETRY_MAX4 = 6e4;
7564
+ var DEFAULT_MAX_TOKENS = 256;
7565
+ function parseRetryAfterMs4(value, now = Date.now()) {
7566
+ if (value === null)
7567
+ return null;
7568
+ const trimmed = value.trim();
7569
+ if (trimmed === "")
7570
+ return null;
7571
+ if (/^[0-9]+(\.[0-9]+)?$/.test(trimmed)) {
7572
+ const secs = Number(trimmed);
7573
+ if (!Number.isFinite(secs) || secs < 0)
7574
+ return null;
7575
+ return Math.round(secs * 1e3);
7576
+ }
7577
+ if (!/[A-Za-z]/.test(trimmed))
7578
+ return null;
7579
+ const ts = Date.parse(trimmed);
7580
+ if (!Number.isFinite(ts))
7581
+ return null;
7582
+ const delta = ts - now;
7583
+ return delta > 0 ? delta : 0;
7584
+ }
7585
+ function sleep7(ms, signal) {
7586
+ return new Promise((resolve6, reject) => {
7587
+ if (signal?.aborted === true) {
7588
+ reject(new Error("aborted"));
7589
+ return;
7590
+ }
7591
+ const timer = setTimeout(() => {
7592
+ signal?.removeEventListener("abort", onAbort);
7593
+ resolve6();
7594
+ }, ms);
7595
+ const onAbort = () => {
7596
+ clearTimeout(timer);
7597
+ reject(new Error("aborted"));
7598
+ };
7599
+ signal?.addEventListener("abort", onAbort, { once: true });
7600
+ });
7601
+ }
7602
+ function nonRetryable7(message) {
7603
+ const err = new Error(message);
7604
+ err.nonRetryable = true;
7605
+ return err;
7606
+ }
7607
+ var GeminiQueryRewriter = class {
7608
+ name;
7609
+ model;
7610
+ region;
7611
+ #baseUrl;
7612
+ #apiKey;
7613
+ #maxRetries;
7614
+ #retryBaseMs;
7615
+ #retryMaxMs;
7616
+ #fetch;
7617
+ constructor(opts) {
7618
+ if (typeof opts.apiKey !== "string" || opts.apiKey === "") {
7619
+ throw new Error("GeminiQueryRewriter: apiKey is required");
7620
+ }
7621
+ this.model = opts.model ?? DEFAULT_MODEL4;
7622
+ this.name = opts.name ?? `gemini:${this.model}`;
7623
+ this.region = opts.region ?? "us";
7624
+ this.#baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL2).replace(/\/+$/, "");
7625
+ this.#apiKey = opts.apiKey;
7626
+ this.#maxRetries = opts.maxRetries ?? DEFAULT_RETRIES7;
7627
+ this.#retryBaseMs = opts.retryBaseMs ?? DEFAULT_BACKOFF7;
7628
+ this.#retryMaxMs = opts.retryMaxMs ?? DEFAULT_RETRY_MAX4;
7629
+ this.#fetch = opts.fetch ?? fetch;
7630
+ }
7631
+ async rewrite(query, opts) {
7632
+ const trimmed = query.trim();
7633
+ if (trimmed === "")
7634
+ return [];
7635
+ const n = opts?.n ?? 1;
7636
+ if (!Number.isInteger(n) || n < 1)
7637
+ return [];
7638
+ const maxTokens = opts?.maxTokens ?? DEFAULT_MAX_TOKENS;
7639
+ const temperature = opts?.temperature ?? 0;
7640
+ const signal = opts?.signal;
7641
+ const body = {
7642
+ contents: [
7643
+ {
7644
+ role: "user",
7645
+ parts: [{ text: renderHydePrompt(trimmed) }]
7646
+ }
7647
+ ],
7648
+ generationConfig: {
7649
+ temperature,
7650
+ maxOutputTokens: maxTokens,
7651
+ candidateCount: n
7652
+ }
7653
+ };
7654
+ try {
7655
+ const payload = await this.#postWithRetry(body, signal);
7656
+ return this.#decode(payload, n);
7657
+ } catch {
7658
+ return [];
7659
+ }
7660
+ }
7661
+ async #postWithRetry(body, signal) {
7662
+ const url = `${this.#baseUrl}/models/${encodeURIComponent(this.model)}:generateContent`;
7663
+ const init = {
7664
+ method: "POST",
7665
+ headers: {
7666
+ "content-type": "application/json",
7667
+ "x-goog-api-key": this.#apiKey
7668
+ },
7669
+ body: JSON.stringify(body)
7670
+ };
7671
+ if (signal !== void 0)
7672
+ init.signal = signal;
7673
+ let attempt = 0;
7674
+ let lastError = null;
7675
+ while (attempt <= this.#maxRetries) {
7676
+ try {
7677
+ const res = await this.#fetch(url, init);
7678
+ if (res.status === 429 || res.status >= 500 && res.status < 600) {
7679
+ lastError = new Error(`${this.name}: HTTP ${res.status}`);
7680
+ attempt += 1;
7681
+ if (attempt > this.#maxRetries)
7682
+ break;
7683
+ const backoff = this.#computeBackoff(attempt, res.headers.get("retry-after"));
7684
+ await sleep7(backoff, signal);
7685
+ continue;
7686
+ }
7687
+ if (!res.ok) {
7688
+ const text = await res.text().catch(() => "");
7689
+ throw nonRetryable7(`${this.name}: HTTP ${res.status} ${res.statusText}` + (text === "" ? "" : ` \u2014 ${text}`));
7690
+ }
7691
+ return await res.json();
7692
+ } catch (err) {
7693
+ if (err?.name === "AbortError")
7694
+ throw err;
7695
+ if (err?.nonRetryable === true) {
7696
+ throw err;
7697
+ }
7698
+ if (attempt >= this.#maxRetries)
7699
+ throw err;
7700
+ lastError = err;
7701
+ attempt += 1;
7702
+ await sleep7(this.#computeBackoff(attempt, null), signal);
7703
+ }
7704
+ }
7705
+ throw lastError instanceof Error ? lastError : new Error(`${this.name}: exhausted ${this.#maxRetries} retries`);
7706
+ }
7707
+ #computeBackoff(attempt, retryAfterHeader) {
7708
+ const exp = this.#retryBaseMs * 2 ** Math.max(0, attempt - 1);
7709
+ const advised = parseRetryAfterMs4(retryAfterHeader);
7710
+ const lower = advised === null ? exp : Math.max(exp, advised);
7711
+ return Math.min(lower, this.#retryMaxMs);
7712
+ }
7713
+ #decode(payload, requested) {
7714
+ const candidates = payload.candidates ?? [];
7715
+ const out = [];
7716
+ for (const cand of candidates) {
7717
+ const parts = cand.content?.parts ?? [];
7718
+ const text = parts.map((p) => typeof p.text === "string" ? p.text : "").join("");
7719
+ const trimmed = text.trim();
7720
+ if (trimmed !== "")
7721
+ out.push(trimmed);
7722
+ if (out.length >= requested)
7723
+ break;
7724
+ }
7725
+ return out;
7726
+ }
7727
+ };
7728
+
7729
+ // ../rewriter-mistral/dist/index.js
7730
+ var DEFAULT_MODEL5 = "mistral-small-latest";
7731
+ var DEFAULT_BASE_URL3 = "https://api.mistral.ai/v1";
7732
+ var DEFAULT_RETRIES8 = 6;
7733
+ var DEFAULT_BACKOFF8 = 2e3;
7734
+ var DEFAULT_RETRY_MAX5 = 6e4;
7735
+ var DEFAULT_MAX_TOKENS2 = 256;
7736
+ function parseRetryAfterMs5(value, now = Date.now()) {
7737
+ if (value === null)
7738
+ return null;
7739
+ const trimmed = value.trim();
7740
+ if (trimmed === "")
7741
+ return null;
7742
+ if (/^[0-9]+(\.[0-9]+)?$/.test(trimmed)) {
7743
+ const secs = Number(trimmed);
7744
+ if (!Number.isFinite(secs) || secs < 0)
7745
+ return null;
7746
+ return Math.round(secs * 1e3);
7747
+ }
7748
+ if (!/[A-Za-z]/.test(trimmed))
7749
+ return null;
7750
+ const ts = Date.parse(trimmed);
7751
+ if (!Number.isFinite(ts))
7752
+ return null;
7753
+ const delta = ts - now;
7754
+ return delta > 0 ? delta : 0;
7755
+ }
7756
+ function sleep8(ms, signal) {
7757
+ return new Promise((resolve6, reject) => {
7758
+ if (signal?.aborted === true) {
7759
+ reject(new Error("aborted"));
7760
+ return;
7761
+ }
7762
+ const timer = setTimeout(() => {
7763
+ signal?.removeEventListener("abort", onAbort);
7764
+ resolve6();
7765
+ }, ms);
7766
+ const onAbort = () => {
7767
+ clearTimeout(timer);
7768
+ reject(new Error("aborted"));
7769
+ };
7770
+ signal?.addEventListener("abort", onAbort, { once: true });
7771
+ });
7772
+ }
7773
+ function nonRetryable8(message) {
7774
+ const err = new Error(message);
7775
+ err.nonRetryable = true;
7776
+ return err;
7777
+ }
7778
+ var MistralQueryRewriter = class {
7779
+ name;
7780
+ model;
7781
+ region;
7782
+ #baseUrl;
7783
+ #apiKey;
7784
+ #maxRetries;
7785
+ #retryBaseMs;
7786
+ #retryMaxMs;
7787
+ #fetch;
7788
+ constructor(opts) {
7789
+ if (typeof opts.apiKey !== "string" || opts.apiKey === "") {
7790
+ throw new Error("MistralQueryRewriter: apiKey is required");
7791
+ }
7792
+ this.model = opts.model ?? DEFAULT_MODEL5;
7793
+ this.name = opts.name ?? `mistral:${this.model}`;
7794
+ this.region = opts.region ?? "eu";
7795
+ this.#baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL3).replace(/\/+$/, "");
7796
+ this.#apiKey = opts.apiKey;
7797
+ this.#maxRetries = opts.maxRetries ?? DEFAULT_RETRIES8;
7798
+ this.#retryBaseMs = opts.retryBaseMs ?? DEFAULT_BACKOFF8;
7799
+ this.#retryMaxMs = opts.retryMaxMs ?? DEFAULT_RETRY_MAX5;
7800
+ this.#fetch = opts.fetch ?? fetch;
7801
+ }
7802
+ async rewrite(query, opts) {
7803
+ const trimmed = query.trim();
7804
+ if (trimmed === "")
7805
+ return [];
7806
+ const n = opts?.n ?? 1;
7807
+ if (!Number.isInteger(n) || n < 1)
7808
+ return [];
7809
+ const maxTokens = opts?.maxTokens ?? DEFAULT_MAX_TOKENS2;
7810
+ const temperature = opts?.temperature ?? 0;
7811
+ const signal = opts?.signal;
7812
+ const body = {
7813
+ model: this.model,
7814
+ messages: [
7815
+ { role: "user", content: renderHydePrompt(trimmed) }
7816
+ ],
7817
+ temperature,
7818
+ max_tokens: maxTokens,
7819
+ n
7820
+ };
7821
+ try {
7822
+ const payload = await this.#postWithRetry(body, signal);
7823
+ return this.#decode(payload, n);
7824
+ } catch {
7825
+ return [];
7826
+ }
7827
+ }
7828
+ async #postWithRetry(body, signal) {
7829
+ const url = `${this.#baseUrl}/chat/completions`;
7830
+ const init = {
7831
+ method: "POST",
7832
+ headers: {
7833
+ "content-type": "application/json",
7834
+ "accept": "application/json",
7835
+ "authorization": `Bearer ${this.#apiKey}`
7836
+ },
7837
+ body: JSON.stringify(body)
7838
+ };
7839
+ if (signal !== void 0)
7840
+ init.signal = signal;
7841
+ let attempt = 0;
7842
+ let lastError = null;
7843
+ while (attempt <= this.#maxRetries) {
7844
+ try {
7845
+ const res = await this.#fetch(url, init);
7846
+ if (res.status === 429 || res.status >= 500 && res.status < 600) {
7847
+ lastError = new Error(`${this.name}: HTTP ${res.status}`);
7848
+ attempt += 1;
7849
+ if (attempt > this.#maxRetries)
7850
+ break;
7851
+ const backoff = this.#computeBackoff(attempt, res.headers.get("retry-after"));
7852
+ await sleep8(backoff, signal);
7853
+ continue;
7854
+ }
7855
+ if (!res.ok) {
7856
+ const text = await res.text().catch(() => "");
7857
+ throw nonRetryable8(`${this.name}: HTTP ${res.status} ${res.statusText}` + (text === "" ? "" : ` \u2014 ${text}`));
7858
+ }
7859
+ return await res.json();
7860
+ } catch (err) {
7861
+ if (err?.name === "AbortError")
7862
+ throw err;
7863
+ if (err?.nonRetryable === true) {
7864
+ throw err;
7865
+ }
7866
+ if (attempt >= this.#maxRetries)
7867
+ throw err;
7868
+ lastError = err;
7869
+ attempt += 1;
7870
+ await sleep8(this.#computeBackoff(attempt, null), signal);
7871
+ }
7872
+ }
7873
+ throw lastError instanceof Error ? lastError : new Error(`${this.name}: exhausted ${this.#maxRetries} retries`);
7874
+ }
7875
+ #computeBackoff(attempt, retryAfterHeader) {
7876
+ const exp = this.#retryBaseMs * 2 ** Math.max(0, attempt - 1);
7877
+ const advised = parseRetryAfterMs5(retryAfterHeader);
7878
+ const lower = advised === null ? exp : Math.max(exp, advised);
7879
+ return Math.min(lower, this.#retryMaxMs);
7880
+ }
7881
+ #decode(payload, requested) {
7882
+ const choices = payload.choices ?? [];
7883
+ const out = [];
7884
+ for (const c of choices) {
7885
+ const text = c.message?.content ?? "";
7886
+ const trimmed = text.trim();
7887
+ if (trimmed !== "")
7888
+ out.push(trimmed);
7889
+ if (out.length >= requested)
7890
+ break;
7891
+ }
7892
+ return out;
7893
+ }
7894
+ };
7895
+
7534
7896
  // dist/composition.js
7535
7897
  var RegionModeViolation = class extends Error {
7536
7898
  mode;
@@ -7816,6 +8178,55 @@ function discoverRerankProvider(env, fetchImpl) {
7816
8178
  }
7817
8179
  throw new NoProviderError(`unknown PROMETHEUS_RERANK_PROVIDER="${forced}" (expected "none", "voyage", or "bge")`);
7818
8180
  }
8181
+ function discoverQueryRewriter(env, fetchImpl) {
8182
+ const regionMode = parseRegionMode(env.PROMETHEUS_REGION_MODE);
8183
+ const forced = env.PROMETHEUS_REWRITER_PROVIDER?.toLowerCase() ?? "none";
8184
+ if (forced === "" || forced === "none")
8185
+ return { id: "none", provider: null };
8186
+ if (forced === "gemini") {
8187
+ const apiKey = env.GEMINI_API_KEY;
8188
+ if (apiKey === void 0 || apiKey === "") {
8189
+ throw new NoProviderError(`rewriter provider "gemini" requested but GEMINI_API_KEY is missing`);
8190
+ }
8191
+ const region = "us";
8192
+ if (regionMode !== "default") {
8193
+ throw new RegionModeViolation(regionMode, "gemini", region, regionMode === "eu-strict" ? ["mistral"] : []);
8194
+ }
8195
+ const model = env.GEMINI_REWRITER_MODEL ?? "gemini-2.5-flash";
8196
+ const provider = new GeminiQueryRewriter({
8197
+ name: `gemini:${model}`,
8198
+ apiKey,
8199
+ model,
8200
+ region,
8201
+ maxRetries: intEnv(env, "GEMINI_REWRITER_MAX_RETRIES", 6),
8202
+ retryBaseMs: intEnv(env, "GEMINI_REWRITER_RETRY_BASE_MS", 2e3),
8203
+ ...fetchOpt(fetchImpl)
8204
+ });
8205
+ return { id: "gemini", provider };
8206
+ }
8207
+ if (forced === "mistral") {
8208
+ const apiKey = env.MISTRAL_API_KEY;
8209
+ if (apiKey === void 0 || apiKey === "") {
8210
+ throw new NoProviderError(`rewriter provider "mistral" requested but MISTRAL_API_KEY is missing`);
8211
+ }
8212
+ const region = "eu";
8213
+ if (regionMode === "oss-only") {
8214
+ throw new RegionModeViolation(regionMode, "mistral", region, []);
8215
+ }
8216
+ const model = env.MISTRAL_REWRITER_MODEL ?? "mistral-small-latest";
8217
+ const provider = new MistralQueryRewriter({
8218
+ name: `mistral:${model}`,
8219
+ apiKey,
8220
+ model,
8221
+ region,
8222
+ maxRetries: intEnv(env, "MISTRAL_REWRITER_MAX_RETRIES", 6),
8223
+ retryBaseMs: intEnv(env, "MISTRAL_REWRITER_RETRY_BASE_MS", 2e3),
8224
+ ...fetchOpt(fetchImpl)
8225
+ });
8226
+ return { id: "mistral", provider };
8227
+ }
8228
+ throw new NoProviderError(`unknown PROMETHEUS_REWRITER_PROVIDER="${forced}" (expected "none", "gemini", or "mistral")`);
8229
+ }
7819
8230
  function getStableDbPath(workspaceRoot) {
7820
8231
  const abs = resolve4(workspaceRoot);
7821
8232
  const hash = createHash3("sha256").update(abs).digest("hex").slice(0, 16);
@@ -7901,6 +8312,7 @@ async function composeFromEnv(opts) {
7901
8312
  const retriever = new HybridRetriever({ storage, embedder });
7902
8313
  const { id: rerankId, provider: reranker } = discoverRerankProvider(env, opts.fetch);
7903
8314
  const rerankTopN = intEnv(env, "PROMETHEUS_RERANK_TOP_N", 100);
8315
+ const { id: queryRewriterId, provider: queryRewriter } = discoverQueryRewriter(env, opts.fetch);
7904
8316
  const managed = apiKeyPresent && storageBackend === "sqlite";
7905
8317
  let closed = false;
7906
8318
  return {
@@ -7918,6 +8330,8 @@ async function composeFromEnv(opts) {
7918
8330
  reranker,
7919
8331
  rerankId,
7920
8332
  rerankTopN,
8333
+ queryRewriter,
8334
+ queryRewriterId,
7921
8335
  async close() {
7922
8336
  if (closed)
7923
8337
  return;
@@ -8072,6 +8486,34 @@ var DEFAULT_K2 = 10;
8072
8486
  var MAX_FILE_BYTES = 256 * 1024;
8073
8487
  var MAX_SNIPPET_BYTES = 1500;
8074
8488
  var RERANK_DOC_BYTES = 4096;
8489
+ var DEMOTE_NONSOURCE = process.env.PROMETHEUS_DEMOTE_NONSOURCE !== "off";
8490
+ function isNonSourcePath(filePath) {
8491
+ const p = filePath.replace(/\\/g, "/").toLowerCase();
8492
+ if (/(^|\/)(tests?|testing|doc|docs|examples?|fixtures?|\.github|benchmarks?|samples?)\//.test(p))
8493
+ return true;
8494
+ if (/(^|\/)(test_[^/]*|conftest)\.py$|(_test|\.test|\.spec)\.[a-z]+$/.test(p))
8495
+ return true;
8496
+ if (/\.(txt|md|rst|cfg|toml|ini|lock|json|ya?ml)$/.test(p))
8497
+ return true;
8498
+ return false;
8499
+ }
8500
+ var NONSOURCE_INTENT = /\b(tests?|testing|specs?|fixtures?|docs?|documentation|examples?|benchmarks?|samples?)\b/i;
8501
+ function queryWantsNonSource(query) {
8502
+ const words = query.trim().split(/\s+/);
8503
+ if (words.length > 12)
8504
+ return false;
8505
+ return NONSOURCE_INTENT.test(query);
8506
+ }
8507
+ function demoteNonSource(ordered, query) {
8508
+ if (!DEMOTE_NONSOURCE || queryWantsNonSource(query))
8509
+ return [...ordered];
8510
+ const source = [];
8511
+ const nonSource = [];
8512
+ for (const r of ordered) {
8513
+ (isNonSourcePath(r.symbol.filePath) ? nonSource : source).push(r);
8514
+ }
8515
+ return source.concat(nonSource);
8516
+ }
8075
8517
  function symbolToJson(s) {
8076
8518
  return {
8077
8519
  name: s.name,
@@ -8228,17 +8670,34 @@ var changedSinceInput = {
8228
8670
  };
8229
8671
  var emptyInput = {};
8230
8672
  function registerTools(server, deps) {
8231
- const { storage, retriever, workspaceRoot, workspaceId, workspaceName, regionMode, providerId, storageBackend, reranker, rerankTopN } = deps;
8673
+ const { storage, retriever, workspaceRoot, workspaceId, workspaceName, regionMode, providerId, storageBackend, reranker, rerankTopN, queryRewriter } = deps;
8232
8674
  server.registerTool("search_code", {
8233
8675
  title: "Hybrid code search",
8234
- description: "PRIMARY code search for this workspace \u2014 call this FIRST to find where something is defined, used or implemented, before reading files or guessing paths. Hybrid retrieval (lexical FTS + vector + symbol graph, RRF-fused) over natural-language or symbol queries. Returns the top-k symbols with provenance AND an inline source snippet per hit, so the result is usually actionable without a follow-up get_file. Set `includeSnippet: false` to omit the inline code (symbols only).",
8676
+ description: (
8677
+ // Adoption lever (B3, 2026-06-22): assertive + HONEST positioning so the
8678
+ // agent reaches for this instead of a grep-then-read loop on the queries
8679
+ // where semantic search genuinely wins. Claims here are true by design.
8680
+ 'PRIMARY code search for this workspace \u2014 the fast path to WHERE code lives. Hybrid retrieval (lexical FTS + dense vector over the actual source body + symbol-graph, RRF-fused, cross-encoder reranked) returns the top symbols with provenance AND an inline source snippet per hit \u2014 ONE call that replaces a manual Grep\u2192Read loop, so prefer it for any code-location question. USE THIS FIRST when: you need a concept or behaviour ("where is auth handled?"), the exact symbol name is unknown or may have been renamed, the term is generic/high-frequency (grep would flood), or the repo is large or cross-language. Plain Grep is fine for an exact, known literal string in a known file \u2014 this tool wins on everything else, and returns the code inline so you usually don\'t need a follow-up read. Set `includeSnippet: false` for symbols only.'
8681
+ ),
8235
8682
  inputSchema: searchInput
8236
8683
  }, async (args) => {
8237
8684
  const k = clampK(args.k);
8238
8685
  const includeSnippet = args.includeSnippet ?? true;
8239
8686
  const cache = /* @__PURE__ */ new Map();
8240
8687
  const poolK = reranker ? Math.max(k, rerankTopN ?? 100) : k;
8241
- const pool = await retriever.search(args.query, { k: poolK });
8688
+ let searchQuery = args.query;
8689
+ if (queryRewriter) {
8690
+ try {
8691
+ const docs = await queryRewriter.rewrite(args.query, { n: 1, maxTokens: 512 });
8692
+ const hyp = docs[0]?.trim();
8693
+ if (hyp)
8694
+ searchQuery = `${args.query}
8695
+
8696
+ ${hyp}`;
8697
+ } catch {
8698
+ }
8699
+ }
8700
+ const pool = await retriever.search(searchQuery, { k: poolK });
8242
8701
  let ordered = pool;
8243
8702
  let reranked = false;
8244
8703
  if (reranker && pool.length > 0) {
@@ -8250,6 +8709,7 @@ function registerTools(server, deps) {
8250
8709
  reranked = true;
8251
8710
  }
8252
8711
  }
8712
+ ordered = demoteNonSource(ordered, args.query);
8253
8713
  const results = ordered.slice(0, k);
8254
8714
  const mapped = await Promise.all(results.map(async (r) => {
8255
8715
  const base = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prom.codes/context-mcp",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "description": "prom.codes Context — local-first codebase indexing & retrieval as an MCP server.",
5
5
  "type": "module",
6
6
  "bin": {