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