@vibgrate/cli 2026.717.1 → 2026.720.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { resolveDsn, resolveIngestHost, dashHostForIngestHost, resolveCliInvocation, availableRegionIds, createWorkspaceDsn, writeStoredCredentials, credentialsPath, findGitRoot, ensureGitignored, gitignoreEntryForCredentials, clearStoredCredentials, inventory, uploadScanArtifact, ingestHostForRegionId, dsnCommand, pushCommand, CliError, ExitCode, NPX_INVOCATION, refreshIfStale, driftCount, embeddingsCached, resolveEmbedModel, loadEmbedder, getNodeEmbeddings, queryGraphSemantic, queryGraph, unavailableMessage, recordCliCall, CLI_TOOL_ALIASES, countPending, resolveOne, indexFor, countTokens, shortestPath, renderReport, impactOf, testsToRun, detectRunner, defaultGraphPath, serveStdio, ASSISTANTS, detectAssistants, usageError, loadGraph, detectServeLaunch, installAssistant, writeNavigationConfig, uninstallAssistant, coveringTests, FREE_PACK, readScanArtifact, enrichOnline, ECOSYSTEMS, discoverModels, readSavings, readUsage, exportGraph, catalogPath, libDir, stableStringify, buildGraph, writeArtifacts, writeSnapshot, UsageError, ResourceLimitError, epistemicBreakdown, cacheDir, probeFreshness, discover, vibgrateDir, modelCacheInfo, clearModelCache, GraphSource, createServer, assistantById, serializeGraph, resolveLimits, loadCatalog, driftFor, addLibrary, parseDsn as parseDsn$1, publishPrivateLibrary, resolveLib, libId, resolveVersion, readDoc, localPackageDocs, localApiSurface, assessDocQuality, symbolsFromApi, fetchHostedDocsCached, selectForBudget, formatForExt, verifyDeterminism, renderHtml, isModelReady, SKIP_DIRS, SKIP_FILES } from './chunk-L42NVMD6.js';
2
+ import { resolveDsn, resolveIngestHost, dashHostForIngestHost, resolveCliInvocation, availableRegionIds, createWorkspaceDsn, writeStoredCredentials, credentialsPath, findGitRoot, ensureGitignored, gitignoreEntryForCredentials, clearStoredCredentials, inventory, uploadScanArtifact, ingestHostForRegionId, dsnCommand, pushCommand, CliError, ExitCode, NPX_INVOCATION, refreshIfStale, driftCount, embeddingsCached, resolveEmbedModel, loadEmbedder, getNodeEmbeddings, queryGraphSemantic, queryGraph, unavailableMessage, recordCliCall, CLI_TOOL_ALIASES, countPending, resolveOne, indexFor, countTokens, shortestPath, renderReport, impactOf, testsToRun, detectRunner, defaultGraphPath, serveStdio, ASSISTANTS, detectAssistants, usageError, loadGraph, detectServeLaunch, installAssistant, writeNavigationConfig, uninstallAssistant, coveringTests, FREE_PACK, readScanArtifact, enrichOnline, ECOSYSTEMS, discoverModels, readSavings, readUsage, exportGraph, catalogPath, libDir, stableStringify, buildGraph, writeArtifacts, writeSnapshot, UsageError, ResourceLimitError, epistemicBreakdown, cacheDir, probeFreshness, discover, vibgrateDir, modelCacheInfo, clearModelCache, GraphSource, createServer, assistantById, serializeGraph, resolveLimits, loadCatalog, driftFor, addLibrary, parseDsn as parseDsn$1, publishPrivateLibrary, resolveLib, libId, resolveVersion, readDoc, localPackageDocs, localApiSurface, assessDocQuality, symbolsFromApi, fetchHostedDocsCached, selectForBudget, formatForExt, hostedBase, verifyDeterminism, renderHtml, isModelReady, SAVINGS_TOOLS, DEFAULT_RATE_LABEL, readStoredCredentials, projectCredentialsPath, DEFAULT_RATE_PER_M, homeCredentialsPath, SKIP_DIRS, SKIP_FILES } from './chunk-VQLOVQZP.js';
3
3
  import { resolvedGrammarFiles, grammarSetVersion, hashString, canonicalize, hashBytes } from './chunk-VFO5UDAT.js';
4
- import { loadAdvancedScanHook, baselineCommand } from './chunk-WZNIDXZP.js';
5
- import { ensureDir, pathExists, writeDefaultConfig, parseDsn, detectVcs, computeRepoFingerprint, resolveRepositoryName, fetchScanPreflight, runCoreScan, VERSION, readJsonFile, writeTextFile, loadConfig, prepareCompressedUpload, computeDriftScore, titleBox, projectTypeToVulnEcosystem, driftBar, gitHistoryAvailable, buildVersionTimelines, findPackageAnyEcosystem, normalizeConstraint, findVersionCrossings, versionSatisfies, PROJECT_TYPE_TO_OSV_ECOSYSTEM, fetchRiskySymbols, brandProgressBar, resolveHead, workingTreeDirty } from './chunk-M62BGJMK.js';
4
+ import { loadAdvancedScanHook, baselineCommand } from './chunk-VYSTOEAK.js';
5
+ import { ensureDir, pathExists, writeDefaultConfig, parseDsn, detectVcs, computeRepoFingerprint, resolveRepositoryName, fetchScanPreflight, runCoreScan, VERSION, readJsonFile, writeTextFile, loadConfig, prepareCompressedUpload, computeDriftScore, titleBox, projectTypeToVulnEcosystem, driftBar, gitHistoryAvailable, buildVersionTimelines, findPackageAnyEcosystem, normalizeConstraint, findVersionCrossings, versionSatisfies, PROJECT_TYPE_TO_OSV_ECOSYSTEM, fetchRiskySymbols, brandProgressBar, resolveHead, workingTreeDirty } from './chunk-AJDUMRVI.js';
6
6
  import './chunk-RXP66R2E.js';
