pi-supernova 0.6.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.
Files changed (49) hide show
  1. package/README.md +27 -3
  2. package/docs/CHANGELOG.md +104 -0
  3. package/docs/TOKEN_COSTS.md +13 -5
  4. package/index.js +120 -79
  5. package/package.json +1 -1
  6. package/src/adapters/bash.js +73 -0
  7. package/src/adapters/edit.js +249 -0
  8. package/src/adapters/errors.js +31 -0
  9. package/src/adapters/index.js +31 -0
  10. package/src/adapters/list.js +102 -0
  11. package/src/adapters/read.js +805 -0
  12. package/src/adapters/refs.js +41 -0
  13. package/src/adapters/write.js +96 -0
  14. package/src/bridge/catalog.js +28 -222
  15. package/src/bridge/host-bridge.js +113 -1668
  16. package/src/bridge/invoke.js +35 -0
  17. package/src/bridge/native-tools.js +1 -198
  18. package/src/context/evidence.js +140 -76
  19. package/src/context/fuzzy.js +42 -24
  20. package/src/context/ledger.js +43 -24
  21. package/src/context/outline.js +23 -18
  22. package/src/context/repo-index.js +206 -170
  23. package/src/context/search.js +157 -77
  24. package/src/context/snap.js +240 -136
  25. package/src/context/spans.js +2 -1
  26. package/src/context/surface.js +14 -5
  27. package/src/contract/bash.js +31 -0
  28. package/src/contract/edit.js +95 -0
  29. package/src/contract/read.js +220 -0
  30. package/src/fs/check.js +12 -8
  31. package/src/fs/diff.js +18 -7
  32. package/src/fs/json-read.js +66 -35
  33. package/src/fs/patch.js +94 -50
  34. package/src/fs/source-window.js +82 -0
  35. package/src/fs/text-ops.js +512 -0
  36. package/src/fs/vfs.js +205 -175
  37. package/src/fs/workspace.js +119 -108
  38. package/src/output/bottleneck.js +195 -116
  39. package/src/output/format.js +101 -67
  40. package/src/runtime/guest-deny-imports.js +34 -0
  41. package/src/runtime/guest-worker.js +289 -292
  42. package/src/runtime/parallel.js +97 -64
  43. package/src/runtime/program-batch.js +178 -69
  44. package/src/runtime/reference.js +15 -14
  45. package/src/runtime/runtime.js +327 -187
  46. package/src/shared/decode.js +58 -36
  47. package/src/ui/omp-frame.js +59 -42
  48. package/src/ui/render-measure.js +51 -29
  49. package/src/ui/render.js +241 -145
