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/output/format.js
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
import { isString, isObject } from "../shared/decode.js";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
10
|
+
return Number.isFinite(numericLimit) ? Math.max(0, Math.floor(numericLimit)) : numericLimit === Infinity ? length : 0;
|
|
11
|
+
}
|
|
9
12
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
98
|
-
|
|
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
|
-
|
|
105
|
-
|
|
112
|
+
function shouldFrameRaw(input) {
|
|
113
|
+
return isString(input) && input.includes("\n") && !hasUnpairedSurrogate(input) && JSON.stringify(input).length - input.length > 64;
|
|
114
|
+
}
|
|
106
115
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
116
|
+
function visitRawStrings(input, strings) {
|
|
117
|
+
if (shouldFrameRaw(input)) {
|
|
118
|
+
const index = strings.push(input) - 1;
|
|
110
119
|
|
|
111
|
-
|
|
112
|
-
|
|
120
|
+
return { [RAW_TEXT]: "raw[" + index + "]" };
|
|
121
|
+
}
|
|
113
122
|
|
|
114
|
-
|
|
123
|
+
if (Array.isArray(input)) return input.map(child => visitRawStrings(child, strings));
|
|
115
124
|
|
|
116
|
-
|
|
125
|
+
if (isObject(input)) return Object.fromEntries(Object.entries(input).map(([key, child]) => [key, visitRawStrings(child, strings)]));
|
|
117
126
|
|
|
118
|
-
|
|
119
|
-
|
|
127
|
+
return input;
|
|
128
|
+
}
|
|
120
129
|
|
|
121
|
-
|
|
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
|
|
175
|
-
if (value
|
|
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
|
-
|
|
185
|
-
if (i && !push(",")) return false;
|
|
199
|
+
if (!push("[")) return false;
|
|
186
200
|
|
|
187
|
-
|
|
188
|
-
|
|
201
|
+
for (let i = 0; i < value.length; i++) {
|
|
202
|
+
if (i && !push(",")) return false;
|
|
189
203
|
|
|
190
|
-
|
|
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
|
-
|
|
269
|
+
if (seen.has(value)) return "[Circular]";
|
|
270
|
+
seen.add(value);
|
|
217
271
|
|
|
218
|
-
|
|
219
|
-
|
|
272
|
+
try {
|
|
273
|
+
const flat = formatFlatWithin(value, width - indent.length);
|
|
220
274
|
|
|
221
|
-
|
|
222
|
-
if (value.length === 0) return "[]";
|
|
275
|
+
if (flat !== null) return flat;
|
|
223
276
|
|
|
224
|
-
return
|
|
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
|
+
}
|