@vibgrate/cli 2026.708.3 → 2026.709.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,8 @@
1
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-QUELGOCR.js';
3
- import * as fs18 from 'fs';
4
- import { execFileSync } from 'child_process';
5
- import * as path18 from 'path';
2
+ import { writeTextFile, pathExists, readJsonFile, computeUpgradeImpact, getChangelogSignals, gitHistoryAvailable, buildVersionTimelines, findPackageTimeline, severityRank, projectTypeToVulnEcosystem, VERSION } from './chunk-PIWHQSAR.js';
3
+ import * as fs19 from 'fs';
4
+ import { spawnSync, execFileSync } from 'child_process';
5
+ import * as path19 from 'path';
6
6
  import ignore from 'ignore';
7
7
  import * as v8 from 'v8';
8
8
  import Graph from 'graphology';
@@ -20,6 +20,7 @@ import chalk from 'chalk';
20
20
  import * as zlib from 'zlib';
21
21
  import { promisify } from 'util';
22
22
  import { encode, decode } from 'gpt-tokenizer/encoding/cl100k_base';
23
+ import { Worker } from 'worker_threads';
23
24
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
24
25
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
25
26
  import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
@@ -38,9 +39,9 @@ function whichOnPath(cmd) {
38
39
  }
39
40
  function isOwnBinary(binPath) {
40
41
  try {
41
- const real = fs18.realpathSync(binPath);
42
+ const real = fs19.realpathSync(binPath);
42
43
  if (/[\\/]@vibgrate[\\/]cli[\\/]/.test(real)) return true;
43
- const head = fs18.readFileSync(real, { encoding: "utf8" }).slice(0, 2048);
44
+ const head = fs19.readFileSync(real, { encoding: "utf8" }).slice(0, 2048);
44
45
  return head.includes("@vibgrate/cli") || head.includes("vibgrate");
45
46
  } catch {
46
47
  return false;
@@ -50,7 +51,7 @@ function isEphemeralNpxBinary(binPath) {
50
51
  const NPX_SEGMENT = /[\\/]_npx[\\/]/;
51
52
  if (NPX_SEGMENT.test(binPath)) return true;
52
53
  try {
53
- return NPX_SEGMENT.test(fs18.realpathSync(binPath));
54
+ return NPX_SEGMENT.test(fs19.realpathSync(binPath));
54
55
  } catch {
55
56
  return false;
56
57
  }
@@ -216,19 +217,19 @@ var SKIP_FILES = /* @__PURE__ */ new Set([
216
217
  "flake.lock"
217
218
  ]);
218
219
  function toPosix(p) {
219
- return p.split(path18.sep).join("/");
220
+ return p.split(path19.sep).join("/");
220
221
  }
221
222
  function buildRootIgnore(root, exclude) {
222
223
  const ig = ignore();
223
- const gitignorePath = path18.join(root, ".gitignore");
224
- if (fs18.existsSync(gitignorePath)) {
225
- ig.add(fs18.readFileSync(gitignorePath, "utf8"));
224
+ const gitignorePath = path19.join(root, ".gitignore");
225
+ if (fs19.existsSync(gitignorePath)) {
226
+ ig.add(fs19.readFileSync(gitignorePath, "utf8"));
226
227
  }
227
228
  if (exclude.length) ig.add(exclude);
228
229
  return ig;
229
230
  }
230
231
  function discover(options) {
231
- const root = path18.resolve(options.root);
232
+ const root = path19.resolve(options.root);
232
233
  const onlyLangs = (options.only ?? []).filter(Boolean);
233
234
  const allowLang = (lang) => onlyLangs.length === 0 || onlyLangs.includes(lang.id);
234
235
  for (const id of onlyLangs) {
@@ -237,27 +238,27 @@ function discover(options) {
237
238
  }
238
239
  }
239
240
  const rootIg = buildRootIgnore(root, options.exclude ?? []);
240
- const scopeAbs = (options.paths && options.paths.length ? options.paths.map((p) => path18.resolve(root, p)) : [root]).filter((p) => fs18.existsSync(p));
241
+ const scopeAbs = (options.paths && options.paths.length ? options.paths.map((p) => path19.resolve(root, p)) : [root]).filter((p) => fs19.existsSync(p));
241
242
  const found = /* @__PURE__ */ new Map();
242
243
  const considerFile = (abs) => {
243
- const rel2 = toPosix(path18.relative(root, abs));
244
+ const rel2 = toPosix(path19.relative(root, abs));
244
245
  if (rel2.startsWith("..")) return;
245
246
  if (rel2 === "" || rootIg.ignores(rel2)) return;
246
- if (SKIP_FILES.has(path18.basename(abs).toLowerCase())) return;
247
- const lang = langForExtension(path18.extname(abs));
247
+ if (SKIP_FILES.has(path19.basename(abs).toLowerCase())) return;
248
+ const lang = langForExtension(path19.extname(abs));
248
249
  if (!lang || !allowLang(lang)) return;
249
250
  found.set(rel2, { rel: rel2, abs, lang });
250
251
  };
251
252
  const walk2 = (dir) => {
252
253
  let entries;
253
254
  try {
254
- entries = fs18.readdirSync(dir, { withFileTypes: true });
255
+ entries = fs19.readdirSync(dir, { withFileTypes: true });
255
256
  } catch {
256
257
  return;
257
258
  }
258
259
  for (const entry of entries) {
259
- const abs = path18.join(dir, entry.name);
260
- const rel2 = toPosix(path18.relative(root, abs));
260
+ const abs = path19.join(dir, entry.name);
261
+ const rel2 = toPosix(path19.relative(root, abs));
261
262
  if (entry.isDirectory()) {
262
263
  if (SKIP_DIRS.has(entry.name)) continue;
263
264
  if (rel2 && rootIg.ignores(`${rel2}/`)) continue;
@@ -268,7 +269,7 @@ function discover(options) {
268
269
  }
269
270
  };
270
271
  for (const scope of scopeAbs) {
271
- const stat = fs18.statSync(scope);
272
+ const stat = fs19.statSync(scope);
272
273
  if (stat.isDirectory()) walk2(scope);
273
274
  else if (stat.isFile()) considerFile(scope);
274
275
  }
@@ -434,10 +435,10 @@ function buildModuleResolver(root, relSet) {
434
435
  resolve(fromRel, source) {
435
436
  if (source.includes("\\")) source = source.split("\\").join(".");
436
437
  if (source.startsWith("./") || source.startsWith("../")) {
437
- return probe(posixJoin(path18.posix.dirname(fromRel), source));
438
+ return probe(posixJoin(path19.posix.dirname(fromRel), source));
438
439
  }
439
440
  if (/\.[A-Za-z0-9]+$/.test(source) && !source.startsWith("<")) {
440
- const hit = probe(posixJoin(path18.posix.dirname(fromRel), source));
441
+ const hit = probe(posixJoin(path19.posix.dirname(fromRel), source));
441
442
  if (hit) return hit;
442
443
  }
443
444
  if (source.startsWith(".")) {
@@ -508,8 +509,8 @@ function resolvePyRelative(probe, fromRel, source) {
508
509
  let dots = 0;
509
510
  while (source[dots] === ".") dots++;
510
511
  const rest = source.slice(dots);
511
- let dir = path18.posix.dirname(fromRel);
512
- for (let i = 1; i < dots; i++) dir = path18.posix.dirname(dir);
512
+ let dir = path19.posix.dirname(fromRel);
513
+ for (let i = 1; i < dots; i++) dir = path19.posix.dirname(dir);
513
514
  const sub = rest.replace(/\./g, "/");
514
515
  return probe(sub ? posixJoin(dir, sub) : dir);
515
516
  }
@@ -517,7 +518,7 @@ function relativeResolver(relSet) {
517
518
  const probe = makeProbe(relSet);
518
519
  return {
519
520
  resolve(fromRel, source) {
520
- if (source.startsWith(".")) return probe(posixJoin(path18.posix.dirname(fromRel), source));
521
+ if (source.startsWith(".")) return probe(posixJoin(path19.posix.dirname(fromRel), source));
521
522
  return resolveDotted(probe, source);
522
523
  }
523
524
  };
@@ -535,12 +536,12 @@ function makeProbe(relSet) {
535
536
  };
536
537
  }
537
538
  function normalizeRel(p) {
538
- const norm = path18.posix.normalize(p);
539
+ const norm = path19.posix.normalize(p);
539
540
  const stripped = norm.startsWith("./") ? norm.slice(2) : norm;
540
541
  return stripped.startsWith("..") ? "" : stripped;
541
542
  }
542
543
  function posixJoin(a, b) {
543
- return path18.posix.normalize(path18.posix.join(a, b));
544
+ return path19.posix.normalize(path19.posix.join(a, b));
544
545
  }
545
546
  function matchPattern(pattern, source) {
546
547
  const star = pattern.indexOf("*");
@@ -553,8 +554,8 @@ function matchPattern(pattern, source) {
553
554
  }
554
555
  function loadTsconfigPaths(root) {
555
556
  for (const name of ["tsconfig.base.json", "tsconfig.json"]) {
556
- const file = path18.join(root, name);
557
- if (fs18.existsSync(file)) {
557
+ const file = path19.join(root, name);
558
+ if (fs19.existsSync(file)) {
558
559
  const merged = readTsconfigChain(root, file, /* @__PURE__ */ new Set());
559
560
  if (merged && merged.paths.length) return merged;
560
561
  }
@@ -566,21 +567,21 @@ function readTsconfigChain(root, file, seen) {
566
567
  seen.add(file);
567
568
  let cfg;
568
569
  try {
569
- cfg = parseJsonc(fs18.readFileSync(file, "utf8"));
570
+ cfg = parseJsonc(fs19.readFileSync(file, "utf8"));
570
571
  } catch {
571
572
  return null;
572
573
  }
573
- const dir = path18.dirname(file);
574
+ const dir = path19.dirname(file);
574
575
  let base = { baseUrlRel: "", paths: [] };
575
576
  if (cfg.extends && cfg.extends.startsWith(".")) {
576
- const extPath = path18.resolve(dir, cfg.extends.endsWith(".json") ? cfg.extends : `${cfg.extends}.json`);
577
- if (fs18.existsSync(extPath)) {
577
+ const extPath = path19.resolve(dir, cfg.extends.endsWith(".json") ? cfg.extends : `${cfg.extends}.json`);
578
+ if (fs19.existsSync(extPath)) {
578
579
  const inherited = readTsconfigChain(root, extPath, seen);
579
580
  if (inherited) base = inherited;
580
581
  }
581
582
  }
582
583
  const co = cfg.compilerOptions ?? {};
583
- const baseUrlRel = co.baseUrl !== void 0 ? path18.relative(root, path18.resolve(dir, co.baseUrl)).split(path18.sep).join("/") : base.baseUrlRel;
584
+ const baseUrlRel = co.baseUrl !== void 0 ? path19.relative(root, path19.resolve(dir, co.baseUrl)).split(path19.sep).join("/") : base.baseUrlRel;
584
585
  const paths = [...base.paths];
585
586
  if (co.paths) for (const [k, v] of Object.entries(co.paths)) paths.push([k, v]);
586
587
  return { baseUrlRel, paths };
@@ -588,27 +589,27 @@ function readTsconfigChain(root, file, seen) {
588
589
  function loadWorkspacePackages(root) {
589
590
  const map = /* @__PURE__ */ new Map();
590
591
  const globs = [];
591
- const rootPkg = path18.join(root, "package.json");
592
- if (fs18.existsSync(rootPkg)) {
592
+ const rootPkg = path19.join(root, "package.json");
593
+ if (fs19.existsSync(rootPkg)) {
593
594
  try {
594
- const pkg = JSON.parse(fs18.readFileSync(rootPkg, "utf8"));
595
+ const pkg = JSON.parse(fs19.readFileSync(rootPkg, "utf8"));
595
596
  const ws = Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces?.packages;
596
597
  if (ws) globs.push(...ws);
597
598
  } catch {
598
599
  }
599
600
  }
600
- const pnpmWs = path18.join(root, "pnpm-workspace.yaml");
601
- if (fs18.existsSync(pnpmWs)) {
602
- for (const line of fs18.readFileSync(pnpmWs, "utf8").split("\n")) {
601
+ const pnpmWs = path19.join(root, "pnpm-workspace.yaml");
602
+ if (fs19.existsSync(pnpmWs)) {
603
+ for (const line of fs19.readFileSync(pnpmWs, "utf8").split("\n")) {
603
604
  const m = /^\s*-\s*['"]?([^'"\n]+)['"]?\s*$/.exec(line);
604
605
  if (m) globs.push(m[1].trim());
605
606
  }
606
607
  }
607
608
  for (const dir of expandWorkspaceGlobs(root, globs)) {
608
- const pj = path18.join(dir, "package.json");
609
+ const pj = path19.join(dir, "package.json");
609
610
  try {
610
- const name = JSON.parse(fs18.readFileSync(pj, "utf8")).name;
611
- if (name) map.set(name, path18.relative(root, dir).split(path18.sep).join("/"));
611
+ const name = JSON.parse(fs19.readFileSync(pj, "utf8")).name;
612
+ if (name) map.set(name, path19.relative(root, dir).split(path19.sep).join("/"));
612
613
  } catch {
613
614
  }
614
615
  }
@@ -618,10 +619,10 @@ function expandWorkspaceGlobs(root, globs) {
618
619
  const out = /* @__PURE__ */ new Set();
619
620
  for (const glob of globs) {
620
621
  const clean = glob.replace(/\/\*\*$/, "").replace(/\/\*$/, "");
621
- const baseDir = path18.join(root, clean);
622
+ const baseDir = path19.join(root, clean);
622
623
  const recurse = glob.endsWith("**");
623
624
  if (glob === clean) {
624
- if (fs18.existsSync(path18.join(baseDir, "package.json"))) out.add(baseDir);
625
+ if (fs19.existsSync(path19.join(baseDir, "package.json"))) out.add(baseDir);
625
626
  continue;
626
627
  }
627
628
  collectPackageDirs(baseDir, recurse ? 4 : 1, out);
@@ -632,14 +633,14 @@ function collectPackageDirs(dir, depth, out) {
632
633
  if (depth < 0) return;
633
634
  let entries;
634
635
  try {
635
- entries = fs18.readdirSync(dir, { withFileTypes: true });
636
+ entries = fs19.readdirSync(dir, { withFileTypes: true });
636
637
  } catch {
637
638
  return;
638
639
  }
639
640
  for (const e of entries) {
640
641
  if (!e.isDirectory() || e.name === "node_modules" || e.name.startsWith(".")) continue;
641
- const child = path18.join(dir, e.name);
642
- if (fs18.existsSync(path18.join(child, "package.json"))) out.add(child);
642
+ const child = path19.join(dir, e.name);
643
+ if (fs19.existsSync(path19.join(child, "package.json"))) out.add(child);
643
644
  else collectPackageDirs(child, depth - 1, out);
644
645
  }
645
646
  }
@@ -1137,14 +1138,14 @@ var DEFAULT_PATHS = [
1137
1138
  ];
1138
1139
  function loadCoverage(root, explicit) {
1139
1140
  const candidates = (explicit && explicit.length ? explicit : DEFAULT_PATHS).map(
1140
- (p) => path18.resolve(root, p)
1141
+ (p) => path19.resolve(root, p)
1141
1142
  );
1142
- const found = candidates.filter((p) => fs18.existsSync(p));
1143
+ const found = candidates.filter((p) => fs19.existsSync(p));
1143
1144
  if (found.length === 0) return null;
1144
1145
  const map = /* @__PURE__ */ new Map();
1145
1146
  for (const file of found) {
1146
1147
  try {
1147
- const text = fs18.readFileSync(file, "utf8");
1148
+ const text = fs19.readFileSync(file, "utf8");
1148
1149
  if (file.endsWith(".json")) mergeIstanbul(map, text, root);
1149
1150
  else mergeLcov(map, text, root);
1150
1151
  } catch {
@@ -1153,8 +1154,8 @@ function loadCoverage(root, explicit) {
1153
1154
  return map.size ? map : null;
1154
1155
  }
1155
1156
  function rel(root, p) {
1156
- const abs = path18.isAbsolute(p) ? p : path18.resolve(root, p);
1157
- return path18.relative(root, abs).split(path18.sep).join("/");
1157
+ const abs = path19.isAbsolute(p) ? p : path19.resolve(root, p);
1158
+ return path19.relative(root, abs).split(path19.sep).join("/");
1158
1159
  }
1159
1160
  function bump(map, file, line, hits) {
1160
1161
  let lh = map.get(file);
@@ -1490,7 +1491,7 @@ async function parseInline(files, options) {
1490
1491
  onProgress?.(0, files.length);
1491
1492
  for (const file of files) {
1492
1493
  try {
1493
- const source = fs18.readFileSync(file.abs, "utf8");
1494
+ const source = fs19.readFileSync(file.abs, "utf8");
1494
1495
  out.push(await parseSource(file.rel, file.lang.id, source));
1495
1496
  } catch (err) {
1496
1497
  out.push(emptyParse(file, `parse failed: ${err.message}`));
@@ -1545,9 +1546,9 @@ function isWorkerOom(err) {
1545
1546
  return e?.code === "ERR_WORKER_OUT_OF_MEMORY" || /out of memory/i.test(e?.message ?? "");
1546
1547
  }
1547
1548
  function resolveWorkerFile() {
1548
- const here = path18.dirname(fileURLToPath(import.meta.url));
1549
- const candidate = path18.join(here, "parse-worker.js");
1550
- return fs18.existsSync(candidate) ? candidate : null;
1549
+ const here = path19.dirname(fileURLToPath(import.meta.url));
1550
+ const candidate = path19.join(here, "parse-worker.js");
1551
+ return fs19.existsSync(candidate) ? candidate : null;
1551
1552
  }
1552
1553
  function chunk(items, buckets) {
1553
1554
  const out = Array.from({ length: Math.min(buckets, items.length || 1) }, () => []);
@@ -1585,7 +1586,7 @@ function resolve4(parses, resolver) {
1585
1586
  addNode(baseNodes, {
1586
1587
  id: fileId,
1587
1588
  kind: "file",
1588
- name: path18.posix.basename(p.rel),
1589
+ name: path19.posix.basename(p.rel),
1589
1590
  qualifiedName: p.rel,
1590
1591
  file: p.rel,
1591
1592
  span: { start: 1, end: 1 },
@@ -2002,14 +2003,14 @@ function enclosing3(nodes, line) {
2002
2003
  return best;
2003
2004
  }
2004
2005
  function normalize2(p) {
2005
- return path18.resolve(p).split(path18.sep).join("/");
2006
+ return path19.resolve(p).split(path19.sep).join("/");
2006
2007
  }
2007
2008
  var CACHE_VERSION = "vg-parse-cache/2";
2008
2009
  function cacheDir(root) {
2009
- return path18.join(root, ".vibgrate", "cache");
2010
+ return path19.join(root, ".vibgrate", "cache");
2010
2011
  }
2011
2012
  function cachePath(root) {
2012
- return path18.join(cacheDir(root), "parse-cache.json");
2013
+ return path19.join(cacheDir(root), "parse-cache.json");
2013
2014
  }
2014
2015
  function loadCache(root, opts) {
2015
2016
  const file = cachePath(root);
@@ -2019,9 +2020,9 @@ function loadCache(root, opts) {
2019
2020
  grammars: opts.grammars,
2020
2021
  entries: {}
2021
2022
  };
2022
- if (!opts.disabled && fs18.existsSync(file)) {
2023
+ if (!opts.disabled && fs19.existsSync(file)) {
2023
2024
  try {
2024
- const loaded = JSON.parse(fs18.readFileSync(file, "utf8"));
2025
+ const loaded = JSON.parse(fs19.readFileSync(file, "utf8"));
2025
2026
  if (loaded.version === CACHE_VERSION && loaded.toolVersion === opts.toolVersion && loaded.grammars === opts.grammars && loaded.entries) {
2026
2027
  data = loaded;
2027
2028
  }
@@ -2042,8 +2043,8 @@ function loadCache(root, opts) {
2042
2043
  }
2043
2044
  },
2044
2045
  save() {
2045
- fs18.mkdirSync(cacheDir(root), { recursive: true });
2046
- fs18.writeFileSync(file, stableStringify(data, 0));
2046
+ fs19.mkdirSync(cacheDir(root), { recursive: true });
2047
+ fs19.writeFileSync(file, stableStringify(data, 0));
2047
2048
  }
2048
2049
  };
2049
2050
  }
@@ -2075,7 +2076,7 @@ function epistemicBreakdown(edges) {
2075
2076
  // src/engine/build.ts
2076
2077
  async function buildGraph(options) {
2077
2078
  const start = nowMs();
2078
- const root = path18.resolve(options.root);
2079
+ const root = path19.resolve(options.root);
2079
2080
  const files = discover({
2080
2081
  root,
2081
2082
  only: options.only,
@@ -2103,7 +2104,7 @@ async function buildGraph(options) {
2103
2104
  for (const file of files) {
2104
2105
  let stat;
2105
2106
  try {
2106
- stat = fs18.statSync(file.abs);
2107
+ stat = fs19.statSync(file.abs);
2107
2108
  } catch {
2108
2109
  continue;
2109
2110
  }
@@ -2121,7 +2122,7 @@ async function buildGraph(options) {
2121
2122
  }
2122
2123
  let hash = "";
2123
2124
  try {
2124
- hash = hashBytes(fs18.readFileSync(file.abs));
2125
+ hash = hashBytes(fs19.readFileSync(file.abs));
2125
2126
  } catch {
2126
2127
  continue;
2127
2128
  }
@@ -2213,7 +2214,7 @@ async function buildGraph(options) {
2213
2214
  toolchain
2214
2215
  },
2215
2216
  meta: {
2216
- root: path18.basename(root) === "" ? "." : ".",
2217
+ root: path19.basename(root) === "" ? "." : ".",
2217
2218
  languages,
2218
2219
  counts: {
2219
2220
  nodes: analysis.nodes.length,
@@ -2259,14 +2260,14 @@ function toRepoRel(p) {
2259
2260
  function loadScipIndex(root, explicit) {
2260
2261
  const candidates = [
2261
2262
  explicit,
2262
- path18.join(root, "index.scip"),
2263
- path18.join(root, ".vibgrate", "index.scip")
2263
+ path19.join(root, "index.scip"),
2264
+ path19.join(root, ".vibgrate", "index.scip")
2264
2265
  ].filter((p) => Boolean(p));
2265
2266
  for (const file of candidates) {
2266
- const abs = path18.isAbsolute(file) ? file : path18.resolve(root, file);
2267
- if (!fs18.existsSync(abs)) continue;
2267
+ const abs = path19.isAbsolute(file) ? file : path19.resolve(root, file);
2268
+ if (!fs19.existsSync(abs)) continue;
2268
2269
  try {
2269
- const index = decodeScipIndex(new Uint8Array(fs18.readFileSync(abs)));
2270
+ const index = decodeScipIndex(new Uint8Array(fs19.readFileSync(abs)));
2270
2271
  if (index.documents.length) {
2271
2272
  return { index, tool: index.toolVersion ? `${index.toolName} ${index.toolVersion}` : index.toolName };
2272
2273
  }
@@ -2427,46 +2428,46 @@ function esc(s) {
2427
2428
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
2428
2429
  }
2429
2430
  function vibgrateDir(root) {
2430
- return path18.join(root, ".vibgrate");
2431
+ return path19.join(root, ".vibgrate");
2431
2432
  }
2432
2433
  function defaultGraphPath(root) {
2433
- return path18.join(vibgrateDir(root), "graph.json");
2434
+ return path19.join(vibgrateDir(root), "graph.json");
2434
2435
  }
2435
2436
  function writeArtifacts(graph, options) {
2436
2437
  const dir = vibgrateDir(options.root);
2437
- fs18.mkdirSync(dir, { recursive: true });
2438
+ fs19.mkdirSync(dir, { recursive: true });
2438
2439
  const graphPath = options.graphPath ?? defaultGraphPath(options.root);
2439
- fs18.mkdirSync(path18.dirname(graphPath), { recursive: true });
2440
+ fs19.mkdirSync(path19.dirname(graphPath), { recursive: true });
2440
2441
  const tmp = `${graphPath}.${process.pid}.tmp`;
2441
2442
  try {
2442
- fs18.writeFileSync(tmp, serializeGraph(graph));
2443
- fs18.renameSync(tmp, graphPath);
2443
+ fs19.writeFileSync(tmp, serializeGraph(graph));
2444
+ fs19.renameSync(tmp, graphPath);
2444
2445
  } catch (err) {
2445
- fs18.rmSync(tmp, { force: true });
2446
+ fs19.rmSync(tmp, { force: true });
2446
2447
  throw err;
2447
2448
  }
2448
2449
  const written = { graphPath };
2449
2450
  if (options.report !== false) {
2450
- const reportPath = path18.join(dir, "GRAPH_REPORT.md");
2451
- fs18.writeFileSync(reportPath, renderReport(graph));
2451
+ const reportPath = path19.join(dir, "GRAPH_REPORT.md");
2452
+ fs19.writeFileSync(reportPath, renderReport(graph));
2452
2453
  written.reportPath = reportPath;
2453
2454
  }
2454
2455
  if (options.html !== false) {
2455
- const htmlPath = path18.join(dir, "graph.html");
2456
- fs18.writeFileSync(htmlPath, renderHtml(graph));
2456
+ const htmlPath = path19.join(dir, "graph.html");
2457
+ fs19.writeFileSync(htmlPath, renderHtml(graph));
2457
2458
  written.htmlPath = htmlPath;
2458
2459
  }
2459
2460
  if (graph.facts && graph.facts.length) {
2460
- const factsPath = path18.join(dir, "facts.jsonl");
2461
- fs18.writeFileSync(factsPath, graph.facts.map((f) => JSON.stringify(f)).join("\n") + "\n");
2461
+ const factsPath = path19.join(dir, "facts.jsonl");
2462
+ fs19.writeFileSync(factsPath, graph.facts.map((f) => JSON.stringify(f)).join("\n") + "\n");
2462
2463
  written.factsPath = factsPath;
2463
2464
  }
2464
2465
  return written;
2465
2466
  }
2466
2467
  function loadGraph(root, graphPath) {
2467
2468
  const file = graphPath ?? defaultGraphPath(root);
2468
- if (!fs18.existsSync(file)) return null;
2469
- return parseGraph(fs18.readFileSync(file, "utf8"));
2469
+ if (!fs19.existsSync(file)) return null;
2470
+ return parseGraph(fs19.readFileSync(file, "utf8"));
2470
2471
  }
2471
2472
 
2472
2473
  // src/engine/verify.ts
@@ -2543,7 +2544,7 @@ function isProcessAlive(pid) {
2543
2544
  }
2544
2545
  function lockIsStale(file, staleMs) {
2545
2546
  try {
2546
- const { pid, at } = JSON.parse(fs18.readFileSync(file, "utf8"));
2547
+ const { pid, at } = JSON.parse(fs19.readFileSync(file, "utf8"));
2547
2548
  if (typeof at === "number" && Date.now() - at > staleMs) return true;
2548
2549
  if (typeof pid === "number" && !isProcessAlive(pid)) return true;
2549
2550
  return false;
@@ -2554,10 +2555,10 @@ function lockIsStale(file, staleMs) {
2554
2555
  function acquireLock(file, staleMs = DEFAULT_LOCK_STALE_MS) {
2555
2556
  const write = () => {
2556
2557
  try {
2557
- fs18.mkdirSync(path18.dirname(file), { recursive: true });
2558
- const fd = fs18.openSync(file, "wx");
2559
- fs18.writeFileSync(fd, JSON.stringify({ pid: process.pid, at: Date.now() }));
2560
- fs18.closeSync(fd);
2558
+ fs19.mkdirSync(path19.dirname(file), { recursive: true });
2559
+ const fd = fs19.openSync(file, "wx");
2560
+ fs19.writeFileSync(fd, JSON.stringify({ pid: process.pid, at: Date.now() }));
2561
+ fs19.closeSync(fd);
2561
2562
  return true;
2562
2563
  } catch {
2563
2564
  return false;
@@ -2566,7 +2567,7 @@ function acquireLock(file, staleMs = DEFAULT_LOCK_STALE_MS) {
2566
2567
  if (write()) return true;
2567
2568
  if (lockIsStale(file, staleMs)) {
2568
2569
  try {
2569
- fs18.rmSync(file, { force: true });
2570
+ fs19.rmSync(file, { force: true });
2570
2571
  } catch {
2571
2572
  }
2572
2573
  return write();
@@ -2575,7 +2576,7 @@ function acquireLock(file, staleMs = DEFAULT_LOCK_STALE_MS) {
2575
2576
  }
2576
2577
  function releaseLock(file) {
2577
2578
  try {
2578
- fs18.rmSync(file, { force: true });
2579
+ fs19.rmSync(file, { force: true });
2579
2580
  } catch {
2580
2581
  }
2581
2582
  }
@@ -2585,14 +2586,14 @@ function resolveEmbedModel(model) {
2585
2586
  return model ?? "bge-small-en-v1.5";
2586
2587
  }
2587
2588
  function modelCacheDir() {
2588
- const base = process.env.XDG_CACHE_HOME || path18.join(os.homedir(), ".cache");
2589
- return path18.join(base, "vibgrate", "models");
2589
+ const base = process.env.XDG_CACHE_HOME || path19.join(os.homedir(), ".cache");
2590
+ return path19.join(base, "vibgrate", "models");
2590
2591
  }
2591
2592
  function modelReadyMarker(modelId) {
2592
- return path18.join(modelCacheDir(), `.ready-${safe(modelId)}`);
2593
+ return path19.join(modelCacheDir(), `.ready-${safe(modelId)}`);
2593
2594
  }
2594
2595
  function isModelReady(modelId = resolveEmbedModel()) {
2595
- return fs18.existsSync(modelReadyMarker(modelId));
2596
+ return fs19.existsSync(modelReadyMarker(modelId));
2596
2597
  }
2597
2598
  function unavailableMessage(reason) {
2598
2599
  switch (reason) {
@@ -2609,13 +2610,13 @@ function unavailableMessage(reason) {
2609
2610
  }
2610
2611
  function modelCacheInfo(modelId = resolveEmbedModel()) {
2611
2612
  const dir = modelCacheDir();
2612
- return { dir, present: isModelReady(modelId) || fs18.existsSync(dir), bytes: dirSize(dir) };
2613
+ return { dir, present: isModelReady(modelId) || fs19.existsSync(dir), bytes: dirSize(dir) };
2613
2614
  }
2614
2615
  function clearModelCache() {
2615
2616
  const dir = modelCacheDir();
2616
2617
  const bytes = dirSize(dir);
2617
2618
  try {
2618
- fs18.rmSync(dir, { recursive: true, force: true });
2619
+ fs19.rmSync(dir, { recursive: true, force: true });
2619
2620
  } catch {
2620
2621
  }
2621
2622
  return bytes;
@@ -2624,16 +2625,16 @@ function dirSize(dir) {
2624
2625
  let total = 0;
2625
2626
  let entries;
2626
2627
  try {
2627
- entries = fs18.readdirSync(dir, { withFileTypes: true });
2628
+ entries = fs19.readdirSync(dir, { withFileTypes: true });
2628
2629
  } catch {
2629
2630
  return 0;
2630
2631
  }
2631
2632
  for (const e of entries) {
2632
- const p = path18.join(dir, e.name);
2633
+ const p = path19.join(dir, e.name);
2633
2634
  if (e.isDirectory()) total += dirSize(p);
2634
2635
  else {
2635
2636
  try {
2636
- total += fs18.statSync(p).size;
2637
+ total += fs19.statSync(p).size;
2637
2638
  } catch {
2638
2639
  }
2639
2640
  }
@@ -2645,7 +2646,7 @@ function isPermissionError(e) {
2645
2646
  return code === "EACCES" || code === "EPERM" || code === "EROFS";
2646
2647
  }
2647
2648
  function embeddingsCached(root, modelId) {
2648
- return fs18.existsSync(path18.join(cacheDir(root), `embeddings-${safe(modelId)}.json`));
2649
+ return fs19.existsSync(path19.join(cacheDir(root), `embeddings-${safe(modelId)}.json`));
2649
2650
  }
2650
2651
  async function loadEmbedder(options = {}) {
2651
2652
  if (options.local) return null;
@@ -2659,7 +2660,7 @@ async function loadEmbedder(options = {}) {
2659
2660
  if (!mod?.FlagEmbedding) return fail("not-installed");
2660
2661
  const cache = modelCacheDir();
2661
2662
  try {
2662
- fs18.mkdirSync(cache, { recursive: true });
2663
+ fs19.mkdirSync(cache, { recursive: true });
2663
2664
  } catch (e) {
2664
2665
  return fail(isPermissionError(e) ? "no-permission" : "init-failed");
2665
2666
  }
@@ -2674,7 +2675,7 @@ async function loadEmbedder(options = {}) {
2674
2675
  "embedding model init timed out"
2675
2676
  );
2676
2677
  try {
2677
- fs18.writeFileSync(modelReadyMarker(modelId), (/* @__PURE__ */ new Date(0)).toISOString());
2678
+ fs19.writeFileSync(modelReadyMarker(modelId), (/* @__PURE__ */ new Date(0)).toISOString());
2678
2679
  } catch {
2679
2680
  }
2680
2681
  return {
@@ -2772,7 +2773,7 @@ function cosine(a, b) {
2772
2773
  var EMBED_CHUNK = 256;
2773
2774
  var CACHE_WRITE_INTERVAL_MS = 1500;
2774
2775
  function vectorCachePath(root, modelId) {
2775
- return path18.join(cacheDir(root), `embeddings-${safe(modelId)}.json`);
2776
+ return path19.join(cacheDir(root), `embeddings-${safe(modelId)}.json`);
2776
2777
  }
2777
2778
  function countPending(graph, root, modelId) {
2778
2779
  const entries = readCacheEntries(vectorCachePath(root, modelId), modelId);
@@ -2787,9 +2788,9 @@ function countPending(graph, root, modelId) {
2787
2788
  return pending;
2788
2789
  }
2789
2790
  function readCacheEntries(file, modelId) {
2790
- if (!fs18.existsSync(file)) return {};
2791
+ if (!fs19.existsSync(file)) return {};
2791
2792
  try {
2792
- const loaded = JSON.parse(fs18.readFileSync(file, "utf8"));
2793
+ const loaded = JSON.parse(fs19.readFileSync(file, "utf8"));
2793
2794
  if (loaded.model === modelId && loaded.entries) return loaded.entries;
2794
2795
  } catch {
2795
2796
  }
@@ -2798,9 +2799,9 @@ function readCacheEntries(file, modelId) {
2798
2799
  async function getNodeEmbeddings(graph, embedder, root, onProgress) {
2799
2800
  const file = vectorCachePath(root, embedder.id);
2800
2801
  let cache = { model: embedder.id, entries: {} };
2801
- if (fs18.existsSync(file)) {
2802
+ if (fs19.existsSync(file)) {
2802
2803
  try {
2803
- const loaded = JSON.parse(fs18.readFileSync(file, "utf8"));
2804
+ const loaded = JSON.parse(fs19.readFileSync(file, "utf8"));
2804
2805
  if (loaded.model === embedder.id && loaded.entries) cache = loaded;
2805
2806
  } catch {
2806
2807
  }
@@ -2818,10 +2819,10 @@ async function getNodeEmbeddings(graph, embedder, root, onProgress) {
2818
2819
  }
2819
2820
  const persist = () => {
2820
2821
  try {
2821
- fs18.mkdirSync(cacheDir(root), { recursive: true });
2822
+ fs19.mkdirSync(cacheDir(root), { recursive: true });
2822
2823
  const tmp = `${file}.tmp.${process.pid}`;
2823
- fs18.writeFileSync(tmp, JSON.stringify(cache));
2824
- fs18.renameSync(tmp, file);
2824
+ fs19.writeFileSync(tmp, JSON.stringify(cache));
2825
+ fs19.renameSync(tmp, file);
2825
2826
  } catch {
2826
2827
  }
2827
2828
  };
@@ -2859,21 +2860,21 @@ function safe(id) {
2859
2860
  }
2860
2861
  var SNAPSHOT_VERSION = "vg-freshness/1";
2861
2862
  function snapshotPath(root) {
2862
- return path18.join(cacheDir(root), "freshness.json");
2863
+ return path19.join(cacheDir(root), "freshness.json");
2863
2864
  }
2864
2865
  function writeSnapshot(root, corpusHash, fileStats, scope = {}) {
2865
2866
  const files = {};
2866
2867
  for (const f of fileStats) files[f.rel] = { size: f.size, mtimeMs: f.mtimeMs, hash: f.hash };
2867
2868
  const snapshot = { version: SNAPSHOT_VERSION, corpusHash, scope: pruneScope(scope), files };
2868
2869
  try {
2869
- fs18.mkdirSync(cacheDir(root), { recursive: true });
2870
- fs18.writeFileSync(snapshotPath(root), stableStringify(snapshot, 0));
2870
+ fs19.mkdirSync(cacheDir(root), { recursive: true });
2871
+ fs19.writeFileSync(snapshotPath(root), stableStringify(snapshot, 0));
2871
2872
  } catch {
2872
2873
  }
2873
2874
  }
2874
2875
  function loadSnapshot(root) {
2875
2876
  try {
2876
- const loaded = JSON.parse(fs18.readFileSync(snapshotPath(root), "utf8"));
2877
+ const loaded = JSON.parse(fs19.readFileSync(snapshotPath(root), "utf8"));
2877
2878
  if (loaded.version === SNAPSHOT_VERSION && loaded.files && typeof loaded.corpusHash === "string") {
2878
2879
  return { ...loaded, scope: loaded.scope ?? {} };
2879
2880
  }
@@ -2909,7 +2910,7 @@ function probeFreshness(root) {
2909
2910
  }
2910
2911
  let stat;
2911
2912
  try {
2912
- stat = fs18.statSync(file.abs);
2913
+ stat = fs19.statSync(file.abs);
2913
2914
  } catch {
2914
2915
  drift.removed.push(file.rel);
2915
2916
  continue;
@@ -2917,7 +2918,7 @@ function probeFreshness(root) {
2917
2918
  if (stat.size === entry.size && stat.mtimeMs === entry.mtimeMs) continue;
2918
2919
  let hash = "";
2919
2920
  try {
2920
- hash = hashBytes(fs18.readFileSync(file.abs));
2921
+ hash = hashBytes(fs19.readFileSync(file.abs));
2921
2922
  } catch {
2922
2923
  drift.removed.push(file.rel);
2923
2924
  continue;
@@ -2934,7 +2935,7 @@ function probeFreshness(root) {
2934
2935
  }
2935
2936
  if (absorbed && !hasDrift(drift)) {
2936
2937
  try {
2937
- fs18.writeFileSync(snapshotPath(root), stableStringify(snapshot, 0));
2938
+ fs19.writeFileSync(snapshotPath(root), stableStringify(snapshot, 0));
2938
2939
  } catch {
2939
2940
  }
2940
2941
  }
@@ -3005,6 +3006,15 @@ function push2(map, key, value) {
3005
3006
  if (list) list.push(value);
3006
3007
  else map.set(key, [value]);
3007
3008
  }
3009
+ var INDEX_CACHE = /* @__PURE__ */ new WeakMap();
3010
+ function indexFor(graph) {
3011
+ let idx = INDEX_CACHE.get(graph);
3012
+ if (!idx) {
3013
+ idx = new GraphIndex(graph);
3014
+ INDEX_CACHE.set(graph, idx);
3015
+ }
3016
+ return idx;
3017
+ }
3008
3018
 
3009
3019
  // src/engine/query.ts
3010
3020
  var STOPWORDS = /* @__PURE__ */ new Set([
@@ -3055,7 +3065,7 @@ function queryGraph(graph, question, options = {}) {
3055
3065
  const limit = options.limit ?? 12;
3056
3066
  const terms = tokenize(question);
3057
3067
  const weightOf = termWeights(graph, terms);
3058
- const index = new GraphIndex(graph);
3068
+ const index = indexFor(graph);
3059
3069
  const scored = [];
3060
3070
  for (const node of graph.nodes) {
3061
3071
  if (node.kind === "file" || node.kind === "external") continue;
@@ -3072,7 +3082,7 @@ async function queryGraphSemantic(graph, question, options) {
3072
3082
  const limit = options.limit ?? 12;
3073
3083
  const terms = tokenize(question);
3074
3084
  const weightOf = termWeights(graph, terms);
3075
- const index = new GraphIndex(graph);
3085
+ const index = indexFor(graph);
3076
3086
  const queryVec = await options.embedder.embedQuery(question);
3077
3087
  const lexRaw = /* @__PURE__ */ new Map();
3078
3088
  let lexMax = 0;
@@ -3215,7 +3225,7 @@ function round2(x) {
3215
3225
  }
3216
3226
  var REFRESH_LOCK_STALE_MS = 10 * 60 * 1e3;
3217
3227
  function refreshLockPath(root) {
3218
- return path18.join(cacheDir(root), "refresh.lock");
3228
+ return path19.join(cacheDir(root), "refresh.lock");
3219
3229
  }
3220
3230
  async function refreshIfStale(root, opts = {}) {
3221
3231
  const probe = probeFreshness(root);
@@ -3246,8 +3256,8 @@ async function refreshIfStale(root, opts = {}) {
3246
3256
  const dir = vibgrateDir(root);
3247
3257
  writeArtifacts(result.graph, {
3248
3258
  root,
3249
- report: fs18.existsSync(path18.join(dir, "GRAPH_REPORT.md")),
3250
- html: fs18.existsSync(path18.join(dir, "graph.html"))
3259
+ report: fs19.existsSync(path19.join(dir, "GRAPH_REPORT.md")),
3260
+ html: fs19.existsSync(path19.join(dir, "graph.html"))
3251
3261
  });
3252
3262
  }
3253
3263
  writeSnapshot(root, result.graph.provenance.corpusHash, result.fileStats, scope);
@@ -3266,6 +3276,157 @@ async function refreshIfStale(root, opts = {}) {
3266
3276
  releaseLock(lock);
3267
3277
  }
3268
3278
  }
3279
+ var LEDGER = "savings.jsonl";
3280
+ var PER_FILE_TOKENS = 400;
3281
+ var SAVINGS_TOOLS = /* @__PURE__ */ new Set(["query_graph", "get_node"]);
3282
+ var CLI_TOOL_ALIASES = {
3283
+ ask: "query_graph",
3284
+ show: "get_node",
3285
+ impact: "impact_of",
3286
+ path: "find_path",
3287
+ hubs: "list_hubs",
3288
+ areas: "list_areas",
3289
+ tree: "tree"
3290
+ };
3291
+ function sanitizeClient(name) {
3292
+ if (typeof name !== "string") return "unknown";
3293
+ const cleaned = name.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
3294
+ return cleaned || "unknown";
3295
+ }
3296
+ function recordCliCall(root, entry, now) {
3297
+ recordSaving(
3298
+ root,
3299
+ {
3300
+ tool: entry.tool,
3301
+ source: "cli",
3302
+ client: sanitizeClient(entry.client),
3303
+ outcome: entry.outcome,
3304
+ vgTokens: entry.vgTokens ?? 0,
3305
+ baselineTokens: (entry.baselineFiles ?? 0) * PER_FILE_TOKENS
3306
+ },
3307
+ now
3308
+ );
3309
+ }
3310
+ function ledgerPath(root) {
3311
+ return path19.join(cacheDir(root), LEDGER);
3312
+ }
3313
+ function savingsRecorded(root) {
3314
+ return fs19.existsSync(ledgerPath(root));
3315
+ }
3316
+ function recordSaving(root, entry, now) {
3317
+ try {
3318
+ fs19.mkdirSync(cacheDir(root), { recursive: true });
3319
+ const line = JSON.stringify({ ts: now, ...entry });
3320
+ fs19.appendFileSync(ledgerPath(root), line + "\n");
3321
+ } catch {
3322
+ }
3323
+ }
3324
+ var DEFAULT_RATE_PER_M = 3;
3325
+ var DEFAULT_RATE_LABEL = "input @ $3/1M";
3326
+ function readSavings(root, days, now, ratePerM = DEFAULT_RATE_PER_M) {
3327
+ const file = ledgerPath(root);
3328
+ const cutoff = now - days * 24 * 60 * 60 * 1e3;
3329
+ let queries = 0;
3330
+ let vgTokens = 0;
3331
+ let baselineTokens = 0;
3332
+ if (fs19.existsSync(file)) {
3333
+ for (const line of fs19.readFileSync(file, "utf8").split("\n")) {
3334
+ if (!line.trim()) continue;
3335
+ try {
3336
+ const e = JSON.parse(line);
3337
+ if (e.ts < cutoff) continue;
3338
+ if (!SAVINGS_TOOLS.has(e.tool)) continue;
3339
+ queries++;
3340
+ vgTokens += e.vgTokens;
3341
+ baselineTokens += e.baselineTokens;
3342
+ } catch {
3343
+ }
3344
+ }
3345
+ }
3346
+ const estCostVg = vgTokens / 1e6 * ratePerM;
3347
+ const estCostBaseline = baselineTokens / 1e6 * ratePerM;
3348
+ return {
3349
+ enabled: savingsRecorded(root),
3350
+ days,
3351
+ queries,
3352
+ vgTokens,
3353
+ baselineTokens,
3354
+ ratio: vgTokens > 0 ? Math.round(baselineTokens / vgTokens * 100) / 100 : 0,
3355
+ estCostVg: round22(estCostVg),
3356
+ estCostBaseline: round22(estCostBaseline),
3357
+ saved: round22(estCostBaseline - estCostVg),
3358
+ rateLabel: DEFAULT_RATE_LABEL
3359
+ };
3360
+ }
3361
+ function round22(x) {
3362
+ return Math.round(x * 100) / 100;
3363
+ }
3364
+ function toDimensionStats(byKey) {
3365
+ return [...byKey.entries()].map(([key, r]) => {
3366
+ const calls = r.complete + r.partial + r.miss;
3367
+ return {
3368
+ key,
3369
+ calls,
3370
+ complete: r.complete,
3371
+ partial: r.partial,
3372
+ miss: r.miss,
3373
+ successPct: calls ? Math.round((r.complete + r.partial) / calls * 100) : 0
3374
+ };
3375
+ }).sort((a, b) => b.calls - a.calls || a.key.localeCompare(b.key));
3376
+ }
3377
+ function readUsage(root, days, now) {
3378
+ const file = ledgerPath(root);
3379
+ const cutoff = now - days * 24 * 60 * 60 * 1e3;
3380
+ const byTool = /* @__PURE__ */ new Map();
3381
+ const bySource = /* @__PURE__ */ new Map();
3382
+ const byClient = /* @__PURE__ */ new Map();
3383
+ const bump2 = (m, key, outcome) => {
3384
+ const row = m.get(key) ?? { complete: 0, partial: 0, miss: 0 };
3385
+ row[outcome]++;
3386
+ m.set(key, row);
3387
+ };
3388
+ if (fs19.existsSync(file)) {
3389
+ for (const line of fs19.readFileSync(file, "utf8").split("\n")) {
3390
+ if (!line.trim()) continue;
3391
+ try {
3392
+ const e = JSON.parse(line);
3393
+ if (e.ts < cutoff) continue;
3394
+ const outcome = e.outcome ?? "complete";
3395
+ bump2(byTool, e.tool, outcome);
3396
+ bump2(bySource, e.source ?? "mcp", outcome);
3397
+ bump2(byClient, e.client ?? "unknown", outcome);
3398
+ } catch {
3399
+ }
3400
+ }
3401
+ }
3402
+ const commands = toDimensionStats(byTool).map((d) => ({
3403
+ tool: d.key,
3404
+ calls: d.calls,
3405
+ complete: d.complete,
3406
+ partial: d.partial,
3407
+ miss: d.miss,
3408
+ successPct: d.successPct
3409
+ }));
3410
+ const totals = commands.reduce(
3411
+ (t, c) => ({
3412
+ calls: t.calls + c.calls,
3413
+ complete: t.complete + c.complete,
3414
+ partial: t.partial + c.partial,
3415
+ miss: t.miss + c.miss
3416
+ }),
3417
+ { calls: 0, complete: 0, partial: 0, miss: 0 }
3418
+ );
3419
+ const avgSuccessPct = commands.length ? Math.round(commands.reduce((s, c) => s + c.successPct, 0) / commands.length) : 0;
3420
+ return {
3421
+ enabled: savingsRecorded(root),
3422
+ days,
3423
+ commands,
3424
+ sources: toDimensionStats(bySource),
3425
+ clients: toDimensionStats(byClient),
3426
+ totals,
3427
+ avgSuccessPct
3428
+ };
3429
+ }
3269
3430
 
3270
3431
  // src/engine/lookup.ts
3271
3432
  function findNodes(graph, query) {
@@ -3342,7 +3503,7 @@ function shortestPath(graph, srcId, dstId) {
3342
3503
  var DEPEND_EDGES = /* @__PURE__ */ new Set(["call", "import", "extends", "implements", "references"]);
3343
3504
  function impactOf(graph, rootId, opts = {}) {
3344
3505
  const maxDepth = Math.max(1, opts.depth ?? 4);
3345
- const index = new GraphIndex(graph);
3506
+ const index = indexFor(graph);
3346
3507
  const root = index.node(rootId);
3347
3508
  const affected = /* @__PURE__ */ new Map();
3348
3509
  let minEdgeConfidence = 1;
@@ -3389,7 +3550,7 @@ function round3(x) {
3389
3550
  return Math.round(x * 1e6) / 1e6;
3390
3551
  }
3391
3552
  function coveringTests(graph, node, index) {
3392
- const idx = index ?? new GraphIndex(graph);
3553
+ const idx = index ?? indexFor(graph);
3393
3554
  const byId = idx.nodeById;
3394
3555
  const out = /* @__PURE__ */ new Map();
3395
3556
  for (const e of idx.in(node.id, "test")) {
@@ -3403,7 +3564,7 @@ function coveringTests(graph, node, index) {
3403
3564
  return [...out.values()].sort((a, b) => b.confidence - a.confidence || a.file.localeCompare(b.file));
3404
3565
  }
3405
3566
  function testsToRun(graph, rootId, depth = 4) {
3406
- const idx = new GraphIndex(graph);
3567
+ const idx = indexFor(graph);
3407
3568
  const impact = impactOf(graph, rootId, { depth });
3408
3569
  const affectedIds = /* @__PURE__ */ new Set([rootId, ...impact.affected.map((a) => a.id)]);
3409
3570
  const testFiles = /* @__PURE__ */ new Set();
@@ -3423,10 +3584,10 @@ function testsToRun(graph, rootId, depth = 4) {
3423
3584
  };
3424
3585
  }
3425
3586
  function detectRunner(root, lang) {
3426
- const pkgPath = path18.join(root, "package.json");
3427
- if (fs18.existsSync(pkgPath)) {
3587
+ const pkgPath = path19.join(root, "package.json");
3588
+ if (fs19.existsSync(pkgPath)) {
3428
3589
  try {
3429
- const pkg = JSON.parse(fs18.readFileSync(pkgPath, "utf8"));
3590
+ const pkg = JSON.parse(fs19.readFileSync(pkgPath, "utf8"));
3430
3591
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
3431
3592
  if (deps.vitest) return { name: "vitest", command: (f) => `npx vitest run ${f.join(" ")}`.trim() };
3432
3593
  if (deps.jest) return { name: "jest", command: (f) => `npx jest ${f.join(" ")}`.trim() };
@@ -3434,10 +3595,10 @@ function detectRunner(root, lang) {
3434
3595
  } catch {
3435
3596
  }
3436
3597
  }
3437
- if (lang === "py" || fs18.existsSync(path18.join(root, "pyproject.toml")) || fs18.existsSync(path18.join(root, "pytest.ini"))) {
3598
+ if (lang === "py" || fs19.existsSync(path19.join(root, "pyproject.toml")) || fs19.existsSync(path19.join(root, "pytest.ini"))) {
3438
3599
  return { name: "pytest", command: (f) => `pytest ${f.join(" ")}`.trim() };
3439
3600
  }
3440
- if (lang === "go" || fs18.existsSync(path18.join(root, "go.mod"))) {
3601
+ if (lang === "go" || fs19.existsSync(path19.join(root, "go.mod"))) {
3441
3602
  return { name: "go test", command: () => "go test ./..." };
3442
3603
  }
3443
3604
  if (lang === "cs" || hasFile(root, /\.csproj$/) || hasFile(root, /\.sln$/)) {
@@ -3448,7 +3609,7 @@ function detectRunner(root, lang) {
3448
3609
  }
3449
3610
  function hasFile(root, re) {
3450
3611
  try {
3451
- return fs18.readdirSync(root).some((f) => re.test(f));
3612
+ return fs19.readdirSync(root).some((f) => re.test(f));
3452
3613
  } catch {
3453
3614
  return false;
3454
3615
  }
@@ -3520,7 +3681,7 @@ function findManifests(root) {
3520
3681
  if (depth > 8 || scanned > MAX_ENTRIES) return;
3521
3682
  let entries;
3522
3683
  try {
3523
- entries = fs18.readdirSync(dir, { withFileTypes: true });
3684
+ entries = fs19.readdirSync(dir, { withFileTypes: true });
3524
3685
  } catch {
3525
3686
  return;
3526
3687
  }
@@ -3528,11 +3689,11 @@ function findManifests(root) {
3528
3689
  scanned++;
3529
3690
  if (scanned > MAX_ENTRIES) break;
3530
3691
  if (e.isDirectory()) {
3531
- if (!SKIP_DIRS2.has(e.name) && !e.name.startsWith(".")) walk2(path18.join(dir, e.name), depth + 1);
3692
+ if (!SKIP_DIRS2.has(e.name) && !e.name.startsWith(".")) walk2(path19.join(dir, e.name), depth + 1);
3532
3693
  continue;
3533
3694
  }
3534
3695
  const eco = MANIFEST_BY_FILE[e.name] ?? (/\.(cs|fs)proj$/i.test(e.name) ? "dotnet" : void 0);
3535
- if (eco) set[eco].push(path18.join(dir, e.name));
3696
+ if (eco) set[eco].push(path19.join(dir, e.name));
3536
3697
  }
3537
3698
  };
3538
3699
  walk2(root, 0);
@@ -3544,11 +3705,11 @@ function npmDeps(files) {
3544
3705
  for (const file of files) {
3545
3706
  let pkg;
3546
3707
  try {
3547
- pkg = JSON.parse(fs18.readFileSync(file, "utf8"));
3708
+ pkg = JSON.parse(fs19.readFileSync(file, "utf8"));
3548
3709
  } catch {
3549
3710
  continue;
3550
3711
  }
3551
- const dir = path18.dirname(file);
3712
+ const dir = path19.dirname(file);
3552
3713
  const all = { ...pkg.dependencies, ...pkg.devDependencies };
3553
3714
  for (const [name, declared] of Object.entries(all)) {
3554
3715
  out.push({ name, ecosystem: "npm", declared, installed: installedNpmVersion(dir, name) });
@@ -3559,12 +3720,12 @@ function npmDeps(files) {
3559
3720
  function installedNpmVersion(dir, name) {
3560
3721
  let cur = dir;
3561
3722
  for (let i = 0; i < 12; i++) {
3562
- const p = path18.join(cur, "node_modules", name, "package.json");
3723
+ const p = path19.join(cur, "node_modules", name, "package.json");
3563
3724
  try {
3564
- return JSON.parse(fs18.readFileSync(p, "utf8")).version;
3725
+ return JSON.parse(fs19.readFileSync(p, "utf8")).version;
3565
3726
  } catch {
3566
3727
  }
3567
- const parent = path18.dirname(cur);
3728
+ const parent = path19.dirname(cur);
3568
3729
  if (parent === cur) break;
3569
3730
  cur = parent;
3570
3731
  }
@@ -3581,21 +3742,21 @@ function normalizePep503(name) {
3581
3742
  function sitePackagesDirs(base) {
3582
3743
  const out = [];
3583
3744
  try {
3584
- for (const d of fs18.readdirSync(path18.join(base, "lib"))) {
3585
- if (d.startsWith("python")) out.push(path18.join(base, "lib", d, "site-packages"));
3745
+ for (const d of fs19.readdirSync(path19.join(base, "lib"))) {
3746
+ if (d.startsWith("python")) out.push(path19.join(base, "lib", d, "site-packages"));
3586
3747
  }
3587
3748
  } catch {
3588
3749
  }
3589
- out.push(path18.join(base, "Lib", "site-packages"));
3750
+ out.push(path19.join(base, "Lib", "site-packages"));
3590
3751
  return out;
3591
3752
  }
3592
3753
  function installedPypiVersion(root, name) {
3593
3754
  const target = normalizePep503(name);
3594
3755
  for (const venv of [".venv", "venv", "env", ".tox"]) {
3595
- for (const sp of sitePackagesDirs(path18.join(root, venv))) {
3756
+ for (const sp of sitePackagesDirs(path19.join(root, venv))) {
3596
3757
  let entries;
3597
3758
  try {
3598
- entries = fs18.readdirSync(sp);
3759
+ entries = fs19.readdirSync(sp);
3599
3760
  } catch {
3600
3761
  continue;
3601
3762
  }
@@ -3610,7 +3771,7 @@ function installedPypiVersion(root, name) {
3610
3771
  function installedPhpVersion(root, name) {
3611
3772
  let data;
3612
3773
  try {
3613
- data = JSON.parse(fs18.readFileSync(path18.join(root, "vendor", "composer", "installed.json"), "utf8"));
3774
+ data = JSON.parse(fs19.readFileSync(path19.join(root, "vendor", "composer", "installed.json"), "utf8"));
3614
3775
  } catch {
3615
3776
  return void 0;
3616
3777
  }
@@ -3629,11 +3790,11 @@ function pypiDeps(files) {
3629
3790
  for (const file of files) {
3630
3791
  let text;
3631
3792
  try {
3632
- text = fs18.readFileSync(file, "utf8");
3793
+ text = fs19.readFileSync(file, "utf8");
3633
3794
  } catch {
3634
3795
  continue;
3635
3796
  }
3636
- if (path18.basename(file) === "requirements.txt") {
3797
+ if (path19.basename(file) === "requirements.txt") {
3637
3798
  for (const line of text.split("\n")) {
3638
3799
  const t = line.trim();
3639
3800
  if (!t || t.startsWith("#") || t.startsWith("-")) continue;
@@ -3717,7 +3878,7 @@ function goDeps(files) {
3717
3878
  for (const mod of files) {
3718
3879
  let text;
3719
3880
  try {
3720
- text = fs18.readFileSync(mod, "utf8");
3881
+ text = fs19.readFileSync(mod, "utf8");
3721
3882
  } catch {
3722
3883
  continue;
3723
3884
  }
@@ -3735,7 +3896,7 @@ function cargoDeps(files) {
3735
3896
  for (const file of files) {
3736
3897
  let text;
3737
3898
  try {
3738
- text = fs18.readFileSync(file, "utf8");
3899
+ text = fs19.readFileSync(file, "utf8");
3739
3900
  } catch {
3740
3901
  continue;
3741
3902
  }
@@ -3767,7 +3928,7 @@ function rubyDeps(files) {
3767
3928
  for (const file of files) {
3768
3929
  let text;
3769
3930
  try {
3770
- text = fs18.readFileSync(file, "utf8");
3931
+ text = fs19.readFileSync(file, "utf8");
3771
3932
  } catch {
3772
3933
  continue;
3773
3934
  }
@@ -3782,7 +3943,7 @@ function phpDeps(files) {
3782
3943
  for (const file of files) {
3783
3944
  let pkg;
3784
3945
  try {
3785
- pkg = JSON.parse(fs18.readFileSync(file, "utf8"));
3946
+ pkg = JSON.parse(fs19.readFileSync(file, "utf8"));
3786
3947
  } catch {
3787
3948
  continue;
3788
3949
  }
@@ -3799,7 +3960,7 @@ function dotnetDeps(files) {
3799
3960
  for (const file of files) {
3800
3961
  let text;
3801
3962
  try {
3802
- text = fs18.readFileSync(file, "utf8");
3963
+ text = fs19.readFileSync(file, "utf8");
3803
3964
  } catch {
3804
3965
  continue;
3805
3966
  }
@@ -3819,7 +3980,7 @@ function swiftDeps(files) {
3819
3980
  for (const file of files) {
3820
3981
  let text;
3821
3982
  try {
3822
- text = fs18.readFileSync(file, "utf8");
3983
+ text = fs19.readFileSync(file, "utf8");
3823
3984
  } catch {
3824
3985
  continue;
3825
3986
  }
@@ -3841,7 +4002,7 @@ function dartDeps(files) {
3841
4002
  for (const file of files) {
3842
4003
  let text;
3843
4004
  try {
3844
- text = fs18.readFileSync(file, "utf8");
4005
+ text = fs19.readFileSync(file, "utf8");
3845
4006
  } catch {
3846
4007
  continue;
3847
4008
  }
@@ -3866,11 +4027,11 @@ function javaDeps(files) {
3866
4027
  for (const file of files) {
3867
4028
  let text;
3868
4029
  try {
3869
- text = fs18.readFileSync(file, "utf8");
4030
+ text = fs19.readFileSync(file, "utf8");
3870
4031
  } catch {
3871
4032
  continue;
3872
4033
  }
3873
- if (path18.basename(file) === "pom.xml") {
4034
+ if (path19.basename(file) === "pom.xml") {
3874
4035
  for (const block of text.match(/<dependency>[\s\S]*?<\/dependency>/g) ?? []) {
3875
4036
  const g = /<groupId>([^<]+)<\/groupId>/.exec(block);
3876
4037
  const a = /<artifactId>([^<]+)<\/artifactId>/.exec(block);
@@ -3916,10 +4077,10 @@ function discoverModels(home = os.homedir()) {
3916
4077
  );
3917
4078
  }
3918
4079
  function ollama(home) {
3919
- const base = path18.join(home, ".ollama", "models", "manifests");
4080
+ const base = path19.join(home, ".ollama", "models", "manifests");
3920
4081
  const out = [];
3921
4082
  walk(base, 4, (file) => {
3922
- const rel2 = path18.relative(base, file).split(path18.sep);
4083
+ const rel2 = path19.relative(base, file).split(path19.sep);
3923
4084
  if (rel2.length >= 2) {
3924
4085
  const tag = rel2[rel2.length - 1];
3925
4086
  const model = rel2[rel2.length - 2];
@@ -3929,24 +4090,24 @@ function ollama(home) {
3929
4090
  return dedupe(out);
3930
4091
  }
3931
4092
  function lmStudio(home) {
3932
- const bases = [path18.join(home, ".lmstudio", "models"), path18.join(home, ".cache", "lm-studio", "models")];
4093
+ const bases = [path19.join(home, ".lmstudio", "models"), path19.join(home, ".cache", "lm-studio", "models")];
3933
4094
  const out = [];
3934
4095
  for (const base of bases) {
3935
4096
  walk(base, 5, (file) => {
3936
4097
  if (!file.endsWith(".gguf")) return;
3937
- const rel2 = path18.relative(base, file).replace(/\\/g, "/");
4098
+ const rel2 = path19.relative(base, file).replace(/\\/g, "/");
3938
4099
  out.push({ runtime: "lm-studio", name: rel2, path: file });
3939
4100
  });
3940
4101
  }
3941
4102
  return dedupe(out);
3942
4103
  }
3943
4104
  function looseGguf(home) {
3944
- const bases = [path18.join(home, "models"), path18.join(home, ".cache", "huggingface")];
4105
+ const bases = [path19.join(home, "models"), path19.join(home, ".cache", "huggingface")];
3945
4106
  const out = [];
3946
4107
  for (const base of bases) {
3947
4108
  walk(base, 4, (file) => {
3948
4109
  if (!file.endsWith(".gguf")) return;
3949
- out.push({ runtime: "gguf", name: path18.basename(file), path: file });
4110
+ out.push({ runtime: "gguf", name: path19.basename(file), path: file });
3950
4111
  });
3951
4112
  }
3952
4113
  return dedupe(out);
@@ -3955,12 +4116,12 @@ function walk(dir, depth, onFile) {
3955
4116
  if (depth < 0) return;
3956
4117
  let entries;
3957
4118
  try {
3958
- entries = fs18.readdirSync(dir, { withFileTypes: true });
4119
+ entries = fs19.readdirSync(dir, { withFileTypes: true });
3959
4120
  } catch {
3960
4121
  return;
3961
4122
  }
3962
4123
  for (const e of entries.sort((a, b) => a.name.localeCompare(b.name))) {
3963
- const abs = path18.join(dir, e.name);
4124
+ const abs = path19.join(dir, e.name);
3964
4125
  if (e.isDirectory()) walk(abs, depth - 1, onFile);
3965
4126
  else if (e.isFile()) onFile(abs);
3966
4127
  }
@@ -3991,7 +4152,7 @@ function lockfileVersion(root, ecosystem, name) {
3991
4152
  function gradleLock(root, name) {
3992
4153
  let text;
3993
4154
  try {
3994
- text = fs18.readFileSync(path18.join(root, "gradle.lockfile"), "utf8");
4155
+ text = fs19.readFileSync(path19.join(root, "gradle.lockfile"), "utf8");
3995
4156
  } catch {
3996
4157
  return void 0;
3997
4158
  }
@@ -4001,7 +4162,7 @@ function gradleLock(root, name) {
4001
4162
  function packageResolved(root, name) {
4002
4163
  let data;
4003
4164
  try {
4004
- data = JSON.parse(fs18.readFileSync(path18.join(root, "Package.resolved"), "utf8"));
4165
+ data = JSON.parse(fs19.readFileSync(path19.join(root, "Package.resolved"), "utf8"));
4005
4166
  } catch {
4006
4167
  return void 0;
4007
4168
  }
@@ -4017,7 +4178,7 @@ function packageResolved(root, name) {
4017
4178
  function pubspecLock(root, name) {
4018
4179
  let text;
4019
4180
  try {
4020
- text = fs18.readFileSync(path18.join(root, "pubspec.lock"), "utf8");
4181
+ text = fs19.readFileSync(path19.join(root, "pubspec.lock"), "utf8");
4021
4182
  } catch {
4022
4183
  return void 0;
4023
4184
  }
@@ -4041,7 +4202,7 @@ function escapeRegExp(s) {
4041
4202
  function gemfileLock(root, name) {
4042
4203
  let text;
4043
4204
  try {
4044
- text = fs18.readFileSync(path18.join(root, "Gemfile.lock"), "utf8");
4205
+ text = fs19.readFileSync(path19.join(root, "Gemfile.lock"), "utf8");
4045
4206
  } catch {
4046
4207
  return void 0;
4047
4208
  }
@@ -4051,7 +4212,7 @@ function gemfileLock(root, name) {
4051
4212
  function composerLock(root, name) {
4052
4213
  let data;
4053
4214
  try {
4054
- data = JSON.parse(fs18.readFileSync(path18.join(root, "composer.lock"), "utf8"));
4215
+ data = JSON.parse(fs19.readFileSync(path19.join(root, "composer.lock"), "utf8"));
4055
4216
  } catch {
4056
4217
  return void 0;
4057
4218
  }
@@ -4066,7 +4227,7 @@ function composerLock(root, name) {
4066
4227
  function packagesLock(root, name) {
4067
4228
  let data;
4068
4229
  try {
4069
- data = JSON.parse(fs18.readFileSync(path18.join(root, "packages.lock.json"), "utf8"));
4230
+ data = JSON.parse(fs19.readFileSync(path19.join(root, "packages.lock.json"), "utf8"));
4070
4231
  } catch {
4071
4232
  return void 0;
4072
4233
  }
@@ -4091,7 +4252,7 @@ function pypiLockVersion(root, name) {
4091
4252
  function tomlPackageLock(root, file, name, normalize3) {
4092
4253
  let text;
4093
4254
  try {
4094
- text = fs18.readFileSync(path18.join(root, file), "utf8");
4255
+ text = fs19.readFileSync(path19.join(root, file), "utf8");
4095
4256
  } catch {
4096
4257
  return void 0;
4097
4258
  }
@@ -4108,7 +4269,7 @@ function tomlPackageLock(root, file, name, normalize3) {
4108
4269
  function pipfileLock(root, name) {
4109
4270
  let data;
4110
4271
  try {
4111
- data = JSON.parse(fs18.readFileSync(path18.join(root, "Pipfile.lock"), "utf8"));
4272
+ data = JSON.parse(fs19.readFileSync(path19.join(root, "Pipfile.lock"), "utf8"));
4112
4273
  } catch {
4113
4274
  return void 0;
4114
4275
  }
@@ -4127,7 +4288,7 @@ function pipfileLock(root, name) {
4127
4288
  function packageLockVersion(root, name) {
4128
4289
  let data;
4129
4290
  try {
4130
- data = JSON.parse(fs18.readFileSync(path18.join(root, "package-lock.json"), "utf8"));
4291
+ data = JSON.parse(fs19.readFileSync(path19.join(root, "package-lock.json"), "utf8"));
4131
4292
  } catch {
4132
4293
  return void 0;
4133
4294
  }
@@ -4139,7 +4300,7 @@ function packageLockVersion(root, name) {
4139
4300
  function yarnLockVersion(root, name) {
4140
4301
  let text;
4141
4302
  try {
4142
- text = fs18.readFileSync(path18.join(root, "yarn.lock"), "utf8");
4303
+ text = fs19.readFileSync(path19.join(root, "yarn.lock"), "utf8");
4143
4304
  } catch {
4144
4305
  return void 0;
4145
4306
  }
@@ -4167,19 +4328,19 @@ function headerNamesPackage(header, name) {
4167
4328
  // src/engine/lib.ts
4168
4329
  var LIB_SCHEMA = "vg-lib/1.0";
4169
4330
  function catalogPath(root) {
4170
- return path18.join(root, "vibgrate.lib.json");
4331
+ return path19.join(root, "vibgrate.lib.json");
4171
4332
  }
4172
4333
  function libDir(root) {
4173
- return path18.join(root, ".vibgrate", "lib");
4334
+ return path19.join(root, ".vibgrate", "lib");
4174
4335
  }
4175
4336
  function libId(name) {
4176
4337
  return name.trim().toLowerCase().replace(/^@/, "").replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
4177
4338
  }
4178
4339
  function loadCatalog(root) {
4179
4340
  const file = catalogPath(root);
4180
- if (fs18.existsSync(file)) {
4341
+ if (fs19.existsSync(file)) {
4181
4342
  try {
4182
- const data = JSON.parse(fs18.readFileSync(file, "utf8"));
4343
+ const data = JSON.parse(fs19.readFileSync(file, "utf8"));
4183
4344
  if (data.schemaVersion === LIB_SCHEMA && data.libraries) return data;
4184
4345
  } catch {
4185
4346
  }
@@ -4187,7 +4348,7 @@ function loadCatalog(root) {
4187
4348
  return { schemaVersion: LIB_SCHEMA, libraries: {} };
4188
4349
  }
4189
4350
  function saveCatalog(root, catalog) {
4190
- fs18.writeFileSync(catalogPath(root), `${stableStringify(catalog, 2)}
4351
+ fs19.writeFileSync(catalogPath(root), `${stableStringify(catalog, 2)}
4191
4352
  `);
4192
4353
  }
4193
4354
  function resolveLib(catalog, name) {
@@ -4263,23 +4424,23 @@ async function addLibrary(source, opts) {
4263
4424
  content = await res.text();
4264
4425
  type = source.endsWith("llms.txt") ? "llms.txt" : source.endsWith(".json") ? "openapi" : "website";
4265
4426
  } else {
4266
- const abs = path18.resolve(root, source);
4267
- if (!fs18.existsSync(abs)) throw new Error(`source not found: ${source}`);
4427
+ const abs = path19.resolve(root, source);
4428
+ if (!fs19.existsSync(abs)) throw new Error(`source not found: ${source}`);
4268
4429
  content = readLocal(abs);
4269
4430
  type = "local";
4270
4431
  }
4271
4432
  const name = opts.name ?? inferName(source);
4272
4433
  const id = libId(name);
4273
4434
  const version = opts.version ?? installedVersion(root, name) ?? "*";
4274
- fs18.mkdirSync(libDir(root), { recursive: true });
4275
- const docFileAbs = path18.join(libDir(root), `${id}.md`);
4276
- fs18.writeFileSync(docFileAbs, content);
4435
+ fs19.mkdirSync(libDir(root), { recursive: true });
4436
+ const docFileAbs = path19.join(libDir(root), `${id}.md`);
4437
+ fs19.writeFileSync(docFileAbs, content);
4277
4438
  const entry = {
4278
4439
  id,
4279
4440
  name,
4280
4441
  version,
4281
4442
  source: { type, location: source },
4282
- docFile: path18.relative(root, docFileAbs).split(path18.sep).join("/"),
4443
+ docFile: path19.relative(root, docFileAbs).split(path19.sep).join("/"),
4283
4444
  docHash: hashString(content),
4284
4445
  bytes: Buffer.byteLength(content, "utf8")
4285
4446
  };
@@ -4289,15 +4450,15 @@ async function addLibrary(source, opts) {
4289
4450
  return entry;
4290
4451
  }
4291
4452
  function readDoc(root, entry) {
4292
- const abs = path18.resolve(root, entry.docFile);
4293
- return fs18.existsSync(abs) ? fs18.readFileSync(abs, "utf8") : "";
4453
+ const abs = path19.resolve(root, entry.docFile);
4454
+ return fs19.existsSync(abs) ? fs19.readFileSync(abs, "utf8") : "";
4294
4455
  }
4295
4456
  function npmPackageDir(root, name) {
4296
4457
  let cur = root;
4297
4458
  for (let i = 0; i < 12; i++) {
4298
- const dir = path18.join(cur, "node_modules", name);
4299
- if (fs18.existsSync(path18.join(dir, "package.json"))) return dir;
4300
- const parent = path18.dirname(cur);
4459
+ const dir = path19.join(cur, "node_modules", name);
4460
+ if (fs19.existsSync(path19.join(dir, "package.json"))) return dir;
4461
+ const parent = path19.dirname(cur);
4301
4462
  if (parent === cur) break;
4302
4463
  cur = parent;
4303
4464
  }
@@ -4308,24 +4469,24 @@ function localPackageDocs(root, name) {
4308
4469
  if (!dir) return void 0;
4309
4470
  const version = resolveVersion(root, name).served;
4310
4471
  for (const f of ["llms.txt", "README.md", "README.mdx", "README", "readme.md"]) {
4311
- const p = path18.join(dir, f);
4472
+ const p = path19.join(dir, f);
4312
4473
  try {
4313
- const docs = fs18.readFileSync(p, "utf8");
4474
+ const docs = fs19.readFileSync(p, "utf8");
4314
4475
  if (docs.trim()) {
4315
- return { docs, version, source: path18.relative(root, p).split(path18.sep).join("/") };
4476
+ return { docs, version, source: path19.relative(root, p).split(path19.sep).join("/") };
4316
4477
  }
4317
4478
  } catch {
4318
4479
  }
4319
4480
  }
4320
4481
  try {
4321
- const pkg = JSON.parse(fs18.readFileSync(path18.join(dir, "package.json"), "utf8"));
4482
+ const pkg = JSON.parse(fs19.readFileSync(path19.join(dir, "package.json"), "utf8"));
4322
4483
  if (pkg.description?.trim()) {
4323
4484
  return {
4324
4485
  docs: `# ${name}
4325
4486
 
4326
4487
  ${pkg.description}`,
4327
4488
  version: version ?? pkg.version,
4328
- source: path18.relative(root, path18.join(dir, "package.json")).split(path18.sep).join("/")
4489
+ source: path19.relative(root, path19.join(dir, "package.json")).split(path19.sep).join("/")
4329
4490
  };
4330
4491
  }
4331
4492
  } catch {
@@ -4334,17 +4495,17 @@ ${pkg.description}`,
4334
4495
  }
4335
4496
  function dtsEntry(dir) {
4336
4497
  try {
4337
- const pkg = JSON.parse(fs18.readFileSync(path18.join(dir, "package.json"), "utf8"));
4498
+ const pkg = JSON.parse(fs19.readFileSync(path19.join(dir, "package.json"), "utf8"));
4338
4499
  const rel2 = pkg.types ?? pkg.typings;
4339
4500
  if (typeof rel2 === "string") {
4340
- const p = path18.join(dir, rel2);
4341
- if (fs18.existsSync(p)) return p;
4501
+ const p = path19.join(dir, rel2);
4502
+ if (fs19.existsSync(p)) return p;
4342
4503
  }
4343
4504
  } catch {
4344
4505
  }
4345
4506
  for (const c of ["index.d.ts", "dist/index.d.ts", "lib/index.d.ts", "types/index.d.ts"]) {
4346
- const p = path18.join(dir, c);
4347
- if (fs18.existsSync(p)) return p;
4507
+ const p = path19.join(dir, c);
4508
+ if (fs19.existsSync(p)) return p;
4348
4509
  }
4349
4510
  return void 0;
4350
4511
  }
@@ -4377,7 +4538,7 @@ function localApiSurface(root, name) {
4377
4538
  if (!dts) return void 0;
4378
4539
  let src;
4379
4540
  try {
4380
- src = fs18.readFileSync(dts, "utf8");
4541
+ src = fs19.readFileSync(dts, "utf8");
4381
4542
  } catch {
4382
4543
  return void 0;
4383
4544
  }
@@ -4385,36 +4546,36 @@ function localApiSurface(root, name) {
4385
4546
  return decls.length ? decls.join("\n\n") : void 0;
4386
4547
  }
4387
4548
  function cloneAndReadGit(url) {
4388
- const dir = fs18.mkdtempSync(path18.join(os.tmpdir(), "vg-lib-git-"));
4549
+ const dir = fs19.mkdtempSync(path19.join(os.tmpdir(), "vg-lib-git-"));
4389
4550
  try {
4390
4551
  execFileSync("git", ["clone", "--depth", "1", "--quiet", url, dir], {
4391
4552
  stdio: ["ignore", "ignore", "pipe"]
4392
4553
  });
4393
4554
  } catch (err) {
4394
- fs18.rmSync(dir, { recursive: true, force: true });
4555
+ fs19.rmSync(dir, { recursive: true, force: true });
4395
4556
  const detail = err instanceof Error && err.message ? `: ${err.message.split("\n")[0]}` : "";
4396
4557
  throw new Error(`git clone failed for ${url}${detail}`);
4397
4558
  }
4398
4559
  try {
4399
4560
  return readGitDocs(dir);
4400
4561
  } finally {
4401
- fs18.rmSync(dir, { recursive: true, force: true });
4562
+ fs19.rmSync(dir, { recursive: true, force: true });
4402
4563
  }
4403
4564
  }
4404
4565
  function readGitDocs(dir) {
4405
- const docsDir = path18.join(dir, "docs");
4406
- if (fs18.existsSync(docsDir) && fs18.statSync(docsDir).isDirectory()) return readLocal(docsDir);
4407
- const readme = ["README.md", "README.mdx", "README.txt", "README.rst", "readme.md"].map((f) => path18.join(dir, f)).find((p) => fs18.existsSync(p));
4408
- if (readme) return fs18.readFileSync(readme, "utf8");
4566
+ const docsDir = path19.join(dir, "docs");
4567
+ if (fs19.existsSync(docsDir) && fs19.statSync(docsDir).isDirectory()) return readLocal(docsDir);
4568
+ const readme = ["README.md", "README.mdx", "README.txt", "README.rst", "readme.md"].map((f) => path19.join(dir, f)).find((p) => fs19.existsSync(p));
4569
+ if (readme) return fs19.readFileSync(readme, "utf8");
4409
4570
  return readLocal(dir);
4410
4571
  }
4411
4572
  function readLocal(abs) {
4412
- const stat = fs18.statSync(abs);
4413
- if (stat.isFile()) return fs18.readFileSync(abs, "utf8");
4573
+ const stat = fs19.statSync(abs);
4574
+ if (stat.isFile()) return fs19.readFileSync(abs, "utf8");
4414
4575
  const parts = [];
4415
- const files = fs18.readdirSync(abs, { withFileTypes: true }).filter((e) => e.isFile() && /\.(md|mdx|txt|rst)$/i.test(e.name)).map((e) => e.name).sort();
4576
+ const files = fs19.readdirSync(abs, { withFileTypes: true }).filter((e) => e.isFile() && /\.(md|mdx|txt|rst)$/i.test(e.name)).map((e) => e.name).sort();
4416
4577
  for (const f of files) parts.push(`<!-- ${f} -->
4417
- ${fs18.readFileSync(path18.join(abs, f), "utf8")}`);
4578
+ ${fs19.readFileSync(path19.join(abs, f), "utf8")}`);
4418
4579
  return parts.join("\n\n");
4419
4580
  }
4420
4581
  function inferName(source) {
@@ -4423,10 +4584,10 @@ function inferName(source) {
4423
4584
  return base.replace(/\.(md|mdx|txt|rst|json)$/i, "").replace(/^llms$/, "docs");
4424
4585
  }
4425
4586
  function findGitRoot(startDir = process.cwd()) {
4426
- let dir = path18.resolve(startDir);
4587
+ let dir = path19.resolve(startDir);
4427
4588
  for (; ; ) {
4428
- if (fs18.existsSync(path18.join(dir, ".git"))) return dir;
4429
- const parent = path18.dirname(dir);
4589
+ if (fs19.existsSync(path19.join(dir, ".git"))) return dir;
4590
+ const parent = path19.dirname(dir);
4430
4591
  if (parent === dir) return null;
4431
4592
  dir = parent;
4432
4593
  }
@@ -4437,19 +4598,19 @@ function normalizeEntry(line) {
4437
4598
  function ensureGitignored(entry, startDir = process.cwd()) {
4438
4599
  const root = findGitRoot(startDir);
4439
4600
  if (!root) return { status: "not-a-repo", entry };
4440
- const gitignorePath = path18.join(root, ".gitignore");
4601
+ const gitignorePath = path19.join(root, ".gitignore");
4441
4602
  const target = normalizeEntry(entry);
4442
4603
  let existing = "";
4443
4604
  let fileExisted = true;
4444
4605
  try {
4445
- existing = fs18.readFileSync(gitignorePath, "utf8");
4606
+ existing = fs19.readFileSync(gitignorePath, "utf8");
4446
4607
  } catch {
4447
4608
  fileExisted = false;
4448
4609
  }
4449
4610
  const alreadyPresent = existing.split(/\r?\n/).some((line) => normalizeEntry(line) === target);
4450
4611
  if (alreadyPresent) return { status: "present", entry, gitignorePath };
4451
4612
  const needsLeadingNewline = existing.length > 0 && !existing.endsWith("\n");
4452
- fs18.appendFileSync(gitignorePath, `${needsLeadingNewline ? "\n" : ""}${entry}
4613
+ fs19.appendFileSync(gitignorePath, `${needsLeadingNewline ? "\n" : ""}${entry}
4453
4614
  `, "utf8");
4454
4615
  return { status: fileExisted ? "added" : "created", entry, gitignorePath };
4455
4616
  }
@@ -4603,14 +4764,14 @@ dsnCommand.command("create").description("Create a new DSN token").option("--ing
4603
4764
  console.log(chalk.dim("The secret must be registered on your Vibgrate ingest API."));
4604
4765
  }
4605
4766
  if (opts.write) {
4606
- const writePath = path18.resolve(opts.write);
4767
+ const writePath = path19.resolve(opts.write);
4607
4768
  await writeTextFile(writePath, dsn + "\n");
4608
4769
  console.log("");
4609
4770
  console.log(chalk.green("\u2714") + ` DSN written to ${opts.write}`);
4610
- const root = findGitRoot(path18.dirname(writePath));
4611
- const rel2 = root ? path18.relative(root, writePath).split(path18.sep).join("/") : "";
4771
+ const root = findGitRoot(path19.dirname(writePath));
4772
+ const rel2 = root ? path19.relative(root, writePath).split(path19.sep).join("/") : "";
4612
4773
  if (root && rel2 && !rel2.startsWith("..")) {
4613
- const res = ensureGitignored(rel2, path18.dirname(writePath));
4774
+ const res = ensureGitignored(rel2, path19.dirname(writePath));
4614
4775
  if (res.status === "created") {
4615
4776
  console.log(chalk.green("\u2714") + ` Created .gitignore and ignored ${rel2}`);
4616
4777
  } else if (res.status === "added") {
@@ -4628,30 +4789,30 @@ dsnCommand.command("create").description("Create a new DSN token").option("--ing
4628
4789
  var STORE_DIRNAME = ".vibgrate";
4629
4790
  var STORE_FILENAME = "credentials.json";
4630
4791
  function homeCredentialsPath() {
4631
- return path18.join(os.homedir(), STORE_DIRNAME, STORE_FILENAME);
4792
+ return path19.join(os.homedir(), STORE_DIRNAME, STORE_FILENAME);
4632
4793
  }
4633
4794
  function projectCredentialsPath(cwd = process.cwd()) {
4634
4795
  const root = findGitRoot(cwd) ?? cwd;
4635
- return path18.join(root, STORE_DIRNAME, STORE_FILENAME);
4796
+ return path19.join(root, STORE_DIRNAME, STORE_FILENAME);
4636
4797
  }
4637
4798
  function credentialsPath(opts = {}) {
4638
4799
  const override = process.env.VIBGRATE_CREDENTIALS?.trim();
4639
- if (override) return path18.resolve(override);
4800
+ if (override) return path19.resolve(override);
4640
4801
  const local = projectCredentialsPath(opts.cwd);
4641
4802
  if (opts.local) return local;
4642
- if (fs18.existsSync(local)) return local;
4803
+ if (fs19.existsSync(local)) return local;
4643
4804
  return homeCredentialsPath();
4644
4805
  }
4645
4806
  function gitignoreEntryForCredentials(repoRoot, credsFile = credentialsPath()) {
4646
- const rel2 = path18.relative(repoRoot, credsFile);
4647
- if (rel2 && !rel2.startsWith("..") && !path18.isAbsolute(rel2)) {
4648
- return rel2.split(path18.sep).join("/");
4807
+ const rel2 = path19.relative(repoRoot, credsFile);
4808
+ if (rel2 && !rel2.startsWith("..") && !path19.isAbsolute(rel2)) {
4809
+ return rel2.split(path19.sep).join("/");
4649
4810
  }
4650
4811
  return `${STORE_DIRNAME}/${STORE_FILENAME}`;
4651
4812
  }
4652
4813
  function readStoredCredentials(opts = {}) {
4653
4814
  try {
4654
- const raw = fs18.readFileSync(credentialsPath(opts), "utf8");
4815
+ const raw = fs19.readFileSync(credentialsPath(opts), "utf8");
4655
4816
  const parsed = JSON.parse(raw);
4656
4817
  return parsed && typeof parsed.dsn === "string" && parsed.dsn ? parsed : null;
4657
4818
  } catch {
@@ -4660,16 +4821,16 @@ function readStoredCredentials(opts = {}) {
4660
4821
  }
4661
4822
  function writeStoredCredentials(creds, opts = {}) {
4662
4823
  const file = credentialsPath(opts);
4663
- fs18.mkdirSync(path18.dirname(file), { recursive: true });
4664
- fs18.writeFileSync(file, JSON.stringify(creds, null, 2) + "\n", { encoding: "utf8", mode: 384 });
4824
+ fs19.mkdirSync(path19.dirname(file), { recursive: true });
4825
+ fs19.writeFileSync(file, JSON.stringify(creds, null, 2) + "\n", { encoding: "utf8", mode: 384 });
4665
4826
  try {
4666
- fs18.chmodSync(file, 384);
4827
+ fs19.chmodSync(file, 384);
4667
4828
  } catch {
4668
4829
  }
4669
4830
  }
4670
4831
  function clearStoredCredentials(opts = {}) {
4671
4832
  try {
4672
- fs18.rmSync(credentialsPath(opts));
4833
+ fs19.rmSync(credentialsPath(opts));
4673
4834
  return true;
4674
4835
  } catch {
4675
4836
  return false;
@@ -5000,7 +5161,7 @@ var pushCommand = new Command("push").description("Push scan results to Vibgrate
5000
5161
  if (opts.strict) process.exit(1);
5001
5162
  return;
5002
5163
  }
5003
- const filePath = path18.resolve(opts.file);
5164
+ const filePath = path19.resolve(opts.file);
5004
5165
  if (!await pathExists(filePath)) {
5005
5166
  console.error(chalk.red(`Scan artifact not found: ${filePath}`));
5006
5167
  console.error(chalk.dim('Run "vibgrate scan" first.'));
@@ -5057,8 +5218,8 @@ var pushCommand = new Command("push").description("Push scan results to Vibgrate
5057
5218
  });
5058
5219
  function readScanArtifact(root) {
5059
5220
  try {
5060
- const file = path18.join(root, ".vibgrate", "scan_result.json");
5061
- return JSON.parse(fs18.readFileSync(file, "utf8"));
5221
+ const file = path19.join(root, ".vibgrate", "scan_result.json");
5222
+ return JSON.parse(fs19.readFileSync(file, "utf8"));
5062
5223
  } catch {
5063
5224
  return null;
5064
5225
  }
@@ -5511,19 +5672,165 @@ function boundList(items, max) {
5511
5672
  return { items: items.slice(0, max), total: items.length };
5512
5673
  }
5513
5674
  var NODE_EDGE_CAP = 50;
5514
- var IGNORE_DIRS = /* @__PURE__ */ new Set([".git", ".vibgrate", "node_modules", "dist", "build", "out", "target", "vendor", "__pycache__"]);
5515
5675
  var MAX_FILE_BYTES = 1e6;
5516
- var MAX_FILES_SCANNED = 2e4;
5517
5676
  var PREVIEW_CHARS = 120;
5518
- function searchSymbols(graph, root, query, limit) {
5677
+ var NUL = "\0";
5678
+ var PARALLEL_MIN_BYTES = 64e6;
5679
+ var PARALLEL_MIN_FILES = 2e3;
5680
+ var ROW_CAP = 1e4;
5681
+ async function scanCandidates(root, files, needle, opts) {
5682
+ const needleLower = needle.toLowerCase();
5683
+ if (!needleLower || files.length === 0) return { hits: [], total: 0, truncated: false };
5684
+ if (!opts.collectAll) return scanInline(root, files, needleLower, opts.budget, true, void 0);
5685
+ if (files.length < PARALLEL_MIN_FILES && !process.env.VG_PARALLEL_MIN_BYTES) {
5686
+ return scanInline(root, files, needleLower, ROW_CAP, false, void 0);
5687
+ }
5688
+ const { over, sizes } = measure(root, files);
5689
+ if (!over) return scanInline(root, files, needleLower, ROW_CAP, false, sizes);
5690
+ return scanParallel(root, files, needleLower);
5691
+ }
5692
+ function parallelMinBytes() {
5693
+ const v = Number(process.env.VG_PARALLEL_MIN_BYTES);
5694
+ return Number.isFinite(v) && v >= 0 ? v : PARALLEL_MIN_BYTES;
5695
+ }
5696
+ function measure(root, files) {
5697
+ const threshold = parallelMinBytes();
5698
+ const sizes = /* @__PURE__ */ new Map();
5699
+ let total = 0;
5700
+ for (const rel2 of files) {
5701
+ let size;
5702
+ try {
5703
+ size = fs19.statSync(path19.join(root, rel2)).size;
5704
+ } catch {
5705
+ continue;
5706
+ }
5707
+ sizes.set(rel2, size);
5708
+ if (size <= MAX_FILE_BYTES) total += size;
5709
+ if (total >= threshold) return { over: true, sizes };
5710
+ }
5711
+ return { over: false, sizes };
5712
+ }
5713
+ function matchLines(text, needleLower) {
5714
+ const low = text.toLowerCase();
5715
+ if (!low.includes(needleLower)) return [];
5716
+ const lowLines = low.split("\n");
5717
+ const rawLines = text.split("\n");
5718
+ const hits = [];
5719
+ for (let i = 0; i < lowLines.length; i++) {
5720
+ if (lowLines[i].includes(needleLower)) hits.push({ line: i + 1, preview: rawLines[i].trim().slice(0, PREVIEW_CHARS) });
5721
+ }
5722
+ return hits;
5723
+ }
5724
+ function readCandidate(abs, knownSize) {
5725
+ try {
5726
+ const size = knownSize ?? fs19.statSync(abs).size;
5727
+ if (size > MAX_FILE_BYTES) return null;
5728
+ const text = fs19.readFileSync(abs, "utf8");
5729
+ return text.includes(NUL) ? null : text;
5730
+ } catch {
5731
+ return null;
5732
+ }
5733
+ }
5734
+ function scanInline(root, files, needleLower, cap, stopAtCap, sizes) {
5735
+ const hits = [];
5736
+ let total = 0;
5737
+ let truncated = false;
5738
+ for (const rel2 of files) {
5739
+ const text = readCandidate(path19.join(root, rel2), sizes?.get(rel2));
5740
+ if (text === null) continue;
5741
+ for (const h of matchLines(text, needleLower)) {
5742
+ total++;
5743
+ if (hits.length < cap) hits.push({ file: rel2, line: h.line, preview: h.preview });
5744
+ else {
5745
+ truncated = true;
5746
+ if (stopAtCap) return { hits, total, truncated };
5747
+ }
5748
+ }
5749
+ }
5750
+ return { hits, total, truncated };
5751
+ }
5752
+ async function scanParallel(root, files, needleLower) {
5753
+ const n = Math.min(files.length, Math.max(2, Math.min(8, os.cpus().length - 1)));
5754
+ const shards = Array.from({ length: n }, () => []);
5755
+ files.forEach((f, i) => shards[i % n].push(f));
5756
+ let results;
5757
+ try {
5758
+ results = await Promise.all(shards.map((shard) => runWorker(root, shard, needleLower)));
5759
+ } catch {
5760
+ return scanInline(root, files, needleLower, ROW_CAP, false, void 0);
5761
+ }
5762
+ const hits = [];
5763
+ let total = 0;
5764
+ let truncated = false;
5765
+ for (const r of results) {
5766
+ total += r.total;
5767
+ truncated = truncated || r.truncated;
5768
+ for (const h of r.hits) hits.push(h);
5769
+ }
5770
+ hits.sort((a, b) => a.file < b.file ? -1 : a.file > b.file ? 1 : a.line - b.line);
5771
+ if (hits.length > ROW_CAP) {
5772
+ hits.length = ROW_CAP;
5773
+ truncated = true;
5774
+ }
5775
+ return { hits, total, truncated };
5776
+ }
5777
+ var WORKER_SOURCE = [
5778
+ "const { parentPort, workerData } = require('worker_threads');",
5779
+ "const fs = require('fs');",
5780
+ "const path = require('path');",
5781
+ `const MAX = ${MAX_FILE_BYTES};`,
5782
+ `const PREVIEW = ${PREVIEW_CHARS};`,
5783
+ `const CAP = ${ROW_CAP};`,
5784
+ "const NUL = String.fromCharCode(0);",
5785
+ "const NL = String.fromCharCode(10);",
5786
+ "const { root, files, needleLower } = workerData;",
5787
+ "const hits = []; let total = 0, truncated = false;",
5788
+ "for (const rel of files) {",
5789
+ " const abs = path.join(root, rel);",
5790
+ " let text;",
5791
+ ' try { if (fs.statSync(abs).size > MAX) continue; text = fs.readFileSync(abs, "utf8"); } catch { continue; }',
5792
+ " if (text.includes(NUL)) continue;",
5793
+ " const low = text.toLowerCase();",
5794
+ " if (!low.includes(needleLower)) continue;",
5795
+ " const lowLines = low.split(NL); const rawLines = text.split(NL);",
5796
+ " for (let i = 0; i < lowLines.length; i++) {",
5797
+ " if (!lowLines[i].includes(needleLower)) continue;",
5798
+ " total++;",
5799
+ " if (hits.length < CAP) hits.push({ file: rel, line: i + 1, preview: rawLines[i].trim().slice(0, PREVIEW) });",
5800
+ " else truncated = true;",
5801
+ " }",
5802
+ "}",
5803
+ "parentPort.postMessage({ hits, total, truncated });"
5804
+ ].join("\n");
5805
+ function runWorker(root, files, needleLower) {
5806
+ return new Promise((resolve12, reject) => {
5807
+ const w = new Worker(WORKER_SOURCE, { eval: true, workerData: { root, files, needleLower } });
5808
+ let settled = false;
5809
+ w.once("message", (m) => {
5810
+ settled = true;
5811
+ resolve12(m);
5812
+ });
5813
+ w.once("error", reject);
5814
+ w.once("exit", (code) => {
5815
+ if (!settled && code !== 0) reject(new Error(`literal-scan worker exited with code ${code}`));
5816
+ });
5817
+ });
5818
+ }
5819
+
5820
+ // src/engine/search.ts
5821
+ var IGNORE_DIRS = /* @__PURE__ */ new Set([".git", ".vibgrate", "node_modules", "dist", "build", "out", "target", "vendor", "__pycache__"]);
5822
+ var MAX_FILES_SCANNED = 2e4;
5823
+ async function searchSymbols(graph, root, query, limit) {
5519
5824
  const q2 = query.trim();
5520
5825
  if (!q2) return { matches: [], moreAvailable: false, hint: "query is required" };
5826
+ const isPhrase = /\s/.test(q2);
5521
5827
  let nodes = findNodes(graph, q2).filter((n) => n.kind !== "file");
5522
5828
  if (nodes.length === 0) {
5523
5829
  const tokens = queryTokens(q2);
5524
5830
  if (tokens.length >= 2) nodes = multiTokenNodes(graph, tokens);
5525
5831
  }
5526
- const symbolHits = nodes.slice(0, limit).map((n, i) => ({
5832
+ const symbolCap = isPhrase ? Math.max(1, Math.floor(limit / 3)) : limit;
5833
+ const symbolHits = nodes.slice(0, symbolCap).map((n, i) => ({
5527
5834
  kind: n.kind,
5528
5835
  name: n.qualifiedName,
5529
5836
  file: n.file,
@@ -5533,9 +5840,12 @@ function searchSymbols(graph, root, query, limit) {
5533
5840
  const spare = limit - symbolHits.length;
5534
5841
  const textHits = [];
5535
5842
  let truncatedScan = false;
5843
+ let totalTextMatches;
5536
5844
  if (spare > 0) {
5537
5845
  const seen = new Set(symbolHits.map((h) => `${h.file}:${h.line}`));
5538
- truncatedScan = scanFiles(root, q2, spare, seen, textHits);
5846
+ const scan = await scanFiles(root, q2, spare, seen, textHits, isPhrase);
5847
+ truncatedScan = scan.truncated;
5848
+ if (isPhrase) totalTextMatches = scan.total;
5539
5849
  }
5540
5850
  const matches = [...symbolHits, ...textHits];
5541
5851
  if (matches.length === 0) {
@@ -5545,7 +5855,14 @@ function searchSymbols(graph, root, query, limit) {
5545
5855
  hint: "no symbol or text match \u2014 for meaning-level questions (symptoms, relationships, what-breaks-if) use query_graph"
5546
5856
  };
5547
5857
  }
5548
- return { matches, moreAvailable: nodes.length > symbolHits.length || truncatedScan };
5858
+ const literalTruncated = totalTextMatches !== void 0 && totalTextMatches > textHits.length;
5859
+ const moreAvailable = isPhrase ? literalTruncated || truncatedScan : nodes.length > symbolHits.length || truncatedScan;
5860
+ const result = { matches, moreAvailable };
5861
+ if (totalTextMatches !== void 0) result.totalTextMatches = totalTextMatches;
5862
+ if (literalTruncated) {
5863
+ result.hint = `showing ${textHits.length} of ${totalTextMatches}${truncatedScan ? "+" : ""} literal matches \u2014 raise limit or narrow the query to get them all`;
5864
+ }
5865
+ return result;
5549
5866
  }
5550
5867
  function queryTokens(q2) {
5551
5868
  return [...new Set(q2.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 2))];
@@ -5562,48 +5879,84 @@ function multiTokenNodes(graph, tokens) {
5562
5879
  }
5563
5880
  return [...cov.values()].sort((a, b) => b.hits - a.hits || b.node.importance - a.node.importance).map((e) => e.node);
5564
5881
  }
5565
- function scanFiles(root, needle, budget, seen, out) {
5566
- const lower = needle.toLowerCase();
5567
- let scanned = 0;
5882
+ async function scanFiles(root, needle, budget, seen, out, countAll) {
5883
+ const listing = listCandidateFiles(root, needle);
5884
+ const outcome = await scanCandidates(root, listing.files, needle, { collectAll: countAll, budget });
5885
+ let overlap = 0;
5886
+ for (const h of outcome.hits) {
5887
+ if (seen.has(`${h.file}:${h.line}`)) {
5888
+ overlap++;
5889
+ continue;
5890
+ }
5891
+ seen.add(`${h.file}:${h.line}`);
5892
+ if (out.length < budget) out.push({ kind: "text", file: h.file, line: h.line, preview: h.preview });
5893
+ }
5894
+ return { total: outcome.total - overlap, truncated: listing.truncated || outcome.truncated };
5895
+ }
5896
+ function listCandidateFiles(root, needle) {
5897
+ return ripgrepCandidates(root, needle) ?? walkCandidates(root);
5898
+ }
5899
+ function walkCandidates(root) {
5900
+ const files = [];
5901
+ let truncated = false;
5568
5902
  const stack = [root];
5569
5903
  while (stack.length) {
5570
5904
  const dir = stack.pop();
5571
5905
  let entries;
5572
5906
  try {
5573
- entries = fs18.readdirSync(dir, { withFileTypes: true });
5907
+ entries = fs19.readdirSync(dir, { withFileTypes: true });
5574
5908
  } catch {
5575
5909
  continue;
5576
5910
  }
5577
- entries.sort((a, b) => a.name.localeCompare(b.name));
5578
5911
  for (const e of entries) {
5579
5912
  if (e.name.startsWith(".") || IGNORE_DIRS.has(e.name)) continue;
5580
- const abs = path18.join(dir, e.name);
5913
+ const abs = path19.join(dir, e.name);
5581
5914
  if (e.isDirectory()) {
5582
5915
  stack.push(abs);
5583
5916
  continue;
5584
5917
  }
5585
- if (++scanned > MAX_FILES_SCANNED) return true;
5586
- let text;
5587
- try {
5588
- if (fs18.statSync(abs).size > MAX_FILE_BYTES) continue;
5589
- text = fs18.readFileSync(abs, "utf8");
5590
- } catch {
5918
+ if (files.length >= MAX_FILES_SCANNED) {
5919
+ truncated = true;
5591
5920
  continue;
5592
5921
  }
5593
- if (!text.toLowerCase().includes(lower)) continue;
5594
- const rel2 = path18.relative(root, abs);
5595
- const lines = text.split("\n");
5596
- for (let i = 0; i < lines.length; i++) {
5597
- if (!lines[i].toLowerCase().includes(lower)) continue;
5598
- const key = `${rel2}:${i + 1}`;
5599
- if (seen.has(key)) continue;
5600
- seen.add(key);
5601
- out.push({ kind: "text", file: rel2, line: i + 1, preview: lines[i].trim().slice(0, PREVIEW_CHARS) });
5602
- if (out.length >= budget) return true;
5603
- }
5922
+ files.push(path19.relative(root, abs));
5604
5923
  }
5605
5924
  }
5606
- return false;
5925
+ files.sort();
5926
+ return { files, truncated };
5927
+ }
5928
+ var rgAvailable;
5929
+ function hasRipgrep() {
5930
+ if (process.env.VG_DISABLE_RIPGREP) return false;
5931
+ if (rgAvailable === void 0) {
5932
+ try {
5933
+ rgAvailable = spawnSync("rg", ["--version"], { stdio: "ignore", timeout: 3e3 }).status === 0;
5934
+ } catch {
5935
+ rgAvailable = false;
5936
+ }
5937
+ }
5938
+ return rgAvailable;
5939
+ }
5940
+ function ripgrepCandidates(root, needle) {
5941
+ if (!hasRipgrep()) return null;
5942
+ const globs = [...IGNORE_DIRS].flatMap((d) => ["--glob", `!**/${d}/**`, "--glob", `!${d}/**`]);
5943
+ const res = spawnSync(
5944
+ "rg",
5945
+ ["--files-with-matches", "--fixed-strings", "--ignore-case", "--no-ignore", "--no-messages", ...globs, "--", needle, "."],
5946
+ { cwd: root, encoding: "utf8", timeout: 15e3, maxBuffer: 32 * 1024 * 1024 }
5947
+ );
5948
+ if (res.error || res.status !== 0 && res.status !== 1 || res.signal) return null;
5949
+ const seen = /* @__PURE__ */ new Set();
5950
+ for (const raw of res.stdout.split("\n")) {
5951
+ const rel2 = raw.startsWith("./") ? raw.slice(2) : raw;
5952
+ if (!rel2) continue;
5953
+ const segs = rel2.split("/");
5954
+ if (segs.some((s) => s.startsWith(".") || IGNORE_DIRS.has(s))) continue;
5955
+ seen.add(rel2);
5956
+ }
5957
+ const files = [...seen].sort();
5958
+ if (files.length > MAX_FILES_SCANNED) return { files: files.slice(0, MAX_FILES_SCANNED), truncated: true };
5959
+ return { files, truncated: false };
5607
5960
  }
5608
5961
 
5609
5962
  // src/mcp/tools.ts
@@ -5722,16 +6075,16 @@ var TOOLS = [
5722
6075
  },
5723
6076
  {
5724
6077
  name: "search_symbols",
5725
- 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.",
6078
+ description: "Find a known name or literal string fast: ranked symbol lookup, plus a complete literal file-search for any quoted/multi-word phrase (config keys, log lines, UI copy). A phrase query reports totalTextMatches so you know you have every occurrence \u2014 use it instead of grep. Use first for most discovery; use query_graph for meaning.",
5726
6079
  inputSchema: obj(
5727
6080
  {
5728
- query: { type: "string", maxLength: 120 },
5729
- limit: { type: "number", description: "default 8, max 20" }
6081
+ query: { type: "string", maxLength: 120, description: "a symbol name, or a literal phrase to sweep for every occurrence of" },
6082
+ limit: { type: "number", description: "default 8, max 50 (raise it to fetch a whole literal sweep in one call)" }
5730
6083
  },
5731
6084
  ["query"]
5732
6085
  ),
5733
- handler: (graph, args, ctx) => {
5734
- const limit = Math.min(20, numOr(args.limit, 8));
6086
+ handler: async (graph, args, ctx) => {
6087
+ const limit = Math.min(50, numOr(args.limit, 8));
5735
6088
  return searchSymbols(graph, ctx.root, String(args.query ?? ""), limit);
5736
6089
  }
5737
6090
  },
@@ -5787,7 +6140,7 @@ var TOOLS = [
5787
6140
  handler: (graph, args, ctx) => {
5788
6141
  const { node, candidates } = resolveOne(graph, String(args.name ?? ""), numOrU(args.pick));
5789
6142
  if (!node) return unresolved(candidates);
5790
- const index = new GraphIndex(graph);
6143
+ const index = indexFor(graph);
5791
6144
  const edgeOk = edgeRecordFilter(args);
5792
6145
  const calleeRecs = index.callees(node.id).filter(edgeOk);
5793
6146
  const callerRecs = index.callers(node.id).filter(edgeOk);
@@ -6273,104 +6626,6 @@ function navigationToolsetConfig(serverName = "vibgrate") {
6273
6626
  configs: Object.fromEntries(HOT_TOOLS.map((n) => [n, { defer_loading: false }]))
6274
6627
  };
6275
6628
  }
6276
- var LEDGER = "savings.jsonl";
6277
- var PER_FILE_TOKENS = 400;
6278
- var SAVINGS_TOOLS = /* @__PURE__ */ new Set(["query_graph", "get_node"]);
6279
- function ledgerPath(root) {
6280
- return path18.join(cacheDir(root), LEDGER);
6281
- }
6282
- function savingsRecorded(root) {
6283
- return fs18.existsSync(ledgerPath(root));
6284
- }
6285
- function recordSaving(root, entry, now) {
6286
- try {
6287
- fs18.mkdirSync(cacheDir(root), { recursive: true });
6288
- const line = JSON.stringify({ ts: now, ...entry });
6289
- fs18.appendFileSync(ledgerPath(root), line + "\n");
6290
- } catch {
6291
- }
6292
- }
6293
- var DEFAULT_RATE_PER_M = 3;
6294
- var DEFAULT_RATE_LABEL = "input @ $3/1M";
6295
- function readSavings(root, days, now, ratePerM = DEFAULT_RATE_PER_M) {
6296
- const file = ledgerPath(root);
6297
- const cutoff = now - days * 24 * 60 * 60 * 1e3;
6298
- let queries = 0;
6299
- let vgTokens = 0;
6300
- let baselineTokens = 0;
6301
- if (fs18.existsSync(file)) {
6302
- for (const line of fs18.readFileSync(file, "utf8").split("\n")) {
6303
- if (!line.trim()) continue;
6304
- try {
6305
- const e = JSON.parse(line);
6306
- if (e.ts < cutoff) continue;
6307
- if (!SAVINGS_TOOLS.has(e.tool)) continue;
6308
- queries++;
6309
- vgTokens += e.vgTokens;
6310
- baselineTokens += e.baselineTokens;
6311
- } catch {
6312
- }
6313
- }
6314
- }
6315
- const estCostVg = vgTokens / 1e6 * ratePerM;
6316
- const estCostBaseline = baselineTokens / 1e6 * ratePerM;
6317
- return {
6318
- enabled: savingsRecorded(root),
6319
- days,
6320
- queries,
6321
- vgTokens,
6322
- baselineTokens,
6323
- ratio: vgTokens > 0 ? Math.round(baselineTokens / vgTokens * 100) / 100 : 0,
6324
- estCostVg: round22(estCostVg),
6325
- estCostBaseline: round22(estCostBaseline),
6326
- saved: round22(estCostBaseline - estCostVg),
6327
- rateLabel: DEFAULT_RATE_LABEL
6328
- };
6329
- }
6330
- function round22(x) {
6331
- return Math.round(x * 100) / 100;
6332
- }
6333
- function readUsage(root, days, now) {
6334
- const file = ledgerPath(root);
6335
- const cutoff = now - days * 24 * 60 * 60 * 1e3;
6336
- const byTool = /* @__PURE__ */ new Map();
6337
- if (fs18.existsSync(file)) {
6338
- for (const line of fs18.readFileSync(file, "utf8").split("\n")) {
6339
- if (!line.trim()) continue;
6340
- try {
6341
- const e = JSON.parse(line);
6342
- if (e.ts < cutoff) continue;
6343
- const outcome = e.outcome ?? "complete";
6344
- const row = byTool.get(e.tool) ?? { complete: 0, partial: 0, miss: 0 };
6345
- row[outcome]++;
6346
- byTool.set(e.tool, row);
6347
- } catch {
6348
- }
6349
- }
6350
- }
6351
- const commands = [...byTool.entries()].map(([tool, r]) => {
6352
- const calls = r.complete + r.partial + r.miss;
6353
- return {
6354
- tool,
6355
- calls,
6356
- complete: r.complete,
6357
- partial: r.partial,
6358
- miss: r.miss,
6359
- successPct: calls ? Math.round((r.complete + r.partial) / calls * 100) : 0
6360
- };
6361
- }).sort((a, b) => b.calls - a.calls || a.tool.localeCompare(b.tool));
6362
- const totals = commands.reduce(
6363
- (t, c) => ({
6364
- calls: t.calls + c.calls,
6365
- complete: t.complete + c.complete,
6366
- partial: t.partial + c.partial,
6367
- miss: t.miss + c.miss
6368
- }),
6369
- { calls: 0, complete: 0, partial: 0, miss: 0 }
6370
- );
6371
- const avgSuccessPct = commands.length ? Math.round(commands.reduce((s, c) => s + c.successPct, 0) / commands.length) : 0;
6372
- return { enabled: savingsRecorded(root), days, commands, totals, avgSuccessPct };
6373
- }
6374
6629
  var PROBE_INTERVAL_MS = 2e3;
6375
6630
  var MAX_PROBE_INTERVAL_MS = 3e4;
6376
6631
  var PROBE_DUTY_FACTOR = 20;
@@ -6381,7 +6636,7 @@ var GraphSource = class {
6381
6636
  this.graphPath = graphPath;
6382
6637
  this.refresh = refresh;
6383
6638
  this.tuning = tuning;
6384
- this.root = path18.dirname(path18.dirname(graphPath));
6639
+ this.root = path19.dirname(path19.dirname(graphPath));
6385
6640
  }
6386
6641
  graphPath;
6387
6642
  refresh;
@@ -6397,9 +6652,9 @@ var GraphSource = class {
6397
6652
  /** Current graph: auto-refreshed if the tree drifted, reloaded if the file changed. */
6398
6653
  async get() {
6399
6654
  if (this.refresh) await this.maybeRefresh();
6400
- const stat = fs18.statSync(this.graphPath);
6655
+ const stat = fs19.statSync(this.graphPath);
6401
6656
  if (stat.mtimeMs !== this.cachedMtimeMs || !this.cached) {
6402
- this.cached = parseGraph(fs18.readFileSync(this.graphPath, "utf8"));
6657
+ this.cached = parseGraph(fs19.readFileSync(this.graphPath, "utf8"));
6403
6658
  this.cachedMtimeMs = stat.mtimeMs;
6404
6659
  }
6405
6660
  return this.cached;
@@ -6433,8 +6688,9 @@ var GraphSource = class {
6433
6688
  }
6434
6689
  };
6435
6690
  function createServer(source, opts = {}) {
6436
- const { savings = false, local = false, dedup = false } = opts;
6437
- const root = path18.dirname(path18.dirname(source.graphPath));
6691
+ const { savings = false, shareStats = false, local = false, dedup = false } = opts;
6692
+ const record = savings || shareStats;
6693
+ const root = path19.dirname(path19.dirname(source.graphPath));
6438
6694
  const seen = /* @__PURE__ */ new Set();
6439
6695
  const server = new Server(
6440
6696
  { name: "vg", version: VERSION },
@@ -6443,7 +6699,7 @@ function createServer(source, opts = {}) {
6443
6699
  // Routing guidance once at the server level (hosts that surface
6444
6700
  // `instructions` get it at zero per-step schema cost): the flashlight
6445
6701
  // vs the map.
6446
- 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.'
6702
+ instructions: 'vg is a code map. Use search_symbols to find a known name or literal string fast \u2014 a multi-word/quoted phrase runs a complete literal sweep and reports totalTextMatches, so reach for it instead of grep even for plain-string "find every occurrence" lookups. 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.'
6447
6703
  }
6448
6704
  );
6449
6705
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
@@ -6470,7 +6726,7 @@ function createServer(source, opts = {}) {
6470
6726
  try {
6471
6727
  const args = request.params.arguments ?? {};
6472
6728
  const result = await tool.handler(graph, args, { root, local, dedup, seen });
6473
- if (savings) recordUsage(root, tool.name, result);
6729
+ if (record) recordUsage(root, tool.name, result, detectClient(server));
6474
6730
  return renderToolResult(result);
6475
6731
  } catch (err) {
6476
6732
  return errorResult(`tool "${tool.name}" failed: ${err.message}`);
@@ -6490,7 +6746,7 @@ function sleep(ms) {
6490
6746
  timer.unref?.();
6491
6747
  });
6492
6748
  }
6493
- function recordUsage(root, tool, result) {
6749
+ function recordUsage(root, tool, result, client) {
6494
6750
  const outcome = classifyOutcome(result);
6495
6751
  let vgTokens = 0;
6496
6752
  let baselineTokens = 0;
@@ -6498,7 +6754,19 @@ function recordUsage(root, tool, result) {
6498
6754
  vgTokens = countTokens(renderedText(renderToolResult(result)));
6499
6755
  baselineTokens = referencedFiles(result).size * PER_FILE_TOKENS;
6500
6756
  }
6501
- recordSaving(root, { tool, outcome, vgTokens, baselineTokens }, Date.now());
6757
+ recordSaving(
6758
+ root,
6759
+ { tool, outcome, vgTokens, baselineTokens, source: "mcp", client: sanitizeClient(client) },
6760
+ Date.now()
6761
+ );
6762
+ }
6763
+ function detectClient(server) {
6764
+ try {
6765
+ const info = server.getClientVersion?.();
6766
+ return typeof info?.name === "string" ? info.name : void 0;
6767
+ } catch {
6768
+ return void 0;
6769
+ }
6502
6770
  }
6503
6771
  function classifyOutcome(result) {
6504
6772
  if (Array.isArray(result)) return result.length === 0 ? "miss" : "complete";
@@ -6549,7 +6817,11 @@ function errorResult(message) {
6549
6817
  // src/install/content.ts
6550
6818
  var NUDGE_BEGIN = "<!-- vg:begin -->";
6551
6819
  var NUDGE_END = "<!-- vg:end -->";
6552
- function skillMarkdown() {
6820
+ function clientFlag(client) {
6821
+ return client ? `--client=${client}` : "--client=<your-ai>";
6822
+ }
6823
+ function skillMarkdown(client) {
6824
+ const cf = clientFlag(client);
6553
6825
  return `---
6554
6826
  name: vg
6555
6827
  description: Query the local code graph (vg) for structure, impact, and navigation instead of grepping/reading many files.
@@ -6560,13 +6832,26 @@ description: Query the local code graph (vg) for structure, impact, and navigati
6560
6832
  This repo has a deterministic code graph built by \`vg\`. Prefer it over reading or
6561
6833
  grepping many files \u2014 it is smaller, more relevant context, and free.
6562
6834
 
6563
- ## When to use it
6835
+ ## Prefer the MCP tools
6836
+
6837
+ If the \`vg\` MCP server is registered (it is after \`vg install\`), call its
6838
+ read-only tools directly \u2014 they are the **fastest** path. The server keeps the
6839
+ map parsed, the relation index warm, and the embedding model loaded across calls,
6840
+ so each query is cheaper than spawning the CLI fresh. Use:
6841
+ \`query_graph\`, \`get_node\`, \`impact_of\`, \`find_path\`, \`list_hubs\`, \`list_areas\`,
6842
+ \`get_graph_summary\`, \`search_symbols\`. They are side-effect-free and
6843
+ auto-approvable, and the server records which client is calling automatically.
6844
+
6845
+ ## If you use the \`vg\` CLI instead
6564
6846
 
6565
- - **Understand code:** \`vg "<question>"\` returns a budget-bounded, fact-annotated
6566
- context block. Start here instead of opening files blindly.
6567
- - **Find a symbol:** \`vg show <name>\` \u2014 what it is, what it calls, what calls it.
6568
- - **Before changing something:** \`vg impact <name>\` \u2014 what breaks if you change it.
6569
- - **Navigate:** \`vg path <A> <B>\`, \`vg tree <name>\`, \`vg hubs\`, \`vg areas\`.
6847
+ When the MCP server isn't available, use the CLI \u2014 and **always pass \`${cf}\`**
6848
+ so your calls are counted (that's how the CLI-vs-MCP split is measured and the
6849
+ tools improved):
6850
+
6851
+ - **Understand code:** \`vg "<question>" ${cf}\` \u2014 a budget-bounded, fact-annotated context block.
6852
+ - **Find a symbol:** \`vg show <name> ${cf}\` \u2014 what it is, what it calls, what calls it.
6853
+ - **Before changing something:** \`vg impact <name> ${cf}\` \u2014 what breaks if you change it.
6854
+ - **Navigate:** \`vg path <A> <B> ${cf}\`, \`vg tree <name> ${cf}\`.
6570
6855
 
6571
6856
  ## Dependencies & library docs
6572
6857
 
@@ -6576,12 +6861,6 @@ grepping many files \u2014 it is smaller, more relevant context, and free.
6576
6861
  - **Version-correct docs:** \`vg lib <name>\` returns drift-annotated, version-
6577
6862
  specific usage docs for a library \u2014 inject these instead of guessing an API.
6578
6863
 
6579
- ## Via MCP
6580
-
6581
- If the \`vg\` MCP server is registered, call its read-only tools directly:
6582
- \`query_graph\`, \`get_node\`, \`impact_of\`, \`find_path\`, \`list_hubs\`, \`list_areas\`,
6583
- \`get_graph_summary\`. They are side-effect-free and auto-approvable.
6584
-
6585
6864
  ## Keep it fresh
6586
6865
 
6587
6866
  The map keeps itself fresh: \`vg ask\` and the MCP tools detect changed files
@@ -6591,28 +6870,33 @@ The map lives at \`.vibgrate/graph.json\` and is deterministic (byte-identical
6591
6870
  across machines).
6592
6871
  `;
6593
6872
  }
6594
- function nudgeMarkdown(smallRepo) {
6873
+ function nudgeMarkdown(smallRepo, client) {
6874
+ const cf = clientFlag(client);
6595
6875
  if (smallRepo) {
6596
6876
  return `${NUDGE_BEGIN}
6597
6877
  ## Code navigation (vg)
6598
6878
 
6599
6879
  This repo is small enough that searching files directly is fine. Still, before a
6600
- change, \`vg impact <name>\` shows what it breaks, and \`vg "<question>"\` gives a
6601
- compact, cited context block when you want one.
6880
+ change, \`vg impact <name> ${cf}\` shows what it breaks, and \`vg "<question>" ${cf}\`
6881
+ gives a compact, cited context block when you want one. Prefer the \`vg\` MCP tools
6882
+ when the server is registered; when you use the CLI, pass \`${cf}\` so the call is
6883
+ counted.
6602
6884
  ${NUDGE_END}`;
6603
6885
  }
6604
6886
  return `${NUDGE_BEGIN}
6605
6887
  ## Code navigation (vg) \u2014 prefer the map
6606
6888
 
6607
6889
  A deterministic code graph is available. It is usually the faster, cheaper path
6608
- than reading or grepping many files:
6890
+ than reading or grepping many files. Prefer the \`vg\` MCP tools when the server is
6891
+ registered (fastest \u2014 the map and model stay warm across calls). When you use the
6892
+ CLI instead, pass \`${cf}\` so the call is counted:
6609
6893
 
6610
- - \`vg "<question>"\` \u2014 a compact, fact-annotated context block for a question.
6611
- - \`vg show <name>\` \u2014 what a symbol is, its callers and callees.
6612
- - \`vg impact <name>\` \u2014 what breaks if you change it.
6894
+ - \`vg "<question>" ${cf}\` \u2014 a compact, fact-annotated context block for a question.
6895
+ - \`vg show <name> ${cf}\` \u2014 what a symbol is, its callers and callees.
6896
+ - \`vg impact <name> ${cf}\` \u2014 what breaks if you change it.
6613
6897
 
6614
- Prefer these (or the \`vg\` MCP tools) before opening many files. This is advisory,
6615
- not mandatory \u2014 remove it with \`vg uninstall\` (or delete this block).
6898
+ This is advisory, not mandatory \u2014 remove it with \`vg uninstall\` (or delete this
6899
+ block).
6616
6900
  ${NUDGE_END}`;
6617
6901
  }
6618
6902
  function mcpServerEntry(launch = { command: "vg", args: ["serve"] }) {
@@ -6702,19 +6986,19 @@ function installAssistant(a, opts) {
6702
6986
  const skipped = [];
6703
6987
  let note;
6704
6988
  if (a.skill) {
6705
- writeFileEnsured(path18.join(opts.root, a.skill), skillMarkdown());
6989
+ writeFileEnsured(path19.join(opts.root, a.skill), skillMarkdown(a.id));
6706
6990
  wrote.push(a.skill);
6707
6991
  }
6708
6992
  if (a.mcp) {
6709
6993
  const launch = opts.launch ?? detectServeLaunch();
6710
- upsertMcp(path18.join(opts.root, a.mcp.file), a.mcp, launch);
6994
+ upsertMcp(path19.join(opts.root, a.mcp.file), a.mcp, launch);
6711
6995
  wrote.push(a.mcp.file);
6712
6996
  note = launch.note;
6713
6997
  } else {
6714
6998
  skipped.push("mcp (host-specific setup)");
6715
6999
  }
6716
7000
  if (a.nudge && opts.hook !== false) {
6717
- writeNudge(path18.join(opts.root, a.nudge.file), a.nudge, opts.smallRepo);
7001
+ writeNudge(path19.join(opts.root, a.nudge.file), a.nudge, opts.smallRepo, a.id);
6718
7002
  wrote.push(a.nudge.file);
6719
7003
  } else if (a.nudge) {
6720
7004
  skipped.push("nudge (--no-hook)");
@@ -6724,40 +7008,40 @@ function installAssistant(a, opts) {
6724
7008
  function uninstallAssistant(a, root, purge) {
6725
7009
  const removed = [];
6726
7010
  if (a.mcp) {
6727
- const file = path18.join(root, a.mcp.file);
7011
+ const file = path19.join(root, a.mcp.file);
6728
7012
  if (removeMcp(file, a.mcp)) removed.push(a.mcp.file);
6729
7013
  }
6730
7014
  if (a.nudge) {
6731
- const file = path18.join(root, a.nudge.file);
7015
+ const file = path19.join(root, a.nudge.file);
6732
7016
  if (a.nudge.kind === "block") {
6733
7017
  if (removeBlock(file)) removed.push(a.nudge.file);
6734
- } else if (fs18.existsSync(file)) {
6735
- fs18.rmSync(file);
7018
+ } else if (fs19.existsSync(file)) {
7019
+ fs19.rmSync(file);
6736
7020
  removed.push(a.nudge.file);
6737
7021
  }
6738
7022
  }
6739
7023
  if (purge && a.skill) {
6740
- const file = path18.join(root, a.skill);
6741
- if (fs18.existsSync(file)) {
6742
- fs18.rmSync(file);
7024
+ const file = path19.join(root, a.skill);
7025
+ if (fs19.existsSync(file)) {
7026
+ fs19.rmSync(file);
6743
7027
  removed.push(a.skill);
6744
7028
  }
6745
7029
  }
6746
7030
  return removed;
6747
7031
  }
6748
7032
  function writeFileEnsured(file, content) {
6749
- fs18.mkdirSync(path18.dirname(file), { recursive: true });
6750
- fs18.writeFileSync(file, content);
7033
+ fs19.mkdirSync(path19.dirname(file), { recursive: true });
7034
+ fs19.writeFileSync(file, content);
6751
7035
  }
6752
7036
  function writeNavigationConfig(root) {
6753
- const rel2 = path18.join(".vibgrate", "mcp-navigation.json");
7037
+ const rel2 = path19.join(".vibgrate", "mcp-navigation.json");
6754
7038
  const doc = {
6755
7039
  _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.",
6756
7040
  hot_core: [...HOT_TOOLS],
6757
7041
  deferred: deferredToolNames(),
6758
7042
  toolset: navigationToolsetConfig()
6759
7043
  };
6760
- writeFileEnsured(path18.join(root, rel2), `${JSON.stringify(doc, null, 2)}
7044
+ writeFileEnsured(path19.join(root, rel2), `${JSON.stringify(doc, null, 2)}
6761
7045
  `);
6762
7046
  return rel2;
6763
7047
  }
@@ -6772,7 +7056,7 @@ function upsertMcp(file, target, launch) {
6772
7056
  `);
6773
7057
  }
6774
7058
  function removeMcp(file, target) {
6775
- if (!fs18.existsSync(file)) return false;
7059
+ if (!fs19.existsSync(file)) return false;
6776
7060
  const config = readJson(file);
6777
7061
  const bag = config[target.key];
6778
7062
  if (!bag || !(bag.vg !== void 0)) return false;
@@ -6781,22 +7065,22 @@ function removeMcp(file, target) {
6781
7065
  `);
6782
7066
  return true;
6783
7067
  }
6784
- function writeNudge(file, target, smallRepo) {
7068
+ function writeNudge(file, target, smallRepo, client) {
6785
7069
  if (target.kind === "file") {
6786
7070
  const body = file.endsWith(".mdc") ? `---
6787
7071
  description: vg code graph
6788
7072
  alwaysApply: true
6789
7073
  ---
6790
- ${stripMarkers(nudgeMarkdown(smallRepo))}` : stripMarkers(nudgeMarkdown(smallRepo));
7074
+ ${stripMarkers(nudgeMarkdown(smallRepo, client))}` : stripMarkers(nudgeMarkdown(smallRepo, client));
6791
7075
  writeFileEnsured(file, `${body}
6792
7076
  `);
6793
7077
  return;
6794
7078
  }
6795
- upsertBlock(file, nudgeMarkdown(smallRepo));
7079
+ upsertBlock(file, nudgeMarkdown(smallRepo, client));
6796
7080
  }
6797
7081
  function upsertBlock(file, block) {
6798
- fs18.mkdirSync(path18.dirname(file), { recursive: true });
6799
- let existing = fs18.existsSync(file) ? fs18.readFileSync(file, "utf8") : "";
7082
+ fs19.mkdirSync(path19.dirname(file), { recursive: true });
7083
+ let existing = fs19.existsSync(file) ? fs19.readFileSync(file, "utf8") : "";
6800
7084
  const re = new RegExp(`${escapeRe(NUDGE_BEGIN)}[\\s\\S]*?${escapeRe(NUDGE_END)}`);
6801
7085
  if (re.test(existing)) existing = existing.replace(re, block);
6802
7086
  else existing = existing.length ? `${existing.replace(/\s*$/, "")}
@@ -6804,20 +7088,20 @@ function upsertBlock(file, block) {
6804
7088
  ${block}
6805
7089
  ` : `${block}
6806
7090
  `;
6807
- fs18.writeFileSync(file, existing);
7091
+ fs19.writeFileSync(file, existing);
6808
7092
  }
6809
7093
  function removeBlock(file) {
6810
- if (!fs18.existsSync(file)) return false;
6811
- const existing = fs18.readFileSync(file, "utf8");
7094
+ if (!fs19.existsSync(file)) return false;
7095
+ const existing = fs19.readFileSync(file, "utf8");
6812
7096
  const re = new RegExp(`\\n*${escapeRe(NUDGE_BEGIN)}[\\s\\S]*?${escapeRe(NUDGE_END)}\\n*`);
6813
7097
  if (!re.test(existing)) return false;
6814
- fs18.writeFileSync(file, existing.replace(re, "\n"));
7098
+ fs19.writeFileSync(file, existing.replace(re, "\n"));
6815
7099
  return true;
6816
7100
  }
6817
7101
  function readJson(file) {
6818
- if (!fs18.existsSync(file)) return {};
7102
+ if (!fs19.existsSync(file)) return {};
6819
7103
  try {
6820
- const parsed = JSON.parse(fs18.readFileSync(file, "utf8"));
7104
+ const parsed = JSON.parse(fs19.readFileSync(file, "utf8"));
6821
7105
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
6822
7106
  return parsed;
6823
7107
  }
@@ -7039,6 +7323,6 @@ function spdx(ctx) {
7039
7323
  return JSON.stringify(doc, null, 2) + "\n";
7040
7324
  }
7041
7325
 
7042
- 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, epistemicBreakdown, exportGraph, fetchHostedDocs, findGitRoot, findNodes, formatForExt, getNodeEmbeddings, gitignoreEntryForCredentials, groundGraph, hasDrift, identifierParts, impactOf, ingestHostForRegionId, 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, readUsage, 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 };
7043
- //# sourceMappingURL=chunk-2DJ3UXG7.js.map
7044
- //# sourceMappingURL=chunk-2DJ3UXG7.js.map
7326
+ export { ASSISTANTS, CLI_TOOL_ALIASES, 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, countTokens, coveringTests, createServer, createWorkspaceDsn, credentialsPath, dashHostForIngestHost, decodeScipIndex, defaultGraphPath, detectRunner, detectServeLaunch, discover, discoverModels, driftCount, driftFor, dsnCommand, embeddingsCached, enrichOnline, ensureGitignored, epistemicBreakdown, exportGraph, fetchHostedDocs, findGitRoot, findNodes, formatForExt, getNodeEmbeddings, gitignoreEntryForCredentials, groundGraph, hasDrift, identifierParts, impactOf, indexFor, ingestHostForRegionId, 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, readUsage, recordCliCall, 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 };
7327
+ //# sourceMappingURL=chunk-3TCE6IGZ.js.map
7328
+ //# sourceMappingURL=chunk-3TCE6IGZ.js.map