@vibgrate/cli 2026.703.7 → 2026.704.3

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.
@@ -1,9 +1,10 @@
1
- import { langById, shortId, canonicalize, grammarSetVersion, hashBytes, hashString, langForExtension, setGrammarsOverride, parseSource } from './chunk-X5YT263H.js';
2
- import { writeTextFile, pathExists, readJsonFile, VERSION, computeUpgradeImpact, getChangelogSignals, gitHistoryAvailable, buildVersionTimelines, findPackageTimeline, severityRank, projectTypeToVulnEcosystem } from './chunk-QWS7M7DG.js';
1
+ import { hashString, langById, shortId, canonicalize, grammarSetVersion, hashBytes, langForExtension, setGrammarsOverride, parseSource } from './chunk-X5YT263H.js';
2
+ import { writeTextFile, pathExists, readJsonFile, computeUpgradeImpact, getChangelogSignals, gitHistoryAvailable, buildVersionTimelines, findPackageTimeline, severityRank, projectTypeToVulnEcosystem, VERSION } from './chunk-YN5OGRWU.js';
3
3
  import * as fs18 from 'fs';
4
4
  import { execFileSync } from 'child_process';
5
5
  import * as path18 from 'path';
6
6
  import ignore from 'ignore';
7
+ import * as v8 from 'v8';
7
8
  import Graph from 'graphology';
8
9
  import pagerank from 'graphology-metrics/centrality/pagerank.js';
9
10
  import betweenness from 'graphology-metrics/centrality/betweenness.js';
@@ -45,47 +46,174 @@ function isOwnBinary(binPath) {
45
46
  return false;
46
47
  }
47
48
  }
49
+ function isEphemeralNpxBinary(binPath) {
50
+ const NPX_SEGMENT = /[\\/]_npx[\\/]/;
51
+ if (NPX_SEGMENT.test(binPath)) return true;
52
+ try {
53
+ return NPX_SEGMENT.test(fs18.realpathSync(binPath));
54
+ } catch {
55
+ return false;
56
+ }
57
+ }
58
+ function isInstalledOwnBinary(binPath) {
59
+ return isOwnBinary(binPath) && !isEphemeralNpxBinary(binPath);
60
+ }
48
61
  var cached;