@@ -0,0 +1,41 @@
1
+ import { relativeSlash } from "../fs/workspace.js";
2
+
3
+ export function outlineOptions(params, references, config) {
4
+ const options = { references };
5
+
6
+ if (Number.isInteger(params?.maxChars) && params.maxChars > 0) options.maxChars = Math.min(params.maxChars, config.maxCallResultChars ?? 65536);
7
+
8
+ return options;
9
+ }
10
+
11
+ /** Outline lines carry their own line numbers (" 330 text"); provenance follows them. */
12
+ export function recordOutlineOrigins(ledger, rel, outlineText) {
13
+ for (const line of outlineText.split("\n")) {
14
+ const m = /^\s*(\d+) (.*)$/.exec(line);
15
+
16
+ if (m && !/ … \d+ lines$/.test(line)) ledger.recordOrigin(rel, Number(m[1]), [line]);
17
+ }
18
+ }
19
+
20
+ /** Where else a name appears (declaration line excluded), for outlines and edit results. */
21
+ export function createReferenceFinder(index, vfs) {
22
+ return async function referenceFinder(cwd, targetPath) {
23
+ let files;
24
+
25
+ try { files = [...new Set([...await index.files(cwd), ...vfs.getOverlayPaths()])]; }
26
+ catch { return () => []; }
27
+
28
+ if (!index.canScan(files)) return () => [];
29
+
30
+ return (name, excludeLine) => {
31
+ if (!name || name.length < 3) return [];
32
+ const escaped = name.replace(/[$]/g, (c) => "\\" + c);
33
+ const regex = new RegExp("\\b" + escaped + "\\b");
34
+
35
+ return index
36
+ .grepRows(files, regex, cwd, file => vfs.getOverlay(file))
37
+ .filter((r) => !(r.line === excludeLine && r.rel === relativeSlash(cwd, targetPath)))
38
+ .map((r) => r.rel + ":" + r.line);
39
+ };
40
+ };
41
+ }
@@ -0,0 +1,96 @@
1
+ import * as path from "node:path";
2
+ import { isString } from "../shared/decode.js";
3
+ import { buildWriteDiff } from "../fs/diff.js";
4
+ import { quickCheck } from "../fs/check.js";
5
+ import { resolveWorkspacePath, relativeSlash } from "../fs/workspace.js";
6
+ import {
7
+ textResult, contentLineInfo, boundedWriteDiff,
8
+ WRITE_DIFF_MAX_READ_BYTES, WRITE_APPEND_MAX_READ_BYTES, QUICK_CHECK_MAX_CHARS,
9
+ writeSnapshot,
10
+ } from "../fs/text-ops.js";
11
+
12
+ const READ_ARTIFACT_MARK = /\[read truncated;|…\[[^\]\n]*truncated[^\]\n]*\]…/u;
13
+
14
+ function assertWriteAppendFlag(append) {
15
+ if (append !== undefined && append !== true && append !== false) throw new Error("write append must be a boolean");
16
+ }
17
+
18
+ function assertWriteArtifactsFlag(allowReadArtifacts) {
19
+ if (allowReadArtifacts !== undefined && allowReadArtifacts !== true && allowReadArtifacts !== false) throw new Error("write allowReadArtifacts must be a boolean");
20
+ }
21
+
22
+ function writeDiffFor(target, prevText, content, removedLines) {
23
+ if (removedLines === undefined && content.length <= WRITE_DIFF_MAX_READ_BYTES) {
24
+ return buildWriteDiff(target, prevText, content);
25
+ }
26
+
27
+ return boundedWriteDiff(target, content, removedLines ?? contentLineInfo(prevText).count);
28
+ }
29
+
30
+ function writeCheckWarning(content, target) {
31
+ if (content.length > QUICK_CHECK_MAX_CHARS) return "";
32
+ const check = quickCheck(content, path.extname(target));
33
+
34
+ return check && !check.ok ? "\ncheck: " + check.message : "";
35
+ }
36
+
37
+ function writeOutcome(rel, target, content, speculative, prevText, removedLines) {
38
+ const diff = writeDiffFor(target, prevText, content, removedLines);
39
+ const tag = speculative ? " (speculative)" : "";
40
+
41
+ return textResult(`wrote ${rel}${tag}${writeCheckWarning(content, target)}`, { path: target, speculative, diff });
42
+ }
43
+
44
+ export function createWrite(ctx) {
45
+ const { getCwd, vfs, index } = ctx;
46
+
47
+ function assertWriteParams(params) {
48
+ if (!isString(params?.content)) throw new Error("write requires string content");
49
+ assertWriteAppendFlag(params.append);
50
+ assertWriteArtifactsFlag(params.allowReadArtifacts);
51
+ const content = String(params.content);
52
+
53
+ if (params.allowReadArtifacts !== true && READ_ARTIFACT_MARK.test(content)) {
54
+ throw new Error("refusing to write truncated read output; use edit() or reconstruct complete source windows. Set allowReadArtifacts:true only to intentionally write literal truncation-marker text");
55
+ }
56
+
57
+ return content;
58
+ }
59
+
60
+ async function applyAppend(target, content, snap) {
61
+ let { previous: prevText, overlay, existingBytes } = snap;
62
+
63
+ if (existingBytes > WRITE_APPEND_MAX_READ_BYTES) throw new Error("append input exceeds " + WRITE_APPEND_MAX_READ_BYTES + " bytes; stream it with bash redirection instead");
64
+
65
+ if (existingBytes !== undefined && existingBytes > WRITE_DIFF_MAX_READ_BYTES) {
66
+ try { prevText = overlay !== undefined ? overlay : await vfs.read(target, { maxBytes: WRITE_APPEND_MAX_READ_BYTES, preserveRead: true }); }
67
+ catch (error) { if (error.code !== "ENOENT") throw error; }
68
+ }
69
+
70
+ return { content: prevText + content, prevText, removedLines: undefined };
71
+ }
72
+
73
+ async function write(params, signal) {
74
+ const cwd = getCwd();
75
+ const target = await resolveWorkspacePath(cwd, params?.path, "write", false);
76
+
77
+ if (signal?.aborted) throw new Error("aborted");
78
+ let content = assertWriteParams(params);
79
+ const snap = await writeSnapshot(vfs, target, signal);
80
+ let { previous: prevText, removedLines } = snap;
81
+
82
+ if (params.append === true) {
83
+ const appended = await applyAppend(target, content, snap);
84
+ content = appended.content;
85
+ prevText = appended.prevText;
86
+ removedLines = appended.removedLines;
87
+ }
88
+
89
+ const { speculative } = await vfs.write(target, content);
90
+ index.touch(relativeSlash(cwd, target));
91
+
92
+ return writeOutcome(relativeSlash(cwd, target), target, content, speculative, prevText, removedLines);
93
+ }
94
+
95
+ return { write };
96
+ }
@@ -1,210 +1,46 @@
1
+ import { isString } from "../shared/decode.js";
1
2
 
