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
@@ -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;
96
-
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);
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);
100
108
 
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$]*$/;
@@ -172,45 +193,69 @@ function formatFlatWithin(value, limit) {
172
193
  return walkFlat(value, push, new Set()) ? parts.join("") : null;
173
194
  }
174
195
 
175
- function walkFlat(value, push, seen) {
176
- if (value?.[RAW_TEXT] !== undefined) return push(value[RAW_TEXT]);
196
+ function walkFlatArray(value, push, seen) {
197
+ if (value.length === 0) return push("[]");
177
198
 
178
- if (!isObject(value) && !Array.isArray(value)) return push(formatPrimitive(value));
199
+ if (!push("[")) return false;
179
200
 
180
- if (seen.has(value)) return push("[Circular]");
181
- seen.add(value);
201
+ for (let i = 0; i < value.length; i++) {
202
+ if (i && !push(",")) return false;
182
203
 
183
- try {
184
- if (Array.isArray(value)) {
185
- if (value.length === 0) return push("[]");
204
+ if (!walkFlat(value[i] === undefined ? null : value[i], push, seen)) return false;
205
+ }
186
206
 
187
- if (!push("[")) return false;
207
+ return push("]");
208
+ }
188
209
 
189
- for (let i = 0; i < value.length; i++) {
190
- if (i && !push(",")) return false;
210
+ function walkFlatObject(value, push, seen) {
211
+ const keys = Object.keys(value).filter((key) => value[key] !== undefined);
191
212
 
192
- if (!walkFlat(value[i] === undefined ? null : value[i], push, seen)) return false;
193
- }
213
+ if (keys.length === 0) return push("{}");
194
214
 
195
- return push("]");
196
- }
215
+ for (let i = 0; i < keys.length; i++) {
216
+ if (!push((i ? "," : "{") + formatKey(keys[i]) + ":")) return false;
197
217
 
198
- const keys = Object.keys(value).filter((key) => value[key] !== undefined);
218
+ if (!walkFlat(value[keys[i]], push, seen)) return false;
219
+ }
199
220
 
200
- if (keys.length === 0) return push("{}");
221
+ return push("}");
222
+ }
201
223
 
202
- for (let i = 0; i < keys.length; i++) {
203
- if (!push((i ? "," : "{") + formatKey(keys[i]) + ":")) return false;
224
+ function walkFlat(value, push, seen) {
225
+ if (value?.[RAW_TEXT] !== undefined) return push(value[RAW_TEXT]);
204
226
 
205
- if (!walkFlat(value[keys[i]], push, seen)) return false;
206
- }
227
+ if (!isObject(value) && !Array.isArray(value)) return push(formatPrimitive(value));
207
228
 
208
- return push("}");
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);
209
234
  } finally {
210
235
  seen.delete(value);
211
236
  }
212
237
  }
213
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
+
214
259
  /**
215
260
  * Compact JS-literal rendering for the model: containers that fit in FORMAT_WIDTH
216
261
  * stay on one line with no separator whitespace, identifier keys are unquoted,
@@ -228,19 +273,8 @@ export function formatValue(value, indent = "", width = FORMAT_WIDTH, seen = new
228
273
  const flat = formatFlatWithin(value, width - indent.length);
229
274
 
230
275
  if (flat !== null) return flat;
231
- const pad = indent + " ";
232
-
233
- if (Array.isArray(value)) {
234
- if (value.length === 0) return "[]";
235
-
236
- return "[\n" + value.map((item) => pad + formatValue(item === undefined ? null : item, pad, width, seen)).join(",\n") + "\n" + indent + "]";
237
- }
238
-
239
- const keys = Object.keys(value).filter((key) => value[key] !== undefined);
240
-
241
- if (keys.length === 0) return "{}";
242
276
 
243
- return "{\n" + keys.map((key) => pad + formatKey(key) + ":" + formatValue(value[key], pad, width, seen)).join(",\n") + "\n" + indent + "}";
277
+ return Array.isArray(value) ? formatArray(value, indent, width, seen) : formatObject(value, indent, width, seen);
244
278
  } finally {
245
279
  seen.delete(value);
246
280
  }
@@ -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
+ }