pi-supernova 0.5.0 → 0.7.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 +97 -11
- package/docs/CHANGELOG.md +150 -0
- package/docs/TOKEN_COSTS.md +71 -29
- package/index.js +126 -82
- package/package.json +2 -2
- package/src/adapters/bash.js +73 -0
- package/src/adapters/edit.js +249 -0
- package/src/adapters/errors.js +31 -0
- package/src/adapters/index.js +31 -0
- package/src/adapters/list.js +102 -0
- package/src/adapters/read.js +805 -0
- package/src/adapters/refs.js +41 -0
- package/src/adapters/write.js +96 -0
- package/src/bridge/catalog.js +30 -220
- package/src/bridge/host-bridge.js +142 -1032
- package/src/bridge/invoke.js +35 -0
- package/src/bridge/native-tools.js +1 -188
- package/src/context/evidence.js +142 -70
- package/src/context/fuzzy.js +61 -22
- package/src/context/ledger.js +43 -24
- package/src/context/outline.js +26 -12
- package/src/context/repo-index.js +242 -71
- package/src/context/search.js +189 -56
- package/src/context/snap.js +306 -150
- package/src/context/spans.js +2 -1
- package/src/context/surface.js +29 -14
- package/src/contract/bash.js +31 -0
- package/src/contract/edit.js +95 -0
- package/src/contract/read.js +220 -0
- package/src/fs/check.js +19 -7
- package/src/fs/diff.js +18 -7
- package/src/fs/json-read.js +66 -35
- package/src/fs/patch.js +97 -51
- package/src/fs/source-window.js +82 -0
- package/src/fs/text-ops.js +512 -0
- package/src/fs/vfs.js +289 -162
- package/src/fs/workspace.js +122 -105
- package/src/output/bottleneck.js +211 -107
- package/src/output/format.js +112 -63
- package/src/runtime/guest-deny-imports.js +34 -0
- package/src/runtime/guest-worker.js +306 -213
- package/src/runtime/parallel.js +99 -63
- package/src/runtime/program-batch.js +189 -69
- package/src/runtime/program-file.js +6 -3
- package/src/runtime/reference.js +13 -12
- package/src/runtime/runtime.js +327 -176
- package/src/shared/decode.js +61 -27
- package/src/ui/omp-frame.js +70 -46
- package/src/ui/render-measure.js +51 -29
- package/src/ui/render.js +242 -146
package/src/context/search.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
1
2
|
import * as path from "node:path";
|
|
2
3
|
import { isString } from "../shared/decode.js";
|
|
3
4
|
import { WorkspaceIndex, globToRegExp } from "./repo-index.js";
|
|
@@ -12,6 +13,63 @@ function textResult(text, details) {
|
|
|
12
13
|
return { content: [{ type: "text", text: String(text ?? "") }], details: details || {} };
|
|
13
14
|
}
|
|
14
15
|
|
|
16
|
+
async function candidateFileList(index, root, includeHidden = false, signal) {
|
|
17
|
+
const stat = await fs.stat(root).catch(() => null);
|
|
18
|
+
|
|
19
|
+
if (stat?.isFile()) return [root];
|
|
20
|
+
|
|
21
|
+
return index.files(root, includeHidden, signal);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function pendingInScope(root, pendingPaths) {
|
|
25
|
+
return pendingPaths.filter(file => {
|
|
26
|
+
const relative = path.relative(root, file);
|
|
27
|
+
|
|
28
|
+
return relative === "" || (relative !== ".." && !relative.startsWith(".." + path.sep) && !path.isAbsolute(relative));
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parseMatchRecord(line, truncated, isLast) {
|
|
33
|
+
if (!line) return { skip: true };
|
|
34
|
+
|
|
35
|
+
try { return { record: JSON.parse(line) }; }
|
|
36
|
+
catch (error) {
|
|
37
|
+
if (truncated && isLast) return { stop: true };
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isRgMatch(record) {
|
|
43
|
+
return record.type === "match" && isString(record.data?.path?.text) && isString(record.data.lines?.text);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function applyMatchRecord(record, root, overlayText, add) {
|
|
47
|
+
if (!isRgMatch(record)) return;
|
|
48
|
+
const file = path.resolve(root, record.data.path.text);
|
|
49
|
+
|
|
50
|
+
if (overlayText(file) === undefined) add(file, record.data.line_number, record.data.lines.text);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function ingestRgMatches(records, result, signal, root, overlayText, add) {
|
|
54
|
+
for (let i = 0; i < records.length; i++) {
|
|
55
|
+
signal?.throwIfAborted();
|
|
56
|
+
const parsed = parseMatchRecord(records[i], result.outputTruncated, i === records.length - 1);
|
|
57
|
+
|
|
58
|
+
if (parsed.stop) break;
|
|
59
|
+
|
|
60
|
+
if (parsed.skip) continue;
|
|
61
|
+
applyMatchRecord(parsed.record, root, overlayText, add);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function ingestPendingRefs(root, pendingPaths, overlayText, add) {
|
|
66
|
+
for (const file of pendingInScope(root, pendingPaths)) {
|
|
67
|
+
const text = overlayText(file);
|
|
68
|
+
|
|
69
|
+
if (text !== undefined && Buffer.byteLength(text, "utf8") <= 512 * 1024) text.split("\n").forEach((line, i) => add(file, i + 1, line));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
15
73
|
/** One bounded direct search for all changed names; no repository index or per-name spawn. */
|
|
16
74
|
export async function referencesForNames({ root, names, excludePath, overlayText, pendingPaths, signal, run = runCommand }) {
|
|
17
75
|
const references = new Map(names.map(name => [name, []]));
|
|
@@ -31,57 +89,90 @@ export async function referencesForNames({ root, names, excludePath, overlayText
|
|
|
31
89
|
{ cwd: root, signal, timeoutMs: 5000, maxOutputChars: 65536 });
|
|
32
90
|
|
|
33
91
|
if (result.exitCode !== 0 && result.exitCode !== 1) throw new Error(result.stderr.trim() || "reference search failed");
|
|
34
|
-
|
|
92
|
+
ingestRgMatches(result.stdout.split("\n"), result, signal, root, overlayText, add);
|
|
93
|
+
ingestPendingRefs(root, pendingPaths, overlayText, add);
|
|
35
94
|
|
|
36
|
-
|
|
37
|
-
|
|
95
|
+
return { references, incomplete: result.outputTruncated === true };
|
|
96
|
+
}
|
|
38
97
|
|
|
39
|
-
|
|
40
|
-
|
|
98
|
+
function grepCaseSensitive(pattern, params) {
|
|
99
|
+
return params?.caseSensitive === true || (params?.caseSensitive !== false && smartCase(pattern));
|
|
100
|
+
}
|
|
41
101
|
|
|
42
|
-
|
|
43
|
-
|
|
102
|
+
function pushGrepFlags(args, pattern, params) {
|
|
103
|
+
if (!grepCaseSensitive(pattern, params)) args.push("--ignore-case");
|
|
44
104
|
|
|
45
|
-
|
|
46
|
-
|
|
105
|
+
if (params?.glob) args.push("--glob", String(params.glob));
|
|
106
|
+
const limit = params?.limit;
|
|
47
107
|
|
|
48
|
-
|
|
49
|
-
|
|
108
|
+
if (Number.isInteger(limit) && limit > 0) args.push("--max-count", String(Math.min(limit, 2000)));
|
|
109
|
+
}
|
|
50
110
|
|
|
51
|
-
|
|
52
|
-
|
|
111
|
+
export function rgGrepArgs(pattern, params, searchPath) {
|
|
112
|
+
const args = ["--line-number", "--no-heading", "--color", "never"];
|
|
113
|
+
pushGrepFlags(args, pattern, params);
|
|
114
|
+
args.push("--", pattern, searchPath);
|
|
53
115
|
|
|
54
|
-
|
|
55
|
-
|
|
116
|
+
return args;
|
|
117
|
+
}
|
|
56
118
|
|
|
57
|
-
|
|
119
|
+
function globMatcher(pattern) {
|
|
120
|
+
if (!pattern) return null;
|
|
121
|
+
|
|
122
|
+
try { return globToRegExp(pattern); }
|
|
123
|
+
catch { return /^$/; }
|
|
58
124
|
}
|
|
59
125
|
|
|
60
|
-
|
|
61
|
-
|
|
126
|
+
function isDirectList(stat, pendingLength) {
|
|
127
|
+
return !!(stat?.isFile() || (!stat?.isDirectory() && pendingLength));
|
|
128
|
+
}
|
|
62
129
|
|
|
63
|
-
|
|
130
|
+
function listDirectRows(stat, searchDir, cwd, pending, matcher) {
|
|
131
|
+
const rel = stat?.isFile() ? relativeSlash(cwd, searchDir) : null;
|
|
64
132
|
|
|
65
|
-
|
|
66
|
-
|
|
133
|
+
return [...new Set([...(rel ? [rel] : []), ...pending])].filter(file => !matcher || matcher.test(file));
|
|
134
|
+
}
|
|
67
135
|
|
|
68
|
-
|
|
136
|
+
function mergeListStdout(stdout, cwd, pendingMerged) {
|
|
137
|
+
const diskRows = String(stdout || "").split("\n").filter(Boolean)
|
|
138
|
+
.map(row => relativeSlash(cwd, path.isAbsolute(row) ? row : path.resolve(cwd, row)));
|
|
139
|
+
const rows = [...new Set([...diskRows, ...pendingMerged])];
|
|
140
|
+
|
|
141
|
+
return rows.length ? rows.join("\n") + "\n" : "";
|
|
69
142
|
}
|
|
70
143
|
|
|
71
|
-
|
|
72
|
-
export async function listWithTools(searchDir, pattern, cwd, signal) {
|
|
144
|
+
async function listDisk(searchDir, pattern, cwd, signal) {
|
|
73
145
|
const args = ["--files"];
|
|
74
146
|
|
|
75
147
|
if (pattern) args.push("-g", pattern);
|
|
76
148
|
const res = await runCommand(["rg", ...args, searchDir], { cwd, timeoutMs: 30_000, signal }).catch(() => null);
|
|
77
149
|
|
|
78
|
-
if (res && (res.exitCode === 0 || res.exitCode === 1)) return
|
|
150
|
+
if (res && (res.exitCode === 0 || res.exitCode === 1)) return { stdout: res.stdout, via: "rg", outputTruncated: res.outputTruncated === true };
|
|
79
151
|
const findArgs = [searchDir];
|
|
80
152
|
|
|
81
153
|
if (pattern) findArgs.push("-name", pattern);
|
|
82
154
|
const findRes = await runCommand(["find", ...findArgs], { cwd, timeoutMs: 30_000, signal });
|
|
83
155
|
|
|
84
|
-
return
|
|
156
|
+
return { stdout: findRes.stdout, via: "find", outputTruncated: findRes.outputTruncated === true };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** rg --files, then find(1) when rg is unavailable; both accept an optional glob/name pattern. */
|
|
160
|
+
export async function listWithTools(searchDir, pattern, cwd, signal, pendingPaths = []) {
|
|
161
|
+
const stat = await fs.stat(searchDir).catch(() => null);
|
|
162
|
+
const pendingAbs = pendingInScope(searchDir, pendingPaths);
|
|
163
|
+
const pending = pendingAbs.map(file => relativeSlash(cwd, file));
|
|
164
|
+
const matcher = globMatcher(pattern);
|
|
165
|
+
|
|
166
|
+
if (isDirectList(stat, pending.length)) {
|
|
167
|
+
const rows = listDirectRows(stat, searchDir, cwd, pending, matcher);
|
|
168
|
+
|
|
169
|
+
return textResult(rows.length ? rows.join("\n") + "\n" : "", { via: pending.length ? "vfs" : "file" });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const pendingMerged = pendingAbs.filter((_, i) => !matcher || matcher.test(pending[i]));
|
|
173
|
+
const listed = await listDisk(searchDir, pattern, cwd, signal);
|
|
174
|
+
|
|
175
|
+
return textResult(mergeListStdout(listed.stdout, cwd, pendingMerged), { via: listed.via, outputTruncated: listed.outputTruncated });
|
|
85
176
|
}
|
|
86
177
|
|
|
87
178
|
const GLOB_CHARS = /[*?[\]{}]/;
|
|
@@ -90,9 +181,9 @@ const GLOB_CHARS = /[*?[\]{}]/;
|
|
|
90
181
|
* fffind: a pattern without glob characters is a fuzzy, typo-tolerant, frecency-ranked path query.
|
|
91
182
|
* Returns "path" rows (best first) or null when the pattern is a real glob.
|
|
92
183
|
*/
|
|
93
|
-
export async function fuzzyFind(index, root, cwd, pattern, limit = 20) {
|
|
184
|
+
export async function fuzzyFind(index, root, cwd, pattern, limit = 20, pendingPaths = []) {
|
|
94
185
|
if (!pattern || GLOB_CHARS.test(pattern)) return null;
|
|
95
|
-
const files = await index
|
|
186
|
+
const files = [...new Set([...await candidateFileList(index, root), ...pendingInScope(root, pendingPaths)])];
|
|
96
187
|
|
|
97
188
|
if (!index.canScan(files)) return null;
|
|
98
189
|
const rel = files.map((f) => relativeSlash(cwd, f));
|
|
@@ -108,29 +199,43 @@ export async function fuzzyFind(index, root, cwd, pattern, limit = 20) {
|
|
|
108
199
|
return rows.map((r) => r.path).join("\n") + "\n";
|
|
109
200
|
}
|
|
110
201
|
|
|
202
|
+
function applyGlob(files, params, cwd) {
|
|
203
|
+
if (!params?.glob) return files;
|
|
204
|
+
const matcher = globToRegExp(String(params.glob));
|
|
205
|
+
|
|
206
|
+
return files.filter((f) => matcher.test(relativeSlash(cwd, f)));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function filesReadable(index, files, overlayText) {
|
|
210
|
+
for (const file of files) {
|
|
211
|
+
const overlay = overlayText(file);
|
|
212
|
+
|
|
213
|
+
if (overlay === undefined && index.entry(file) === null) return false;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
|
|
111
219
|
/** fff-style grep: smart-case, definition lines first, fuzzy fallback when the literal has no hits. */
|
|
112
|
-
export async function grepIndexed(index, pattern, params, searchPath, cwd) {
|
|
220
|
+
export async function grepIndexed(index, pattern, params, searchPath, cwd, overlayText = () => undefined, pendingPaths = []) {
|
|
113
221
|
const compiled = grepRegex(pattern, params);
|
|
114
222
|
|
|
115
223
|
if (!compiled) return null;
|
|
116
224
|
const { regex, caseSensitive } = compiled;
|
|
117
|
-
let files = await index
|
|
225
|
+
let files = [...new Set([...await candidateFileList(index, searchPath), ...pendingInScope(searchPath, pendingPaths)])];
|
|
118
226
|
|
|
119
227
|
if (!index.canScan(files)) return null;
|
|
228
|
+
files = applyGlob(files, params, cwd);
|
|
120
229
|
|
|
121
|
-
if (
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const rows = index.grepRows(files, regex, cwd);
|
|
127
|
-
const fallback = rows.length === 0 && /^[\w$.-]{4,}$/.test(pattern) ? fuzzyGrepRows(index, files, pattern, cwd, caseSensitive) : rows;
|
|
230
|
+
if (!filesReadable(index, files, overlayText)) return null;
|
|
231
|
+
const rows = index.grepRows(files, regex, cwd, overlayText);
|
|
232
|
+
const fallback = rows.length === 0 && /^[\w$.-]{4,}$/.test(pattern) ? fuzzyGrepRows(index, files, pattern, cwd, caseSensitive, overlayText) : rows;
|
|
128
233
|
|
|
129
234
|
return formatGrepRows(fallback, grepLimit(params));
|
|
130
235
|
}
|
|
131
236
|
|
|
132
237
|
function grepLimit(params) {
|
|
133
|
-
return Number.isInteger(params?.limit) && params.limit > 0 ? params.limit : 200;
|
|
238
|
+
return Number.isInteger(params?.limit) && params.limit > 0 ? Math.min(params.limit, 2000) : 200;
|
|
134
239
|
}
|
|
135
240
|
|
|
136
241
|
function grepRegex(pattern, params) {
|
|
@@ -143,32 +248,45 @@ function grepRegex(pattern, params) {
|
|
|
143
248
|
}
|
|
144
249
|
}
|
|
145
250
|
|
|
251
|
+
function overlaySearchEntry(index, filePath, overlayText) {
|
|
252
|
+
const pending = overlayText(filePath);
|
|
253
|
+
|
|
254
|
+
return pending === undefined
|
|
255
|
+
? index.entry(filePath)
|
|
256
|
+
: Buffer.byteLength(pending, "utf8") <= 512 * 1024 ? WorkspaceIndex.fromText(filePath, pending) : null;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function fuzzyLineRow(pattern, rawLine, defName, rel, line, maxTypos, caseSensitive) {
|
|
260
|
+
const m = fuzzyMatch(pattern, rawLine, { maxTypos, caseSensitive });
|
|
261
|
+
|
|
262
|
+
if (!m || m.end - m.start > pattern.length + 2) return null;
|
|
263
|
+
|
|
264
|
+
return { rel, line, text: rawLine, def: defName !== "" && fuzzyMatch(pattern, defName, { maxTypos }) !== null };
|
|
265
|
+
}
|
|
266
|
+
|
|
146
267
|
/** Zero literal hits: retry each line fuzzily (1 typo, 2 for long names) within a tight span, so IsOffTheRecord finds is_off_the_record. */
|
|
147
|
-
function fuzzyGrepRows(index, files, pattern, cwd, caseSensitive) {
|
|
268
|
+
function fuzzyGrepRows(index, files, pattern, cwd, caseSensitive, overlayText = () => undefined) {
|
|
148
269
|
const maxTypos = pattern.length >= 8 ? 2 : 1;
|
|
149
270
|
const rows = [];
|
|
150
271
|
|
|
151
272
|
for (const filePath of files) {
|
|
152
|
-
const e = index
|
|
273
|
+
const e = overlaySearchEntry(index, filePath, overlayText);
|
|
153
274
|
|
|
154
275
|
if (!e) continue;
|
|
155
276
|
const { raw, defNames } = WorkspaceIndex.linesOf(e);
|
|
156
277
|
const rel = relativeSlash(cwd, filePath);
|
|
157
278
|
|
|
158
279
|
for (let i = 0; i < raw.length && rows.length <= 400; i++) {
|
|
159
|
-
const
|
|
280
|
+
const row = fuzzyLineRow(pattern, raw[i], defNames[i], rel, i + 1, maxTypos, caseSensitive);
|
|
160
281
|
|
|
161
|
-
if (
|
|
162
|
-
rows.push({ rel, line: i + 1, text: raw[i], def: defNames[i] !== "" && fuzzyMatch(pattern, defNames[i], { maxTypos }) !== null });
|
|
282
|
+
if (row) rows.push(row);
|
|
163
283
|
}
|
|
164
284
|
}
|
|
165
285
|
|
|
166
286
|
return rows;
|
|
167
287
|
}
|
|
168
288
|
|
|
169
|
-
|
|
170
|
-
function formatGrepRows(rows, limit) {
|
|
171
|
-
if (rows.length === 0) return "";
|
|
289
|
+
function groupGrepRows(rows) {
|
|
172
290
|
const groups = new Map();
|
|
173
291
|
|
|
174
292
|
for (const r of rows) {
|
|
@@ -176,19 +294,34 @@ function formatGrepRows(rows, limit) {
|
|
|
176
294
|
groups.get(r.rel).push(r);
|
|
177
295
|
}
|
|
178
296
|
|
|
179
|
-
|
|
297
|
+
return [...groups.values()].sort((a, b) => Number(b.some((r) => r.def)) - Number(a.some((r) => r.def)));
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function formatGroup(group, limit, shown) {
|
|
301
|
+
let out = group[0].rel + "\n";
|
|
302
|
+
group.sort((a, b) => Number(b.def) - Number(a.def) || a.line - b.line);
|
|
303
|
+
let n = shown;
|
|
304
|
+
|
|
305
|
+
for (const r of group) {
|
|
306
|
+
if (n++ >= limit) break;
|
|
307
|
+
out += " " + r.line + (r.def ? "*" : ":") + " " + r.text.trim() + "\n";
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return { out, shown: n };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** fff definition-first hinting: files that declare the name come first, declarations first within a file; one header per file. */
|
|
314
|
+
function formatGrepRows(rows, limit) {
|
|
315
|
+
if (rows.length === 0) return "";
|
|
316
|
+
const files = groupGrepRows(rows);
|
|
180
317
|
let out = "";
|
|
181
318
|
let shown = 0;
|
|
182
319
|
|
|
183
320
|
for (const group of files) {
|
|
184
321
|
if (shown >= limit) break;
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
for (const r of group) {
|
|
189
|
-
if (shown++ >= limit) break;
|
|
190
|
-
out += " " + r.line + (r.def ? "*" : ":") + " " + r.text.trim() + "\n";
|
|
191
|
-
}
|
|
322
|
+
const next = formatGroup(group, limit, shown);
|
|
323
|
+
out += next.out;
|
|
324
|
+
shown = next.shown;
|
|
192
325
|
}
|
|
193
326
|
|
|
194
327
|
if (rows.length > limit) out += "… " + (rows.length - limit) + " more matches (pass limit or narrow the pattern)\n";
|
|
@@ -197,8 +330,8 @@ function formatGrepRows(rows, limit) {
|
|
|
197
330
|
}
|
|
198
331
|
|
|
199
332
|
/** rg --files [-g pattern] served from the index; null when the tree is too large. */
|
|
200
|
-
export async function listIndexed(index, root, cwd, pattern) {
|
|
201
|
-
const files = await index
|
|
333
|
+
export async function listIndexed(index, root, cwd, pattern, pendingPaths = []) {
|
|
334
|
+
const files = [...new Set([...await candidateFileList(index, root), ...pendingInScope(root, pendingPaths)])];
|
|
202
335
|
|
|
203
336
|
if (!index.canScan(files)) return null;
|
|
204
337
|
const rel = files.map((f) => path.relative(cwd, f).split(path.sep).join("/"));
|