@vibgrate/cli 2026.721.2 → 2026.722.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/DOCS.md +185 -0
  2. package/README.md +2 -1
  3. package/dist/baseline-J44M7MWP.js +7 -0
  4. package/dist/{baseline-KK7ZLRZB.js.map → baseline-J44M7MWP.js.map} +1 -1
  5. package/dist/chunk-2EVQKZTO.js +696 -0
  6. package/dist/chunk-2EVQKZTO.js.map +1 -0
  7. package/dist/chunk-6CXTPC74.js +234 -0
  8. package/dist/chunk-6CXTPC74.js.map +1 -0
  9. package/dist/chunk-CRAMOEBE.js +675 -0
  10. package/dist/chunk-CRAMOEBE.js.map +1 -0
  11. package/dist/chunk-CS37OBE3.js +861 -0
  12. package/dist/chunk-CS37OBE3.js.map +1 -0
  13. package/dist/chunk-CT5BS56U.js +23 -0
  14. package/dist/chunk-CT5BS56U.js.map +1 -0
  15. package/dist/{chunk-MUMBLIDO.js → chunk-EXQV26MF.js} +4 -3
  16. package/dist/chunk-EXQV26MF.js.map +1 -0
  17. package/dist/chunk-GGJZA3Q6.js +961 -0
  18. package/dist/chunk-GGJZA3Q6.js.map +1 -0
  19. package/dist/chunk-JBXNQCGE.js +484 -0
  20. package/dist/chunk-JBXNQCGE.js.map +1 -0
  21. package/dist/chunk-JFGYT6BI.js +32 -0
  22. package/dist/chunk-JFGYT6BI.js.map +1 -0
  23. package/dist/{chunk-NHLSAZ6N.js → chunk-JSNHZCIV.js} +42 -71
  24. package/dist/chunk-JSNHZCIV.js.map +1 -0
  25. package/dist/chunk-JYS4OBN2.js +148 -0
  26. package/dist/chunk-JYS4OBN2.js.map +1 -0
  27. package/dist/chunk-K3SLGULW.js +50 -0
  28. package/dist/chunk-K3SLGULW.js.map +1 -0
  29. package/dist/chunk-LEPTUB4H.js +108 -0
  30. package/dist/chunk-LEPTUB4H.js.map +1 -0
  31. package/dist/{chunk-JWBS5GS6.js → chunk-PY3DNX5H.js} +94 -34
  32. package/dist/chunk-PY3DNX5H.js.map +1 -0
  33. package/dist/{chunk-3DPZQS2Q.js → chunk-VXT6LUGP.js} +356 -2532
  34. package/dist/chunk-VXT6LUGP.js.map +1 -0
  35. package/dist/chunk-WNIIKCNF.js +972 -0
  36. package/dist/chunk-WNIIKCNF.js.map +1 -0
  37. package/dist/chunk-XKXKWYJF.js +417 -0
  38. package/dist/chunk-XKXKWYJF.js.map +1 -0
  39. package/dist/cli.js +2479 -1234
  40. package/dist/cli.js.map +1 -1
  41. package/dist/fs-KCABDURV.js +3 -0
  42. package/dist/fs-KCABDURV.js.map +1 -0
  43. package/dist/index.d.ts +5 -3
  44. package/dist/index.js +8 -3
  45. package/dist/index.js.map +1 -1
  46. package/dist/interactive-736PBC7J.js +820 -0
  47. package/dist/interactive-736PBC7J.js.map +1 -0
  48. package/dist/mcp-tools-I6PME3AV.js +3 -0
  49. package/dist/mcp-tools-I6PME3AV.js.map +1 -0
  50. package/dist/parse-worker.d.ts +1 -1
  51. package/dist/parse-worker.js +2 -1
  52. package/dist/parse-worker.js.map +1 -1
  53. package/dist/session-POJUDFPH.js +6 -0
  54. package/dist/session-POJUDFPH.js.map +1 -0
  55. package/dist/session-store-W5RZZHA3.js +3 -0
  56. package/dist/session-store-W5RZZHA3.js.map +1 -0
  57. package/dist/stream-json-Q2JGWPHB.js +65 -0
  58. package/dist/stream-json-Q2JGWPHB.js.map +1 -0
  59. package/dist/{types-BgNa-FZQ.d.ts → types-Jl6RJ175.d.ts} +8 -0
  60. package/dist/ui-HOVOPWMX.js +4 -0
  61. package/dist/ui-HOVOPWMX.js.map +1 -0
  62. package/package.json +3 -2
  63. package/dist/baseline-KK7ZLRZB.js +0 -6
  64. package/dist/chunk-3DPZQS2Q.js.map +0 -1
  65. package/dist/chunk-JWBS5GS6.js.map +0 -1
  66. package/dist/chunk-MUMBLIDO.js.map +0 -1
  67. package/dist/chunk-NHLSAZ6N.js.map +0 -1
