@vibgrate/cli 2026.728.3 → 2026.728.5

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 (131) hide show
  1. package/dist/baseline-Y5FBYWNJ.js +8 -0
  2. package/dist/{baseline-PPZJBYAJ.js.map → baseline-Y5FBYWNJ.js.map} +1 -1
  3. package/dist/{chunk-A2IYFNCQ.js → chunk-2ZHWF7C6.js} +227 -19
  4. package/dist/chunk-2ZHWF7C6.js.map +1 -0
  5. package/dist/{chunk-UZJS7GMK.js → chunk-3LSG7Z4I.js} +10 -6
  6. package/dist/chunk-3LSG7Z4I.js.map +1 -0
  7. package/dist/{chunk-3BIRGSRS.js → chunk-4JD63RUR.js} +11 -11
  8. package/dist/{chunk-3BIRGSRS.js.map → chunk-4JD63RUR.js.map} +1 -1
  9. package/dist/{chunk-6GRLZ4Z2.js → chunk-643KXZ54.js} +8 -8
  10. package/dist/{chunk-6GRLZ4Z2.js.map → chunk-643KXZ54.js.map} +1 -1
  11. package/dist/{chunk-R2QNFXE5.js → chunk-DBOT2ELH.js} +3 -3
  12. package/dist/{chunk-R2QNFXE5.js.map → chunk-DBOT2ELH.js.map} +1 -1
  13. package/dist/{chunk-6UK6QPTR.js → chunk-EETK7S43.js} +137 -34
  14. package/dist/chunk-EETK7S43.js.map +1 -0
  15. package/dist/{chunk-JRAGO6OB.js → chunk-EVWW3BPM.js} +4 -4
  16. package/dist/{chunk-JRAGO6OB.js.map → chunk-EVWW3BPM.js.map} +1 -1
  17. package/dist/chunk-G4HTI4ZM.js +1537 -0
  18. package/dist/chunk-G4HTI4ZM.js.map +1 -0
  19. package/dist/chunk-IPICO2RV.js +789 -0
  20. package/dist/chunk-IPICO2RV.js.map +1 -0
  21. package/dist/{chunk-VI4ITXHQ.js → chunk-NH5NJ3UO.js} +7 -6
  22. package/dist/chunk-NH5NJ3UO.js.map +1 -0
  23. package/dist/{chunk-6UQMXIAG.js → chunk-OWSFZQOP.js} +62 -12
  24. package/dist/chunk-OWSFZQOP.js.map +1 -0
  25. package/dist/{chunk-WH2N22S3.js → chunk-PRGCCW7D.js} +4 -4
  26. package/dist/{chunk-WH2N22S3.js.map → chunk-PRGCCW7D.js.map} +1 -1
  27. package/dist/chunk-RCP2KQGG.js +3059 -0
  28. package/dist/chunk-RCP2KQGG.js.map +1 -0
  29. package/dist/{chunk-OSD546W7.js → chunk-REIGWTTH.js} +5 -3
  30. package/dist/chunk-REIGWTTH.js.map +1 -0
  31. package/dist/{chunk-B3YUO3NW.js → chunk-RYNAK4LK.js} +3 -3
  32. package/dist/{chunk-B3YUO3NW.js.map → chunk-RYNAK4LK.js.map} +1 -1
  33. package/dist/{chunk-YF43FOPP.js → chunk-RZEXHDCF.js} +33 -14
  34. package/dist/chunk-RZEXHDCF.js.map +1 -0
  35. package/dist/{chunk-VYGEOOLY.js → chunk-SGI5ZP2Y.js} +3 -3
  36. package/dist/{chunk-VYGEOOLY.js.map → chunk-SGI5ZP2Y.js.map} +1 -1
  37. package/dist/{chunk-HK3AWUXH.js → chunk-TCJTQGAO.js} +3 -3
  38. package/dist/{chunk-HK3AWUXH.js.map → chunk-TCJTQGAO.js.map} +1 -1
  39. package/dist/{chunk-ZBTY6VV4.js → chunk-US2DAKCM.js} +15 -5
  40. package/dist/chunk-US2DAKCM.js.map +1 -0
  41. package/dist/{chunk-ON25BX6T.js → chunk-UTNNGKKX.js} +3 -3
  42. package/dist/{chunk-ON25BX6T.js.map → chunk-UTNNGKKX.js.map} +1 -1
  43. package/dist/chunk-VBVY733D.js +2184 -0
  44. package/dist/chunk-VBVY733D.js.map +1 -0
  45. package/dist/{chunk-SZSNPXSW.js → chunk-WHJMS7XC.js} +3 -3
  46. package/dist/{chunk-SZSNPXSW.js.map → chunk-WHJMS7XC.js.map} +1 -1
  47. package/dist/chunk-Z6ZWNWWR.js +30 -0
  48. package/dist/{chunk-DGUM43GV.js.map → chunk-Z6ZWNWWR.js.map} +1 -1
  49. package/dist/cli.js +192 -90
  50. package/dist/cli.js.map +1 -1
  51. package/dist/{execution-env-ZU45JZ4G.js → execution-env-4MS5AYAN.js} +3 -3
  52. package/dist/{execution-env-ZU45JZ4G.js.map → execution-env-4MS5AYAN.js.map} +1 -1
  53. package/dist/{federation-SZWZIFZ7.js → federation-MDQPO3PW.js} +3 -3
  54. package/dist/{federation-SZWZIFZ7.js.map → federation-MDQPO3PW.js.map} +1 -1
  55. package/dist/{fs-O5IQYXHO.js → fs-4G5A65KS.js} +3 -3
  56. package/dist/fs-4G5A65KS.js.map +1 -0
  57. package/dist/{fs-B5HZ2MIU.js → fs-6JKZNQ3U.js} +3 -3
  58. package/dist/fs-6JKZNQ3U.js.map +1 -0
  59. package/dist/{git-ref-XDXB6JP5.js → git-ref-RSRAN4OH.js} +3 -3
  60. package/dist/{git-ref-XDXB6JP5.js.map → git-ref-RSRAN4OH.js.map} +1 -1
  61. package/dist/{graph-backend-UTOL3SHE.js → graph-backend-GBL33GMW.js} +5 -5
  62. package/dist/{graph-backend-UTOL3SHE.js.map → graph-backend-GBL33GMW.js.map} +1 -1
  63. package/dist/index.d.ts +3 -2
  64. package/dist/index.js +12 -10
  65. package/dist/index.js.map +1 -1
  66. package/dist/{interactive-OCWQ326U.js → interactive-KVDKRKNK.js} +45 -24
  67. package/dist/interactive-KVDKRKNK.js.map +1 -0
  68. package/dist/{llm-host-XYBT4267.js → llm-host-5RE34322.js} +3 -3
  69. package/dist/{llm-host-XYBT4267.js.map → llm-host-5RE34322.js.map} +1 -1
  70. package/dist/load-SEFJA3M5.js +8 -0
  71. package/dist/{load-QPOWQD7C.js.map → load-SEFJA3M5.js.map} +1 -1
  72. package/dist/local-runtime-2ED33KK6.js +6 -0
  73. package/dist/{local-runtime-5O2NN3GA.js.map → local-runtime-2ED33KK6.js.map} +1 -1
  74. package/dist/mcp-tools-KAJS34TN.js +4 -0
  75. package/dist/{mcp-tools-I7JK3S2M.js.map → mcp-tools-KAJS34TN.js.map} +1 -1
  76. package/dist/{model-execution-profile-TFBKCNJG.js → model-execution-profile-ELWF3Z6H.js} +3 -3
  77. package/dist/{model-execution-profile-TFBKCNJG.js.map → model-execution-profile-ELWF3Z6H.js.map} +1 -1
  78. package/dist/model-manager-GJ2YSTSK.js +10 -0
  79. package/dist/{model-manager-LB54VSV7.js.map → model-manager-GJ2YSTSK.js.map} +1 -1
  80. package/dist/{model-orchestrator-4M5FG6MN.js → model-orchestrator-LHYPJBFH.js} +3 -3
  81. package/dist/{model-orchestrator-4M5FG6MN.js.map → model-orchestrator-LHYPJBFH.js.map} +1 -1
  82. package/dist/parse-worker.js +2 -2
  83. package/dist/{paths-2RAH3GME.js → paths-62AKTFSR.js} +3 -3
  84. package/dist/{paths-2RAH3GME.js.map → paths-62AKTFSR.js.map} +1 -1
  85. package/dist/{resolve-gguf-4AFRN4CP.js → resolve-gguf-SDIYGGI6.js} +3 -3
  86. package/dist/{resolve-gguf-4AFRN4CP.js.map → resolve-gguf-SDIYGGI6.js.map} +1 -1
  87. package/dist/{runtime-session-EJQZXANB.js → runtime-session-4IACVDSY.js} +7 -7
  88. package/dist/{runtime-session-EJQZXANB.js.map → runtime-session-4IACVDSY.js.map} +1 -1
  89. package/dist/session-2JT4AUZT.js +13 -0
  90. package/dist/{session-ZJGENHKX.js.map → session-2JT4AUZT.js.map} +1 -1
  91. package/dist/session-store-O4XPXKOV.js +4 -0
  92. package/dist/{session-store-WFBX7HXL.js.map → session-store-O4XPXKOV.js.map} +1 -1
  93. package/dist/{sql-extract-JELDZA6W.js → sql-extract-6UJOLJDL.js} +3 -3
  94. package/dist/{sql-extract-JELDZA6W.js.map → sql-extract-6UJOLJDL.js.map} +1 -1
  95. package/dist/stream-json-Y45JBNYP.js +112 -0
  96. package/dist/stream-json-Y45JBNYP.js.map +1 -0
  97. package/dist/ui-K7D3BCHD.js +5 -0
  98. package/dist/ui-K7D3BCHD.js.map +1 -0
  99. package/dist/vg-mcp-bridge-B35WCSAS.js +75 -0
  100. package/dist/vg-mcp-bridge-B35WCSAS.js.map +1 -0
  101. package/dist/{vgd-3MHSB4KA.js → vgd-KDQFS33M.js} +6 -6
  102. package/dist/vgd-KDQFS33M.js.map +1 -0
  103. package/package.json +1 -1
  104. package/dist/baseline-PPZJBYAJ.js +0 -8
  105. package/dist/chunk-57QU36P6.js +0 -5645
  106. package/dist/chunk-57QU36P6.js.map +0 -1
  107. package/dist/chunk-6AXHLQBV.js +0 -1892
  108. package/dist/chunk-6AXHLQBV.js.map +0 -1
  109. package/dist/chunk-6UK6QPTR.js.map +0 -1
  110. package/dist/chunk-6UQMXIAG.js.map +0 -1
  111. package/dist/chunk-A2IYFNCQ.js.map +0 -1
  112. package/dist/chunk-DGUM43GV.js +0 -10
  113. package/dist/chunk-OSD546W7.js.map +0 -1
  114. package/dist/chunk-UZJS7GMK.js.map +0 -1
  115. package/dist/chunk-VI4ITXHQ.js.map +0 -1
  116. package/dist/chunk-YF43FOPP.js.map +0 -1
  117. package/dist/chunk-ZBTY6VV4.js.map +0 -1
  118. package/dist/fs-B5HZ2MIU.js.map +0 -1
  119. package/dist/fs-O5IQYXHO.js.map +0 -1
  120. package/dist/interactive-OCWQ326U.js.map +0 -1
  121. package/dist/load-QPOWQD7C.js +0 -8
  122. package/dist/local-runtime-5O2NN3GA.js +0 -5
  123. package/dist/mcp-tools-I7JK3S2M.js +0 -4
  124. package/dist/model-manager-LB54VSV7.js +0 -10
  125. package/dist/session-ZJGENHKX.js +0 -13
  126. package/dist/session-store-WFBX7HXL.js +0 -4
  127. package/dist/stream-json-Z5KC2DM3.js +0 -78
  128. package/dist/stream-json-Z5KC2DM3.js.map +0 -1
  129. package/dist/ui-Q6DJ3GCU.js +0 -5
  130. package/dist/ui-Q6DJ3GCU.js.map +0 -1
  131. package/dist/vgd-3MHSB4KA.js.map +0 -1
