@vibgrate/cli 2026.721.3 → 2026.722.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DOCS.md +185 -0
- package/README.md +2 -1
- package/dist/baseline-BD3A7EXD.js +7 -0
- package/dist/{baseline-37G4TINM.js.map → baseline-BD3A7EXD.js.map} +1 -1
- package/dist/chunk-2O5YZVZV.js +417 -0
- package/dist/chunk-2O5YZVZV.js.map +1 -0
- package/dist/{chunk-ELUFCQDR.js → chunk-3MXNNBLI.js} +240 -2519
- package/dist/chunk-3MXNNBLI.js.map +1 -0
- package/dist/{chunk-JLRF5TFA.js → chunk-5BDHQ7DD.js} +42 -71
- package/dist/chunk-5BDHQ7DD.js.map +1 -0
- package/dist/chunk-6CXTPC74.js +234 -0
- package/dist/chunk-6CXTPC74.js.map +1 -0
- package/dist/chunk-CRAMOEBE.js +675 -0
- package/dist/chunk-CRAMOEBE.js.map +1 -0
- package/dist/chunk-CS37OBE3.js +861 -0
- package/dist/chunk-CS37OBE3.js.map +1 -0
- package/dist/chunk-CT5BS56U.js +23 -0
- package/dist/chunk-CT5BS56U.js.map +1 -0
- package/dist/chunk-GGJZA3Q6.js +961 -0
- package/dist/chunk-GGJZA3Q6.js.map +1 -0
- package/dist/chunk-I7GY7MVN.js +833 -0
- package/dist/chunk-I7GY7MVN.js.map +1 -0
- package/dist/chunk-JBXNQCGE.js +484 -0
- package/dist/chunk-JBXNQCGE.js.map +1 -0
- package/dist/chunk-JFGYT6BI.js +32 -0
- package/dist/chunk-JFGYT6BI.js.map +1 -0
- package/dist/chunk-JYS4OBN2.js +148 -0
- package/dist/chunk-JYS4OBN2.js.map +1 -0
- package/dist/chunk-K3SLGULW.js +50 -0
- package/dist/chunk-K3SLGULW.js.map +1 -0
- package/dist/chunk-LEPTUB4H.js +108 -0
- package/dist/chunk-LEPTUB4H.js.map +1 -0
- package/dist/{chunk-5PLKADD6.js → chunk-NONYJLOJ.js} +4 -3
- package/dist/chunk-NONYJLOJ.js.map +1 -0
- package/dist/{chunk-IKJBELUV.js → chunk-PY3DNX5H.js} +4 -29
- package/dist/chunk-PY3DNX5H.js.map +1 -0
- package/dist/chunk-WNIIKCNF.js +972 -0
- package/dist/chunk-WNIIKCNF.js.map +1 -0
- package/dist/cli.js +2512 -1224
- package/dist/cli.js.map +1 -1
- package/dist/fs-KCABDURV.js +3 -0
- package/dist/fs-KCABDURV.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +8 -3
- package/dist/index.js.map +1 -1
- package/dist/interactive-A4DISSIW.js +681 -0
- package/dist/interactive-A4DISSIW.js.map +1 -0
- package/dist/mcp-tools-I6PME3AV.js +3 -0
- package/dist/mcp-tools-I6PME3AV.js.map +1 -0
- package/dist/parse-worker.js +2 -1
- package/dist/parse-worker.js.map +1 -1
- package/dist/session-POJUDFPH.js +6 -0
- package/dist/session-POJUDFPH.js.map +1 -0
- package/dist/session-store-W5RZZHA3.js +3 -0
- package/dist/session-store-W5RZZHA3.js.map +1 -0
- package/dist/stream-json-Q2JGWPHB.js +65 -0
- package/dist/stream-json-Q2JGWPHB.js.map +1 -0
- package/dist/ui-HOVOPWMX.js +4 -0
- package/dist/ui-HOVOPWMX.js.map +1 -0
- package/package.json +1 -1
- package/dist/baseline-37G4TINM.js +0 -6
- package/dist/chunk-5PLKADD6.js.map +0 -1
- package/dist/chunk-ELUFCQDR.js.map +0 -1
- package/dist/chunk-IKJBELUV.js.map +0 -1
- package/dist/chunk-JLRF5TFA.js.map +0 -1
|
@@ -0,0 +1,972 @@
|
|
|
1
|
+
import { hashString } from './chunk-JFGYT6BI.js';
|
|
2
|
+
import * as fs3 from 'fs';
|
|
3
|
+
import { createRequire } from 'module';
|
|
4
|
+
import * as os from 'os';
|
|
5
|
+
import * as path3 from 'path';
|
|
6
|
+
import { pathToFileURL } from 'url';
|
|
7
|
+
|
|
8
|
+
// src/engine/serialize.ts
|
|
9
|
+
function serializeGraph(graph) {
|
|
10
|
+
return `${stableStringify(graph, 2)}
|
|
11
|
+
`;
|
|
12
|
+
}
|
|
13
|
+
function stableStringify(value, indent = 2) {
|
|
14
|
+
return JSON.stringify(sortKeys(value), null, indent);
|
|
15
|
+
}
|
|
16
|
+
function sortKeys(value) {
|
|
17
|
+
if (Array.isArray(value)) return value.map(sortKeys);
|
|
18
|
+
if (value && typeof value === "object") {
|
|
19
|
+
const input = value;
|
|
20
|
+
const out = {};
|
|
21
|
+
for (const key of Object.keys(input).sort()) {
|
|
22
|
+
const v = input[key];
|
|
23
|
+
if (v === void 0) continue;
|
|
24
|
+
out[key] = sortKeys(v);
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
function parseGraph(json) {
|
|
31
|
+
return JSON.parse(json);
|
|
32
|
+
}
|
|
33
|
+
var CACHE_VERSION = "vg-parse-cache/3";
|
|
34
|
+
function cacheDir(root) {
|
|
35
|
+
return path3.join(root, ".vibgrate", "cache");
|
|
36
|
+
}
|
|
37
|
+
function cachePath(root) {
|
|
38
|
+
return path3.join(cacheDir(root), "parse-cache.json");
|
|
39
|
+
}
|
|
40
|
+
function loadCache(root, opts) {
|
|
41
|
+
const file = cachePath(root);
|
|
42
|
+
let data = {
|
|
43
|
+
version: CACHE_VERSION,
|
|
44
|
+
toolVersion: opts.toolVersion,
|
|
45
|
+
grammars: opts.grammars,
|
|
46
|
+
entries: {}
|
|
47
|
+
};
|
|
48
|
+
if (!opts.disabled && fs3.existsSync(file)) {
|
|
49
|
+
try {
|
|
50
|
+
const loaded = JSON.parse(fs3.readFileSync(file, "utf8"));
|
|
51
|
+
if (loaded.version === CACHE_VERSION && loaded.toolVersion === opts.toolVersion && loaded.grammars === opts.grammars && loaded.entries) {
|
|
52
|
+
data = loaded;
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
get(rel, hash) {
|
|
59
|
+
const entry = data.entries[rel];
|
|
60
|
+
return entry && entry.hash === hash ? entry.parse : void 0;
|
|
61
|
+
},
|
|
62
|
+
set(rel, parse) {
|
|
63
|
+
data.entries[rel] = { hash: parse.hash, parse };
|
|
64
|
+
},
|
|
65
|
+
prune(currentRels) {
|
|
66
|
+
for (const rel of Object.keys(data.entries)) {
|
|
67
|
+
if (!currentRels.has(rel)) delete data.entries[rel];
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
save() {
|
|
71
|
+
fs3.mkdirSync(cacheDir(root), { recursive: true });
|
|
72
|
+
fs3.writeFileSync(file, stableStringify(data, 0));
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
var DEFAULT_LOCK_STALE_MS = 15 * 60 * 1e3;
|
|
77
|
+
function isProcessAlive(pid) {
|
|
78
|
+
try {
|
|
79
|
+
process.kill(pid, 0);
|
|
80
|
+
return true;
|
|
81
|
+
} catch (e) {
|
|
82
|
+
return e.code === "EPERM";
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function lockIsStale(file, staleMs) {
|
|
86
|
+
try {
|
|
87
|
+
const { pid, at } = JSON.parse(fs3.readFileSync(file, "utf8"));
|
|
88
|
+
if (typeof at === "number" && Date.now() - at > staleMs) return true;
|
|
89
|
+
if (typeof pid === "number" && !isProcessAlive(pid)) return true;
|
|
90
|
+
return false;
|
|
91
|
+
} catch {
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function acquireLock(file, staleMs = DEFAULT_LOCK_STALE_MS) {
|
|
96
|
+
const write = () => {
|
|
97
|
+
try {
|
|
98
|
+
fs3.mkdirSync(path3.dirname(file), { recursive: true });
|
|
99
|
+
const fd = fs3.openSync(file, "wx");
|
|
100
|
+
fs3.writeFileSync(fd, JSON.stringify({ pid: process.pid, at: Date.now() }));
|
|
101
|
+
fs3.closeSync(fd);
|
|
102
|
+
return true;
|
|
103
|
+
} catch {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
if (write()) return true;
|
|
108
|
+
if (lockIsStale(file, staleMs)) {
|
|
109
|
+
try {
|
|
110
|
+
fs3.rmSync(file, { force: true });
|
|
111
|
+
} catch {
|
|
112
|
+
}
|
|
113
|
+
return write();
|
|
114
|
+
}
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
function releaseLock(file) {
|
|
118
|
+
try {
|
|
119
|
+
fs3.rmSync(file, { force: true });
|
|
120
|
+
} catch {
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/engine/embeddings.ts
|
|
125
|
+
function resolveEmbedModel(model) {
|
|
126
|
+
return model ?? "bge-small-en-v1.5";
|
|
127
|
+
}
|
|
128
|
+
function modelCacheDir() {
|
|
129
|
+
const base = process.env.XDG_CACHE_HOME || path3.join(os.homedir(), ".cache");
|
|
130
|
+
return path3.join(base, "vibgrate", "models");
|
|
131
|
+
}
|
|
132
|
+
function modelReadyMarker(modelId) {
|
|
133
|
+
return path3.join(modelCacheDir(), `.ready-${safe(modelId)}`);
|
|
134
|
+
}
|
|
135
|
+
function isModelReady(modelId = resolveEmbedModel()) {
|
|
136
|
+
return fs3.existsSync(modelReadyMarker(modelId));
|
|
137
|
+
}
|
|
138
|
+
function unavailableMessage(reason) {
|
|
139
|
+
switch (reason) {
|
|
140
|
+
case "no-permission":
|
|
141
|
+
return `semantic search couldn't write its model cache at ${modelCacheDir()} \u2014 using fast lexical search instead. Relocate it with the standard XDG_CACHE_HOME, or run with --local to stay lexical quietly. (No admin/sudo is ever needed \u2014 the cache lives in your home folder.)`;
|
|
142
|
+
case "download-failed":
|
|
143
|
+
return `the semantic model isn't downloaded yet and couldn't be fetched (offline?) \u2014 using fast lexical search. It'll try again next time; run with --local to keep lexical without retrying.`;
|
|
144
|
+
case "not-installed":
|
|
145
|
+
return `the optional semantic backend isn't available here \u2014 using fast lexical search.`;
|
|
146
|
+
case "init-failed":
|
|
147
|
+
default:
|
|
148
|
+
return `the semantic model couldn't load \u2014 using fast lexical search.`;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function modelCacheInfo(modelId = resolveEmbedModel()) {
|
|
152
|
+
const dir = modelCacheDir();
|
|
153
|
+
return { dir, present: isModelReady(modelId) || fs3.existsSync(dir), bytes: dirSize(dir) };
|
|
154
|
+
}
|
|
155
|
+
function clearModelCache() {
|
|
156
|
+
const dir = modelCacheDir();
|
|
157
|
+
const bytes = dirSize(dir);
|
|
158
|
+
try {
|
|
159
|
+
fs3.rmSync(dir, { recursive: true, force: true });
|
|
160
|
+
} catch {
|
|
161
|
+
}
|
|
162
|
+
return bytes;
|
|
163
|
+
}
|
|
164
|
+
function dirSize(dir) {
|
|
165
|
+
let total = 0;
|
|
166
|
+
let entries;
|
|
167
|
+
try {
|
|
168
|
+
entries = fs3.readdirSync(dir, { withFileTypes: true });
|
|
169
|
+
} catch {
|
|
170
|
+
return 0;
|
|
171
|
+
}
|
|
172
|
+
for (const e of entries) {
|
|
173
|
+
const p = path3.join(dir, e.name);
|
|
174
|
+
if (e.isDirectory()) total += dirSize(p);
|
|
175
|
+
else {
|
|
176
|
+
try {
|
|
177
|
+
total += fs3.statSync(p).size;
|
|
178
|
+
} catch {
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return total;
|
|
183
|
+
}
|
|
184
|
+
function isPermissionError(e) {
|
|
185
|
+
const code = e?.code;
|
|
186
|
+
return code === "EACCES" || code === "EPERM" || code === "EROFS";
|
|
187
|
+
}
|
|
188
|
+
function embeddingsCached(root, modelId) {
|
|
189
|
+
return fs3.existsSync(path3.join(cacheDir(root), `embeddings-${safe(modelId)}.json`));
|
|
190
|
+
}
|
|
191
|
+
async function loadEmbedder(options = {}) {
|
|
192
|
+
if (options.local) return null;
|
|
193
|
+
const modelId = resolveEmbedModel(options.model);
|
|
194
|
+
if (options.noDownload && !isModelReady(modelId)) return null;
|
|
195
|
+
const fail = (reason) => {
|
|
196
|
+
options.onUnavailable?.(reason);
|
|
197
|
+
return null;
|
|
198
|
+
};
|
|
199
|
+
const mod = await importEmbedBackend();
|
|
200
|
+
if (!mod?.FlagEmbedding) return fail("not-installed");
|
|
201
|
+
const cache = modelCacheDir();
|
|
202
|
+
try {
|
|
203
|
+
fs3.mkdirSync(cache, { recursive: true });
|
|
204
|
+
} catch (e) {
|
|
205
|
+
return fail(isPermissionError(e) ? "no-permission" : "init-failed");
|
|
206
|
+
}
|
|
207
|
+
try {
|
|
208
|
+
const model = await withTimeout(
|
|
209
|
+
mod.FlagEmbedding.init({
|
|
210
|
+
model: mapModel(mod, modelId),
|
|
211
|
+
cacheDir: cache,
|
|
212
|
+
showDownloadProgress: options.showDownloadProgress ?? false
|
|
213
|
+
}),
|
|
214
|
+
embedInitTimeoutMs(),
|
|
215
|
+
"embedding model init timed out"
|
|
216
|
+
);
|
|
217
|
+
try {
|
|
218
|
+
fs3.writeFileSync(modelReadyMarker(modelId), (/* @__PURE__ */ new Date(0)).toISOString());
|
|
219
|
+
} catch {
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
id: modelId,
|
|
223
|
+
async embed(texts) {
|
|
224
|
+
const out = [];
|
|
225
|
+
for await (const batch of model.embed(texts, 64)) {
|
|
226
|
+
for (const v of batch) out.push(Array.from(v));
|
|
227
|
+
}
|
|
228
|
+
return out;
|
|
229
|
+
},
|
|
230
|
+
async embedQuery(text) {
|
|
231
|
+
const v = await model.queryEmbed(text);
|
|
232
|
+
return Array.from(v);
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
} catch (e) {
|
|
236
|
+
return fail(isPermissionError(e) ? "no-permission" : "download-failed");
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
async function importEmbedBackend() {
|
|
240
|
+
const hostDir = process.env.VIBGRATE_EMBEDDER_PATH;
|
|
241
|
+
if (hostDir) {
|
|
242
|
+
try {
|
|
243
|
+
const req = createRequire(path3.join(hostDir, "package.json"));
|
|
244
|
+
const mod = await import(pathToFileURL(req.resolve("fastembed")).href);
|
|
245
|
+
const resolved = mod?.FlagEmbedding ? mod : mod?.default;
|
|
246
|
+
if (resolved?.FlagEmbedding) return resolved;
|
|
247
|
+
} catch {
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return await import('fastembed').catch(() => null);
|
|
251
|
+
}
|
|
252
|
+
function embedInitTimeoutMs() {
|
|
253
|
+
const n = Number(process.env.VG_EMBED_TIMEOUT_MS);
|
|
254
|
+
return Number.isFinite(n) && n > 0 ? n : 6e4;
|
|
255
|
+
}
|
|
256
|
+
async function withTimeout(promise, ms, message) {
|
|
257
|
+
let timer;
|
|
258
|
+
try {
|
|
259
|
+
return await Promise.race([
|
|
260
|
+
promise,
|
|
261
|
+
new Promise((_, reject) => {
|
|
262
|
+
timer = setTimeout(() => reject(new Error(message)), ms);
|
|
263
|
+
timer.unref?.();
|
|
264
|
+
})
|
|
265
|
+
]);
|
|
266
|
+
} finally {
|
|
267
|
+
clearTimeout(timer);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
function mapModel(mod, id) {
|
|
271
|
+
const m = mod.EmbeddingModel ?? {};
|
|
272
|
+
if (/bge-small/i.test(id)) return m.BGESmallENV15 ?? m.BGESmallEN ?? id;
|
|
273
|
+
if (/bge-base/i.test(id)) return m.BGEBaseENV15 ?? id;
|
|
274
|
+
if (/all-minilm/i.test(id)) return m.AllMiniLML6V2 ?? id;
|
|
275
|
+
return id;
|
|
276
|
+
}
|
|
277
|
+
var PATH_NOISE = /* @__PURE__ */ new Set([
|
|
278
|
+
"src",
|
|
279
|
+
"app",
|
|
280
|
+
"apps",
|
|
281
|
+
"lib",
|
|
282
|
+
"libs",
|
|
283
|
+
"dist",
|
|
284
|
+
"build",
|
|
285
|
+
"out",
|
|
286
|
+
"index",
|
|
287
|
+
"main",
|
|
288
|
+
"packages",
|
|
289
|
+
"pkg",
|
|
290
|
+
"test",
|
|
291
|
+
"tests",
|
|
292
|
+
"__tests__",
|
|
293
|
+
"spec",
|
|
294
|
+
"node_modules",
|
|
295
|
+
"internal",
|
|
296
|
+
"com",
|
|
297
|
+
"example",
|
|
298
|
+
"demo",
|
|
299
|
+
"java",
|
|
300
|
+
"net",
|
|
301
|
+
"org",
|
|
302
|
+
"www"
|
|
303
|
+
]);
|
|
304
|
+
function pathContext(file) {
|
|
305
|
+
const parts = file.split("/");
|
|
306
|
+
const stem = (parts.pop() ?? "").replace(/\.[^.]+$/, "");
|
|
307
|
+
const dirs = parts.filter((p) => p && !PATH_NOISE.has(p.toLowerCase()));
|
|
308
|
+
return [...dirs.slice(-2), stem].filter(Boolean).join(" ");
|
|
309
|
+
}
|
|
310
|
+
function nodeEmbedText(node, areaLabel) {
|
|
311
|
+
return [node.qualifiedName, node.kind, node.signature ?? "", node.doc ?? "", pathContext(node.file), areaLabel ?? ""].filter(Boolean).join(" ");
|
|
312
|
+
}
|
|
313
|
+
function cosine(a, b) {
|
|
314
|
+
let dot = 0;
|
|
315
|
+
let na = 0;
|
|
316
|
+
let nb = 0;
|
|
317
|
+
const n = Math.min(a.length, b.length);
|
|
318
|
+
for (let i = 0; i < n; i++) {
|
|
319
|
+
dot += a[i] * b[i];
|
|
320
|
+
na += a[i] * a[i];
|
|
321
|
+
nb += b[i] * b[i];
|
|
322
|
+
}
|
|
323
|
+
if (na === 0 || nb === 0) return 0;
|
|
324
|
+
return dot / (Math.sqrt(na) * Math.sqrt(nb));
|
|
325
|
+
}
|
|
326
|
+
var EMBED_CHUNK = 256;
|
|
327
|
+
var CACHE_WRITE_INTERVAL_MS = 1500;
|
|
328
|
+
function vectorCachePath(root, modelId) {
|
|
329
|
+
return path3.join(cacheDir(root), `embeddings-${safe(modelId)}.json`);
|
|
330
|
+
}
|
|
331
|
+
function countPending(graph, root, modelId) {
|
|
332
|
+
const entries = readCacheEntries(vectorCachePath(root, modelId), modelId);
|
|
333
|
+
const areaLabel = new Map(graph.areas.map((a) => [a.id, a.label]));
|
|
334
|
+
let pending = 0;
|
|
335
|
+
for (const n of graph.nodes) {
|
|
336
|
+
if (n.kind === "file" || n.kind === "external") continue;
|
|
337
|
+
const h = hashString(nodeEmbedText(n, areaLabel.get(n.area)));
|
|
338
|
+
const cached = entries[n.id];
|
|
339
|
+
if (!cached || cached.hash !== h) pending++;
|
|
340
|
+
}
|
|
341
|
+
return pending;
|
|
342
|
+
}
|
|
343
|
+
function readCacheEntries(file, modelId) {
|
|
344
|
+
if (!fs3.existsSync(file)) return {};
|
|
345
|
+
try {
|
|
346
|
+
const loaded = JSON.parse(fs3.readFileSync(file, "utf8"));
|
|
347
|
+
if (loaded.model === modelId && loaded.entries) return loaded.entries;
|
|
348
|
+
} catch {
|
|
349
|
+
}
|
|
350
|
+
return {};
|
|
351
|
+
}
|
|
352
|
+
async function getNodeEmbeddings(graph, embedder, root, onProgress) {
|
|
353
|
+
const file = vectorCachePath(root, embedder.id);
|
|
354
|
+
let cache = { model: embedder.id, entries: {} };
|
|
355
|
+
if (fs3.existsSync(file)) {
|
|
356
|
+
try {
|
|
357
|
+
const loaded = JSON.parse(fs3.readFileSync(file, "utf8"));
|
|
358
|
+
if (loaded.model === embedder.id && loaded.entries) cache = loaded;
|
|
359
|
+
} catch {
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
const areaLabel = new Map(graph.areas.map((a) => [a.id, a.label]));
|
|
363
|
+
const targets = graph.nodes.filter((n) => n.kind !== "file" && n.kind !== "external");
|
|
364
|
+
const toEmbed = [];
|
|
365
|
+
const vectors = /* @__PURE__ */ new Map();
|
|
366
|
+
for (const n of targets) {
|
|
367
|
+
const text = nodeEmbedText(n, areaLabel.get(n.area));
|
|
368
|
+
const h = hashString(text);
|
|
369
|
+
const cached = cache.entries[n.id];
|
|
370
|
+
if (cached && cached.hash === h) vectors.set(n.id, cached.vec);
|
|
371
|
+
else toEmbed.push({ id: n.id, text, hash: h });
|
|
372
|
+
}
|
|
373
|
+
const persist = () => {
|
|
374
|
+
try {
|
|
375
|
+
fs3.mkdirSync(cacheDir(root), { recursive: true });
|
|
376
|
+
const tmp = `${file}.tmp.${process.pid}`;
|
|
377
|
+
fs3.writeFileSync(tmp, JSON.stringify(cache));
|
|
378
|
+
fs3.renameSync(tmp, file);
|
|
379
|
+
} catch {
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
if (toEmbed.length) {
|
|
383
|
+
const lock = `${file}.lock`;
|
|
384
|
+
if (!acquireLock(lock)) return vectors;
|
|
385
|
+
try {
|
|
386
|
+
onProgress?.(0, toEmbed.length);
|
|
387
|
+
let lastWrite = Date.now();
|
|
388
|
+
for (let i = 0; i < toEmbed.length; i += EMBED_CHUNK) {
|
|
389
|
+
const slice = toEmbed.slice(i, i + EMBED_CHUNK);
|
|
390
|
+
const vecs = await embedder.embed(slice.map((t) => t.text));
|
|
391
|
+
slice.forEach((t, j) => {
|
|
392
|
+
const vec = vecs[j] ?? [];
|
|
393
|
+
vectors.set(t.id, vec);
|
|
394
|
+
cache.entries[t.id] = { hash: t.hash, vec };
|
|
395
|
+
});
|
|
396
|
+
onProgress?.(Math.min(i + EMBED_CHUNK, toEmbed.length), toEmbed.length);
|
|
397
|
+
if (Date.now() - lastWrite >= CACHE_WRITE_INTERVAL_MS) {
|
|
398
|
+
persist();
|
|
399
|
+
lastWrite = Date.now();
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
const live = new Set(targets.map((n) => n.id));
|
|
403
|
+
for (const id of Object.keys(cache.entries)) if (!live.has(id)) delete cache.entries[id];
|
|
404
|
+
persist();
|
|
405
|
+
} finally {
|
|
406
|
+
releaseLock(lock);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return vectors;
|
|
410
|
+
}
|
|
411
|
+
function safe(id) {
|
|
412
|
+
return id.replace(/[^a-z0-9.-]+/gi, "_");
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// src/engine/relations.ts
|
|
416
|
+
var GraphIndex = class {
|
|
417
|
+
constructor(graph) {
|
|
418
|
+
this.graph = graph;
|
|
419
|
+
for (const n of graph.nodes) this.nodeById.set(n.id, n);
|
|
420
|
+
for (const e of graph.edges) {
|
|
421
|
+
push(this.outById, e.src, e);
|
|
422
|
+
push(this.inById, e.dst, e);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
graph;
|
|
426
|
+
nodeById = /* @__PURE__ */ new Map();
|
|
427
|
+
outById = /* @__PURE__ */ new Map();
|
|
428
|
+
inById = /* @__PURE__ */ new Map();
|
|
429
|
+
out(id, kind) {
|
|
430
|
+
const all = this.outById.get(id) ?? [];
|
|
431
|
+
return kind ? all.filter((e) => e.kind === kind) : all;
|
|
432
|
+
}
|
|
433
|
+
in(id, kind) {
|
|
434
|
+
const all = this.inById.get(id) ?? [];
|
|
435
|
+
return kind ? all.filter((e) => e.kind === kind) : all;
|
|
436
|
+
}
|
|
437
|
+
node(id) {
|
|
438
|
+
return this.nodeById.get(id);
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Resolved nodes called by `id` — invocations (`call`) plus structural
|
|
442
|
+
* dependency references (`references`, e.g. a constructor-injected field's
|
|
443
|
+
* type), since both represent real usage a caller/impact question cares
|
|
444
|
+
* about. `references` is otherwise only emitted by the precise SCIP/tsc
|
|
445
|
+
* rungs and (for Java DI wiring) the heuristic rung — never a guess.
|
|
446
|
+
*/
|
|
447
|
+
callees(id) {
|
|
448
|
+
return this.resolveTargets(this.out(id).filter(isUsageEdge), "dst");
|
|
449
|
+
}
|
|
450
|
+
/** Resolved nodes that call or structurally reference `id`. */
|
|
451
|
+
callers(id) {
|
|
452
|
+
return this.resolveTargets(this.in(id).filter(isUsageEdge), "src");
|
|
453
|
+
}
|
|
454
|
+
resolveTargets(edges, end) {
|
|
455
|
+
const out = [];
|
|
456
|
+
for (const e of edges) {
|
|
457
|
+
const node = this.nodeById.get(e[end]);
|
|
458
|
+
if (node) out.push({ edge: e, node });
|
|
459
|
+
}
|
|
460
|
+
return out;
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
var USAGE_KINDS = /* @__PURE__ */ new Set(["call", "references"]);
|
|
464
|
+
function isUsageEdge(e) {
|
|
465
|
+
return USAGE_KINDS.has(e.kind);
|
|
466
|
+
}
|
|
467
|
+
function push(map, key, value) {
|
|
468
|
+
const list = map.get(key);
|
|
469
|
+
if (list) list.push(value);
|
|
470
|
+
else map.set(key, [value]);
|
|
471
|
+
}
|
|
472
|
+
var INDEX_CACHE = /* @__PURE__ */ new WeakMap();
|
|
473
|
+
function indexFor(graph) {
|
|
474
|
+
let idx = INDEX_CACHE.get(graph);
|
|
475
|
+
if (!idx) {
|
|
476
|
+
idx = new GraphIndex(graph);
|
|
477
|
+
INDEX_CACHE.set(graph, idx);
|
|
478
|
+
}
|
|
479
|
+
return idx;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// src/engine/query.ts
|
|
483
|
+
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
484
|
+
"the",
|
|
485
|
+
"a",
|
|
486
|
+
"an",
|
|
487
|
+
"is",
|
|
488
|
+
"are",
|
|
489
|
+
"was",
|
|
490
|
+
"were",
|
|
491
|
+
"be",
|
|
492
|
+
"to",
|
|
493
|
+
"of",
|
|
494
|
+
"in",
|
|
495
|
+
"on",
|
|
496
|
+
"for",
|
|
497
|
+
"and",
|
|
498
|
+
"or",
|
|
499
|
+
"where",
|
|
500
|
+
"what",
|
|
501
|
+
"which",
|
|
502
|
+
"how",
|
|
503
|
+
"do",
|
|
504
|
+
"does",
|
|
505
|
+
"did",
|
|
506
|
+
"i",
|
|
507
|
+
"we",
|
|
508
|
+
"it",
|
|
509
|
+
"this",
|
|
510
|
+
"that",
|
|
511
|
+
"with",
|
|
512
|
+
"when",
|
|
513
|
+
"who",
|
|
514
|
+
"why",
|
|
515
|
+
"can",
|
|
516
|
+
"should",
|
|
517
|
+
"my",
|
|
518
|
+
"our",
|
|
519
|
+
"you",
|
|
520
|
+
"your",
|
|
521
|
+
"from",
|
|
522
|
+
"by",
|
|
523
|
+
"at",
|
|
524
|
+
"as",
|
|
525
|
+
// Discovery-question scaffolding: words a caller uses to FRAME the ask
|
|
526
|
+
// ("find the code responsible for X", "I need to modify X, where do I
|
|
527
|
+
// start?", "explain how X works in this codebase") rather than to name the
|
|
528
|
+
// target. Left in, these compete on equal footing with the real identifier
|
|
529
|
+
// terms and can outrank it outright — e.g. "find the code responsible for
|
|
530
|
+
// deleteAsync" let "find" alone drag in every FindByIdAsync/FindAll method
|
|
531
|
+
// in the repo, none of them the target (VG-LOCATE-FAILURE-ANALYSIS.md).
|
|
532
|
+
"find",
|
|
533
|
+
"code",
|
|
534
|
+
"responsible",
|
|
535
|
+
"need",
|
|
536
|
+
"modify",
|
|
537
|
+
"me",
|
|
538
|
+
"implementation",
|
|
539
|
+
"explain",
|
|
540
|
+
"works",
|
|
541
|
+
"codebase",
|
|
542
|
+
"contains",
|
|
543
|
+
"file"
|
|
544
|
+
]);
|
|
545
|
+
function queryGraph(graph, question, options = {}) {
|
|
546
|
+
const budget = options.budget ?? 2e3;
|
|
547
|
+
const limit = options.limit ?? 12;
|
|
548
|
+
const terms = tokenize(question);
|
|
549
|
+
const weightOf = termWeights(graph, terms);
|
|
550
|
+
const index = indexFor(graph);
|
|
551
|
+
const scored = [];
|
|
552
|
+
for (const node of graph.nodes) {
|
|
553
|
+
if (node.kind === "file" || node.kind === "external") continue;
|
|
554
|
+
const { score, why } = scoreNode(node, terms, weightOf);
|
|
555
|
+
if (score > 0) scored.push({ node, score: round(score * (1 + IMPORTANCE_WEIGHT * node.importance)), why });
|
|
556
|
+
}
|
|
557
|
+
scored.sort((a, b) => b.score - a.score || a.node.qualifiedName.localeCompare(b.node.qualifiedName));
|
|
558
|
+
const seeds = scored.slice(0, limit);
|
|
559
|
+
const { context, tokensEstimate } = buildContext(graph, index, question, seeds, budget);
|
|
560
|
+
return { question, matches: seeds, context, tokensEstimate };
|
|
561
|
+
}
|
|
562
|
+
async function queryGraphSemantic(graph, question, options) {
|
|
563
|
+
const budget = options.budget ?? 2e3;
|
|
564
|
+
const limit = options.limit ?? 12;
|
|
565
|
+
const terms = tokenize(question);
|
|
566
|
+
const weightOf = termWeights(graph, terms);
|
|
567
|
+
const index = indexFor(graph);
|
|
568
|
+
const queryVec = await options.embedder.embedQuery(question);
|
|
569
|
+
const lexRaw = /* @__PURE__ */ new Map();
|
|
570
|
+
let lexMax = 0;
|
|
571
|
+
const whyById = /* @__PURE__ */ new Map();
|
|
572
|
+
for (const node of graph.nodes) {
|
|
573
|
+
if (node.kind === "file" || node.kind === "external") continue;
|
|
574
|
+
const { score, why } = scoreNode(node, terms, weightOf);
|
|
575
|
+
lexRaw.set(node.id, score);
|
|
576
|
+
whyById.set(node.id, why);
|
|
577
|
+
if (score > lexMax) lexMax = score;
|
|
578
|
+
}
|
|
579
|
+
const scored = [];
|
|
580
|
+
for (const node of graph.nodes) {
|
|
581
|
+
if (node.kind === "file" || node.kind === "external") continue;
|
|
582
|
+
const lexNorm = lexMax > 0 ? (lexRaw.get(node.id) ?? 0) / lexMax : 0;
|
|
583
|
+
const vec = options.nodeVectors.get(node.id);
|
|
584
|
+
const sem = vec ? Math.max(0, cosine(queryVec, vec)) : 0;
|
|
585
|
+
const hybrid = 0.5 * lexNorm + 0.5 * sem;
|
|
586
|
+
if (hybrid <= 0) continue;
|
|
587
|
+
const lexWhy = whyById.get(node.id);
|
|
588
|
+
const why = lexWhy || (sem > 0.3 ? `semantic match (${sem.toFixed(2)})` : "weak match");
|
|
589
|
+
scored.push({ node, score: round(hybrid * (1 + IMPORTANCE_WEIGHT * node.importance)), why });
|
|
590
|
+
}
|
|
591
|
+
scored.sort((a, b) => b.score - a.score || a.node.qualifiedName.localeCompare(b.node.qualifiedName));
|
|
592
|
+
const seeds = scored.slice(0, limit);
|
|
593
|
+
const { context, tokensEstimate } = buildContext(graph, index, question, seeds, budget);
|
|
594
|
+
return { question, matches: seeds, context, tokensEstimate };
|
|
595
|
+
}
|
|
596
|
+
function tokenize(q) {
|
|
597
|
+
return [
|
|
598
|
+
...new Set(
|
|
599
|
+
q.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter((t) => t.length >= 1 && !STOPWORDS.has(t))
|
|
600
|
+
)
|
|
601
|
+
];
|
|
602
|
+
}
|
|
603
|
+
function scoreNode(node, terms, weightOf = () => 1) {
|
|
604
|
+
let score = 0;
|
|
605
|
+
const hits = [];
|
|
606
|
+
const name = node.name.toLowerCase();
|
|
607
|
+
const qn = node.qualifiedName.toLowerCase();
|
|
608
|
+
const file = node.file.toLowerCase();
|
|
609
|
+
const nameParts = identifierParts(node.name);
|
|
610
|
+
for (const t of terms) {
|
|
611
|
+
const w = weightOf(t);
|
|
612
|
+
if (name === t) {
|
|
613
|
+
score += 10 * w;
|
|
614
|
+
hits.push(t);
|
|
615
|
+
} else if (nameParts.has(t)) {
|
|
616
|
+
score += 6 * w;
|
|
617
|
+
hits.push(t);
|
|
618
|
+
} else if (name.includes(t)) {
|
|
619
|
+
score += 4 * w;
|
|
620
|
+
hits.push(t);
|
|
621
|
+
} else if (qn.includes(t)) {
|
|
622
|
+
score += 3 * w;
|
|
623
|
+
hits.push(t);
|
|
624
|
+
} else if (fuzzyPartMatch(t, nameParts)) {
|
|
625
|
+
score += 2 * w;
|
|
626
|
+
hits.push(`~${t}`);
|
|
627
|
+
} else if (file.includes(t)) {
|
|
628
|
+
score += 1 * w;
|
|
629
|
+
hits.push(t);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
return { score, why: hits.length ? `matched: ${hits.join(", ")}` : "" };
|
|
633
|
+
}
|
|
634
|
+
var IMPORTANCE_WEIGHT = 0.4;
|
|
635
|
+
function termWeights(graph, terms) {
|
|
636
|
+
if (terms.length === 0) return () => 1;
|
|
637
|
+
const df = /* @__PURE__ */ new Map();
|
|
638
|
+
let n = 0;
|
|
639
|
+
for (const node of graph.nodes) {
|
|
640
|
+
if (node.kind === "file" || node.kind === "external") continue;
|
|
641
|
+
n++;
|
|
642
|
+
const name = node.name.toLowerCase();
|
|
643
|
+
const parts = identifierParts(node.name);
|
|
644
|
+
for (const t of terms) {
|
|
645
|
+
if (parts.has(t) || name.includes(t)) df.set(t, (df.get(t) ?? 0) + 1);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
const w = /* @__PURE__ */ new Map();
|
|
649
|
+
for (const t of terms) {
|
|
650
|
+
const idf = Math.log((n + 1) / ((df.get(t) ?? 0) + 1)) + 1;
|
|
651
|
+
w.set(t, Math.max(0.5, Math.min(8, idf)));
|
|
652
|
+
}
|
|
653
|
+
return (t) => w.get(t) ?? 1;
|
|
654
|
+
}
|
|
655
|
+
function identifierParts(name) {
|
|
656
|
+
return new Set(
|
|
657
|
+
name.split(/[^\p{L}\p{N}]+|(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])/u).filter(Boolean).map((s) => s.toLowerCase())
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
function fuzzyPartMatch(term, parts) {
|
|
661
|
+
for (const part of parts) {
|
|
662
|
+
const shared = sharedPrefixLen(term, part);
|
|
663
|
+
if (shared >= 5 && shared >= 0.6 * Math.min(term.length, part.length)) return true;
|
|
664
|
+
}
|
|
665
|
+
return false;
|
|
666
|
+
}
|
|
667
|
+
function sharedPrefixLen(a, b) {
|
|
668
|
+
const n = Math.min(a.length, b.length);
|
|
669
|
+
let i = 0;
|
|
670
|
+
while (i < n && a[i] === b[i]) i++;
|
|
671
|
+
return i;
|
|
672
|
+
}
|
|
673
|
+
function buildContext(graph, index, question, seeds, budget) {
|
|
674
|
+
const lines = [];
|
|
675
|
+
lines.push(`# Context for: ${question}`);
|
|
676
|
+
lines.push("");
|
|
677
|
+
if (seeds.length === 0) {
|
|
678
|
+
lines.push("_No matching symbols found in the map. Try different terms, or `vg hubs` for the most important code._");
|
|
679
|
+
const text2 = lines.join("\n");
|
|
680
|
+
return { context: text2, tokensEstimate: estimateTokens(text2) };
|
|
681
|
+
}
|
|
682
|
+
for (const { node, why } of seeds) {
|
|
683
|
+
const block = [];
|
|
684
|
+
block.push(`## ${node.qualifiedName} (${node.kind}, ${node.file}:${node.span.start})`);
|
|
685
|
+
if (node.signature) block.push("`" + node.signature + "`");
|
|
686
|
+
const callees = index.callees(node.id).map((x) => x.node.qualifiedName);
|
|
687
|
+
const callers = index.callers(node.id).map((x) => x.node.qualifiedName);
|
|
688
|
+
if (callees.length) block.push(`calls: ${unique(callees).slice(0, 8).join(", ")}`);
|
|
689
|
+
if (callers.length) block.push(`called by: ${unique(callers).slice(0, 8).join(", ")}`);
|
|
690
|
+
block.push(`importance ${node.importance.toFixed(3)} \xB7 area #${node.area}${node.isHub ? " \xB7 hub" : ""} \xB7 ${why}`);
|
|
691
|
+
block.push("");
|
|
692
|
+
const candidate = lines.concat(block).join("\n");
|
|
693
|
+
if (estimateTokens(candidate) > budget && lines.length > 2) break;
|
|
694
|
+
lines.push(...block);
|
|
695
|
+
}
|
|
696
|
+
const text = lines.join("\n");
|
|
697
|
+
return { context: text, tokensEstimate: estimateTokens(text) };
|
|
698
|
+
}
|
|
699
|
+
function unique(xs) {
|
|
700
|
+
return [...new Set(xs)];
|
|
701
|
+
}
|
|
702
|
+
function estimateTokens(text) {
|
|
703
|
+
return Math.ceil(text.length / 4);
|
|
704
|
+
}
|
|
705
|
+
function round(x) {
|
|
706
|
+
return Math.round(x * 1e6) / 1e6;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// src/engine/impact.ts
|
|
710
|
+
var DEPEND_EDGES = /* @__PURE__ */ new Set(["call", "import", "extends", "implements", "references"]);
|
|
711
|
+
function impactOf(graph, rootId, opts = {}) {
|
|
712
|
+
const maxDepth = Math.max(1, opts.depth ?? 4);
|
|
713
|
+
const index = indexFor(graph);
|
|
714
|
+
const root = index.node(rootId);
|
|
715
|
+
const affected = /* @__PURE__ */ new Map();
|
|
716
|
+
let minEdgeConfidence = 1;
|
|
717
|
+
let frontier = [{ id: rootId, conf: 1 }];
|
|
718
|
+
for (let depth = 1; depth <= maxDepth; depth++) {
|
|
719
|
+
const nextFrontier = [];
|
|
720
|
+
for (const { id, conf } of frontier) {
|
|
721
|
+
for (const e of index.in(id)) {
|
|
722
|
+
if (!DEPEND_EDGES.has(e.kind)) continue;
|
|
723
|
+
minEdgeConfidence = Math.min(minEdgeConfidence, e.confidence);
|
|
724
|
+
const dependentId = e.src;
|
|
725
|
+
if (dependentId === rootId || affected.has(dependentId)) continue;
|
|
726
|
+
const node = index.node(dependentId);
|
|
727
|
+
if (!node) continue;
|
|
728
|
+
const conf2 = round2(conf * e.confidence * 0.9);
|
|
729
|
+
affected.set(dependentId, {
|
|
730
|
+
id: node.id,
|
|
731
|
+
name: node.qualifiedName,
|
|
732
|
+
kind: node.kind,
|
|
733
|
+
file: node.file,
|
|
734
|
+
line: node.span.start,
|
|
735
|
+
depth,
|
|
736
|
+
confidence: conf2
|
|
737
|
+
});
|
|
738
|
+
nextFrontier.push({ id: dependentId, conf: conf2 });
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
frontier = nextFrontier;
|
|
742
|
+
if (frontier.length === 0) break;
|
|
743
|
+
}
|
|
744
|
+
const items = [...affected.values()].sort(
|
|
745
|
+
(a, b) => a.depth - b.depth || b.confidence - a.confidence || a.name.localeCompare(b.name)
|
|
746
|
+
);
|
|
747
|
+
return {
|
|
748
|
+
root: { id: rootId, name: root?.qualifiedName ?? rootId },
|
|
749
|
+
depth: maxDepth,
|
|
750
|
+
affected: items,
|
|
751
|
+
direct: items.filter((i) => i.depth === 1).length,
|
|
752
|
+
transitive: items.filter((i) => i.depth > 1).length,
|
|
753
|
+
minEdgeConfidence: round2(minEdgeConfidence)
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
function round2(x) {
|
|
757
|
+
return Math.round(x * 1e6) / 1e6;
|
|
758
|
+
}
|
|
759
|
+
var LEDGER = "savings.jsonl";
|
|
760
|
+
var PER_FILE_TOKENS = 400;
|
|
761
|
+
var SAVINGS_TOOLS = /* @__PURE__ */ new Set(["query_graph", "get_node", "search_symbols"]);
|
|
762
|
+
var CLI_TOOL_ALIASES = {
|
|
763
|
+
ask: "query_graph",
|
|
764
|
+
show: "get_node",
|
|
765
|
+
impact: "impact_of",
|
|
766
|
+
path: "find_path",
|
|
767
|
+
hubs: "list_hubs",
|
|
768
|
+
areas: "list_areas",
|
|
769
|
+
tree: "tree"
|
|
770
|
+
};
|
|
771
|
+
function sanitizeClient(name) {
|
|
772
|
+
if (typeof name !== "string") return "unknown";
|
|
773
|
+
const cleaned = name.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
|
|
774
|
+
return cleaned || "unknown";
|
|
775
|
+
}
|
|
776
|
+
function sanitizeModelId(name) {
|
|
777
|
+
if (typeof name !== "string") return void 0;
|
|
778
|
+
const cleaned = name.trim().toLowerCase().replace(/[^a-z0-9._:/-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60);
|
|
779
|
+
return cleaned || void 0;
|
|
780
|
+
}
|
|
781
|
+
function recordCliCall(root, entry, now) {
|
|
782
|
+
recordSaving(
|
|
783
|
+
root,
|
|
784
|
+
{
|
|
785
|
+
tool: entry.tool,
|
|
786
|
+
source: "cli",
|
|
787
|
+
client: sanitizeClient(entry.client),
|
|
788
|
+
provider: entry.provider ? sanitizeClient(entry.provider) : void 0,
|
|
789
|
+
model: sanitizeModelId(entry.model),
|
|
790
|
+
outcome: entry.outcome,
|
|
791
|
+
vgTokens: entry.vgTokens ?? 0,
|
|
792
|
+
baselineTokens: (entry.baselineFiles ?? 0) * PER_FILE_TOKENS
|
|
793
|
+
},
|
|
794
|
+
now
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
function savingsLedgerPath(root) {
|
|
798
|
+
return path3.join(cacheDir(root), LEDGER);
|
|
799
|
+
}
|
|
800
|
+
function ledgerPath(root) {
|
|
801
|
+
return savingsLedgerPath(root);
|
|
802
|
+
}
|
|
803
|
+
function savingsRecorded(root) {
|
|
804
|
+
return fs3.existsSync(ledgerPath(root));
|
|
805
|
+
}
|
|
806
|
+
function clearSavings(root) {
|
|
807
|
+
const file = ledgerPath(root);
|
|
808
|
+
const existed = fs3.existsSync(file);
|
|
809
|
+
fs3.rmSync(file, { force: true });
|
|
810
|
+
fs3.rmSync(path3.join(cacheDir(root), "stats-share.json"), { force: true });
|
|
811
|
+
return existed;
|
|
812
|
+
}
|
|
813
|
+
function recordSaving(root, entry, now) {
|
|
814
|
+
try {
|
|
815
|
+
fs3.mkdirSync(cacheDir(root), { recursive: true });
|
|
816
|
+
const line = JSON.stringify({ ts: now, ...entry });
|
|
817
|
+
fs3.appendFileSync(ledgerPath(root), line + "\n");
|
|
818
|
+
} catch {
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
var DEFAULT_RATE_PER_M = 3;
|
|
822
|
+
var DEFAULT_RATE_LABEL = "input @ $3/1M";
|
|
823
|
+
function readSavings(root, days, now, ratePerM = DEFAULT_RATE_PER_M) {
|
|
824
|
+
const file = ledgerPath(root);
|
|
825
|
+
const cutoff = now - days * 24 * 60 * 60 * 1e3;
|
|
826
|
+
let queries = 0;
|
|
827
|
+
let vgTokens = 0;
|
|
828
|
+
let baselineTokens = 0;
|
|
829
|
+
if (fs3.existsSync(file)) {
|
|
830
|
+
for (const line of fs3.readFileSync(file, "utf8").split("\n")) {
|
|
831
|
+
if (!line.trim()) continue;
|
|
832
|
+
try {
|
|
833
|
+
const e = JSON.parse(line);
|
|
834
|
+
if (e.ts < cutoff) continue;
|
|
835
|
+
if (!SAVINGS_TOOLS.has(e.tool)) continue;
|
|
836
|
+
queries++;
|
|
837
|
+
vgTokens += e.vgTokens;
|
|
838
|
+
baselineTokens += e.baselineTokens;
|
|
839
|
+
} catch {
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
const estCostVg = vgTokens / 1e6 * ratePerM;
|
|
844
|
+
const estCostBaseline = baselineTokens / 1e6 * ratePerM;
|
|
845
|
+
return {
|
|
846
|
+
enabled: savingsRecorded(root),
|
|
847
|
+
days,
|
|
848
|
+
queries,
|
|
849
|
+
vgTokens,
|
|
850
|
+
baselineTokens,
|
|
851
|
+
ratio: vgTokens > 0 ? Math.round(baselineTokens / vgTokens * 100) / 100 : 0,
|
|
852
|
+
estCostVg: round22(estCostVg),
|
|
853
|
+
estCostBaseline: round22(estCostBaseline),
|
|
854
|
+
saved: round22(estCostBaseline - estCostVg),
|
|
855
|
+
rateLabel: DEFAULT_RATE_LABEL
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
function round22(x) {
|
|
859
|
+
return Math.round(x * 100) / 100;
|
|
860
|
+
}
|
|
861
|
+
function readModelSavings(root, days, now, ratePerM = DEFAULT_RATE_PER_M) {
|
|
862
|
+
const file = ledgerPath(root);
|
|
863
|
+
const cutoff = now - days * 24 * 60 * 60 * 1e3;
|
|
864
|
+
const byModel = /* @__PURE__ */ new Map();
|
|
865
|
+
if (fs3.existsSync(file)) {
|
|
866
|
+
for (const line of fs3.readFileSync(file, "utf8").split("\n")) {
|
|
867
|
+
if (!line.trim()) continue;
|
|
868
|
+
try {
|
|
869
|
+
const e = JSON.parse(line);
|
|
870
|
+
if (e.ts < cutoff || !e.model || !SAVINGS_TOOLS.has(e.tool)) continue;
|
|
871
|
+
const provider = e.provider ?? "unknown";
|
|
872
|
+
const key = `${provider}/${e.model}`;
|
|
873
|
+
const row = byModel.get(key) ?? { provider, model: e.model, queries: 0, vg: 0, base: 0 };
|
|
874
|
+
row.queries++;
|
|
875
|
+
row.vg += e.vgTokens;
|
|
876
|
+
row.base += e.baselineTokens;
|
|
877
|
+
byModel.set(key, row);
|
|
878
|
+
} catch {
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
return [...byModel.entries()].map(([key, r]) => ({
|
|
883
|
+
key,
|
|
884
|
+
provider: r.provider,
|
|
885
|
+
model: r.model,
|
|
886
|
+
queries: r.queries,
|
|
887
|
+
vgTokens: r.vg,
|
|
888
|
+
baselineTokens: r.base,
|
|
889
|
+
saved: round22((r.base - r.vg) / 1e6 * ratePerM)
|
|
890
|
+
})).sort((a, b) => b.saved - a.saved || a.key.localeCompare(b.key));
|
|
891
|
+
}
|
|
892
|
+
function toDimensionStats(byKey) {
|
|
893
|
+
return [...byKey.entries()].map(([key, r]) => {
|
|
894
|
+
const calls = r.complete + r.partial + r.miss;
|
|
895
|
+
return {
|
|
896
|
+
key,
|
|
897
|
+
calls,
|
|
898
|
+
complete: r.complete,
|
|
899
|
+
partial: r.partial,
|
|
900
|
+
miss: r.miss,
|
|
901
|
+
successPct: calls ? Math.round((r.complete + r.partial) / calls * 100) : 0
|
|
902
|
+
};
|
|
903
|
+
}).sort((a, b) => b.calls - a.calls || a.key.localeCompare(b.key));
|
|
904
|
+
}
|
|
905
|
+
function readUsage(root, days, now) {
|
|
906
|
+
const file = ledgerPath(root);
|
|
907
|
+
const cutoff = now - days * 24 * 60 * 60 * 1e3;
|
|
908
|
+
const byTool = /* @__PURE__ */ new Map();
|
|
909
|
+
const bySource = /* @__PURE__ */ new Map();
|
|
910
|
+
const byClient = /* @__PURE__ */ new Map();
|
|
911
|
+
const msByTool = /* @__PURE__ */ new Map();
|
|
912
|
+
const bump = (m, key, outcome) => {
|
|
913
|
+
const row = m.get(key) ?? { complete: 0, partial: 0, miss: 0 };
|
|
914
|
+
row[outcome]++;
|
|
915
|
+
m.set(key, row);
|
|
916
|
+
};
|
|
917
|
+
if (fs3.existsSync(file)) {
|
|
918
|
+
for (const line of fs3.readFileSync(file, "utf8").split("\n")) {
|
|
919
|
+
if (!line.trim()) continue;
|
|
920
|
+
try {
|
|
921
|
+
const e = JSON.parse(line);
|
|
922
|
+
if (e.ts < cutoff) continue;
|
|
923
|
+
const outcome = e.outcome ?? "complete";
|
|
924
|
+
bump(byTool, e.tool, outcome);
|
|
925
|
+
bump(bySource, e.source ?? "mcp", outcome);
|
|
926
|
+
bump(byClient, e.client ?? "unknown", outcome);
|
|
927
|
+
if (typeof e.ms === "number" && e.ms >= 0) {
|
|
928
|
+
const timing = msByTool.get(e.tool) ?? { sum: 0, n: 0 };
|
|
929
|
+
timing.sum += e.ms;
|
|
930
|
+
timing.n++;
|
|
931
|
+
msByTool.set(e.tool, timing);
|
|
932
|
+
}
|
|
933
|
+
} catch {
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
const commands = toDimensionStats(byTool).map((d) => {
|
|
938
|
+
const timing = msByTool.get(d.key);
|
|
939
|
+
return {
|
|
940
|
+
tool: d.key,
|
|
941
|
+
calls: d.calls,
|
|
942
|
+
complete: d.complete,
|
|
943
|
+
partial: d.partial,
|
|
944
|
+
miss: d.miss,
|
|
945
|
+
successPct: d.successPct,
|
|
946
|
+
avgMs: timing && timing.n > 0 ? Math.round(timing.sum / timing.n) : null
|
|
947
|
+
};
|
|
948
|
+
});
|
|
949
|
+
const totals = commands.reduce(
|
|
950
|
+
(t, c) => ({
|
|
951
|
+
calls: t.calls + c.calls,
|
|
952
|
+
complete: t.complete + c.complete,
|
|
953
|
+
partial: t.partial + c.partial,
|
|
954
|
+
miss: t.miss + c.miss
|
|
955
|
+
}),
|
|
956
|
+
{ calls: 0, complete: 0, partial: 0, miss: 0 }
|
|
957
|
+
);
|
|
958
|
+
const avgSuccessPct = commands.length ? Math.round(commands.reduce((s, c) => s + c.successPct, 0) / commands.length) : 0;
|
|
959
|
+
return {
|
|
960
|
+
enabled: savingsRecorded(root),
|
|
961
|
+
days,
|
|
962
|
+
commands,
|
|
963
|
+
sources: toDimensionStats(bySource),
|
|
964
|
+
clients: toDimensionStats(byClient),
|
|
965
|
+
totals,
|
|
966
|
+
avgSuccessPct
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
export { CLI_TOOL_ALIASES, GraphIndex, PER_FILE_TOKENS, SAVINGS_TOOLS, acquireLock, cacheDir, clearModelCache, clearSavings, cosine, countPending, embeddingsCached, getNodeEmbeddings, identifierParts, impactOf, indexFor, isModelReady, loadCache, loadEmbedder, modelCacheInfo, nodeEmbedText, parseGraph, queryGraph, queryGraphSemantic, readModelSavings, readSavings, readUsage, recordCliCall, recordSaving, releaseLock, resolveEmbedModel, sanitizeClient, savingsLedgerPath, savingsRecorded, serializeGraph, stableStringify, unavailableMessage, withTimeout };
|
|
971
|
+
//# sourceMappingURL=chunk-WNIIKCNF.js.map
|
|
972
|
+
//# sourceMappingURL=chunk-WNIIKCNF.js.map
|