@vibgrate/cli 2026.721.3 → 2026.722.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.
Files changed (65) hide show
  1. package/DOCS.md +185 -0
  2. package/README.md +2 -1
  3. package/dist/baseline-BD3A7EXD.js +7 -0
  4. package/dist/{baseline-37G4TINM.js.map → baseline-BD3A7EXD.js.map} +1 -1
  5. package/dist/chunk-2O5YZVZV.js +417 -0
  6. package/dist/chunk-2O5YZVZV.js.map +1 -0
  7. package/dist/{chunk-ELUFCQDR.js → chunk-3MXNNBLI.js} +240 -2519
  8. package/dist/chunk-3MXNNBLI.js.map +1 -0
  9. package/dist/{chunk-JLRF5TFA.js → chunk-5BDHQ7DD.js} +42 -71
  10. package/dist/chunk-5BDHQ7DD.js.map +1 -0
  11. package/dist/chunk-6CXTPC74.js +234 -0
  12. package/dist/chunk-6CXTPC74.js.map +1 -0
  13. package/dist/chunk-CRAMOEBE.js +675 -0
  14. package/dist/chunk-CRAMOEBE.js.map +1 -0
  15. package/dist/chunk-CS37OBE3.js +861 -0
  16. package/dist/chunk-CS37OBE3.js.map +1 -0
  17. package/dist/chunk-CT5BS56U.js +23 -0
  18. package/dist/chunk-CT5BS56U.js.map +1 -0
  19. package/dist/chunk-GGJZA3Q6.js +961 -0
  20. package/dist/chunk-GGJZA3Q6.js.map +1 -0
  21. package/dist/chunk-I7GY7MVN.js +833 -0
  22. package/dist/chunk-I7GY7MVN.js.map +1 -0
  23. package/dist/chunk-JBXNQCGE.js +484 -0
  24. package/dist/chunk-JBXNQCGE.js.map +1 -0
  25. package/dist/chunk-JFGYT6BI.js +32 -0
  26. package/dist/chunk-JFGYT6BI.js.map +1 -0
  27. package/dist/chunk-JYS4OBN2.js +148 -0
  28. package/dist/chunk-JYS4OBN2.js.map +1 -0
  29. package/dist/chunk-K3SLGULW.js +50 -0
  30. package/dist/chunk-K3SLGULW.js.map +1 -0
  31. package/dist/chunk-LEPTUB4H.js +108 -0
  32. package/dist/chunk-LEPTUB4H.js.map +1 -0
  33. package/dist/{chunk-5PLKADD6.js → chunk-NONYJLOJ.js} +4 -3
  34. package/dist/chunk-NONYJLOJ.js.map +1 -0
  35. package/dist/{chunk-IKJBELUV.js → chunk-PY3DNX5H.js} +4 -29
  36. package/dist/chunk-PY3DNX5H.js.map +1 -0
  37. package/dist/chunk-WNIIKCNF.js +972 -0
  38. package/dist/chunk-WNIIKCNF.js.map +1 -0
  39. package/dist/cli.js +2512 -1224
  40. package/dist/cli.js.map +1 -1
  41. package/dist/fs-KCABDURV.js +3 -0
  42. package/dist/fs-KCABDURV.js.map +1 -0
  43. package/dist/index.d.ts +3 -1
  44. package/dist/index.js +8 -3
  45. package/dist/index.js.map +1 -1
  46. package/dist/interactive-A4DISSIW.js +681 -0
  47. package/dist/interactive-A4DISSIW.js.map +1 -0
  48. package/dist/mcp-tools-I6PME3AV.js +3 -0
  49. package/dist/mcp-tools-I6PME3AV.js.map +1 -0
  50. package/dist/parse-worker.js +2 -1
  51. package/dist/parse-worker.js.map +1 -1
  52. package/dist/session-POJUDFPH.js +6 -0
  53. package/dist/session-POJUDFPH.js.map +1 -0
  54. package/dist/session-store-W5RZZHA3.js +3 -0
  55. package/dist/session-store-W5RZZHA3.js.map +1 -0
  56. package/dist/stream-json-Q2JGWPHB.js +65 -0
  57. package/dist/stream-json-Q2JGWPHB.js.map +1 -0
  58. package/dist/ui-HOVOPWMX.js +4 -0
  59. package/dist/ui-HOVOPWMX.js.map +1 -0
  60. package/package.json +1 -1
  61. package/dist/baseline-37G4TINM.js +0 -6
  62. package/dist/chunk-5PLKADD6.js.map +0 -1
  63. package/dist/chunk-ELUFCQDR.js.map +0 -1
  64. package/dist/chunk-IKJBELUV.js.map +0 -1
  65. package/dist/chunk-JLRF5TFA.js.map +0 -1
