@prom.codes/context-mcp 0.4.2 → 0.4.4

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 (3) hide show
  1. package/README.md +28 -21
  2. package/dist/bin.js +230 -30
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -2,28 +2,33 @@
2
2
 
3
3
  prom.codes Context — local-first codebase indexing & retrieval as an MCP server (stdio).
4
4
 
5
- ## Quick start
6
-
7
- ```jsonc
8
- // Claude Desktop / Cursor MCP config
9
- {
10
- "mcpServers": {
11
- "prometheus": {
12
- "command": "npx",
13
- "args": ["-y", "@prom.codes/context-mcp@latest"],
14
- "env": {
15
- "PROMETHEUS_API_KEY": "prom_live_…",
16
- "PROMETHEUS_WORKSPACE_ROOT": "/absolute/path/to/your/repo"
17
- }
18
- }
19
- }
20
- }
5
+ ## Quick start (Claude Code)
6
+
7
+ ```bash
8
+ claude mcp add context --env PROMETHEUS_API_KEY=prom_live_… -- npx -y @prom.codes/context-mcp@latest
21
9
  ```
22
10
 
23
- With a `PROMETHEUS_API_KEY` the server runs in managed mode: it indexes
24
- your workspace into a local SQLite database (`~/.prometheus/<hash>.db`)
25
- and embeds via the prom.codes API. Your code never leaves your machine
26
- only embedding text transits to the API.
11
+ `claude mcp add` defaults to local scope (just you); add `--scope project` to
12
+ write a committable `.mcp.json`, or `--scope user` for all your projects. Other
13
+ MCP hosts (Cursor, VS Code) use the same command/args in their own config.
14
+
15
+ ## Configuration
16
+
17
+ - **`PROMETHEUS_API_KEY`** (required for semantic search) — a real key minted at
18
+ [app.prom.codes/app/api-keys](https://app.prom.codes), shape
19
+ `prom_live_<tag>_<secret>`. Embeddings route through the managed prom.codes
20
+ proxy — you never bring your own provider key. Without a valid key, code
21
+ search **degrades gracefully** to lexical (keyword) + symbol-graph retrieval
22
+ (no embeddings needed) and every structural tool still works.
23
+ - **Workspace root is auto-detected** — no need to set `PROMETHEUS_WORKSPACE_ROOT`.
24
+ Claude Code passes the open project via `CLAUDE_PROJECT_DIR`; Cursor/VS Code via
25
+ the MCP `roots` capability. Set it only to point at a different folder.
26
+ - The index is a local SQLite DB at `~/.prometheus/<hash>.db` (one per project).
27
+ Your code never leaves your machine — only embedding *text* transits to the proxy.
28
+
29
+ Tools: `search_code`, `get_symbol`, `find_references`, `find_callers`,
30
+ `find_callees`, `expand_context`, `get_file`, `list_changed_since`,
31
+ `list_workspaces`, `framework_overview`.
27
32
 
28
33
  ## Native modules
29
34
 
@@ -32,6 +37,8 @@ Prebuilt binaries are fetched automatically on the mainstream platforms
32
37
  (macOS x64/arm64, Linux x64, Windows x64) — no compiler needed. On other
33
38
  platforms (e.g. Linux/Windows arm64) or a Node ABI without a prebuild, install
34
39
  C/C++ build tools so the native modules can compile (Windows: VS Build Tools).
40
+ If `npx` skips the native build under a hardened npm (`ignore-scripts=true`),
41
+ install globally once and point Claude Code at the built binary — see the docs.
35
42
  Requires Node ≥ 20.10.
36
43
 
37
- Docs: https://prom.codes/docs
44
+ Docs: https://prom.codes/docs/mcp/claude-code
package/dist/bin.js CHANGED
@@ -3484,6 +3484,153 @@ function isSensitivePath(path) {
3484
3484
  }
3485
3485
  var SENSITIVE_PATH_ERROR = "path matches the sensitive-file deny-list";
3486
3486
 
3487
+ // ../shared/dist/update-check.js
3488
+ import { mkdir, readFile as readFile2, writeFile } from "node:fs/promises";
3489
+ import { homedir } from "node:os";
3490
+ import { join } from "node:path";
3491
+ import { fileURLToPath } from "node:url";
3492
+ async function packageIdentity(binImportMetaUrl) {
3493
+ try {
3494
+ const binPath = fileURLToPath(binImportMetaUrl);
3495
+ const pkgPath = join(binPath, "..", "..", "package.json");
3496
+ const raw = await readFile2(pkgPath, "utf8");
3497
+ const parsed = JSON.parse(raw);
3498
+ if (typeof parsed.name !== "string" || typeof parsed.version !== "string") {
3499
+ return null;
3500
+ }
3501
+ return { name: parsed.name, version: parsed.version };
3502
+ } catch {
3503
+ return null;
3504
+ }
3505
+ }
3506
+ async function maybeNotifyUpdate(binImportMetaUrl, env = process.env) {
3507
+ try {
3508
+ const id = await packageIdentity(binImportMetaUrl);
3509
+ if (id === null)
3510
+ return;
3511
+ await checkForUpdate({ name: id.name, version: id.version, env });
3512
+ } catch {
3513
+ }
3514
+ }
3515
+ var DEFAULT_TTL_MS = 24 * 60 * 60 * 1e3;
3516
+ var DEFAULT_TIMEOUT_MS = 1500;
3517
+ var OPT_OUT_RE = /^(1|true|yes|on)$/i;
3518
+ function parseSemver(v) {
3519
+ const m = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/.exec(v.trim());
3520
+ if (m === null)
3521
+ return null;
3522
+ return {
3523
+ core: [Number(m[1]), Number(m[2]), Number(m[3])],
3524
+ pre: m[4] ?? null
3525
+ };
3526
+ }
3527
+ function isNewerVersion(latest, current) {
3528
+ const a = parseSemver(latest);
3529
+ const b = parseSemver(current);
3530
+ if (a === null || b === null)
3531
+ return false;
3532
+ for (let i = 0; i < 3; i++) {
3533
+ if (a.core[i] > b.core[i])
3534
+ return true;
3535
+ if (a.core[i] < b.core[i])
3536
+ return false;
3537
+ }
3538
+ if (a.pre === null && b.pre !== null)
3539
+ return true;
3540
+ return false;
3541
+ }
3542
+ function cachePath(dir, name) {
3543
+ const safe = name.replace(/[^a-zA-Z0-9._-]+/g, "_");
3544
+ return join(dir, `.update-check-${safe}.json`);
3545
+ }
3546
+ async function readCache(path) {
3547
+ try {
3548
+ const raw = await readFile2(path, "utf8");
3549
+ const parsed = JSON.parse(raw);
3550
+ if (typeof parsed.checkedAt !== "number")
3551
+ return null;
3552
+ return {
3553
+ checkedAt: parsed.checkedAt,
3554
+ latest: typeof parsed.latest === "string" ? parsed.latest : null
3555
+ };
3556
+ } catch {
3557
+ return null;
3558
+ }
3559
+ }
3560
+ async function writeCache(path, data) {
3561
+ try {
3562
+ await writeFile(path, JSON.stringify(data), "utf8");
3563
+ } catch {
3564
+ }
3565
+ }
3566
+ async function fetchLatest(name, fetchImpl, timeoutMs) {
3567
+ const controller = new AbortController();
3568
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
3569
+ timer.unref?.();
3570
+ try {
3571
+ const url = `https://registry.npmjs.org/${name.replace("/", "%2F")}/latest`;
3572
+ const res = await fetchImpl(url, {
3573
+ signal: controller.signal,
3574
+ headers: { accept: "application/vnd.npm.install-v1+json" }
3575
+ });
3576
+ if (!res.ok)
3577
+ return null;
3578
+ const body = await res.json();
3579
+ return typeof body.version === "string" ? body.version : null;
3580
+ } catch {
3581
+ return null;
3582
+ } finally {
3583
+ clearTimeout(timer);
3584
+ }
3585
+ }
3586
+ async function checkForUpdate(options) {
3587
+ const { name, version, env = process.env, log = (line) => process.stderr.write(line), fetch: fetchImpl = globalThis.fetch, cacheDir = join(homedir(), ".prometheus"), cacheTtlMs = DEFAULT_TTL_MS, timeoutMs = DEFAULT_TIMEOUT_MS, force = false } = options;
3588
+ const base = {
3589
+ latest: null,
3590
+ current: version
3591
+ };
3592
+ if (OPT_OUT_RE.test(env.PROMETHEUS_NO_UPDATE_CHECK ?? "")) {
3593
+ return { ...base, checked: false, updateAvailable: false, reason: "opted-out" };
3594
+ }
3595
+ if (parseSemver(version) === null || version === "0.0.0") {
3596
+ return { ...base, checked: false, updateAvailable: false, reason: "invalid-version" };
3597
+ }
3598
+ if (typeof fetchImpl !== "function") {
3599
+ return { ...base, checked: false, updateAvailable: false, reason: "error" };
3600
+ }
3601
+ const file = cachePath(cacheDir, name);
3602
+ const now = Date.now();
3603
+ if (!force) {
3604
+ const cached = await readCache(file);
3605
+ if (cached !== null && now - cached.checkedAt < cacheTtlMs) {
3606
+ const updateAvailable2 = cached.latest !== null && isNewerVersion(cached.latest, version);
3607
+ if (updateAvailable2)
3608
+ notify(log, name, version, cached.latest);
3609
+ return {
3610
+ ...base,
3611
+ latest: cached.latest,
3612
+ checked: false,
3613
+ updateAvailable: updateAvailable2,
3614
+ reason: "throttled"
3615
+ };
3616
+ }
3617
+ }
3618
+ await mkdir(cacheDir, { recursive: true }).catch(() => void 0);
3619
+ const latest = await fetchLatest(name, fetchImpl, timeoutMs);
3620
+ await writeCache(file, { checkedAt: now, latest });
3621
+ if (latest === null) {
3622
+ return { ...base, checked: true, updateAvailable: false, reason: "error" };
3623
+ }
3624
+ const updateAvailable = isNewerVersion(latest, version);
3625
+ if (updateAvailable)
3626
+ notify(log, name, version, latest);
3627
+ return { ...base, latest, checked: true, updateAvailable };
3628
+ }
3629
+ function notify(log, name, current, latest) {
3630
+ log(`${name}: a newer version (${latest}) is available \u2014 you are on ${current}. npx users get it automatically on the next restart; for a global install run \`npm update -g ${name}\`. (Set PROMETHEUS_NO_UPDATE_CHECK=1 to silence.)
3631
+ `);
3632
+ }
3633
+
3487
3634
  // ../shared/dist/index.js