2
- import { isString, isObject } from "../shared/decode.js";
3
-
4
- const NATIVE_TOOL_DEFINITIONS = [
5
- {
6
- name: "read",
7
- description: "Read files, images or directories (directory reads return entries). JSON selectors project full documents within output budgets. Source questions locate and open source directly; resolve returns structured source/status without guessing.",
8
- parameters: { type: "object", properties: {
9
- path: { anyOf: [{ type: "string" }, { type: "array", items: { type: "string" }, maxItems: 64 }], description: "Workspace-relative file or directory, source question, or up to 64 paths" },
10
- target: { anyOf: [{ type: "string" }, { type: "array" }], description: "File path/query or array of paths" },
11
- offset: { type: "number", description: "One-based starting line" },
12
- limit: { type: "number", description: "Maximum lines to return" },
13
- about: { type: "string", description: "Question or symbol (at most 16 keywords): expand file bodies, or locate/open source inside a directory" },
14
- query: { type: "string", description: "Source question (at most 16 keywords); optional path scopes the search directory" },
15
- outline: { type: "boolean", description: "Return a compact structural outline for the target file" },
16
- evidence: { type: "boolean", description: "Rank source spans answering the target/path question" },
17
- resolve: { type: "boolean", description: "Return structured source/status for a direct resolve-to-edit handoff" },
18
- complete: { type: "boolean", description: "Fail unless the entire requested file fits without clipping" },
19
- json: { anyOf: [{ type: "boolean" }, { type: "string" }, { type: "array", items: { type: "string" }, minItems: 1, maxItems: 64 }], description: "Parse complete JSON input up to 16 MiB, then select .field, .items[0:3], quoted keys, true, or 1-64 selectors. Oversized selections fail, never clip." },
20
- } },
21
- },
22
- {
23
- name: "write", description: "Write UTF-8 content to a workspace file.",
24
- parameters: { type: "object", properties: { path: { type: "string" }, content: { type: "string" }, append: { type: "boolean", description: "Append to an existing file instead of replacing it" }, allowReadArtifacts: { type: "boolean", description: "Explicit opt-in for intentionally writing literal truncation-marker text" } }, required: ["path", "content"] },
25
- },
26
- {
27
- name: "edit", description: "Apply unique text replacements to a workspace file; returns the post-edit lines, a structural check, and references to changed declarations.",
28
- parameters: { type: "object", properties: {
29
- path: { type: "string" }, oldText: { type: "string" }, newText: { type: "string" }, edits: { type: "array", description: "[{oldText, newText}] for several replacements in one call" },
30
- }, required: ["path"] },
31
- },
32
- {
33
- name: "apply_patch", description: "Apply a unified diff to one workspace file.",
34
- parameters: { type: "object", properties: { path: { type: "string" }, patch: { type: "string" } }, required: ["patch"] },
35
- },
36
- {
37
- name: "snap", description: "Select source with found, ambiguous, not_found, or incomplete status. The read command uses the same engine.",
38
- parameters: { type: "object", properties: {
39
- query: { type: "string", description: "Source concept to resolve" },
40
- path: { type: "string", description: "Optional workspace search root; explicitly targeting a hidden directory includes its hidden files, but Git metadata is always excluded" },
41
- }, required: ["query"] },
42
- },
43
- {
44
- name: "evidence", description: "Top-K source spans (with path and line provenance) that answer a concept question; read these instead of whole files.",
45
- parameters: { type: "object", properties: {
46
- query: { type: "string", description: "Concept, symbol, or question" },
47
- path: { type: "string", description: "Optional search root" },
48
- k: { type: "number", description: "Main spans to return (default 5)" },
49
- maxChars: { type: "number", description: "Total text budget (default 6000)" },
50
- }, required: ["query"] },
51
- },
52
- {
53
- name: "surface", description: "Extract a structural outline from a workspace source file.",
54
- parameters: { type: "object", properties: { path: { type: "string" } }, required: ["path"] },
55
- },
56
- {
57
- name: "bash", description: "Run a shell command inside the workspace and capture bounded output.",
58
- parameters: { type: "object", properties: { command: { type: "string" }, args: { type: "array", items: { type: "string" }, description: "Literal argv without shell interpretation (POSIX)" }, cwd: { type: "string" }, timeoutMs: { type: "number" } }, required: ["command"] },
59
- },
60
- {
61
- name: "grep", description: "Search file contents. Smart-case regex; definition lines first (marked *); fuzzy fallback when nothing matches literally.",
62
- parameters: { type: "object", properties: {
63
- pattern: { type: "string" }, path: { type: "string" }, glob: { type: "string" }, caseSensitive: { type: "boolean" }, limit: { type: "number" },
64
- }, required: ["pattern"] },
65
- },
66
- {
67
- name: "glob", description: "Find files: a glob pattern, or free text for typo-tolerant, frecency-ranked path search.",
68
- parameters: { type: "object", properties: { pattern: { type: "string" } }, required: ["pattern"] },
69
- },
70
- {
71
- name: "find", description: "List workspace files, optionally constrained by path and pattern.",
72
- parameters: { type: "object", properties: { path: { type: "string" }, pattern: { type: "string" }, glob: { type: "string" } } },
73
- },
74
- {
75
- name: "ls", description: "List direct entries in a workspace directory.",
76
- parameters: { type: "object", properties: { path: { type: "string" } } },
77
- },
78
- ];
79
-
80
- export function mergeNativeToolDefinitions(tools, capturedNames = []) {
81
- const nativeByName = new Map(NATIVE_TOOL_DEFINITIONS.map((tool) => [tool.name, tool]));
82
- const captured = new Set(capturedNames);
83
- const seen = new Set();
84
- const merged = [];
85
-
86
- for (const tool of tools || []) {
87
- const fallback = nativeByName.get(tool?.name);
88
- merged.push(fallback && !captured.has(tool.name)
89
- ? { ...tool, ...fallback, sourceInfo: { path: "<native:" + tool.name + ">" } }
90
- : tool);
91
-
92
- if (tool?.name) seen.add(tool.name);
93
- }
94
-
95
- for (const fallback of NATIVE_TOOL_DEFINITIONS) {
96
- if (!seen.has(fallback.name)) {
97
- merged.push({ ...fallback, sourceInfo: { path: "<native:" + fallback.name + ">" } });
98
- }
99
- }
100
-
101
- return merged;
102
- }
103
-
104
- function sourcePathOf(tool) {
105
- if (tool.sourceInfo && isString(tool.sourceInfo.path)) return tool.sourceInfo.path;
106
-
107
- if (isString(tool.extensionPath)) return tool.extensionPath;
108
-
109
- if (isString(tool.sourcePath)) return tool.sourcePath;
110
-
111
- return undefined;
112
- }
113
-
114
- function normalizeTool(tool) {
115
- if (!tool || !isObject(tool)) return null;
116
- const name = isString(tool.name) ? tool.name : "";
117
-
118
- if (!name) return null;
119
- const description = isString(tool.description) ? tool.description : "";
120
-
121
- return {
122
- name,
123
- nameLower: name.toLowerCase(),
124
- description,
125
- descLower: description.toLowerCase(),
126
- parameters: tool.parameters,
127
- schemaError: tool.schemaError,
128
- sourcePath: sourcePathOf(tool),
129
- };
130
- }
131
-
132
- export function buildCatalog(tools, excludeNames = []) {
133
- const exclude = new Set(excludeNames);
134
- const rows = [];
135
-
136
- for (const tool of tools || []) {
137
- const row = normalizeTool(tool);
138
-
139
- if (!row || exclude.has(row.name)) continue;
140
- rows.push(row);
141
- }
3
+ /** Optimal string alignment distance: insert/delete/substitute/adjacent-transpose cost 1. */
4
+ function osaCell(a, b, rows, i, j) {
5
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
6
+ let best = Math.min(rows[i - 1][j] + 1, rows[i][j - 1] + 1, rows[i - 1][j - 1] + cost);
142
7
 
143
- rows.sort((a, b) => a.name.localeCompare(b.name));
8
+ if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) best = Math.min(best, rows[i - 2][j - 2] + 1);
144
9
 
145
- return rows;
10
+ return best;
146
11
  }