49
62
  function resolveCliInvocation(which) {
50
63
  if (!which && cached !== void 0) return cached;
51
64
  const lookup = which ?? whichOnPath;
52
65
  const vg = lookup("vg");
53
66
  let result;
54
- if (vg && isOwnBinary(vg)) {
67
+ if (vg && isInstalledOwnBinary(vg)) {
55
68
  result = "vg";
56
69
  } else {
57
70
  const vibgrate = lookup("vibgrate");
58
- result = vibgrate && isOwnBinary(vibgrate) ? "vibgrate" : NPX_INVOCATION;
71
+ result = vibgrate && isInstalledOwnBinary(vibgrate) ? "vibgrate" : NPX_INVOCATION;
59
72
  }
60
73
  if (!which) cached = result;
61
74
  return result;
62
75
  }
63
76
  var SKIP_DIRS = /* @__PURE__ */ new Set([
77
+ // Version control, IDE & tool metadata
64
78
  ".git",
65
- "node_modules",
79
+ ".svn",
80
+ ".hg",
81
+ ".idea",
82
+ ".vscode",
83
+ ".vs",
66
84
  ".vibgrate",
85
+ ".wrangler",
86
+ ".turbo",
87
+ ".cache",
88
+ // JavaScript / Node — installed dependency trees
89
+ "node_modules",
90
+ "bower_components",
91
+ "jspm_packages",
92
+ "web_modules",
93
+ ".pnpm-store",
94
+ ".yarn",
95
+ // JS/TS meta-framework & bundler build output
96
+ ".next",
97
+ ".nuxt",
98
+ ".output",
99
+ ".svelte-kit",
100
+ ".astro",
101
+ ".vercel",
102
+ ".netlify",
103
+ ".angular",
104
+ ".parcel-cache",
105
+ ".docusaurus",
106
+ "storybook-static",
107
+ // Generic build / dist / coverage output
67
108
  "dist",
68
109
  "build",
69
110
  "out",
70
111
  "coverage",
71
- ".next",
72
- ".nuxt",
73
- ".svelte-kit",
74
- ".turbo",
75
- ".cache",
112
+ // Vendored third-party source trees
76
113
  "vendor",
77
- "target",
78
- // rust / java
79
- "bin",
80
- "obj",
81
- // .NET
82
- "__pycache__",
114
+ "Pods",
115
+ "Carthage",
116
+ // Python — virtualenvs & tool caches
83
117
  ".venv",
84
118
  "venv",
119
+ "virtualenv",
85
120
  ".tox",
121
+ ".nox",
122
+ "__pycache__",
123
+ ".mypy_cache",
124
+ ".pytest_cache",
125
+ ".ruff_cache",
126
+ ".eggs",
127
+ ".ipynb_checkpoints",
128
+ // Ruby
129
+ ".bundle",
130
+ // Rust / Maven / sbt / Clojure — all build into target/
131
+ "target",
132
+ // JVM (Gradle / Maven) & .NET build output
86
133
  ".gradle",
87
- ".idea",
88
- ".vscode"
134
+ "bin",
135
+ "obj",
136
+ "TestResults",
137
+ // Swift / Xcode
138
+ ".build",
139
+ ".swiftpm",
140
+ "DerivedData",
141
+ // Dart / Flutter
142
+ ".dart_tool",
143
+ // Elixir / Erlang
144
+ "_build",
145
+ "deps",
146
+ // Haskell (Cabal / Stack)
147
+ "dist-newstyle",
148
+ ".stack-work",
149
+ // Infrastructure-as-code build/state
150
+ ".terraform",
151
+ ".serverless",
152
+ "cdk.out",
153
+ ".aws-sam"
154
+ ]);
155
+ var SKIP_FILES = /* @__PURE__ */ new Set([
156
+ // JavaScript / Node
157
+ "package-lock.json",
158
+ "npm-shrinkwrap.json",
159
+ "yarn.lock",
160
+ "pnpm-lock.yaml",
161
+ "bun.lockb",
162
+ "bun.lock",
163
+ "deno.lock",
164
+ ".pnp.cjs",
165
+ ".pnp.data.json",
166
+ ".pnp.loader.mjs",
167
+ // Python
168
+ "poetry.lock",
169
+ "pipfile.lock",
170
+ "pdm.lock",
171
+ "uv.lock",
172
+ "pylock.toml",
173
+ "conda-lock.yml",
174
+ // Ruby
175
+ "gemfile.lock",
176
+ // PHP
177
+ "composer.lock",
178
+ // Rust
179
+ "cargo.lock",
180
+ // Go
181
+ "go.sum",
182
+ "go.work.sum",
183
+ "gopkg.lock",
184
+ "glide.lock",
185
+ // .NET
186
+ "packages.lock.json",
187
+ // Java / JVM (Gradle)
188
+ "gradle.lockfile",
189
+ // Swift / Xcode
190
+ "package.resolved",
191
+ // Dart / Flutter
192
+ "pubspec.lock",
193
+ // Elixir
194
+ "mix.lock",
195
+ // Haskell (Cabal / Stack)
196
+ "cabal.project.freeze",
197
+ "stack.yaml.lock",
198
+ // Julia
199
+ "manifest.toml",
200
+ // R
201
+ "renv.lock",
202
+ // Perl (Carton)
203
+ "cpanfile.snapshot",
204
+ // CocoaPods / Carthage
205
+ "podfile.lock",
206
+ "cartfile.resolved",
207
+ // Terraform / IaC
208
+ ".terraform.lock.hcl",
209
+ // Helm
210
+ "chart.lock",
211
+ // C / C++ (Conan)
212
+ "conan.lock",
213
+ // Bazel
214
+ "module.bazel.lock",
215
+ // Nix
216
+ "flake.lock"
89
217
  ]);
90
218
  function toPosix(p) {
91
219
  return p.split(path18.sep).join("/");
@@ -115,6 +243,7 @@ function discover(options) {
115
243
  const rel2 = toPosix(path18.relative(root, abs));
116
244
  if (rel2.startsWith("..")) return;
117
245
  if (rel2 === "" || rootIg.ignores(rel2)) return;
246
+ if (SKIP_FILES.has(path18.basename(abs).toLowerCase())) return;
118
247
  const lang = langForExtension(path18.extname(abs));
119
248
  if (!lang || !allowLang(lang)) return;
120
249
  found.set(rel2, { rel: rel2, abs, lang });
@@ -152,6 +281,58 @@ var UsageError = class extends Error {
152
281
  this.name = "UsageError";
153
282
  }
154
283
  };
284
+ var MIB = 1024 * 1024;
285
+ var DEFAULT_MAX_FILE_BYTES = 2 * MIB;
286
+ var DEFAULT_MAX_FILES = 1e5;
287
+ var DEFAULT_TSC_MAX_FILES = 1e4;
288
+ var HEAP_GUARD_FRACTION = 0.9;
289
+ var ResourceLimitError = class extends Error {
290
+ isResourceLimitError = true;
291
+ constructor(message) {
292
+ super(message);
293
+ this.name = "ResourceLimitError";
294
+ }
295
+ };
296
+ function envInt(name, fallback) {
297
+ const raw = process.env[name];
298
+ if (raw === void 0 || raw.trim() === "") return fallback;
299
+ const n = Number(raw);
300
+ return Number.isInteger(n) && n >= 0 ? n : fallback;
301
+ }
302
+ function envIntOptional(name) {
303
+ const n = envInt(name, 0);
304
+ return n > 0 ? n : void 0;
305
+ }
306
+ function defaultMemoryBudgetMb() {
307
+ return Math.floor(v8.getHeapStatistics().heap_size_limit / MIB * HEAP_GUARD_FRACTION);
308
+ }
309
+ function resolveLimits(overrides = {}) {
310
+ return {
311
+ maxFileBytes: overrides.maxFileBytes ?? envInt("VG_MAX_FILE_BYTES", DEFAULT_MAX_FILE_BYTES),
312
+ maxFiles: overrides.maxFiles ?? envInt("VG_MAX_FILES", DEFAULT_MAX_FILES),
313
+ tscMaxFiles: overrides.tscMaxFiles ?? envInt("VG_TSC_MAX_FILES", DEFAULT_TSC_MAX_FILES),
314
+ memoryBudgetMb: overrides.memoryBudgetMb ?? envInt("VG_MEMORY_BUDGET_MB", defaultMemoryBudgetMb())
315
+ };
316
+ }
317
+ function envJobs() {
318
+ return envIntOptional("VG_JOBS");
319
+ }
320
+ function envWorkerHeapMb() {
321
+ return envIntOptional("VG_WORKER_HEAP_MB");
322
+ }
323
+ function checkMemoryBudget(phase, budgetMb) {
324
+ if (budgetMb <= 0) return;
325
+ const usedMb = process.memoryUsage().heapUsed / MIB;
326
+ if (usedMb <= budgetMb) return;
327
+ throw new ResourceLimitError(
328
+ `graph build stopped during ${phase}: heap use ${Math.round(usedMb)} MiB exceeds the ${budgetMb} MiB budget. Narrow the build (scope to sub-paths, add --exclude globs, or --only <langs>), raise the Node heap (NODE_OPTIONS=--max-old-space-size=8192), or set VG_MEMORY_BUDGET_MB (0 disables this safeguard).`
329
+ );
330
+ }
331
+ function formatBytes(n) {
332
+ if (n >= MIB) return `${(n / MIB).toFixed(1)} MiB`;
333
+ if (n >= 1024) return `${(n / 1024).toFixed(1)} KiB`;
334
+ return `${n} B`;
335
+ }
155
336
  var JS_TS_EXT = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
156
337
  var OTHER_EXT = [
157
338
  ".py",
@@ -1293,18 +1474,18 @@ var DEFAULT_INLINE_THRESHOLD = 24;
1293
1474
  async function parseFiles(files, options = {}) {
1294
1475
  const threshold = options.inlineThreshold ?? DEFAULT_INLINE_THRESHOLD;
1295
1476
  const cores = Math.max(1, os.cpus()?.length ?? 1);
1296
- const jobs = Math.max(1, options.jobs ?? (Math.min(cores - 1, files.length) || 1));
1477
+ const jobs = Math.max(1, options.jobs ?? envJobs() ?? (Math.min(cores - 1, files.length) || 1));
1297
1478
  const workerFile = resolveWorkerFile();
1298
1479
  const useInline = options.inline === true || jobs <= 1 || files.length < threshold || workerFile === null;
1299
1480
  if (useInline) {
1300
1481
  if (options.grammarsDir) setGrammarsOverride(options.grammarsDir);
1301
- return sortByRel(await parseInline(files, options.onProgress));
1482
+ return sortByRel(await parseInline(files, options));
1302
1483
  }
1303
- return sortByRel(
1304
- await parsePooled(files, jobs, workerFile, options.onProgress, options.grammarsDir)
1305
- );
1484
+ return sortByRel(await parsePooled(files, jobs, workerFile, options));
1306
1485
  }
1307
- async function parseInline(files, onProgress) {
1486
+ var MEM_CHECK_EVERY = 64;
1487
+ async function parseInline(files, options) {
1488
+ const { onProgress, memoryBudgetMb = 0 } = options;
1308
1489
  const out = [];
1309
1490
  onProgress?.(0, files.length);
1310
1491
  for (const file of files) {
@@ -1315,12 +1496,20 @@ async function parseInline(files, onProgress) {
1315
1496
  out.push(emptyParse(file, `parse failed: ${err.message}`));
1316
1497
  }
1317
1498
  onProgress?.(out.length, files.length);
1499
+ if (out.length % MEM_CHECK_EVERY === 0) checkMemoryBudget("parse", memoryBudgetMb);
1318
1500
  }
1319
1501
  return out;
1320
1502
  }
1321
- async function parsePooled(files, jobs, workerFile, onProgress, grammarsDir) {
1503
+ async function parsePooled(files, jobs, workerFile, options) {
1504
+ const { onProgress, grammarsDir, memoryBudgetMb = 0 } = options;
1322
1505
  const { default: Tinypool } = await import('tinypool');
1323
- const pool = new Tinypool({ filename: workerFile, maxThreads: jobs, minThreads: 1 });
1506
+ const workerHeapMb = envWorkerHeapMb();
1507
+ const pool = new Tinypool({
1508
+ filename: workerFile,
1509
+ maxThreads: jobs,
1510
+ minThreads: 1,
1511
+ ...workerHeapMb ? { resourceLimits: { maxOldGenerationSizeMb: workerHeapMb } } : {}
1512
+ });
1324
1513
  try {
1325
1514
  const total = files.length;
1326
1515
  const buckets = chunk(
@@ -1334,15 +1523,27 @@ async function parsePooled(files, jobs, workerFile, onProgress, grammarsDir) {
1334
1523
  (b) => pool.run({ tasks: b, grammarsDir }).then((r) => {
1335
1524
  done += b.length;
1336
1525
  onProgress?.(done, total);
1526
+ checkMemoryBudget("parse", memoryBudgetMb);
1337
1527
  return r;
1338
1528
  })
1339
1529
  )
1340
1530
  );
1341
1531
  return results.flat();
1532
+ } catch (err) {
1533
+ if (isWorkerOom(err)) {
1534
+ throw new ResourceLimitError(
1535
+ `graph build stopped: a parse worker exceeded its ${workerHeapMb ?? "?"} MiB heap cap (VG_WORKER_HEAP_MB). Raise the cap, exclude the offending files (--exclude), or run single-threaded with --jobs 1.`
1536
+ );
1537
+ }
1538
+ throw err;
1342
1539
  } finally {
1343
1540
  await pool.destroy();
1344
1541
  }
1345
1542
  }
1543
+ function isWorkerOom(err) {
1544
+ const e = err;
1545
+ return e?.code === "ERR_WORKER_OUT_OF_MEMORY" || /out of memory/i.test(e?.message ?? "");
1546
+ }
1346
1547
  function resolveWorkerFile() {
1347
1548
  const here = path18.dirname(fileURLToPath(import.meta.url));
1348
1549
  const candidate = path18.join(here, "parse-worker.js");
@@ -1845,6 +2046,13 @@ async function buildGraph(options) {
1845
2046
  exclude: options.exclude,
1846
2047
  paths: options.paths
1847
2048
  });
2049
+ const limits = resolveLimits(options.limits);
2050
+ if (limits.maxFiles > 0 && files.length > limits.maxFiles) {
2051
+ throw new ResourceLimitError(
2052
+ `graph build stopped: ${files.length.toLocaleString()} files exceed the ${limits.maxFiles.toLocaleString()}-file limit. Scope the build (pass sub-paths, add --exclude globs, or --only <langs>), or set VG_MAX_FILES to raise the limit (0 disables it).`
2053
+ );
2054
+ }
2055
+ checkMemoryBudget("discovery", limits.memoryBudgetMb);
1848
2056
  const grammars = grammarSetVersion();
1849
2057
  const cache = loadCache(root, {
1850
2058
  toolVersion: VERSION,
@@ -1855,11 +2063,28 @@ async function buildGraph(options) {
1855
2063
  const fileStats = [];
1856
2064
  const toParse = [];
1857
2065
  const reused = [];
2066
+ const buildWarnings = [];
1858
2067
  for (const file of files) {
1859
- let hash = "";
1860
2068
  let stat;
1861
2069
  try {
1862
2070
  stat = fs18.statSync(file.abs);
2071
+ } catch {
2072
+ continue;
2073
+ }
2074
+ if (limits.maxFileBytes > 0 && stat.size > limits.maxFileBytes) {
2075
+ fileStats.push({
2076
+ rel: file.rel,
2077
+ size: stat.size,
2078
+ mtimeMs: stat.mtimeMs,
2079
+ hash: hashString(`vg:oversize:${stat.size}`)
2080
+ });
2081
+ buildWarnings.push(
2082
+ `${file.rel}: skipped \u2014 ${formatBytes(stat.size)} exceeds the ${formatBytes(limits.maxFileBytes)} per-file limit (set VG_MAX_FILE_BYTES to raise it, 0 to disable)`
2083
+ );
2084
+ continue;
2085
+ }
2086
+ let hash = "";
2087
+ try {
1863
2088
  hash = hashBytes(fs18.readFileSync(file.abs));
1864
2089
  } catch {
1865
2090
  continue;
@@ -1874,8 +2099,10 @@ async function buildGraph(options) {
1874
2099
  jobs: options.jobs,
1875
2100
  inline: options.inline,
1876
2101
  onProgress: options.onParseProgress,
1877
- grammarsDir: options.grammarsDir
2102
+ grammarsDir: options.grammarsDir,
2103
+ memoryBudgetMb: limits.memoryBudgetMb
1878
2104
  });
2105
+ checkMemoryBudget("parse", limits.memoryBudgetMb);
1879
2106
  for (const p of parsedNew) cache.set(p.rel, p);
1880
2107
  const currentRels = new Set(files.map((f) => f.rel));
1881
2108
  cache.prune(currentRels);
@@ -1883,14 +2110,21 @@ async function buildGraph(options) {
1883
2110
  const parses = [...reused, ...parsedNew].sort(
1884
2111
  (a, b) => a.rel < b.rel ? -1 : a.rel > b.rel ? 1 : 0
1885
2112
  );
1886
- const warnings = parses.flatMap((p) => p.warnings ?? []);
2113
+ const warnings = [...buildWarnings, ...parses.flatMap((p) => p.warnings ?? [])];
1887
2114
  const moduleResolver = buildModuleResolver(root, new Set(parses.map((p) => p.rel)));
1888
2115
  const resolved = resolve4(parses, moduleResolver);
2116
+ checkMemoryBudget("resolve", limits.memoryBudgetMb);
1889
2117
  const nodeFileById = new Map(resolved.nodes.map((n) => [n.id, n.file]));
1890
2118
  let edges = resolved.edges;
1891
2119
  const resolvers = [...resolved.stats.resolvers];
1892
2120
  let tscStats;
1893
- const tsFiles = options.noTsc ? [] : files.filter((f) => f.lang.id === "ts" || f.lang.id === "tsx" || f.lang.id === "js").map((f) => ({ rel: f.rel, abs: f.abs }));
2121
+ let tsFiles = options.noTsc ? [] : files.filter((f) => (f.lang.id === "ts" || f.lang.id === "tsx" || f.lang.id === "js") && hashes.has(f.rel)).map((f) => ({ rel: f.rel, abs: f.abs }));
2122
+ if (limits.tscMaxFiles > 0 && tsFiles.length > limits.tscMaxFiles) {
2123
+ warnings.push(
2124
+ `typescript resolver skipped \u2014 ${tsFiles.length.toLocaleString()} TS/JS files exceed the ${limits.tscMaxFiles.toLocaleString()}-file limit; calls use the heuristic resolver (set VG_TSC_MAX_FILES to raise it, 0 to disable)`
2125
+ );
2126
+ tsFiles = [];
2127
+ }
1894
2128
  if (tsFiles.length) {
1895
2129
  const res = tsResolveEdges(root, tsFiles, resolved.nodes);
1896
2130
  if (res.stats.files > 0) {
@@ -1898,6 +2132,7 @@ async function buildGraph(options) {
1898
2132
  if (!resolvers.includes("tsc")) resolvers.unshift("tsc");
1899
2133
  tscStats = res.stats;
1900
2134
  }
2135
+ checkMemoryBudget("typescript resolution", limits.memoryBudgetMb);
1901
2136
  }
1902
2137
  const scip = options.noScip ? null : loadScipIndex(root, options.scip);
1903
2138
  let scipStats;
@@ -1912,6 +2147,7 @@ async function buildGraph(options) {
1912
2147
  const coverage = options.noCoverage ? null : loadCoverage(root, options.coverage);
1913
2148
  if (coverage) nodes = applyCoverage(nodes, coverage);
1914
2149
  const analysis = analyze(nodes, linked.edges, { cluster: options.cluster });
2150
+ checkMemoryBudget("analysis", limits.memoryBudgetMb);
1915
2151
  const languages = [...new Set(parses.map((p) => p.lang))].sort();
1916
2152
  const edgeKinds = [...new Set(analysis.edges.map((e) => e.kind))].sort();
1917
2153
  const corpusHash = computeCorpusHash(parses, hashes);
@@ -5173,33 +5409,144 @@ function boundList(items, max) {
5173
5409
  return { items: items.slice(0, max), total: items.length };
5174
5410
  }
5175
5411
  var NODE_EDGE_CAP = 50;
5412
+ var IGNORE_DIRS = /* @__PURE__ */ new Set([".git", ".vibgrate", "node_modules", "dist", "build", "out", "target", "vendor", "__pycache__"]);
5413
+ var MAX_FILE_BYTES = 1e6;
5414
+ var MAX_FILES_SCANNED = 2e4;
5415
+ var PREVIEW_CHARS = 120;
5416
+ function searchSymbols(graph, root, query, limit) {
5417
+ const q2 = query.trim();
5418
+ if (!q2) return { matches: [], moreAvailable: false, hint: "query is required" };
5419
+ const nodes = findNodes(graph, q2).filter((n) => n.kind !== "file");
5420
+ const symbolHits = nodes.slice(0, limit).map((n, i) => ({
5421
+ kind: n.kind,
5422
+ name: n.qualifiedName,
5423
+ file: n.file,
5424
+ line: n.span.start,
5425
+ score: Math.round((1 - i / Math.max(nodes.length, 1)) * 100) / 100
5426
+ }));
5427
+ const spare = limit - symbolHits.length;
5428
+ const textHits = [];
5429
+ let truncatedScan = false;
5430
+ if (spare > 0) {
5431
+ const seen = new Set(symbolHits.map((h) => `${h.file}:${h.line}`));
5432
+ truncatedScan = scanFiles(root, q2, spare, seen, textHits);
5433
+ }
5434
+ const matches = [...symbolHits, ...textHits];
5435
+ if (matches.length === 0) {
5436
+ return {
5437
+ matches,
5438
+ moreAvailable: false,
5439
+ hint: "no symbol or text match \u2014 for meaning-level questions (symptoms, relationships, what-breaks-if) use query_graph"
5440
+ };
5441
+ }
5442
+ return { matches, moreAvailable: nodes.length > symbolHits.length || truncatedScan };
5443
+ }
5444
+ function scanFiles(root, needle, budget, seen, out) {
5445
+ const lower = needle.toLowerCase();
5446
+ let scanned = 0;
5447
+ const stack = [root];
5448
+ while (stack.length) {
5449
+ const dir = stack.pop();
5450
+ let entries;
5451
+ try {
5452
+ entries = fs18.readdirSync(dir, { withFileTypes: true });
5453
+ } catch {
5454
+ continue;
5455
+ }
5456
+ entries.sort((a, b) => a.name.localeCompare(b.name));
5457
+ for (const e of entries) {
5458
+ if (e.name.startsWith(".") || IGNORE_DIRS.has(e.name)) continue;
5459
+ const abs = path18.join(dir, e.name);
5460
+ if (e.isDirectory()) {
5461
+ stack.push(abs);
5462
+ continue;
5463
+ }
5464
+ if (++scanned > MAX_FILES_SCANNED) return true;
5465
+ let text;
5466
+ try {
5467
+ if (fs18.statSync(abs).size > MAX_FILE_BYTES) continue;
5468
+ text = fs18.readFileSync(abs, "utf8");
5469
+ } catch {
5470
+ continue;
5471
+ }
5472
+ if (!text.toLowerCase().includes(lower)) continue;
5473
+ const rel2 = path18.relative(root, abs);
5474
+ const lines = text.split("\n");
5475
+ for (let i = 0; i < lines.length; i++) {
5476
+ if (!lines[i].toLowerCase().includes(lower)) continue;
5477
+ const key = `${rel2}:${i + 1}`;
5478
+ if (seen.has(key)) continue;
5479
+ seen.add(key);
5480
+ out.push({ kind: "text", file: rel2, line: i + 1, preview: lines[i].trim().slice(0, PREVIEW_CHARS) });
5481
+ if (out.length >= budget) return true;
5482
+ }
5483
+ }
5484
+ }
5485
+ return false;
5486
+ }
5176
5487
 
5177
5488
  // src/mcp/tools.ts
5178
- var embedderPromise;
5179
- function sharedEmbedder(local) {
5180
- if (!embedderPromise) embedderPromise = loadEmbedder({ local });
5181
- return embedderPromise;
5489
+ var warmEmbedder = null;
5490
+ var bgWarmStarted = false;
5491
+ function warmEmbedderInBackground(local) {
5492
+ if (local || bgWarmStarted || isModelReady()) return;
5493
+ bgWarmStarted = true;
5494
+ loadEmbedder({}).then((e) => {
5495
+ if (e) warmEmbedder = Promise.resolve(e);
5496
+ }).catch(() => {
5497
+ bgWarmStarted = false;
5498
+ });
5182
5499
  }
5500
+ function readyEmbedder(local) {
5501
+ if (warmEmbedder) return warmEmbedder;
5502
+ if (local) return warmEmbedder = loadEmbedder({ local, noDownload: true });
5503
+ if (isModelReady()) return warmEmbedder = loadEmbedder({ noDownload: true });
5504
+ warmEmbedderInBackground(local);
5505
+ return Promise.resolve(null);
5506
+ }
5507
+ async function retrieve(graph, question, budget, ctx) {
5508
+ let mode = "lexical";
5509
+ try {
5510
+ const q2 = await withTimeout(
5511
+ (async () => {
5512
+ const embedder = await readyEmbedder(ctx.local);
5513
+ if (!embedder) return null;
5514
+ const nodeVectors = await getNodeEmbeddings(graph, embedder, ctx.root);
5515
+ const r = await queryGraphSemantic(graph, question, { budget, embedder, nodeVectors });
5516
+ mode = `semantic (${embedder.id})`;
5517
+ return r;
5518
+ })(),
5519
+ SEMANTIC_BUDGET_MS,
5520
+ "semantic path over budget"
5521
+ );
5522
+ if (q2) return { q: q2, mode };
5523
+ } catch {
5524
+ }
5525
+ return { q: queryGraph(graph, question, { budget }), mode: "lexical" };
5526
+ }
5527
+ var SEMANTIC_BUDGET_MS = (() => {
5528
+ const n = Number(process.env.VG_SEMANTIC_BUDGET_MS);
5529
+ return Number.isFinite(n) && n > 0 ? n : 4e3;
5530
+ })();
5183
5531
  var obj = (properties, required = []) => ({
5184
5532
  type: "object",
5185
5533
  properties,
5186
- required,
5534
+ ...required.length ? { required } : {},
5187
5535
  additionalProperties: false
5188
5536
  });
5537
+ var RESPONSE_FORMAT = { type: "string", enum: ["concise", "detailed"] };
5538
+ var isDetailed = (args) => args.response_format === "detailed";
5539
+ var CONCISE_MATCHES = 5;
5189
5540
  var TOOLS = [
5190
- {
5191
- name: "get_graph_summary",
5192
- description: "High-level summary of the code map: counts, languages, clustering, top areas and hubs.",
5193
- inputSchema: obj({}),
5194
- handler: (graph) => summarize(graph)
5195
- },
5196
5541
  {
5197
5542
  name: "orient",
5198
- description: "ONE-SHOT orientation before changing code: the map summary + the most relevant nodes for your question + the blast radius of the top hit, in a single call. Prefer this over separate get_graph_summary + query_graph + impact_of \u2014 same answer, far fewer round-trips (each round-trip re-bills the whole conversation).",
5543
+ description: "Start here: one call returns the map overview, ranked matches for your question, and the top hit\u2019s blast radius \u2014 replaces summary+query+impact round-trips.",
5199
5544
  inputSchema: obj(
5200
5545
  {
5201
- question: { type: "string", description: "what you are about to do or look for" },
5202
- budget: { type: "number", description: "approx token budget for the context block (default 1500)" }
5546
+ question: { type: "string", maxLength: 300 },
5547
+ scope: { type: "string", description: "path prefix to orient within" },
5548
+ budget: { type: "number", description: "token budget in detailed mode (default 1500)" },
5549
+ response_format: RESPONSE_FORMAT
5203
5550
  },
5204
5551
  ["question"]
5205
5552
  ),
@@ -5207,67 +5554,93 @@ var TOOLS = [
5207
5554
  const question = String(args.question ?? "");
5208
5555
  if (!question) return { error: "bad_request", message: "question is required" };
5209
5556
  const budget = numOr(args.budget, 1500);
5210
- const embedder = await sharedEmbedder(ctx.local);
5211
- let q2;
5212
- let mode = "lexical";
5213
- if (embedder) {
5214
- const nodeVectors = await getNodeEmbeddings(graph, embedder, ctx.root);
5215
- q2 = await queryGraphSemantic(graph, question, { budget, embedder, nodeVectors });
5216
- mode = `semantic (${embedder.id})`;
5217
- } else {
5218
- q2 = queryGraph(graph, question, { budget });
5219
- }
5557
+ const { q: q2, mode } = await retrieve(graph, question, budget, ctx);
5558
+ const rawScope = String(args.scope ?? "").trim();
5559
+ const scope = rawScope === "." || rawScope === "./" ? "" : rawScope.replace(/^\.\//, "");
5560
+ const scoped = scope ? q2.matches.filter((m) => m.node.file.startsWith(scope)) : q2.matches;
5561
+ const detailed = isDetailed(args);
5220
5562
  let topImpact = null;
5221
- const top = q2.matches[0]?.node;
5222
- if (top) {
5563
+ const top = scoped[0]?.node;
5564
+ if (detailed && top) {
5223
5565
  const r = impactOf(graph, top.id, { depth: 3 });
5224
5566
  topImpact = { node: top.qualifiedName, direct: r.direct, transitive: r.transitive, affected: r.affected.slice(0, 10).map(stripId) };
5225
5567
  }
5568
+ const shown = detailed ? scoped : scoped.slice(0, CONCISE_MATCHES);
5226
5569
  return {
5227
- summary: summarize(graph),
5570
+ summary: summarize(graph, detailed ? 10 : CONCISE_MATCHES),
5228
5571
  mode,
5229
- context: q2.context,
5230
- matches: q2.matches.map((m) => ({ name: m.node.qualifiedName, kind: m.node.kind, file: m.node.file, line: m.node.span.start, score: m.score })),
5231
- topImpact
5572
+ // The fact-annotated context block is detail: concise callers navigate
5573
+ // by the ranked matches and fetch nodes on demand (plan P2).
5574
+ ...detailed ? { context: q2.context } : {},
5575
+ matches: shown.map((m) => ({ name: m.node.qualifiedName, kind: m.node.kind, file: m.node.file, line: m.node.span.start, score: m.score })),
5576
+ // A neutral availability flag, not a directive to keep querying: a
5577
+ // retrieval tool that coaches "narrow the question / see more" nudges the
5578
+ // over-navigation that re-bills the whole context on every extra step.
5579
+ ...scoped.length > shown.length ? { moreAvailable: true } : {},
5580
+ ...detailed ? { topImpact } : {}
5232
5581
  };
5233
5582
  }
5234
5583
  },
5584
+ {
5585
+ name: "search_symbols",
5586
+ description: "Find a known name or string fast: ranked symbol lookup with literal file-search fallthrough. Use first for most discovery; use query_graph for meaning.",
5587
+ inputSchema: obj(
5588
+ {
5589
+ query: { type: "string", maxLength: 120 },
5590
+ limit: { type: "number", description: "default 8, max 20" }
5591
+ },
5592
+ ["query"]
5593
+ ),
5594
+ handler: (graph, args, ctx) => {
5595
+ const limit = Math.min(20, numOr(args.limit, 8));
5596
+ return searchSymbols(graph, ctx.root, String(args.query ?? ""), limit);
5597
+ }
5598
+ },
5235
5599
  {
5236
5600
  name: "query_graph",
5237
- description: "Ask the map a natural-language question; returns a budget-bounded, fact-annotated context block plus ranked matches.",
5601
+ description: "Find code by meaning when you don\u2019t know the name: symptoms, relationships, what-breaks-if. For a known name or literal string use search_symbols.",
5238
5602
  inputSchema: obj(
5239
5603
  {
5240
- question: { type: "string", description: "the question" },
5241
- budget: { type: "number", description: "approx token budget (default 2000)" }
5604
+ question: { type: "string", maxLength: 300 },
5605
+ limit: { type: "number", description: "ranked matches to return (default 5)" },
5606
+ offset: { type: "number" },
5607
+ budget: { type: "number", description: "context-block token budget in detailed mode (default 2000)" },
5608
+ response_format: RESPONSE_FORMAT
5242
5609
  },
5243
5610
  ["question"]
5244
5611
  ),
5245
5612
  handler: async (graph, args, ctx) => {
5246
5613
  const question = String(args.question ?? "");
5247
5614
  const budget = numOr(args.budget, 2e3);
5248
- let r;
5249
- let mode = "lexical";
5250
- const embedder = await sharedEmbedder(ctx.local);
5251
- if (embedder) {
5252
- const nodeVectors = await getNodeEmbeddings(graph, embedder, ctx.root);
5253
- r = await queryGraphSemantic(graph, question, { budget, embedder, nodeVectors });
5254
- mode = `semantic (${embedder.id})`;
5255
- } else {
5256
- r = queryGraph(graph, question, { budget });
5615
+ const { q: r, mode } = await retrieve(graph, question, budget, ctx);
5616
+ if (r.matches.length === 0) {
5617
+ return { mode, matches: [], hint: "no semantic match \u2014 for a known name or literal string use search_symbols; otherwise rephrase around the behaviour you observe" };
5257
5618
  }
5619
+ const limit = numOr(args.limit, CONCISE_MATCHES);
5620
+ const offset = Math.max(0, numOrU(args.offset) ?? 0);
5621
+ const page = r.matches.slice(offset, offset + limit);
5258
5622
  return {
5259
5623
  mode,
5260
- context: r.context,
5261
- tokensEstimate: r.tokensEstimate,
5262
- matches: r.matches.map((m) => ({ name: m.node.qualifiedName, kind: m.node.kind, file: m.node.file, line: m.node.span.start, score: m.score }))
5624
+ summary: `${r.matches.length} match${r.matches.length === 1 ? "" : "es"}; top: ${r.matches[0].node.qualifiedName}`,
5625
+ // The fact-annotated context block is detail (plan P2): ranked matches
5626
+ // first, fetch nodes on demand.
5627
+ ...isDetailed(args) ? { context: r.context, tokensEstimate: r.tokensEstimate } : {},
5628
+ matches: page.map((m) => ({ name: m.node.qualifiedName, kind: m.node.kind, file: m.node.file, line: m.node.span.start, score: m.score })),
5629
+ // Neutral pagination fact (nextOffset if the model genuinely needs more),
5630
+ // not a "narrow the question" nudge that invites another discovery round.
5631
+ ...offset + page.length < r.matches.length ? { moreAvailable: true, nextOffset: offset + page.length } : {}
5263
5632
  };
5264
5633
  }
5265
5634
  },
5266
5635
  {
5267
5636
  name: "get_node",
5268
- description: "Explain a node: kind, signature, callers, callees, area, importance. Resolves by qualified/short name, file:line, glob, or id.",
5637
+ description: "Inspect one symbol: signature, callers, callees, area. Accepts name, file:line, glob or id.",
5269
5638
  inputSchema: obj(
5270
- { name: { type: "string" }, pick: { type: "number", description: "choose nth candidate when ambiguous" } },
5639
+ {
5640
+ name: { type: "string" },
5641
+ pick: { type: "number", description: "nth candidate if ambiguous" },
5642
+ response_format: RESPONSE_FORMAT
5643
+ },
5271
5644
  ["name"]
5272
5645
  ),
5273
5646
  handler: (graph, args, ctx) => {
@@ -5287,8 +5660,9 @@ var TOOLS = [
5287
5660
  if (ctx?.dedup && ctx.seen?.has(node.id)) {
5288
5661
  return { ...base, repeat: true, callsTotal: uniqueNames(index.callees(node.id).map((x) => x.node.qualifiedName)).length, calledByTotal: uniqueNames(index.callers(node.id).map((x) => x.node.qualifiedName)).length };
5289
5662
  }
5290
- const calls = boundList(uniqueNames(index.callees(node.id).map((x) => x.node.qualifiedName)), NODE_EDGE_CAP);
5291
- const calledBy = boundList(uniqueNames(index.callers(node.id).map((x) => x.node.qualifiedName)), NODE_EDGE_CAP);
5663
+ const edgeCap = isDetailed(args) ? NODE_EDGE_CAP : CONCISE_MATCHES;
5664
+ const calls = boundList(uniqueNames(index.callees(node.id).map((x) => x.node.qualifiedName)), edgeCap);
5665
+ const calledBy = boundList(uniqueNames(index.callers(node.id).map((x) => x.node.qualifiedName)), edgeCap);
5292
5666
  ctx?.seen?.add(node.id);
5293
5667
  return {
5294
5668
  ...base,
@@ -5301,13 +5675,13 @@ var TOOLS = [
5301
5675
  },
5302
5676
  {
5303
5677
  name: "find_path",
5304
- description: "Shortest connection between two nodes (how A reaches B).",
5678
+ description: "Shortest connection from a to b.",
5305
5679
  inputSchema: obj(
5306
5680
  {
5307
5681
  a: { type: "string" },
5308
5682
  b: { type: "string" },
5309
- pick_a: { type: "number", description: "choose nth candidate for a when ambiguous" },
5310
- pick_b: { type: "number", description: "choose nth candidate for b when ambiguous" }
5683
+ pick_a: { type: "number" },
5684
+ pick_b: { type: "number" }
5311
5685
  },
5312
5686
  ["a", "b"]
5313
5687
  ),
@@ -5324,25 +5698,76 @@ var TOOLS = [
5324
5698
  },
5325
5699
  {
5326
5700
  name: "impact_of",
5327
- description: "What breaks if a node changes: the reverse-reachability blast radius with per-result depth and confidence.",
5701
+ description: "Blast radius of a change: what breaks if this symbol changes \u2014 dependents, files, covering tests, risk.",
5328
5702
  inputSchema: obj(
5329
5703
  {
5330
5704
  name: { type: "string" },
5331
- depth: { type: "number", description: "max depth (default 4)" },
5332
- pick: { type: "number", description: "choose nth candidate when ambiguous" }
5705
+ change_type: { type: "string", enum: ["modify", "delete", "rename", "add_dependency"] },
5706
+ depth: { type: "number", description: "default 4" },
5707
+ pick: { type: "number", description: "nth candidate if ambiguous" },
5708
+ response_format: RESPONSE_FORMAT
5333
5709
  },
5334
5710
  ["name"]
5335
5711
  ),
5336
5712
  handler: (graph, args) => {
5337
5713
  const { node, candidates } = resolveOne(graph, String(args.name ?? ""), numOrU(args.pick));
5338
5714
  if (!node) return unresolved(candidates);
5715
+ const changeType = ["modify", "delete", "rename", "add_dependency"].includes(String(args.change_type)) ? String(args.change_type) : "modify";
5339
5716
  const r = impactOf(graph, node.id, { depth: numOr(args.depth, 4) });
5340
- return { root: r.root.name, direct: r.direct, transitive: r.transitive, affected: r.affected.slice(0, 100).map(stripId) };
5717
+ const tests = coveringTests(graph, node);
5718
+ const filesAffected = [...new Set(r.affected.map((a) => a.file))];
5719
+ const heavyChange = changeType === "delete" || changeType === "rename";
5720
+ const riskLevel = node.isHub || r.direct >= 10 || r.transitive >= 50 || heavyChange && r.direct >= 3 ? "high" : r.direct >= 3 || r.transitive >= 10 ? "medium" : "low";
5721
+ const summary = `${changeType} ${r.root.name}: ${r.direct} direct dependent${r.direct === 1 ? "" : "s"}, ${r.transitive} transitive across ${filesAffected.length} file${filesAffected.length === 1 ? "" : "s"}; ${tests.length} covering test${tests.length === 1 ? "" : "s"} \u2014 ${riskLevel} risk.`;
5722
+ return {
5723
+ root: r.root.name,
5724
+ changeType,
5725
+ directCallers: r.direct,
5726
+ transitiveCount: r.transitive,
5727
+ filesAffected: filesAffected.slice(0, 20),
5728
+ testsAffected: tests.length,
5729
+ riskLevel,
5730
+ summary,
5731
+ // The full row set is detail; concise callers act on the contract above.
5732
+ ...isDetailed(args) ? { affected: r.affected.slice(0, 100).map(stripId) } : r.affected.length > 0 ? { hint: 'response_format:"detailed" lists the affected nodes' } : {}
5733
+ };
5341
5734
  }
5342
5735
  },
5736
+ {
5737
+ name: "tests_for",
5738
+ description: "Which tests cover this symbol \u2014 is a change here tested?",
5739
+ inputSchema: obj(
5740
+ {
5741
+ name: { type: "string" },
5742
+ pick: { type: "number", description: "nth candidate if ambiguous" },
5743
+ response_format: RESPONSE_FORMAT
5744
+ },
5745
+ ["name"]
5746
+ ),
5747
+ handler: (graph, args) => {
5748
+ const { node, candidates } = resolveOne(graph, String(args.name ?? ""), numOrU(args.pick));
5749
+ if (!node) return unresolved(candidates);
5750
+ const covers = coveringTests(graph, node);
5751
+ return {
5752
+ node: node.qualifiedName,
5753
+ tested: node.tested,
5754
+ coverage: node.coverage ?? null,
5755
+ testCount: covers.length,
5756
+ covers: isDetailed(args) ? covers : covers.slice(0, 10),
5757
+ ...covers.length > 10 && !isDetailed(args) ? { moreAvailable: true } : {},
5758
+ ...covers.length === 0 ? { hint: "no covering tests found \u2014 a change here lands untested" } : {}
5759
+ };
5760
+ }
5761
+ },
5762
+ {
5763
+ name: "get_graph_summary",
5764
+ description: "Code map overview: counts, languages, top areas and hubs.",
5765
+ inputSchema: obj({}),
5766
+ handler: (graph) => summarize(graph)
5767
+ },
5343
5768
  {
5344
5769
  name: "list_areas",
5345
- description: "The natural code groupings (communities), each labelled, sized, with cohesion.",
5770
+ description: "Code areas (communities) by size.",
5346
5771
  inputSchema: obj({ limit: { type: "number" } }),
5347
5772
  // Strip `members` (raw content-hash id arrays): a model cannot use them and
5348
5773
  // on a large repo they ballooned this result past 20k tokens.
@@ -5350,23 +5775,13 @@ var TOOLS = [
5350
5775
  },
5351
5776
  {
5352
5777
  name: "list_hubs",
5353
- description: "The most-depended-on code (centrality outliers).",
5778
+ description: "Most-depended-on symbols.",
5354
5779
  inputSchema: obj({ limit: { type: "number" } }),
5355
5780
  handler: (graph, args) => graph.nodes.filter((n) => n.kind !== "file" && n.kind !== "external").sort((a, b) => b.importance - a.importance).slice(0, numOr(args.limit, 20)).map((n) => ({ name: n.qualifiedName, kind: n.kind, file: n.file, line: n.span.start, importance: n.importance, isHub: n.isHub }))
5356
5781
  },
5357
- {
5358
- name: "tests_for",
5359
- description: "Which tests cover a node (static call linkage + runtime coverage), with the linkage basis.",
5360
- inputSchema: obj({ name: { type: "string" }, pick: { type: "number", description: "choose nth candidate when ambiguous" } }, ["name"]),
5361
- handler: (graph, args) => {
5362
- const { node, candidates } = resolveOne(graph, String(args.name ?? ""), numOrU(args.pick));
5363
- if (!node) return unresolved(candidates);
5364
- return { node: node.qualifiedName, tested: node.tested, coverage: node.coverage ?? null, covers: coveringTests(graph, node) };
5365
- }
5366
- },
5367
5782
  {
5368
5783
  name: "get_facts",
5369
- description: "The deterministic open facts for a node (contract/invariant/characterization). Requires a --deep build.",
5784
+ description: "Deterministic facts for a node (contract/invariant); needs a --deep build.",
5370
5785
  inputSchema: obj({ name: { type: "string" } }, ["name"]),
5371
5786
  handler: (graph, args) => {
5372
5787
  const { node, candidates } = resolveOne(graph, String(args.name ?? ""));
@@ -5377,7 +5792,7 @@ var TOOLS = [
5377
5792
  },
5378
5793
  {
5379
5794
  name: "guide_node",
5380
- description: "Cited relevant standards/practices for a node (OWASP/CWE), honest about recommended vs conjectured.",
5795
+ description: "Cited standards/practices for a node (OWASP/CWE).",
5381
5796
  inputSchema: obj({ name: { type: "string" } }, ["name"]),
5382
5797
  handler: (graph, args) => {
5383
5798
  const { node, candidates } = resolveOne(graph, String(args.name ?? ""));
@@ -5391,17 +5806,17 @@ var TOOLS = [
5391
5806
  },
5392
5807
  {
5393
5808
  name: "check_drift",
5394
- description: 'Offline dependency inventory across npm/pypi/go (currency enrichment is the CLI\u2019s --online opt-in). Pass attribute:true to add git "who added this / who set the version" attribution for npm deps.',
5809
+ description: "Offline dependency inventory (npm/pypi/go); attribute:true adds git who-added attribution.",
5395
5810
  inputSchema: obj(
5396
- { attribute: { type: "boolean", description: "enrich npm deps with git introduction attribution (requires git; slower)" } },
5811
+ { attribute: { type: "boolean" } },
5397
5812
  []
5398
5813
  ),
5399
5814
  handler: (_graph, args, ctx) => attributedInventory(ctx.root, { attribute: args.attribute === true })
5400
5815
  },
5401
5816
  {
5402
5817
  name: "vuln_attribution",
5403
- description: "Who introduced each open vulnerability and how long you have been exposed, plus CRA remediation metrics: open exposure windows, SLA breaches, and real remediation time (MTTR) reconstructed from vulnerable versions that were later bumped out or removed in git history. Offline read of the last `vg scan --vulns`.",
5404
- inputSchema: obj({ package: { type: "string", description: "optional: restrict to one package" } }, []),
5818
+ description: "Who introduced each open vulnerability, exposure windows, and CRA remediation metrics (MTTR, SLA breaches). Reads the last `vg scan --vulns`.",
5819
+ inputSchema: obj({ package: { type: "string", description: "restrict to one package" } }, []),
5405
5820
  handler: (_graph, args, ctx) => {
5406
5821
  const data = loadVulnerabilities(ctx.root);
5407
5822
  if (!data) {
@@ -5430,9 +5845,9 @@ var TOOLS = [
5430
5845
  },
5431
5846
  {
5432
5847
  name: "list_vulnerabilities",
5433
- description: "Known vulnerabilities for installed dependencies, from the last `vg scan --vulns`. Offline read of the local scan artifact (no network); reports advisory id/CVE, severity, CVSS, and the fixed version.",
5848
+ description: "Known vulnerabilities from the last `vg scan --vulns`: id/CVE, severity, CVSS, fixed version.",
5434
5849
  inputSchema: obj(
5435
- { severity: { type: "string", enum: ["low", "moderate", "high", "critical"], description: "optional minimum severity to include" } },
5850
+ { severity: { type: "string", enum: ["low", "moderate", "high", "critical"], description: "minimum severity" } },
5436
5851
  []
5437
5852
  ),
5438
5853
  handler: (_graph, args, ctx) => {
@@ -5468,11 +5883,11 @@ var TOOLS = [
5468
5883
  },
5469
5884
  {
5470
5885
  name: "upgrade_impact",
5471
- description: 'A local "what breaks if I upgrade this" brief for a package: major-version distance + interim majors to step through, source blast radius (how many files import it), open vulnerabilities the upgrade would fix, and a recommended posture. Offline; richest after `vg scan`. Pass changelog:true to also fetch breaking-change signals from the package\'s GitHub releases (online; ignored under --local).',
5886
+ description: "What breaks if you upgrade a package: major distance, import blast radius, vulns fixed, recommended posture. changelog:true adds GitHub breaking-change signals (online).",
5472
5887
  inputSchema: obj(
5473
5888
  {
5474
- package: { type: "string", description: "package name to assess" },
5475
- changelog: { type: "boolean", description: "also fetch breaking-change signals from GitHub releases between your version and latest (online; ignored under --local)" }
5889
+ package: { type: "string" },
5890
+ changelog: { type: "boolean" }
5476
5891
  },
5477
5892
  ["package"]
5478
5893
  ),
@@ -5488,19 +5903,19 @@ var TOOLS = [
5488
5903
  },
5489
5904
  {
5490
5905
  name: "list_models",
5491
- description: "The local model fleet discovered on disk (Ollama / LM Studio / gguf). Offline, no launch.",
5906
+ description: "Local models on disk (Ollama / LM Studio / gguf).",
5492
5907
  inputSchema: obj({}),
5493
5908
  handler: () => ({ models: discoverModels() })
5494
5909
  },
5495
5910
  {
5496
5911
  name: "resolve_library",
5497
- description: "Resolve a library name/query to its canonical id + the version for YOUR project (version-correct + drift-annotated).",
5912
+ description: "Resolve a library to its canonical id and the version YOUR project uses (drift-annotated).",
5498
5913
  // Canonical §4: `query` (+ optional `context_project`). `name` kept as a back-compat alias.
5499
5914
  inputSchema: obj(
5500
5915
  {
5501
- query: { type: "string", description: "library name or natural-language query" },
5502
- name: { type: "string", description: "alias for query (back-compat)" },
5503
- context_project: { type: "string", description: "optional lockfile/manifest context (informational; the local server uses the project root)" }
5916
+ query: { type: "string", description: "library name or query" },
5917
+ name: { type: "string" },
5918
+ context_project: { type: "string" }
5504
5919
  },
5505
5920
  []
5506
5921
  ),
@@ -5543,19 +5958,19 @@ var TOOLS = [
5543
5958
  },
5544
5959
  {
5545
5960
  name: "library_docs",
5546
- description: "Version-correct, drift-annotated usage docs for a library \u2014 from the committed catalog or the installed package on disk.",
5961
+ description: "Version-correct usage docs for a library, sliced to a token budget.",
5547
5962
  // Canonical §4: `targetId`/`query`, `verbosity`, `max_tokens`. `name`/`tokens` kept as aliases.
5548
5963
  inputSchema: obj(
5549
5964
  {
5550
- targetId: { type: "string", description: "canonical id from resolve_library" },
5965
+ targetId: { type: "string", description: "id from resolve_library" },
5551
5966
  query: { type: "string", description: "library name or query" },
5552
- name: { type: "string", description: "alias for query (back-compat)" },
5553
- context_project: { type: "string", description: "optional project context (informational)" },
5554
- verbosity: { type: "string", enum: ["concise", "balanced", "exhaustive"], description: "detail level (sets the default token budget)" },
5555
- max_tokens: { type: "number", description: "explicit token budget (overrides verbosity)" },
5556
- tokens: { type: "number", description: "alias for max_tokens (back-compat)" },
5557
- enterprise_strict: { type: "boolean", description: "reserved (enterprise policy enforcement \u2014 hosted surface)" },
5558
- follow_up: { type: "boolean", description: "reserved (next slice \u2014 hosted surface)" }
5967
+ name: { type: "string" },
5968
+ context_project: { type: "string" },
5969
+ verbosity: { type: "string", enum: ["concise", "balanced", "exhaustive"] },
5970
+ max_tokens: { type: "number", description: "token budget" },
5971
+ tokens: { type: "number" },
5972
+ enterprise_strict: { type: "boolean" },
5973
+ follow_up: { type: "boolean" }
5559
5974
  },
5560
5975
  []
5561
5976
  ),
@@ -5650,15 +6065,15 @@ var TOOLS = [
5650
6065
  }
5651
6066
  ];
5652
6067
  var VERBOSITY_BUDGET = { concise: 1500, balanced: 4e3, exhaustive: 12e3 };
5653
- function summarize(graph) {
6068
+ function summarize(graph, top = 10) {
5654
6069
  return {
5655
6070
  counts: graph.meta.counts,
5656
6071
  languages: graph.meta.languages,
5657
6072
  cluster: graph.meta.cluster,
5658
6073
  resolver: graph.provenance.resolver,
5659
6074
  generatedAt: graph.generatedAt,
5660
- topAreas: [...graph.areas].sort((a, b) => b.size - a.size).slice(0, 10).map((a) => ({ id: a.id, label: a.label, size: a.size })),
5661
- topHubs: graph.nodes.filter((n) => n.isHub).sort((a, b) => b.importance - a.importance).slice(0, 10).map((n) => ({ name: n.qualifiedName, file: n.file, importance: n.importance }))
6075
+ topAreas: [...graph.areas].sort((a, b) => b.size - a.size).slice(0, top).map((a) => ({ id: a.id, label: a.label, size: a.size })),
6076
+ topHubs: graph.nodes.filter((n) => n.isHub).sort((a, b) => b.importance - a.importance).slice(0, top).map((n) => ({ name: n.qualifiedName, file: n.file, importance: n.importance }))
5662
6077
  };
5663
6078
  }
5664
6079
  function stripId(item) {
@@ -5689,6 +6104,19 @@ function numOrU(v) {
5689
6104
  function uniqueNames(xs) {
5690
6105
  return [...new Set(xs)];
5691
6106
  }
6107
+ var HOT_TOOLS = ["orient", "search_symbols", "query_graph", "get_node"];
6108
+ function deferredToolNames() {
6109
+ const hot = new Set(HOT_TOOLS);
6110
+ return TOOLS.map((t) => t.name).filter((n) => !hot.has(n));
6111
+ }
6112
+ function navigationToolsetConfig(serverName = "vibgrate") {
6113
+ return {
6114
+ type: "mcp_toolset",
6115
+ mcp_server_name: serverName,
6116
+ default_config: { defer_loading: true },
6117
+ configs: Object.fromEntries(HOT_TOOLS.map((n) => [n, { defer_loading: false }]))
6118
+ };
6119
+ }
5692
6120
  var LEDGER = "savings.jsonl";
5693
6121
  var PER_FILE_TOKENS = 400;
5694
6122
  function ledgerPath(root) {
@@ -5811,7 +6239,13 @@ function createServer(source, opts = {}) {
5811
6239
  const seen = /* @__PURE__ */ new Set();
5812
6240
  const server = new Server(
5813
6241
  { name: "vg", version: VERSION },
5814
- { capabilities: { tools: {} } }
6242
+ {
6243
+ capabilities: { tools: {} },
6244
+ // Routing guidance once at the server level (hosts that surface
6245
+ // `instructions` get it at zero per-step schema cost): the flashlight
6246
+ // vs the map.
6247
+ instructions: 'vg is a code map. Use search_symbols to find a known name or literal string fast. Use orient/query_graph for meaning: symptoms, relationships, and what-breaks-if. Responses are concise by default; pass response_format:"detailed" only when a node proves load-bearing. Navigate as little as possible: one good search/query usually locates the code. As soon as you have the file and line, read that file and make the edit \u2014 do not call further graph tools unless the edit fails or the match was wrong.'
6248
+ }
5815
6249
  );
5816
6250
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
5817
6251
  tools: TOOLS.map((t) => ({
@@ -5850,6 +6284,7 @@ function createServer(source, opts = {}) {
5850
6284
  async function serveStdio(graphPath, opts = {}) {
5851
6285
  const source = new GraphSource(graphPath, opts.refresh !== false);
5852
6286
  const server = createServer(source, opts);
6287
+ warmEmbedderInBackground(opts.local);
5853
6288
  await server.connect(new StdioServerTransport());
5854
6289
  }
5855
6290
  function sleep(ms) {
@@ -6007,9 +6442,9 @@ function assistantById(id) {
6007
6442
  }
6008
6443
  function detectServeLaunch(which = whichOnPath) {
6009
6444
  const vg = which("vg");
6010
- if (vg && isOwnBinary(vg)) return { command: "vg", args: ["serve"] };
6445
+ if (vg && isInstalledOwnBinary(vg)) return { command: "vg", args: ["serve"] };
6011
6446
  const vibgrate = which("vibgrate");
6012
- if (vibgrate && isOwnBinary(vibgrate)) {
6447
+ if (vibgrate && isInstalledOwnBinary(vibgrate)) {
6013
6448
  return {
6014
6449
  command: "vibgrate",
6015
6450
  args: ["serve"],
@@ -6074,6 +6509,18 @@ function writeFileEnsured(file, content) {
6074
6509
  fs18.mkdirSync(path18.dirname(file), { recursive: true });
6075
6510
  fs18.writeFileSync(file, content);
6076
6511
  }
6512
+ function writeNavigationConfig(root) {
6513
+ const rel2 = path18.join(".vibgrate", "mcp-navigation.json");
6514
+ const doc = {
6515
+ _readme: "Deferred-loading config for agents embedding `vg serve` via the Claude API (defer_loading + tool-search). The hot navigation core stays in context; the rest load on demand \u2014 ~350-450 schema tokens/step vs ~1,881 for the full set. Hosts without defer_loading ignore this and serve the whole (already optimized) tool set. See docs/graph/VG-NAVIGATION-PROFILE.md.",
6516
+ hot_core: [...HOT_TOOLS],
6517
+ deferred: deferredToolNames(),
6518
+ toolset: navigationToolsetConfig()
6519
+ };
6520
+ writeFileEnsured(path18.join(root, rel2), `${JSON.stringify(doc, null, 2)}
6521
+ `);
6522
+ return rel2;
6523
+ }
6077
6524
  function upsertMcp(file, target, launch) {
6078
6525
  const config = readJson(file);
6079
6526
  const entry = mcpServerEntry(launch);
@@ -6288,6 +6735,6 @@ function spdx(ctx) {
6288
6735
  return JSON.stringify(doc, null, 2) + "\n";
6289
6736
  }
6290
6737
 
6291
- export { ASSISTANTS, CliError, ECOSYSTEMS, ExitCode, FREE_PACK, GraphIndex, GraphSource, NPX_INVOCATION, SCHEMA_VERSION, SKIP_DIRS, TOOLS, UsageError, addLibrary, analyze, applyCoverage, applyStaticTestLinkage, assessDocQuality, assistantById, availableRegionIds, buildFacts, buildGraph, buildModuleResolver, cacheDir, catalogPath, clearModelCache, clearStoredCredentials, cosine, countPending, coveringTests, createServer, createWorkspaceDsn, credentialsPath, dashHostForIngestHost, decodeScipIndex, defaultGraphPath, detectRunner, detectServeLaunch, discover, discoverModels, driftCount, driftFor, dsnCommand, embeddingsCached, enrichOnline, ensureGitignored, exportGraph, fetchHostedDocs, findGitRoot, findNodes, formatForExt, getNodeEmbeddings, gitignoreEntryForCredentials, groundGraph, hasDrift, identifierParts, impactOf, installAssistant, inventory, isModelReady, isTestFile, libDir, libId, loadCatalog, loadCoverage, loadEmbedder, loadGraph, loadSnapshot, localApiSurface, localPackageDocs, modelCacheInfo, nodeById, nodeEmbedText, parseDsn, parseGraph, parseJsonc, probeFreshness, publishPrivateLibrary, pushCommand, queryGraph, queryGraphSemantic, readDoc, readSavings, readScanArtifact, recordSaving, refreshIfStale, relativeResolver, renderHtml, renderReport, resolveCliInvocation, resolveDsn, resolveEmbedModel, resolveIngestHost, resolveLib, resolveOne, resolveVersion, saveCatalog, savingsRecorded, scipEdges, selectForBudget, serializeGraph, serveStdio, shortestPath, stableStringify, symbolsFromApi, testsToRun, unavailableMessage, uninstallAssistant, uploadScanArtifact, usageError, verifyDeterminism, vibgrateDir, writeArtifacts, writeSnapshot, writeStoredCredentials };
6292
- //# sourceMappingURL=chunk-WGXDYBAN.js.map
6293
- //# sourceMappingURL=chunk-WGXDYBAN.js.map
6738
+ export { ASSISTANTS, CliError, ECOSYSTEMS, ExitCode, FREE_PACK, GraphIndex, GraphSource, NPX_INVOCATION, ResourceLimitError, SCHEMA_VERSION, SKIP_DIRS, SKIP_FILES, TOOLS, UsageError, addLibrary, analyze, applyCoverage, applyStaticTestLinkage, assessDocQuality, assistantById, availableRegionIds, buildFacts, buildGraph, buildModuleResolver, cacheDir, catalogPath, clearModelCache, clearStoredCredentials, cosine, countPending, coveringTests, createServer, createWorkspaceDsn, credentialsPath, dashHostForIngestHost, decodeScipIndex, defaultGraphPath, detectRunner, detectServeLaunch, discover, discoverModels, driftCount, driftFor, dsnCommand, embeddingsCached, enrichOnline, ensureGitignored, exportGraph, fetchHostedDocs, findGitRoot, findNodes, formatForExt, getNodeEmbeddings, gitignoreEntryForCredentials, groundGraph, hasDrift, identifierParts, impactOf, installAssistant, inventory, isModelReady, isTestFile, libDir, libId, loadCatalog, loadCoverage, loadEmbedder, loadGraph, loadSnapshot, localApiSurface, localPackageDocs, modelCacheInfo, nodeById, nodeEmbedText, parseDsn, parseGraph, parseJsonc, probeFreshness, publishPrivateLibrary, pushCommand, queryGraph, queryGraphSemantic, readDoc, readSavings, readScanArtifact, recordSaving, refreshIfStale, relativeResolver, renderHtml, renderReport, resolveCliInvocation, resolveDsn, resolveEmbedModel, resolveIngestHost, resolveLib, resolveLimits, resolveOne, resolveVersion, saveCatalog, savingsRecorded, scipEdges, selectForBudget, serializeGraph, serveStdio, shortestPath, stableStringify, symbolsFromApi, testsToRun, unavailableMessage, uninstallAssistant, uploadScanArtifact, usageError, verifyDeterminism, vibgrateDir, writeArtifacts, writeNavigationConfig, writeSnapshot, writeStoredCredentials };
6739
+ //# sourceMappingURL=chunk-JL7FIC6W.js.map
6740
+ //# sourceMappingURL=chunk-JL7FIC6W.js.map