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.
Files changed (50) hide show
  1. package/README.md +97 -11
  2. package/docs/CHANGELOG.md +150 -0
  3. package/docs/TOKEN_COSTS.md +71 -29
  4. package/index.js +126 -82
  5. package/package.json +2 -2
  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 +30 -220
  15. package/src/bridge/host-bridge.js +142 -1032
  16. package/src/bridge/invoke.js +35 -0
  17. package/src/bridge/native-tools.js +1 -188
  18. package/src/context/evidence.js +142 -70
  19. package/src/context/fuzzy.js +61 -22
  20. package/src/context/ledger.js +43 -24
  21. package/src/context/outline.js +26 -12
  22. package/src/context/repo-index.js +242 -71
  23. package/src/context/search.js +189 -56
  24. package/src/context/snap.js +306 -150
  25. package/src/context/spans.js +2 -1
  26. package/src/context/surface.js +29 -14
  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 +19 -7
  31. package/src/fs/diff.js +18 -7
  32. package/src/fs/json-read.js +66 -35
  33. package/src/fs/patch.js +97 -51
  34. package/src/fs/source-window.js +82 -0
  35. package/src/fs/text-ops.js +512 -0
  36. package/src/fs/vfs.js +289 -162
  37. package/src/fs/workspace.js +122 -105
  38. package/src/output/bottleneck.js +211 -107
  39. package/src/output/format.js +112 -63
  40. package/src/runtime/guest-deny-imports.js +34 -0
  41. package/src/runtime/guest-worker.js +306 -213
  42. package/src/runtime/parallel.js +99 -63
  43. package/src/runtime/program-batch.js +189 -69
  44. package/src/runtime/program-file.js +6 -3
  45. package/src/runtime/reference.js +13 -12
  46. package/src/runtime/runtime.js +327 -176
  47. package/src/shared/decode.js +61 -27
  48. package/src/ui/omp-frame.js +70 -46
  49. package/src/ui/render-measure.js +51 -29
  50. package/src/ui/render.js +242 -146
@@ -1,20 +1,20 @@
1
1
  import { isString, isObject } from "../shared/decode.js";
2
2
 
