pi-supernova 0.2.0 → 0.3.0
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/README.md +196 -196
- package/{CHANGELOG.md → docs/CHANGELOG.md} +44 -1
- package/index.js +81 -68
- package/package.json +12 -31
- package/{catalog.js → src/bridge/catalog.js} +7 -5
- package/{host-bridge.js → src/bridge/host-bridge.js} +232 -87
- package/src/bridge/native-tools.js +155 -0
- package/src/bridge/pi-extension.ts +2 -0
- package/{config.js → src/config/config.js} +1 -1
- package/{evidence.js → src/context/evidence.js} +23 -12
- package/{outline.js → src/context/outline.js} +1 -1
- package/{repo-index.js → src/context/repo-index.js} +11 -9
- package/{search.js → src/context/search.js} +34 -1
- package/{snap.js → src/context/snap.js} +51 -31
- package/{surface.js → src/context/surface.js} +1 -1
- package/{diff.js → src/fs/diff.js} +7 -5
- package/{patch.js → src/fs/patch.js} +1 -1
- package/{vfs.js → src/fs/vfs.js} +55 -14
- package/{workspace.js → src/fs/workspace.js} +22 -6
- package/{bottleneck.js → src/output/bottleneck.js} +23 -6
- package/{format.js → src/output/format.js} +20 -1
- package/{guest-worker.js → src/runtime/guest-worker.js} +90 -21
- package/{parallel.js → src/runtime/parallel.js} +68 -1
- package/{runtime.js → src/runtime/runtime.js} +14 -5
- package/{omp-frame.js → src/ui/omp-frame.js} +1 -1
- package/{render-measure.js → src/ui/render-measure.js} +27 -1
- package/{render.js → src/ui/render.js} +42 -20
- /package/{config.default.json → src/config/config.default.json} +0 -0
- /package/{fuzzy.js → src/context/fuzzy.js} +0 -0
- /package/{ledger.js → src/context/ledger.js} +0 -0
- /package/{check.js → src/fs/check.js} +0 -0
- /package/{decode.js → src/shared/decode.js} +0 -0
|
@@ -1,23 +1,24 @@
|
|
|
1
1
|
|
|
2
2
|
import * as fs from "node:fs/promises";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { packageHostResult, hostResultFailed } from "../output/bottleneck.js";
|
|
6
|
+
import { isString, isNumber, isFunction, isObject } from "../shared/decode.js";
|
|
7
|
+
import { isMutatingTool, runParallelWave, createNativeScheduler } from "../runtime/parallel.js";
|
|
7
8
|
import { unknownToolMessage } from "./catalog.js";
|
|
8
|
-
import { extractStructuralSurface } from "
|
|
9
|
-
import { buildEditDiff, buildMultiEditDiff, buildPatchDiff, buildWriteDiff } from "
|
|
10
|
-
import { executeSnap } from "
|
|
11
|
-
import { selectEvidence } from "
|
|
12
|
-
import { WorkspaceIndex } from "
|
|
13
|
-
import { outlineFile } from "
|
|
14
|
-
import { SeenLedger } from "
|
|
15
|
-
import { quickCheck } from "
|
|
16
|
-
import { declaredName } from "
|
|
17
|
-
import { CausalVfs } from "
|
|
18
|
-
import { applyPatchToText } from "
|
|
19
|
-
import { resolveWorkspacePath, runCommand, clearPathCache, relativeSlash } from "
|
|
20
|
-
import { fuzzyFind, grepIndexed, listIndexed, listWithTools, rgGrepArgs } from "
|
|
9
|
+
import { extractStructuralSurface } from "../context/surface.js";
|
|
10
|
+
import { buildEditDiff, buildMultiEditDiff, buildPatchDiff, buildWriteDiff } from "../fs/diff.js";
|
|
11
|
+
import { executeSnap } from "../context/snap.js";
|
|
12
|
+
import { selectEvidence } from "../context/evidence.js";
|
|
13
|
+
import { WorkspaceIndex } from "../context/repo-index.js";
|
|
14
|
+
import { outlineFile } from "../context/outline.js";
|
|
15
|
+
import { SeenLedger } from "../context/ledger.js";
|
|
16
|
+
import { quickCheck } from "../fs/check.js";
|
|
17
|
+
import { declaredName } from "../context/repo-index.js";
|
|
18
|
+
import { CausalVfs } from "../fs/vfs.js";
|
|
19
|
+
import { applyPatchToText } from "../fs/patch.js";
|
|
20
|
+
import { resolveWorkspacePath, runCommand, clearPathCache, relativeSlash } from "../fs/workspace.js";
|
|
21
|
+
import { fuzzyFind, grepIndexed, listIndexed, listWithTools, rgGrepArgs, referencesForNames } from "../context/search.js";
|
|
21
22
|
|
|
22
23
|
function textResult(text, details) {
|
|
23
24
|
return {
|
|
@@ -55,9 +56,15 @@ function looksLikePath(target) {
|
|
|
55
56
|
);
|
|
56
57
|
}
|
|
57
58
|
|
|
59
|
+
function resolveReadPath(cwd, target) {
|
|
60
|
+
if (!isString(target) || !target.trim()) throw new Error("read requires path");
|
|
61
|
+
const input = target.trim();
|
|
62
|
+
return path.resolve(cwd, input === "~" ? homedir() : input.startsWith("~/") ? path.join(homedir(), input.slice(2)) : input);
|
|
63
|
+
}
|
|
64
|
+
|
|
58
65
|
async function probeExistingPath(cwd, targetParam, vfs) {
|
|
59
|
-
const targetPath =
|
|
60
|
-
if (vfs.getOverlay(targetPath) !== undefined
|
|
66
|
+
const targetPath = resolveReadPath(cwd, targetParam);
|
|
67
|
+
if (vfs.getOverlay(targetPath) !== undefined) return { path: targetPath, directory: false };
|
|
61
68
|
try {
|
|
62
69
|
const st = await fs.stat(targetPath);
|
|
63
70
|
return { path: targetPath, directory: st.isDirectory() };
|
|
@@ -115,14 +122,30 @@ async function formatLsEntry(dirPath, entry) {
|
|
|
115
122
|
return formatDirectoryEntry(entry.name, typeLabel, size);
|
|
116
123
|
}
|
|
117
124
|
|
|
118
|
-
function createNativeAdapters(getCwd, vfs, config, index, ledger) {
|
|
119
|
-
|
|
125
|
+
function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
|
|
126
|
+
const reads = createNativeScheduler();
|
|
127
|
+
async function sourceRead(query, searchDir, signal, params = {}) {
|
|
120
128
|
const cwd = getCwd();
|
|
121
129
|
const includeHidden = path.relative(cwd, searchDir).split(path.sep)
|
|
122
130
|
.some(segment => segment.startsWith(".") && segment.length > 1);
|
|
123
|
-
const result = await executeSnap({ query, searchDir, root: cwd, includeHidden,
|
|
131
|
+
const result = await executeSnap({ query, searchDir, root: cwd, includeHidden,
|
|
132
|
+
pathContext: { frecency: index.frecency, currentFile: index.lastTouched },
|
|
124
133
|
overlayText: p => vfs.getOverlay(p), pendingPaths: vfs.getOverlayPaths(), signal });
|
|
125
|
-
return
|
|
134
|
+
return openSource(result, params, signal);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function openSource(result, params, signal) {
|
|
138
|
+
const cwd = getCwd();
|
|
139
|
+
if (result.status !== "found") return textResult(JSON.stringify(result), { isSnap: true });
|
|
140
|
+
signal?.throwIfAborted();
|
|
141
|
+
const opened = await readFile(path.resolve(cwd, result.path), { ...params, about: undefined }, result.line);
|
|
142
|
+
const block = opened.content[0];
|
|
143
|
+
if (block.type !== "text") throw new Error("source resolution requires a text file; read the image path directly");
|
|
144
|
+
const { firstLine, lastLine, sourceChars, nextOffset, complete } = opened.details;
|
|
145
|
+
const source = { status: "found", path: result.path, line: result.line, lines: [firstLine, lastLine],
|
|
146
|
+
text: block.text.slice(0, sourceChars), complete, nextOffset };
|
|
147
|
+
return textResult(params.resolve ? JSON.stringify(source) : "// " + result.path + ":" + firstLine + "-" + lastLine + "\n" + block.text,
|
|
148
|
+
{ ...opened.details, isSnap: true });
|
|
126
149
|
}
|
|
127
150
|
|
|
128
151
|
async function readDirectory(dirPath, signal) {
|
|
@@ -150,28 +173,55 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger) {
|
|
|
150
173
|
const cwd = getCwd();
|
|
151
174
|
const targetParam = params?.path ?? params?.target;
|
|
152
175
|
if (Array.isArray(targetParam)) {
|
|
153
|
-
|
|
154
|
-
|
|
176
|
+
if (targetParam.some(p => !isString(p) || !p.trim())) throw new Error("read paths must be non-empty strings");
|
|
177
|
+
if (targetParam.length > 64) throw new Error("read accepts at most 64 paths per batch");
|
|
178
|
+
const results = await Promise.all(targetParam.map(async p => {
|
|
179
|
+
try {
|
|
180
|
+
const block = (await readAdapter({ ...params, path: p }, signal)).content[0];
|
|
181
|
+
return { text: block.type === "image" ? block : block.text };
|
|
182
|
+
} catch (error) {
|
|
183
|
+
signal?.throwIfAborted();
|
|
184
|
+
return { text: `[read error: ${p}] ${error.message}`, error: { path: p, message: error.message } };
|
|
185
|
+
}
|
|
186
|
+
}));
|
|
187
|
+
signal?.throwIfAborted();
|
|
188
|
+
return textResult("", { count: results.length, batch: true, independent: params._independent === true, items: results.map(r => r.text), itemErrors: results.map(r => r.error?.message ?? null), errors: results.filter(r => r.error).map(r => r.error) });
|
|
189
|
+
}
|
|
190
|
+
return reads.schedule("read", () => readSingle(params, cwd, targetParam, signal), signal);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function readSingle(params, cwd, targetParam, signal) {
|
|
194
|
+
if (isString(params?.query)) {
|
|
195
|
+
const scope = targetParam && targetParam !== params.query ? resolveReadPath(cwd, targetParam) : cwd;
|
|
196
|
+
return sourceRead(params.query, scope, signal, params);
|
|
155
197
|
}
|
|
156
198
|
const existing = await probeExistingPath(cwd, targetParam, vfs);
|
|
157
199
|
if (existing) {
|
|
158
|
-
if (!existing.directory) return
|
|
159
|
-
|
|
200
|
+
if (!existing.directory) return params.resolve
|
|
201
|
+
? openSource({ status: "found", path: relativeSlash(cwd, existing.path), line: params.offset ?? 1 }, params, signal)
|
|
202
|
+
: readFile(existing.path, params);
|
|
203
|
+
return isString(params?.about) ? sourceRead(params.about, existing.path, signal, params) : readDirectory(existing.path, signal);
|
|
160
204
|
}
|
|
161
|
-
if (!looksLikePath(targetParam)) return sourceRead(targetParam, cwd, signal);
|
|
162
|
-
const targetPath =
|
|
205
|
+
if (!looksLikePath(targetParam)) return sourceRead(targetParam, cwd, signal, params);
|
|
206
|
+
const targetPath = resolveReadPath(cwd, targetParam);
|
|
163
207
|
return readFile(targetPath, params);
|
|
164
208
|
}
|
|
165
209
|
|
|
166
210
|
/** Plain text, a line window, or (with `about`) a relevance-folded outline of the whole file. */
|
|
167
|
-
async function readFile(targetPath, params) {
|
|
211
|
+
async function readFile(targetPath, params, sourceLine) {
|
|
168
212
|
const cwd = getCwd();
|
|
169
213
|
const rel = relativeSlash(cwd, targetPath);
|
|
214
|
+
const mime = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp", ".bmp": "image/bmp" }[path.extname(targetPath).toLowerCase()];
|
|
215
|
+
if (mime) {
|
|
216
|
+
if ((await fs.stat(targetPath)).size > 20 * 1024 * 1024) throw new Error("image exceeds 20 MiB; resize it before reading");
|
|
217
|
+
const staged = vfs.getOverlay(targetPath);
|
|
218
|
+
const bytes = staged === undefined ? await fs.readFile(targetPath) : Buffer.from(staged);
|
|
219
|
+
return { content: [{ type: "image", mimeType: mime, data: bytes.toString("base64") }], details: { path: targetPath } };
|
|
220
|
+
}
|
|
170
221
|
const text = await vfs.read(targetPath);
|
|
171
222
|
index.touch(rel);
|
|
172
223
|
if (isString(params?.about)) {
|
|
173
|
-
const
|
|
174
|
-
const entry = pending === undefined ? index.entry(targetPath) : WorkspaceIndex.fromText(targetPath, pending);
|
|
224
|
+
const entry = WorkspaceIndex.fromText(targetPath, text);
|
|
175
225
|
const outline = entry && outlineFile(entry, rel, params.about, outlineOptions(params, await referenceFinder(cwd, targetPath)));
|
|
176
226
|
if (outline) {
|
|
177
227
|
recordOutlineOrigins(rel, outline.text);
|
|
@@ -179,65 +229,115 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger) {
|
|
|
179
229
|
}
|
|
180
230
|
}
|
|
181
231
|
const explicit = isNumber(params?.offset) || isNumber(params?.limit);
|
|
182
|
-
const
|
|
183
|
-
const
|
|
232
|
+
const budget = Math.max(1, Math.min(config.maxCallResultChars ?? 65536, config.maxReturnChars ?? 32000) - (params.resolve ? 1024 : 256));
|
|
233
|
+
const offset = params?.offset ?? (sourceLine && text.length > budget ? Math.max(1, sourceLine - 2) : 1);
|
|
234
|
+
const firstLine = isNumber(offset) ? Math.max(1, Math.floor(offset)) : 1;
|
|
235
|
+
const sliced = sliceLines(text, offset, params?.limit);
|
|
236
|
+
if (sliced.length > budget || (params.resolve && JSON.stringify(sliced).length > budget)) {
|
|
237
|
+
let cap = budget - 160;
|
|
238
|
+
if (params.resolve) {
|
|
239
|
+
// Budget the actual JSON string, not a pessimistic fixed escape multiplier.
|
|
240
|
+
let low = 0, high = Math.max(0, cap);
|
|
241
|
+
while (low < high) {
|
|
242
|
+
const mid = Math.ceil((low + high) / 2);
|
|
243
|
+
if (JSON.stringify(sliced.slice(0, mid)).length <= budget - 160) low = mid;
|
|
244
|
+
else high = mid - 1;
|
|
245
|
+
}
|
|
246
|
+
cap = low;
|
|
247
|
+
}
|
|
248
|
+
const end = sliced.lastIndexOf("\n", cap);
|
|
249
|
+
if (end < 0) throw new Error(`line ${firstLine} exceeds the read budget; use bash to inspect a bounded substring`);
|
|
250
|
+
const body = sliced.slice(0, end + 1);
|
|
251
|
+
const next = firstLine + body.split("\n").length - 1;
|
|
252
|
+
return textResult(body + `\n[read truncated; continue with read({path:${JSON.stringify(rel)}, offset:${next}})]`, { path: targetPath, outputTruncated: true, nextOffset: next, firstLine, lastLine: next - 1, sourceChars: body.length, complete: false });
|
|
253
|
+
}
|
|
184
254
|
ledger.recordOrigin(rel, firstLine, sliced.split("\n"), explicit);
|
|
185
|
-
return textResult(sliced, { path: targetPath });
|
|
255
|
+
return textResult(sliced, { path: targetPath, firstLine, lastLine: firstLine + sliced.split("\n").length - 1 - Number(sliced.endsWith("\n")), sourceChars: sliced.length, complete: sliced === text });
|
|
186
256
|
}
|
|
187
257
|
|
|
188
258
|
/**
|
|
189
259
|
* The edit result answers the follow-ups a model would otherwise spend turns on: the post-edit
|
|
190
|
-
* lines with numbers
|
|
191
|
-
*
|
|
260
|
+
* lines with numbers, a quick structural check, and bounded lexical reference hints.
|
|
261
|
+
* These do not replace tests or semantic caller resolution.
|
|
192
262
|
*/
|
|
193
|
-
async function editSummary(cwd, target, original, updated, diff) {
|
|
263
|
+
async function editSummary(cwd, target, original, updated, diff, signal) {
|
|
194
264
|
const rel = relativeSlash(cwd, target);
|
|
195
265
|
const newLines = updated.split("\n");
|
|
196
|
-
const
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
266
|
+
const ranges = [];
|
|
267
|
+
const positions = diff.lines.filter(row => row.type !== "context")
|
|
268
|
+
.map(row => Math.min(newLines.length, row.newLineNum ?? row.lineNum)).sort((a, b) => a - b);
|
|
269
|
+
for (const line of positions) {
|
|
270
|
+
const start = Math.max(1, line - 2), end = Math.min(newLines.length, line + 2);
|
|
271
|
+
if (ranges.length && start <= ranges.at(-1).end + 1) ranges.at(-1).end = Math.max(ranges.at(-1).end, end);
|
|
272
|
+
else ranges.push({ start, end });
|
|
273
|
+
}
|
|
274
|
+
const blocks = [];
|
|
275
|
+
const perRange = Math.max(1, Math.floor(40 / Math.max(1, ranges.length)));
|
|
276
|
+
for (const { start, end } of ranges) {
|
|
277
|
+
const last = Math.min(end, start + perRange - 1);
|
|
278
|
+
const lines = newLines.slice(start - 1, last);
|
|
279
|
+
ledger.recordOrigin(rel, start, lines);
|
|
280
|
+
blocks.push("edited " + rel + ":" + start + "-" + last + "\n" + lines.map((line, i) => String(start + i).padStart(5) + " " + line).join("\n"));
|
|
281
|
+
if (last < end) blocks.push("[continue with read({path:" + JSON.stringify(rel) + ",offset:" + (last + 1) + ",limit:" + (end - last) + "})]");
|
|
282
|
+
}
|
|
283
|
+
let out = blocks.join("\n");
|
|
203
284
|
const check = quickCheck(updated, path.extname(target));
|
|
204
285
|
if (check && !check.ok) out += `\ncheck: ${check.message}`;
|
|
205
|
-
const refs = await changedDeclarationRefs(cwd, target, original, updated, diff);
|
|
286
|
+
const refs = await changedDeclarationRefs(cwd, target, original, updated, diff, signal);
|
|
206
287
|
if (refs) out += `\n${refs}`;
|
|
207
288
|
return out;
|
|
208
289
|
}
|
|
209
290
|
|
|
210
|
-
async function changedDeclarationRefs(cwd, target, original, updated, diff) {
|
|
291
|
+
async function changedDeclarationRefs(cwd, target, original, updated, diff, signal) {
|
|
211
292
|
// Diff rows carry the replaced fragments; declarations live on whole file lines.
|
|
212
293
|
const oldLines = original.split("\n");
|
|
213
294
|
const newLines = updated.split("\n");
|
|
214
295
|
const names = new Set();
|
|
296
|
+
const spans = new Map();
|
|
215
297
|
for (const l of diff.lines) {
|
|
216
298
|
if (l.type === "context") continue;
|
|
217
|
-
const
|
|
299
|
+
const number = l.type === "remove" ? l.lineNum : l.newLineNum ?? l.lineNum;
|
|
300
|
+
const name = declaredName((l.type === "remove" ? oldLines : newLines)[number - 1] ?? "");
|
|
218
301
|
if (name) names.add(name);
|
|
302
|
+
else {
|
|
303
|
+
if (!spans.has(l.type)) spans.set(l.type, WorkspaceIndex.spansOf(WorkspaceIndex.fromText(target, l.type === "remove" ? original : updated)));
|
|
304
|
+
const owner = spans.get(l.type).find(span => span.start <= number && number <= span.end);
|
|
305
|
+
if (owner?.name) names.add(owner.name);
|
|
306
|
+
}
|
|
307
|
+
if (names.size >= 3) break;
|
|
219
308
|
}
|
|
220
309
|
if (names.size === 0) return "";
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
const
|
|
225
|
-
|
|
310
|
+
try {
|
|
311
|
+
const { references, incomplete } = await referencesForNames({ root: cwd, names: [...names].slice(0, 3),
|
|
312
|
+
excludePath: target, overlayText: file => vfs.getOverlay(file), pendingPaths: vfs.getOverlayPaths(), signal });
|
|
313
|
+
const parts = [];
|
|
314
|
+
for (const [name, refs] of references) {
|
|
315
|
+
if (refs.length) parts.push(name + " also referenced in " + refs.slice(0, 6).join(", ") + (refs.length > 6 ? " (more matches)" : ""));
|
|
316
|
+
}
|
|
317
|
+
if (incomplete) parts.push("references incomplete: search budget reached");
|
|
318
|
+
return parts.join("\n");
|
|
319
|
+
} catch (error) {
|
|
320
|
+
signal?.throwIfAborted();
|
|
321
|
+
return "references unavailable: " + error.message;
|
|
226
322
|
}
|
|
227
|
-
return parts.join("\n");
|
|
228
323
|
}
|
|
229
324
|
|
|
230
|
-
const SOURCE_REF = /((?:[\w.@-]
|
|
325
|
+
const SOURCE_REF = /((?:\/|[A-Za-z]:[\\/])?(?:[\w.@-]+[\\/])*[\w.@-]+\.(?:m?[jt]sx?|c[jt]s|py|rs|go|java|kt|rb|php|c|cc|cpp|h|hpp|cs|swift|json|ya?ml|toml))(?::|\()(\d+)/g;
|
|
231
326
|
|
|
232
|
-
/**
|
|
233
|
-
function sourceWindow(cwd, file, lineNo) {
|
|
234
|
-
const candidate = path.resolve(
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
327
|
+
/** Fresh bounded source window for a diagnostic; no index warmup or stale cached bodies. */
|
|
328
|
+
async function sourceWindow(cwd, commandCwd, file, lineNo) {
|
|
329
|
+
const candidate = path.resolve(commandCwd, file);
|
|
330
|
+
let text, rel;
|
|
331
|
+
try {
|
|
332
|
+
const root = await fs.realpath(cwd);
|
|
333
|
+
if (!candidate.startsWith(path.resolve(cwd) + path.sep) && !candidate.startsWith(root + path.sep)) return null;
|
|
334
|
+
const real = await fs.realpath(candidate);
|
|
335
|
+
if (!real.startsWith(root + path.sep) || (await fs.stat(real)).size > 1024 * 1024) return null;
|
|
336
|
+
text = await fs.readFile(real, "utf8");
|
|
337
|
+
rel = relativeSlash(root, real);
|
|
338
|
+
} catch { return null; }
|
|
339
|
+
const raw = text.split("\n");
|
|
239
340
|
if (lineNo < 1 || lineNo > raw.length) return null;
|
|
240
|
-
const rel = relativeSlash(cwd, candidate);
|
|
241
341
|
const start = Math.max(1, lineNo - 2);
|
|
242
342
|
const rows = [];
|
|
243
343
|
for (let l = start; l <= Math.min(raw.length, lineNo + 2); l++) rows.push((l === lineNo ? "►" : " ") + String(l).padStart(4) + " " + raw[l - 1]);
|
|
@@ -246,16 +346,16 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger) {
|
|
|
246
346
|
}
|
|
247
347
|
|
|
248
348
|
/** A failing command names path:line; the model wants those lines next. Attach them (≤4 sites). */
|
|
249
|
-
function sourceForReferences(cwd, output) {
|
|
349
|
+
async function sourceForReferences(cwd, commandCwd, output) {
|
|
250
350
|
const seen = new Set();
|
|
251
351
|
const blocks = [];
|
|
252
352
|
for (const m of output.matchAll(SOURCE_REF)) {
|
|
253
353
|
const key = m[1] + ":" + m[2];
|
|
254
354
|
if (seen.has(key)) continue;
|
|
355
|
+
if (seen.size >= 4) break;
|
|
255
356
|
seen.add(key);
|
|
256
|
-
const block = sourceWindow(cwd, m[1], Number(m[2]));
|
|
357
|
+
const block = await sourceWindow(cwd, commandCwd, m[1], Number(m[2]));
|
|
257
358
|
if (block) blocks.push(block);
|
|
258
|
-
if (blocks.length >= 4) break;
|
|
259
359
|
}
|
|
260
360
|
return blocks.length ? "\n--- source\n" + blocks.join("\n") : "";
|
|
261
361
|
}
|
|
@@ -276,19 +376,20 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger) {
|
|
|
276
376
|
|
|
277
377
|
/** Where else a name appears (declaration line excluded), for outlines and edit results. */
|
|
278
378
|
async function referenceFinder(cwd, targetPath) {
|
|
279
|
-
const files = await index.files(cwd);
|
|
379
|
+
const files = [...new Set([...await index.files(cwd), ...vfs.getOverlayPaths()])];
|
|
280
380
|
if (!index.canScan(files)) return () => [];
|
|
281
381
|
return (name, excludeLine) => {
|
|
282
382
|
if (!name || name.length < 3) return [];
|
|
283
383
|
const escaped = name.replace(/[$]/g, (c) => "\\" + c);
|
|
284
384
|
const regex = new RegExp("\\b" + escaped + "\\b");
|
|
285
385
|
return index
|
|
286
|
-
.grepRows(files, regex, cwd)
|
|
386
|
+
.grepRows(files, regex, cwd, file => vfs.getOverlay(file))
|
|
287
387
|
.filter((r) => !(r.line === excludeLine && r.rel === relativeSlash(cwd, targetPath)))
|
|
288
388
|
.map((r) => r.rel + ":" + r.line);
|
|
289
389
|
};
|
|
290
390
|
}
|
|
291
391
|
|
|
392
|
+
hooks.summarizeEdit = editSummary;
|
|
292
393
|
return {
|
|
293
394
|
read: readAdapter,
|
|
294
395
|
async write(params, signal) {
|
|
@@ -299,13 +400,15 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger) {
|
|
|
299
400
|
const content = params.content;
|
|
300
401
|
let prevText = "";
|
|
301
402
|
try {
|
|
302
|
-
prevText = await vfs.read(target);
|
|
303
|
-
} catch {}
|
|
403
|
+
prevText = await vfs.read(target, { preserveRead: true });
|
|
404
|
+
} catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
304
405
|
const { speculative } = await vfs.write(target, content);
|
|
305
406
|
index.touch(relativeSlash(cwd, target));
|
|
306
407
|
const diff = buildWriteDiff(target, prevText, content);
|
|
307
408
|
const tag = speculative ? " (speculative)" : "";
|
|
308
|
-
|
|
409
|
+
const check = quickCheck(content, path.extname(target));
|
|
410
|
+
const warning = check && !check.ok ? "\ncheck: " + check.message : "";
|
|
411
|
+
return textResult(`wrote ${target}${tag}${warning}`, { path: target, speculative, diff });
|
|
309
412
|
},
|
|
310
413
|
async edit(params, signal) {
|
|
311
414
|
const cwd = getCwd();
|
|
@@ -323,7 +426,7 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger) {
|
|
|
323
426
|
matches.length === 1
|
|
324
427
|
? buildEditDiff(target, content, matches[0].oldText, matches[0].newText)
|
|
325
428
|
: buildMultiEditDiff(target, content, matches);
|
|
326
|
-
const summary = await editSummary(cwd, target, content, updated, diff);
|
|
429
|
+
const summary = await editSummary(cwd, target, content, updated, diff, signal);
|
|
327
430
|
return textResult(summary, { path: target, speculative, diff });
|
|
328
431
|
},
|
|
329
432
|
async apply_patch(params, signal) {
|
|
@@ -343,8 +446,9 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger) {
|
|
|
343
446
|
const { resultText, hunkCount } = applyPatchToText(original, params.patch);
|
|
344
447
|
const { speculative } = await vfs.write(target, resultText);
|
|
345
448
|
const diff = buildPatchDiff(target, params.patch);
|
|
346
|
-
|
|
347
|
-
|
|
449
|
+
index.touch(relativeSlash(cwd, target));
|
|
450
|
+
const summary = await editSummary(cwd, target, original, resultText, diff, signal);
|
|
451
|
+
return textResult(summary, {
|
|
348
452
|
path: target,
|
|
349
453
|
hunks: hunkCount,
|
|
350
454
|
speculative,
|
|
@@ -367,7 +471,6 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger) {
|
|
|
367
471
|
searchDir: snapTarget,
|
|
368
472
|
root: cwd,
|
|
369
473
|
includeHidden,
|
|
370
|
-
index,
|
|
371
474
|
overlayText: (p) => vfs.getOverlay(p),
|
|
372
475
|
pendingPaths: vfs.getOverlayPaths(),
|
|
373
476
|
signal,
|
|
@@ -397,30 +500,37 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger) {
|
|
|
397
500
|
},
|
|
398
501
|
async bash(params, signal) {
|
|
399
502
|
const cwd = getCwd();
|
|
400
|
-
const
|
|
401
|
-
if (!command) throw new Error("bash requires command");
|
|
503
|
+
const literal = params?._directArgv === true;
|
|
504
|
+
if (literal && (!isString(params.command) || !Array.isArray(params.args) || params.args.some(arg => !isString(arg)))) throw new Error("bash argv requires a command string and an array of string args");
|
|
505
|
+
const command = literal ? String(params.command) : unwrapIfFullyQuoted(String(params?.command ?? "").trim());
|
|
506
|
+
if (!command.trim()) throw new Error("bash requires command");
|
|
402
507
|
const targetCwd = params?.cwd ? await resolveWorkspacePath(cwd, params.cwd, "bash cwd", true) : cwd;
|
|
403
508
|
|
|
404
|
-
//
|
|
405
|
-
|
|
406
|
-
const argv = ["bash", "-c", command];
|
|
509
|
+
// String commands keep shell semantics; literal argv needs no quoting or shell startup.
|
|
510
|
+
const argv = literal ? [command, ...params.args] : ["bash", "-c", command];
|
|
407
511
|
|
|
408
512
|
const transactionBarrier = await vfs.prepareExternalMutation("bash");
|
|
409
513
|
let res;
|
|
410
514
|
try {
|
|
411
515
|
res = await runCommand(argv, {
|
|
412
516
|
cwd: targetCwd,
|
|
517
|
+
env: hooks.commandEnv(),
|
|
518
|
+
commandLabel: literal ? command : undefined,
|
|
413
519
|
timeoutMs: params?.timeoutMs,
|
|
414
520
|
signal,
|
|
415
521
|
maxOutputChars: config.maxCallResultChars,
|
|
416
522
|
});
|
|
523
|
+
} catch (error) {
|
|
524
|
+
if (!signal?.aborted) error.message += await sourceForReferences(cwd, targetCwd, error.message);
|
|
525
|
+
throw error;
|
|
417
526
|
} finally {
|
|
418
527
|
vfs.invalidateCache();
|
|
419
528
|
index.invalidate();
|
|
529
|
+
clearPathCache();
|
|
420
530
|
}
|
|
421
531
|
const { stdout, stderr } = res;
|
|
422
532
|
let text = stdout && stderr ? stdout + (stdout.endsWith("\n") ? "" : "\n") + stderr : stdout || stderr;
|
|
423
|
-
if (res.exitCode !== 0) text += sourceForReferences(cwd, text);
|
|
533
|
+
if (res.exitCode !== 0) text += await sourceForReferences(cwd, targetCwd, text);
|
|
424
534
|
return {
|
|
425
535
|
content: [{ type: "text", text }],
|
|
426
536
|
details: { exitCode: res.exitCode, signal: res.signal, outputTruncated: res.outputTruncated, transactionBarrier },
|
|
@@ -486,12 +596,13 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger) {
|
|
|
486
596
|
export function createHostBridge({ pi, config, getCwd, registry, ledger: runLedger }) {
|
|
487
597
|
const index = registry?.index ?? new WorkspaceIndex((argv, opts) => runCommand(argv, opts));
|
|
488
598
|
const ledger = runLedger ?? new SeenLedger({ window: config.seenWindow ?? 40 });
|
|
489
|
-
const vfs = new CausalVfs(() => index.invalidate());
|
|
599
|
+
const vfs = new CausalVfs(() => index.invalidate(), target => resolveWorkspacePath(getCwd(), target, "commit", false, true));
|
|
490
600
|
const executors = registry?.executors ?? new Map();
|
|
491
601
|
const definitions = registry?.definitions ?? new Map();
|
|
492
602
|
const sharedRegistry = registry ?? { executors, definitions, index, callSeq: 0 };
|
|
493
603
|
let closed = false;
|
|
494
|
-
const
|
|
604
|
+
const hooks = {};
|
|
605
|
+
const natives = createNativeAdapters(getCwd, vfs, config, index, ledger, hooks);
|
|
495
606
|
let callCount = 0;
|
|
496
607
|
let activeCtx = null;
|
|
497
608
|
let hostSession = null;
|
|
@@ -499,6 +610,22 @@ export function createHostBridge({ pi, config, getCwd, registry, ledger: runLedg
|
|
|
499
610
|
let activeSignal = undefined;
|
|
500
611
|
let trace = [];
|
|
501
612
|
let callListener = null;
|
|
613
|
+
const scheduler = createNativeScheduler();
|
|
614
|
+
hooks.commandEnv = () => {
|
|
615
|
+
const env = { ...process.env };
|
|
616
|
+
const current = {
|
|
617
|
+
PI_SESSION_ID: activeCtx?.sessionManager?.getSessionId?.(),
|
|
618
|
+
PI_SESSION_FILE: activeCtx?.sessionManager?.getSessionFile?.(),
|
|
619
|
+
PI_PROVIDER: activeCtx?.model?.provider,
|
|
620
|
+
PI_MODEL: activeCtx?.model?.id,
|
|
621
|
+
PI_REASONING_LEVEL: activeCtx?.thinkingLevel,
|
|
622
|
+
};
|
|
623
|
+
for (const [key, value] of Object.entries(current)) {
|
|
624
|
+
if (isString(value)) env[key] = value;
|
|
625
|
+
else delete env[key];
|
|
626
|
+
}
|
|
627
|
+
return env;
|
|
628
|
+
};
|
|
502
629
|
|
|
503
630
|
if (!registry && pi && isFunction(pi.registerTool)) {
|
|
504
631
|
const original = pi.registerTool.bind(pi);
|
|
@@ -622,7 +749,7 @@ export function createHostBridge({ pi, config, getCwd, registry, ledger: runLedg
|
|
|
622
749
|
callCount += 1;
|
|
623
750
|
if (callCount > maxCalls) {
|
|
624
751
|
throw new Error(
|
|
625
|
-
`host call budget exceeded (${maxCalls} calls per program):
|
|
752
|
+
`host call budget exceeded (${maxCalls} calls per program): split the work across programs`,
|
|
626
753
|
);
|
|
627
754
|
}
|
|
628
755
|
if (activeSignal?.aborted) throw new Error("aborted");
|
|
@@ -634,7 +761,7 @@ export function createHostBridge({ pi, config, getCwd, registry, ledger: runLedg
|
|
|
634
761
|
const excluded = new Set(config.excludeTools || []);
|
|
635
762
|
if (name === "supernova" || excluded.has(name)) {
|
|
636
763
|
throw new Error(
|
|
637
|
-
|
|
764
|
+
`${name} is blocked (excluded / non-reentrant).`,
|
|
638
765
|
);
|
|
639
766
|
}
|
|
640
767
|
}
|
|
@@ -664,7 +791,8 @@ export function createHostBridge({ pi, config, getCwd, registry, ledger: runLedg
|
|
|
664
791
|
assertCallableTarget(name);
|
|
665
792
|
if (!isCallable(name)) throw new Error(unknownToolMessage(name, [...definitions.keys(), ...Object.keys(natives)].filter(isCallable)));
|
|
666
793
|
|
|
667
|
-
const
|
|
794
|
+
const command = { apply_patch: "edit", surface: "read", evidence: "read", snap: "read" }[name] ?? name;
|
|
795
|
+
const record = { name: command, adapter: name, args: args || {}, time: Date.now() };
|
|
668
796
|
trace.push(record);
|
|
669
797
|
notifyCall(record);
|
|
670
798
|
|
|
@@ -714,8 +842,9 @@ export function createHostBridge({ pi, config, getCwd, registry, ledger: runLedg
|
|
|
714
842
|
|
|
715
843
|
async function call(name, args) {
|
|
716
844
|
if (!isString(name) || !name) throw new Error("nova.call requires a tool name");
|
|
717
|
-
const
|
|
718
|
-
|
|
845
|
+
const invoke = async () => packageHostResult(await invokeRaw(name, args), config);
|
|
846
|
+
const kind = isMutatingTool(name, config, args, definitions.get(name)) ? "write" : "read";
|
|
847
|
+
return scheduler.schedule(kind, invoke, activeSignal);
|
|
719
848
|
}
|
|
720
849
|
|
|
721
850
|
async function callMany(calls) {
|
|
@@ -748,6 +877,21 @@ export function createHostBridge({ pi, config, getCwd, registry, ledger: runLedg
|
|
|
748
877
|
isCallable,
|
|
749
878
|
externalNames,
|
|
750
879
|
supportsBatchRead: () => !hostTool("read") && !executors.has("read"),
|
|
880
|
+
// Windows command shims need shell handling; preserve the existing route there.
|
|
881
|
+
supportsNativeArgv: () => process.platform !== "win32" && !hostTool("bash") && !executors.has("bash"),
|
|
882
|
+
summarizeEdit: (target, before, after, diff) => hooks.summarizeEdit(getCwd(), target, before, after, diff),
|
|
883
|
+
invalidateFiles() { vfs.invalidateCache(); index.invalidate(); clearPathCache(); },
|
|
884
|
+
fileOperations: {
|
|
885
|
+
access: (target, mode) => fs.access(target, mode),
|
|
886
|
+
readFile: async target => Buffer.from(await vfs.read(target), "utf8"),
|
|
887
|
+
// VFS owns parent creation and atomic replacement, inside Pi's file queue.
|
|
888
|
+
mkdir: async () => {},
|
|
889
|
+
async writeFile(target, content) {
|
|
890
|
+
await resolveWorkspacePath(getCwd(), target, "write", false);
|
|
891
|
+
await vfs.write(target, content);
|
|
892
|
+
index.touch(relativeSlash(getCwd(), target));
|
|
893
|
+
},
|
|
894
|
+
},
|
|
751
895
|
fork(options) {
|
|
752
896
|
return createHostBridge({ pi, config, getCwd: options.getCwd, registry: sharedRegistry, ledger: ledger.fork() });
|
|
753
897
|
},
|
|
@@ -756,6 +900,7 @@ export function createHostBridge({ pi, config, getCwd, registry, ledger: runLedg
|
|
|
756
900
|
resetCallBudget,
|
|
757
901
|
getTrace,
|
|
758
902
|
setCallListener,
|
|
903
|
+
barrier: run => scheduler.schedule("write", run, activeSignal),
|
|
759
904
|
beginSpeculation,
|
|
760
905
|
commitSpeculation,
|
|
761
906
|
rollbackSpeculation,
|