7
7
  import { pathExists as pathExists$1, parseExcludePatterns } from './chunk-GI6W53LM.js';
8
8
  import * as fs8 from 'fs';
@@ -16,7 +16,7 @@ import * as crypto from 'crypto';
16
16
  import { randomUUID } from 'crypto';
17
17
  import chalk8 from 'chalk';
18
18
  import * as os from 'os';
19
- import * as fs14 from 'fs/promises';
19
+ import * as fs15 from 'fs/promises';
20
20
  import * as readline from 'readline';
21
21
  import semver from 'semver';
22
22
 
@@ -288,21 +288,23 @@ var ROBOT = [
288
288
  " " + c.dim("\u2524") + teal("\u2502") + " " + c.dim("\u2581\u2581") + " " + teal("\u2502") + c.dim("\u251C"),
289
289
  " " + teal("\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u256F")
290
290
  ];
291
- function logoLines(root) {
291
+ function logoLines(root, opts = {}) {
292
+ const product = opts.product ?? "Graph";
293
+ const tagline = opts.tagline ?? "Code Intelligence Engine";
292
294
  return [
293
295
  "",
294
296
  ` ${ROBOT[0]}`,
295
297
  ` ${ROBOT[1]}`,
296
298
  ` ${ROBOT[2]}`,
297
299
  ` ${ROBOT[3]}`,
298
- ` ${c.bold.white("Vibgrate")} ${teal("Graph")} ${c.dim(`\xB7 Code Intelligence Engine \xB7 v${VERSION}`)}`,
300
+ ` ${c.bold.white("Vibgrate")} ${teal(product)} ${c.dim(`\xB7 ${tagline} \xB7 v${VERSION}`)}`,
299
301
  ...root ? [` ${c.dim(root)}`] : [],
300
302
  ""
301
303
  ];
302
304
  }
303
- function printLogo(root) {
305
+ function printLogo(root, opts = {}) {
304
306
  if (!process.stderr.isTTY) return;
305
- process.stderr.write(`${logoLines(root).join("\n")}
307
+ process.stderr.write(`${logoLines(root, opts).join("\n")}
306
308
  `);
307
309
  }
308
310
 
@@ -1287,7 +1289,31 @@ var OFFSET_FILE = "stats-share.json";
1287
1289
  var INSTALL_ID_DIR = path.join(os.homedir(), ".vibgrate");
1288
1290
  var INSTALL_ID_FILE = path.join(INSTALL_ID_DIR, "install-id");
1289
1291
  var SEND_TIMEOUT_MS = 4e3;
1290
- function installId() {
1292
+ function telemetryOptOut(env = process.env) {
1293
+ const dnt = (env.DO_NOT_TRACK ?? "").trim().toLowerCase();
1294
+ if (dnt !== "" && dnt !== "0" && dnt !== "false") return "DO_NOT_TRACK";
1295
+ const vt = (env.VIBGRATE_TELEMETRY ?? "").trim().toLowerCase();
1296
+ if (vt === "0" || vt === "false" || vt === "off") return "VIBGRATE_TELEMETRY";
1297
+ return null;
1298
+ }
1299
+ var CI_ENV_VARS = [
1300
+ "CI",
1301
+ "GITHUB_ACTIONS",
1302
+ "GITLAB_CI",
1303
+ "CIRCLECI",
1304
+ "TRAVIS",
1305
+ "BUILDKITE",
1306
+ "JENKINS_URL",
1307
+ "TF_BUILD"
1308
+ ];
1309
+ function isCI(env = process.env) {
1310
+ return CI_ENV_VARS.some((v) => {
1311
+ const val = (env[v] ?? "").trim().toLowerCase();
1312
+ return val !== "" && val !== "0" && val !== "false";
1313
+ });
1314
+ }
1315
+ function installId(env = process.env) {
1316
+ if (telemetryOptOut(env) !== null || isCI(env)) return randomUuid();
1291
1317
  try {
1292
1318
  const existing = fs8.readFileSync(INSTALL_ID_FILE, "utf8").trim();
1293
1319
  if (isUuid(existing)) return existing;
@@ -1463,6 +1489,241 @@ var StatsSharer = class {
1463
1489
  }
1464
1490
  };
1465
1491
 
1492
+ // src/util/origin.ts
1493
+ function originAllowed(origin, allowlist) {
1494
+ if (origin === void 0 || origin === "") return true;
1495
+ const allowed = (allowlist ?? "").split(",").map((s) => s.trim()).filter(Boolean);
1496
+ if (allowed.includes("*")) return true;
1497
+ if (allowed.includes(origin)) return true;
1498
+ return isLoopbackOrigin(origin);
1499
+ }
1500
+ function isLoopbackOrigin(origin) {
1501
+ let url;
1502
+ try {
1503
+ url = new URL(origin);
1504
+ } catch {
1505
+ return false;
1506
+ }
1507
+ if (url.protocol !== "http:" && url.protocol !== "https:") return false;
1508
+ const host = url.hostname;
1509
+ if (host === "localhost" || host === "[::1]" || host === "::1") return true;
1510
+ return /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host);
1511
+ }
1512
+
1513
+ // src/mcp/serve-stats.ts
1514
+ function zeroRow(key2) {
1515
+ return { key: key2, calls: 0, complete: 0, partial: 0, miss: 0, totalMs: 0, vgTokens: 0, baselineTokens: 0 };
1516
+ }
1517
+ var SessionStats = class {
1518
+ startedAt;
1519
+ revision = 0;
1520
+ lastCallAt = null;
1521
+ totals = zeroRow("total");
1522
+ byClient = /* @__PURE__ */ new Map();
1523
+ byTool = /* @__PURE__ */ new Map();
1524
+ constructor(now2 = Date.now()) {
1525
+ this.startedAt = now2;
1526
+ }
1527
+ record(sample, now2 = Date.now()) {
1528
+ this.revision++;
1529
+ this.lastCallAt = now2;
1530
+ for (const row of [this.totals, this.rowFor(this.byClient, sample.client), this.rowFor(this.byTool, sample.tool)]) {
1531
+ row.calls++;
1532
+ row[sample.outcome]++;
1533
+ row.totalMs += sample.ms;
1534
+ row.vgTokens += sample.vgTokens;
1535
+ row.baselineTokens += sample.baselineTokens;
1536
+ }
1537
+ }
1538
+ snapshot() {
1539
+ const sorted = (m) => [...m.values()].map((r) => ({ ...r })).sort((a, b) => b.calls - a.calls || a.key.localeCompare(b.key));
1540
+ return {
1541
+ startedAt: this.startedAt,
1542
+ revision: this.revision,
1543
+ lastCallAt: this.lastCallAt,
1544
+ totals: { ...this.totals },
1545
+ clients: sorted(this.byClient),
1546
+ tools: sorted(this.byTool)
1547
+ };
1548
+ }
1549
+ rowFor(m, key2) {
1550
+ let row = m.get(key2);
1551
+ if (!row) {
1552
+ row = zeroRow(key2);
1553
+ m.set(key2, row);
1554
+ }
1555
+ return row;
1556
+ }
1557
+ };
1558
+ function fmtUptime(ms) {
1559
+ const s = Math.max(0, Math.floor(ms / 1e3));
1560
+ if (s < 60) return `${s}s`;
1561
+ const m = Math.floor(s / 60);
1562
+ if (m < 60) return `${m}m ${String(s % 60).padStart(2, "0")}s`;
1563
+ const h = Math.floor(m / 60);
1564
+ if (h < 24) return `${h}h ${String(m % 60).padStart(2, "0")}m`;
1565
+ const d = Math.floor(h / 24);
1566
+ return `${d}d ${String(h % 24).padStart(2, "0")}h`;
1567
+ }
1568
+ function fmtTokens(n) {
1569
+ if (n >= 1e6) return `${(n / 1e6).toFixed(2)}M`;
1570
+ if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k`;
1571
+ return String(n);
1572
+ }
1573
+ function fmtAvgMs(row) {
1574
+ if (row.calls === 0) return "\u2014";
1575
+ const avg = row.totalMs / row.calls;
1576
+ return avg >= 1e3 ? `${(avg / 1e3).toFixed(1)}s` : `${Math.round(avg)}ms`;
1577
+ }
1578
+ var teal2 = (s) => c.hex("#3FB0A4")(s);
1579
+ var mint2 = (s) => c.hex("#4FE3C1")(s);
1580
+ var SPINNER = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1581
+ var MAX_TOOL_ROWS = 8;
1582
+ function serveStatusLines(snap, now2, tick = 0) {
1583
+ const spinner = mint2(SPINNER[tick % SPINNER.length]);
1584
+ const up = fmtUptime(now2 - snap.startedAt);
1585
+ const lines = [];
1586
+ if (snap.totals.calls === 0) {
1587
+ lines.push(
1588
+ ` ${spinner} ${c.bold.white("vg serve")} ${c.dim("\xB7")} up ${teal2(up)} ${c.dim("\xB7 waiting for your assistant\u2019s first tool call\u2026")}`
1589
+ );
1590
+ return lines;
1591
+ }
1592
+ const t = snap.totals;
1593
+ const answered = Math.round((t.complete + t.partial) / t.calls * 100);
1594
+ const ago = snap.lastCallAt === null ? "" : c.dim(` \xB7 last call ${fmtUptime(now2 - snap.lastCallAt)} ago`);
1595
+ lines.push(
1596
+ ` ${spinner} ${c.bold.white("vg serve")} ${c.dim("\xB7")} up ${teal2(up)} ${c.dim("\xB7")} ${c.bold.white(String(t.calls))} call${t.calls === 1 ? "" : "s"} ${c.dim("\xB7")} ${mint2(`${answered}%`)} answered${ago}`
1597
+ );
1598
+ lines.push(
1599
+ ` ${c.dim("clients")} ` + snap.clients.map((cl) => `${c.white(cl.key)} ${teal2(String(cl.calls))}${c.dim(` (avg ${fmtAvgMs(cl)})`)}`).join(c.dim(" \xB7 "))
1600
+ );
1601
+ const shown = snap.tools.slice(0, MAX_TOOL_ROWS);
1602
+ const nameW = Math.max(...shown.map((r) => r.key.length));
1603
+ for (const row of shown) {
1604
+ let line = ` ${c.white(row.key.padEnd(nameW))} ${teal2(String(row.calls).padStart(4))} ${c.dim("\xB7")} ` + c.dim(`avg ${fmtAvgMs(row)}`);
1605
+ if (SAVINGS_TOOLS.has(row.key) && row.baselineTokens > 0) {
1606
+ line += c.dim(" \xB7 ctx ") + c.white(fmtTokens(row.vgTokens)) + c.dim(` vs \u2248${fmtTokens(row.baselineTokens)} grep/read`);
1607
+ }
1608
+ if (row.miss > 0) line += c.dim(" \xB7 ") + c.yellow(`${row.miss} miss`);
1609
+ lines.push(line);
1610
+ }
1611
+ if (snap.tools.length > shown.length) {
1612
+ lines.push(` ${c.dim(`\u2026 +${snap.tools.length - shown.length} more tools`)}`);
1613
+ }
1614
+ if (t.baselineTokens > 0) {
1615
+ const savedTokens = Math.max(0, t.baselineTokens - t.vgTokens);
1616
+ const savedUsd = savedTokens / 1e6 * DEFAULT_RATE_PER_M;
1617
+ const ratio = t.vgTokens > 0 ? (t.baselineTokens / t.vgTokens).toFixed(1) : "\u2014";
1618
+ lines.push(
1619
+ ` ${c.dim("session")} ctx ${c.bold.white(fmtTokens(t.vgTokens))} ` + c.dim(`vs grep/read \u2248${fmtTokens(t.baselineTokens)} \u2192 `) + mint2(`${ratio}\xD7 fewer`)
1620
+ );
1621
+ lines.push(
1622
+ ` ${c.dim("est. saved \u2248")} ${mint2(`${fmtTokens(savedTokens)} tokens ($${savedUsd.toFixed(2)})`)} ` + c.dim(`(${DEFAULT_RATE_LABEL}; estimate)`)
1623
+ );
1624
+ }
1625
+ return lines;
1626
+ }
1627
+ function serveHeartbeatLine(snap, now2) {
1628
+ const up = fmtUptime(now2 - snap.startedAt);
1629
+ const t = snap.totals;
1630
+ if (t.calls === 0) return c.dim(`vg \xB7 serving for ${up} \u2014 no tool calls yet`);
1631
+ const clients = snap.clients.map((cl) => `${cl.key} ${cl.calls}`).join(", ");
1632
+ let line = `vg \xB7 serving for ${up} \u2014 ${t.calls} call${t.calls === 1 ? "" : "s"} (${clients})`;
1633
+ if (t.baselineTokens > 0) {
1634
+ const savedTokens = Math.max(0, t.baselineTokens - t.vgTokens);
1635
+ line += ` \xB7 ctx ${fmtTokens(t.vgTokens)} vs \u2248${fmtTokens(t.baselineTokens)} grep/read \xB7 est. saved \u2248 ${fmtTokens(savedTokens)} tokens`;
1636
+ }
1637
+ return c.dim(line);
1638
+ }
1639
+ var TTY_INTERVAL_MS = 1e3;
1640
+ var HEARTBEAT_INTERVAL_MS = 15 * 60 * 1e3;
1641
+ var ServeStatusDisplay = class {
1642
+ constructor(stats, stream = process.stderr) {
1643
+ this.stats = stats;
1644
+ this.stream = stream;
1645
+ this.isTTY = (stream.isTTY ?? false) && process.env.VIBGRATE_PROGRESS_MODE !== "plain";
1646
+ }
1647
+ stats;
1648
+ stream;
1649
+ timer = null;
1650
+ tick = 0;
1651
+ lastRowCount = 0;
1652
+ lastFrame = "";
1653
+ lastHeartbeatRevision = -1;
1654
+ cursorHidden = false;
1655
+ isTTY;
1656
+ start() {
1657
+ if (this.timer) return;
1658
+ if (this.isTTY) {
1659
+ this.hideCursor();
1660
+ process.on("exit", this.restoreCursor);
1661
+ process.once("SIGINT", () => {
1662
+ this.restoreCursor();
1663
+ process.exit(130);
1664
+ });
1665
+ process.once("SIGTERM", () => {
1666
+ this.restoreCursor();
1667
+ process.exit(143);
1668
+ });
1669
+ this.timer = setInterval(() => this.renderTTY(), TTY_INTERVAL_MS);
1670
+ this.renderTTY();
1671
+ } else {
1672
+ this.timer = setInterval(() => this.renderHeartbeat(), HEARTBEAT_INTERVAL_MS);
1673
+ }
1674
+ this.timer.unref?.();
1675
+ }
1676
+ stop() {
1677
+ if (this.timer) {
1678
+ clearInterval(this.timer);
1679
+ this.timer = null;
1680
+ }
1681
+ if (this.isTTY) {
1682
+ let buf = "";
1683
+ if (this.lastRowCount > 0) buf += `\x1B[${this.lastRowCount}A\x1B[J`;
1684
+ this.stream.write(buf);
1685
+ this.lastRowCount = 0;
1686
+ this.restoreCursor();
1687
+ }
1688
+ }
1689
+ renderTTY() {
1690
+ this.tick++;
1691
+ const lines = serveStatusLines(this.stats.snapshot(), Date.now(), this.tick);
1692
+ const content = lines.join("\n") + "\n";
1693
+ if (content === this.lastFrame) return;
1694
+ this.lastFrame = content;
1695
+ let buf = "";
1696
+ if (this.lastRowCount > 0) buf += `\x1B[${this.lastRowCount}A\x1B[J`;
1697
+ buf += content;
1698
+ this.stream.write(buf);
1699
+ this.lastRowCount = this.countRows(lines);
1700
+ }
1701
+ renderHeartbeat() {
1702
+ const snap = this.stats.snapshot();
1703
+ if (snap.revision === this.lastHeartbeatRevision) return;
1704
+ this.lastHeartbeatRevision = snap.revision;
1705
+ this.stream.write(serveHeartbeatLine(snap, Date.now()) + "\n");
1706
+ }
1707
+ /** Terminal rows the frame occupies, accounting for line wrap. */
1708
+ countRows(lines) {
1709
+ const columns = Math.max(this.stream.columns ?? 80, 20);
1710
+ return lines.reduce((total, line) => {
1711
+ const visible = line.replace(/\x1b\[[0-9;]*m/g, "").length;
1712
+ return total + Math.max(1, Math.ceil(visible / columns));
1713
+ }, 0);
1714
+ }
1715
+ hideCursor() {
1716
+ if (this.cursorHidden) return;
1717
+ this.stream.write("\x1B[?25l");
1718
+ this.cursorHidden = true;
1719
+ }
1720
+ restoreCursor = () => {
1721
+ if (!this.cursorHidden) return;
1722
+ this.stream.write("\x1B[?25h");
1723
+ this.cursorHidden = false;
1724
+ };
1725
+ };
1726
+
1466
1727
  // src/commands/serve.ts
1467
1728
  var SHARE_FLUSH_INTERVAL_MS = 5 * 60 * 1e3;
1468
1729
  function registerServe(program) {
@@ -1472,25 +1733,40 @@ function registerServe(program) {
1472
1733
  const graphPath = global.graph ?? defaultGraphPath(root);
1473
1734
  const refresh = opts.refresh !== false && !global.graph;
1474
1735
  const local = global.local === true;
1475
- const shareStats = opts.shareStats === true && !local;
1736
+ const optOut = telemetryOptOut();
1737
+ const shareStats = opts.shareStats === true && !local && optOut === null;
1738
+ const quiet = global.quiet === true;
1739
+ const stats = quiet ? void 0 : new SessionStats();
1740
+ if (!quiet) {
1741
+ printLogo(root, { product: "AI Context", tagline: "Local-first MCP for your AI" });
1742
+ }
1476
1743
  const serveOpts = {
1477
- savings: opts.savings === true || shareStats,
1744
+ // A *requested* --share-stats always implies local recording, even when
1745
+ // the upload itself is suppressed (--local / env opt-out) — the local
1746
+ // ledger never leaves the machine, and the disclosure messages below
1747
+ // promise "recording locally".
1748
+ savings: opts.savings === true || opts.shareStats === true,
1478
1749
  shareStats,
1479
1750
  local,
1480
1751
  dedup: opts.dedup === true,
1481
- refresh
1752
+ refresh,
1753
+ stats
1482
1754
  };
1483
1755
  await ensureServableGraph(root, graphPath, global, refresh);
1484
1756
  if (opts.shareStats === true && local) {
1485
1757
  info(c.dim("vg \xB7 --share-stats ignored under --local (air-gapped): recording locally, not uploading."));
1758
+ } else if (opts.shareStats === true && optOut !== null) {
1759
+ info(c.dim(`vg \xB7 --share-stats disabled by ${optOut}: recording locally, not uploading.`));
1486
1760
  }
1487
1761
  if (shareStats) startSharing(root);
1762
+ const display = stats ? new ServeStatusDisplay(stats) : void 0;
1488
1763
  const freshness = refresh ? "auto-refresh" : "as built";
1489
1764
  if (opts.http) {
1490
- await serveHttp(graphPath, opts.host ?? "127.0.0.1", Number(opts.port) || 7437, serveOpts, freshness);
1765
+ await serveHttp(graphPath, opts.host ?? "127.0.0.1", Number(opts.port) || 7437, serveOpts, freshness, () => display?.start());
1491
1766
  } else {
1492
1767
  info(c.dim(`vg \xB7 MCP server on stdio (read-only, ${freshness}). Connect your assistant to this process.`));
1493
1768
  await serveStdio(graphPath, serveOpts);
1769
+ display?.start();
1494
1770
  }
1495
1771
  });
1496
1772
  applyGlobalOptions(cmd);
@@ -1546,7 +1822,7 @@ function startSharing(root) {
1546
1822
  });
1547
1823
  process.once("beforeExit", finalFlush);
1548
1824
  }
1549
- async function serveHttp(graphPath, host, port, opts, freshness) {
1825
+ async function serveHttp(graphPath, host, port, opts, freshness, onReady) {
1550
1826
  const { createServer: createHttp } = await import('http');
1551
1827
  const { StreamableHTTPServerTransport } = await import('@modelcontextprotocol/sdk/server/streamableHttp.js');
1552
1828
  const source = new GraphSource(graphPath, opts.refresh !== false);
@@ -1555,6 +1831,13 @@ async function serveHttp(graphPath, host, port, opts, freshness) {
1555
1831
  res.writeHead(404).end("not found");
1556
1832
  return;
1557
1833
  }
1834
+ const origin = req.headers.origin;
1835
+ if (!originAllowed(origin, process.env.VIBGRATE_ALLOWED_ORIGINS)) {
1836
+ res.writeHead(403, { "Content-Type": "text/plain" }).end(
1837
+ "forbidden origin \u2014 vg serve only accepts browser requests from loopback origins. Set VIBGRATE_ALLOWED_ORIGINS to allow others."
1838
+ );
1839
+ return;
1840
+ }
1558
1841
  try {
1559
1842
  const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
1560
1843
  const server = createServer(source, opts);
@@ -1571,6 +1854,7 @@ async function serveHttp(graphPath, host, port, opts, freshness) {
1571
1854
  });
1572
1855
  await new Promise((resolve17) => httpServer.listen(port, host, resolve17));
1573
1856
  info(c.dim(`vg \xB7 MCP server on http://${host}:${port}/mcp (read-only, local, ${freshness})`));
1857
+ onReady?.();
1574
1858
  await new Promise(() => {
1575
1859
  });
1576
1860
  }
@@ -3499,17 +3783,18 @@ function printBreakdown(usage) {
3499
3783
  info("");
3500
3784
  info(c.bold(" by command") + c.dim(" (complete = full result \xB7 partial = capped/paginated \xB7 miss = no result)"));
3501
3785
  const nameW = Math.max(7, ...usage.commands.map((cmd) => cmd.tool.length));
3502
- const header = " " + "command".padEnd(nameW) + ["calls", "complete", "partial", "miss", "success%"].map((h) => h.padStart(9)).join("");
3786
+ const header = " " + "command".padEnd(nameW) + ["calls", "complete", "partial", "miss", "success%", "avg ms"].map((h) => h.padStart(9)).join("");
3503
3787
  info(c.dim(header));
3504
3788
  for (const cmd of usage.commands) {
3505
3789
  info(
3506
- " " + cmd.tool.padEnd(nameW) + String(cmd.calls).padStart(9) + String(cmd.complete).padStart(9) + String(cmd.partial).padStart(9) + String(cmd.miss).padStart(9) + `${cmd.successPct}%`.padStart(9)
3790
+ " " + cmd.tool.padEnd(nameW) + String(cmd.calls).padStart(9) + String(cmd.complete).padStart(9) + String(cmd.partial).padStart(9) + String(cmd.miss).padStart(9) + `${cmd.successPct}%`.padStart(9) + // Absent ≠ zero: calls recorded before timing existed show '—'.
3791
+ (cmd.avgMs === null ? "\u2014" : String(cmd.avgMs)).padStart(9)
3507
3792
  );
3508
3793
  }
3509
3794
  const t = usage.totals;
3510
- info(c.dim(" " + "\u2500".repeat(nameW + 45)));
3795
+ info(c.dim(" " + "\u2500".repeat(nameW + 54)));
3511
3796
  info(
3512
- " " + c.bold("total".padEnd(nameW)) + String(t.calls).padStart(9) + String(t.complete).padStart(9) + String(t.partial).padStart(9) + String(t.miss).padStart(9) + "".padStart(9)
3797
+ " " + c.bold("total".padEnd(nameW)) + String(t.calls).padStart(9) + String(t.complete).padStart(9) + String(t.partial).padStart(9) + String(t.miss).padStart(9) + "".padStart(9) + "".padStart(9)
3513
3798
  );
3514
3799
  info(c.dim(` avg success across commands: ${usage.avgSuccessPct}%`));
3515
3800
  }
@@ -3803,6 +4088,125 @@ function registerBundle(program) {
3803
4088
  });
3804
4089
  applyGlobalOptions(cmd);
3805
4090
  }
4091
+ function registerDoctor(program) {
4092
+ const cmd = program.command("doctor").description("diagnose your setup: config, credentials, map freshness, hosted reachability, MCP launch").action(async function() {
4093
+ await runDoctor(readGlobal(this));
4094
+ });
4095
+ applyGlobalOptions(cmd);
4096
+ }
4097
+ var REACH_TIMEOUT_MS = 3e3;
4098
+ var CONFIG_BASENAMES = ["vibgrate.config.ts", "vibgrate.config.js", "vibgrate.config.json"];
4099
+ async function runDoctor(global) {
4100
+ const root = rootOf(global);
4101
+ const local = global.local === true;
4102
+ const graphPath = global.graph ?? defaultGraphPath(root);
4103
+ const configFile = CONFIG_BASENAMES.find((f) => fs8.existsSync(path.join(root, f))) ?? null;
4104
+ const graph = loadGraph(root, graphPath);
4105
+ let staleFiles = null;
4106
+ if (graph) {
4107
+ const probe = probeFreshness(root);
4108
+ if (probe) staleFiles = driftCount(probe.drift);
4109
+ }
4110
+ const catalog = loadCatalog(root);
4111
+ const libCount = Object.keys(catalog.libraries).length;
4112
+ const creds = diagnoseCredentials(root);
4113
+ const base = hostedBase();
4114
+ const reachable = local ? null : await probeReachable(base);
4115
+ const launch = detectServeLaunch();
4116
+ const optOut = telemetryOptOut();
4117
+ const d = {
4118
+ version: VERSION,
4119
+ node: process.version,
4120
+ platform: `${process.platform}/${process.arch}`,
4121
+ root,
4122
+ configFile,
4123
+ map: {
4124
+ path: path.relative(root, graphPath),
4125
+ built: graph !== null,
4126
+ generatedAt: graph?.generatedAt ?? null,
4127
+ staleFiles
4128
+ },
4129
+ libCatalog: {
4130
+ path: path.relative(root, catalogPath(root)),
4131
+ present: fs8.existsSync(catalogPath(root)),
4132
+ libraries: libCount
4133
+ },
4134
+ credentials: creds,
4135
+ hosted: { base, checked: !local, reachable },
4136
+ mcpLaunch: { command: launch.command, args: launch.args, note: launch.note ?? null },
4137
+ telemetry: { optOut, ci: isCI(), endpoint: statsEndpoint() }
4138
+ };
4139
+ if (global.json) {
4140
+ json(d);
4141
+ return;
4142
+ }
4143
+ info(`${c.cyan("vg")} doctor \xB7 v${d.version} \xB7 node ${d.node} \xB7 ${d.platform}`);
4144
+ info(` root ${d.root}`);
4145
+ info(` config ${d.configFile ? c.green(d.configFile) : c.dim("none (defaults) \u2014 `vg init` writes one")}`);
4146
+ if (!d.map.built) {
4147
+ info(` map ${c.yellow("none")} \u2014 run ${c.bold("vg")} to build ${c.dim(d.map.path)}`);
4148
+ } else {
4149
+ const fresh = d.map.staleFiles == null ? c.dim("freshness unknown (no snapshot)") : d.map.staleFiles === 0 ? c.green("up to date") : c.yellow(`${d.map.staleFiles} file(s) stale \u2014 auto-refreshes on next query`);
4150
+ info(` map ${c.green("built")} ${c.dim(d.map.generatedAt ?? "")} \xB7 ${fresh}`);
4151
+ }
4152
+ info(
4153
+ ` lib ${d.libCatalog.present ? `${c.green(String(d.libCatalog.libraries))} librar${d.libCatalog.libraries === 1 ? "y" : "ies"} in ${d.libCatalog.path}` : c.dim("no catalog \u2014 `vg lib add <source>` starts one; `vg lib <name>` works without it")}`
4154
+ );
4155
+ if (d.credentials.source === "none") {
4156
+ info(` auth ${c.dim("anonymous \u2014 fine for everything local; `vg login` enables push/publish")}`);
4157
+ } else {
4158
+ info(
4159
+ ` auth ${c.green(d.credentials.source)} ${c.dim(d.credentials.path ?? "")} \xB7 workspace ${d.credentials.workspaceId ?? "?"} \xB7 ${d.credentials.host ?? "?"}`
4160
+ );
4161
+ }
4162
+ if (!d.hosted.checked) {
4163
+ info(` hosted ${c.dim(`skipped under --local (${d.hosted.base})`)}`);
4164
+ } else {
4165
+ info(
4166
+ ` hosted ${d.hosted.base} \xB7 ${d.hosted.reachable ? c.green("reachable") : c.yellow("unreachable \u2014 local answers still work")}`
4167
+ );
4168
+ }
4169
+ info(
4170
+ ` mcp ${c.bold(`${d.mcpLaunch.command} ${d.mcpLaunch.args.join(" ")}`)}${d.mcpLaunch.note ? c.dim(` (${d.mcpLaunch.note})`) : ""}`
4171
+ );
4172
+ const tel = d.telemetry.optOut ? c.green(`opted out via ${d.telemetry.optOut}`) : "off by default \u2014 only `vg serve --share-stats` ever uploads";
4173
+ info(` telemetry ${tel}${d.telemetry.ci ? c.dim(" \xB7 CI detected") : ""}`);
4174
+ }
4175
+ function diagnoseCredentials(root) {
4176
+ const none = { source: "none", path: null, host: null, workspaceId: null };
4177
+ if (process.env.VIBGRATE_DSN) {
4178
+ const parsed2 = parseDsn$1(process.env.VIBGRATE_DSN);
4179
+ return { source: "env", path: null, host: parsed2?.host ?? null, workspaceId: parsed2?.workspaceId ?? null };
4180
+ }
4181
+ const stored = readStoredCredentials({ cwd: root });
4182
+ if (!stored) return none;
4183
+ const file = credentialsPath({ cwd: root });
4184
+ const source = process.env.VIBGRATE_CREDENTIALS ? "env" : file === projectCredentialsPath(root) ? "project" : "home";
4185
+ const parsed = parseDsn$1(stored.dsn);
4186
+ return {
4187
+ source,
4188
+ path: shortenHome(file),
4189
+ host: stored.ingestHost ?? parsed?.host ?? null,
4190
+ workspaceId: stored.workspaceId ?? parsed?.workspaceId ?? null
4191
+ };
4192
+ }
4193
+ function shortenHome(p) {
4194
+ const home = homeCredentialsPath().slice(0, -"/.vibgrate/credentials.json".length);
4195
+ return p.startsWith(home) ? `~${p.slice(home.length)}` : p;
4196
+ }
4197
+ async function probeReachable(base) {
4198
+ const controller = new AbortController();
4199
+ const timer = setTimeout(() => controller.abort(), REACH_TIMEOUT_MS);
4200
+ timer.unref?.();
4201
+ try {
4202
+ await fetch(base, { method: "HEAD", signal: controller.signal });
4203
+ return true;
4204
+ } catch {
4205
+ return false;
4206
+ } finally {
4207
+ clearTimeout(timer);
4208
+ }
4209
+ }
3806
4210
  var initCommand = new Command("init").description("Initialize vibgrate in a project").argument("[path]", "Path to initialize", ".").option("--baseline", "Create initial baseline after init").option("--yes", "Skip confirmation prompts").action(async (targetPath, opts) => {
3807
4211
  const rootDir = path.resolve(targetPath);
3808
4212
  const vibgrateDir2 = path.join(rootDir, ".vibgrate");
@@ -3816,7 +4220,7 @@ var initCommand = new Command("init").description("Initialize vibgrate in a proj
3816
4220
  console.log(chalk8.green("\u2714") + ` Created ${chalk8.bold("vibgrate.config.ts")}`);
3817
4221
  }
3818
4222
  if (opts.baseline) {
3819
- const { runBaseline } = await import('./baseline-YWGSHCKW.js');
4223
+ const { runBaseline } = await import('./baseline-5QPV6API.js');
3820
4224
  await runBaseline(rootDir);
3821
4225
  }
3822
4226
  console.log("");
@@ -3926,9 +4330,9 @@ async function fileReferencesIdentifier(rootDir, relFile, identifier, readFile3)
3926
4330
  }
3927
4331
  async function defaultReadFile(absPath) {
3928
4332
  try {
3929
- const stat2 = await fs14.stat(absPath);
4333
+ const stat2 = await fs15.stat(absPath);
3930
4334
  if (!stat2.isFile() || stat2.size > MAX_FILE_BYTES) return null;
3931
- return await fs14.readFile(absPath, "utf8");
4335
+ return await fs15.readFile(absPath, "utf8");
3932
4336
  } catch {
3933
4337
  return null;
3934
4338
  }
@@ -4121,22 +4525,22 @@ async function detectAiAssistant(rootDir) {
4121
4525
  return null;
4122
4526
  }
4123
4527
  function printAiContextPrompt(detectedAssistant) {
4124
- const teal2 = chalk8.hex("#3FB0A4");
4125
- const mint2 = chalk8.hex("#4FE3C1");
4528
+ const teal3 = chalk8.hex("#3FB0A4");
4529
+ const mint3 = chalk8.hex("#4FE3C1");
4126
4530
  const cli = resolveCliInvocation();
4127
4531
  const installArgs = detectedAssistant ? `install ${detectedAssistant}` : "install --all";
4128
4532
  const installCmd = `${cli} ${installArgs}`;
4129
4533
  const detectedNote = detectedAssistant ? chalk8.dim(` (${detectedAssistant} config detected)`) : "";
4130
4534
  console.log("");
4131
- console.log(teal2(" \u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E"));
4132
- console.log(teal2(" \u2502") + " " + mint2("\u25C6") + " " + chalk8.bold.white("Get AI-aware answers in your editor") + " " + teal2("\u2502"));
4133
- console.log(teal2(" \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F"));
4535
+ console.log(teal3(" \u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E"));
4536
+ console.log(teal3(" \u2502") + " " + mint3("\u25C6") + " " + chalk8.bold.white("Get AI-aware answers in your editor") + " " + teal3("\u2502"));
4537
+ console.log(teal3(" \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F"));
4134
4538
  console.log("");
4135
4539
  console.log(" " + chalk8.bold.white(installCmd) + detectedNote);
4136
4540
  console.log("");
4137
- console.log(" " + teal2("\xB7") + " " + chalk8.white("Code map in your assistant") + chalk8.dim(" \u2014 call trees, impact surfaces, import paths"));
4138
- console.log(" " + teal2("\xB7") + " " + chalk8.white("Offline drift") + chalk8.dim(" \u2014 DriftScore and upgrade priorities, right inside your editor"));
4139
- console.log(" " + teal2("\xB7") + " " + chalk8.white("Version-correct library docs") + chalk8.dim(" \u2014 pinned to your lockfile, no hallucinated APIs"));
4541
+ console.log(" " + teal3("\xB7") + " " + chalk8.white("Code map in your assistant") + chalk8.dim(" \u2014 call trees, impact surfaces, import paths"));
4542
+ console.log(" " + teal3("\xB7") + " " + chalk8.white("Offline drift") + chalk8.dim(" \u2014 DriftScore and upgrade priorities, right inside your editor"));
4543
+ console.log(" " + teal3("\xB7") + " " + chalk8.white("Version-correct library docs") + chalk8.dim(" \u2014 pinned to your lockfile, no hallucinated APIs"));
4140
4544
  console.log("");
4141
4545
  }
4142
4546
 
@@ -5121,8 +5525,8 @@ async function fetchLatestVersion() {
5121
5525
  }
5122
5526
  async function writeCache(data) {
5123
5527
  try {
5124
- await fs14.mkdir(CACHE_DIR, { recursive: true });
5125
- await fs14.writeFile(CACHE_FILE, JSON.stringify(data), "utf-8");
5528
+ await fs15.mkdir(CACHE_DIR, { recursive: true });
5529
+ await fs15.writeFile(CACHE_FILE, JSON.stringify(data), "utf-8");
5126
5530
  } catch {
5127
5531
  }
5128
5532
  }
@@ -5662,15 +6066,15 @@ Applied the ${chosen.label} plan:`));
5662
6066
  }
5663
6067
  function formatText(artifact) {
5664
6068
  const lines = [];
5665
- const teal2 = chalk8.hex("#3FB0A4");
5666
- const mint2 = chalk8.hex("#4FE3C1");
6069
+ const teal3 = chalk8.hex("#3FB0A4");
6070
+ const mint3 = chalk8.hex("#4FE3C1");
5667
6071
  lines.push("");
5668
- lines.push(" " + teal2("\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u256E") + mint2("\u279C"));
5669
- lines.push(" " + chalk8.dim("\u2524") + teal2("\u2502") + " " + mint2("\u25FC") + " " + mint2("\u25FC") + " " + teal2("\u2502") + chalk8.dim("\u251C") + " " + chalk8.bold.white("vibgrate"));
5670
- lines.push(" " + chalk8.dim("\u2524") + teal2("\u2502") + " " + chalk8.dim("\u2581\u2581") + " " + teal2("\u2502") + chalk8.dim("\u251C") + " " + chalk8.dim(`Code Intelligence Engine v${VERSION}`));
5671
- lines.push(" " + teal2("\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u256F"));
6072
+ lines.push(" " + teal3("\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u256E") + mint3("\u279C"));
6073
+ lines.push(" " + chalk8.dim("\u2524") + teal3("\u2502") + " " + mint3("\u25FC") + " " + mint3("\u25FC") + " " + teal3("\u2502") + chalk8.dim("\u251C") + " " + chalk8.bold.white("vibgrate"));
6074
+ lines.push(" " + chalk8.dim("\u2524") + teal3("\u2502") + " " + chalk8.dim("\u2581\u2581") + " " + teal3("\u2502") + chalk8.dim("\u251C") + " " + chalk8.dim(`Code Intelligence Engine v${VERSION}`));
6075
+ lines.push(" " + teal3("\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u256F"));
5672
6076
  lines.push("");
5673
- lines.push(...titleBox("Vibgrate Drift Report", teal2));
6077
+ lines.push(...titleBox("Vibgrate Drift Report", teal3));
5674
6078
  lines.push("");
5675
6079
  for (const project of artifact.projects) {
5676
6080
  lines.push(chalk8.bold(` \u2500\u2500 ${project.name} `) + chalk8.dim(`(${project.type}) ${project.path}`));
@@ -6949,6 +7353,7 @@ var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
6949
7353
  "oddities",
6950
7354
  "serve",
6951
7355
  "lsp",
7356
+ "doctor",
6952
7357
  "install",
6953
7358
  "uninstall",
6954
7359
  "share",
@@ -7004,6 +7409,7 @@ ${aliasNote}Docs: https://vibgrate.com/help`;
7004
7409
  registerLib(program);
7005
7410
  registerExport(program);
7006
7411
  registerBundle(program);
7412
+ registerDoctor(program);
7007
7413
  program.addCommand(initCommand);
7008
7414
  program.addCommand(scanCommand);
7009
7415
  program.addCommand(fixCommand);