3488
3635
  var PROMETHEUS_VERSION = "0.1.0";
3489
3636
 
@@ -3542,6 +3689,8 @@ var WorkspaceWatcher = class extends EventEmitter {
3542
3689
  #root;
3543
3690
  #ignored;
3544
3691
  #debounceMs;
3692
+ #usePolling;
3693
+ #pollIntervalMs;
3545
3694
  #watcher = null;
3546
3695
  #pending = /* @__PURE__ */ new Map();
3547
3696
  constructor(options) {
@@ -3549,6 +3698,10 @@ var WorkspaceWatcher = class extends EventEmitter {
3549
3698
  this.#root = resolve(options.root);
3550
3699
  this.#ignored = options.ignored ?? DEFAULT_IGNORED;
3551
3700
  this.#debounceMs = options.debounceMs ?? 50;
3701
+ const envPolling = process.env.CHOKIDAR_USEPOLLING === "true" || process.env.CHOKIDAR_USEPOLLING === "1";
3702
+ this.#usePolling = options.usePolling ?? envPolling;
3703
+ const envInterval = Number.parseInt(process.env.CHOKIDAR_INTERVAL ?? "", 10);
3704
+ this.#pollIntervalMs = options.pollIntervalMs ?? (Number.isInteger(envInterval) && envInterval > 0 ? envInterval : 80);
3552
3705
  }
3553
3706
  /** Absolute workspace root. */
3554
3707
  get root() {
@@ -3558,14 +3711,25 @@ var WorkspaceWatcher = class extends EventEmitter {
3558
3711
  async start() {
3559
3712
  if (this.#watcher !== null)
3560
3713
  return;
3714
+ const relativeMatch = (pat) => (abs) => {
3715
+ const rel = toRelative(this.#root, abs);
3716
+ if (rel === abs)
3717
+ return false;
3718
+ if (typeof pat === "string") {
3719
+ return rel === pat || rel.startsWith(`${pat}/`);
3720
+ }
3721
+ return pat.test(rel);
3722
+ };
3561
3723
  const denySensitive = (abs) => {
3562
3724
  const rel = toRelative(this.#root, abs);
3563
3725
  return rel !== abs && isSensitivePath(rel);
3564
3726
  };
3565
3727
  const watcher = chokidar.watch(this.#root, {
3566
- ignored: [...this.#ignored, denySensitive],
3728
+ ignored: [...this.#ignored.map(relativeMatch), denySensitive],
3567
3729
  ignoreInitial: false,
3568
3730
  persistent: true,
3731
+ usePolling: this.#usePolling,
3732
+ ...this.#usePolling ? { interval: this.#pollIntervalMs, binaryInterval: this.#pollIntervalMs } : {},
3569
3733
  awaitWriteFinish: { stabilityThreshold: 50, pollInterval: 10 }
3570
3734
  });
3571
3735
  this.#watcher = watcher;
@@ -3608,8 +3772,8 @@ var WorkspaceWatcher = class extends EventEmitter {
3608
3772
  };
3609
3773
 
3610
3774
  // ../indexer/dist/workspace-indexer.js
3611
- import { readFile as readFile2, readdir, stat } from "node:fs/promises";
3612
- import { join, resolve as resolve2, sep as sep2 } from "node:path";
3775
+ import { readFile as readFile3, readdir, stat } from "node:fs/promises";
3776
+ import { join as join2, resolve as resolve2, sep as sep2 } from "node:path";
3613
3777
 
3614
3778
  // ../indexer/dist/co-change.js
3615
3779
  import { spawn } from "node:child_process";
@@ -3811,6 +3975,7 @@ var WorkspaceIndexer = class {
3811
3975
  #ignored;
3812
3976
  #concurrency;
3813
3977
  #coChangeBuilder;
3978
+ #watchTuning;
3814
3979
  #watcher = null;
3815
3980
  constructor(options) {
3816
3981
  this.#root = resolve2(options.root);
@@ -3819,6 +3984,7 @@ var WorkspaceIndexer = class {
3819
3984
  this.#ownsIndexer = options.indexer === void 0;
3820
3985
  this.#ignored = options.ignored;
3821
3986
  this.#concurrency = Math.max(1, options.concurrency ?? 8);
3987
+ this.#watchTuning = options.watch;
3822
3988
  this.#coChangeBuilder = options.coChange === false ? null : new CoChangeBuilder({
3823
3989
  repoRoot: this.#root,
3824
3990
  storage: this.#storage,
@@ -3882,7 +4048,7 @@ var WorkspaceIndexer = class {
3882
4048
  return;
3883
4049
  }
3884
4050
  for (const entry of entries) {
3885
- const abs = join(dir, entry.name);
4051
+ const abs = join2(dir, entry.name);
3886
4052
  const rel = toRelative2(this.#root, abs);
3887
4053
  if (this.#isIgnored(rel))
3888
4054
  continue;
@@ -3929,7 +4095,11 @@ var WorkspaceIndexer = class {
3929
4095
  this.#indexer.dispose();
3930
4096
  }
3931
4097
  #watcherOptions() {
3932
- return this.#ignored === void 0 ? { root: this.#root } : { root: this.#root, ignored: this.#ignored };
4098
+ return {
4099
+ root: this.#root,
4100
+ ...this.#ignored !== void 0 ? { ignored: this.#ignored } : {},
4101
+ ...this.#watchTuning ?? {}
4102
+ };
3933
4103
  }
3934
4104
  async #handleEvent(ev) {
3935
4105
  try {
@@ -3949,7 +4119,7 @@ var WorkspaceIndexer = class {
3949
4119
  let raw;
3950
4120
  let stats;
3951
4121
  try {
3952
- raw = await readFile2(absPath);
4122
+ raw = await readFile3(absPath);
3953
4123
  stats = await stat(absPath);
3954
4124
  } catch (err) {
3955
4125
  return { error: err instanceof Error ? err.message : String(err) };
@@ -4001,8 +4171,8 @@ var WorkspaceIndexer = class {
4001
4171
  // dist/composition.js
4002
4172
  import { createHash as createHash3 } from "node:crypto";
4003
4173
  import { mkdirSync } from "node:fs";
4004
- import { homedir } from "node:os";
4005
- import { basename as basename5, dirname as dirname2, join as join3, resolve as resolve4 } from "node:path";
4174
+ import { homedir as homedir2 } from "node:os";
4175
+ import { basename as basename5, dirname as dirname2, join as join4, resolve as resolve4 } from "node:path";
4006
4176
 
4007
4177
  // ../storage-sqlite/dist/adapter.js
4008
4178
  import Database from "better-sqlite3";
@@ -4727,8 +4897,8 @@ var DEFAULT_VECTOR_DIMENSION = 1024;
4727
4897
  var DEFAULT_SCHEMA = "public";
4728
4898
 
4729
4899
  // ../storage-supabase/dist/migrations.js
4730
- import { dirname, join as join2, resolve as resolve3 } from "node:path";
4731
- import { fileURLToPath } from "node:url";
4900
+ import { dirname, join as join3, resolve as resolve3 } from "node:path";
4901
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
4732
4902
 
4733
4903
  // ../storage-supabase/dist/migrations-data.generated.js
4734
4904
  var EMBEDDED_MIGRATIONS = {
@@ -4819,13 +4989,13 @@ COMMIT;
4819
4989
  };
4820
4990
 
4821
4991
  // ../storage-supabase/dist/migrations.js
4822
- var HERE = dirname(fileURLToPath(import.meta.url));
4992
+ var HERE = dirname(fileURLToPath2(import.meta.url));
4823
4993
  var REPO_ROOT = resolve3(HERE, "..", "..", "..");
4824
- var MIGRATIONS_DIR = join2(REPO_ROOT, "infra", "supabase", "migrations");
4994
+ var MIGRATIONS_DIR = join3(REPO_ROOT, "infra", "supabase", "migrations");
4825
4995
  var MIGRATIONS = Object.keys(EMBEDDED_MIGRATIONS).sort().map((filename, idx) => ({
4826
4996
  id: idx + 1,
4827
4997
  filename,
4828
- path: join2(MIGRATIONS_DIR, filename)
4998
+ path: join3(MIGRATIONS_DIR, filename)
4829
4999
  }));
4830
5000
  async function loadMigrationSql(filename) {
4831
5001
  const sql = EMBEDDED_MIGRATIONS[filename];
@@ -6149,7 +6319,7 @@ var HybridRetriever = class {
6149
6319
  const coChangeActive = effectiveEdgeWeights[CO_CHANGE] > 0;
6150
6320
  const lexCols = options.lexicalColumnWeights;
6151
6321
  const lexicalPromise = wLex > 0 ? this.#storage.searchByText(trimmed, candidateLimit, lexCols !== void 0 ? { columnWeights: lexCols } : void 0) : Promise.resolve([]);
6152
- const vectorPromise = wVec > 0 ? this.#runVector(trimmed, candidateLimit, options.signal) : Promise.resolve({ hits: [], queryVector: null });
6322
+ const vectorPromise = wVec > 0 ? this.#runVector(trimmed, candidateLimit, options.signal, options.onVectorError) : Promise.resolve({ hits: [], queryVector: null });
6153
6323
  const [lexicalHits, vectorResult] = await Promise.all([
6154
6324
  lexicalPromise,
6155
6325
  vectorPromise
@@ -6240,15 +6410,24 @@ var HybridRetriever = class {
6240
6410
  }
6241
6411
  return fusedResults;
6242
6412
  }
6243
- async #runVector(query, limit, signal) {
6413
+ async #runVector(query, limit, signal, onVectorError) {
6244
6414
  const opts = signal !== void 0 ? { inputType: "query", signal } : { inputType: "query" };
6245
- const vectors = await this.#embedder.embed([query], opts);
6246
- if (vectors.length === 0 || vectors[0] === void 0) {
6415
+ try {
6416
+ const vectors = await this.#embedder.embed([query], opts);
6417
+ if (vectors.length === 0 || vectors[0] === void 0) {
6418
+ return { hits: [], queryVector: null };
6419
+ }
6420
+ const queryVector = vectors[0];
6421
+ const hits = await this.#storage.searchByVector(queryVector, limit);
6422
+ return { hits, queryVector };
6423
+ } catch (err) {
6424
+ if (err?.name === "AbortError")
6425
+ throw err;
6426
+ if (onVectorError === void 0)
6427
+ throw err;
6428
+ onVectorError(err);
6247
6429
  return { hits: [], queryVector: null };
6248
6430
  }
6249
- const queryVector = vectors[0];
6250
- const hits = await this.#storage.searchByVector(queryVector, limit);
6251
- return { hits, queryVector };
6252
6431
  }
6253
6432
  /**
6254
6433
  * Run typed symbol-graph expansion from `seeds`.
@@ -8257,7 +8436,7 @@ function discoverQueryRewriter(env, fetchImpl) {
8257
8436
  function getStableDbPath(workspaceRoot) {
8258
8437
  const abs = resolve4(workspaceRoot);
8259
8438
  const hash = createHash3("sha256").update(abs).digest("hex").slice(0, 16);
8260
- return join3(homedir(), ".prometheus", `${hash}.db`);
8439
+ return join4(homedir2(), ".prometheus", `${hash}.db`);
8261
8440
  }
8262
8441
  var StorageConfigError = class extends Error {
8263
8442
  constructor(reason) {
@@ -8375,7 +8554,7 @@ async function composeFromEnv(opts) {
8375
8554
  }
8376
8555
 
8377
8556
  // dist/roots.js
8378
- import { fileURLToPath as fileURLToPath2 } from "node:url";
8557
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
8379
8558
  async function rootFromClient(server, timeoutMs = 2500) {
8380
8559
  let supportsRoots = false;
8381
8560
  try {
@@ -8396,7 +8575,7 @@ async function rootFromClient(server, timeoutMs = 2500) {
8396
8575
  const uri = typeof r?.uri === "string" ? r.uri : "";
8397
8576
  if (uri.startsWith("file://")) {
8398
8577
  try {
8399
- return fileURLToPath2(uri);
8578
+ return fileURLToPath3(uri);
8400
8579
  } catch {
8401
8580
  }
8402
8581
  }
@@ -8408,8 +8587,8 @@ async function rootFromClient(server, timeoutMs = 2500) {
8408
8587
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8409
8588
 
8410
8589
  // dist/tools.js
8411
- import { readFile as readFile3 } from "node:fs/promises";
8412
- import { isAbsolute, join as join4, normalize, relative, resolve as resolve5, sep as sep3 } from "node:path";
8590
+ import { readFile as readFile4 } from "node:fs/promises";
8591
+ import { isAbsolute, join as join5, normalize, relative, resolve as resolve5, sep as sep3 } from "node:path";
8413
8592
  import { z } from "zod";
8414
8593
 
8415
8594
  // dist/frameworks.js
@@ -8611,7 +8790,7 @@ async function snippetForSymbol(workspaceRoot, symbol, cache, capBytes = MAX_SNI
8611
8790
  let buf = cache.get(relPath);
8612
8791
  if (buf === void 0) {
8613
8792
  const abs = resolveInWorkspace(workspaceRoot, relPath);
8614
- buf = await readFile3(abs).catch(() => null);
8793
+ buf = await readFile4(abs).catch(() => null);
8615
8794
  cache.set(relPath, buf);
8616
8795
  }
8617
8796
  if (buf === null)
@@ -8760,7 +8939,16 @@ ${hyp}`;
8760
8939
  } catch {
8761
8940
  }
8762
8941
  }
8763
- const pool = await retriever.search(searchQuery, { k: poolK });
8942
+ let vectorError = null;
8943
+ const pool = await retriever.search(searchQuery, {
8944
+ k: poolK,
8945
+ onVectorError: (err) => {
8946
+ if (vectorError !== null)
8947
+ return;
8948
+ vectorError = err instanceof Error ? err.message : String(err);
8949
+ console.error(`[context-mcp] search_code: vector (semantic) search unavailable \u2014 falling back to lexical + symbol-graph. Likely a missing/invalid embedding API key; set PROMETHEUS_API_KEY to a real minted prom_live_<tag>_<secret> key (mint at app.prom.codes/app/api-keys) to enable semantic ranking. Cause: ${vectorError}`);
8950
+ }
8951
+ });
8764
8952
  let ordered = pool;
8765
8953
  let reranked = false;
8766
8954
  if (reranker && pool.length > 0) {
@@ -8785,7 +8973,18 @@ ${hyp}`;
8785
8973
  const snip = await snippetForSymbol(workspaceRoot, r.symbol, cache);
8786
8974
  return snip === null ? base : { ...base, snippet: snip.text, snippetTruncated: snip.truncated };
8787
8975
  }));
8788
- return textResult({ query: args.query, k, reranked, results: mapped });
8976
+ const payload = {
8977
+ query: args.query,
8978
+ k,
8979
+ reranked,
8980
+ results: mapped
8981
+ };
8982
+ if (vectorError !== null) {
8983
+ payload.degraded = "lexical+graph-only";
8984
+ payload.note = "Semantic (vector) search was unavailable \u2014 results are lexical (keyword) + symbol-graph only, which can rank less precisely on concept queries. Set PROMETHEUS_API_KEY to a real minted prom_live_<tag>_<secret> key (app.prom.codes/app/api-keys) to enable semantic ranking.";
8985
+ payload.vectorError = vectorError;
8986
+ }
8987
+ return textResult(payload);
8789
8988
  });
8790
8989
  server.registerTool("get_symbol", {
8791
8990
  title: "Exact symbol lookup",
@@ -8855,7 +9054,7 @@ ${hyp}`;
8855
9054
  if (isSensitivePath(relative(workspaceRoot, abs))) {
8856
9055
  throw new Error(`${SENSITIVE_PATH_ERROR}: "${args.path}".`);
8857
9056
  }
8858
- const buf = await readFile3(abs);
9057
+ const buf = await readFile4(abs);
8859
9058
  const start = args.startByte ?? 0;
8860
9059
  const end = args.endByte ?? buf.byteLength;
8861
9060
  if (end < start) {
@@ -8926,7 +9125,7 @@ ${hyp}`;
8926
9125
  const manifests = {};
8927
9126
  for (const name of FRAMEWORK_MANIFESTS) {
8928
9127
  try {
8929
- manifests[name] = await readFile3(join4(workspaceRoot, name), "utf8");
9128
+ manifests[name] = await readFile4(join5(workspaceRoot, name), "utf8");
8930
9129
  } catch {
8931
9130
  }
8932
9131
  }
@@ -8994,6 +9193,7 @@ async function main() {
8994
9193
  const explicitRoot = (env.PROMETHEUS_WORKSPACE_ROOT ?? "").trim();
8995
9194
  const claudeRoot = (env.CLAUDE_PROJECT_DIR ?? "").trim();
8996
9195
  const eagerVia = explicitRoot !== "" ? "PROMETHEUS_WORKSPACE_ROOT" : claudeRoot !== "" ? "CLAUDE_PROJECT_DIR" : null;
9196
+ void maybeNotifyUpdate(import.meta.url, env);
8997
9197
  const transport = new StdioServerTransport();
8998
9198
  const server = new McpServer2(SERVER_IDENTITY, { capabilities: { tools: {} } });
8999
9199
  let composed = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prom.codes/context-mcp",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
4
4
  "description": "prom.codes Context — local-first codebase indexing & retrieval as an MCP server.",
5
5
  "type": "module",
6
6
  "bin": {