3
- export function truncateChars(text, maxChars, label = "value") {
4
- const normalized = isString(text) ? text : String(text ?? "");
3
+ function normalizeText(text) {
4
+ return isString(text) ? text : String(text ?? "");
5
+ }
6
+
7
+ function charLimit(maxChars, length) {
5
8
  const numericLimit = Number(maxChars);
6
- const limit = Number.isFinite(numericLimit) ? Math.max(0, Math.floor(numericLimit)) : numericLimit === Infinity ? normalized.length : 0;
7
9
 
8
- if (normalized.length <= limit) return { text: normalized, truncated: false };
10
+ return Number.isFinite(numericLimit) ? Math.max(0, Math.floor(numericLimit)) : numericLimit === Infinity ? length : 0;
11
+ }
9
12
 
10
- if (limit <= 100) {
11
- return {
12
- text: normalized.slice(0, headEnd(normalized, limit)),
13
- truncated: true,
14
- originalChars: normalized.length,
15
- };
16
- }
13
+ function truncatedSlice(text, end) {
14
+ return { text: text.slice(0, headEnd(text, end)), truncated: true, originalChars: text.length };
15
+ }
17
16
 
17
+ function truncateHeadTail(normalized, limit, label) {
18
18
  let head = headEnd(normalized, Math.floor(limit * 0.7));
19
19
  let tail = 0;
20
20
  let marker = "";
@@ -23,7 +23,7 @@ export function truncateChars(text, maxChars, label = "value") {
23
23
  const omitted = normalized.length - head - tail;
24
24
  marker = "\n…[" + label + " truncated " + omitted + " chars]…\n";
25
25
 
26
- if (marker.length > limit) return { text: normalized.slice(0, headEnd(normalized, limit)), truncated: true, originalChars: normalized.length };
26
+ if (marker.length > limit) return truncatedSlice(normalized, limit);
27
27
  const budget = limit - marker.length;
28
28
  const nextHead = headEnd(normalized, Math.min(head, budget));
29
29
  const nextTail = normalized.length - tailStartIndex(normalized, Math.max(0, budget - nextHead));
@@ -36,6 +36,17 @@ export function truncateChars(text, maxChars, label = "value") {
36
36
  return { text: normalized.slice(0, head) + marker + normalized.slice(normalized.length - tail), truncated: true, originalChars: normalized.length };
37
37
  }
38
38
 
39
+ export function truncateChars(text, maxChars, label = "value") {
40
+ const normalized = normalizeText(text);
41
+ const limit = charLimit(maxChars, normalized.length);
42
+
43
+ if (normalized.length <= limit) return { text: normalized, truncated: false };
44
+
45
+ if (limit <= 100) return truncatedSlice(normalized, limit);
46
+
47
+ return truncateHeadTail(normalized, limit, label);
48
+ }
49
+
39
50
  // Lone surrogates in a tool result make the message invalid UTF-8 at the API
40
51
  // boundary, so a cut must never split a surrogate pair.
41
52
  function headEnd(text, end) {
@@ -90,35 +101,35 @@ export function formatBoundedStringArray(values, budget) {
90
101
  return out;
91
102
  }
92
103
 
93
- /** Lossless framing for source arrays, not string escaping or source compression. */
94
- export function formatReturn(value) {
95
- if (isString(value)) return value;
104
+ function formatRawStringArray(value) {
105
+ if (!(Array.isArray(value) && value.length && hasWellFormedStrings(value) && value.some(text => text.includes("\n")))) return null;
106
+ const raw = "strings[" + value.length + "]\n" + value.map((text, i) => "[" + i + "] " + text.length + " UTF-16 units\n" + text + "\n").join("");
107
+ const escapedSize = value.reduce((sum, text) => sum + JSON.stringify(text).length, value.length + 1);
96
108
 
97
- if (Array.isArray(value) && value.length && hasWellFormedStrings(value) && value.some(text => text.includes("\n"))) {
98
- const raw = "strings[" + value.length + "]\n" + value.map((text, i) => "[" + i + "] " + text.length + " UTF-16 units\n" + text + "\n").join("");
99
- const escapedSize = value.reduce((sum, text) => sum + JSON.stringify(text).length, value.length + 1);
100
-
101
- if (raw.length < escapedSize) return raw;
102
- }
109
+ return raw.length < escapedSize ? raw : null;
110
+ }
103
111
 
104
- const escaped = formatValue(value);
105
- const strings = [];
112
+ function shouldFrameRaw(input) {
113
+ return isString(input) && input.includes("\n") && !hasUnpairedSurrogate(input) && JSON.stringify(input).length - input.length > 64;
114
+ }
106
115
 
107
- const visit = input => {
108
- if (isString(input) && input.includes("\n") && !hasUnpairedSurrogate(input) && JSON.stringify(input).length - input.length > 64) {
109
- const index = strings.push(input) - 1;
116
+ function visitRawStrings(input, strings) {
117
+ if (shouldFrameRaw(input)) {
118
+ const index = strings.push(input) - 1;
110
119
 
111
- return { [RAW_TEXT]: "raw[" + index + "]" };
112
- }
120
+ return { [RAW_TEXT]: "raw[" + index + "]" };
121
+ }
113
122
 
114
- if (Array.isArray(input)) return input.map(visit);
123
+ if (Array.isArray(input)) return input.map(child => visitRawStrings(child, strings));
115
124
 
116
- if (isObject(input)) return Object.fromEntries(Object.entries(input).map(([key, child]) => [key, visit(child)]));
125
+ if (isObject(input)) return Object.fromEntries(Object.entries(input).map(([key, child]) => [key, visitRawStrings(child, strings)]));
117
126
 
118
- return input;
119
- };
127
+ return input;
128
+ }
120
129
 
121
- const referencedValue = visit(value);
130
+ function framedReturn(value, escaped) {
131
+ const strings = [];
132
+ const referencedValue = visitRawStrings(value, strings);
122
133
 
123
134
  if (!strings.length) return escaped;
124
135
  // Keep every key, value, duplicate string and byte. References are unquoted
@@ -128,6 +139,16 @@ export function formatReturn(value) {
128
139
  return framed.length < escaped.length ? framed : escaped;
129
140
  }
130
141
 
142
+ /** Lossless framing for source arrays, not string escaping or source compression. */
143
+ export function formatReturn(value) {
144
+ if (isString(value)) return value;
145
+ const rawArray = formatRawStringArray(value);
146
+
147
+ if (rawArray !== null) return rawArray;
148
+
149
+ return framedReturn(value, formatValue(value));
150
+ }
151
+
131
152
  const RAW_TEXT = Symbol("raw text reference");
132
153
 
133
154
  const IDENT_KEY = /^[A-Za-z_$][\w$]*$/;
@@ -143,7 +164,8 @@ function formatPrimitive(value) {
143
164
 
144
165
  if (Number.isNaN(value) || value === Infinity || value === -Infinity) return String(value);
145
166
 
146
- return JSON.stringify(value) ?? String(value);
167
+ try { return JSON.stringify(value) ?? String(value); }
168
+ catch { return String(value); }
147
169
  }
148
170
 
149
171
  /**
@@ -168,28 +190,24 @@ function formatFlatWithin(value, limit) {
168
190
  return true;
169
191
  };
170
192
 
171
- return walkFlat(value, push) ? parts.join("") : null;
193
+ return walkFlat(value, push, new Set()) ? parts.join("") : null;
172
194
  }
173
195
 
174
- function walkFlat(value, push) {
175
- if (value?.[RAW_TEXT] !== undefined) return push(value[RAW_TEXT]);
176
-
177
- if (!isObject(value) && !Array.isArray(value)) return push(formatPrimitive(value));
178
-
179
- if (Array.isArray(value)) {
180
- if (value.length === 0) return push("[]");
181
-
182
- if (!push("[")) return false;
196
+ function walkFlatArray(value, push, seen) {
197
+ if (value.length === 0) return push("[]");
183
198
 
184
- for (let i = 0; i < value.length; i++) {
185
- if (i && !push(",")) return false;
199
+ if (!push("[")) return false;
186
200
 
187
- if (!walkFlat(value[i] === undefined ? null : value[i], push)) return false;
188
- }
201
+ for (let i = 0; i < value.length; i++) {
202
+ if (i && !push(",")) return false;
189
203
 
190
- return push("]");
204
+ if (!walkFlat(value[i] === undefined ? null : value[i], push, seen)) return false;
191
205
  }
192
206
 
207
+ return push("]");
208
+ }
209
+
210
+ function walkFlatObject(value, push, seen) {
193
211
  const keys = Object.keys(value).filter((key) => value[key] !== undefined);
194
212
 
195
213
  if (keys.length === 0) return push("{}");
@@ -197,36 +215,67 @@ function walkFlat(value, push) {
197
215
  for (let i = 0; i < keys.length; i++) {
198
216
  if (!push((i ? "," : "{") + formatKey(keys[i]) + ":")) return false;
199
217
 
200
- if (!walkFlat(value[keys[i]], push)) return false;
218
+ if (!walkFlat(value[keys[i]], push, seen)) return false;
201
219
  }
202
220
 
203
221
  return push("}");
204
222
  }
205
223
 
224
+ function walkFlat(value, push, seen) {
225
+ if (value?.[RAW_TEXT] !== undefined) return push(value[RAW_TEXT]);
226
+
227
+ if (!isObject(value) && !Array.isArray(value)) return push(formatPrimitive(value));
228
+
229
+ if (seen.has(value)) return push("[Circular]");
230
+ seen.add(value);
231
+
232
+ try {
233
+ return Array.isArray(value) ? walkFlatArray(value, push, seen) : walkFlatObject(value, push, seen);
234
+ } finally {
235
+ seen.delete(value);
236
+ }
237
+ }
238
+
239
+ function definedKeys(value) {
240
+ return Object.keys(value).filter((key) => value[key] !== undefined);
241
+ }
242
+
243
+ function formatArray(value, indent, width, seen) {
244
+ if (value.length === 0) return "[]";
245
+ const pad = indent + " ";
246
+
247
+ return "[\n" + value.map((item) => pad + formatValue(item === undefined ? null : item, pad, width, seen)).join(",\n") + "\n" + indent + "]";
248
+ }
249
+
250
+ function formatObject(value, indent, width, seen) {
251
+ const keys = definedKeys(value);
252
+
253
+ if (keys.length === 0) return "{}";
254
+ const pad = indent + " ";
255
+
256
+ return "{\n" + keys.map((key) => pad + formatKey(key) + ":" + formatValue(value[key], pad, width, seen)).join(",\n") + "\n" + indent + "}";
257
+ }
258
+
206
259
  /**
207
260
  * Compact JS-literal rendering for the model: containers that fit in FORMAT_WIDTH
208
261
  * stay on one line with no separator whitespace, identifier keys are unquoted,
209
262
  * indent is one space. Whitespace is what costs tokens: this measures ~43% fewer
210
263
  * than JSON.stringify(value, null, 2) on typical shaped returns (gpt-tokenizer).
211
264
  */
212
- export function formatValue(value, indent = "", width = FORMAT_WIDTH) {
265
+ export function formatValue(value, indent = "", width = FORMAT_WIDTH, seen = new Set()) {
213
266
  if (value?.[RAW_TEXT] !== undefined) return value[RAW_TEXT];
214
267
 
215
268
  if (!isObject(value) && !Array.isArray(value)) return formatPrimitive(value);
216
- const flat = formatFlatWithin(value, width - indent.length);
269
+ if (seen.has(value)) return "[Circular]";
270
+ seen.add(value);
217
271
 
218
- if (flat !== null) return flat;
219
- const pad = indent + " ";
272
+ try {
273
+ const flat = formatFlatWithin(value, width - indent.length);
220
274
 
221
- if (Array.isArray(value)) {
222
- if (value.length === 0) return "[]";
275
+ if (flat !== null) return flat;
223
276
 
224
- return "[\n" + value.map((item) => pad + formatValue(item === undefined ? null : item, pad, width)).join(",\n") + "\n" + indent + "]";
277
+ return Array.isArray(value) ? formatArray(value, indent, width, seen) : formatObject(value, indent, width, seen);
278
+ } finally {
279
+ seen.delete(value);
225
280
  }
226
-
227
- const keys = Object.keys(value).filter((key) => value[key] !== undefined);
228
-
229
- if (keys.length === 0) return "{}";
230
-
231
- return "{\n" + keys.map((key) => pad + formatKey(key) + ":" + formatValue(value[key], pad, width)).join(",\n") + "\n" + indent + "}";
232
281
  }
@@ -0,0 +1,34 @@
1
+ /** Custom loader: guest programs cannot import host I/O modules. */
2
+ import { isString } from "../shared/decode.js";
3
+
4
+ const DENY = new Set([
5
+ "fs", "node:fs", "fs/promises", "node:fs/promises",
6
+ "child_process", "node:child_process",
7
+ "module", "node:module",
8
+ "worker_threads", "node:worker_threads",
9
+ "net", "node:net", "http", "node:http", "https", "node:https",
10
+ "os", "node:os", "vm", "node:vm",
11
+ "dgram", "node:dgram", "cluster", "node:cluster",
12
+ "inspector", "node:inspector", "sqlite", "node:sqlite",
13
+ ]);
14
+
15
+ export function guestImportMessage(specifier) {
16
+ return "guest cannot import " + specifier + "; use read, edit, write, or bash";
17
+ }
18
+
19
+ export function isDeniedGuestImport(specifier) {
20
+ if (!isString(specifier)) return false;
21
+ const key = specifier.replace(/^node:/, "");
22
+
23
+ return DENY.has(specifier) || DENY.has("node:" + key) || DENY.has(key);
24
+ }
25
+
26
+ export async function resolve(specifier, context, nextResolve) {
27
+ if (isDeniedGuestImport(specifier)) {
28
+ const error = new Error(guestImportMessage(specifier));
29
+ error.code = "ERR_GUEST_IMPORT";
30
+ throw error;
31
+ }
32
+
33
+ return nextResolve(specifier, context);
34
+ }