pi-supernova 0.10.1 → 0.11.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 +376 -221
- package/docs/CHANGELOG.md +107 -5
- package/docs/TOKEN_COSTS.md +149 -324
- package/index.js +40 -11
- package/package.json +4 -3
- package/src/adapters/bash.js +40 -10
- package/src/adapters/edit.js +77 -23
- package/src/adapters/errors.js +4 -0
- package/src/adapters/index.js +1 -0
- package/src/adapters/list.js +6 -3
- package/src/adapters/read-focus.js +6 -0
- package/src/adapters/read-image.js +17 -9
- package/src/adapters/read-indexed.js +72 -0
- package/src/adapters/read-text.js +19 -2
- package/src/adapters/read.js +57 -13
- package/src/adapters/refs.js +17 -7
- package/src/adapters/write.js +14 -21
- package/src/bridge/host-bridge.js +152 -39
- package/src/bridge/invoke.js +3 -1
- package/src/bridge/tool-registry.js +132 -16
- package/src/context/evidence-graph.js +1 -0
- package/src/context/evidence-rank.js +1 -0
- package/src/context/evidence.js +4 -2
- package/src/context/fuzzy.js +31 -8
- package/src/context/outline.js +33 -9
- package/src/context/query.js +7 -2
- package/src/context/repo-index.js +8 -3
- package/src/context/search-files.js +20 -0
- package/src/context/search.js +25 -10
- package/src/context/snap-search.js +28 -6
- package/src/context/snap.js +8 -3
- package/src/context/source-entry.js +64 -28
- package/src/context/spans.js +1 -0
- package/src/contract/bash.js +16 -7
- package/src/contract/edit.js +26 -7
- package/src/contract/program.js +2 -1
- package/src/contract/read.js +46 -46
- package/src/fs/background.js +10 -2
- package/src/fs/check.js +844 -46
- package/src/fs/commit.js +89 -15
- package/src/fs/diff.js +12 -9
- package/src/fs/directory.js +14 -4
- package/src/fs/file-io.js +28 -7
- package/src/fs/glob.js +93 -11
- package/src/fs/json-read.js +3 -2
- package/src/fs/json-size.js +2 -1
- package/src/fs/lines.js +49 -2
- package/src/fs/patch.js +26 -10
- package/src/fs/process-tree.js +19 -1
- package/src/fs/read-window.js +36 -20
- package/src/fs/source-window.js +5 -10
- package/src/fs/text-ops.js +39 -19
- package/src/fs/vfs.js +296 -41
- package/src/fs/workspace.js +24 -6
- package/src/output/bottleneck.js +50 -25
- package/src/output/final.js +20 -0
- package/src/output/format.js +97 -36
- package/src/output/outcome.js +25 -9
- package/src/runtime/batch-input.js +17 -3
- package/src/runtime/guest-api.js +32 -7
- package/src/runtime/guest-worker.js +22 -4
- package/src/runtime/parallel.js +9 -49
- package/src/runtime/program-batch.js +53 -28
- package/src/runtime/program-file.js +2 -1
- package/src/runtime/program.js +94 -41
- package/src/runtime/reference.js +10 -9
- package/src/runtime/runtime.js +86 -17
- package/src/runtime/worker-pool.js +10 -1
- package/src/shared/decode.js +9 -8
- package/src/shared/image-worker.js +7 -0
- package/src/shared/image.js +58 -15
- package/src/shared/png.js +18 -0
- package/src/shared/result.js +30 -9
- package/src/shared/syntax-context.js +7 -1
- package/src/shared/utf8.js +11 -13
- package/src/ui/host-render.js +4 -0
- package/src/ui/omp-frame.js +2 -0
- package/src/ui/progress.js +2 -1
- package/src/ui/render-measure.js +11 -0
- package/src/ui/render.js +92 -31
- package/src/ui/trace.js +8 -5
package/src/contract/edit.js
CHANGED
|
@@ -20,7 +20,7 @@ export function viewSpan(value) {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
export function isEditView(value) {
|
|
23
|
-
return isObject(value) && !Array.isArray(value) && isString(value.path) && value.path.
|
|
23
|
+
return isObject(value) && !Array.isArray(value) && isString(value.path) && value.path.length > 0 && isString(value.text) && (value.status === undefined || value.status === "found") && viewSpan(value);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
function classifyViewEdit(p, oldText, newText) {
|
|
@@ -60,7 +60,7 @@ function normalizeEditArgs(p, oldText, newText) {
|
|
|
60
60
|
function namedEditArgs(p, oldText, newText) {
|
|
61
61
|
const args = normalizeEditArgs(p, oldText, newText);
|
|
62
62
|
|
|
63
|
-
if (!isString(args.path) ||
|
|
63
|
+
if (!isString(args.path) || args.path.length === 0) throw new Error(EDIT_USAGE);
|
|
64
64
|
|
|
65
65
|
return args;
|
|
66
66
|
}
|
|
@@ -82,18 +82,32 @@ function classifyReplacements(args) {
|
|
|
82
82
|
|
|
83
83
|
if (!Array.isArray(edits) || !edits.length) throw new Error(EDIT_USAGE);
|
|
84
84
|
|
|
85
|
-
for (const
|
|
85
|
+
for (const [i, edit] of edits.entries()) {
|
|
86
|
+
if (!validReplacement(edit)) throw new Error(replacementError(edit));
|
|
87
|
+
|
|
88
|
+
if (args.edits !== undefined) assertEditOptions(edit, REPLACEMENT_OPTION_KEYS, "edit " + (i + 1) + " of " + edits.length);
|
|
89
|
+
}
|
|
86
90
|
|
|
87
91
|
return { kind: "edits", command: "edit", args };
|
|
88
92
|
}
|
|
89
93
|
|
|
94
|
+
function validReplacement(edit) {
|
|
95
|
+
return isString(edit?.oldText) && !!edit.oldText.length && isString(edit?.newText);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function replacementError(edit) {
|
|
99
|
+
return EDIT_USAGE + "; replacements require non-empty oldText and string newText" + (isString(edit?.oldText) && !edit.oldText.length ? "; to insert, include adjacent existing text in oldText and repeat it in newText" : "");
|
|
100
|
+
}
|
|
101
|
+
|
|
90
102
|
const EDIT_OPTION_KEYS = ["path", "oldText", "newText", "edits", "patch"];
|
|
91
103
|
|
|
104
|
+
const REPLACEMENT_OPTION_KEYS = ["oldText", "newText"];
|
|
105
|
+
|
|
92
106
|
/** A view object carries host fields; only named replacements are validated. */
|
|
93
|
-
function assertEditOptions(args) {
|
|
94
|
-
const unknown = Object.keys(args).filter(key => !
|
|
107
|
+
function assertEditOptions(args, allowed = EDIT_OPTION_KEYS, label = "edit") {
|
|
108
|
+
const unknown = Object.keys(args).filter(key => !allowed.includes(key));
|
|
95
109
|
|
|
96
|
-
if (unknown.length) throw new Error("
|
|
110
|
+
if (unknown.length) throw new Error(label + " does not accept option " + unknown.map(key => JSON.stringify(key)).join(", ") + "; supported options are " + allowed.join(", "));
|
|
97
111
|
}
|
|
98
112
|
|
|
99
113
|
function classifyNamedEdit(p, oldText, newText) {
|
|
@@ -106,7 +120,12 @@ function classifyNamedEdit(p, oldText, newText) {
|
|
|
106
120
|
|
|
107
121
|
/** Guest signature → { command, args } for one host call. */
|
|
108
122
|
export function classifyEdit(p, oldText, newText) {
|
|
109
|
-
if (isFunction(p))
|
|
123
|
+
if (isFunction(p)) {
|
|
124
|
+
if (oldText !== undefined || newText !== undefined) throw new Error(EDIT_USAGE);
|
|
125
|
+
|
|
126
|
+
return { kind: "checkpoint", fn: p };
|
|
127
|
+
}
|
|
128
|
+
|
|
110
129
|
if (isEditView(p) && isString(oldText) && (newText === undefined || isString(newText))) return classifyViewEdit(p, oldText, newText);
|
|
111
130
|
|
|
112
131
|
return classifyNamedEdit(p, oldText, newText);
|
package/src/contract/program.js
CHANGED
|
@@ -9,7 +9,8 @@ try {
|
|
|
9
9
|
Type = require("typebox").Type;
|
|
10
10
|
} catch {
|
|
11
11
|
Type = {
|
|
12
|
-
|
|
12
|
+
// Match Type.Object defaults; explicit nested-entry guards live in opts.
|
|
13
|
+
Object: (props, opts) => ({ type: "object", properties: props || {}, ...opts }),
|
|
13
14
|
String: (opts) => ({ type: "string", ...opts }),
|
|
14
15
|
Unknown: (opts) => ({ ...opts }),
|
|
15
16
|
Array: (items, opts) => ({ type: "array", items, ...opts }),
|
package/src/contract/read.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { errorMessage, isString, isObject,
|
|
1
|
+
import { errorMessage, isString, isObject, looksLikePath } from "../shared/decode.js";
|
|
2
2
|
import { foldJsonSelectorAlias, sessionJsonArgs, validateJsonRead } from "../fs/json-read.js";
|
|
3
3
|
|
|
4
4
|
export const SESSION_URI = /^(?:agent|artifact):\/\//i;
|
|
5
5
|
|
|
6
|
-
const BOOL_KEYS = ["resolve", "complete", "outline", "evidence"];
|
|
6
|
+
const BOOL_KEYS = ["resolve", "complete", "outline", "evidence", "indexed"];
|
|
7
7
|
|
|
8
|
-
const READ_OPTION_KEYS = ["path", "target", "about", "query", "offset", "limit", "json", "resolve", "complete", "outline", "evidence", "maxChars", "_independent"];
|
|
8
|
+
const READ_OPTION_KEYS = ["path", "target", "about", "query", "offset", "limit", "json", "resolve", "complete", "outline", "evidence", "maxChars", "indexed", "_independent"];
|
|
9
9
|
|
|
10
10
|
/** Unknown options used to be dropped silently: {start,end} read the whole file. */
|
|
11
11
|
function assertReadOptions(args) {
|
|
@@ -26,25 +26,28 @@ export function isSessionUri(value) {
|
|
|
26
26
|
|
|
27
27
|
/** Guest call shape → one options object. */
|
|
28
28
|
export function gatherReadArgs(p, a, b) {
|
|
29
|
-
if (isObject(p) && !Array.isArray(p))
|
|
30
|
-
const args = { ...p, path: p.path ?? p.target ?? p.query };
|
|
31
|
-
|
|
32
|
-
if (p.path === undefined && p.target !== undefined) delete args.target;
|
|
33
|
-
|
|
34
|
-
return args;
|
|
35
|
-
}
|
|
29
|
+
if (isObject(p) && !Array.isArray(p)) return gatherObjectReadArgs(p);
|
|
36
30
|
|
|
37
31
|
// Only the string shorthand guesses between a path and a symbol. Explicit
|
|
38
32
|
// path/target objects and path arrays must not turn missing files into search.
|
|
39
33
|
return autoResolve(isObject(a) && !Array.isArray(a) ? { path: p, ...a } : { path: p, offset: a, limit: b });
|
|
40
34
|
}
|
|
41
35
|
|
|
36
|
+
function gatherObjectReadArgs(p) {
|
|
37
|
+
// Query text is not a scope, even when it looks like a filename.
|
|
38
|
+
const args = { ...p, path: p.path ?? p.target ?? (p.query !== undefined ? "." : undefined) };
|
|
39
|
+
|
|
40
|
+
if (p.path === undefined && p.target !== undefined) delete args.target;
|
|
41
|
+
|
|
42
|
+
return args;
|
|
43
|
+
}
|
|
44
|
+
|
|
42
45
|
export function assertReadPaths(targetParam) {
|
|
43
46
|
if (!Array.isArray(targetParam)) return;
|
|
44
47
|
|
|
45
48
|
if (targetParam.length > 64) throw new Error("read accepts at most 64 paths per batch");
|
|
46
49
|
|
|
47
|
-
for (const item of targetParam) if (!isString(item) ||
|
|
50
|
+
for (const item of targetParam) if (!isString(item) || item.length === 0) throw new Error("read paths must be non-empty strings");
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
function assertReadFlags(args) {
|
|
@@ -57,11 +60,19 @@ function assertReadFlags(args) {
|
|
|
57
60
|
if (args.query !== undefined && !isString(args.query)) throw new Error("read query must be a string");
|
|
58
61
|
}
|
|
59
62
|
|
|
60
|
-
function
|
|
63
|
+
function assertFocusModes(args) {
|
|
61
64
|
const focusModes = [args.about !== undefined, args.query !== undefined, args.outline === true].filter(Boolean).length;
|
|
62
65
|
|
|
63
66
|
if (focusModes > 1 || (args.outline === true && args.evidence === true)) throw new Error("read accepts only one of about, query, outline, or evidence");
|
|
64
67
|
|
|
68
|
+
return focusModes;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function assertExclusiveRead(args) {
|
|
72
|
+
if (args.indexed === true && indexedConflict(args)) throw new Error("indexed:true requires a source query, not another read mode or explicit window");
|
|
73
|
+
|
|
74
|
+
const focusModes = assertFocusModes(args);
|
|
75
|
+
|
|
65
76
|
if (args.resolve === true && args.complete === true) throw new Error("read accepts either resolve or complete, not both");
|
|
66
77
|
|
|
67
78
|
if ((focusModes === 1 || args.evidence === true) && args.complete === true) throw new Error("complete:true requires a raw file read, not a source view");
|
|
@@ -70,7 +81,7 @@ function assertExclusiveRead(args) {
|
|
|
70
81
|
function autoResolve(args) {
|
|
71
82
|
if (!isString(args.path) || args.resolve !== undefined || args.complete === true || args.json !== undefined) return args;
|
|
72
83
|
|
|
73
|
-
if (args
|
|
84
|
+
if (hasDefinedOption(args, ["about", "query", "offset", "limit"]) || looksLikePath(args.path) || isSessionUri(args.path)) return args;
|
|
74
85
|
|
|
75
86
|
return { ...args, resolve: true };
|
|
76
87
|
}
|
|
@@ -103,20 +114,30 @@ export function needsProbe(params) {
|
|
|
103
114
|
* Kind of read after exclusive modes are already validated.
|
|
104
115
|
* `existing` is the probe result, or null/undefined when needsProbe is false or the path is missing.
|
|
105
116
|
*/
|
|
106
|
-
function
|
|
107
|
-
|
|
108
|
-
|
|
117
|
+
function hasDefinedOption(args, keys) {
|
|
118
|
+
return keys.some(key => args[key] !== undefined);
|
|
119
|
+
}
|
|
109
120
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
121
|
+
function indexedConflict(args) {
|
|
122
|
+
return hasDefinedOption(args, ["about", "json", "offset", "limit"]) || ["outline", "evidence", "complete"].some(key => args[key] === true);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function classifyDirectory(params, existing) {
|
|
126
|
+
if (params.json !== undefined) throw new Error("JSON read requires a file, not a directory");
|
|
127
|
+
|
|
128
|
+
return isString(params.about)
|
|
129
|
+
? { kind: "snap", query: params.about, scoped: true, existing }
|
|
130
|
+
: { kind: "dir", existing };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function classifyExisting(params, existing) {
|
|
134
|
+
if (existing.directory) return classifyDirectory(params, existing);
|
|
114
135
|
|
|
115
136
|
if (params.resolve === true && isString(params.about)) {
|
|
116
137
|
throw new Error("resolve:true cannot combine with about on a file; use about for a focused outline or resolve for source text");
|
|
117
138
|
}
|
|
118
139
|
|
|
119
|
-
if (isString(params.about) && existing.size > 512 * 1024) return { kind: "focus", existing, about: params.about };
|
|
140
|
+
if (isString(params.about) && (existing.size > 512 * 1024 || /\.(?:md|markdown)$/i.test(existing.path))) return { kind: "focus", existing, about: params.about };
|
|
120
141
|
|
|
121
142
|
return { kind: params.resolve ? "open" : "file", existing };
|
|
122
143
|
}
|
|
@@ -166,37 +187,16 @@ function jsonSelectorNote(args) {
|
|
|
166
187
|
return args.json === undefined ? "" : " (" + (Array.isArray(args.json) ? args.json.join(", ") : String(args.json)) + ")";
|
|
167
188
|
}
|
|
168
189
|
|
|
169
|
-
export const ROUTING_STATUS = "too_large";
|
|
170
|
-
|
|
171
|
-
const ROUTING_PREFIX = '{"status":"too_large",';
|
|
172
|
-
|
|
173
|
-
export function isRoutingPayload(value) {
|
|
174
|
-
return isString(value) && value.startsWith(ROUTING_PREFIX);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function isRoutingObject(parsed) {
|
|
178
|
-
return isObject(parsed) && parsed.status === ROUTING_STATUS && isString(parsed.path)
|
|
179
|
-
&& isNumber(parsed.chars) && (Array.isArray(parsed.keys) || isNumber(parsed.length));
|
|
180
|
-
}
|
|
181
|
-
|
|
182
190
|
function decodeByArgs(args, value) {
|
|
183
191
|
return (args.resolve || args.json !== undefined || args.outline || args.evidence) && isString(value);
|
|
184
192
|
}
|
|
185
193
|
|
|
186
194
|
export function decodeReadValue(args, value) {
|
|
187
|
-
|
|
195
|
+
// Decode only by requested mode, never by fields inside literal host text.
|
|
196
|
+
if (!decodeByArgs(args, value)) return value;
|
|
188
197
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
try {
|
|
192
|
-
const parsed = JSON.parse(value);
|
|
193
|
-
|
|
194
|
-
return sniffed && !isRoutingObject(parsed) ? value : parsed;
|
|
195
|
-
} catch (error) {
|
|
196
|
-
if (sniffed && !decodeByArgs(args, value)) return value;
|
|
197
|
-
|
|
198
|
-
throw jsonReadError(args, error);
|
|
199
|
-
}
|
|
198
|
+
try { return JSON.parse(value); }
|
|
199
|
+
catch (error) { throw jsonReadError(args, error); }
|
|
200
200
|
}
|
|
201
201
|
|
|
202
202
|
function jsonReadError(args, error) {
|
package/src/fs/background.js
CHANGED
|
@@ -36,6 +36,14 @@ function wake(job) {
|
|
|
36
36
|
for (const notify of job.waiters) notify();
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
// Decoded streams contain complete pairs, but retention/cursor slicing may
|
|
40
|
+
// land on a low surrogate. Deliver only the next complete code point.
|
|
41
|
+
function completeCharacterStart(text, index) {
|
|
42
|
+
const code = text.charCodeAt(index);
|
|
43
|
+
|
|
44
|
+
return code >= 0xdc00 && code <= 0xdfff ? index + 1 : index;
|
|
45
|
+
}
|
|
46
|
+
|
|
39
47
|
function appendOutput(job, chunk) {
|
|
40
48
|
job.total += chunk.length;
|
|
41
49
|
job.output = (job.output + chunk).slice(-OUTPUT_CHARS);
|
|
@@ -45,12 +53,12 @@ function appendOutput(job, chunk) {
|
|
|
45
53
|
function snapshot(job, cursor = 0) {
|
|
46
54
|
if (cursor > job.total) throw new Error("terminal cursor is beyond available output");
|
|
47
55
|
const start = job.total - job.output.length;
|
|
48
|
-
const outputStart = Math.max(
|
|
56
|
+
const outputStart = start + completeCharacterStart(job.output, Math.max(0, cursor - start));
|
|
49
57
|
|
|
50
58
|
const result = {
|
|
51
59
|
sessionId:job.id, pid:job.child.pid, status:job.status, pty:job.pty,
|
|
52
60
|
exitCode:job.exitCode, signal:job.signal, outputStart, cursor:job.total,
|
|
53
|
-
truncated:cursor <
|
|
61
|
+
truncated:cursor < outputStart, output:job.output.slice(outputStart-start),
|
|
54
62
|
};
|
|
55
63
|
|
|
56
64
|
if (job.error) result.error = job.error;
|