pi-supernova 0.3.1 → 0.4.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 +220 -3
- package/docs/CHANGELOG.md +40 -0
- package/docs/TOKEN_COSTS.md +171 -0
- package/index.js +116 -37
- package/package.json +3 -1
- package/src/bridge/catalog.js +37 -2
- package/src/bridge/host-bridge.js +340 -13
- package/src/bridge/native-tools.js +35 -1
- package/src/config/config.default.json +1 -1
- package/src/config/config.js +19 -0
- package/src/context/evidence.js +119 -5
- package/src/context/fuzzy.js +37 -0
- package/src/context/ledger.js +128 -6
- package/src/context/outline.js +45 -1
- package/src/context/repo-index.js +63 -1
- package/src/context/search.js +45 -0
- package/src/context/snap.js +80 -4
- package/src/context/surface.js +25 -0
- package/src/fs/check.js +50 -0
- package/src/fs/diff.js +16 -0
- package/src/fs/json-read.js +87 -0
- package/src/fs/patch.js +26 -0
- package/src/fs/vfs.js +101 -6
- package/src/fs/workspace.js +36 -0
- package/src/output/bottleneck.js +46 -0
- package/src/output/format.js +117 -17
- package/src/runtime/guest-worker.js +151 -18
- package/src/runtime/parallel.js +33 -0
- package/src/runtime/program-batch.js +112 -0
- package/src/runtime/program-file.js +40 -0
- package/src/runtime/reference.js +25 -0
- package/src/runtime/runtime.js +90 -6
- package/src/shared/decode.js +29 -0
- package/src/ui/omp-frame.js +30 -1
- package/src/ui/render-measure.js +24 -0
- package/src/ui/render.js +96 -2
|
@@ -14,6 +14,7 @@ export const NATIVE_NAMES = Object.freeze(["read", "edit", "write", "bash"]);
|
|
|
14
14
|
function userPath(cwd, input) {
|
|
15
15
|
if (!isString(input) || !input.trim()) throw new Error("path must be a non-empty string");
|
|
16
16
|
const value = input.trim().replace(/^@/, "");
|
|
17
|
+
|
|
17
18
|
return path.resolve(cwd, value === "~" ? homedir() : value.startsWith("~/") ? path.join(homedir(), value.slice(2)) : value);
|
|
18
19
|
}
|
|
19
20
|
|
|
@@ -22,15 +23,20 @@ function boundText(result, maxChars) {
|
|
|
22
23
|
const truncated = total > maxChars;
|
|
23
24
|
const notice = truncated ? truncateChars("\n[Truncated: read files individually or narrow the line range/question.]", maxChars, "read-result").text : "";
|
|
24
25
|
let remaining = maxChars - notice.length;
|
|
26
|
+
|
|
25
27
|
const content = result.content.map(block => {
|
|
26
28
|
if (block.type !== "text") return block; // Images remain image attachments.
|
|
27
29
|
const bounded = truncateChars(block.text, remaining, "read-result");
|
|
28
30
|
remaining -= bounded.text.length;
|
|
31
|
+
|
|
29
32
|
return { ...block, text: bounded.text };
|
|
30
33
|
});
|
|
34
|
+
|
|
31
35
|
if (notice) content.push({ type: "text", text: notice });
|
|
32
36
|
const details = { ...result.details };
|
|
37
|
+
|
|
33
38
|
if (truncated) details.outputTruncated = true;
|
|
39
|
+
|
|
34
40
|
return { ...result, content, details };
|
|
35
41
|
}
|
|
36
42
|
|
|
@@ -39,6 +45,7 @@ export function registerNativeTools(pi, host, config = loadConfig()) {
|
|
|
39
45
|
let cwd = process.cwd();
|
|
40
46
|
const base = createHostBridge({ pi: null, config: { ...config, seenWindow: 0 }, getCwd: () => cwd });
|
|
41
47
|
const scheduler = createNativeScheduler();
|
|
48
|
+
|
|
42
49
|
const factories = {
|
|
43
50
|
read: host.createReadToolDefinition,
|
|
44
51
|
edit: host.createEditToolDefinition,
|
|
@@ -53,15 +60,19 @@ export function registerNativeTools(pi, host, config = loadConfig()) {
|
|
|
53
60
|
async function readOne(args, signal, ctx, bridge, id) {
|
|
54
61
|
const target = userPath(ctx.cwd, args.path);
|
|
55
62
|
let stat;
|
|
63
|
+
|
|
56
64
|
try { stat = await fs.stat(target); }
|
|
57
65
|
catch (error) { if (error.code !== "ENOENT" && error.code !== "ENOTDIR") throw error; }
|
|
66
|
+
|
|
58
67
|
signal?.throwIfAborted();
|
|
59
68
|
const image = /\.(png|jpe?g|gif|webp|bmp)$/i.test(target);
|
|
69
|
+
|
|
60
70
|
if (stat?.isFile() && (args.about === undefined || image)) {
|
|
61
71
|
// Pi retains image handling, full text, line windows, truncation metadata,
|
|
62
72
|
// and actionable continuation offsets. Do not summarize or dedupe these.
|
|
63
73
|
return factories.read(ctx.cwd, { autoResizeImages: image ? settingsFor(ctx)?.getImageAutoResize() : undefined }).execute(id, { ...args, path: target }, signal, undefined, ctx);
|
|
64
74
|
}
|
|
75
|
+
|
|
65
76
|
return boundText(await bridge.natives.read({ ...args, path: stat ? target : args.path }, signal), config.maxCallResultChars);
|
|
66
77
|
}
|
|
67
78
|
|
|
@@ -70,69 +81,90 @@ export function registerNativeTools(pi, host, config = loadConfig()) {
|
|
|
70
81
|
const bridge = base.fork({ getCwd: () => ctx.cwd });
|
|
71
82
|
bridge.bindCallContext(ctx, signal);
|
|
72
83
|
signal?.throwIfAborted();
|
|
84
|
+
|
|
73
85
|
try {
|
|
74
86
|
if (name === "read") {
|
|
75
87
|
if (!Array.isArray(args.path)) return await readOne(args, signal, ctx, bridge, id);
|
|
88
|
+
|
|
76
89
|
if (args.path.some(p => !isString(p) || !p.trim())) throw new Error("read paths must be non-empty strings");
|
|
90
|
+
|
|
77
91
|
if (args.path.length > 64) throw new Error("read path arrays support at most 64 entries; use smaller batches");
|
|
78
92
|
const groups = [], errors = [];
|
|
93
|
+
|
|
79
94
|
// Bound fan-out even when a model submits a large path array.
|
|
80
95
|
for (let offset = 0; offset < args.path.length; offset += 8) {
|
|
81
96
|
const results = await Promise.all(args.path.slice(offset, offset + 8).map(async file => {
|
|
82
97
|
try {
|
|
83
98
|
const result = await readOne({ ...args, path: file }, signal, ctx, bridge, id);
|
|
99
|
+
|
|
84
100
|
return { content: [{ type: "text", text: "File: " + file }, ...result.content] };
|
|
85
101
|
} catch (error) {
|
|
86
102
|
signal?.throwIfAborted();
|
|
87
103
|
errors.push({ path: file, message: error.message });
|
|
104
|
+
|
|
88
105
|
return { content: [{ type: "text", text: "[read error: " + file + "] " + error.message }] };
|
|
89
106
|
}
|
|
90
107
|
}));
|
|
108
|
+
|
|
91
109
|
groups.push(...results);
|
|
92
110
|
}
|
|
111
|
+
|
|
93
112
|
const content = [];
|
|
94
113
|
let remaining = config.maxCallResultChars, outputTruncated = false;
|
|
114
|
+
|
|
95
115
|
for (let i = 0; i < groups.length; i++) {
|
|
96
116
|
const bounded = boundText(groups[i], Math.floor(remaining / (groups.length - i)));
|
|
97
117
|
remaining -= bounded.content.reduce((sum, block) => sum + (block.type === "text" ? block.text.length : 0), 0);
|
|
98
118
|
outputTruncated ||= bounded.details.outputTruncated === true;
|
|
99
119
|
content.push(...bounded.content);
|
|
100
120
|
}
|
|
121
|
+
|
|
101
122
|
return { content, details: { batch: true, count: args.path.length, errors, outputTruncated } };
|
|
102
123
|
}
|
|
124
|
+
|
|
103
125
|
if (name === "bash") {
|
|
104
126
|
const settings = settingsFor(ctx);
|
|
127
|
+
|
|
105
128
|
try { return await factories.bash(ctx.cwd, { shellPath: settings?.getShellPath(), commandPrefix: settings?.getShellCommandPrefix() }).execute(id, args, signal, onUpdate, ctx); }
|
|
106
129
|
finally { bridge.invalidateFiles(); }
|
|
107
130
|
}
|
|
131
|
+
|
|
108
132
|
clearPathCache();
|
|
109
133
|
const target = await resolveWorkspacePath(ctx.cwd, userPath(ctx.cwd, args.path), name);
|
|
110
134
|
const ops = bridge.fileOperations;
|
|
111
135
|
let before, after;
|
|
136
|
+
|
|
112
137
|
const tool = factories[name](ctx.cwd, { operations: {
|
|
113
138
|
...ops,
|
|
114
|
-
async readFile(file) { const buffer = await ops.readFile(file); before = buffer.toString("utf8");
|
|
139
|
+
async readFile(file) { const buffer = await ops.readFile(file); before = buffer.toString("utf8");
|
|
140
|
+
|
|
141
|
+
return buffer; },
|
|
115
142
|
async writeFile(file, content) { await ops.writeFile(file, content); after = content; },
|
|
116
143
|
} });
|
|
144
|
+
|
|
117
145
|
// Pi's edit/write implementations hold its shared canonical per-file queue
|
|
118
146
|
// over the entire mutation, including our atomic replacement operation.
|
|
119
147
|
const result = await tool.execute(id, { ...args, path: target }, signal, onUpdate, ctx);
|
|
148
|
+
|
|
120
149
|
if (name === "edit" && before !== undefined && after !== undefined && result.details?.patch) {
|
|
121
150
|
const summary = await bridge.summarizeEdit(target, before, after, buildPatchDiff(target, result.details.patch));
|
|
122
151
|
result.content = [{ type: "text", text: summary }];
|
|
123
152
|
}
|
|
153
|
+
|
|
124
154
|
return result;
|
|
125
155
|
} finally { bridge.close(); }
|
|
126
156
|
}
|
|
127
157
|
|
|
128
158
|
for (const name of NATIVE_NAMES) {
|
|
129
159
|
const definition = factories[name](cwd);
|
|
160
|
+
|
|
130
161
|
const tool = {
|
|
131
162
|
...definition,
|
|
132
163
|
execute(id, args, signal, onUpdate, ctx) {
|
|
133
164
|
return scheduler.schedule(name, () => execute(name, id, args, signal, onUpdate, ctx), signal);
|
|
134
165
|
},
|
|
135
166
|
};
|
|
167
|
+
|
|
136
168
|
if (name === "read") {
|
|
137
169
|
tool.description += " Also reads directories or finds source from a symbol/question passed as path. Use about to focus a file or directory on a question. An array of paths returns all readable files and labels individual errors.";
|
|
138
170
|
tool.parameters = { ...definition.parameters, properties: {
|
|
@@ -142,8 +174,10 @@ export function registerNativeTools(pi, host, config = loadConfig()) {
|
|
|
142
174
|
} };
|
|
143
175
|
tool.promptGuidelines = [...(definition.promptGuidelines || []), "read can find source from a symbol or question; check its selection status before choosing a file. Plain file reads preserve full text within the stated limits."];
|
|
144
176
|
}
|
|
177
|
+
|
|
145
178
|
pi.registerTool(tool);
|
|
146
179
|
}
|
|
180
|
+
|
|
147
181
|
pi.on("session_start", (_event, ctx) => {
|
|
148
182
|
cwd = ctx?.cwd || cwd;
|
|
149
183
|
base.invalidateFiles();
|
package/src/config/config.js
CHANGED
|
@@ -5,10 +5,13 @@ import { createRequire } from "node:module";
|
|
|
5
5
|
import { isString, isObject } from "../shared/decode.js";
|
|
6
6
|
|
|
7
7
|
const require = createRequire(import.meta.url);
|
|
8
|
+
|
|
8
9
|
const DEFAULTS = require("./config.default.json");
|
|
9
10
|
|
|
10
11
|
const KNOWN_KEYS = new Set(Object.keys(DEFAULTS));
|
|
12
|
+
|
|
11
13
|
const NONNEGATIVE_INTEGER_KEYS = new Set(["maxLogLines", "seenWindow"]);
|
|
14
|
+
|
|
12
15
|
const POSITIVE_INTEGER_KEYS = new Set([
|
|
13
16
|
"timeoutMs",
|
|
14
17
|
"maxCodeChars",
|
|
@@ -28,11 +31,15 @@ const VALIDATORS = {
|
|
|
28
31
|
};
|
|
29
32
|
|
|
30
33
|
const KEY_VALIDATOR = new Map();
|
|
34
|
+
|
|
31
35
|
for (const k of POSITIVE_INTEGER_KEYS) KEY_VALIDATOR.set(k, VALIDATORS.positiveInt);
|
|
36
|
+
|
|
32
37
|
for (const k of NONNEGATIVE_INTEGER_KEYS) KEY_VALIDATOR.set(k, VALIDATORS.nonNegativeInt);
|
|
38
|
+
|
|
33
39
|
for (const k of Object.keys(DEFAULTS)) {
|
|
34
40
|
if (Array.isArray(DEFAULTS[k])) KEY_VALIDATOR.set(k, VALIDATORS.stringArray);
|
|
35
41
|
}
|
|
42
|
+
|
|
36
43
|
KEY_VALIDATOR.set("spillDir", VALIDATORS.spillDir);
|
|
37
44
|
|
|
38
45
|
export function packageDefaults() {
|
|
@@ -41,21 +48,28 @@ export function packageDefaults() {
|
|
|
41
48
|
|
|
42
49
|
function userConfigPath() {
|
|
43
50
|
if (process.env.PI_SUPERNOVA_CONFIG) return process.env.PI_SUPERNOVA_CONFIG;
|
|
51
|
+
|
|
44
52
|
for (const key of ["PI_CONFIG_DIR", "OMP_CONFIG_DIR"]) {
|
|
45
53
|
const root = process.env[key];
|
|
54
|
+
|
|
46
55
|
if (!root || !isString(root)) continue;
|
|
56
|
+
|
|
47
57
|
return path.join(path.resolve(root), "agent", "supernova.json");
|
|
48
58
|
}
|
|
59
|
+
|
|
49
60
|
const install = import.meta.dirname ?? path.dirname(new URL(import.meta.url).pathname);
|
|
50
61
|
const norm = String(install).replace(/\\/g, "/").toLowerCase();
|
|
51
62
|
const home = os.homedir();
|
|
63
|
+
|
|
52
64
|
if (norm.includes("/.omp/")) return path.join(home, ".omp", "agent", "supernova.json");
|
|
65
|
+
|
|
53
66
|
return path.join(home, ".pi", "agent", "supernova.json");
|
|
54
67
|
}
|
|
55
68
|
|
|
56
69
|
function readJson(filePath) {
|
|
57
70
|
try {
|
|
58
71
|
const text = fs.readFileSync(filePath, "utf8");
|
|
72
|
+
|
|
59
73
|
return JSON.parse(text);
|
|
60
74
|
} catch (err) {
|
|
61
75
|
if (err && (err.code === "ENOENT" || err.code === "ENOTDIR")) return null;
|
|
@@ -66,18 +80,23 @@ function readJson(filePath) {
|
|
|
66
80
|
export function mergeConfig(base, overlay) {
|
|
67
81
|
if (!overlay || !isObject(overlay) || Array.isArray(overlay)) return base;
|
|
68
82
|
const out = { ...base };
|
|
83
|
+
|
|
69
84
|
for (const [key, value] of Object.entries(overlay)) {
|
|
70
85
|
if (!KNOWN_KEYS.has(key)) continue;
|
|
71
86
|
const validate = KEY_VALIDATOR.get(key);
|
|
87
|
+
|
|
72
88
|
if (!validate) continue;
|
|
89
|
+
|
|
73
90
|
if (validate(value)) out[key] = Array.isArray(value) ? value.slice() : value;
|
|
74
91
|
}
|
|
92
|
+
|
|
75
93
|
return out;
|
|
76
94
|
}
|
|
77
95
|
|
|
78
96
|
export function loadConfig() {
|
|
79
97
|
const userPath = userConfigPath();
|
|
80
98
|
const user = readJson(userPath);
|
|
99
|
+
|
|
81
100
|
return mergeConfig(packageDefaults(), user ?? null);
|
|
82
101
|
}
|
|
83
102
|
|