package/DOCS.md CHANGED
@@ -15,6 +15,7 @@ For a quick overview, see the [README](./README.md). This document covers everyt
15
15
  - [vg bisect](#vg-bisect)
16
16
  - [vg drift](#vg-drift)
17
17
  - [vg dsn create](#vg-dsn-create)
18
+ - [vg evidence](#vg-evidence)
18
19
  - [vg fix](#vg-fix)
19
20
  - [vg init](#vg-init)
20
21
  - [vg login](#vg-login)
@@ -243,6 +244,55 @@ with the Vibgrate API. Rate limited to 1 new DSN per 5 minutes per IP address.
243
244
 
244
245
  ---
245
246
 
247
+ ### vg evidence
248
+
249
+ Vibgrate Evidence — signed, reproducible regulatory evidence. Register products with digital elements, freeze a shipped release into an immutable component manifest, then answer "which shipped products contain this vulnerability, at which versions, in which markets, still in support?" as evidence a third party can verify offline.
250
+
251
+ Reporting duties are modelled as **regimes** (jurisdiction-neutral): the EU Cyber Resilience Act (`--regime cra`) is the first; DORA incident reporting (`--regime dora-incident`) ships too. A new jurisdiction is a regime profile, not a new command.
252
+
253
+ ```bash
254
+ vg evidence init [--regime <id>] [--coordinator <csirt>] [--responsible <name>] [--filing-authority] [--ooo <contact>]
255
+ vg evidence regimes
256
+ vg evidence product add <name> [--markets DE,FR] [--classification <id>] [--in-scope] [--rationale <text>] [--bind <ref>] [--until <date>]
257
+ vg evidence product list
258
+ vg evidence product show <id>
259
+ vg evidence release <product> <version> --from <sbom-or-scan> [--ship-date <date>] [--build-id <id>] [--digest <sha256>] [--markets DE,FR]
260
+ vg evidence exposure <vuln> [--regime <id>] [--advisory <file>] [--offline] [--as-of <date>] [--products <substr>] [--include-eol] [--format table|json] [--pack --stage <stage>] [--bundle <dir>] [--tsa <url>]
261
+ vg evidence readiness [--regime <id>] [--format table|json]
262
+ vg evidence support-period <product> [--from <date>] [--until <date>]
263
+ vg evidence pack <vuln> [--regime <id>] [--stage <stage>] [--advisory <file>] [--offline] [--out <file>]
264
+ vg evidence drill [--regime <id>] [--scenario <name>] [--elapsed <seconds>]
265
+ vg evidence watch [--regime <id>] [--since <date>] [--webhook <url>] [--format table|json]
266
+ vg evidence verify <bundle> [--pub <file>]
267
+ vg evidence push [--result <bundle-or-file>] [--regime <id>] [--dsn <dsn>] [--signed]
268
+ vg evidence export [--out <dir>] [--regime <id>]
269
+ ```
270
+
271
+ | Command | Description |
272
+ |---------|-------------|
273
+ | `vg evidence init` | Set org, coordinator CSIRT, and the person with filing authority |
274
+ | `vg evidence regimes` | List available reporting regimes and their clocks |
275
+ | `vg evidence product add` | Register a product with digital elements (PDE) |
276
+ | `vg evidence release` | Freeze a shipped release into an immutable component manifest |
277
+ | `vg evidence exposure` | Which shipped products contain a vulnerability — with signed evidence |
278
+ | `vg evidence readiness` | Deterministic gap report against the regime's obligations |
279
+ | `vg evidence drill` | Timed dry-run of the determination against a simulated advisory |
280
+ | `vg evidence pack` | Build the submission pack a human pastes into the reporting platform |
281
+ | `vg evidence watch` | Check CISA KEV for new exposure against your shipped components |
282
+ | `vg evidence verify` | Verify an evidence bundle offline — no account, no network |
283
+ | `vg evidence push` | Push the product registry (and an optional exposure result) to Vibgrate Cloud |
284
+ | `vg evidence export` | Air-gap bundle of all evidence state |
285
+
286
+ `exposure` matches against manifests **frozen at ship time**, not the current tree, and never guesses: a bound product with no frozen manifest returns `undetermined` with a reason. It runs fully `--offline` against a local advisory file, and can emit a signed evidence bundle (`--bundle <dir>`) that `vg evidence verify` checks offline with honest `verified` / `unverified` / `failed` states. Pass `--tsa <url>` to anchor the bundle to a trusted **RFC 3161** timestamp (`timestamp.tsr`), fully verifiable with `openssl ts -verify`.
287
+
288
+ `watch` joins the CISA **Known Exploited Vulnerabilities (KEV)** catalog to the components in your frozen manifests (via OSV) and reports any KEV-listed vulnerability that affects a shipped release — alerting via stdout or `--webhook`. It **surfaces the KEV listing**; whether a vulnerability is "actively exploited" for a filing is your determination, not the tool's.
289
+
290
+ **Exit codes** (CI-usable): `0` no exposure · `2` exposure found · `3` undetermined (manual review) · `1` operational error.
291
+
292
+ No language model touches any figure in the evidence path, and every determination carries an evidence-not-compliance disclaimer. Vibgrate Evidence produces evidence to support your obligations under a regime; it does not determine compliance and is not legal advice.
293
+
294
+ ---
295
+
246
296
  ### vg fix
247
297
 
248
298
  Turn a drift scan into ranked, risk-tiered upgrade plans and **apply** the one
@@ -819,6 +869,141 @@ vg models
819
869
 
820
870
  Lists the local inference backends and models Vibgrate can see, so you know what is available without any network calls. Add `--json` for machine-readable output.
821
871
 
872
+ To fetch a model through your Ollama runtime, use the `pull` subcommand. **No model is ever downloaded by default** — the download only happens when you pass `--yes`; without it, `pull` just prints the plan.
873
+
874
+ ```bash
875
+ vg models pull qwen2.5-coder:7b --yes
876
+ ```
877
+
878
+ | Flag | Default | Description |
879
+ |------|---------|-------------|
880
+ | `<name>` | — | Model to pull, e.g. `qwen2.5-coder:7b` |
881
+ | `--runtime <id>` | `ollama` | Runtime to pull with |
882
+ | `--yes` | — | Actually download (without this it only prints the plan) |
883
+
884
+ ---
885
+
886
+ ### vg code
887
+
888
+ Propose a code edit for a plain-language instruction, grounded in the deterministic code graph. `vg code` is **dry-run by default**: it prints the proposed diff and writes nothing.
889
+
890
+ ```bash
891
+ vg code "add a --timeout flag to the scan command"
892
+ ```
893
+
894
+ **Agentic sessions.** With a real model, `vg code` is a coding *agent*, not just a one-shot editor: the model is given tools and works in steps — **search the code graph**, read files, check a symbol's blast radius, edit, create/delete files, and run your tests or build — until the task is done. Every mutating step (an edit or a command) is **governed**: you approve it, or run autonomously with `--auto`. Read-only steps (search/read/list/impact) run without prompting. `--single` forces the old one-shot diff; `--max-steps <n>` caps the loop.
895
+
896
+ **Guided mode.** Run `vg code` with no instruction at an interactive terminal and it walks you through everything: it builds the code map, then asks where the model should run — a local model, or one of the current top providers (Claude, GPT, Grok, Gemini, …) surfaced live from the catalog — and which model, with an "enter a slug myself" option at every step. Before pulling any local model it runs a memory pre-flight (estimated footprint vs free RAM/VRAM and already-loaded models) and won't pull a model your machine can't run; then it drops into an agent session where you describe tasks and approve each change. For scripts and CI, pass an instruction with `--auto` (or `--mock`) — the agent only prompts at a TTY, so automation never blocks.
897
+
898
+ While a session is active, Vibgrate Graph (`vg serve`) runs as a separate process for the life of the session and is stopped when you exit. Every graph-backed call is attributed to VG Code and the model in use, so `vg savings` reports token/$ savings **per model**.
899
+
900
+ **A full local session (no cloud):**
901
+
902
+ ```bash
903
+ # one-time: a local coding model (nothing is downloaded by default)
904
+ vg models pull qwen2.5-coder:7b --yes
905
+
906
+ # start a guided agent session — pick "Local model" → qwen2.5-coder:7b
907
+ vg code
908
+ ```
909
+
910
+ ```text
911
+ VG Code · graph-grounded coding · v2026.x
912
+
913
+ ✔ Code map built
914
+ ✔ Model catalog loaded
915
+ ? Where should the model run?
916
+ ❯ Local model (on your machine) 1 found
917
+ Anthropic (Claude) via OpenRouter
918
+
919
+ ✔ qwen2.5-coder:7b
920
+ ◆ Ready — ollama/qwen2.5-coder:7b · graph 48213. Describe a task; empty line to exit.
921
+
922
+ code › add a --timeout flag to the scan command and use it
923
+
924
+ → search_code(query: --timeout flag scan command)
925
+ scanCommand (function) src/commands/scan.ts:12
926
+ → read_file(path: src/commands/scan.ts)
927
+ src/commands/scan.ts (40 lines): …
928
+ → graph_impact(symbol: runScan)
929
+ 3 symbol(s) depend on runScan: …
930
+ I'll add the option and thread it through.
931
+ → edit_file(path: src/commands/scan.ts, …)
932
+ --- a/src/commands/scan.ts
933
+ +++ b/src/commands/scan.ts
934
+ @@ …
935
+ + .option('--timeout <ms>', 'abort the scan after <ms> milliseconds')
936
+ ? Apply edit to src/commands/scan.ts? [Y/n] y
937
+ ✔ edited src/commands/scan.ts
938
+ → run_command(command: npm test -- scan)
939
+ ? Run `npm test -- scan`? [y/N] y
940
+ ✔ exit 0 … 12 passing
941
+ → finish(summary: added a --timeout flag to scan and covered it with tests)
942
+
943
+ ✔ added a --timeout flag to scan and covered it with tests
944
+ +6 -1 across 1 file(s) · via ollama/qwen2.5-coder:7b
945
+ ```
946
+
947
+ Run it non-interactively with `vg code "add a --timeout flag to scan" --provider ollama --model qwen2.5-coder:7b --auto`, or against a hosted model with `--provider openrouter --model anthropic/claude-3.5-sonnet` (set `OPENROUTER_API_KEY`).
948
+
949
+ **In a session** you can type slash-commands: `/undo` reverts the last change, `/diff` shows it, `/model` switches model, `/cost` shows the running token/$ cost, `/help` lists them, `/exit` quits.
950
+
951
+ **More session controls:**
952
+
953
+ - `--stream` streams the model's output live as it's generated.
954
+ - `--verify [command]` runs your tests after the agent finishes and, if they fail, feeds the failures back so it fixes them (uses the `testCommand` from config if you don't name one).
955
+ - `--continue` resumes your most recent session — it recaps what was already done for the model and restores `/undo`.
956
+ - A live **token/$ meter** shows after each task and via `/cost` (cost is shown when the model's price is known; local models are free).
957
+ - **External MCP tools:** list servers under `mcpServers` in `.vibgrate/code.json` and the agent can call their tools (namespaced `mcp__<server>__<tool>`); read-only tools run freely, anything else is approved like a built-in mutating tool. VG Code also **adopts the standard MCP config files** already in your repo — `.mcp.json` (Claude Code), `.cursor/mcp.json` (Cursor), and `.vscode/mcp.json` (VS Code) — and merges them with your `.vibgrate/code.json` (which wins on any name clash), so servers you've already configured for another tool work here with no extra setup. Both local (`command`) and remote (`url`) servers are supported.
958
+
959
+ **Tools the agent has:** searching is the code graph (`search_code`) — not a grep — plus `read_file`, `list_files`, `graph_impact` (blast radius), **`library_docs`** (version-correct docs for a dependency you actually have installed, so the model uses the right API for your version), `edit_file`, `create_file`, `delete_file`, and `run_command`.
960
+
961
+ **Safety.** The agent never sends a secrets file (`.env`, keys, credentials) to the model, and redacts stray credential shapes from any file it reads. Under `--auto`, a denylist blocks catastrophic commands (filesystem wipes, `curl … | sh`, force-push, …); interactively you see and approve each command yourself.
962
+
963
+ **Configure once** in `.vibgrate/code.json` so you can then just run `vg code` (flags still override):
964
+
965
+ ```json
966
+ {
967
+ "provider": "ollama",
968
+ "model": "qwen2.5-coder:7b",
969
+ "testCommand": "npm test",
970
+ "auto": false,
971
+ "denyCommands": ["deploy", "kubectl\\s+delete"],
972
+ "maxSteps": 24,
973
+ "mcpServers": {
974
+ "playwright": { "command": "npx", "args": ["-y", "@playwright/mcp"] }
975
+ }
976
+ }
977
+ ```
978
+
979
+ It assembles a small, high-signal context from the map (the relevant symbols, their relations, the blast radius of changing them, and any hard constraints), asks the model you choose for a minimal edit, and applies that edit through a deterministic merge so the change lands exactly where it was meant to.
980
+
981
+ Writing is opt-in and confirmed. `--apply` walks the full inspect → assess → dry-run → approve → execute → verify → log lifecycle, and still requires your explicit `--yes` (or an interactive confirmation) — there is no write-without-consent path.
982
+
983
+ ```bash
984
+ vg code "rename readCfg to readConfig everywhere it is called" --apply --yes
985
+ ```
986
+
987
+ Pick a backend with `--provider` and `--model`. No model is bundled, and nothing is installed until you first use a backend that needs it:
988
+
989
+ - **Local** — `--provider ollama` or `--provider lmstudio` (or `--local` to force on-device only, no network).
990
+ - **Hosted** — any OpenAI-compatible endpoint: `--provider openrouter` / `litellm` / `openai` / `together`. API keys are read from the environment only (e.g. `OPENROUTER_API_KEY`), never passed as flags.
991
+
992
+ With no `--provider`, `vg code` chooses from what you have already configured (a hosted key, or a locally-pulled model) and never dials a cloud endpoint you didn't set up.
993
+
994
+ | Flag | Default | Description |
995
+ |------|---------|-------------|
996
+ | `<instruction>` | — | What to change, in plain language |
997
+ | `--provider <id>` | auto | `ollama`, `lmstudio`, `openrouter`, `litellm`, `openai`, `together`, `llama-cpp` |
998
+ | `--model <id>` | — | Model id (or set `VG_CODE_MODEL`) |
999
+ | `--file <path>` | — | Restrict the edit surface to this file (repeatable) |
1000
+ | `--budget <n>` | `3000` | Approx context token budget |
1001
+ | `--apply` | — | Write the change (still requires `--yes` or a confirmation) |
1002
+ | `--yes` | — | Consent to write, or to a first-use package install, non-interactively |
1003
+ | `--local` | — | On-device backends only; never touch the network |
1004
+
1005
+ Add `--json` for the full machine-readable result (proposed changes, diffs, and the verification summary), or `--out <file>` to write it for CI. Requires a map — run `vg` first if you have not built one.
1006
+
822
1007
  ---
823
1008
 
824
1009
  ### vg path
package/README.md CHANGED
@@ -301,6 +301,7 @@ See [docs/QUICKSTART-PROMPT.md](./docs/QUICKSTART-PROMPT.md) for the full prompt
301
301
  | `vg benchmark` | Reproducible build + memory + token-reduction benchmark (honest estimates) |
302
302
  | `vg build [path]` | Build / update the code map (incremental, deterministic) |
303
303
  | `vg bundle` | Build an air-gapped bundle (grammars + graph + library catalog) |
304
+ | `vg code "<instruction>"` | Propose a graph-grounded code edit (dry-run by default; `--apply --yes` to write) |
304
305
  | `vg embed` | Precompute the semantic index for instant `vg ask` |
305
306
  | `vg export` | Export the map (json / ndjson / graphml / dot / cypher / md / html / SBOM) |
306
307
  | `vg facts <file>` | Deterministic facts for a node (contracts, invariants) |
@@ -309,7 +310,7 @@ See [docs/QUICKSTART-PROMPT.md](./docs/QUICKSTART-PROMPT.md) for the full prompt
309
310
  | `vg install` / `vg uninstall` | Wire (or remove) **Vibgrate AI Context** + skill in your AI assistant |
310
311
  | `vg lib <package>` | Version-correct, drift-annotated library docs |
311
312
  | `vg map` / `vg hubs` / `vg areas` / `vg oddities` | Map insights: overview, most-depended-on code, natural groupings, cross-area smells |
312
- | `vg models` | The local model fleet (Ollama / LM Studio / gguf), discovered offline |
313
+ | `vg models` / `vg models pull` | The local model fleet (Ollama / LM Studio / gguf), discovered offline; `pull` fetches one (needs `--yes`) |
313
314
  | `vg path <from> <to>` | How A connects to B (shortest path) |
314
315
  | `vg savings` | Local report of tokens/$ saved vs a grep baseline (estimates) |
315
316
  | `vg serve` | Start **Vibgrate AI Context** (local-first MCP: code map + drift + version-correct docs) |
@@ -0,0 +1,7 @@
1
+ export { baselineCommand, runBaseline } from './chunk-NONYJLOJ.js';
2
+ import './chunk-5BDHQ7DD.js';
3
+ import './chunk-RXP66R2E.js';
4
+ import './chunk-GI6W53LM.js';
5
+ import './chunk-CRAMOEBE.js';
6
+ //# sourceMappingURL=baseline-BD3A7EXD.js.map
7
+ //# sourceMappingURL=baseline-BD3A7EXD.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":[],"names":[],"mappings":"","file":"baseline-37G4TINM.js"}
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"baseline-BD3A7EXD.js"}
@@ -0,0 +1,417 @@
1
+ import { refreshIfStale, TOOLS, renderToolResult, warmEmbedderInBackground, countTokens, renderHtml, renderReport } from './chunk-3MXNNBLI.js';
2
+ import { VERSION } from './chunk-5BDHQ7DD.js';
3
+ import { parseGraph, sanitizeClient, recordSaving, SAVINGS_TOOLS, PER_FILE_TOKENS, serializeGraph } from './chunk-WNIIKCNF.js';
4
+ import * as fs from 'fs';
5
+ import * as path from 'path';
6
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
7
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
8
+ import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
9
+
10
+ var PROBE_INTERVAL_MS = 2e3;
11
+ var MAX_PROBE_INTERVAL_MS = 3e4;
12
+ var PROBE_DUTY_FACTOR = 20;
13
+ var REFRESH_BUDGET_MS = 5e3;
14
+ var FAILURE_COOLDOWN_MS = 6e4;
15
+ var GraphSource = class {
16
+ constructor(graphPath, refresh = false, tuning = {}) {
17
+ this.graphPath = graphPath;
18
+ this.refresh = refresh;
19
+ this.tuning = tuning;
20
+ this.root = path.dirname(path.dirname(graphPath));
21
+ }
22
+ graphPath;
23
+ refresh;
24
+ tuning;
25
+ cachedMtimeMs = -1;
26
+ cached = null;
27
+ root;
28
+ lastProbeAt = 0;
29
+ failedUntil = 0;
30
+ inflight = null;
31
+ /** Self-tuned: grows with measured probe cost so huge repos aren't penalized. */
32
+ probeIntervalMs = PROBE_INTERVAL_MS;
33
+ /** Current graph: auto-refreshed if the tree drifted, reloaded if the file changed. */
34
+ async get() {
35
+ if (this.refresh) await this.maybeRefresh();
36
+ const stat = fs.statSync(this.graphPath);
37
+ if (stat.mtimeMs !== this.cachedMtimeMs || !this.cached) {
38
+ this.cached = parseGraph(fs.readFileSync(this.graphPath, "utf8"));
39
+ this.cachedMtimeMs = stat.mtimeMs;
40
+ }
41
+ return this.cached;
42
+ }
43
+ /**
44
+ * Debounced, single-flight refresh. Never throws — a refresh problem must
45
+ * degrade to "answer from the current map", not break the tool call.
46
+ */
47
+ async maybeRefresh() {
48
+ const now = Date.now();
49
+ if (!this.inflight) {
50
+ const interval = this.tuning.probeIntervalMs ?? this.probeIntervalMs;
51
+ if (now < this.failedUntil || now - this.lastProbeAt < interval) return;
52
+ this.lastProbeAt = now;
53
+ this.inflight = refreshIfStale(this.root).then((r) => {
54
+ if (r.status === "error") this.failedUntil = Date.now() + FAILURE_COOLDOWN_MS;
55
+ if (r.status === "fresh" || r.status === "no-snapshot" || r.status === "locked") {
56
+ const cost = Date.now() - now;
57
+ this.probeIntervalMs = Math.min(
58
+ MAX_PROBE_INTERVAL_MS,
59
+ Math.max(PROBE_INTERVAL_MS, cost * PROBE_DUTY_FACTOR)
60
+ );
61
+ }
62
+ }).catch(() => {
63
+ this.failedUntil = Date.now() + FAILURE_COOLDOWN_MS;
64
+ }).finally(() => {
65
+ this.inflight = null;
66
+ });
67
+ }
68
+ await Promise.race([this.inflight, sleep(this.tuning.refreshBudgetMs ?? REFRESH_BUDGET_MS)]);
69
+ }
70
+ };
71
+ function createServer(source, opts = {}) {
72
+ const { savings = false, shareStats = false, local = false, dedup = false, stats } = opts;
73
+ const record = savings || shareStats;
74
+ const root = path.dirname(path.dirname(source.graphPath));
75
+ const seen = /* @__PURE__ */ new Set();
76
+ const server = new Server(
77
+ { name: "vg", version: VERSION },
78
+ {
79
+ capabilities: { tools: {} },
80
+ // Routing guidance once at the server level (hosts that surface
81
+ // `instructions` get it at zero per-step schema cost): the flashlight
82
+ // vs the map.
83
+ 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. For how-do-I-use-this-library questions call resolve_library once, then library_docs with the returned targetId and a focused query: the docs are official and matched to the version THIS project has installed (drift-annotated) \u2014 prefer them over web search or training-data recall when they conflict. Skip them for language built-ins or APIs already shown in context. If two library_docs calls have not surfaced the section you need, read the package source under node_modules instead of searching again.'
84
+ }
85
+ );
86
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
87
+ tools: TOOLS.map((t) => ({
88
+ name: t.name,
89
+ description: t.description,
90
+ inputSchema: t.inputSchema,
91
+ annotations: { readOnlyHint: true, openWorldHint: false }
92
+ }))
93
+ }));
94
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
95
+ const tool = TOOLS.find((t) => t.name === request.params.name);
96
+ if (!tool) {
97
+ return errorResult(`unknown tool "${request.params.name}"`);
98
+ }
99
+ let graph;
100
+ try {
101
+ graph = await source.get();
102
+ } catch {
103
+ return errorResult(
104
+ "no code map found. Run `vg` in the project to build .vibgrate/graph.json, then retry."
105
+ );
106
+ }
107
+ const startedAt = Date.now();
108
+ try {
109
+ const args = request.params.arguments ?? {};
110
+ const result = await tool.handler(graph, args, { root, local, dedup, seen });
111
+ const ms = Date.now() - startedAt;
112
+ stats?.record({ ...measureCall(tool.name, result), client: sanitizeClient(detectClient(server)), ms });
113
+ if (record) recordUsage(root, tool.name, result, detectClient(server), ms);
114
+ return renderToolResult(result);
115
+ } catch (err) {
116
+ stats?.record({
117
+ tool: tool.name,
118
+ client: sanitizeClient(detectClient(server)),
119
+ outcome: "miss",
120
+ ms: Date.now() - startedAt,
121
+ vgTokens: 0,
122
+ baselineTokens: 0
123
+ });
124
+ return errorResult(`tool "${tool.name}" failed: ${err.message}`);
125
+ }
126
+ });
127
+ return server;
128
+ }
129
+ async function serveStdio(graphPath, opts = {}) {
130
+ const source = new GraphSource(graphPath, opts.refresh !== false);
131
+ const server = createServer(source, opts);
132
+ warmEmbedderInBackground(opts.local);
133
+ await server.connect(new StdioServerTransport());
134
+ }
135
+ function sleep(ms) {
136
+ return new Promise((resolve) => {
137
+ const timer = setTimeout(resolve, ms);
138
+ timer.unref?.();
139
+ });
140
+ }
141
+ function recordUsage(root, tool, result, client, ms) {
142
+ recordSaving(
143
+ root,
144
+ { ...measureCall(tool, result), source: "mcp", client: sanitizeClient(client), ...ms !== void 0 ? { ms } : {} },
145
+ Date.now()
146
+ );
147
+ }
148
+ function measureCall(tool, result) {
149
+ const outcome = classifyOutcome(result);
150
+ let vgTokens = 0;
151
+ let baselineTokens = 0;
152
+ if (SAVINGS_TOOLS.has(tool) && result && typeof result === "object") {
153
+ vgTokens = countTokens(renderedText(renderToolResult(result)));
154
+ baselineTokens = referencedFiles(result).size * PER_FILE_TOKENS;
155
+ }
156
+ return { tool, outcome, vgTokens, baselineTokens };
157
+ }
158
+ function detectClient(server) {
159
+ try {
160
+ const info = server.getClientVersion?.();
161
+ return typeof info?.name === "string" ? info.name : void 0;
162
+ } catch {
163
+ return void 0;
164
+ }
165
+ }
166
+ function classifyOutcome(result) {
167
+ if (Array.isArray(result)) return result.length === 0 ? "miss" : "complete";
168
+ if (!result || typeof result !== "object") return "miss";
169
+ const r = result;
170
+ if (typeof r.error === "string" && r.error) return "miss";
171
+ if (r.connected === false) return "miss";
172
+ if (Array.isArray(r.matches) && r.matches.length === 0) return "miss";
173
+ return isPartial(r) ? "partial" : "complete";
174
+ }
175
+ function isPartial(r) {
176
+ if (r.moreAvailable === true) return true;
177
+ if (r._truncated && typeof r._truncated === "object") return true;
178
+ for (const [key, value] of Object.entries(r)) {
179
+ if (typeof value !== "number" || !key.endsWith("Total")) continue;
180
+ const base = key.slice(0, -"Total".length);
181
+ const shown = Array.isArray(r[base]) ? r[base].length : 0;
182
+ if (value > shown) return true;
183
+ }
184
+ return false;
185
+ }
186
+ function renderedText(rendered) {
187
+ const block = rendered.content?.find((b) => b.type === "text");
188
+ return block && "text" in block && typeof block.text === "string" ? block.text : "";
189
+ }
190
+ function referencedFiles(result) {
191
+ const r = result;
192
+ const files = /* @__PURE__ */ new Set();
193
+ if (typeof r.file === "string" && r.file) files.add(r.file);
194
+ for (const m of asArray(r.matches)) {
195
+ const f = m.file;
196
+ if (typeof f === "string" && f) files.add(f);
197
+ }
198
+ for (const name of [...asArray(r.calls), ...asArray(r.calledBy)]) {
199
+ if (typeof name !== "string") continue;
200
+ const i = name.indexOf(":");
201
+ if (i > 0) files.add(name.slice(0, i));
202
+ }
203
+ return files;
204
+ }
205
+ function asArray(v) {
206
+ return Array.isArray(v) ? v : [];
207
+ }
208
+ function errorResult(message) {
209
+ return { content: [{ type: "text", text: message }], isError: true };
210
+ }
211
+
212
+ // src/engine/export.ts
213
+ function formatForExt(ext) {
214
+ switch (ext.toLowerCase()) {
215
+ case ".json":
216
+ return "json";
217
+ case ".ndjson":
218
+ return "ndjson";
219
+ case ".graphml":
220
+ return "graphml";
221
+ case ".dot":
222
+ case ".gv":
223
+ return "dot";
224
+ case ".cypher":
225
+ return "cypher";
226
+ case ".sql":
227
+ return "sql";
228
+ case ".md":
229
+ return "md";
230
+ case ".html":
231
+ return "html";
232
+ default:
233
+ return null;
234
+ }
235
+ }
236
+ function exportGraph(format, ctx) {
237
+ switch (format) {
238
+ case "json":
239
+ return serializeGraph(ctx.graph);
240
+ case "md":
241
+ return renderReport(ctx.graph);
242
+ case "html":
243
+ return renderHtml(ctx.graph);
244
+ case "ndjson":
245
+ return ndjson(ctx.graph);
246
+ case "graphml":
247
+ return graphml(ctx.graph);
248
+ case "dot":
249
+ return dot(ctx.graph);
250
+ case "cypher":
251
+ return cypher(ctx.graph);
252
+ case "sql":
253
+ return sql(ctx);
254
+ case "cyclonedx":
255
+ return cyclonedx(ctx);
256
+ case "spdx":
257
+ return spdx(ctx);
258
+ }
259
+ }
260
+ function ndjson(graph) {
261
+ const rows = graph.facts && graph.facts.length ? graph.facts : graph.nodes;
262
+ return rows.map((r) => JSON.stringify(r)).join("\n") + "\n";
263
+ }
264
+ function esc(s) {
265
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
266
+ }
267
+ function graphml(graph) {
268
+ const lines = [];
269
+ lines.push('<?xml version="1.0" encoding="UTF-8"?>');
270
+ lines.push('<graphml xmlns="http://graphml.graphdrawing.org/xmlns">');
271
+ lines.push(' <key id="name" for="node" attr.name="name" attr.type="string"/>');
272
+ lines.push(' <key id="kind" for="node" attr.name="kind" attr.type="string"/>');
273
+ lines.push(' <key id="file" for="node" attr.name="file" attr.type="string"/>');
274
+ lines.push(' <key id="ekind" for="edge" attr.name="kind" attr.type="string"/>');
275
+ lines.push(' <graph edgedefault="directed">');
276
+ for (const n of graph.nodes) {
277
+ lines.push(` <node id="${esc(n.id)}"><data key="name">${esc(n.qualifiedName)}</data><data key="kind">${esc(n.kind)}</data><data key="file">${esc(n.file)}</data></node>`);
278
+ }
279
+ for (const e of graph.edges) {
280
+ lines.push(` <edge source="${esc(e.src)}" target="${esc(e.dst)}"><data key="ekind">${esc(e.kind)}</data></edge>`);
281
+ }
282
+ lines.push(" </graph>");
283
+ lines.push("</graphml>");
284
+ return lines.join("\n") + "\n";
285
+ }
286
+ function dot(graph) {
287
+ const lines = ["digraph vg {"];
288
+ for (const n of graph.nodes) lines.push(` "${n.id}" [label="${dotEsc(n.qualifiedName)}"];`);
289
+ for (const e of graph.edges) lines.push(` "${e.src}" -> "${e.dst}" [label="${e.kind}"];`);
290
+ lines.push("}");
291
+ return lines.join("\n") + "\n";
292
+ }
293
+ function dotEsc(s) {
294
+ return s.replace(/"/g, '\\"');
295
+ }
296
+ function cypher(graph) {
297
+ const lines = [];
298
+ for (const n of graph.nodes) {
299
+ lines.push(
300
+ `CREATE (:\`${cypherLabel(n.kind)}\` {id:${q(n.id)}, name:${q(n.qualifiedName)}, file:${q(n.file)}});`
301
+ );
302
+ }
303
+ for (const e of graph.edges) {
304
+ lines.push(
305
+ `MATCH (a {id:${q(e.src)}}),(b {id:${q(e.dst)}}) CREATE (a)-[:\`${cypherLabel(e.kind)}\`]->(b);`
306
+ );
307
+ }
308
+ return lines.join("\n") + "\n";
309
+ }
310
+ function cypherLabel(s) {
311
+ return s.replace(/[^A-Za-z0-9_]/g, "_");
312
+ }
313
+ function q(s) {
314
+ return JSON.stringify(s);
315
+ }
316
+ function sql(ctx) {
317
+ const graph = ctx.graph;
318
+ const lines = [];
319
+ lines.push("BEGIN;");
320
+ lines.push("CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT);");
321
+ const meta = [
322
+ ["schemaVersion", graph.schemaVersion],
323
+ ["generatedAt", ctx.generatedAt],
324
+ ["corpusHash", graph.provenance.corpusHash],
325
+ ["tool", graph.provenance.tool],
326
+ ["toolVersion", graph.provenance.version],
327
+ ["resolver", graph.provenance.resolver.join(",")],
328
+ ["fingerprint", graph.provenance.toolchain?.fingerprint ?? null]
329
+ ];
330
+ for (const [k, v] of meta) lines.push(`INSERT INTO meta VALUES (${sqlStr(k)}, ${sqlStr(v)});`);
331
+ lines.push(
332
+ "CREATE TABLE IF NOT EXISTS nodes (id TEXT PRIMARY KEY, kind TEXT, name TEXT, qualified_name TEXT, file TEXT, span_start INTEGER, span_end INTEGER, lang TEXT, visibility TEXT, signature TEXT, importance REAL, area INTEGER, is_hub INTEGER, tested INTEGER, coverage REAL);"
333
+ );
334
+ for (const n of graph.nodes) {
335
+ lines.push(
336
+ `INSERT INTO nodes VALUES (${sqlStr(n.id)}, ${sqlStr(n.kind)}, ${sqlStr(n.name)}, ${sqlStr(n.qualifiedName)}, ${sqlStr(n.file)}, ${sqlNum(n.span.start)}, ${sqlNum(n.span.end)}, ${sqlStr(n.lang)}, ${sqlStr(n.visibility)}, ${sqlStr(n.signature)}, ${sqlNum(n.importance)}, ${sqlNum(n.area)}, ${sqlBool(n.isHub)}, ${sqlBool(n.tested)}, ${sqlNum(n.coverage)});`
337
+ );
338
+ }
339
+ lines.push(
340
+ 'CREATE TABLE IF NOT EXISTS edges (id TEXT PRIMARY KEY, kind TEXT, src TEXT, dst TEXT, resolution TEXT, confidence REAL, epistemic TEXT, surprise REAL, "count" INTEGER);'
341
+ );
342
+ for (const e of graph.edges) {
343
+ lines.push(
344
+ `INSERT INTO edges VALUES (${sqlStr(e.id)}, ${sqlStr(e.kind)}, ${sqlStr(e.src)}, ${sqlStr(e.dst)}, ${sqlStr(e.resolution)}, ${sqlNum(e.confidence)}, ${sqlStr(e.epistemic)}, ${sqlNum(e.surprise)}, ${sqlNum(e.count)});`
345
+ );
346
+ }
347
+ lines.push("CREATE TABLE IF NOT EXISTS areas (id INTEGER PRIMARY KEY, label TEXT, size INTEGER, cohesion REAL, external_edges INTEGER);");
348
+ lines.push("CREATE TABLE IF NOT EXISTS area_members (area_id INTEGER, node_id TEXT);");
349
+ for (const a of graph.areas) {
350
+ lines.push(`INSERT INTO areas VALUES (${sqlNum(a.id)}, ${sqlStr(a.label)}, ${sqlNum(a.size)}, ${sqlNum(a.cohesion)}, ${sqlNum(a.externalEdges)});`);
351
+ for (const m of a.members) lines.push(`INSERT INTO area_members VALUES (${sqlNum(a.id)}, ${sqlStr(m)});`);
352
+ }
353
+ if (graph.facts && graph.facts.length) {
354
+ lines.push("CREATE TABLE IF NOT EXISTS facts (id TEXT PRIMARY KEY, kind TEXT, predicate_json TEXT, derived_by TEXT, confidence TEXT);");
355
+ lines.push("CREATE TABLE IF NOT EXISTS fact_subjects (fact_id TEXT, node_id TEXT);");
356
+ lines.push("CREATE TABLE IF NOT EXISTS fact_evidence (fact_id TEXT, file TEXT, span_start INTEGER, span_end INTEGER);");
357
+ for (const f of graph.facts) {
358
+ lines.push(`INSERT INTO facts VALUES (${sqlStr(f.id)}, ${sqlStr(f.kind)}, ${sqlStr(JSON.stringify(f.predicate))}, ${sqlStr(f.derivedBy)}, ${sqlStr(f.confidence)});`);
359
+ for (const s of f.subjectIds) lines.push(`INSERT INTO fact_subjects VALUES (${sqlStr(f.id)}, ${sqlStr(s)});`);
360
+ for (const ev of f.evidence) lines.push(`INSERT INTO fact_evidence VALUES (${sqlStr(f.id)}, ${sqlStr(ev.file)}, ${sqlNum(ev.span.start)}, ${sqlNum(ev.span.end)});`);
361
+ }
362
+ }
363
+ lines.push("COMMIT;");
364
+ return lines.join("\n") + "\n";
365
+ }
366
+ function sqlStr(v) {
367
+ if (v == null) return "NULL";
368
+ return `'${v.replace(/'/g, "''")}'`;
369
+ }
370
+ function sqlNum(v) {
371
+ return v == null || !Number.isFinite(v) ? "NULL" : String(v);
372
+ }
373
+ function sqlBool(v) {
374
+ return v == null ? "NULL" : v ? "1" : "0";
375
+ }
376
+ function cyclonedx(ctx) {
377
+ const components = [];
378
+ for (const d of ctx.deps ?? []) {
379
+ components.push({
380
+ type: "library",
381
+ name: d.name,
382
+ version: d.installed ?? d.declared,
383
+ purl: d.ecosystem === "npm" ? `pkg:npm/${d.name}@${d.installed ?? ""}` : void 0
384
+ });
385
+ }
386
+ for (const m of ctx.models ?? []) {
387
+ components.push({ type: "machine-learning-model", name: m.name, properties: [{ name: "vg:runtime", value: m.runtime }] });
388
+ }
389
+ const bom = {
390
+ bomFormat: "CycloneDX",
391
+ specVersion: "1.6",
392
+ metadata: { timestamp: ctx.generatedAt, tools: [{ name: "vg", version: ctx.graph.provenance.version }] },
393
+ components
394
+ };
395
+ return JSON.stringify(bom, null, 2) + "\n";
396
+ }
397
+ function spdx(ctx) {
398
+ const packages = (ctx.deps ?? []).map((d) => ({
399
+ SPDXID: `SPDXRef-Package-${cypherLabel(d.name)}`,
400
+ name: d.name,
401
+ versionInfo: d.installed ?? d.declared,
402
+ downloadLocation: "NOASSERTION"
403
+ }));
404
+ const doc = {
405
+ spdxVersion: "SPDX-2.3",
406
+ dataLicense: "CC0-1.0",
407
+ SPDXID: "SPDXRef-DOCUMENT",
408
+ name: "vg-sbom",
409
+ creationInfo: { created: ctx.generatedAt, creators: [`Tool: vg-${ctx.graph.provenance.version}`] },
410
+ packages
411
+ };
412
+ return JSON.stringify(doc, null, 2) + "\n";
413
+ }
414
+
415
+ export { GraphSource, createServer, exportGraph, formatForExt, serveStdio };
416
+ //# sourceMappingURL=chunk-2O5YZVZV.js.map
417
+ //# sourceMappingURL=chunk-2O5YZVZV.js.map