@@ -0,0 +1,820 @@
1
+ import { newSession, loadLatestSession, summarizeSession, saveSession, recordTask } from './chunk-K3SLGULW.js';
2
+ import { runAgent } from './chunk-JBXNQCGE.js';
3
+ import { printCodeLogo, runBuild, resolveProviders } from './chunk-2EVQKZTO.js';
4
+ import { loadGraph, refreshIfStale, discoverModels, detectServeLaunch } from './chunk-VXT6LUGP.js';
5
+ import './chunk-GGJZA3Q6.js';
6
+ import './chunk-JSNHZCIV.js';
7
+ import './chunk-PY3DNX5H.js';
8
+ import './chunk-RXP66R2E.js';
9
+ import './chunk-GI6W53LM.js';
10
+ import './chunk-CRAMOEBE.js';
11
+ import { nodeCodeFs, undoChanges } from './chunk-6CXTPC74.js';
12
+ import { summarizeDiffs } from './chunk-CS37OBE3.js';
13
+ import { readModelSavings } from './chunk-WNIIKCNF.js';
14
+ import './chunk-JFGYT6BI.js';
15
+ import { TtyPrompter, PromptCancelled } from './chunk-JYS4OBN2.js';
16
+ import { info, c, out } from './chunk-CT5BS56U.js';
17
+ import { McpToolset, defaultMcpConnect } from './chunk-LEPTUB4H.js';
18
+ import * as fs from 'fs';
19
+ import * as os2 from 'os';
20
+ import * as path from 'path';
21
+ import { spawnSync, spawn } from 'child_process';
22
+
23
+ var PROVIDER_LABELS = {
24
+ anthropic: "Anthropic (Claude)",
25
+ openai: "OpenAI (GPT)",
26
+ "x-ai": "xAI (Grok)",
27
+ google: "Google (Gemini)",
28
+ "meta-llama": "Meta (Llama)",
29
+ deepseek: "DeepSeek",
30
+ mistralai: "Mistral",
31
+ qwen: "Qwen"
32
+ };
33
+ var FEATURED_ORDER = Object.keys(PROVIDER_LABELS);
34
+ var OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
35
+ var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
36
+ function catalogCachePath() {
37
+ const base = process.env.XDG_CACHE_HOME || path.join(os2.homedir(), ".cache");
38
+ return path.join(base, "vibgrate", "catalog", "openrouter-models.json");
39
+ }
40
+ async function fetchCatalog(options = {}) {
41
+ const now = options.now ?? (() => 0);
42
+ const fetchImpl = options.fetchImpl ?? fetch;
43
+ if (!options.noCache) {
44
+ const cached = readCache();
45
+ if (cached && (options.offline || now() - cached.ts < CACHE_TTL_MS)) {
46
+ return { providers: groupModels(cached.models), fresh: false, source: "cache" };
47
+ }
48
+ }
49
+ if (options.offline) {
50
+ const cached = readCache();
51
+ if (cached) return { providers: groupModels(cached.models), fresh: false, source: "cache" };
52
+ return { providers: curatedFallback(), fresh: false, source: "fallback" };
53
+ }
54
+ try {
55
+ const controller = new AbortController();
56
+ const timer = setTimeout(() => controller.abort(), options.timeoutMs ?? 8e3);
57
+ let raw;
58
+ try {
59
+ const res = await fetchImpl(OPENROUTER_MODELS_URL, { signal: controller.signal });
60
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
61
+ raw = await res.json();
62
+ } finally {
63
+ clearTimeout(timer);
64
+ }
65
+ const models = parseModels(raw);
66
+ if (models.length === 0) throw new Error("empty catalog");
67
+ writeCache(models, now());
68
+ return { providers: groupModels(models), fresh: true, source: "network" };
69
+ } catch {
70
+ const cached = readCache();
71
+ if (cached) return { providers: groupModels(cached.models), fresh: false, source: "cache" };
72
+ return { providers: curatedFallback(), fresh: false, source: "fallback" };
73
+ }
74
+ }
75
+ function parseModels(raw) {
76
+ const data = raw?.data;
77
+ if (!Array.isArray(data)) return [];
78
+ const out2 = [];
79
+ for (const entry of data) {
80
+ const e = entry;
81
+ if (typeof e.id !== "string" || !e.id.includes("/")) continue;
82
+ const provider = e.id.split("/")[0];
83
+ out2.push({
84
+ id: e.id,
85
+ name: typeof e.name === "string" ? e.name : e.id,
86
+ provider,
87
+ contextLength: numeric(e.context_length),
88
+ promptPricePerM: perMillion(e.pricing?.prompt),
89
+ completionPricePerM: perMillion(e.pricing?.completion)
90
+ });
91
+ }
92
+ return out2;
93
+ }
94
+ function groupModels(models) {
95
+ const byProvider = /* @__PURE__ */ new Map();
96
+ for (const m of models) {
97
+ const list = byProvider.get(m.provider) ?? [];
98
+ list.push(m);
99
+ byProvider.set(m.provider, list);
100
+ }
101
+ const groups = [];
102
+ for (const [id, list] of byProvider) {
103
+ groups.push({ id, label: PROVIDER_LABELS[id] ?? titleCase(id), models: list.sort(rankModels) });
104
+ }
105
+ groups.sort((a, b) => {
106
+ const ia = FEATURED_ORDER.indexOf(a.id);
107
+ const ib = FEATURED_ORDER.indexOf(b.id);
108
+ if (ia !== -1 || ib !== -1) return (ia === -1 ? 1e9 : ia) - (ib === -1 ? 1e9 : ib);
109
+ return a.label.localeCompare(b.label);
110
+ });
111
+ return groups;
112
+ }
113
+ function rankModels(a, b) {
114
+ const ca = isCoding(a) ? 0 : 1;
115
+ const cb = isCoding(b) ? 0 : 1;
116
+ if (ca !== cb) return ca - cb;
117
+ const la = a.contextLength ?? 0;
118
+ const lb = b.contextLength ?? 0;
119
+ if (la !== lb) return lb - la;
120
+ return a.id.localeCompare(b.id);
121
+ }
122
+ function isCoding(m) {
123
+ return /cod(e|er|ing)/i.test(m.id) || /cod(e|er|ing)/i.test(m.name);
124
+ }
125
+ function topProviders(catalog, n = 6) {
126
+ return catalog.providers.slice(0, n);
127
+ }
128
+ function readCache() {
129
+ try {
130
+ const parsed = JSON.parse(fs.readFileSync(catalogCachePath(), "utf8"));
131
+ if (Array.isArray(parsed.models) && typeof parsed.ts === "number") return parsed;
132
+ } catch {
133
+ }
134
+ return null;
135
+ }
136
+ function writeCache(models, ts) {
137
+ try {
138
+ const file = catalogCachePath();
139
+ fs.mkdirSync(path.dirname(file), { recursive: true });
140
+ fs.writeFileSync(file, JSON.stringify({ ts, models }));
141
+ } catch {
142
+ }
143
+ }
144
+ function curatedFallback() {
145
+ return FEATURED_ORDER.map((id) => ({ id, label: PROVIDER_LABELS[id], models: [] }));
146
+ }
147
+ function numeric(v) {
148
+ const n = typeof v === "string" ? Number(v) : typeof v === "number" ? v : NaN;
149
+ return Number.isFinite(n) ? n : void 0;
150
+ }
151
+ function perMillion(v) {
152
+ const n = numeric(v);
153
+ if (n === void 0) return void 0;
154
+ return Math.round(n * 1e6 * 1e3) / 1e3;
155
+ }
156
+ function titleCase(id) {
157
+ return id.replace(/[-_]/g, " ").replace(/\b\w/g, (ch) => ch.toUpperCase());
158
+ }
159
+
160
+ // src/code/wizard.ts
161
+ var ENTER_SLUG = "__enter_slug__";
162
+ var LOCAL = "__local__";
163
+ var BROWSE = "__browse__";
164
+ async function runProviderWizard(prompter, deps) {
165
+ const providers = topProviders(deps.catalog, deps.topN ?? 6);
166
+ const choice = await prompter.select("Where should the model run?", [
167
+ { label: "Local model (on your machine)", value: LOCAL, hint: deps.localModels.length ? `${deps.localModels.length} found` : "nothing installed yet" },
168
+ ...providers.map((p) => ({ label: p.label, value: p.id, hint: "via OpenRouter" })),
169
+ { label: "Enter a provider or model slug myself", value: ENTER_SLUG }
170
+ ]);
171
+ if (choice === LOCAL) return pickLocal(prompter, deps);
172
+ if (choice === ENTER_SLUG) {
173
+ const slug = (await prompter.input("Model slug (e.g. anthropic/claude-3.5-sonnet, or a provider like openai)")).trim();
174
+ if (slug.includes("/")) return { kind: "hosted", provider: "openrouter", model: slug, providerSlug: slug.split("/")[0] };
175
+ const group2 = deps.catalog.providers.find((p) => p.id === slug);
176
+ if (group2 && group2.models.length) return pickHostedModel(prompter, group2.id, group2.models);
177
+ const model = (await prompter.input(`Model id for "${slug}"`)).trim();
178
+ return { kind: "hosted", provider: "openrouter", model: model || slug, providerSlug: slug };
179
+ }
180
+ const group = providers.find((p) => p.id === choice);
181
+ return pickHostedModel(prompter, group.id, group.models);
182
+ }
183
+ async function pickHostedModel(prompter, providerSlug, models) {
184
+ if (models.length === 0) {
185
+ const model2 = (await prompter.input(`Enter a ${providerSlug} model id`)).trim();
186
+ return { kind: "hosted", provider: "openrouter", model: model2, providerSlug };
187
+ }
188
+ const model = await prompter.select("Choose a model", [
189
+ ...models.slice(0, 12).map((m) => ({ label: m.name, value: m.id, hint: modelHint(m.contextLength, m.promptPricePerM) })),
190
+ { label: "Enter a model slug myself", value: ENTER_SLUG }
191
+ ]);
192
+ if (model === ENTER_SLUG) {
193
+ const typed = (await prompter.input(`Enter a ${providerSlug} model id`)).trim();
194
+ return { kind: "hosted", provider: "openrouter", model: typed, providerSlug };
195
+ }
196
+ return { kind: "hosted", provider: "openrouter", model, providerSlug };
197
+ }
198
+ async function pickLocal(prompter, deps) {
199
+ const installed = deps.localModels.filter((m) => m.runtime === "ollama");
200
+ const choices = [
201
+ ...installed.map((m) => ({ label: m.name, value: m.name, hint: "installed" })),
202
+ { label: "Browse coding models to pull (Ollama)", value: BROWSE },
203
+ { label: "Enter a model name to pull", value: ENTER_SLUG }
204
+ ];
205
+ const picked = await prompter.select("Local model", choices);
206
+ if (picked === ENTER_SLUG) {
207
+ const name = (await prompter.input("Ollama model to pull (e.g. qwen2.5-coder:7b)")).trim();
208
+ return { kind: "local", provider: "ollama", model: name, providerSlug: "ollama", needsPull: true };
209
+ }
210
+ if (picked === BROWSE) {
211
+ const coding = deps.catalog.providers.flatMap((p) => p.models).filter((m) => /cod(e|er|ing)/i.test(m.id)).slice(0, 10);
212
+ if (coding.length === 0) {
213
+ const name = (await prompter.input("Ollama model to pull (e.g. qwen2.5-coder:7b)")).trim();
214
+ return { kind: "local", provider: "ollama", model: name, providerSlug: "ollama", needsPull: true };
215
+ }
216
+ const slug = await prompter.select("Coding model to pull", [
217
+ ...coding.map((m) => ({ label: m.name, value: toOllamaTag(m.id), hint: m.id })),
218
+ { label: "Enter a model name myself", value: ENTER_SLUG }
219
+ ]);
220
+ if (slug === ENTER_SLUG) {
221
+ const name = (await prompter.input("Ollama model to pull (e.g. qwen2.5-coder:7b)")).trim();
222
+ return { kind: "local", provider: "ollama", model: name, providerSlug: "ollama", needsPull: true };
223
+ }
224
+ return { kind: "local", provider: "ollama", model: slug, providerSlug: "ollama", needsPull: true };
225
+ }
226
+ return { kind: "local", provider: "ollama", model: picked, providerSlug: "ollama", needsPull: false };
227
+ }
228
+ function toOllamaTag(slug) {
229
+ const tail = slug.split("/").pop() ?? slug;
230
+ const size = tail.match(/(\d+(?:\.\d+)?)b\b/i);
231
+ const base = tail.replace(/-instruct\b/i, "").replace(/(\d+(?:\.\d+)?)b\b/i, "").replace(/[-.]+$/g, "").replace(/\s+/g, "-");
232
+ return size ? `${base}:${size[1]}b` : base;
233
+ }
234
+ function modelHint(contextLength, pricePerM) {
235
+ const parts = [];
236
+ if (contextLength) parts.push(`${Math.round(contextLength / 1e3)}k ctx`);
237
+ if (typeof pricePerM === "number") parts.push(pricePerM === 0 ? "free" : `$${pricePerM}/M in`);
238
+ return parts.join(" \xB7 ");
239
+ }
240
+
241
+ // src/code/capability.ts
242
+ var GiB = 1024 ** 3;
243
+ var QUANT_BYTES_PER_PARAM = {
244
+ f16: 2,
245
+ fp16: 2,
246
+ f32: 4,
247
+ q8: 1.06,
248
+ q8_0: 1.06,
249
+ q6: 0.82,
250
+ q6_k: 0.82,
251
+ q5: 0.7,
252
+ q5_k_m: 0.72,
253
+ q5_k_s: 0.68,
254
+ q5_0: 0.71,
255
+ q5_1: 0.77,
256
+ q4: 0.58,
257
+ q4_k_m: 0.6,
258
+ q4_k_s: 0.56,
259
+ q4_0: 0.56,
260
+ q4_1: 0.63,
261
+ q3: 0.46,
262
+ q3_k_m: 0.48,
263
+ q3_k_s: 0.44,
264
+ q2: 0.36,
265
+ q2_k: 0.4
266
+ };
267
+ var DEFAULT_QUANT = "q4_k_m";
268
+ var RUNTIME_OVERHEAD = 1.2;
269
+ function estimateModelBytes(slug, defaultParamsB = 7) {
270
+ const lower = slug.toLowerCase();
271
+ const paramMatch = lower.match(/(\d+(?:\.\d+)?)\s*b\b/);
272
+ const paramsB = paramMatch ? Number(paramMatch[1]) : void 0;
273
+ const quant = detectQuant(lower);
274
+ const bytesPerParam = QUANT_BYTES_PER_PARAM[quant] ?? QUANT_BYTES_PER_PARAM[DEFAULT_QUANT];
275
+ const effectiveParams = (paramsB ?? defaultParamsB) * 1e9;
276
+ const bytes = Math.round(effectiveParams * bytesPerParam * RUNTIME_OVERHEAD);
277
+ return { paramsB, quant, bytes, guessed: paramsB === void 0 };
278
+ }
279
+ function detectQuant(lower) {
280
+ const keys = Object.keys(QUANT_BYTES_PER_PARAM).sort((a, b) => b.length - a.length);
281
+ for (const k of keys) if (lower.includes(k)) return k;
282
+ return DEFAULT_QUANT;
283
+ }
284
+ function parseOllamaPs(stdout) {
285
+ const lines = stdout.split("\n").map((l) => l.trim()).filter(Boolean);
286
+ const out2 = [];
287
+ for (const line of lines) {
288
+ if (/^NAME\b/i.test(line)) continue;
289
+ const sizeMatch = line.match(/(\d+(?:\.\d+)?)\s*(GB|GiB|MB|MiB|TB)/i);
290
+ const name = line.split(/\s+/)[0];
291
+ if (!name || !sizeMatch) continue;
292
+ out2.push({ name, bytes: toBytes(Number(sizeMatch[1]), sizeMatch[2]) });
293
+ }
294
+ return out2;
295
+ }
296
+ function parseNvidiaSmi(stdout) {
297
+ const line = stdout.split("\n").map((l) => l.trim()).find(Boolean);
298
+ if (!line) return null;
299
+ const parts = line.split(",").map((p) => Number(p.trim()));
300
+ if (parts.length < 2 || !Number.isFinite(parts[0]) || !Number.isFinite(parts[1])) return null;
301
+ return { totalBytes: parts[0] * 1024 * 1024, usedBytes: parts[1] * 1024 * 1024 };
302
+ }
303
+ function assessCapability(model, sys) {
304
+ const useVram = typeof sys.vramTotalBytes === "number" && sys.vramTotalBytes > 0;
305
+ const free = useVram ? sys.vramFreeBytes ?? 0 : sys.freeRamBytes;
306
+ const loadedBytes = sys.loaded.reduce((n, m) => n + m.bytes, 0);
307
+ const margin = Math.min(2 * GiB, (useVram ? sys.vramTotalBytes : sys.totalRamBytes) * 0.1);
308
+ const available = Math.max(0, free - margin);
309
+ const availableIfUnloaded = Math.max(0, free + loadedBytes - margin);
310
+ const need = model.bytes;
311
+ const canRunNow = available >= need;
312
+ const canRunUnloaded = availableIfUnloaded >= need;
313
+ const suggestions = [];
314
+ if (!canRunNow && canRunUnloaded && sys.loaded.length) {
315
+ const biggest = [...sys.loaded].sort((a, b) => b.bytes - a.bytes);
316
+ suggestions.push(`unload a loaded model to free memory (e.g. \`ollama stop ${biggest[0].name}\` frees ~${fmt(biggest[0].bytes)})`);
317
+ }
318
+ if (!canRunUnloaded) {
319
+ suggestions.push(`this model needs ~${fmt(need)} but only ~${fmt(availableIfUnloaded)} is available \u2014 choose a smaller model or a lower quant (e.g. q4_K_M)`);
320
+ }
321
+ if (model.guessed) {
322
+ suggestions.push(`couldn't read the parameter count from the name \u2014 this estimate assumes a ~${(model.bytes / GiB).toFixed(1)} GiB model; verify before pulling`);
323
+ }
324
+ return {
325
+ model,
326
+ availableBytes: available,
327
+ availableIfUnloadedBytes: availableIfUnloaded,
328
+ canRun: canRunNow || canRunUnloaded,
329
+ needsUnload: !canRunNow && canRunUnloaded,
330
+ suggestions,
331
+ loaded: sys.loaded
332
+ };
333
+ }
334
+ function fmt(bytes) {
335
+ if (bytes >= GiB) return `${(bytes / GiB).toFixed(1)} GiB`;
336
+ return `${(bytes / (1024 * 1024)).toFixed(0)} MiB`;
337
+ }
338
+ function toBytes(n, unit) {
339
+ const u = unit.toLowerCase();
340
+ if (u.startsWith("t")) return n * 1e3 ** 4;
341
+ if (u.startsWith("g")) return n * (u.includes("i") ? GiB : 1e9);
342
+ if (u.startsWith("m")) return n * (u.includes("i") ? 1024 * 1024 : 1e6);
343
+ return n;
344
+ }
345
+ function gatherSystemMemory() {
346
+ const loaded = safeRun("ollama", ["ps"]).map(parseOllamaPs).flat();
347
+ const smi = safeRun("nvidia-smi", ["--query-gpu=memory.total,memory.used", "--format=csv,noheader,nounits"]).map(parseNvidiaSmi).find(Boolean);
348
+ const sys = {
349
+ totalRamBytes: os2.totalmem(),
350
+ freeRamBytes: os2.freemem(),
351
+ loaded
352
+ };
353
+ if (smi) {
354
+ sys.vramTotalBytes = smi.totalBytes;
355
+ sys.vramFreeBytes = Math.max(0, smi.totalBytes - smi.usedBytes);
356
+ }
357
+ return sys;
358
+ }
359
+ function hasOllama() {
360
+ return spawnSync(process.platform === "win32" ? "where" : "which", ["ollama"], { stdio: "ignore" }).status === 0;
361
+ }
362
+ function stopModel(name) {
363
+ return spawnSync("ollama", ["stop", name], { stdio: "ignore" }).status === 0;
364
+ }
365
+ function pullModelClean(name, spinner) {
366
+ return new Promise((resolve) => {
367
+ const child = spawn("ollama", ["pull", name]);
368
+ const onData = (buf) => {
369
+ const line = lastMeaningfulLine(buf.toString());
370
+ if (line) spinner.update(`pulling ${name} \u2014 ${line}`);
371
+ };
372
+ child.stdout?.on("data", onData);
373
+ child.stderr?.on("data", onData);
374
+ child.on("error", () => resolve(false));
375
+ child.on("close", (code) => resolve(code === 0));
376
+ });
377
+ }
378
+ function lastMeaningfulLine(chunk) {
379
+ const lines = chunk.split(/[\r\n]+/).map((l) => l.trim()).map((l) => l.replace(/\x1b\[[0-9;]*[A-Za-z]/g, "").replace(/[▏▎▍▌▋▊▉█░▒▓⣀-⣿]/g, "").trim()).filter(Boolean);
380
+ const last = lines[lines.length - 1] ?? "";
381
+ const pct = last.match(/(\d+)\s*%/);
382
+ const status = last.replace(/\s{2,}.*$/, "").slice(0, 48);
383
+ return pct ? `${status} ${pct[1]}%`.trim() : status;
384
+ }
385
+ function safeRun(bin, args) {
386
+ try {
387
+ const res = spawnSync(bin, args, { encoding: "utf8", timeout: 5e3 });
388
+ if (res.status !== 0 || typeof res.stdout !== "string") return [];
389
+ return [res.stdout];
390
+ } catch {
391
+ return [];
392
+ }
393
+ }
394
+
395
+ // src/code/cost.ts
396
+ function costOf(usage, pricing) {
397
+ const inCost = (usage.promptTokens ?? 0) / 1e6 * (pricing.promptPerM ?? 0);
398
+ const outCost = (usage.completionTokens ?? 0) / 1e6 * (pricing.completionPerM ?? 0);
399
+ return round4(inCost + outCost);
400
+ }
401
+ var SessionMeter = class {
402
+ constructor(pricing = {}) {
403
+ this.pricing = pricing;
404
+ }
405
+ pricing;
406
+ prompt = 0;
407
+ completion = 0;
408
+ cost = 0;
409
+ turns = 0;
410
+ /** Update the pricing (e.g. after `/model` switches model mid-session). */
411
+ setPricing(pricing) {
412
+ this.pricing = pricing;
413
+ }
414
+ /** Record one model turn; returns the incremental cost of just this turn. */
415
+ add(usage) {
416
+ this.prompt += usage.promptTokens ?? 0;
417
+ this.completion += usage.completionTokens ?? 0;
418
+ this.turns += 1;
419
+ const delta = costOf(usage, this.pricing);
420
+ this.cost = round4(this.cost + delta);
421
+ return delta;
422
+ }
423
+ get totals() {
424
+ return { promptTokens: this.prompt, completionTokens: this.completion, totalTokens: this.prompt + this.completion, cost: this.cost, turns: this.turns };
425
+ }
426
+ /** A compact one-line summary, e.g. `3.1k tok · ~$0.0042` (cost only when priced). */
427
+ summary() {
428
+ const t = this.totals;
429
+ const tok = `${fmtTokens(t.totalTokens)} tok`;
430
+ const priced = this.pricing.promptPerM || this.pricing.completionPerM;
431
+ return priced ? `${tok} \xB7 ~$${t.cost.toFixed(4)}` : tok;
432
+ }
433
+ };
434
+ function fmtTokens(n) {
435
+ if (n >= 1e6) return `${(n / 1e6).toFixed(2)}M`;
436
+ if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k`;
437
+ return String(n);
438
+ }
439
+ function round4(x) {
440
+ return Math.round(x * 1e4) / 1e4;
441
+ }
442
+ var GraphProcess = class _GraphProcess {
443
+ child;
444
+ disposed = false;
445
+ command;
446
+ args;
447
+ constructor(child, command, args) {
448
+ this.child = child;
449
+ this.command = command;
450
+ this.args = args;
451
+ child.once("exit", () => {
452
+ this.child = null;
453
+ });
454
+ }
455
+ static start(options) {
456
+ const launch = options.launch ?? detectServeLaunch();
457
+ const client = options.client ?? "vg-code";
458
+ const args = [...launch.args, "--client", client];
459
+ const spawnImpl = options.spawnImpl ?? spawn;
460
+ try {
461
+ const child = spawnImpl(launch.command, args, { cwd: options.root, stdio: "ignore", detached: false });
462
+ if (!child || typeof child.pid !== "number") return null;
463
+ child.on("error", () => {
464
+ });
465
+ return new _GraphProcess(child, launch.command, args);
466
+ } catch {
467
+ return null;
468
+ }
469
+ }
470
+ get pid() {
471
+ return this.child?.pid;
472
+ }
473
+ /** True while the child is believed to be running. */
474
+ get running() {
475
+ return this.child !== null && !this.disposed;
476
+ }
477
+ /** Kill the child. Idempotent and safe to call after the child already exited. */
478
+ dispose() {
479
+ if (this.disposed) return;
480
+ this.disposed = true;
481
+ const child = this.child;
482
+ this.child = null;
483
+ if (!child || child.killed) return;
484
+ try {
485
+ child.kill("SIGTERM");
486
+ } catch {
487
+ }
488
+ }
489
+ };
490
+
491
+ // src/code/interactive.ts
492
+ async function runInteractive(root, global, opts, prompter = new TtyPrompter()) {
493
+ printCodeLogo(root);
494
+ try {
495
+ await ensureMap(root, global, prompter);
496
+ const selection = await chooseModel(root, global, opts, prompter);
497
+ if (!selection) return;
498
+ await codingRepl(root, global, opts, selection, prompter);
499
+ } catch (e) {
500
+ if (e instanceof PromptCancelled) {
501
+ info(c.dim("\n cancelled \u2014 no changes were made"));
502
+ return;
503
+ }
504
+ throw e;
505
+ }
506
+ }
507
+ async function ensureMap(root, global, prompter) {
508
+ const existing = loadGraph(root, global.graph);
509
+ if (!existing) {
510
+ const sp2 = prompter.spinner("Building the code map\u2026");
511
+ try {
512
+ await runBuild([root], {}, { ...global, quiet: true });
513
+ sp2.stop("Code map built");
514
+ } catch (e) {
515
+ sp2.fail(`couldn't build the code map: ${e.message}`);
516
+ throw e;
517
+ }
518
+ return;
519
+ }
520
+ const sp = prompter.spinner("Checking the code map is up to date\u2026");
521
+ const refreshed = await refreshIfStale(root).catch(() => null);
522
+ if (refreshed?.status === "refreshed") sp.stop("Code map refreshed");
523
+ else sp.stop("Code map ready");
524
+ }
525
+ async function chooseModel(root, global, opts, prompter) {
526
+ if (opts.provider && (opts.model || opts.modelPath)) {
527
+ const local = opts.provider === "ollama" || opts.provider === "lmstudio" || opts.provider === "llama-cpp" || !!global.local;
528
+ return { kind: local ? "local" : "hosted", provider: opts.provider === "ollama" ? "ollama" : "openrouter", model: opts.model ?? opts.modelPath ?? "", providerSlug: opts.provider, needsPull: false };
529
+ }
530
+ const sp = prompter.spinner("Loading the model catalog\u2026");
531
+ const catalog = await fetchCatalog({ offline: global.local });
532
+ sp.stop(catalog.source === "network" ? "Model catalog loaded" : catalog.source === "cache" ? "Model catalog (cached)" : "Model catalog (offline list)");
533
+ const localModels = discoverModels();
534
+ const selection = await runProviderWizard(prompter, { catalog, localModels });
535
+ if (selection.kind === "local" && selection.needsPull) {
536
+ const ok = await preflightAndPull(selection, prompter);
537
+ if (!ok) return null;
538
+ }
539
+ if (selection.kind === "hosted" && !process.env.OPENROUTER_API_KEY) {
540
+ prompter.note(c.yellow("Set OPENROUTER_API_KEY in your environment to use this hosted model (keys are never passed as flags)."));
541
+ }
542
+ return selection;
543
+ }
544
+ async function preflightAndPull(selection, prompter) {
545
+ if (!hasOllama()) {
546
+ prompter.note(c.yellow("Ollama isn\u2019t installed \u2014 get it from https://ollama.com, then re-run. (We never install a runtime for you.)"));
547
+ return false;
548
+ }
549
+ const estimate = estimateModelBytes(selection.model);
550
+ const report = assessCapability(estimate, gatherSystemMemory());
551
+ prompter.note(`${c.bold(selection.model)} \u2014 estimated ${fmt(estimate.bytes)}${estimate.guessed ? c.dim(" (size guessed)") : ""}; ~${fmt(report.availableBytes)} free now`);
552
+ for (const s of report.suggestions) prompter.note(c.dim(`\xB7 ${s}`));
553
+ if (!report.canRun) {
554
+ prompter.note(c.red("This machine doesn\u2019t have enough memory for that model \u2014 not pulling it. Pick a smaller model or a lower quant."));
555
+ return false;
556
+ }
557
+ if (report.needsUnload) {
558
+ const biggest = [...report.loaded].sort((a, b) => b.bytes - a.bytes)[0];
559
+ if (biggest && await prompter.confirm(`Unload ${biggest.name} to free ~${fmt(biggest.bytes)}?`, true)) {
560
+ stopModel(biggest.name);
561
+ }
562
+ }
563
+ if (!await prompter.confirm(`Pull ${selection.model} (~${fmt(estimate.bytes)})?`, true)) return false;
564
+ const sp = prompter.spinner(`Pulling ${selection.model}\u2026`);
565
+ const ok = await pullModelClean(selection.model, sp);
566
+ if (ok) sp.stop(`Pulled ${selection.model}`);
567
+ else sp.fail(`couldn\u2019t pull ${selection.model}`);
568
+ return ok;
569
+ }
570
+ async function codingRepl(root, global, opts, selection, prompter) {
571
+ let sel = selection;
572
+ let route = resolveProviders({ provider: sel.provider, model: sel.model, local: sel.kind === "local" || global.local });
573
+ const fsImpl = nodeCodeFs(root);
574
+ const meter = new SessionMeter();
575
+ let lastChanges = [];
576
+ let store = newSession(sessionId(), sel.providerSlug, sel.model, Date.now());
577
+ let priorSummary;
578
+ if (opts.continueSession) {
579
+ const prev = loadLatestSession(root);
580
+ if (prev) {
581
+ store = prev;
582
+ lastChanges = prev.lastChanges ?? [];
583
+ priorSummary = summarizeSession(prev);
584
+ prompter.note(c.dim(`resumed session \u2014 ${prev.tasks.length} earlier task(s); /undo restores the last change`));
585
+ } else prompter.note(c.dim("no previous session to continue"));
586
+ }
587
+ let mcp;
588
+ if (opts.mcpServers && Object.keys(opts.mcpServers).length) {
589
+ const sp = prompter.spinner("Connecting MCP tools\u2026");
590
+ const { toolset, warnings } = await McpToolset.connect(opts.mcpServers, opts.mcpConnect ?? defaultMcpConnect);
591
+ mcp = toolset;
592
+ const from = opts.mcpSources?.length ? ` (from ${opts.mcpSources.join(", ")})` : "";
593
+ sp.stop(`MCP tools: ${toolset.specs().length} from ${Object.keys(opts.mcpServers).length} server(s)${from}`);
594
+ for (const w of warnings) prompter.note(c.yellow(` ${w}`));
595
+ }
596
+ const graphProc = GraphProcess.start({ root });
597
+ const cleanup = () => {
598
+ graphProc?.dispose();
599
+ void mcp?.dispose();
600
+ };
601
+ process.once("SIGINT", cleanup);
602
+ const autoNote = opts.auto ? c.yellow(" \xB7 auto-approve ON") : "";
603
+ prompter.intro(`Ready \u2014 ${sel.providerSlug}/${sel.model}${graphProc ? c.dim(" \xB7 graph " + graphProc.pid) : ""}${autoNote}. Describe a task, or /help. Empty line to exit.`);
604
+ try {
605
+ for (; ; ) {
606
+ const line = (await prompter.input("code \u203A")).trim();
607
+ if (!line || /^(exit|quit)$/i.test(line) || line === "/exit" || line === "/quit") break;
608
+ if (line.startsWith("/")) {
609
+ const [cmd] = line.slice(1).split(/\s+/);
610
+ if (cmd === "help") {
611
+ prompter.note("commands: /undo /diff /model /cost /clear /exit");
612
+ } else if (cmd === "undo") {
613
+ if (lastChanges.length === 0) prompter.note(c.dim("nothing to undo"));
614
+ else {
615
+ const restored = undoChanges(lastChanges, fsImpl);
616
+ prompter.note(c.green(`\u21A9 reverted ${restored.length} file(s): ${restored.join(", ")}`));
617
+ lastChanges = [];
618
+ store = { ...store, lastChanges: [] };
619
+ saveSession(root, store);
620
+ }
621
+ } else if (cmd === "diff") {
622
+ if (lastChanges.length === 0) prompter.note(c.dim("no recent change"));
623
+ else for (const ch of lastChanges) if (ch.diff) out(ch.diff);
624
+ } else if (cmd === "model") {
625
+ const swapped = await reselectModel(root, global, prompter);
626
+ if (swapped) {
627
+ sel = swapped;
628
+ route = resolveProviders({ provider: sel.provider, model: sel.model, local: sel.kind === "local" || global.local });
629
+ prompter.note(c.green(`now using ${sel.providerSlug}/${sel.model}`));
630
+ }
631
+ } else if (cmd === "cost") {
632
+ prompter.note(`this session: ${meter.summary()}`);
633
+ printCost(root);
634
+ } else if (cmd === "clear") {
635
+ prompter.note(c.dim("each task already starts fresh \u2014 nothing to clear"));
636
+ } else {
637
+ prompter.note(c.yellow(`unknown command /${cmd} \u2014 try /help`));
638
+ }
639
+ continue;
640
+ }
641
+ const graph = loadGraph(root, global.graph);
642
+ if (!graph) {
643
+ prompter.note(c.red("the code map disappeared \u2014 run `vg` to rebuild"));
644
+ break;
645
+ }
646
+ const result = await agentTask({
647
+ root,
648
+ graph,
649
+ instruction: line,
650
+ providers: route.providers,
651
+ fsImpl,
652
+ attribution: { client: "vg-code", provider: sel.providerSlug, model: sel.model },
653
+ auto: !!opts.auto,
654
+ maxSteps: opts.maxSteps,
655
+ denyCommands: opts.denyCommands,
656
+ testCommand: opts.testCommand,
657
+ contextBudget: opts.contextBudget,
658
+ stream: opts.stream,
659
+ verify: opts.verify,
660
+ priorSummary,
661
+ externalTools: mcp ? mcpExternalTools(mcp, agentApprove(opts, prompter)) : void 0,
662
+ meter,
663
+ prompter
664
+ });
665
+ priorSummary = void 0;
666
+ if (result.changes.length) {
667
+ lastChanges = result.changes;
668
+ prompter.note(c.dim(" /undo to revert \xB7 /diff to review"));
669
+ }
670
+ store = recordTask(store, { instruction: line, summary: result.finalText, changes: result.changes, stopped: result.stopped }, Date.now());
671
+ saveSession(root, store);
672
+ }
673
+ } finally {
674
+ cleanup();
675
+ process.removeListener("SIGINT", cleanup);
676
+ }
677
+ prompter.outro("Done.");
678
+ }
679
+ function agentApprove(opts, prompter) {
680
+ return async (action) => {
681
+ if (opts.auto) return true;
682
+ if (action.kind === "tool") return prompter.confirm(`Call ${action.name}?`, false);
683
+ return true;
684
+ };
685
+ }
686
+ function mcpExternalTools(mcp, approve) {
687
+ return { specs: mcp.specs(), owns: (name) => mcp.owns(name), execute: (call) => mcp.execute(call, approve) };
688
+ }
689
+ function sessionId() {
690
+ let s = "";
691
+ const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789";
692
+ for (let i = 0; i < 10; i++) s += alphabet[Math.floor(Math.random() * alphabet.length)];
693
+ return s;
694
+ }
695
+ async function reselectModel(root, global, prompter) {
696
+ try {
697
+ const sp = prompter.spinner("Loading the model catalog\u2026");
698
+ const catalog = await fetchCatalog({ offline: global.local });
699
+ sp.stop("Model catalog loaded");
700
+ const selection = await runProviderWizard(prompter, { catalog, localModels: discoverModels() });
701
+ if (selection.kind === "local" && selection.needsPull) {
702
+ const ok = await preflightAndPull(selection, prompter);
703
+ if (!ok) return null;
704
+ }
705
+ return selection;
706
+ } catch (e) {
707
+ if (e instanceof PromptCancelled) return null;
708
+ prompter.note(c.yellow(`couldn't switch model: ${e.message}`));
709
+ return null;
710
+ }
711
+ }
712
+ function printCost(root) {
713
+ const now = Date.now();
714
+ const models = readModelSavings(root, 30, now);
715
+ if (models.length === 0) {
716
+ info(c.dim(" no usage recorded yet this session"));
717
+ return;
718
+ }
719
+ info(c.bold(" savings by model (last 30 days, local)"));
720
+ for (const m of models) info(` ${m.key.padEnd(32)} ${m.queries} call(s) \xB7 ~${m.vgTokens} tokens \xB7 saved \u2248 $${m.saved}`);
721
+ }
722
+ async function agentTask(params) {
723
+ const { prompter } = params;
724
+ const approve = async (action) => {
725
+ if (params.auto) return true;
726
+ if (!prompter) return false;
727
+ switch (action.kind) {
728
+ case "edit":
729
+ out(action.diff);
730
+ return prompter.confirm(`Apply edit to ${action.file}?`, true);
731
+ case "create":
732
+ return prompter.confirm(`Create ${action.file} (${action.bytes} bytes)?`, true);
733
+ case "delete":
734
+ return prompter.confirm(`Delete ${action.file}?`, false);
735
+ case "run":
736
+ return prompter.confirm(`Run \`${action.command}\`?`, false);
737
+ case "tool":
738
+ return prompter.confirm(`Call ${action.name}(${briefArgs(action.args)})?`, false);
739
+ }
740
+ };
741
+ let streaming = false;
742
+ const closeStream = () => {
743
+ if (streaming) {
744
+ process.stderr.write("\n");
745
+ streaming = false;
746
+ }
747
+ };
748
+ const onEvent = (e) => {
749
+ if (e.type === "token") {
750
+ process.stderr.write(e.text);
751
+ streaming = true;
752
+ return;
753
+ }
754
+ closeStream();
755
+ if (e.type === "assistant") {
756
+ if (e.text.trim()) info(e.text.trim());
757
+ } else if (e.type === "tool-call") {
758
+ info(c.dim(` \u2192 ${e.name}(${briefArgs(e.args)})`));
759
+ } else if (e.type === "tool-result") {
760
+ info(c.dim(` ${e.mutated ? c.green("\u2714 ") : ""}${e.content.split("\n")[0].slice(0, 100)}`));
761
+ } else if (e.type === "compact") {
762
+ info(c.dim(` \xB7 compacted context (${e.droppedRounds} earlier round(s) summarized)`));
763
+ } else if (e.type === "verify") {
764
+ info(e.passed ? c.green(` \u2714 ${e.command} passed`) : c.yellow(` \u2717 ${e.command} failed \u2014 asking the model to fix it`));
765
+ }
766
+ };
767
+ const result = await runAgent({
768
+ graph: params.graph,
769
+ root: params.root,
770
+ instruction: params.instruction,
771
+ providers: params.providers,
772
+ fsImpl: params.fsImpl,
773
+ run: shellRunner(params.root),
774
+ approve,
775
+ onEvent,
776
+ attribution: params.attribution,
777
+ maxSteps: params.maxSteps,
778
+ budget: params.budget,
779
+ contextBudget: params.contextBudget,
780
+ auto: params.auto,
781
+ denyCommands: params.denyCommands,
782
+ testCommand: params.testCommand,
783
+ stream: params.stream,
784
+ verify: params.verify,
785
+ priorSummary: params.priorSummary,
786
+ externalTools: params.externalTools,
787
+ now: () => Date.now()
788
+ });
789
+ closeStream();
790
+ const perTask = params.meter?.add(result.usage);
791
+ const summary = summarizeDiffs(result.changes.map((ch) => ({ file: ch.file, diff: ch.diff })));
792
+ info("");
793
+ info(
794
+ result.stopped === "finished" ? c.green(`\u2714 ${result.finalText}`) : result.stopped === "max-steps" ? c.yellow(`stopped at the step limit \u2014 ${result.changes.length} change(s) so far`) : c.dim(result.finalText)
795
+ );
796
+ const meterNote = perTask !== void 0 ? ` \xB7 ${params.meter.summary()}` : "";
797
+ if (result.changes.length || perTask) info(c.dim(` ${summary} \xB7 via ${result.provider.id}/${result.provider.model}${result.provider.fellBack ? " (fell back)" : ""}${meterNote}`));
798
+ return result;
799
+ }
800
+ function briefArgs(args) {
801
+ const parts = [];
802
+ for (const [k, v] of Object.entries(args)) {
803
+ const s = typeof v === "string" ? v : JSON.stringify(v);
804
+ parts.push(`${k}: ${s.length > 40 ? s.slice(0, 40) + "\u2026" : s}`);
805
+ if (parts.length >= 2) break;
806
+ }
807
+ return parts.join(", ");
808
+ }
809
+ function shellRunner(root) {
810
+ return (command) => {
811
+ const res = spawnSync(command, { cwd: root, shell: true, encoding: "utf8", timeout: 12e4, maxBuffer: 10 * 1024 * 1024 });
812
+ const stdout = (res.stdout ?? "") + (res.stderr ? `
813
+ ${res.stderr}` : "");
814
+ return { stdout, exitCode: res.status ?? 1 };
815
+ };
816
+ }
817
+
818
+ export { agentTask, runInteractive };
819
+ //# sourceMappingURL=interactive-736PBC7J.js.map
820
+ //# sourceMappingURL=interactive-736PBC7J.js.map