@@ -0,0 +1,2184 @@
1
+ import { searchSymbols, resolveVersion, resolveLib, loadCatalog, driftFor, localPackageDocs, readDoc, inventory, localApiSurface } from './chunk-G4HTI4ZM.js';
2
+ import { loadConfig, computeUpgradeImpact, getChangelogSignals, gitHistoryAvailable, buildVersionTimelines, findPackageTimeline, severityRank, projectTypeToVulnEcosystem } from './chunk-4JD63RUR.js';
3
+ import { writeTextFile, pathExists, readJsonFile } from './chunk-CRAMOEBE.js';
4
+ import { resolveOne } from './chunk-J7LKEB2R.js';
5
+ import { impactOf, indexFor, withTimeout, getNodeEmbeddings, queryGraphSemantic, queryGraph, loadEmbedder, isModelReady } from './chunk-EETK7S43.js';
6
+ import { discoverModels } from './chunk-CQDUBAQY.js';
7
+ import { cacheDir } from './chunk-ZKNB4YH4.js';
8
+ import { bidirectional } from 'graphology-shortest-path/unweighted.js';
9
+ import Graph from 'graphology';
10
+ import * as fs from 'fs';
11
+ import * as path4 from 'path';
12
+ import * as crypto from 'crypto';
13
+ import { createHash } from 'crypto';
14
+ import { Command } from 'commander';
15
+ import chalk from 'chalk';
16
+ import * as os from 'os';
17
+ import * as zlib from 'zlib';
18
+ import { promisify } from 'util';
19
+ import { encode, decode } from 'gpt-tokenizer/encoding/cl100k_base';
20
+
21
+ // src/grounding/pack.ts
22
+ var FREE_PACK = {
23
+ id: "vibgrate-free",
24
+ version: "2026.06",
25
+ license: "CC-BY (standards summaries) + Apache-2.0 (own content)",
26
+ entries: [
27
+ {
28
+ id: "owasp-a01-access-control",
29
+ topic: "access-control",
30
+ summary: "Enforce authorization on every request server-side; deny by default and check the user\u2019s role and ownership, not just authentication.",
31
+ citation: { title: "OWASP Top 10 2021 A01: Broken Access Control", url: "https://owasp.org/Top10/A01_2021-Broken_Access_Control/" },
32
+ kind: "should_follow",
33
+ rationale: "recommended",
34
+ match: {
35
+ keywords: ["auth", "authorize", "authorization", "permission", "role", "access", "acl", "rbac", "tenant"],
36
+ imports: ["casl", "accesscontrol", "next-auth"]
37
+ }
38
+ },
39
+ {
40
+ id: "owasp-a07-auth-failures",
41
+ topic: "authentication",
42
+ summary: "Use vetted auth libraries, hash passwords with a strong adaptive function, rate-limit logins, and keep sessions short and rotated.",
43
+ citation: { title: "OWASP Top 10 2021 A07: Identification & Authentication Failures", url: "https://owasp.org/Top10/A07_2021-Identification_and_Authentication_Failures/" },
44
+ kind: "should_follow",
45
+ rationale: "recommended",
46
+ match: {
47
+ keywords: ["login", "authenticate", "signin", "session", "jwt", "password", "credential"],
48
+ imports: ["passport", "jsonwebtoken", "bcrypt", "argon2", "next-auth", "lucia"]
49
+ }
50
+ },
51
+ {
52
+ id: "owasp-a02-crypto-failures",
53
+ topic: "cryptography",
54
+ summary: "Prefer well-reviewed primitives; never roll your own crypto; use authenticated encryption and a strong KDF; never hardcode keys.",
55
+ citation: { title: "OWASP Top 10 2021 A02: Cryptographic Failures", url: "https://owasp.org/Top10/A02_2021-Cryptographic_Failures/" },
56
+ kind: "should_follow",
57
+ rationale: "recommended",
58
+ match: { keywords: ["encrypt", "decrypt", "cipher", "hash", "hmac", "crypto", "sign", "verify"], imports: ["crypto", "bcrypt", "argon2", "node:crypto", "tweetnacl"], calls: ["createCipheriv", "createHash", "randomBytes"] }
59
+ },
60
+ {
61
+ id: "owasp-a03-injection",
62
+ topic: "input-validation",
63
+ summary: "Validate and parameterize all untrusted input; use parameterized queries and schema validation rather than string concatenation.",
64
+ citation: { title: "OWASP Top 10 2021 A03: Injection", url: "https://owasp.org/Top10/A03_2021-Injection/" },
65
+ kind: "should_follow",
66
+ rationale: "recommended",
67
+ match: { keywords: ["validate", "sanitize", "escape", "schema", "parse"], imports: ["zod", "joi", "yup", "ajv", "validator", "class-validator"] }
68
+ },
69
+ {
70
+ id: "cwe-89-sql-injection",
71
+ topic: "sql-injection",
72
+ summary: "Use parameterized queries / prepared statements or a query builder; never build SQL by concatenating user input.",
73
+ citation: { title: "CWE-89: SQL Injection", url: "https://cwe.mitre.org/data/definitions/89.html" },
74
+ kind: "relevant_to",
75
+ rationale: "recommended",
76
+ match: { keywords: ["sql", "query", "where", "select"], imports: ["pg", "mysql", "mysql2", "sqlite3", "better-sqlite3", "knex", "sequelize", "typeorm", "prisma", "drizzle"], calls: ["query", "execute", "raw"] }
77
+ },
78
+ {
79
+ id: "cwe-798-hardcoded-credentials",
80
+ topic: "secrets",
81
+ summary: "Do not embed secrets/API keys/passwords in source; load them from a secrets manager or environment and keep them out of version control.",
82
+ citation: { title: "CWE-798: Use of Hard-coded Credentials", url: "https://cwe.mitre.org/data/definitions/798.html" },
83
+ kind: "smells_like",
84
+ rationale: "conjectured",
85
+ match: { keywords: ["secret", "apikey", "apikeys", "token", "credential", "password", "privatekey"] }
86
+ },
87
+ {
88
+ id: "cwe-502-deserialization",
89
+ topic: "deserialization",
90
+ summary: "Avoid deserializing untrusted data and never eval it; prefer safe parsers and explicit schemas.",
91
+ citation: { title: "CWE-502: Deserialization of Untrusted Data", url: "https://cwe.mitre.org/data/definitions/502.html" },
92
+ kind: "smells_like",
93
+ rationale: "conjectured",
94
+ match: { keywords: ["deserialize", "unserialize", "eval"], calls: ["eval", "Function", "deserialize"] }
95
+ },
96
+ {
97
+ id: "owasp-a10-ssrf",
98
+ topic: "ssrf",
99
+ summary: "Validate and allowlist outbound URLs built from user input to prevent server-side request forgery.",
100
+ citation: { title: "OWASP Top 10 2021 A10: Server-Side Request Forgery", url: "https://owasp.org/Top10/A10_2021-Server-Side_Request_Forgery_%28SSRF%29/" },
101
+ kind: "relevant_to",
102
+ rationale: "recommended",
103
+ match: { keywords: ["fetch", "request", "webhook", "proxy"], imports: ["axios", "node-fetch", "got", "undici", "superagent"], calls: ["fetch", "request", "get", "post"] }
104
+ },
105
+ {
106
+ id: "owasp-a09-logging",
107
+ topic: "logging",
108
+ summary: "Log security-relevant events with enough context to detect and investigate, but never log secrets or PII.",
109
+ citation: { title: "OWASP Top 10 2021 A09: Security Logging & Monitoring Failures", url: "https://owasp.org/Top10/A09_2021-Security_Logging_and_Monitoring_Failures/" },
110
+ kind: "relevant_to",
111
+ rationale: "recommended",
112
+ match: { keywords: ["log", "logger", "audit", "trace"], imports: ["winston", "pino", "bunyan", "log4js"] }
113
+ }
114
+ ]
115
+ };
116
+ function buildGraphologyGraph(nodes, edges) {
117
+ const g = new Graph({ type: "directed", multi: true, allowSelfLoops: true });
118
+ for (const n of nodes) {
119
+ g.addNode(n.id, { kind: n.kind, name: n.name });
120
+ }
121
+ for (const e of edges) {
122
+ if (!g.hasNode(e.src) || !g.hasNode(e.dst)) continue;
123
+ if (g.hasEdge(e.id)) continue;
124
+ g.addDirectedEdgeWithKey(e.id, e.src, e.dst, { kind: e.kind, weight: e.confidence });
125
+ }
126
+ return g;
127
+ }
128
+
129
+ // src/engine/paths.ts
130
+ function shortestPath(graph, srcId, dstId) {
131
+ const g = buildGraphologyGraph(graph.nodes, graph.edges);
132
+ if (!g.hasNode(srcId) || !g.hasNode(dstId)) return null;
133
+ const forward = bidirectional(g, srcId, dstId);
134
+ if (forward) return { ids: forward, direction: "forward" };
135
+ const reverse = bidirectional(g, dstId, srcId);
136
+ if (reverse) return { ids: reverse.slice().reverse(), direction: "reverse" };
137
+ return null;
138
+ }
139
+ var NEIGHBOR_CAP = 6;
140
+ function pathDisconnect(graph, srcId, dstId) {
141
+ const idx = indexFor(graph);
142
+ const from = neighborhood(idx, srcId);
143
+ const to = neighborhood(idx, dstId);
144
+ const hint = from.calls.length || from.calledBy.length || to.calls.length || to.calledBy.length ? "No direct call/import chain links these symbols. Try `vg show` / `vg impact` on each end, or path via a shared hub." : "Neither endpoint has call/import neighbors in the map \u2014 they may be isolated docs/externals, or the link needs a more precise build.";
145
+ return { connected: false, from, to, hint };
146
+ }
147
+ function neighborhood(idx, id) {
148
+ const node = idx.node(id);
149
+ const name = node?.qualifiedName ?? id;
150
+ const calls = uniqueNames(idx.callees(id).map((x) => x.node.qualifiedName));
151
+ const calledBy = uniqueNames(idx.callers(id).map((x) => x.node.qualifiedName));
152
+ const imports = uniqueNames(
153
+ idx.out(id, "import").map((e) => idx.node(e.dst)?.qualifiedName).filter((n) => !!n)
154
+ );
155
+ const importedBy = uniqueNames(
156
+ idx.in(id, "import").map((e) => idx.node(e.src)?.qualifiedName).filter((n) => !!n)
157
+ );
158
+ return { name, calls, calledBy, imports, importedBy };
159
+ }
160
+ function uniqueNames(names) {
161
+ return [...new Set(names)].sort((a, b) => a.localeCompare(b)).slice(0, NEIGHBOR_CAP);
162
+ }
163
+ function coveringTests(graph, node, index) {
164
+ const idx = index ?? indexFor(graph);
165
+ const byId = idx.nodeById;
166
+ const out = /* @__PURE__ */ new Map();
167
+ for (const e of idx.in(node.id, "test")) {
168
+ const testNode = byId.get(e.src);
169
+ if (!testNode) continue;
170
+ out.set(testNode.file, { file: testNode.file, basis: "call", confidence: e.confidence });
171
+ }
172
+ if (typeof node.coverage === "number" && node.coverage > 0 && out.size === 0) {
173
+ out.set("(coverage)", { file: "(coverage report)", basis: "coverage", confidence: node.coverage });
174
+ }
175
+ return [...out.values()].sort((a, b) => b.confidence - a.confidence || a.file.localeCompare(b.file));
176
+ }
177
+ function testsToRun(graph, rootId, depth = 4) {
178
+ const idx = indexFor(graph);
179
+ const impact = impactOf(graph, rootId, { depth });
180
+ const affectedIds = /* @__PURE__ */ new Set([rootId, ...impact.affected.map((a) => a.id)]);
181
+ const testFiles = /* @__PURE__ */ new Set();
182
+ const untested = [];
183
+ for (const id of affectedIds) {
184
+ const node = idx.node(id);
185
+ if (!node) continue;
186
+ const covers = coveringTests(graph, node, idx).filter((c) => c.basis === "call");
187
+ for (const c of covers) testFiles.add(c.file);
188
+ if ((node.kind === "function" || node.kind === "method") && node.tested === false) {
189
+ untested.push({ id: node.id, name: node.qualifiedName, file: node.file });
190
+ }
191
+ }
192
+ return {
193
+ affectedTestFiles: [...testFiles].sort(),
194
+ untestedAffected: untested.sort((a, b) => a.name.localeCompare(b.name))
195
+ };
196
+ }
197
+ function detectRunner(root, lang) {
198
+ const pkgPath = path4.join(root, "package.json");
199
+ if (fs.existsSync(pkgPath)) {
200
+ try {
201
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
202
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
203
+ if (deps.vitest) return { name: "vitest", command: (f) => `npx vitest run ${f.join(" ")}`.trim() };
204
+ if (deps.jest) return { name: "jest", command: (f) => `npx jest ${f.join(" ")}`.trim() };
205
+ if (deps.mocha) return { name: "mocha", command: (f) => `npx mocha ${f.join(" ")}`.trim() };
206
+ } catch {
207
+ }
208
+ }
209
+ if (lang === "py" || fs.existsSync(path4.join(root, "pyproject.toml")) || fs.existsSync(path4.join(root, "pytest.ini"))) {
210
+ return { name: "pytest", command: (f) => `pytest ${f.join(" ")}`.trim() };
211
+ }
212
+ if (lang === "go" || fs.existsSync(path4.join(root, "go.mod"))) {
213
+ return { name: "go test", command: () => "go test ./..." };
214
+ }
215
+ if (lang === "cs" || hasFile(root, /\.csproj$/) || hasFile(root, /\.sln$/)) {
216
+ return { name: "dotnet test", command: () => "dotnet test" };
217
+ }
218
+ if (lang === "rb") return { name: "rspec", command: (f) => `bundle exec rspec ${f.join(" ")}`.trim() };
219
+ return { name: "tests", command: (f) => f.length ? f.join(" ") : "(no runner detected)" };
220
+ }
221
+ function hasFile(root, re) {
222
+ try {
223
+ return fs.readdirSync(root).some((f) => re.test(f));
224
+ } catch {
225
+ return false;
226
+ }
227
+ }
228
+ function findGitRoot(startDir = process.cwd()) {
229
+ let dir = path4.resolve(startDir);
230
+ for (; ; ) {
231
+ if (fs.existsSync(path4.join(dir, ".git"))) return dir;
232
+ const parent = path4.dirname(dir);
233
+ if (parent === dir) return null;
234
+ dir = parent;
235
+ }
236
+ }
237
+ function normalizeEntry(line) {
238
+ return line.trim().replace(/^\.\//, "").replace(/\/+$/, "");
239
+ }
240
+ function ensureGitignored(entry, startDir = process.cwd()) {
241
+ const root = findGitRoot(startDir);
242
+ if (!root) return { status: "not-a-repo", entry };
243
+ const gitignorePath = path4.join(root, ".gitignore");
244
+ const target = normalizeEntry(entry);
245
+ let existing = "";
246
+ let fileExisted = true;
247
+ try {
248
+ existing = fs.readFileSync(gitignorePath, "utf8");
249
+ } catch {
250
+ fileExisted = false;
251
+ }
252
+ const alreadyPresent = existing.split(/\r?\n/).some((line) => normalizeEntry(line) === target);
253
+ if (alreadyPresent) return { status: "present", entry, gitignorePath };
254
+ const needsLeadingNewline = existing.length > 0 && !existing.endsWith("\n");
255
+ fs.appendFileSync(gitignorePath, `${needsLeadingNewline ? "\n" : ""}${entry}
256
+ `, "utf8");
257
+ return { status: fileExisted ? "added" : "created", entry, gitignorePath };
258
+ }
259
+
260
+ // src/reporting/regions.ts
261
+ var REGIONS = [
262
+ {
263
+ id: "us",
264
+ label: "United States",
265
+ ingestHost: "us.ingest.vibgrate.com",
266
+ dashHost: "dash.vibgrate.com",
267
+ available: true
268
+ },
269
+ {
270
+ id: "eu",
271
+ label: "European Union",
272
+ ingestHost: "eu.ingest.vibgrate.com",
273
+ dashHost: "dash.vibgrate.eu",
274
+ available: true
275
+ },
276
+ {
277
+ id: "apac",
278
+ label: "Asia-Pacific (coming soon)",
279
+ ingestHost: "apac.ingest.vibgrate.com",
280
+ dashHost: "dash.vibgrate.com",
281
+ available: false
282
+ }
283
+ ];
284
+ var DEFAULT_REGION = "us";
285
+ function availableRegionIds() {
286
+ return REGIONS.filter((r) => r.available).map((r) => r.id);
287
+ }
288
+ function findRegion(id) {
289
+ return REGIONS.find((r) => r.id === id);
290
+ }
291
+ function resolveIngestHost(region, ingest) {
292
+ if (ingest) {
293
+ try {
294
+ return new URL(ingest).host;
295
+ } catch {
296
+ throw new Error(`Invalid ingest URL: ${ingest}`);
297
+ }
298
+ }
299
+ const id = (region ?? DEFAULT_REGION).toLowerCase();
300
+ const match = findRegion(id);
301
+ if (!match) {
302
+ throw new Error(`Unknown region "${id}". Supported: ${availableRegionIds().join(", ")}`);
303
+ }
304
+ if (!match.available) {
305
+ throw new Error(`Region "${id}" (${match.label}) is not yet available. Supported: ${availableRegionIds().join(", ")}`);
306
+ }
307
+ return match.ingestHost;
308
+ }
309
+ function dashHostForIngestHost(ingestHost) {
310
+ const match = REGIONS.find((r) => r.ingestHost === ingestHost);
311
+ return match?.dashHost ?? "dash.vibgrate.com";
312
+ }
313
+ function ingestHostForRegionId(region) {
314
+ return findRegion(region.toLowerCase())?.ingestHost;
315
+ }
316
+
317
+ // src/reporting/commands/dsn.ts
318
+ function generateKeyMaterial() {
319
+ return {
320
+ keyId: crypto.randomBytes(12).toString("hex"),
321
+ secret: crypto.randomBytes(32).toString("hex")
322
+ };
323
+ }
324
+ function buildDsn(keyId, secret, ingestHost, workspaceId) {
325
+ return `vibgrate+https://${keyId}:${secret}@${ingestHost}/${workspaceId}`;
326
+ }
327
+ async function provisionDsn(keyId, secret, workspaceId, ingestHost, region) {
328
+ const url = `https://${ingestHost}/v1/provision`;
329
+ try {
330
+ const response = await fetch(url, {
331
+ method: "POST",
332
+ headers: {
333
+ "Content-Type": "application/json",
334
+ "Connection": "close"
335
+ // Prevent keep-alive delays on exit
336
+ },
337
+ // Pin the workspace to the selected region. The API rejects the request
338
+ // if `region` doesn't match the endpoint host (residency guard). For a
339
+ // custom --ingest host we omit it and let the API derive it from the host.
340
+ body: JSON.stringify(region ? { keyId, secret, workspaceId, region } : { keyId, secret, workspaceId })
341
+ });
342
+ if (!response.ok) {
343
+ const result = await response.json();
344
+ return { success: false, error: result.error || `HTTP ${response.status}` };
345
+ }
346
+ return { success: true };
347
+ } catch (e) {
348
+ return { success: false, error: e instanceof Error ? e.message : String(e) };
349
+ }
350
+ }
351
+ async function createWorkspaceDsn(opts) {
352
+ const ingestHost = resolveIngestHost(opts.region, opts.ingest);
353
+ const { keyId, secret } = generateKeyMaterial();
354
+ const workspaceId = crypto.randomBytes(8).toString("hex");
355
+ const region = opts.ingest ? void 0 : (opts.region ?? "us").toLowerCase();
356
+ const result = await provisionDsn(keyId, secret, workspaceId, ingestHost, region);
357
+ if (!result.success) {
358
+ throw new Error(`Failed to provision workspace: ${result.error}`);
359
+ }
360
+ return { dsn: buildDsn(keyId, secret, ingestHost, workspaceId), keyId, workspaceId, ingestHost, region };
361
+ }
362
+ var dsnCommand = new Command("dsn").description("Manage DSN tokens");
363
+ dsnCommand.command("create").description("Create a new DSN token").option("--ingest <url>", "Ingest API URL (overrides --region)").option("--region <region>", `Data residency region (${availableRegionIds().join(", ")})`, "us").requiredOption("--workspace <id>", 'Workspace ID (use "new" to auto-generate)').option("--write <path>", "Write DSN to file").action(async (opts) => {
364
+ const keyId = crypto.randomBytes(12).toString("hex");
365
+ const secret = crypto.randomBytes(32).toString("hex");
366
+ let ingestHost;
367
+ try {
368
+ ingestHost = resolveIngestHost(opts.region, opts.ingest);
369
+ } catch (e) {
370
+ console.error(chalk.red(e instanceof Error ? e.message : String(e)));
371
+ process.exit(1);
372
+ }
373
+ let workspaceId = opts.workspace;
374
+ const isNewWorkspace = opts.workspace.toLowerCase() === "new";
375
+ if (isNewWorkspace) {
376
+ workspaceId = crypto.randomBytes(8).toString("hex");
377
+ console.log(chalk.dim(`Provisioning new workspace ${workspaceId}...`));
378
+ }
379
+ const region = opts.ingest ? void 0 : opts.region.toLowerCase();
380
+ if (isNewWorkspace) {
381
+ const result = await provisionDsn(keyId, secret, workspaceId, ingestHost, region);
382
+ if (!result.success) {
383
+ console.error(chalk.red(`Failed to provision DSN: ${result.error}`));
384
+ process.exit(1);
385
+ }
386
+ }
387
+ const dsn = buildDsn(keyId, secret, ingestHost, workspaceId);
388
+ console.log(chalk.green("\u2714") + " DSN created");
389
+ console.log("");
390
+ console.log(chalk.bold("Region:"));
391
+ console.log(` ${region ?? "custom"} (${ingestHost})`);
392
+ console.log("");
393
+ console.log(chalk.bold("DSN:"));
394
+ console.log(` ${dsn}`);
395
+ console.log("");
396
+ console.log(chalk.bold("Key ID:"));
397
+ console.log(` ${keyId}`);
398
+ if (isNewWorkspace) {
399
+ console.log("");
400
+ console.log(chalk.bold("Workspace ID:"));
401
+ console.log(` ${workspaceId}`);
402
+ }
403
+ console.log("");
404
+ console.log(chalk.dim("Set this as VIBGRATE_DSN in your CI environment."));
405
+ if (!isNewWorkspace) {
406
+ console.log(chalk.dim("The secret must be registered on your Vibgrate ingest API."));
407
+ }
408
+ if (opts.write) {
409
+ const writePath = path4.resolve(opts.write);
410
+ await writeTextFile(writePath, dsn + "\n");
411
+ console.log("");
412
+ console.log(chalk.green("\u2714") + ` DSN written to ${opts.write}`);
413
+ const root = findGitRoot(path4.dirname(writePath));
414
+ const rel = root ? path4.relative(root, writePath).split(path4.sep).join("/") : "";
415
+ if (root && rel && !rel.startsWith("..")) {
416
+ const res = ensureGitignored(rel, path4.dirname(writePath));
417
+ if (res.status === "created") {
418
+ console.log(chalk.green("\u2714") + ` Created .gitignore and ignored ${rel}`);
419
+ } else if (res.status === "added") {
420
+ console.log(chalk.green("\u2714") + ` Added ${rel} to .gitignore`);
421
+ } else if (res.status === "present") {
422
+ console.log(chalk.dim(` ${rel} is already in .gitignore`));
423
+ }
424
+ } else {
425
+ console.log(
426
+ chalk.yellow("\u26A0") + " Add this file to .gitignore to keep the DSN out of version control."
427
+ );
428
+ }
429
+ }
430
+ });
431
+ var STORE_DIRNAME = ".vibgrate";
432
+ var STORE_FILENAME = "credentials.json";
433
+ function homeCredentialsPath() {
434
+ return path4.join(os.homedir(), STORE_DIRNAME, STORE_FILENAME);
435
+ }
436
+ function projectCredentialsPath(cwd = process.cwd()) {
437
+ const root = findGitRoot(cwd) ?? cwd;
438
+ return path4.join(root, STORE_DIRNAME, STORE_FILENAME);
439
+ }
440
+ function credentialsPath(opts = {}) {
441
+ const override = process.env.VIBGRATE_CREDENTIALS?.trim();
442
+ if (override) return path4.resolve(override);
443
+ const local = projectCredentialsPath(opts.cwd);
444
+ if (opts.local) return local;
445
+ if (fs.existsSync(local)) return local;
446
+ return homeCredentialsPath();
447
+ }
448
+ function gitignoreEntryForCredentials(repoRoot, credsFile = credentialsPath()) {
449
+ const rel = path4.relative(repoRoot, credsFile);
450
+ if (rel && !rel.startsWith("..") && !path4.isAbsolute(rel)) {
451
+ return rel.split(path4.sep).join("/");
452
+ }
453
+ return `${STORE_DIRNAME}/${STORE_FILENAME}`;
454
+ }
455
+ function readStoredCredentials(opts = {}) {
456
+ try {
457
+ const raw = fs.readFileSync(credentialsPath(opts), "utf8");
458
+ const parsed = JSON.parse(raw);
459
+ return parsed && typeof parsed.dsn === "string" && parsed.dsn ? parsed : null;
460
+ } catch {
461
+ return null;
462
+ }
463
+ }
464
+ function writeStoredCredentials(creds, opts = {}) {
465
+ const file = credentialsPath(opts);
466
+ fs.mkdirSync(path4.dirname(file), { recursive: true });
467
+ fs.writeFileSync(file, JSON.stringify(creds, null, 2) + "\n", { encoding: "utf8", mode: 384 });
468
+ try {
469
+ fs.chmodSync(file, 384);
470
+ } catch {
471
+ }
472
+ }
473
+ function clearStoredCredentials(opts = {}) {
474
+ try {
475
+ fs.rmSync(credentialsPath(opts));
476
+ return true;
477
+ } catch {
478
+ return false;
479
+ }
480
+ }
481
+ function resolveDsn(explicitDsn) {
482
+ if (explicitDsn) return explicitDsn;
483
+ if (process.env.VIBGRATE_DSN) return process.env.VIBGRATE_DSN;
484
+ return readStoredCredentials()?.dsn ?? void 0;
485
+ }
486
+
487
+ // src/reporting/utils/compact-evidence.ts
488
+ var CATEGORY_PATTERNS = [
489
+ { category: "pricing", pattern: /price|pricing|billing|subscri|trial|credit|plan|tier|upgrade|premium|pro|enterprise/i },
490
+ { category: "auth", pattern: /sign[- ]?in|sign[- ]?up|log[- ]?in|log[- ]?out|auth|sso|oauth|password|register|invite|onboard/i },
491
+ { category: "dashboard", pattern: /dashboard|overview|home|main|summary|stats/i },
492
+ { category: "settings", pattern: /setting|config|preference|option|profile|account/i },
493
+ { category: "users", pattern: /user|member|team|role|permission|access|admin|owner/i },
494
+ { category: "integrations", pattern: /integrat|connect|webhook|api[- ]?key|sync|import|export/i },
495
+ { category: "reports", pattern: /report|analy|metric|chart|graph|insight|track/i },
496
+ { category: "workflows", pattern: /workflow|automat|schedule|trigger|action|job|task|pipeline/i },
497
+ { category: "projects", pattern: /project|workspace|organization|folder|repo/i },
498
+ { category: "navigation", pattern: /menu|nav|sidebar|header|footer|breadcrumb/i }
499
+ ];
500
+ function compactUiPurpose(result, maxSamplesPerCategory = 3) {
501
+ const evidence = result.topEvidence;
502
+ const dependencies = evidence.filter((e) => e.kind === "dependency").map((e) => e.value).slice(0, 10);
503
+ const routes = dedupeRoutes(
504
+ evidence.filter((e) => e.kind === "route").map((e) => e.value)
505
+ ).slice(0, 15);
506
+ const textEvidence = evidence.filter(
507
+ (e) => e.kind !== "dependency" && e.kind !== "route" && e.kind !== "feature_flag"
508
+ );
509
+ const byCategory = /* @__PURE__ */ new Map();
510
+ const categoryCounts = {};
511
+ for (const item of textEvidence) {
512
+ const category = categorize(item.value);
513
+ if (!byCategory.has(category)) {
514
+ byCategory.set(category, /* @__PURE__ */ new Set());
515
+ }
516
+ const normalized = normalizeValue(item.value);
517
+ if (normalized.length >= 3) {
518
+ byCategory.get(category).add(normalized);
519
+ }
520
+ }
521
+ const samples = [];
522
+ for (const [category, values] of byCategory) {
523
+ const deduped = dedupeStrings([...values]);
524
+ categoryCounts[category] = deduped.length;
525
+ for (const value of deduped.slice(0, maxSamplesPerCategory)) {
526
+ samples.push({ kind: "text", value, category });
527
+ }
528
+ }
529
+ const featureFlags = evidence.filter((e) => e.kind === "feature_flag");
530
+ if (featureFlags.length > 0) {
531
+ categoryCounts["feature_flags"] = featureFlags.length;
532
+ samples.push({ kind: "feature_flag", value: "feature flags detected", category: "feature_flags" });
533
+ }
534
+ return {
535
+ samples,
536
+ categoryCounts,
537
+ originalCount: evidence.length,
538
+ dependencies,
539
+ routes,
540
+ detectedFrameworks: result.detectedFrameworks
541
+ };
542
+ }
543
+ function categorize(value) {
544
+ for (const { category, pattern } of CATEGORY_PATTERNS) {
545
+ if (pattern.test(value)) return category;
546
+ }
547
+ return "general";
548
+ }
549
+ function normalizeValue(value) {
550
+ return value.toLowerCase().replace(/[^a-z0-9\s-]/g, " ").replace(/\s+/g, " ").trim().slice(0, 60);
551
+ }
552
+ function dedupeStrings(values) {
553
+ const sorted = values.sort((a, b) => b.length - a.length);
554
+ const kept = [];
555
+ for (const value of sorted) {
556
+ const isDupe = kept.some((k) => {
557
+ const stem = value.slice(0, 6);
558
+ return k.startsWith(stem) || k.includes(value) || value.includes(k);
559
+ });
560
+ if (!isDupe) {
561
+ kept.push(value);
562
+ }
563
+ }
564
+ return kept;
565
+ }
566
+ function dedupeRoutes(routes) {
567
+ const seen = /* @__PURE__ */ new Set();
568
+ const result = [];
569
+ for (const route of routes) {
570
+ const normalized = route.replace(/:[a-z_]+/gi, ":param").replace(/\[\[*\.*\.*[a-z_]+\]*\]/gi, ":param").replace(/\/+$/, "").toLowerCase();
571
+ if (!seen.has(normalized)) {
572
+ seen.add(normalized);
573
+ result.push(route);
574
+ }
575
+ }
576
+ return result;
577
+ }
578
+
579
+ // src/reporting/utils/compact-artifact.ts
580
+ var gzip2 = promisify(zlib.gzip);
581
+ var MAX_ITEMS = 50;
582
+ var MAX_DB_MODELS = 300;
583
+ var MAX_DB_FIELDS_PER_MODEL = 100;
584
+ var MAX_DB_MODEL_FILES = 5;
585
+ var MAX_DB_FILES_SCANNED = 500;
586
+ var DB_UPLOAD_HARD_CEILING = {
587
+ maxModels: 2e3,
588
+ maxFieldsPerModel: 500,
589
+ maxFilesPerModel: 20,
590
+ maxFilesScanned: 5e3
591
+ };
592
+ function clampCap(configured, fallback, ceiling) {
593
+ if (typeof configured !== "number" || !Number.isFinite(configured) || configured <= 0) return fallback;
594
+ return Math.min(Math.floor(configured), ceiling);
595
+ }
596
+ function extractName(entry) {
597
+ const match = entry.match(/^(.+?)\s*\(/);
598
+ return match ? match[1].trim() : entry.trim();
599
+ }
600
+ function compactDataStores(result) {
601
+ return {
602
+ databaseTechnologies: result.databaseTechnologies.slice(0, 10),
603
+ connectionStrings: [],
604
+ // Don't include connection strings in upload
605
+ connectionPoolSettings: result.connectionPoolSettings.slice(0, MAX_ITEMS),
606
+ replicationSettings: result.replicationSettings.slice(0, 20),
607
+ readReplicaSettings: result.readReplicaSettings.slice(0, 20),
608
+ failoverSettings: result.failoverSettings.slice(0, 20),
609
+ collationAndEncoding: result.collationAndEncoding.slice(0, 20),
610
+ queryTimeoutDefaults: result.queryTimeoutDefaults.slice(0, 20),
611
+ manualIndexes: result.manualIndexes.map(extractName).slice(0, MAX_ITEMS),
612
+ tables: result.tables.map(extractName).slice(0, MAX_ITEMS),
613
+ views: result.views.map(extractName).slice(0, MAX_ITEMS),
614
+ storedProcedures: result.storedProcedures.map(extractName).slice(0, MAX_ITEMS),
615
+ triggers: result.triggers.map(extractName).slice(0, MAX_ITEMS),
616
+ rowLevelSecurityPolicies: result.rowLevelSecurityPolicies.slice(0, 20),
617
+ otherServices: result.otherServices.slice(0, 20)
618
+ };
619
+ }
620
+ function compactApiSurface(result) {
621
+ const seenProviders = /* @__PURE__ */ new Set();
622
+ const uniqueIntegrations = result.integrations.filter((i) => {
623
+ const domain = i.provider.split(":")[0];
624
+ if (seenProviders.has(domain)) return false;
625
+ seenProviders.add(domain);
626
+ return true;
627
+ }).slice(0, MAX_ITEMS).map((i) => ({
628
+ provider: i.provider,
629
+ endpoint: "",
630
+ // Don't include full endpoints
631
+ version: i.version,
632
+ parameters: [],
633
+ // Don't include params
634
+ configOptions: [],
635
+ authHints: [],
636
+ files: []
637
+ // Don't include file paths
638
+ }));
639
+ return {
640
+ integrations: uniqueIntegrations,
641
+ openApiSpecifications: result.openApiSpecifications.slice(0, 10),
642
+ webhookUrls: result.webhookUrls.slice(0, 20),
643
+ callbackEndpoints: result.callbackEndpoints.slice(0, 20),
644
+ apiVersionPins: result.apiVersionPins.slice(0, 20),
645
+ tokenExpirationPolicies: result.tokenExpirationPolicies.slice(0, 20),
646
+ rateLimitOverrides: result.rateLimitOverrides.slice(0, 20),
647
+ customHeaders: result.customHeaders.slice(0, 20),
648
+ corsPolicies: result.corsPolicies.slice(0, 20),
649
+ oauthScopes: result.oauthScopes.slice(0, 20),
650
+ apiTokens: []
651
+ // Don't include token references
652
+ };
653
+ }
654
+ function compactAssetBranding(result) {
655
+ return {
656
+ faviconFiles: result.faviconFiles.slice(0, 1),
657
+ productLogos: []
658
+ // Don't include logos
659
+ };
660
+ }
661
+ function compactDatabaseSchema(result, caps) {
662
+ const maxModels = clampCap(caps?.maxModels, MAX_DB_MODELS, DB_UPLOAD_HARD_CEILING.maxModels);
663
+ const maxFieldsPerModel = clampCap(caps?.maxFieldsPerModel, MAX_DB_FIELDS_PER_MODEL, DB_UPLOAD_HARD_CEILING.maxFieldsPerModel);
664
+ const maxFilesPerModel = clampCap(caps?.maxFilesPerModel, MAX_DB_MODEL_FILES, DB_UPLOAD_HARD_CEILING.maxFilesPerModel);
665
+ const maxFilesScanned = clampCap(caps?.maxFilesScanned, MAX_DB_FILES_SCANNED, DB_UPLOAD_HARD_CEILING.maxFilesScanned);
666
+ return {
667
+ providers: result.providers.slice(0, MAX_ITEMS),
668
+ models: result.models.slice(0, maxModels).map((model) => ({
669
+ ...model,
670
+ fields: model.fields.slice(0, maxFieldsPerModel),
671
+ files: model.files.slice(0, maxFilesPerModel)
672
+ })),
673
+ enums: result.enums.slice(0, MAX_ITEMS),
674
+ filesScanned: result.filesScanned.slice(0, maxFilesScanned),
675
+ projects: result.projects.slice(0, MAX_ITEMS).map((project) => ({
676
+ project: project.project,
677
+ filesScanned: project.filesScanned.slice(0, MAX_ITEMS),
678
+ models: project.models.slice(0, MAX_ITEMS),
679
+ enums: project.enums.slice(0, MAX_ITEMS)
680
+ }))
681
+ };
682
+ }
683
+ function prepareArtifactForUpload(artifact, opts) {
684
+ const compacted = { ...artifact };
685
+ if (compacted.extended) {
686
+ const ext = { ...compacted.extended };
687
+ if (ext.dataStores) {
688
+ ext.dataStores = compactDataStores(ext.dataStores);
689
+ }
690
+ if (ext.apiSurface) {
691
+ ext.apiSurface = compactApiSurface(ext.apiSurface);
692
+ }
693
+ if (ext.assetBranding) {
694
+ ext.assetBranding = compactAssetBranding(ext.assetBranding);
695
+ }
696
+ if (ext.uiPurpose) {
697
+ const compactedUi = compactUiPurpose(ext.uiPurpose);
698
+ ext.uiPurpose = {
699
+ enabled: ext.uiPurpose.enabled,
700
+ detectedFrameworks: compactedUi.detectedFrameworks,
701
+ evidenceCount: compactedUi.originalCount,
702
+ capped: ext.uiPurpose.capped,
703
+ topEvidence: [],
704
+ // Clear full evidence
705
+ unknownSignals: [],
706
+ // Add compacted data under extended properties
707
+ ...{ compacted: compactedUi }
708
+ };
709
+ }
710
+ if (ext.runtimeConfiguration) {
711
+ ext.runtimeConfiguration = {
712
+ ...ext.runtimeConfiguration,
713
+ environmentVariables: ext.runtimeConfiguration.environmentVariables.slice(0, 100),
714
+ hiddenConfigFiles: ext.runtimeConfiguration.hiddenConfigFiles.slice(0, MAX_ITEMS),
715
+ startupArguments: ext.runtimeConfiguration.startupArguments.slice(0, 100)
716
+ };
717
+ }
718
+ if (ext.operationalResilience) {
719
+ const ops = ext.operationalResilience;
720
+ ext.operationalResilience = {
721
+ implicitTimeouts: ops.implicitTimeouts.slice(0, 30),
722
+ defaultPaginationSize: ops.defaultPaginationSize.slice(0, 30),
723
+ implicitRetryLogic: ops.implicitRetryLogic.slice(0, 30),
724
+ defaultLocale: ops.defaultLocale.slice(0, 20),
725
+ defaultCurrency: ops.defaultCurrency.slice(0, 20),
726
+ implicitTimezone: ops.implicitTimezone.slice(0, 20),
727
+ defaultCharacterEncoding: ops.defaultCharacterEncoding.slice(0, 20),
728
+ sessionStores: ops.sessionStores.slice(0, 20),
729
+ distributedLocks: ops.distributedLocks.slice(0, 20),
730
+ jobSchedulers: ops.jobSchedulers.slice(0, 30),
731
+ idempotencyKeys: ops.idempotencyKeys.slice(0, 20),
732
+ rateLimitingCounters: ops.rateLimitingCounters.slice(0, 20),
733
+ circuitBreakerState: ops.circuitBreakerState.slice(0, 20),
734
+ abTestToggles: ops.abTestToggles.slice(0, 20),
735
+ regionalEnablementRules: ops.regionalEnablementRules.slice(0, 20),
736
+ betaAccessGroups: ops.betaAccessGroups.slice(0, 20),
737
+ licensingEnforcementLogic: ops.licensingEnforcementLogic.slice(0, 20),
738
+ killSwitches: ops.killSwitches.slice(0, 20),
739
+ connectorRetryLogic: ops.connectorRetryLogic.slice(0, 20),
740
+ apiPollingIntervals: ops.apiPollingIntervals.slice(0, 20),
741
+ fieldMappings: ops.fieldMappings.slice(0, 20),
742
+ schemaRegistryRules: ops.schemaRegistryRules.slice(0, 20),
743
+ deadLetterQueueBehavior: ops.deadLetterQueueBehavior.slice(0, 20),
744
+ dataMaskingRules: ops.dataMaskingRules.slice(0, 20),
745
+ transformationLogic: ops.transformationLogic.slice(0, 20),
746
+ timezoneHandling: ops.timezoneHandling.slice(0, 20),
747
+ encryptionSettings: ops.encryptionSettings.slice(0, 30),
748
+ hardcodedSecretSignals: ops.hardcodedSecretSignals.slice(0, 20)
749
+ };
750
+ }
751
+ if (ext.dependencyGraph) {
752
+ ext.dependencyGraph = {
753
+ ...ext.dependencyGraph,
754
+ phantomDependencies: ext.dependencyGraph.phantomDependencies.slice(0, MAX_ITEMS),
755
+ phantomDependencyDetails: ext.dependencyGraph.phantomDependencyDetails?.slice(0, MAX_ITEMS),
756
+ duplicatedPackages: ext.dependencyGraph.duplicatedPackages.slice(0, MAX_ITEMS)
757
+ };
758
+ }
759
+ if (ext.databaseSchema) {
760
+ ext.databaseSchema = compactDatabaseSchema(ext.databaseSchema, opts?.databaseSchemaCaps);
761
+ }
762
+ compacted.extended = ext;
763
+ }
764
+ return compacted;
765
+ }
766
+ async function compressArtifact(artifact) {
767
+ const json = JSON.stringify(artifact);
768
+ return gzip2(json, { level: 9 });
769
+ }
770
+ async function prepareCompressedUpload(artifact, opts) {
771
+ const compacted = prepareArtifactForUpload(artifact, opts);
772
+ const compressed = await compressArtifact(compacted);
773
+ return { body: compressed, contentEncoding: "gzip" };
774
+ }
775
+ function postOnce(input, host) {
776
+ const url = input.force ? `${input.scheme}://${host}/v1/ingest/scan?force=1` : `${input.scheme}://${host}/v1/ingest/scan`;
777
+ return fetch(url, {
778
+ method: "POST",
779
+ headers: {
780
+ "Content-Type": "application/json",
781
+ "Content-Encoding": input.contentEncoding,
782
+ "X-Vibgrate-Timestamp": input.timestamp,
783
+ "Authorization": `VibgrateDSN ${input.keyId}:${input.secret}`,
784
+ "Connection": "close",
785
+ // Prevent keep-alive delays on exit
786
+ ...input.runId && input.runToken ? { "X-Vibgrate-Run-Id": input.runId, "X-Vibgrate-Run-Token": input.runToken } : {}
787
+ },
788
+ body: input.body
789
+ });
790
+ }
791
+ async function regionRedirectHost(response) {
792
+ try {
793
+ const payload = await response.clone().json();
794
+ if (payload?.code !== "REGION_MISMATCH" || !payload.region) return void 0;
795
+ return ingestHostForRegionId(payload.region);
796
+ } catch {
797
+ return void 0;
798
+ }
799
+ }
800
+ async function uploadScanArtifact(input) {
801
+ let host = input.host;
802
+ let response = await postOnce(input, host);
803
+ if (response.status === 409) {
804
+ const target = await regionRedirectHost(response);
805
+ if (target && target !== host) {
806
+ console.log(
807
+ chalk.yellow(
808
+ `\u21BB Workspace is pinned to a different region \u2014 retrying upload to ${target}...`
809
+ )
810
+ );
811
+ host = target;
812
+ response = await postOnce(input, host);
813
+ }
814
+ }
815
+ return { response, host };
816
+ }
817
+
818
+ // src/reporting/commands/push.ts
819
+ function parseDsn(dsn) {
820
+ const cleaned = dsn.replace(/[\x00-\x1F\x7F\uFEFF\u200B-\u200D\u2060]/g, "").trim();
821
+ const match = cleaned.match(/^vibgrate\+(https?):?\/\/([^:]+):([^@]+)@([^/]+)\/(.+)$/);
822
+ if (!match) return null;
823
+ return {
824
+ scheme: match[1],
825
+ keyId: match[2],
826
+ secret: match[3],
827
+ host: match[4],
828
+ workspaceId: match[5]
829
+ };
830
+ }
831
+ var pushCommand = new Command("push").description("Push scan results to Vibgrate API").option("--dsn <dsn>", "DSN token (or use VIBGRATE_DSN env)").option("--region <region>", `Override data residency region (${availableRegionIds().join(", ")})`).option("--file <file>", "Scan artifact file", ".vibgrate/scan_result.json").option("--strict", "Fail on upload errors").action(async (opts) => {
832
+ const dsn = resolveDsn(opts.dsn);
833
+ if (!dsn) {
834
+ console.error(chalk.red("No DSN provided."));
835
+ console.error(chalk.dim('Run "vibgrate login", set VIBGRATE_DSN, or use the --dsn flag.'));
836
+ if (opts.strict) process.exit(1);
837
+ return;
838
+ }
839
+ const parsed = parseDsn(dsn);
840
+ if (!parsed) {
841
+ console.error(chalk.red("Invalid DSN format."));
842
+ console.error(chalk.dim("Expected: vibgrate+https://<key_id>:<secret>@<host>/<workspace_id>"));
843
+ if (opts.strict) process.exit(1);
844
+ return;
845
+ }
846
+ const filePath = path4.resolve(opts.file);
847
+ if (!await pathExists(filePath)) {
848
+ console.error(chalk.red(`Scan artifact not found: ${filePath}`));
849
+ console.error(chalk.dim('Run "vibgrate scan" first.'));
850
+ if (opts.strict) process.exit(1);
851
+ return;
852
+ }
853
+ const artifact = await readJsonFile(filePath);
854
+ const config = await loadConfig(process.cwd());
855
+ const databaseSchemaCaps = config.scanners !== false ? config.scanners?.databaseSchema : void 0;
856
+ const { body, contentEncoding } = await prepareCompressedUpload(artifact, { databaseSchemaCaps });
857
+ const timestamp = String(Date.now());
858
+ let host = parsed.host;
859
+ if (opts.region) {
860
+ try {
861
+ host = resolveIngestHost(opts.region);
862
+ } catch (e) {
863
+ console.error(chalk.red(e instanceof Error ? e.message : String(e)));
864
+ if (opts.strict) process.exit(1);
865
+ return;
866
+ }
867
+ }
868
+ const originalSize = JSON.stringify(artifact).length;
869
+ const compressedSize = body.length;
870
+ const ratio = ((1 - compressedSize / originalSize) * 100).toFixed(0);
871
+ console.log(chalk.dim(`Uploading to ${host}... (${(compressedSize / 1024).toFixed(0)} KB, ${ratio}% smaller)`));
872
+ try {
873
+ const { response, host: uploadedHost } = await uploadScanArtifact({
874
+ scheme: parsed.scheme,
875
+ host,
876
+ keyId: parsed.keyId,
877
+ secret: parsed.secret,
878
+ body,
879
+ contentEncoding,
880
+ timestamp,
881
+ // Set only by an automated caller running the scan on the workspace's
882
+ // behalf (e.g. a Vibgrate-hosted remediation run) — never a customer flag.
883
+ runId: process.env.VIBGRATE_SCAN_RUN_ID,
884
+ runToken: process.env.VIBGRATE_SCAN_RUN_TOKEN
885
+ });
886
+ host = uploadedHost;
887
+ if (!response.ok) {
888
+ const text = await response.text();
889
+ throw new Error(`HTTP ${response.status}: ${text}`);
890
+ }
891
+ const result = await response.json();
892
+ console.log(chalk.green("\u2714") + ` Scan queued for processing (${result.ingestId ?? "ok"})`);
893
+ console.log();
894
+ console.log(chalk.dim("Processing continues in the background. Results available shortly."));
895
+ console.log();
896
+ if (result.ingestId) {
897
+ const dashHost = dashHostForIngestHost(host);
898
+ const reportUrl = `https://${dashHost}/${parsed.workspaceId}/scan/${result.ingestId}`;
899
+ console.log(chalk.dim("View report: ") + chalk.underline(reportUrl));
900
+ }
901
+ } catch (e) {
902
+ const msg = e instanceof Error ? e.message : String(e);
903
+ console.error(chalk.red(`Upload failed: ${msg}`));
904
+ if (opts.strict) process.exit(1);
905
+ }
906
+ });
907
+ function readScanArtifact(root) {
908
+ try {
909
+ const file = path4.join(root, ".vibgrate", "scan_result.json");
910
+ return JSON.parse(fs.readFileSync(file, "utf8"));
911
+ } catch {
912
+ return null;
913
+ }
914
+ }
915
+ function loadVulnerabilities(root) {
916
+ return readScanArtifact(root)?.extended?.vulnerabilities ?? null;
917
+ }
918
+ function resolvePackageTarget(root, pkg) {
919
+ const artifact = readScanArtifact(root);
920
+ for (const project of artifact?.projects ?? []) {
921
+ const dep = project.dependencies?.find((d) => d.package === pkg);
922
+ if (dep) {
923
+ return {
924
+ ecosystem: projectTypeToVulnEcosystem(project.type) ?? "unknown",
925
+ currentVersion: dep.resolvedVersion,
926
+ latestVersion: dep.latestStable,
927
+ majorsBehind: dep.majorsBehind
928
+ };
929
+ }
930
+ }
931
+ const rec = inventory(root).records.find((r) => r.name === pkg);
932
+ if (rec) {
933
+ const ecosystem = rec.ecosystem === "npm" ? "npm" : rec.ecosystem === "pypi" ? "pypi" : "unknown";
934
+ return { ecosystem, currentVersion: rec.installed ?? null, latestVersion: rec.latest ?? null, majorsBehind: null };
935
+ }
936
+ return { ecosystem: "unknown", currentVersion: null, latestVersion: null, majorsBehind: null };
937
+ }
938
+ function openFixableAdvisories(root, pkg) {
939
+ const vulns = loadVulnerabilities(root);
940
+ const match = vulns?.packages.find((p) => p.package === pkg);
941
+ return (match?.advisories ?? []).filter((a) => a.fixedVersions.length).map((a) => a.id);
942
+ }
943
+ function filterBySeverity(result, minSeverity) {
944
+ if (!minSeverity || minSeverity === "unknown") return result;
945
+ const floor = severityRank(minSeverity);
946
+ const packages = result.packages.map((p) => ({ ...p, advisories: p.advisories.filter((a) => severityRank(a.severity) >= floor) })).filter((p) => p.advisories.length > 0);
947
+ const totalAdvisories = packages.reduce((n, p) => n + p.advisories.length, 0);
948
+ return { ...result, packages, totalAdvisories };
949
+ }
950
+
951
+ // src/mcp/attribution.ts
952
+ var MCP_MAX_COMMITS = 300;
953
+ var ECOSYSTEM_MAP = {
954
+ npm: "npm",
955
+ pypi: "pypi",
956
+ go: "go",
957
+ rust: "cargo",
958
+ ruby: "rubygems",
959
+ php: "composer",
960
+ dotnet: "nuget",
961
+ dart: "pub"
962
+ // swift (no advisory ecosystem) and java (no resolved lockfile) have no timeline.
963
+ };
964
+ function brief(commit) {
965
+ return { sha: commit.shortSha, author: commit.authorName, date: commit.date, subject: commit.subject };
966
+ }
967
+ async function attributedInventory(root, opts = {}) {
968
+ const inv = inventory(root);
969
+ if (!opts.attribute) return { ...inv, attribution: "off" };
970
+ if (!await gitHistoryAvailable(root)) return { ...inv, attribution: "unavailable" };
971
+ const timelines = await buildVersionTimelines(root, { maxCommits: MCP_MAX_COMMITS });
972
+ if (!timelines) return { ...inv, attribution: "unavailable" };
973
+ const records = inv.records.map((r) => {
974
+ const ecosystem = ECOSYSTEM_MAP[r.ecosystem];
975
+ if (!ecosystem) return r;
976
+ const pt = findPackageTimeline(timelines, ecosystem, r.name);
977
+ if (!pt || !pt.changes.length) return r;
978
+ return {
979
+ ...r,
980
+ addedBy: brief(pt.changes[0].commit),
981
+ currentVersionBy: brief(pt.changes[pt.changes.length - 1].commit)
982
+ };
983
+ });
984
+ return { ...inv, records, attribution: "git" };
985
+ }
986
+ function countTokens(text) {
987
+ return encode(text).length;
988
+ }
989
+ function truncateToTokens(text, budget, marker = "\n\u2026(truncated)") {
990
+ if (!Number.isFinite(budget) || budget <= 0) return { text, truncated: false, tokens: countTokens(text) };
991
+ const tokens = encode(text);
992
+ if (tokens.length <= budget) return { text, truncated: false, tokens: tokens.length };
993
+ const markerTokens = encode(marker).length;
994
+ const keep = Math.max(0, budget - markerTokens);
995
+ let slice = decode(tokens.slice(0, keep));
996
+ const lastNl = slice.lastIndexOf("\n");
997
+ if (lastNl > 0) slice = slice.slice(0, lastNl);
998
+ const out = slice + marker;
999
+ return { text: out, truncated: true, tokens: countTokens(out) };
1000
+ }
1001
+
1002
+ // src/engine/selection-weights.ts
1003
+ var DEFAULT_SELECTION_WEIGHTS = {
1004
+ headingUsage: 4,
1005
+ headingPreamble: -6,
1006
+ hasCode: 3,
1007
+ codeDensity: 0.5,
1008
+ queryOverlap: 5,
1009
+ symbolMatch: 2,
1010
+ position: -0.1,
1011
+ linkDensity: -3
1012
+ };
1013
+
1014
+ // src/engine/select.ts
1015
+ var STOPWORDS = /* @__PURE__ */ new Set(["the", "a", "an", "to", "of", "in", "for", "and", "or", "is", "how", "do", "i", "use", "using", "with", "on", "my"]);
1016
+ var USAGE_RE = /\b(usage|example|examples|quick ?start|getting started|api|guide|how to|reference|tutorial|basic)\b/i;
1017
+ var PREAMBLE_RE = /\b(install|installation|license|licence|contributing|contributors?|sponsors?|table of contents|toc|changelog|funding|code of conduct|security|acknowledge)\b/i;
1018
+ function splitSections(md) {
1019
+ const out = [];
1020
+ let cur = { heading: "", level: 0};
1021
+ let buf = [];
1022
+ const flush = () => {
1023
+ const body = buf.join("\n").trim();
1024
+ if (cur.heading || body) out.push({ heading: cur.heading, level: cur.level, body });
1025
+ buf = [];
1026
+ };
1027
+ for (const line of md.split("\n")) {
1028
+ const h = /^(#{1,6})\s+(.*\S)\s*$/.exec(line);
1029
+ if (h) {
1030
+ flush();
1031
+ cur = { heading: h[2].trim(), level: h[1].length, body: "" };
1032
+ } else {
1033
+ buf.push(line);
1034
+ }
1035
+ }
1036
+ flush();
1037
+ return out;
1038
+ }
1039
+ function chunkBody(body) {
1040
+ const chunks = [];
1041
+ let cur = [];
1042
+ let inFence = false;
1043
+ const flush = () => {
1044
+ const c = cur.join("\n").trim();
1045
+ if (c) chunks.push(c);
1046
+ cur = [];
1047
+ };
1048
+ for (const line of body.split("\n")) {
1049
+ if (/^\s*```/.test(line)) inFence = !inFence;
1050
+ if (!inFence && line.trim() === "") {
1051
+ flush();
1052
+ continue;
1053
+ }
1054
+ cur.push(line);
1055
+ }
1056
+ flush();
1057
+ return chunks;
1058
+ }
1059
+ function stripNoise(body) {
1060
+ return body.split("\n").filter((l) => !/^\s*(?:!\[[^\]]*\]\([^)]*\)\s*)+$/.test(l)).join("\n").trim();
1061
+ }
1062
+ function tokenizeQuery(q) {
1063
+ if (!q) return [];
1064
+ return [...new Set(q.toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length > 1 && !STOPWORDS.has(w)))];
1065
+ }
1066
+ function symbolsFromApi(api) {
1067
+ if (!api) return [];
1068
+ const out = /* @__PURE__ */ new Set();
1069
+ const re = /\b(?:function|class|interface|type|const|enum|namespace)\s+([A-Za-z_$][\w$]*)/g;
1070
+ let m;
1071
+ while (m = re.exec(api)) out.add(m[1]);
1072
+ return [...out];
1073
+ }
1074
+ function scoreSection(s, idx, queryTerms, symbols, w) {
1075
+ const text = `${s.heading}
1076
+ ${s.body}`.toLowerCase();
1077
+ let score = 0;
1078
+ if (USAGE_RE.test(s.heading)) score += w.headingUsage;
1079
+ if (PREAMBLE_RE.test(s.heading)) score += w.headingPreamble;
1080
+ const codeBlocks = Math.floor((s.body.match(/```/g) ?? []).length / 2);
1081
+ if (codeBlocks > 0) score += w.hasCode + Math.min(codeBlocks, 5) * w.codeDensity;
1082
+ for (const t of queryTerms) if (text.includes(t)) score += w.queryOverlap;
1083
+ for (const sym of symbols) if (sym.length > 2 && text.includes(sym.toLowerCase())) score += w.symbolMatch;
1084
+ score += idx * w.position;
1085
+ const links = (s.body.match(/\]\(|https?:\/\//g) ?? []).length;
1086
+ const lineCount = Math.max(1, s.body.split("\n").filter((l) => l.trim()).length);
1087
+ if (links / lineCount > 0.5) score += w.linkDensity;
1088
+ return score;
1089
+ }
1090
+ var apiBlock = (api) => api ? `## API (types)
1091
+
1092
+ \`\`\`ts
1093
+ ${api}
1094
+ \`\`\`` : "";
1095
+ function selectForBudget(input) {
1096
+ const w = input.weights ?? DEFAULT_SELECTION_WEIGHTS;
1097
+ const api = apiBlock(input.apiSurface);
1098
+ const readme = input.readme.trim();
1099
+ if (!input.budget || !Number.isFinite(input.budget)) {
1100
+ const full2 = [readme, api].filter(Boolean).join("\n\n");
1101
+ return { text: full2, tokens: countTokens(full2), truncated: false };
1102
+ }
1103
+ const budget = input.budget;
1104
+ const queryTerms = tokenizeQuery(input.query);
1105
+ const symbols = symbolsFromApi(input.apiSurface);
1106
+ const render = (s) => {
1107
+ const body = stripNoise(s.body);
1108
+ if (!body) return "";
1109
+ return `${s.heading ? `## ${s.heading}
1110
+ ` : ""}${body}`.trim();
1111
+ };
1112
+ const sections = splitSections(readme).filter((s) => !PREAMBLE_RE.test(s.heading));
1113
+ const full = [api, ...sections.map(render)].filter(Boolean).join("\n\n").trim();
1114
+ if (countTokens(full) <= budget) {
1115
+ return { text: full, tokens: countTokens(full), truncated: false };
1116
+ }
1117
+ const units = [];
1118
+ for (const s of sections) {
1119
+ if (countTokens(s.body) > budget) {
1120
+ chunkBody(s.body).forEach((p, i) => units.push({ heading: i === 0 ? s.heading : "", level: s.level, body: p }));
1121
+ } else {
1122
+ units.push(s);
1123
+ }
1124
+ }
1125
+ const ranked = units.map((s, idx) => ({ s, idx, score: scoreSection(s, idx, queryTerms, symbols, w) })).sort((a, b) => b.score - a.score || a.idx - b.idx);
1126
+ const out = [];
1127
+ let used = 0;
1128
+ const add = (chunk) => {
1129
+ const t = countTokens(chunk);
1130
+ if (used + t <= budget) {
1131
+ out.push(chunk);
1132
+ used += t;
1133
+ return true;
1134
+ }
1135
+ return false;
1136
+ };
1137
+ if (api && !add(api) && used < budget) {
1138
+ out.push(truncateToTokens(api, budget - used).text);
1139
+ used = budget;
1140
+ }
1141
+ const picked = [];
1142
+ for (const { s, idx } of ranked) {
1143
+ const chunk = render(s);
1144
+ if (!chunk) continue;
1145
+ if (used + countTokens(`
1146
+ ${chunk}`) <= budget) {
1147
+ picked.push(idx);
1148
+ used += countTokens(`
1149
+ ${chunk}`);
1150
+ }
1151
+ }
1152
+ picked.sort((a, b) => a - b);
1153
+ for (const idx of picked) out.push(render(units[idx]));
1154
+ if (used < budget - 20) {
1155
+ const next = ranked.find((r) => !picked.includes(r.idx) && render(r.s));
1156
+ if (next) {
1157
+ const head = truncateToTokens(render(next.s), budget - used).text;
1158
+ if (head.trim()) out.push(head);
1159
+ }
1160
+ }
1161
+ const text = out.join("\n\n").trim();
1162
+ return { text, tokens: countTokens(text), truncated: true };
1163
+ }
1164
+
1165
+ // src/engine/quality.ts
1166
+ var IDENT_RE = /^(?:[A-Za-z][a-z0-9]*[A-Z][A-Za-z0-9]*|[a-z][a-z0-9]*(?:_[a-z0-9]+)+|[a-z][a-z0-9]*(?:-[a-z0-9]+)+)$/;
1167
+ function identifierTerms(query) {
1168
+ if (!query) return [];
1169
+ const raw = [...new Set(query.split(/[^A-Za-z0-9_-]+/).filter((w) => w.length > 2))];
1170
+ return raw.filter((t) => IDENT_RE.test(t) || t.length >= 6 && /[A-Z]/.test(t) && /[a-z]/.test(t));
1171
+ }
1172
+ function assessDocQuality(content, opts = {}) {
1173
+ const minTokens = opts.minTokens ?? 60;
1174
+ const lc = content.toLowerCase();
1175
+ const reasons = [];
1176
+ let score = 0;
1177
+ const hasCode = /```/.test(content) || /(^|\n)( {4}|\t)\S/.test(content);
1178
+ if (hasCode) score += 2;
1179
+ else reasons.push("no code example");
1180
+ const tokens = countTokens(content);
1181
+ if (tokens >= minTokens) score += 1;
1182
+ else reasons.push(`thin (${tokens} tokens)`);
1183
+ const bareName = opts.name?.toLowerCase().replace(/^@[^/]+\//, "");
1184
+ if (bareName && bareName.length > 1) {
1185
+ if (lc.includes(bareName)) score += 1;
1186
+ else reasons.push("library name absent");
1187
+ }
1188
+ const qt = tokenizeQuery(opts.query);
1189
+ const idents = identifierTerms(opts.query);
1190
+ let onTopic = qt.length === 0;
1191
+ if (qt.length) {
1192
+ const missingIdents = idents.filter((id) => !lc.includes(id.toLowerCase()));
1193
+ if (missingIdents.length) {
1194
+ reasons.push(`API symbol(s) absent: ${missingIdents.join(", ")}`);
1195
+ onTopic = false;
1196
+ } else {
1197
+ const hits = qt.filter((t) => lc.includes(t)).length;
1198
+ const need = Math.max(1, Math.floor(qt.length / 2) + 1);
1199
+ if (hits >= need) {
1200
+ score += Math.min(hits, 2);
1201
+ onTopic = true;
1202
+ } else {
1203
+ reasons.push(hits === 0 ? "query terms absent" : `query terms weak (${hits}/${qt.length})`);
1204
+ onTopic = false;
1205
+ }
1206
+ }
1207
+ }
1208
+ if (opts.symbols?.length) {
1209
+ if (opts.symbols.some((s) => s.length > 2 && lc.includes(s.toLowerCase()))) score += 1;
1210
+ else reasons.push("no API symbols present");
1211
+ }
1212
+ if (opts.readmeOnly && (idents.length > 0 || qt.length >= 2)) {
1213
+ onTopic = false;
1214
+ if (!reasons.includes("README-only source cannot answer a focused API query")) {
1215
+ reasons.push("README-only source cannot answer a focused API query");
1216
+ }
1217
+ score = Math.min(score, 4);
1218
+ }
1219
+ const sufficient = hasCode && tokens >= minTokens && onTopic;
1220
+ return { score, sufficient, reasons };
1221
+ }
1222
+
1223
+ // src/engine/hosted.ts
1224
+ function hostedBase(opts = {}) {
1225
+ const explicit = opts.base || process.env.VIBGRATE_LIB_HOST;
1226
+ if (explicit) return explicit.replace(/\/+$/, "");
1227
+ let host;
1228
+ try {
1229
+ host = resolveIngestHost(opts.region, opts.ingest);
1230
+ } catch {
1231
+ host = "us.ingest.vibgrate.com";
1232
+ }
1233
+ return `https://${host}`;
1234
+ }
1235
+ async function fetchHostedDocs(req, opts = {}) {
1236
+ const f = opts.fetchImpl ?? globalThis.fetch;
1237
+ if (!f) return null;
1238
+ const url = `${hostedBase(opts)}/v1/lib/docs`;
1239
+ const ctrl = new AbortController();
1240
+ const timer = setTimeout(() => ctrl.abort(), opts.timeoutMs ?? 4e3);
1241
+ const headers = { "content-type": "application/json" };
1242
+ if (opts.auth) {
1243
+ headers["Authorization"] = `VibgrateDSN ${opts.auth.keyId}:${opts.auth.secret}`;
1244
+ headers["X-Vibgrate-Timestamp"] = String(Date.now());
1245
+ }
1246
+ try {
1247
+ const res = await f(url, {
1248
+ method: "POST",
1249
+ headers,
1250
+ body: JSON.stringify({
1251
+ name: req.name,
1252
+ targetId: req.targetId,
1253
+ query: req.query,
1254
+ verbosity: req.verbosity,
1255
+ max_tokens: req.maxTokens
1256
+ }),
1257
+ signal: ctrl.signal
1258
+ });
1259
+ if (!res.ok) return null;
1260
+ const data = await res.json();
1261
+ const content = typeof data.content === "string" ? data.content : typeof data.docs === "string" ? data.docs : "";
1262
+ if (!content.trim()) return null;
1263
+ return {
1264
+ content,
1265
+ version: typeof data.version === "string" ? data.version : null,
1266
+ source: "hosted",
1267
+ metadata: data.metadata ?? void 0
1268
+ };
1269
+ } catch {
1270
+ return null;
1271
+ } finally {
1272
+ clearTimeout(timer);
1273
+ }
1274
+ }
1275
+ async function publishPrivateLibrary(req, opts = {}) {
1276
+ const f = opts.fetchImpl ?? globalThis.fetch;
1277
+ if (!f) return { ok: false, status: 0, message: "no network available" };
1278
+ if (!opts.auth) return { ok: false, status: 401, message: "a DSN is required to publish a private library (run `vibgrate login` or set VIBGRATE_DSN)" };
1279
+ const url = `${hostedBase(opts)}/v1/lib/private`;
1280
+ const ctrl = new AbortController();
1281
+ const timer = setTimeout(() => ctrl.abort(), opts.timeoutMs ?? 15e3);
1282
+ try {
1283
+ const res = await f(url, {
1284
+ method: "POST",
1285
+ headers: {
1286
+ "content-type": "application/json",
1287
+ Authorization: `VibgrateDSN ${opts.auth.keyId}:${opts.auth.secret}`,
1288
+ "X-Vibgrate-Timestamp": String(Date.now())
1289
+ },
1290
+ body: JSON.stringify({
1291
+ name: req.name,
1292
+ version: req.version,
1293
+ readme: req.readme,
1294
+ dts: req.dts,
1295
+ compliance: req.compliance,
1296
+ language: req.language,
1297
+ sourceUrl: req.sourceUrl
1298
+ }),
1299
+ signal: ctrl.signal
1300
+ });
1301
+ const data = await res.json().catch(() => ({}));
1302
+ if (!res.ok) return { ok: false, status: res.status, error: typeof data.error === "string" ? data.error : void 0, message: typeof data.message === "string" ? data.message : void 0 };
1303
+ return {
1304
+ ok: true,
1305
+ targetId: String(data.targetId ?? ""),
1306
+ ecosystem: String(data.ecosystem ?? ""),
1307
+ entities: Number(data.entities) || 0,
1308
+ indexedTokens: Number(data.indexedTokens) || 0
1309
+ };
1310
+ } catch (err) {
1311
+ return { ok: false, status: 0, message: err instanceof Error ? err.message : "request failed" };
1312
+ } finally {
1313
+ clearTimeout(timer);
1314
+ }
1315
+ }
1316
+
1317
+ // src/engine/hosted-cache.ts
1318
+ var CACHE_VERSION = "vg-hosted-docs/1";
1319
+ var HOSTED_DOCS_TTL_MS = 24 * 60 * 60 * 1e3;
1320
+ var MAX_ENTRIES = 64;
1321
+ function cachePath(root) {
1322
+ return path4.join(cacheDir(root), "hosted-docs.json");
1323
+ }
1324
+ function hostedDocsCacheKey(req, opts = {}) {
1325
+ const material = JSON.stringify([
1326
+ req.name ?? "",
1327
+ req.targetId ?? "",
1328
+ req.query ?? "",
1329
+ req.verbosity ?? "",
1330
+ req.maxTokens ?? 0,
1331
+ hostedBase(opts),
1332
+ opts.auth?.keyId ?? "anon"
1333
+ ]);
1334
+ return createHash("sha256").update(material).digest("hex");
1335
+ }
1336
+ function loadFile(root) {
1337
+ try {
1338
+ const parsed = JSON.parse(fs.readFileSync(cachePath(root), "utf8"));
1339
+ if (parsed && parsed.version === CACHE_VERSION && parsed.entries && typeof parsed.entries === "object") return parsed;
1340
+ } catch {
1341
+ }
1342
+ return { version: CACHE_VERSION, entries: {} };
1343
+ }
1344
+ function saveFile(root, file) {
1345
+ try {
1346
+ const keys = Object.keys(file.entries);
1347
+ if (keys.length > MAX_ENTRIES) {
1348
+ for (const k of keys.sort((a, b) => file.entries[a].at - file.entries[b].at).slice(0, keys.length - MAX_ENTRIES)) {
1349
+ delete file.entries[k];
1350
+ }
1351
+ }
1352
+ fs.mkdirSync(cacheDir(root), { recursive: true });
1353
+ fs.writeFileSync(cachePath(root), JSON.stringify(file));
1354
+ } catch {
1355
+ }
1356
+ }
1357
+ async function fetchHostedDocsCached(root, req, opts = {}) {
1358
+ const now = opts.now ?? Date.now;
1359
+ const ttl = opts.ttlMs ?? HOSTED_DOCS_TTL_MS;
1360
+ const key = hostedDocsCacheKey(req, opts);
1361
+ const file = loadFile(root);
1362
+ if (!opts.bypass) {
1363
+ const hit = file.entries[key];
1364
+ if (hit && now() - hit.at < ttl && hit.result?.content) {
1365
+ return { ...hit.result, metadata: { ...hit.result.metadata ?? {}, cached: true } };
1366
+ }
1367
+ }
1368
+ const result = await fetchHostedDocs(req, opts);
1369
+ if (result) {
1370
+ file.entries[key] = { at: now(), result };
1371
+ saveFile(root, file);
1372
+ }
1373
+ return result;
1374
+ }
1375
+
1376
+ // src/mcp/response.ts
1377
+ var MAX_RESULT_TOKENS = 25e3;
1378
+ function renderToolResult(result, maxTokens = MAX_RESULT_TOKENS) {
1379
+ return toolResult(clampToBudget(compactResult(result), maxTokens));
1380
+ }
1381
+ function toolResult(result) {
1382
+ return {
1383
+ content: [{ type: "text", text: JSON.stringify(result) }],
1384
+ structuredContent: asStructured(result)
1385
+ };
1386
+ }
1387
+ function compactResult(value) {
1388
+ return prune(value);
1389
+ }
1390
+ function prune(v) {
1391
+ if (Array.isArray(v)) return v.map(prune);
1392
+ if (v && typeof v === "object") {
1393
+ const out = {};
1394
+ for (const [k, raw] of Object.entries(v)) {
1395
+ const pv = prune(raw);
1396
+ if (!isEmpty(pv)) out[k] = pv;
1397
+ }
1398
+ return out;
1399
+ }
1400
+ return v;
1401
+ }
1402
+ function isEmpty(v) {
1403
+ if (v === null || v === void 0 || v === "") return true;
1404
+ if (Array.isArray(v)) return v.length === 0;
1405
+ if (typeof v === "object") return Object.keys(v).length === 0;
1406
+ return false;
1407
+ }
1408
+ function clampToBudget(value, maxTokens = MAX_RESULT_TOKENS) {
1409
+ if (!value || typeof value !== "object" || Array.isArray(value)) return value;
1410
+ if (tokensOf(value) <= maxTokens) return value;
1411
+ const obj2 = { ...value };
1412
+ const truncated = {};
1413
+ for (let guard = 0; guard < 64 && tokensOf(obj2) > maxTokens; guard++) {
1414
+ const arrays = Object.entries(obj2).filter(
1415
+ (e) => Array.isArray(e[1]) && e[1].length > 0
1416
+ );
1417
+ if (arrays.length === 0) break;
1418
+ arrays.sort((a, b) => tokensOf(b[1]) - tokensOf(a[1]));
1419
+ const [field, arr] = arrays[0];
1420
+ const total = truncated[field]?.total ?? arr.length;
1421
+ const shown = Math.max(1, Math.floor(arr.length / 2));
1422
+ obj2[field] = arr.slice(0, shown);
1423
+ truncated[field] = { shown, total };
1424
+ }
1425
+ if (Object.keys(truncated).length) obj2._truncated = truncated;
1426
+ return obj2;
1427
+ }
1428
+ function tokensOf(v) {
1429
+ return countTokens(JSON.stringify(v));
1430
+ }
1431
+ function asStructured(result) {
1432
+ if (result && typeof result === "object" && !Array.isArray(result)) {
1433
+ return result;
1434
+ }
1435
+ return { result };
1436
+ }
1437
+ function boundList(items, max) {
1438
+ if (!Number.isFinite(max) || max < 0 || items.length <= max) {
1439
+ return { items, total: items.length };
1440
+ }
1441
+ return { items: items.slice(0, max), total: items.length };
1442
+ }
1443
+ var NODE_EDGE_CAP = 50;
1444
+
1445
+ // src/mcp/tools.ts
1446
+ var warmEmbedder = null;
1447
+ var bgWarmStarted = false;
1448
+ function warmEmbedderInBackground(local) {
1449
+ if (local || bgWarmStarted || isModelReady()) return;
1450
+ bgWarmStarted = true;
1451
+ loadEmbedder({}).then((e) => {
1452
+ if (e) warmEmbedder = Promise.resolve(e);
1453
+ }).catch(() => {
1454
+ bgWarmStarted = false;
1455
+ });
1456
+ }
1457
+ function readyEmbedder(local) {
1458
+ if (warmEmbedder) return warmEmbedder;
1459
+ if (local) return warmEmbedder = loadEmbedder({ local, noDownload: true });
1460
+ if (isModelReady()) return warmEmbedder = loadEmbedder({ noDownload: true });
1461
+ warmEmbedderInBackground(local);
1462
+ return Promise.resolve(null);
1463
+ }
1464
+ async function retrieve(graph, question, budget, ctx) {
1465
+ let mode = "lexical";
1466
+ try {
1467
+ const q = await withTimeout(
1468
+ (async () => {
1469
+ const embedder = await readyEmbedder(ctx.local);
1470
+ if (!embedder) return null;
1471
+ const nodeVectors = await getNodeEmbeddings(graph, embedder, ctx.root);
1472
+ const r = await queryGraphSemantic(graph, question, { budget, embedder, nodeVectors });
1473
+ mode = `semantic (${embedder.id})`;
1474
+ return r;
1475
+ })(),
1476
+ SEMANTIC_BUDGET_MS,
1477
+ "semantic path over budget"
1478
+ );
1479
+ if (q) return { q, mode };
1480
+ } catch {
1481
+ }
1482
+ return { q: queryGraph(graph, question, { budget }), mode: "lexical" };
1483
+ }
1484
+ var SEMANTIC_BUDGET_MS = (() => {
1485
+ const n = Number(process.env.VG_SEMANTIC_BUDGET_MS);
1486
+ return Number.isFinite(n) && n > 0 ? n : 4e3;
1487
+ })();
1488
+ var obj = (properties, required = []) => ({
1489
+ type: "object",
1490
+ properties,
1491
+ ...required.length ? { required } : {},
1492
+ additionalProperties: false
1493
+ });
1494
+ var RESPONSE_FORMAT = { type: "string", enum: ["concise", "detailed"] };
1495
+ var isDetailed = (args) => args.response_format === "detailed";
1496
+ var CONCISE_MATCHES = 5;
1497
+ var EPISTEMIC_TIERS = ["observed", "name-matched", "declared"];
1498
+ var EDGE_FILTER_SCHEMA = {
1499
+ min_edge_confidence: { type: "number", description: "0..1 \u2014 drop edges below this confidence" },
1500
+ epistemic: { type: "string", enum: EPISTEMIC_TIERS, description: "keep only edges of this evidence tier" }
1501
+ };
1502
+ function edgeRecordFilter(args) {
1503
+ const min = numOrU(args.min_edge_confidence);
1504
+ const tier = EPISTEMIC_TIERS.includes(String(args.epistemic)) ? String(args.epistemic) : void 0;
1505
+ if (min == null && !tier) return () => true;
1506
+ return (x) => (min == null || x.edge.confidence >= min) && (!tier || x.edge.epistemic === tier);
1507
+ }
1508
+ var TOOLS = [
1509
+ {
1510
+ name: "orient",
1511
+ description: "Start here: one call returns the map overview, ranked matches for your question, and the top hit\u2019s blast radius \u2014 replaces summary+query+impact round-trips.",
1512
+ // `query` is a no-description back-compat alias for `question`: the sibling
1513
+ // discovery tools (search_symbols, resolve_library) take `query`, so an agent
1514
+ // naturally reaches for it here too — accepting it turns what was a wasted
1515
+ // "question is required" round-trip into a hit. Neither is `required` (either
1516
+ // satisfies the in-handler check), matching library_docs/resolve_library.
1517
+ inputSchema: obj({
1518
+ question: { type: "string", maxLength: 300 },
1519
+ query: { type: "string", maxLength: 300 },
1520
+ scope: { type: "string", description: "path prefix to orient within" },
1521
+ budget: { type: "number", description: "token budget in detailed mode (default 1500)" },
1522
+ response_format: RESPONSE_FORMAT
1523
+ }),
1524
+ handler: async (graph, args, ctx) => {
1525
+ const question = String(args.question ?? args.query ?? "");
1526
+ if (!question) return { error: "bad_request", message: "question is required" };
1527
+ const budget = numOr(args.budget, 1500);
1528
+ const { q, mode } = await retrieve(graph, question, budget, ctx);
1529
+ const rawScope = String(args.scope ?? "").trim();
1530
+ const scope = rawScope === "." || rawScope === "./" ? "" : rawScope.replace(/^\.\//, "");
1531
+ const scoped = scope ? q.matches.filter((m) => m.node.file.startsWith(scope)) : q.matches;
1532
+ const detailed = isDetailed(args);
1533
+ let topImpact = null;
1534
+ const top = scoped[0]?.node;
1535
+ if (detailed && top) {
1536
+ const r = impactOf(graph, top.id, { depth: 3 });
1537
+ topImpact = { node: top.qualifiedName, direct: r.direct, transitive: r.transitive, affected: r.affected.slice(0, 10).map(stripId) };
1538
+ }
1539
+ const shown = detailed ? scoped : scoped.slice(0, CONCISE_MATCHES);
1540
+ return {
1541
+ // Concise "find X" calls need a one-line map (counts + languages), not the
1542
+ // full topAreas/topHubs blocks — those were ~half of every orient
1543
+ // response's tokens on the hot discovery path and the caller navigates by
1544
+ // `matches`. Detailed keeps the full overview.
1545
+ summary: detailed ? summarize(graph, 10) : conciseSummary(graph),
1546
+ mode,
1547
+ // The fact-annotated context block is detail: concise callers navigate
1548
+ // by the ranked matches and fetch nodes on demand (plan P2).
1549
+ ...detailed ? { context: q.context } : {},
1550
+ matches: shown.map((m) => ({ name: m.node.qualifiedName, kind: m.node.kind, file: m.node.file, line: m.node.span.start, score: m.score })),
1551
+ // A neutral availability flag, not a directive to keep querying: a
1552
+ // retrieval tool that coaches "narrow the question / see more" nudges the
1553
+ // over-navigation that re-bills the whole context on every extra step.
1554
+ ...scoped.length > shown.length ? { moreAvailable: true } : {},
1555
+ ...detailed ? { topImpact } : {}
1556
+ };
1557
+ }
1558
+ },
1559
+ {
1560
+ name: "search_symbols",
1561
+ description: 'Find a known name or literal string fast: ranked symbol lookup, plus a complete literal file-search for any quoted, multi-word, or path-like query (config keys, routes, log lines, UI copy). Quote a single name ("AddJwtBearer") to sweep every occurrence of it as text. A sweep 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.',
1562
+ inputSchema: obj(
1563
+ {
1564
+ query: { type: "string", maxLength: 120, description: "a symbol name, or a literal phrase to sweep for every occurrence of" },
1565
+ limit: { type: "number", description: "default 8, max 50 (raise it to fetch a whole literal sweep in one call)" }
1566
+ },
1567
+ ["query"]
1568
+ ),
1569
+ handler: async (graph, args, ctx) => {
1570
+ const limit = Math.min(50, numOr(args.limit, 8));
1571
+ return searchSymbols(graph, ctx.root, String(args.query ?? ""), limit);
1572
+ }
1573
+ },
1574
+ {
1575
+ name: "query_graph",
1576
+ description: "Find code by meaning when you don\u2019t know the name: symptoms, relationships, what-breaks-if. For a known name or literal string use search_symbols.",
1577
+ // `query` is a no-description back-compat alias for `question` (see orient):
1578
+ // sibling tools take `query`, so accept it here rather than reject the call.
1579
+ inputSchema: obj({
1580
+ question: { type: "string", maxLength: 300 },
1581
+ query: { type: "string", maxLength: 300 },
1582
+ limit: { type: "number", description: "ranked matches to return (default 5)" },
1583
+ offset: { type: "number" },
1584
+ budget: { type: "number", description: "context-block token budget in detailed mode (default 2000)" },
1585
+ response_format: RESPONSE_FORMAT
1586
+ }),
1587
+ handler: async (graph, args, ctx) => {
1588
+ const question = String(args.question ?? args.query ?? "");
1589
+ if (!question) return { mode: "lexical", matches: [], hint: "question (or query) is required" };
1590
+ const budget = numOr(args.budget, 2e3);
1591
+ const { q: r, mode } = await retrieve(graph, question, budget, ctx);
1592
+ if (r.matches.length === 0) {
1593
+ return { mode, matches: [], hint: "no semantic match \u2014 for a known name or literal string use search_symbols; otherwise rephrase around the behaviour you observe" };
1594
+ }
1595
+ const limit = numOr(args.limit, CONCISE_MATCHES);
1596
+ const offset = Math.max(0, numOrU(args.offset) ?? 0);
1597
+ const page = r.matches.slice(offset, offset + limit);
1598
+ return {
1599
+ mode,
1600
+ summary: `${r.matches.length} match${r.matches.length === 1 ? "" : "es"}; top: ${r.matches[0].node.qualifiedName}`,
1601
+ // The fact-annotated context block is detail (plan P2): ranked matches
1602
+ // first, fetch nodes on demand.
1603
+ ...isDetailed(args) ? { context: r.context, tokensEstimate: r.tokensEstimate } : {},
1604
+ matches: page.map((m) => ({ name: m.node.qualifiedName, kind: m.node.kind, file: m.node.file, line: m.node.span.start, score: m.score })),
1605
+ // Neutral pagination fact (nextOffset if the model genuinely needs more),
1606
+ // not a "narrow the question" nudge that invites another discovery round.
1607
+ ...offset + page.length < r.matches.length ? { moreAvailable: true, nextOffset: offset + page.length } : {}
1608
+ };
1609
+ }
1610
+ },
1611
+ {
1612
+ name: "get_node",
1613
+ description: "Inspect one symbol: signature, callers, callees, area. Accepts name, file:line, glob or id.",
1614
+ inputSchema: obj(
1615
+ {
1616
+ name: { type: "string" },
1617
+ pick: { type: "number", description: "nth candidate if ambiguous" },
1618
+ ...EDGE_FILTER_SCHEMA,
1619
+ response_format: RESPONSE_FORMAT
1620
+ },
1621
+ ["name"]
1622
+ ),
1623
+ handler: (graph, args, ctx) => {
1624
+ const { node, candidates } = resolveOne(graph, String(args.name ?? ""), numOrU(args.pick));
1625
+ if (!node) return unresolved(candidates);
1626
+ const index = indexFor(graph);
1627
+ const edgeOk = edgeRecordFilter(args);
1628
+ const calleeRecs = index.callees(node.id).filter(edgeOk);
1629
+ const callerRecs = index.callers(node.id).filter(edgeOk);
1630
+ const base = {
1631
+ name: node.qualifiedName,
1632
+ kind: node.kind,
1633
+ file: node.file,
1634
+ line: node.span.start,
1635
+ signature: node.signature ?? null,
1636
+ importance: node.importance,
1637
+ isHub: node.isHub,
1638
+ area: node.area
1639
+ };
1640
+ if (ctx?.dedup && ctx.seen?.has(node.id)) {
1641
+ return { ...base, repeat: true, callsTotal: uniqueNames2(calleeRecs.map((x) => x.node.qualifiedName)).length, calledByTotal: uniqueNames2(callerRecs.map((x) => x.node.qualifiedName)).length };
1642
+ }
1643
+ const edgeCap = isDetailed(args) ? NODE_EDGE_CAP : CONCISE_MATCHES;
1644
+ const calls = boundList(uniqueNames2(calleeRecs.map((x) => x.node.qualifiedName)), edgeCap);
1645
+ const calledBy = boundList(uniqueNames2(callerRecs.map((x) => x.node.qualifiedName)), edgeCap);
1646
+ ctx?.seen?.add(node.id);
1647
+ return {
1648
+ ...base,
1649
+ calls: calls.items,
1650
+ callsTotal: calls.total,
1651
+ calledBy: calledBy.items,
1652
+ calledByTotal: calledBy.total
1653
+ };
1654
+ }
1655
+ },
1656
+ {
1657
+ name: "find_path",
1658
+ description: "Shortest connection from a to b.",
1659
+ inputSchema: obj(
1660
+ {
1661
+ a: { type: "string" },
1662
+ b: { type: "string" },
1663
+ pick_a: { type: "number" },
1664
+ pick_b: { type: "number" }
1665
+ },
1666
+ ["a", "b"]
1667
+ ),
1668
+ handler: (graph, args) => {
1669
+ const ra = resolveOne(graph, String(args.a ?? ""), numOrU(args.pick_a));
1670
+ const rb = resolveOne(graph, String(args.b ?? ""), numOrU(args.pick_b));
1671
+ if (!ra.node) return { endpoint: "a", ...unresolved(ra.candidates) };
1672
+ if (!rb.node) return { endpoint: "b", ...unresolved(rb.candidates) };
1673
+ const result = shortestPath(graph, ra.node.id, rb.node.id);
1674
+ if (!result) return pathDisconnect(graph, ra.node.id, rb.node.id);
1675
+ const byId = new Map(graph.nodes.map((n) => [n.id, n]));
1676
+ return { connected: true, direction: result.direction, path: result.ids.map((id) => byId.get(id)?.qualifiedName ?? id) };
1677
+ }
1678
+ },
1679
+ {
1680
+ name: "impact_of",
1681
+ description: "Blast radius of a change: what breaks if this symbol changes \u2014 dependents, files, covering tests, risk.",
1682
+ inputSchema: obj(
1683
+ {
1684
+ name: { type: "string" },
1685
+ change_type: { type: "string", enum: ["modify", "delete", "rename", "add_dependency"] },
1686
+ depth: { type: "number", description: "default 4" },
1687
+ pick: { type: "number", description: "nth candidate if ambiguous" },
1688
+ min_edge_confidence: { type: "number", description: "0..1 \u2014 drop dependents below this (decayed) confidence" },
1689
+ response_format: RESPONSE_FORMAT
1690
+ },
1691
+ ["name"]
1692
+ ),
1693
+ handler: (graph, args) => {
1694
+ const { node, candidates } = resolveOne(graph, String(args.name ?? ""), numOrU(args.pick));
1695
+ if (!node) return unresolved(candidates);
1696
+ const changeType = ["modify", "delete", "rename", "add_dependency"].includes(String(args.change_type)) ? String(args.change_type) : "modify";
1697
+ const r = impactOf(graph, node.id, { depth: numOr(args.depth, 4) });
1698
+ const minConf = numOrU(args.min_edge_confidence);
1699
+ if (minConf != null) {
1700
+ const kept = r.affected.filter((a) => a.confidence >= minConf);
1701
+ r.affected = kept;
1702
+ r.direct = kept.filter((i) => i.depth === 1).length;
1703
+ r.transitive = kept.filter((i) => i.depth > 1).length;
1704
+ r.minEdgeConfidence = kept.length ? Math.min(...kept.map((i) => i.confidence)) : 1;
1705
+ }
1706
+ const tests = coveringTests(graph, node);
1707
+ const filesAffected = [...new Set(r.affected.map((a) => a.file))];
1708
+ const heavyChange = changeType === "delete" || changeType === "rename";
1709
+ const riskLevel = node.isHub || r.direct >= 10 || r.transitive >= 50 || heavyChange && r.direct >= 3 ? "high" : r.direct >= 3 || r.transitive >= 10 ? "medium" : "low";
1710
+ const summary = `${changeType} ${r.root.name}: ${r.direct} direct dependent${r.direct === 1 ? "" : "s"}, ${r.transitive} transitive across ${filesAffected.length} file${filesAffected.length === 1 ? "" : "s"}; ${tests.length} covering test${tests.length === 1 ? "" : "s"} \u2014 ${riskLevel} risk.`;
1711
+ return {
1712
+ root: r.root.name,
1713
+ changeType,
1714
+ directCallers: r.direct,
1715
+ transitiveCount: r.transitive,
1716
+ filesAffected: filesAffected.slice(0, 20),
1717
+ testsAffected: tests.length,
1718
+ riskLevel,
1719
+ summary,
1720
+ // The full row set is detail; concise callers act on the contract above.
1721
+ ...isDetailed(args) ? { affected: r.affected.slice(0, 100).map(stripId) } : r.affected.length > 0 ? { hint: 'response_format:"detailed" lists the affected nodes' } : {}
1722
+ };
1723
+ }
1724
+ },
1725
+ {
1726
+ name: "tests_for",
1727
+ description: "Which tests cover this symbol \u2014 is a change here tested?",
1728
+ inputSchema: obj(
1729
+ {
1730
+ name: { type: "string" },
1731
+ pick: { type: "number", description: "nth candidate if ambiguous" },
1732
+ response_format: RESPONSE_FORMAT
1733
+ },
1734
+ ["name"]
1735
+ ),
1736
+ handler: (graph, args) => {
1737
+ const { node, candidates } = resolveOne(graph, String(args.name ?? ""), numOrU(args.pick));
1738
+ if (!node) return unresolved(candidates);
1739
+ const covers = coveringTests(graph, node);
1740
+ return {
1741
+ node: node.qualifiedName,
1742
+ tested: node.tested,
1743
+ coverage: node.coverage ?? null,
1744
+ testCount: covers.length,
1745
+ covers: isDetailed(args) ? covers : covers.slice(0, 10),
1746
+ ...covers.length > 10 && !isDetailed(args) ? { moreAvailable: true } : {},
1747
+ ...covers.length === 0 ? { hint: "no covering tests found \u2014 a change here lands untested" } : {}
1748
+ };
1749
+ }
1750
+ },
1751
+ {
1752
+ name: "get_graph_summary",
1753
+ description: "Code map overview: counts, languages, top areas and hubs.",
1754
+ inputSchema: obj({}),
1755
+ handler: (graph) => summarize(graph)
1756
+ },
1757
+ {
1758
+ name: "list_areas",
1759
+ description: "Code areas (communities) by size.",
1760
+ inputSchema: obj({ limit: { type: "number" } }),
1761
+ // Strip `members` (raw content-hash id arrays): a model cannot use them and
1762
+ // on a large repo they ballooned this result past 20k tokens.
1763
+ handler: (graph, args) => [...graph.areas].sort((a, b) => b.size - a.size).slice(0, numOr(args.limit, 30)).map((a) => ({ id: a.id, label: a.label, size: a.size, cohesion: a.cohesion, externalEdges: a.externalEdges }))
1764
+ },
1765
+ {
1766
+ name: "list_hubs",
1767
+ description: "Most-depended-on symbols (includes precomputed blast-radius counts when available).",
1768
+ inputSchema: obj({ limit: { type: "number" } }),
1769
+ handler: (graph, args) => {
1770
+ const limit = numOr(args.limit, 20);
1771
+ if (graph.summaries?.hubs?.length) {
1772
+ return graph.summaries.hubs.slice(0, limit).map((h) => ({
1773
+ name: h.name,
1774
+ kind: h.kind,
1775
+ file: h.file,
1776
+ importance: h.importance,
1777
+ isHub: true,
1778
+ directDependents: h.direct,
1779
+ depth2Dependents: h.depth2,
1780
+ filesAffected: h.files,
1781
+ tested: h.tested
1782
+ }));
1783
+ }
1784
+ return graph.nodes.filter((n) => n.kind !== "file" && n.kind !== "external").sort((a, b) => b.importance - a.importance).slice(0, limit).map((n) => ({
1785
+ name: n.qualifiedName,
1786
+ kind: n.kind,
1787
+ file: n.file,
1788
+ line: n.span.start,
1789
+ importance: n.importance,
1790
+ isHub: n.isHub
1791
+ }));
1792
+ }
1793
+ },
1794
+ {
1795
+ name: "get_facts",
1796
+ description: "Deterministic facts for a node (contract / invariant / characterization).",
1797
+ inputSchema: obj(
1798
+ {
1799
+ name: { type: "string" },
1800
+ pick: { type: "number", description: "nth candidate if ambiguous" }
1801
+ },
1802
+ ["name"]
1803
+ ),
1804
+ handler: (graph, args) => {
1805
+ const { node, candidates } = resolveOne(graph, String(args.name ?? ""), numOrU(args.pick));
1806
+ if (!node) return unresolved(candidates);
1807
+ if (!graph.facts) {
1808
+ return {
1809
+ node: node.qualifiedName,
1810
+ facts: [],
1811
+ note: "map has no facts \u2014 rebuild with a current vg (facts ship on every build)"
1812
+ };
1813
+ }
1814
+ return { node: node.qualifiedName, facts: graph.facts.filter((f) => f.subjectIds.includes(node.id)) };
1815
+ }
1816
+ },
1817
+ {
1818
+ name: "guide_node",
1819
+ description: "Cited standards/practices for a node (OWASP/CWE).",
1820
+ inputSchema: obj(
1821
+ {
1822
+ name: { type: "string" },
1823
+ pick: { type: "number", description: "nth candidate if ambiguous" }
1824
+ },
1825
+ ["name"]
1826
+ ),
1827
+ handler: (graph, args) => {
1828
+ const { node, candidates } = resolveOne(graph, String(args.name ?? ""), numOrU(args.pick));
1829
+ if (!node) return unresolved(candidates);
1830
+ const entries = new Map(FREE_PACK.entries.map((e) => [e.id, e]));
1831
+ return {
1832
+ node: node.qualifiedName,
1833
+ guidance: (graph.grounding ?? []).filter((g) => g.src === node.id).map((g) => ({ kind: g.kind, rationale: g.rationale, confidence: g.confidence, summary: entries.get(g.packEntryId)?.summary ?? "", citation: g.citation }))
1834
+ };
1835
+ }
1836
+ },
1837
+ {
1838
+ name: "check_drift",
1839
+ description: "Offline dependency inventory (npm/pypi/go); attribute:true adds git who-added attribution.",
1840
+ inputSchema: obj(
1841
+ { attribute: { type: "boolean" } },
1842
+ []
1843
+ ),
1844
+ handler: (_graph, args, ctx) => attributedInventory(ctx.root, { attribute: args.attribute === true })
1845
+ },
1846
+ {
1847
+ name: "vuln_attribution",
1848
+ description: "Who introduced each open vulnerability, exposure windows, and CRA remediation metrics (MTTR, SLA breaches). Reads the last `vg scan --vulns`.",
1849
+ inputSchema: obj({ package: { type: "string", description: "restrict to one package" } }, []),
1850
+ handler: (_graph, args, ctx) => {
1851
+ const data = loadVulnerabilities(ctx.root);
1852
+ if (!data) {
1853
+ return {
1854
+ status: "not_scanned",
1855
+ message: "No vulnerability data found. Run `vg scan --vulns` first (attribution needs git history at scan time)."
1856
+ };
1857
+ }
1858
+ const only = args.package ? String(args.package) : null;
1859
+ const packages = (only ? data.packages.filter((p) => p.package === only) : data.packages).map((p) => ({
1860
+ ecosystem: p.ecosystem,
1861
+ package: p.package,
1862
+ installedVersion: p.version,
1863
+ advisories: p.advisories.map((a) => ({
1864
+ id: a.id,
1865
+ cve: a.aliases.find((x) => x.startsWith("CVE-")) ?? null,
1866
+ severity: a.severity,
1867
+ cvss: a.cvss,
1868
+ exposureDays: a.exposureDays ?? null,
1869
+ introduced: a.introduced ?? null,
1870
+ fixedVersions: a.fixedVersions
1871
+ }))
1872
+ }));
1873
+ return { status: "ok", source: data.source, cra: data.cra ?? null, packages };
1874
+ }
1875
+ },
1876
+ {
1877
+ name: "list_vulnerabilities",
1878
+ description: "Known vulnerabilities from the last `vg scan --vulns`: id/CVE, severity, CVSS, fixed version.",
1879
+ inputSchema: obj(
1880
+ { severity: { type: "string", enum: ["low", "moderate", "high", "critical"], description: "minimum severity" } },
1881
+ []
1882
+ ),
1883
+ handler: (_graph, args, ctx) => {
1884
+ const data = loadVulnerabilities(ctx.root);
1885
+ if (!data) {
1886
+ return {
1887
+ status: "not_scanned",
1888
+ message: "No vulnerability data found. Run `vg scan --vulns` first (online OSV, or offline with a --package-manifest carrying advisories)."
1889
+ };
1890
+ }
1891
+ const filtered = filterBySeverity(data, args.severity);
1892
+ return {
1893
+ status: "ok",
1894
+ source: filtered.source,
1895
+ totalAdvisories: filtered.totalAdvisories,
1896
+ affectedPackages: filtered.packages.length,
1897
+ severityCounts: filtered.severityCounts,
1898
+ packages: filtered.packages.map((p) => ({
1899
+ ecosystem: p.ecosystem,
1900
+ package: p.package,
1901
+ installedVersion: p.version,
1902
+ advisories: p.advisories.map((a) => ({
1903
+ id: a.id,
1904
+ cve: a.aliases.find((x) => x.startsWith("CVE-")) ?? null,
1905
+ severity: a.severity,
1906
+ cvss: a.cvss,
1907
+ fixedVersions: a.fixedVersions,
1908
+ summary: a.summary
1909
+ }))
1910
+ }))
1911
+ };
1912
+ }
1913
+ },
1914
+ {
1915
+ name: "upgrade_impact",
1916
+ description: "What breaks if you upgrade a package: major distance, import blast radius, vulns fixed, recommended posture. changelog:true adds GitHub breaking-change signals (online).",
1917
+ inputSchema: obj(
1918
+ {
1919
+ package: { type: "string" },
1920
+ changelog: { type: "boolean" }
1921
+ },
1922
+ ["package"]
1923
+ ),
1924
+ handler: async (_graph, args, ctx) => {
1925
+ const pkg = String(args.package ?? "");
1926
+ if (!pkg) return { error: "bad_request", message: "package is required" };
1927
+ const target = resolvePackageTarget(ctx.root, pkg);
1928
+ const fixesVulnerabilities = openFixableAdvisories(ctx.root, pkg);
1929
+ const impact = computeUpgradeImpact(ctx.root, { package: pkg, ...target }, { fixesVulnerabilities });
1930
+ const changelog = args.changelog === true && !ctx.local && target.ecosystem !== "unknown" ? await getChangelogSignals(target.ecosystem, pkg, target.currentVersion, target.latestVersion) : void 0;
1931
+ return { status: "ok", ...impact, ...changelog ? { changelog } : {} };
1932
+ }
1933
+ },
1934
+ {
1935
+ name: "list_models",
1936
+ description: "Local models on disk (Ollama / LM Studio / gguf).",
1937
+ inputSchema: obj({}),
1938
+ handler: () => ({ models: discoverModels() })
1939
+ },
1940
+ {
1941
+ name: "resolve_library",
1942
+ description: "Resolve a library to its canonical id and the version YOUR project uses (drift-annotated). Call once per library and reuse the returned targetId for every library_docs follow-up \u2014 never guess an id.",
1943
+ // Canonical §4: `query` (+ optional `context_project`). `name` kept as a back-compat alias.
1944
+ inputSchema: obj(
1945
+ {
1946
+ query: {
1947
+ type: "string",
1948
+ description: 'the package name as written in the dependency file. Good: "react-hook-form". Bad: "forms"'
1949
+ },
1950
+ name: { type: "string" },
1951
+ context_project: { type: "string" }
1952
+ },
1953
+ []
1954
+ ),
1955
+ handler: (_graph, args, ctx) => {
1956
+ const query = String(args.query ?? args.name ?? "");
1957
+ if (!query) return { error: "bad_request", message: "query is required" };
1958
+ const ver = resolveVersion(ctx.root, query);
1959
+ const entry = resolveLib(loadCatalog(ctx.root), query);
1960
+ if (entry) {
1961
+ const drift = driftFor(ctx.root, entry);
1962
+ return {
1963
+ targetId: entry.id,
1964
+ id: entry.id,
1965
+ // back-compat
1966
+ name: entry.name,
1967
+ resolvedVersion: ver.served ?? entry.version,
1968
+ version: entry.version,
1969
+ // back-compat
1970
+ isInstalled: ver.installed != null,
1971
+ driftStatus: driftStatusOf(drift),
1972
+ drift,
1973
+ version_mismatch: ver.mismatch ?? null,
1974
+ source: "catalog"
1975
+ };
1976
+ }
1977
+ const local = localPackageDocs(ctx.root, query);
1978
+ if (local) {
1979
+ return {
1980
+ targetId: query,
1981
+ name: query,
1982
+ resolvedVersion: local.version ?? ver.served ?? null,
1983
+ isInstalled: true,
1984
+ driftStatus: "unknown",
1985
+ version_mismatch: ver.mismatch ?? null,
1986
+ source: local.source
1987
+ };
1988
+ }
1989
+ return { error: "not_found" };
1990
+ }
1991
+ },
1992
+ {
1993
+ name: "library_docs",
1994
+ description: "Version-correct usage docs for a library, sliced to a token budget \u2014 official content matched to the version this project has installed, not web-search results. Ask a focused question via query; one call usually answers. After 2 calls without the section you need, stop and read the package source under node_modules instead of searching again.",
1995
+ // Canonical §4: `targetId`/`query`, `verbosity`, `max_tokens`. `name`/`tokens` kept as aliases.
1996
+ inputSchema: obj(
1997
+ {
1998
+ targetId: { type: "string", description: "id from resolve_library (preferred over query)" },
1999
+ query: {
2000
+ type: "string",
2001
+ description: 'the library plus the specific need. Good: "zod refine custom error message". Bad: "zod" or "docs"'
2002
+ },
2003
+ name: { type: "string" },
2004
+ context_project: { type: "string" },
2005
+ verbosity: { type: "string", enum: ["concise", "balanced", "exhaustive"] },
2006
+ // Tolerant on purpose: models often send budgets as strings — the
2007
+ // handler coerces, so don't fail validation over "4000" vs 4000.
2008
+ max_tokens: { type: ["number", "string"], description: "token budget (a numeric string is accepted)" },
2009
+ tokens: { type: ["number", "string"] },
2010
+ enterprise_strict: { type: "boolean" },
2011
+ follow_up: { type: "boolean" }
2012
+ },
2013
+ []
2014
+ ),
2015
+ handler: (_graph, args, ctx) => {
2016
+ const id = String(args.targetId ?? args.query ?? args.name ?? "");
2017
+ if (!id) return { error: "bad_request", message: "targetId or query is required" };
2018
+ const query = String(args.query ?? args.name ?? "");
2019
+ const verbosity = ["concise", "balanced", "exhaustive"].includes(String(args.verbosity)) ? String(args.verbosity) : query ? "concise" : "balanced";
2020
+ const budget = numOrU(args.max_tokens) ?? numOrU(args.tokens) ?? VERBOSITY_BUDGET[verbosity];
2021
+ const ver = resolveVersion(ctx.root, id);
2022
+ const focusQuery = (() => {
2023
+ const q = query.trim();
2024
+ if (!q) return q;
2025
+ const bare = id.replace(/^@[^/]+\//, "").toLowerCase();
2026
+ const parts = q.split(/\s+/);
2027
+ if (parts.length > 1 && parts[0].toLowerCase().replace(/^@[^/]+\//, "") === bare) {
2028
+ return parts.slice(1).join(" ");
2029
+ }
2030
+ return q;
2031
+ })();
2032
+ const render = (readme, libName, readmeOnly) => {
2033
+ const apiSurface = localApiSurface(ctx.root, libName, focusQuery || void 0);
2034
+ const sel = selectForBudget({ readme, query: focusQuery || query, apiSurface, budget });
2035
+ const quality = assessDocQuality(sel.text, {
2036
+ name: libName,
2037
+ query: focusQuery || query || void 0,
2038
+ symbols: symbolsFromApi(apiSurface),
2039
+ readmeOnly: readmeOnly === true && !apiSurface
2040
+ });
2041
+ return {
2042
+ content: sel.text,
2043
+ metadata: {
2044
+ verbosity,
2045
+ tokens: sel.tokens,
2046
+ truncated: sel.truncated,
2047
+ quality: { sufficient: quality.sufficient, score: quality.score, reasons: quality.reasons },
2048
+ escalate: quality.sufficient ? null : "hosted"
2049
+ }
2050
+ };
2051
+ };
2052
+ const entry = resolveLib(loadCatalog(ctx.root), id);
2053
+ let answer = null;
2054
+ let sufficient = false;
2055
+ if (entry) {
2056
+ const r = render(readDoc(ctx.root, entry), entry.name, false);
2057
+ sufficient = r.metadata.quality.sufficient;
2058
+ answer = {
2059
+ targetId: entry.id,
2060
+ name: entry.name,
2061
+ version: entry.version,
2062
+ source: "catalog",
2063
+ drift: driftFor(ctx.root, entry),
2064
+ version_mismatch: ver.mismatch ?? null,
2065
+ content: r.content,
2066
+ docs: r.content,
2067
+ // back-compat
2068
+ metadata: r.metadata
2069
+ };
2070
+ } else {
2071
+ const local = localPackageDocs(ctx.root, id, { query: focusQuery || query || void 0 });
2072
+ if (local) {
2073
+ const r = render(local.docs, id, local.readmeOnly);
2074
+ sufficient = r.metadata.quality.sufficient;
2075
+ answer = {
2076
+ targetId: id,
2077
+ name: id,
2078
+ version: local.version ?? ver.served,
2079
+ source: local.source,
2080
+ version_mismatch: ver.mismatch ?? null,
2081
+ content: r.content,
2082
+ docs: r.content,
2083
+ // back-compat
2084
+ metadata: r.metadata
2085
+ };
2086
+ }
2087
+ }
2088
+ if (!sufficient && !ctx.local) {
2089
+ return (async () => {
2090
+ const dsn = resolveDsn();
2091
+ const parsed = dsn ? parseDsn(dsn) : null;
2092
+ const hosted = await fetchHostedDocsCached(
2093
+ ctx.root,
2094
+ { name: entry?.name ?? id, targetId: entry?.id, query: query || void 0, maxTokens: budget },
2095
+ { auth: parsed ? { keyId: parsed.keyId, secret: parsed.secret } : void 0 }
2096
+ );
2097
+ if (hosted) {
2098
+ return {
2099
+ targetId: entry?.id ?? id,
2100
+ name: entry?.name ?? id,
2101
+ version: hosted.version ?? ver.served ?? null,
2102
+ source: "hosted",
2103
+ version_mismatch: ver.mismatch ?? null,
2104
+ content: hosted.content,
2105
+ docs: hosted.content,
2106
+ // back-compat
2107
+ metadata: { verbosity, escalated: true, ...hosted.metadata ?? {} }
2108
+ };
2109
+ }
2110
+ return answer ?? { error: "not_found" };
2111
+ })();
2112
+ }
2113
+ return answer ?? { error: "not_found" };
2114
+ }
2115
+ }
2116
+ ];
2117
+ var VERBOSITY_BUDGET = { concise: 1500, balanced: 4e3, exhaustive: 12e3 };
2118
+ function conciseSummary(graph) {
2119
+ return { counts: graph.meta.counts, languages: graph.meta.languages };
2120
+ }
2121
+ function summarize(graph, top = 10) {
2122
+ const topHubs = graph.summaries?.hubs?.length ? graph.summaries.hubs.slice(0, top).map((h) => ({
2123
+ name: h.name,
2124
+ file: h.file,
2125
+ importance: h.importance,
2126
+ directDependents: h.direct,
2127
+ depth2Dependents: h.depth2
2128
+ })) : graph.nodes.filter((n) => n.isHub).sort((a, b) => b.importance - a.importance).slice(0, top).map((n) => ({ name: n.qualifiedName, file: n.file, importance: n.importance }));
2129
+ return {
2130
+ counts: graph.meta.counts,
2131
+ languages: graph.meta.languages,
2132
+ cluster: graph.meta.cluster,
2133
+ analysisTier: graph.meta.analysisTier ?? "full",
2134
+ resolver: graph.provenance.resolver,
2135
+ generatedAt: graph.generatedAt,
2136
+ topAreas: [...graph.areas].sort((a, b) => b.size - a.size).slice(0, top).map((a) => ({ id: a.id, label: a.label, size: a.size })),
2137
+ topHubs
2138
+ };
2139
+ }
2140
+ function stripId(item) {
2141
+ const { id: _id, ...rest } = item;
2142
+ return rest;
2143
+ }
2144
+ function unresolved(candidates) {
2145
+ return {
2146
+ error: candidates.length ? "ambiguous" : "not_found",
2147
+ candidates: candidates.slice(0, 10).map((n, i) => ({ pick: i + 1, name: n.qualifiedName, kind: n.kind, file: n.file, line: n.span.start })),
2148
+ ...candidates.length ? { hint: "call again with pick:<n> or a file:line name" } : {}
2149
+ };
2150
+ }
2151
+ function driftStatusOf(d) {
2152
+ if (d.drift === "current") return "current";
2153
+ if (d.drift === "behind") return "outdated";
2154
+ if (d.drift === "ahead") return "ahead";
2155
+ return "unknown";
2156
+ }
2157
+ function numOr(v, def) {
2158
+ const n = Number(v);
2159
+ return Number.isFinite(n) && n > 0 ? n : def;
2160
+ }
2161
+ function numOrU(v) {
2162
+ const n = Number(v);
2163
+ return Number.isFinite(n) ? n : void 0;
2164
+ }
2165
+ function uniqueNames2(xs) {
2166
+ return [...new Set(xs)];
2167
+ }
2168
+ var HOT_TOOLS = ["orient", "search_symbols", "query_graph", "get_node"];
2169
+ function deferredToolNames() {
2170
+ const hot = new Set(HOT_TOOLS);
2171
+ return TOOLS.map((t) => t.name).filter((n) => !hot.has(n));
2172
+ }
2173
+ function navigationToolsetConfig(serverName = "vibgrate") {
2174
+ return {
2175
+ type: "mcp_toolset",
2176
+ mcp_server_name: serverName,
2177
+ default_config: { defer_loading: true },
2178
+ configs: Object.fromEntries(HOT_TOOLS.map((n) => [n, { defer_loading: false }]))
2179
+ };
2180
+ }
2181
+
2182
+ export { FREE_PACK, HOT_TOOLS, TOOLS, assessDocQuality, availableRegionIds, clearStoredCredentials, countTokens, coveringTests, createWorkspaceDsn, credentialsPath, dashHostForIngestHost, deferredToolNames, detectRunner, dsnCommand, ensureGitignored, fetchHostedDocsCached, findGitRoot, gitignoreEntryForCredentials, homeCredentialsPath, hostedBase, ingestHostForRegionId, navigationToolsetConfig, parseDsn, pathDisconnect, projectCredentialsPath, publishPrivateLibrary, pushCommand, readScanArtifact, readStoredCredentials, renderToolResult, resolveDsn, resolveIngestHost, selectForBudget, shortestPath, symbolsFromApi, testsToRun, uploadScanArtifact, warmEmbedderInBackground, writeStoredCredentials };
2183
+ //# sourceMappingURL=chunk-VBVY733D.js.map
2184
+ //# sourceMappingURL=chunk-VBVY733D.js.map