147
12
 
148
- function tokenize(query) {
149
- return String(query || "")
150
- .toLowerCase()
151
- .split(/[^a-z0-9_]+/g)
152
- .filter((t) => t.length > 1);
153
- }
13
+ function editDistance(a, b) {
14
+ const rows = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array.from({ length: b.length }, () => 0)]);
154
15
 
155
- function scoreRow(row, tokens) {
156
- if (tokens.length === 0) return 1;
157
- const name = row.nameLower || row.name.toLowerCase();
158
- const desc = row.descLower || row.description.toLowerCase();
159
- let score = 0;
16
+ for (let j = 1; j <= b.length; j++) rows[0][j] = j;
160
17
 
161
- for (const token of tokens) {
162
- if (name === token) score += 10;
163
- else if (name.includes(token)) score += 5;
164
- else if (desc.includes(token)) score += 2;
18
+ for (let i = 1; i < rows.length; i++) {
19
+ for (let j = 1; j <= b.length; j++) rows[i][j] = osaCell(a, b, rows, i, j);
165
20
  }
166
21
 
167
- return score;
22
+ return rows[a.length][b.length];
168
23
  }
169
24
 
170
- export function searchCatalog(catalog, query, limit = 12) {
171
- const tokens = tokenize(query);
172
- const scored = [];
173
-
174
- for (const row of catalog) {
175
- const score = scoreRow(row, tokens);
25
+ function scoreName(needle, candidate, maxDistance) {
26
+ const lower = candidate.toLowerCase();
176
27
 
177
- if (score <= 0 && tokens.length > 0) continue;
178
- scored.push({ name: row.name, description: row.description.slice(0, 160), score });
179
- }
28
+ if (lower === needle) return null;
29
+ const distance = lower.includes(needle) || needle.includes(lower) ? 1 : editDistance(needle, lower);
180
30
 
181
- scored.sort((a, b) => b.score - a.score || a.name.localeCompare(b.name));
31
+ if (distance > maxDistance) return null;
182
32
 
183
- const capped = Math.min(64, Math.max(1, Math.floor(Number(limit)) || 1));
184
-
185
- return scored.slice(0, capped).map(({ score: _s, ...hit }) => hit);
33
+ return { candidate, distance };
186
34
  }
187
35
 
188
- /** Optimal string alignment distance: insert/delete/substitute/adjacent-transpose cost 1. */
189
- function editDistance(a, b) {
190
- const rows = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array.from({ length: b.length }, () => 0)]);
191
-
192
- for (let j = 1; j <= b.length; j++) rows[0][j] = j;
193
-
194
- for (let i = 1; i <= a.length; i++) {
195
- for (let j = 1; j <= b.length; j++) {
196
- const cost = a[i - 1] === b[j - 1] ? 0 : 1;
197
- let best = Math.min(rows[i - 1][j] + 1, rows[i][j - 1] + 1, rows[i - 1][j - 1] + cost);
198
-
199
- if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) best = Math.min(best, rows[i - 2][j - 2] + 1);
200
- rows[i][j] = best;
201
- }
202
- }
203
-
204
- return rows[a.length][b.length];
36
+ function bySuggestionRank(needle, a, b) {
37
+ return (
38
+ a.distance - b.distance ||
39
+ Math.abs(a.candidate.length - needle.length) - Math.abs(b.candidate.length - needle.length) ||
40
+ a.candidate.localeCompare(b.candidate)
41
+ );
205
42
  }
