@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.
- package/DOCS.md +185 -0
- package/README.md +2 -1
- package/dist/baseline-J44M7MWP.js +7 -0
- package/dist/{baseline-KK7ZLRZB.js.map → baseline-J44M7MWP.js.map} +1 -1
- package/dist/chunk-2EVQKZTO.js +696 -0
- package/dist/chunk-2EVQKZTO.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-MUMBLIDO.js → chunk-EXQV26MF.js} +4 -3
- package/dist/chunk-EXQV26MF.js.map +1 -0
- package/dist/chunk-GGJZA3Q6.js +961 -0
- package/dist/chunk-GGJZA3Q6.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-NHLSAZ6N.js → chunk-JSNHZCIV.js} +42 -71
- package/dist/chunk-JSNHZCIV.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-JWBS5GS6.js → chunk-PY3DNX5H.js} +94 -34
- package/dist/chunk-PY3DNX5H.js.map +1 -0
- package/dist/{chunk-3DPZQS2Q.js → chunk-VXT6LUGP.js} +356 -2532
- package/dist/chunk-VXT6LUGP.js.map +1 -0
- package/dist/chunk-WNIIKCNF.js +972 -0
- package/dist/chunk-WNIIKCNF.js.map +1 -0
- package/dist/chunk-XKXKWYJF.js +417 -0
- package/dist/chunk-XKXKWYJF.js.map +1 -0
- package/dist/cli.js +2479 -1234
- 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 +5 -3
- package/dist/index.js +8 -3
- package/dist/index.js.map +1 -1
- package/dist/interactive-736PBC7J.js +820 -0
- package/dist/interactive-736PBC7J.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.d.ts +1 -1
- 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/{types-BgNa-FZQ.d.ts → types-Jl6RJ175.d.ts} +8 -0
- package/dist/ui-HOVOPWMX.js +4 -0
- package/dist/ui-HOVOPWMX.js.map +1 -0
- package/package.json +3 -2
- package/dist/baseline-KK7ZLRZB.js +0 -6
- package/dist/chunk-3DPZQS2Q.js.map +0 -1
- package/dist/chunk-JWBS5GS6.js.map +0 -1
- package/dist/chunk-MUMBLIDO.js.map +0 -1
- package/dist/chunk-NHLSAZ6N.js.map +0 -1
|
@@ -0,0 +1,861 @@
|
|
|
1
|
+
import { indexFor, queryGraph, impactOf } from './chunk-WNIIKCNF.js';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import { spawnSync } from 'child_process';
|
|
6
|
+
import { createRequire } from 'module';
|
|
7
|
+
import { pathToFileURL } from 'url';
|
|
8
|
+
|
|
9
|
+
function runtimeCacheDir() {
|
|
10
|
+
const base = process.env.XDG_CACHE_HOME || path.join(os.homedir(), ".cache");
|
|
11
|
+
return path.join(base, "vibgrate", "runtime");
|
|
12
|
+
}
|
|
13
|
+
function isPackageInstalled(spec) {
|
|
14
|
+
const name = packageName(spec);
|
|
15
|
+
try {
|
|
16
|
+
const req = createRequire(path.join(runtimeCacheDir(), "package.json"));
|
|
17
|
+
req.resolve(name);
|
|
18
|
+
return true;
|
|
19
|
+
} catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function ensureUnavailableMessage(reason, spec) {
|
|
24
|
+
const name = packageName(spec);
|
|
25
|
+
switch (reason) {
|
|
26
|
+
case "offline":
|
|
27
|
+
return `${name} is needed for this backend but --local/offline is set \u2014 using a backend that needs no install, or drop --local to allow a one-time install.`;
|
|
28
|
+
case "no-consent":
|
|
29
|
+
return `${name} is needed for this backend and isn't installed yet. Re-run with --yes to install it once into ${runtimeCacheDir()} (nothing is written into the CLI itself), or pick a backend that needs no install (e.g. --provider ollama).`;
|
|
30
|
+
case "install-failed":
|
|
31
|
+
return `couldn't install ${name} (offline, or npm unavailable) \u2014 using a backend that needs no install. It will try again next time.`;
|
|
32
|
+
case "load-failed":
|
|
33
|
+
default:
|
|
34
|
+
return `${name} installed but couldn't be loaded here \u2014 using a backend that needs no install.`;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async function ensurePackage(spec, options = {}) {
|
|
38
|
+
const dir = runtimeCacheDir();
|
|
39
|
+
const fail = (reason) => {
|
|
40
|
+
options.onUnavailable?.(reason, spec);
|
|
41
|
+
return { module: null, dir, reason };
|
|
42
|
+
};
|
|
43
|
+
if (isPackageInstalled(spec)) {
|
|
44
|
+
const mod2 = await load(spec, dir);
|
|
45
|
+
return mod2 ? { module: mod2, dir } : fail("load-failed");
|
|
46
|
+
}
|
|
47
|
+
if (options.local) return fail("offline");
|
|
48
|
+
const interactive = options.interactive ?? Boolean(process.stdout.isTTY && process.stdin.isTTY);
|
|
49
|
+
if (!options.consent && !interactive) return fail("no-consent");
|
|
50
|
+
if (!options.consent && interactive) return fail("no-consent");
|
|
51
|
+
try {
|
|
52
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
53
|
+
ensureRuntimeManifest(dir);
|
|
54
|
+
} catch {
|
|
55
|
+
return fail("install-failed");
|
|
56
|
+
}
|
|
57
|
+
const installer = options.install ?? defaultInstaller;
|
|
58
|
+
const ok = installer(spec, dir);
|
|
59
|
+
if (!ok || !isPackageInstalled(spec)) return fail("install-failed");
|
|
60
|
+
const mod = await load(spec, dir);
|
|
61
|
+
return mod ? { module: mod, dir } : fail("load-failed");
|
|
62
|
+
}
|
|
63
|
+
function ensureRuntimeManifest(dir) {
|
|
64
|
+
const manifest = path.join(dir, "package.json");
|
|
65
|
+
if (!fs.existsSync(manifest)) {
|
|
66
|
+
fs.writeFileSync(
|
|
67
|
+
manifest,
|
|
68
|
+
JSON.stringify({ name: "vibgrate-runtime-deps", private: true, description: "On-demand vg runtime packages" }, null, 2)
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async function load(spec, dir) {
|
|
73
|
+
const name = packageName(spec);
|
|
74
|
+
try {
|
|
75
|
+
const req = createRequire(path.join(dir, "package.json"));
|
|
76
|
+
const resolved = req.resolve(name);
|
|
77
|
+
const mod = await import(pathToFileURL(resolved).href);
|
|
78
|
+
return mod?.default ?? mod;
|
|
79
|
+
} catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function defaultInstaller(spec, dir) {
|
|
84
|
+
const res = spawnSync("npm", ["install", "--prefix", dir, "--no-audit", "--no-fund", "--ignore-scripts", spec], {
|
|
85
|
+
stdio: "ignore",
|
|
86
|
+
timeout: 18e4
|
|
87
|
+
});
|
|
88
|
+
return res.status === 0;
|
|
89
|
+
}
|
|
90
|
+
function packageName(spec) {
|
|
91
|
+
if (spec.startsWith("@")) {
|
|
92
|
+
const at2 = spec.indexOf("@", 1);
|
|
93
|
+
return at2 === -1 ? spec : spec.slice(0, at2);
|
|
94
|
+
}
|
|
95
|
+
const at = spec.indexOf("@");
|
|
96
|
+
return at === -1 ? spec : spec.slice(0, at);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// src/code/providers.ts
|
|
100
|
+
function openAiMessages(messages) {
|
|
101
|
+
return messages.map((m) => {
|
|
102
|
+
if (m.role === "tool") return { role: "tool", tool_call_id: m.toolCallId, name: m.name, content: m.content };
|
|
103
|
+
if (m.role === "assistant" && m.toolCalls?.length) {
|
|
104
|
+
return {
|
|
105
|
+
role: "assistant",
|
|
106
|
+
content: m.content || null,
|
|
107
|
+
tool_calls: m.toolCalls.map((t) => ({ id: t.id, type: "function", function: { name: t.name, arguments: JSON.stringify(t.arguments) } }))
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
return { role: m.role, content: m.content };
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
function openAiTools(tools) {
|
|
114
|
+
if (!tools?.length) return void 0;
|
|
115
|
+
return tools.map((t) => ({ type: "function", function: { name: t.name, description: t.description, parameters: t.parameters } }));
|
|
116
|
+
}
|
|
117
|
+
function parseToolCalls(raw, synthesizePrefix = "call") {
|
|
118
|
+
const calls = raw;
|
|
119
|
+
if (!Array.isArray(calls)) return [];
|
|
120
|
+
return calls.map((c, i) => ({
|
|
121
|
+
id: typeof c.id === "string" && c.id ? c.id : `${synthesizePrefix}_${i}`,
|
|
122
|
+
name: c.function?.name ?? "unknown",
|
|
123
|
+
arguments: coerceArgs(c.function?.arguments)
|
|
124
|
+
}));
|
|
125
|
+
}
|
|
126
|
+
function coerceArgs(raw) {
|
|
127
|
+
if (raw && typeof raw === "object") return raw;
|
|
128
|
+
if (typeof raw === "string") {
|
|
129
|
+
try {
|
|
130
|
+
const parsed = JSON.parse(raw);
|
|
131
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
132
|
+
} catch {
|
|
133
|
+
return {};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return {};
|
|
137
|
+
}
|
|
138
|
+
function redactSecrets(s) {
|
|
139
|
+
return s.replace(/\b(sk|xoxb|ghp|gho|glpat|AKIA)[-_][A-Za-z0-9-_]{6,}/g, "$1-***redacted***").replace(/Bearer\s+[A-Za-z0-9._-]{8,}/gi, "Bearer ***redacted***");
|
|
140
|
+
}
|
|
141
|
+
function httpError(label, status, hint) {
|
|
142
|
+
return new Error(`${label} returned HTTP ${status} \u2014 ${hint}`);
|
|
143
|
+
}
|
|
144
|
+
async function* streamLines(res) {
|
|
145
|
+
const reader = res.body?.getReader();
|
|
146
|
+
if (!reader) return;
|
|
147
|
+
const decoder = new TextDecoder();
|
|
148
|
+
let buf = "";
|
|
149
|
+
for (; ; ) {
|
|
150
|
+
const { done, value } = await reader.read();
|
|
151
|
+
if (done) break;
|
|
152
|
+
buf += decoder.decode(value, { stream: true });
|
|
153
|
+
let idx;
|
|
154
|
+
while ((idx = buf.indexOf("\n")) >= 0) {
|
|
155
|
+
const line = buf.slice(0, idx).trim();
|
|
156
|
+
buf = buf.slice(idx + 1);
|
|
157
|
+
if (line) yield line;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (buf.trim()) yield buf.trim();
|
|
161
|
+
}
|
|
162
|
+
function accumulateOpenAiDelta(acc, chunk, onToken) {
|
|
163
|
+
const c = chunk;
|
|
164
|
+
const delta = c.choices?.[0]?.delta;
|
|
165
|
+
if (delta?.content) {
|
|
166
|
+
acc.text += delta.content;
|
|
167
|
+
onToken?.(delta.content);
|
|
168
|
+
}
|
|
169
|
+
for (const tc of delta?.tool_calls ?? []) {
|
|
170
|
+
const idx = tc.index ?? 0;
|
|
171
|
+
const e = acc.tools.get(idx) ?? { args: "" };
|
|
172
|
+
if (tc.id) e.id = tc.id;
|
|
173
|
+
if (tc.function?.name) e.name = tc.function.name;
|
|
174
|
+
if (tc.function?.arguments) e.args += tc.function.arguments;
|
|
175
|
+
acc.tools.set(idx, e);
|
|
176
|
+
}
|
|
177
|
+
if (c.usage) {
|
|
178
|
+
acc.usage.promptTokens = c.usage.prompt_tokens;
|
|
179
|
+
acc.usage.completionTokens = c.usage.completion_tokens;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function finalizeTools(tools) {
|
|
183
|
+
return [...tools.entries()].sort((a, b) => a[0] - b[0]).map(([i, e]) => ({ id: e.id ?? `call_${i}`, name: e.name ?? "unknown", arguments: coerceArgs(e.args) }));
|
|
184
|
+
}
|
|
185
|
+
async function postJson(url, body, headers, timeoutMs) {
|
|
186
|
+
const controller = new AbortController();
|
|
187
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
188
|
+
try {
|
|
189
|
+
return await fetch(url, { method: "POST", headers: { "content-type": "application/json", ...headers }, body: JSON.stringify(body), signal: controller.signal });
|
|
190
|
+
} finally {
|
|
191
|
+
clearTimeout(timer);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
var MockProvider = class {
|
|
195
|
+
constructor(model, reply) {
|
|
196
|
+
this.model = model;
|
|
197
|
+
this.reply = reply;
|
|
198
|
+
}
|
|
199
|
+
model;
|
|
200
|
+
reply;
|
|
201
|
+
id = "mock";
|
|
202
|
+
label = "Mock (deterministic)";
|
|
203
|
+
local = true;
|
|
204
|
+
async chat(_messages, _opts) {
|
|
205
|
+
return { text: this.reply, model: this.model, provider: this.id };
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
var OllamaProvider = class {
|
|
209
|
+
constructor(model, host = process.env.OLLAMA_HOST || "http://127.0.0.1:11434") {
|
|
210
|
+
this.model = model;
|
|
211
|
+
this.host = host;
|
|
212
|
+
}
|
|
213
|
+
model;
|
|
214
|
+
host;
|
|
215
|
+
id = "ollama";
|
|
216
|
+
label = "Ollama (local)";
|
|
217
|
+
local = true;
|
|
218
|
+
async chat(messages, opts = {}) {
|
|
219
|
+
const streaming = !!(opts.stream && opts.onToken);
|
|
220
|
+
let res;
|
|
221
|
+
try {
|
|
222
|
+
res = await postJson(
|
|
223
|
+
`${this.host.replace(/\/$/, "")}/api/chat`,
|
|
224
|
+
{ model: this.model, messages: openAiMessages(messages), stream: streaming, tools: openAiTools(opts.tools), options: { temperature: opts.temperature ?? 0 } },
|
|
225
|
+
{},
|
|
226
|
+
opts.timeoutMs ?? 12e4
|
|
227
|
+
);
|
|
228
|
+
} catch {
|
|
229
|
+
throw new Error(`couldn't reach Ollama at ${this.host} \u2014 is \`ollama serve\` running? (start it, or pass --provider to use a different backend)`);
|
|
230
|
+
}
|
|
231
|
+
if (!res.ok) throw httpError("Ollama", res.status, res.status === 404 ? `model "${this.model}" isn't pulled \u2014 run \`ollama pull ${this.model}\` or pick another with --model` : "check `ollama serve` and the model name");
|
|
232
|
+
if (streaming) {
|
|
233
|
+
let text = "";
|
|
234
|
+
let toolCallsRaw;
|
|
235
|
+
const usage = {};
|
|
236
|
+
for await (const line of streamLines(res)) {
|
|
237
|
+
try {
|
|
238
|
+
const obj = JSON.parse(line);
|
|
239
|
+
const delta = obj.message?.content;
|
|
240
|
+
if (delta) {
|
|
241
|
+
text += delta;
|
|
242
|
+
opts.onToken?.(delta);
|
|
243
|
+
}
|
|
244
|
+
if (obj.message?.tool_calls) toolCallsRaw = obj.message.tool_calls;
|
|
245
|
+
if (typeof obj.prompt_eval_count === "number") usage.promptTokens = obj.prompt_eval_count;
|
|
246
|
+
if (typeof obj.eval_count === "number") usage.completionTokens = obj.eval_count;
|
|
247
|
+
} catch {
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return { text, model: this.model, provider: this.id, toolCalls: parseToolCalls(toolCallsRaw), usage };
|
|
251
|
+
}
|
|
252
|
+
const data = await res.json();
|
|
253
|
+
return {
|
|
254
|
+
text: data.message?.content ?? "",
|
|
255
|
+
model: this.model,
|
|
256
|
+
provider: this.id,
|
|
257
|
+
toolCalls: parseToolCalls(data.message?.tool_calls),
|
|
258
|
+
usage: { promptTokens: data.prompt_eval_count, completionTokens: data.eval_count }
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
var OpenAiCompatibleProvider = class {
|
|
263
|
+
constructor(model, config) {
|
|
264
|
+
this.model = model;
|
|
265
|
+
this.config = config;
|
|
266
|
+
this.id = config.id;
|
|
267
|
+
this.label = config.label;
|
|
268
|
+
this.local = config.local;
|
|
269
|
+
}
|
|
270
|
+
model;
|
|
271
|
+
config;
|
|
272
|
+
id;
|
|
273
|
+
label;
|
|
274
|
+
local;
|
|
275
|
+
async chat(messages, opts = {}) {
|
|
276
|
+
const headers = {};
|
|
277
|
+
if (this.config.apiKeyEnv) {
|
|
278
|
+
const key = process.env[this.config.apiKeyEnv];
|
|
279
|
+
if (!key) {
|
|
280
|
+
throw new Error(`${this.label} needs an API key in ${this.config.apiKeyEnv} \u2014 set it in your environment (never pass keys as flags), or use --local for an on-device backend.`);
|
|
281
|
+
}
|
|
282
|
+
headers.authorization = `Bearer ${key}`;
|
|
283
|
+
}
|
|
284
|
+
const streaming = !!(opts.stream && opts.onToken);
|
|
285
|
+
const url = `${this.config.baseUrl.replace(/\/$/, "")}/chat/completions`;
|
|
286
|
+
const body = {
|
|
287
|
+
model: this.model,
|
|
288
|
+
messages: openAiMessages(messages),
|
|
289
|
+
tools: openAiTools(opts.tools),
|
|
290
|
+
temperature: opts.temperature ?? 0,
|
|
291
|
+
max_tokens: opts.maxTokens,
|
|
292
|
+
stream: streaming,
|
|
293
|
+
...streaming ? { stream_options: { include_usage: true } } : {}
|
|
294
|
+
};
|
|
295
|
+
let res;
|
|
296
|
+
try {
|
|
297
|
+
res = await postJson(url, body, headers, opts.timeoutMs ?? 12e4);
|
|
298
|
+
} catch {
|
|
299
|
+
throw new Error(`couldn't reach ${this.label} at ${this.config.baseUrl} \u2014 check the endpoint is up (or use --local).`);
|
|
300
|
+
}
|
|
301
|
+
if (!res.ok) {
|
|
302
|
+
const hint = res.status === 401 || res.status === 403 ? `the key in ${this.config.apiKeyEnv ?? "(none)"} was rejected \u2014 check it has access to "${this.model}"` : `check the model id "${this.model}" and the endpoint`;
|
|
303
|
+
throw httpError(this.label, res.status, hint);
|
|
304
|
+
}
|
|
305
|
+
if (streaming) {
|
|
306
|
+
const acc = { text: "", tools: /* @__PURE__ */ new Map(), usage: {} };
|
|
307
|
+
for await (const line of streamLines(res)) {
|
|
308
|
+
if (!line.startsWith("data:")) continue;
|
|
309
|
+
const payload = line.slice(5).trim();
|
|
310
|
+
if (payload === "[DONE]") break;
|
|
311
|
+
try {
|
|
312
|
+
accumulateOpenAiDelta(acc, JSON.parse(payload), opts.onToken);
|
|
313
|
+
} catch {
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
const tools = finalizeTools(acc.tools);
|
|
317
|
+
return { text: acc.text, model: this.model, provider: this.id, toolCalls: tools.length ? tools : void 0, usage: acc.usage };
|
|
318
|
+
}
|
|
319
|
+
const data = await res.json();
|
|
320
|
+
const message = data.choices?.[0]?.message;
|
|
321
|
+
return {
|
|
322
|
+
text: message?.content ?? "",
|
|
323
|
+
model: this.model,
|
|
324
|
+
provider: this.id,
|
|
325
|
+
toolCalls: parseToolCalls(message?.tool_calls),
|
|
326
|
+
usage: { promptTokens: data.usage?.prompt_tokens, completionTokens: data.usage?.completion_tokens }
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
var OPENAI_COMPATIBLE = {
|
|
331
|
+
openrouter: { baseUrl: "https://openrouter.ai/api/v1", apiKeyEnv: "OPENROUTER_API_KEY", local: false, label: "OpenRouter" },
|
|
332
|
+
litellm: { baseUrl: process.env.LITELLM_BASE_URL || "http://127.0.0.1:4000", apiKeyEnv: "LITELLM_API_KEY", local: false, label: "LiteLLM" },
|
|
333
|
+
openai: { baseUrl: "https://api.openai.com/v1", apiKeyEnv: "OPENAI_API_KEY", local: false, label: "OpenAI" },
|
|
334
|
+
together: { baseUrl: "https://api.together.xyz/v1", apiKeyEnv: "TOGETHER_API_KEY", local: false, label: "Together" },
|
|
335
|
+
lmstudio: { baseUrl: process.env.LMSTUDIO_BASE_URL || "http://127.0.0.1:1234/v1", local: true, label: "LM Studio (local)" }
|
|
336
|
+
};
|
|
337
|
+
var LocalLlamaProvider = class {
|
|
338
|
+
constructor(model, modelPath, ensure = ensurePackage, consent = false) {
|
|
339
|
+
this.model = model;
|
|
340
|
+
this.modelPath = modelPath;
|
|
341
|
+
this.ensure = ensure;
|
|
342
|
+
this.consent = consent;
|
|
343
|
+
}
|
|
344
|
+
model;
|
|
345
|
+
modelPath;
|
|
346
|
+
ensure;
|
|
347
|
+
consent;
|
|
348
|
+
id = "llama-cpp";
|
|
349
|
+
label = "llama.cpp (local)";
|
|
350
|
+
local = true;
|
|
351
|
+
session = null;
|
|
352
|
+
async chat(messages, opts = {}) {
|
|
353
|
+
const res = await this.ensure("node-llama-cpp@^3", { consent: this.consent, onUnavailable: () => {
|
|
354
|
+
} });
|
|
355
|
+
if (!res.module) {
|
|
356
|
+
throw new Error(ensureUnavailableMessage(res.reason ?? "no-consent", "node-llama-cpp"));
|
|
357
|
+
}
|
|
358
|
+
const lib = res.module;
|
|
359
|
+
try {
|
|
360
|
+
const llama = await lib.getLlama();
|
|
361
|
+
const model = await llama.loadModel({ modelPath: this.modelPath });
|
|
362
|
+
const ctx = await model.createContext();
|
|
363
|
+
const { LlamaChatSession } = lib;
|
|
364
|
+
const chat = new LlamaChatSession({ contextSequence: ctx.getSequence() });
|
|
365
|
+
const prompt = messages.map((m) => `${m.role}: ${m.content}`).join("\n\n");
|
|
366
|
+
const text = await chat.prompt(prompt, { temperature: opts.temperature ?? 0, maxTokens: opts.maxTokens });
|
|
367
|
+
return { text, model: this.model, provider: this.id };
|
|
368
|
+
} catch (e) {
|
|
369
|
+
throw new Error(`local llama.cpp inference failed for ${this.modelPath} \u2014 ${redactSecrets(e.message)}`);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
// src/code/context.ts
|
|
375
|
+
function buildCodeContext(graph, instruction, options = {}) {
|
|
376
|
+
const budget = options.budget ?? 3e3;
|
|
377
|
+
const seedLimit = options.seeds ?? 8;
|
|
378
|
+
const impactDepth = options.impactDepth ?? 2;
|
|
379
|
+
const index = indexFor(graph);
|
|
380
|
+
const q = queryGraph(graph, instruction, { budget: Math.floor(budget * 0.6), limit: seedLimit * 2 });
|
|
381
|
+
let seeds = q.matches.map((m) => ({ node: m.node, why: m.why }));
|
|
382
|
+
if (options.files && options.files.length) {
|
|
383
|
+
const set = new Set(options.files.map(normalize));
|
|
384
|
+
const inScope = seeds.filter((s) => set.has(normalize(s.node.file)));
|
|
385
|
+
if (inScope.length) seeds = inScope;
|
|
386
|
+
}
|
|
387
|
+
seeds = seeds.slice(0, seedLimit);
|
|
388
|
+
const impacted = /* @__PURE__ */ new Map();
|
|
389
|
+
for (const s of seeds) {
|
|
390
|
+
const impact = impactOf(graph, s.node.id, { depth: impactDepth });
|
|
391
|
+
for (const item of impact.affected.slice(0, 6)) {
|
|
392
|
+
const node = index.node(item.id);
|
|
393
|
+
if (node && !impacted.has(node.id)) impacted.set(node.id, { node, via: s.node.qualifiedName });
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
const targetFiles = options.files?.length ? [...new Set(options.files.map(normalize))].sort() : [...new Set(seeds.map((s) => s.node.file))].sort();
|
|
397
|
+
const seedIds = new Set(seeds.map((s) => s.node.id));
|
|
398
|
+
const pinnedFacts = pinnedFactsFor(graph, seedIds);
|
|
399
|
+
const rendered = render(instruction, seeds, [...impacted.values()], targetFiles, pinnedFacts, index, budget);
|
|
400
|
+
return {
|
|
401
|
+
instruction,
|
|
402
|
+
seeds,
|
|
403
|
+
targetFiles,
|
|
404
|
+
impacted: [...impacted.values()],
|
|
405
|
+
pinnedFacts,
|
|
406
|
+
rendered,
|
|
407
|
+
tokensEstimate: estimateTokens(rendered)
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
function pinnedFactsFor(graph, seedIds) {
|
|
411
|
+
if (!graph.facts?.length) return [];
|
|
412
|
+
const out = [];
|
|
413
|
+
for (const f of graph.facts) {
|
|
414
|
+
if (f.kind === "characterization") continue;
|
|
415
|
+
if (!f.subjectIds.some((id) => seedIds.has(id))) continue;
|
|
416
|
+
const subjects = f.subjectIds.map((id) => graph.nodes.find((n) => n.id === id)?.qualifiedName ?? id).join(", ");
|
|
417
|
+
out.push(`${f.kind}: ${subjects} \u2014 ${JSON.stringify(f.predicate)}`);
|
|
418
|
+
}
|
|
419
|
+
return out.sort().slice(0, 12);
|
|
420
|
+
}
|
|
421
|
+
function render(instruction, seeds, impacted, targetFiles, pinnedFacts, index, budget) {
|
|
422
|
+
const lines = [];
|
|
423
|
+
lines.push("# Repository context (from the deterministic code graph)");
|
|
424
|
+
lines.push("");
|
|
425
|
+
if (pinnedFacts.length) {
|
|
426
|
+
lines.push("## Hard constraints (do not violate)");
|
|
427
|
+
for (const f of pinnedFacts) lines.push(`- ${f}`);
|
|
428
|
+
lines.push("");
|
|
429
|
+
}
|
|
430
|
+
lines.push("## Relevant symbols");
|
|
431
|
+
for (const { node, why } of seeds) {
|
|
432
|
+
const block = [];
|
|
433
|
+
block.push(`### ${node.qualifiedName} (${node.kind}, ${node.file}:${node.span.start}-${node.span.end})`);
|
|
434
|
+
if (node.signature) block.push("`" + node.signature + "`");
|
|
435
|
+
const callees = uniqueNames(index.callees(node.id).map((x) => x.node.qualifiedName)).slice(0, 6);
|
|
436
|
+
const callers = uniqueNames(index.callers(node.id).map((x) => x.node.qualifiedName)).slice(0, 6);
|
|
437
|
+
if (callees.length) block.push(`calls: ${callees.join(", ")}`);
|
|
438
|
+
if (callers.length) block.push(`called by: ${callers.join(", ")}`);
|
|
439
|
+
block.push(`importance ${node.importance.toFixed(3)}${node.isHub ? " \xB7 hub (high blast radius)" : ""} \xB7 ${why}`);
|
|
440
|
+
block.push("");
|
|
441
|
+
const candidate = lines.concat(block).join("\n");
|
|
442
|
+
if (estimateTokens(candidate) > budget && lines.length > 3) break;
|
|
443
|
+
lines.push(...block);
|
|
444
|
+
}
|
|
445
|
+
if (impacted.length) {
|
|
446
|
+
lines.push("## Blast radius (would be affected by a change here)");
|
|
447
|
+
for (const { node, via } of impacted.slice(0, 10)) {
|
|
448
|
+
lines.push(`- ${node.qualifiedName} (${node.file}:${node.span.start}) \u2014 depends on ${via}`);
|
|
449
|
+
}
|
|
450
|
+
lines.push("");
|
|
451
|
+
}
|
|
452
|
+
lines.push("## Files in scope for this edit");
|
|
453
|
+
for (const f of targetFiles) lines.push(`- ${f}`);
|
|
454
|
+
lines.push("");
|
|
455
|
+
lines.push("## Task");
|
|
456
|
+
lines.push(instruction);
|
|
457
|
+
return lines.join("\n");
|
|
458
|
+
}
|
|
459
|
+
function uniqueNames(xs) {
|
|
460
|
+
return [...new Set(xs)];
|
|
461
|
+
}
|
|
462
|
+
function normalize(file) {
|
|
463
|
+
return file.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
464
|
+
}
|
|
465
|
+
function estimateTokens(text) {
|
|
466
|
+
return Math.ceil(text.length / 4);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// src/code/apply.ts
|
|
470
|
+
function parseEdits(text) {
|
|
471
|
+
const edits = [];
|
|
472
|
+
const lines = text.split("\n");
|
|
473
|
+
let i = 0;
|
|
474
|
+
let pendingFile = "";
|
|
475
|
+
const isFence = (s) => /^\s*```/.test(s);
|
|
476
|
+
while (i < lines.length) {
|
|
477
|
+
const line = lines[i];
|
|
478
|
+
const create = /^\s*CREATE\s+(\S.*?)\s*$/.exec(line);
|
|
479
|
+
if (create) {
|
|
480
|
+
const file = create[1];
|
|
481
|
+
const body = [];
|
|
482
|
+
i++;
|
|
483
|
+
while (i < lines.length && !/^\s*END CREATE\s*$/.test(lines[i])) {
|
|
484
|
+
if (!isFence(lines[i])) body.push(lines[i]);
|
|
485
|
+
i++;
|
|
486
|
+
}
|
|
487
|
+
i++;
|
|
488
|
+
edits.push({ op: "create", file, content: body.join("\n") });
|
|
489
|
+
pendingFile = "";
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
const del = /^\s*DELETE\s+(\S.*?)\s*$/.exec(line);
|
|
493
|
+
if (del) {
|
|
494
|
+
edits.push({ op: "delete", file: del[1] });
|
|
495
|
+
i++;
|
|
496
|
+
pendingFile = "";
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
if (/^\s*<{5,}\s*SEARCH/.test(line)) {
|
|
500
|
+
const file = pendingFile;
|
|
501
|
+
const search = [];
|
|
502
|
+
const replace = [];
|
|
503
|
+
i++;
|
|
504
|
+
while (i < lines.length && !/^\s*={5,}\s*$/.test(lines[i])) {
|
|
505
|
+
search.push(lines[i]);
|
|
506
|
+
i++;
|
|
507
|
+
}
|
|
508
|
+
i++;
|
|
509
|
+
while (i < lines.length && !/^\s*>{5,}\s*REPLACE/.test(lines[i])) {
|
|
510
|
+
replace.push(lines[i]);
|
|
511
|
+
i++;
|
|
512
|
+
}
|
|
513
|
+
i++;
|
|
514
|
+
edits.push({
|
|
515
|
+
op: "replace",
|
|
516
|
+
file,
|
|
517
|
+
search: search.join("\n"),
|
|
518
|
+
replace: replace.join("\n"),
|
|
519
|
+
anchorSymbol: void 0
|
|
520
|
+
});
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
const trimmed = line.trim();
|
|
524
|
+
if (trimmed && !isFence(line) && looksLikePath(trimmed)) pendingFile = trimmed.replace(/[:`]+$/, "");
|
|
525
|
+
i++;
|
|
526
|
+
}
|
|
527
|
+
return edits;
|
|
528
|
+
}
|
|
529
|
+
function looksLikePath(s) {
|
|
530
|
+
if (/\s/.test(s.replace(/:$/, ""))) return false;
|
|
531
|
+
return /\//.test(s) || /\.[A-Za-z0-9]{1,8}:?$/.test(s);
|
|
532
|
+
}
|
|
533
|
+
function applyEdit(content, edit, spans = []) {
|
|
534
|
+
if (edit.op === "create") {
|
|
535
|
+
if (content !== null && content !== edit.content) {
|
|
536
|
+
return { content, outcome: { edit, status: "conflict", reason: `${edit.file} already exists \u2014 refusing to overwrite it with a create; use a replace edit instead` } };
|
|
537
|
+
}
|
|
538
|
+
return { content: edit.content, outcome: { edit, status: content === edit.content ? "no-op" : "applied", matchedBy: "exact" } };
|
|
539
|
+
}
|
|
540
|
+
if (edit.op === "delete") {
|
|
541
|
+
if (content === null) return { content: null, outcome: { edit, status: "no-op", reason: `${edit.file} does not exist \u2014 nothing to delete` } };
|
|
542
|
+
return { content: null, outcome: { edit, status: "applied", matchedBy: "exact" } };
|
|
543
|
+
}
|
|
544
|
+
if (content === null) {
|
|
545
|
+
return { content: null, outcome: { edit, status: "not-found", reason: `${edit.file} does not exist \u2014 a replace needs an existing file (did you mean CREATE ${edit.file}?)` } };
|
|
546
|
+
}
|
|
547
|
+
if (edit.search === "") {
|
|
548
|
+
if (content === "") return { content: edit.replace, outcome: { edit, status: "applied", matchedBy: "exact" } };
|
|
549
|
+
return { content, outcome: { edit, status: "invalid", reason: "empty SEARCH on a non-empty file is ambiguous \u2014 quote the exact lines to replace" } };
|
|
550
|
+
}
|
|
551
|
+
if (edit.search === edit.replace) {
|
|
552
|
+
return { content, outcome: { edit, status: "no-op", reason: "SEARCH and REPLACE are identical \u2014 no change" } };
|
|
553
|
+
}
|
|
554
|
+
const located = locate(content, edit.search, edit.anchorSymbol, spans, edit.file);
|
|
555
|
+
if (located.kind === "none") {
|
|
556
|
+
return { content, outcome: { edit, status: "not-found", reason: `the SEARCH text was not found in ${edit.file} \u2014 it must match the current file exactly (whitespace-flexible)` } };
|
|
557
|
+
}
|
|
558
|
+
if (located.kind === "ambiguous") {
|
|
559
|
+
return {
|
|
560
|
+
content,
|
|
561
|
+
outcome: {
|
|
562
|
+
edit,
|
|
563
|
+
status: "ambiguous",
|
|
564
|
+
reason: `the SEARCH text matches ${located.count} places in ${edit.file} \u2014 add more surrounding lines, or name the symbol so the graph can disambiguate`
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
const next = content.slice(0, located.from) + edit.replace + content.slice(located.to);
|
|
569
|
+
return { content: next, outcome: { edit, status: "applied", matchedBy: located.matchedBy } };
|
|
570
|
+
}
|
|
571
|
+
function applyEdits(edits, read, spansByFile = () => []) {
|
|
572
|
+
const state = /* @__PURE__ */ new Map();
|
|
573
|
+
for (const edit of edits) {
|
|
574
|
+
let entry = state.get(edit.file);
|
|
575
|
+
if (!entry) {
|
|
576
|
+
const before = read(edit.file);
|
|
577
|
+
entry = { before, after: before, outcomes: [] };
|
|
578
|
+
state.set(edit.file, entry);
|
|
579
|
+
}
|
|
580
|
+
const { content, outcome } = applyEdit(entry.after, edit, spansByFile(edit.file));
|
|
581
|
+
entry.after = content;
|
|
582
|
+
entry.outcomes.push(outcome);
|
|
583
|
+
}
|
|
584
|
+
return state;
|
|
585
|
+
}
|
|
586
|
+
function locate(content, search, anchor, spans, file) {
|
|
587
|
+
const exact = allIndexes(content, search);
|
|
588
|
+
if (exact.length === 1) return { kind: "unique", from: exact[0], to: exact[0] + search.length, matchedBy: "exact" };
|
|
589
|
+
if (exact.length > 1) {
|
|
590
|
+
const scoped = scopeToAnchor(content, exact.map((from) => ({ from, to: from + search.length })), anchor, spans, file);
|
|
591
|
+
if (scoped) return { ...scoped, matchedBy: "graph-span" };
|
|
592
|
+
return { kind: "ambiguous", count: exact.length };
|
|
593
|
+
}
|
|
594
|
+
const flex = flexIndexes(content, search);
|
|
595
|
+
if (flex.length === 1) return { kind: "unique", from: flex[0].from, to: flex[0].to, matchedBy: "whitespace" };
|
|
596
|
+
if (flex.length > 1) {
|
|
597
|
+
const scoped = scopeToAnchor(content, flex, anchor, spans, file);
|
|
598
|
+
if (scoped) return { ...scoped, matchedBy: "graph-span" };
|
|
599
|
+
return { kind: "ambiguous", count: flex.length };
|
|
600
|
+
}
|
|
601
|
+
return { kind: "none" };
|
|
602
|
+
}
|
|
603
|
+
function allIndexes(hay, needle) {
|
|
604
|
+
const out = [];
|
|
605
|
+
if (needle === "") return out;
|
|
606
|
+
let from = 0;
|
|
607
|
+
for (; ; ) {
|
|
608
|
+
const idx = hay.indexOf(needle, from);
|
|
609
|
+
if (idx === -1) break;
|
|
610
|
+
out.push(idx);
|
|
611
|
+
from = idx + needle.length;
|
|
612
|
+
}
|
|
613
|
+
return out;
|
|
614
|
+
}
|
|
615
|
+
function flexIndexes(content, search) {
|
|
616
|
+
const cLines = content.split("\n");
|
|
617
|
+
const sLines = search.split("\n");
|
|
618
|
+
while (sLines.length && sLines[0].trim() === "") sLines.shift();
|
|
619
|
+
while (sLines.length && sLines[sLines.length - 1].trim() === "") sLines.pop();
|
|
620
|
+
if (sLines.length === 0) return [];
|
|
621
|
+
const sNorm = sLines.map((l) => l.trim());
|
|
622
|
+
const lineStart = [];
|
|
623
|
+
let acc = 0;
|
|
624
|
+
for (const l of cLines) {
|
|
625
|
+
lineStart.push(acc);
|
|
626
|
+
acc += l.length + 1;
|
|
627
|
+
}
|
|
628
|
+
const out = [];
|
|
629
|
+
for (let i = 0; i + sNorm.length <= cLines.length; i++) {
|
|
630
|
+
let ok = true;
|
|
631
|
+
for (let j = 0; j < sNorm.length; j++) {
|
|
632
|
+
if (cLines[i + j].trim() !== sNorm[j]) {
|
|
633
|
+
ok = false;
|
|
634
|
+
break;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
if (!ok) continue;
|
|
638
|
+
const from = lineStart[i];
|
|
639
|
+
const lastLine = i + sNorm.length - 1;
|
|
640
|
+
const to = lineStart[lastLine] + cLines[lastLine].length;
|
|
641
|
+
out.push({ from, to });
|
|
642
|
+
}
|
|
643
|
+
return out;
|
|
644
|
+
}
|
|
645
|
+
function scopeToAnchor(content, candidates, anchor, spans, file) {
|
|
646
|
+
if (!anchor) return null;
|
|
647
|
+
const span = spans.find((s) => s.file === file && s.qualifiedName === anchor);
|
|
648
|
+
if (!span) return null;
|
|
649
|
+
const lineOf = lineIndexer(content);
|
|
650
|
+
const inside = candidates.filter((c) => {
|
|
651
|
+
const line = lineOf(c.from);
|
|
652
|
+
return line >= span.start && line <= span.end;
|
|
653
|
+
});
|
|
654
|
+
if (inside.length === 1) return { kind: "unique", from: inside[0].from, to: inside[0].to };
|
|
655
|
+
return null;
|
|
656
|
+
}
|
|
657
|
+
function lineIndexer(content) {
|
|
658
|
+
const starts = [0];
|
|
659
|
+
for (let i = 0; i < content.length; i++) if (content[i] === "\n") starts.push(i + 1);
|
|
660
|
+
return (offset) => {
|
|
661
|
+
let lo = 0;
|
|
662
|
+
let hi = starts.length - 1;
|
|
663
|
+
while (lo < hi) {
|
|
664
|
+
const mid = lo + hi + 1 >> 1;
|
|
665
|
+
if (starts[mid] <= offset) lo = mid;
|
|
666
|
+
else hi = mid - 1;
|
|
667
|
+
}
|
|
668
|
+
return lo + 1;
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// src/code/diff.ts
|
|
673
|
+
function unifiedDiff(before, after, file, context = 3) {
|
|
674
|
+
if (before === after) return "";
|
|
675
|
+
if (before === null) return newFileDiff(after ?? "", file);
|
|
676
|
+
if (after === null) return deletedFileDiff(before, file);
|
|
677
|
+
const a = splitLines(before);
|
|
678
|
+
const b = splitLines(after);
|
|
679
|
+
const ops = diffLines(a, b);
|
|
680
|
+
const hunks = groupHunks(ops, context);
|
|
681
|
+
if (hunks.length === 0) return "";
|
|
682
|
+
const out = [`--- a/${file}`, `+++ b/${file}`];
|
|
683
|
+
for (const h of hunks) {
|
|
684
|
+
out.push(`@@ -${h.aStart},${h.aLen} +${h.bStart},${h.bLen} @@`);
|
|
685
|
+
for (const line of h.lines) out.push(line);
|
|
686
|
+
}
|
|
687
|
+
return out.join("\n");
|
|
688
|
+
}
|
|
689
|
+
function newFileDiff(after, file) {
|
|
690
|
+
const lines = splitLines(after);
|
|
691
|
+
const out = [`--- /dev/null`, `+++ b/${file}`, `@@ -0,0 +1,${lines.length} @@`];
|
|
692
|
+
for (const l of lines) out.push(`+${l}`);
|
|
693
|
+
return out.join("\n");
|
|
694
|
+
}
|
|
695
|
+
function deletedFileDiff(before, file) {
|
|
696
|
+
const lines = splitLines(before);
|
|
697
|
+
const out = [`--- a/${file}`, `+++ /dev/null`, `@@ -1,${lines.length} +0,0 @@`];
|
|
698
|
+
for (const l of lines) out.push(`-${l}`);
|
|
699
|
+
return out.join("\n");
|
|
700
|
+
}
|
|
701
|
+
function splitLines(s) {
|
|
702
|
+
if (s === "") return [];
|
|
703
|
+
const lines = s.split("\n");
|
|
704
|
+
if (lines.length && lines[lines.length - 1] === "") lines.pop();
|
|
705
|
+
return lines;
|
|
706
|
+
}
|
|
707
|
+
function diffLines(a, b) {
|
|
708
|
+
const n = a.length;
|
|
709
|
+
const m = b.length;
|
|
710
|
+
const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
|
|
711
|
+
for (let i2 = n - 1; i2 >= 0; i2--) {
|
|
712
|
+
for (let j2 = m - 1; j2 >= 0; j2--) {
|
|
713
|
+
dp[i2][j2] = a[i2] === b[j2] ? dp[i2 + 1][j2 + 1] + 1 : Math.max(dp[i2 + 1][j2], dp[i2][j2 + 1]);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
const ops = [];
|
|
717
|
+
let i = 0;
|
|
718
|
+
let j = 0;
|
|
719
|
+
while (i < n && j < m) {
|
|
720
|
+
if (a[i] === b[j]) {
|
|
721
|
+
ops.push({ tag: "eq", line: a[i] });
|
|
722
|
+
i++;
|
|
723
|
+
j++;
|
|
724
|
+
} else if (dp[i + 1][j] >= dp[i][j + 1]) {
|
|
725
|
+
ops.push({ tag: "del", line: a[i] });
|
|
726
|
+
i++;
|
|
727
|
+
} else {
|
|
728
|
+
ops.push({ tag: "add", line: b[j] });
|
|
729
|
+
j++;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
while (i < n) ops.push({ tag: "del", line: a[i++] });
|
|
733
|
+
while (j < m) ops.push({ tag: "add", line: b[j++] });
|
|
734
|
+
return ops;
|
|
735
|
+
}
|
|
736
|
+
function groupHunks(ops, context) {
|
|
737
|
+
const changeIdx = ops.map((o, i) => o.tag === "eq" ? -1 : i).filter((i) => i >= 0);
|
|
738
|
+
if (changeIdx.length === 0) return [];
|
|
739
|
+
const windows = [];
|
|
740
|
+
let start = Math.max(0, changeIdx[0] - context);
|
|
741
|
+
let end = Math.min(ops.length - 1, changeIdx[0] + context);
|
|
742
|
+
for (let k = 1; k < changeIdx.length; k++) {
|
|
743
|
+
const cs = Math.max(0, changeIdx[k] - context);
|
|
744
|
+
const ce = Math.min(ops.length - 1, changeIdx[k] + context);
|
|
745
|
+
if (cs <= end + 1) {
|
|
746
|
+
end = ce;
|
|
747
|
+
} else {
|
|
748
|
+
windows.push([start, end]);
|
|
749
|
+
start = cs;
|
|
750
|
+
end = ce;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
windows.push([start, end]);
|
|
754
|
+
const hunks = [];
|
|
755
|
+
let aLine = 1;
|
|
756
|
+
let bLine = 1;
|
|
757
|
+
let idx = 0;
|
|
758
|
+
for (const [ws, we] of windows) {
|
|
759
|
+
while (idx < ws) {
|
|
760
|
+
const o = ops[idx];
|
|
761
|
+
if (o.tag !== "add") aLine++;
|
|
762
|
+
if (o.tag !== "del") bLine++;
|
|
763
|
+
idx++;
|
|
764
|
+
}
|
|
765
|
+
const hunk = { aStart: aLine, aLen: 0, bStart: bLine, bLen: 0, lines: [] };
|
|
766
|
+
for (let k = ws; k <= we; k++) {
|
|
767
|
+
const o = ops[k];
|
|
768
|
+
if (o.tag === "eq") {
|
|
769
|
+
hunk.lines.push(` ${o.line}`);
|
|
770
|
+
hunk.aLen++;
|
|
771
|
+
hunk.bLen++;
|
|
772
|
+
aLine++;
|
|
773
|
+
bLine++;
|
|
774
|
+
} else if (o.tag === "del") {
|
|
775
|
+
hunk.lines.push(`-${o.line}`);
|
|
776
|
+
hunk.aLen++;
|
|
777
|
+
aLine++;
|
|
778
|
+
} else {
|
|
779
|
+
hunk.lines.push(`+${o.line}`);
|
|
780
|
+
hunk.bLen++;
|
|
781
|
+
bLine++;
|
|
782
|
+
}
|
|
783
|
+
idx = k + 1;
|
|
784
|
+
}
|
|
785
|
+
if (hunk.aLen === 0) hunk.aStart = Math.max(0, hunk.aStart - 1);
|
|
786
|
+
if (hunk.bLen === 0) hunk.bStart = Math.max(0, hunk.bStart - 1);
|
|
787
|
+
hunks.push(hunk);
|
|
788
|
+
}
|
|
789
|
+
return hunks;
|
|
790
|
+
}
|
|
791
|
+
function summarizeDiffs(diffs) {
|
|
792
|
+
let adds = 0;
|
|
793
|
+
let dels = 0;
|
|
794
|
+
let touched = 0;
|
|
795
|
+
for (const { diff } of diffs) {
|
|
796
|
+
if (!diff) continue;
|
|
797
|
+
touched++;
|
|
798
|
+
for (const line of diff.split("\n")) {
|
|
799
|
+
if (line.startsWith("+") && !line.startsWith("+++")) adds++;
|
|
800
|
+
else if (line.startsWith("-") && !line.startsWith("---")) dels++;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
return `+${adds} -${dels} across ${touched} file(s)`;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// src/code/prompt.ts
|
|
807
|
+
var CODE_SYSTEM_PROMPT = [
|
|
808
|
+
"You are a precise code-editing assistant operating inside the Vibgrate CLI.",
|
|
809
|
+
"You are given repository context derived from a deterministic code graph, then a task.",
|
|
810
|
+
"Propose the smallest correct change. Do not restate unchanged code.",
|
|
811
|
+
"",
|
|
812
|
+
"Reply ONLY with edit blocks in this exact format, one per change:",
|
|
813
|
+
"",
|
|
814
|
+
"<path/to/file>",
|
|
815
|
+
"<<<<<<< SEARCH",
|
|
816
|
+
"<exact current lines to find>",
|
|
817
|
+
"=======",
|
|
818
|
+
"<replacement lines>",
|
|
819
|
+
">>>>>>> REPLACE",
|
|
820
|
+
"",
|
|
821
|
+
"The SEARCH text must match the current file (whitespace differences are tolerated).",
|
|
822
|
+
"To add a new file use:",
|
|
823
|
+
"CREATE <path/to/file>",
|
|
824
|
+
"<full file body>",
|
|
825
|
+
"END CREATE",
|
|
826
|
+
"To remove a file use: DELETE <path/to/file>",
|
|
827
|
+
"",
|
|
828
|
+
"Respect every hard constraint in the context. Keep edits minimal and self-consistent."
|
|
829
|
+
].join("\n");
|
|
830
|
+
function buildMessages(context) {
|
|
831
|
+
return [
|
|
832
|
+
{ role: "system", content: CODE_SYSTEM_PROMPT },
|
|
833
|
+
{ role: "user", content: context.rendered },
|
|
834
|
+
{ role: "user", content: `Now produce the edit blocks for: ${context.instruction}` }
|
|
835
|
+
];
|
|
836
|
+
}
|
|
837
|
+
var AGENT_SYSTEM_PROMPT = [
|
|
838
|
+
"You are VG Code, a precise coding agent working inside a real repository.",
|
|
839
|
+
"You have tools: search_code (searches a deterministic code graph \u2014 prefer it over guessing),",
|
|
840
|
+
"read_file, list_files, graph_impact (blast radius of a change), edit_file, create_file,",
|
|
841
|
+
"delete_file, run_command, and finish.",
|
|
842
|
+
"",
|
|
843
|
+
"Work in small steps: search or read to understand before you edit; check graph_impact before",
|
|
844
|
+
"changing shared code; make the smallest correct edit; run the tests or build to verify when useful.",
|
|
845
|
+
"edit_file replaces an exact snippet \u2014 the search text must match the current file (whitespace is",
|
|
846
|
+
"tolerated). Do not restate unchanged code. When the task is complete, call finish with a short summary.",
|
|
847
|
+
"",
|
|
848
|
+
"Editing, creating, deleting, and running commands require user approval \u2014 expect some to be declined,",
|
|
849
|
+
"and adapt if so. Never invent file contents; read first."
|
|
850
|
+
].join("\n");
|
|
851
|
+
function buildAgentMessages(context) {
|
|
852
|
+
return [
|
|
853
|
+
{ role: "system", content: AGENT_SYSTEM_PROMPT },
|
|
854
|
+
{ role: "user", content: context.rendered },
|
|
855
|
+
{ role: "user", content: `Task: ${context.instruction}` }
|
|
856
|
+
];
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
export { LocalLlamaProvider, MockProvider, OPENAI_COMPATIBLE, OllamaProvider, OpenAiCompatibleProvider, applyEdit, applyEdits, buildAgentMessages, buildCodeContext, buildMessages, parseEdits, redactSecrets, summarizeDiffs, unifiedDiff };
|
|
860
|
+
//# sourceMappingURL=chunk-CS37OBE3.js.map
|
|
861
|
+
//# sourceMappingURL=chunk-CS37OBE3.js.map
|