@prom.codes/context-mcp 0.2.1 → 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.
- package/README.md +2 -2
- package/dist/bin.js +466 -6
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @prom.codes/context-mcp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
prom.codes Context — local-first codebase indexing & retrieval as an MCP server (stdio).
|
|
4
4
|
|
|
5
5
|
## Quick start
|
|
6
6
|
|
|
@@ -22,7 +22,7 @@ Prometheus Context Engine — local-first codebase indexing & retrieval as an MC
|
|
|
22
22
|
|
|
23
23
|
With a `PROMETHEUS_API_KEY` the server runs in managed mode: it indexes
|
|
24
24
|
your workspace into a local SQLite database (`~/.prometheus/<hash>.db`)
|
|
25
|
-
and embeds via the
|
|
25
|
+
and embeds via the prom.codes API. Your code never leaves your machine —
|
|
26
26
|
only embedding text transits to the API.
|
|
27
27
|
|
|
28
28
|
## Native modules
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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 = {
|
|
@@ -8424,7 +8884,7 @@ function registerTools(server, deps) {
|
|
|
8424
8884
|
var SERVER_IDENTITY = {
|
|
8425
8885
|
name: "prometheus-context-mcp",
|
|
8426
8886
|
version: PROMETHEUS_VERSION,
|
|
8427
|
-
title: "
|
|
8887
|
+
title: "prom.codes Context"
|
|
8428
8888
|
};
|
|
8429
8889
|
function createServer(deps, options = {}) {
|
|
8430
8890
|
const identity = { ...SERVER_IDENTITY, ...options.identity ?? {} };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prom.codes/context-mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "prom.codes Context — local-first codebase indexing & retrieval as an MCP server.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"prometheus-context-mcp": "dist/bin.js"
|