206
43
 
207
- /** Closest tool names for a mistyped name: substring hits first, then a length-scaled edit distance. */
208
44
  function suggestNames(name, candidates, limit = 3) {
209
45
  const needle = String(name || "").toLowerCase().slice(0, 128);
210
46
 
@@ -213,20 +49,12 @@ function suggestNames(name, candidates, limit = 3) {
213
49
  const scored = [];
214
50
 
215
51
  for (const candidate of candidates) {
216
- const lower = candidate.toLowerCase();
217
-
218
- if (lower === needle) continue;
219
- const distance = lower.includes(needle) || needle.includes(lower) ? 1 : editDistance(needle, lower);
52
+ const hit = scoreName(needle, candidate, maxDistance);
220
53
 
221
- if (distance <= maxDistance) scored.push({ candidate, distance });
54
+ if (hit) scored.push(hit);
222
55
  }
223
56
 
224
- scored.sort(
225
- (a, b) =>
226
- a.distance - b.distance ||
227
- Math.abs(a.candidate.length - needle.length) - Math.abs(b.candidate.length - needle.length) ||
228
- a.candidate.localeCompare(b.candidate),
229
- );
57
+ scored.sort((a, b) => bySuggestionRank(needle, a, b));
230
58
 
231
59
  return scored.slice(0, limit).map((s) => s.candidate);
232
60
  }
@@ -237,25 +65,3 @@ export function unknownToolMessage(name, candidates) {
237
65
 
238
66
  return `unknown tool "${name}".${hint} Check the command name and configured tool exclusions.`;
239
67
  }
240
-
241
- export function describeTool(catalog, name) {
242
- const row = catalog.find((t) => t.name === name);
243
-
244
- if (!row) {
245
- return { ok: false, error: unknownToolMessage(name, catalog.map((t) => t.name)) };
246
- }
247
-
248
- if (!isObject(row.parameters)) return { ok: false, name, error: "tool schema unavailable: " + (row.schemaError ?? name) };
249
-
250
- if (!row._described) {
251
- row._described = {
252
- ok: true,
253
- name: row.name,
254
- description: row.description,
255
- parameters: row.parameters,
256
- sourcePath: row.sourcePath,
257
- };
258
- }
259
-
260
- return row._described;
261
- }