pi-supernova 0.2.0 → 0.3.1
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 +196 -196
- package/{CHANGELOG.md → docs/CHANGELOG.md} +54 -1
- package/index.js +81 -68
- package/package.json +12 -31
- package/{catalog.js → src/bridge/catalog.js} +7 -5
- package/{host-bridge.js → src/bridge/host-bridge.js} +232 -87
- package/src/bridge/native-tools.js +155 -0
- package/src/bridge/pi-extension.ts +2 -0
- package/{config.js → src/config/config.js} +1 -1
- package/{evidence.js → src/context/evidence.js} +23 -12
- package/{outline.js → src/context/outline.js} +1 -1
- package/{repo-index.js → src/context/repo-index.js} +11 -9
- package/{search.js → src/context/search.js} +34 -1
- package/{snap.js → src/context/snap.js} +51 -31
- package/{surface.js → src/context/surface.js} +1 -1
- package/{diff.js → src/fs/diff.js} +12 -9
- package/{patch.js → src/fs/patch.js} +1 -1
- package/{vfs.js → src/fs/vfs.js} +55 -14
- package/{workspace.js → src/fs/workspace.js} +22 -6
- package/{bottleneck.js → src/output/bottleneck.js} +23 -6
- package/{format.js → src/output/format.js} +20 -1
- package/{guest-worker.js → src/runtime/guest-worker.js} +90 -21
- package/{parallel.js → src/runtime/parallel.js} +68 -1
- package/{runtime.js → src/runtime/runtime.js} +14 -5
- package/{omp-frame.js → src/ui/omp-frame.js} +1 -1
- package/{render-measure.js → src/ui/render-measure.js} +27 -1
- package/{render.js → src/ui/render.js} +42 -20
- /package/{config.default.json → src/config/config.default.json} +0 -0
- /package/{fuzzy.js → src/context/fuzzy.js} +0 -0
- /package/{ledger.js → src/context/ledger.js} +0 -0
- /package/{check.js → src/fs/check.js} +0 -0
- /package/{decode.js → src/shared/decode.js} +0 -0
package/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
import { isString, isFunction } from "./decode.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { runGuestProgram, warmGuestWorker } from "./runtime.js";
|
|
7
|
-
import { renderSupernovaCall, renderSupernovaResult } from "./render.js";
|
|
2
|
+
import { isString, isFunction } from "./src/shared/decode.js";
|
|
3
|
+
import { loadConfig } from "./src/config/config.js";
|
|
4
|
+
import { createHostBridge } from "./src/bridge/host-bridge.js";
|
|
5
|
+
import { truncateChars } from "./src/output/format.js";
|
|
6
|
+
import { runGuestProgram, warmGuestWorker, stopWarmGuestWorker } from "./src/runtime/runtime.js";
|
|
7
|
+
import { renderSupernovaCall, renderSupernovaResult } from "./src/ui/render.js";
|
|
8
8
|
|
|
9
9
|
export { renderSupernovaCall, renderSupernovaResult };
|
|
10
10
|
|
|
@@ -26,37 +26,36 @@ function result(text, details) {
|
|
|
26
26
|
return { content: [{ type: "text", text }], details };
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
const PROGRESS_FRAME_MS =
|
|
29
|
+
const PROGRESS_FRAME_MS = 80;
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
32
|
* Live trace updates for the card. The first update is immediate (seeds the result slot);
|
|
33
33
|
* later ones are coalesced to one host re-render per frame so a tight loop of nova.calls
|
|
34
34
|
* is not throttled by the TUI. A throwing host callback must never break the run.
|
|
35
35
|
*/
|
|
36
|
-
function progressEmitter(onUpdate) {
|
|
36
|
+
export function progressEmitter(onUpdate) {
|
|
37
37
|
if (!isFunction(onUpdate)) return Object.assign(() => {}, { flush() {} });
|
|
38
38
|
let pending = null;
|
|
39
39
|
let timer = null;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
onUpdate({ content: [{ type: "text", text: "" }], details: { trace, running: true } });
|
|
43
|
-
} catch {}
|
|
44
|
-
};
|
|
45
|
-
const flush = () => {
|
|
40
|
+
let lastSent = -Infinity;
|
|
41
|
+
const send = () => {
|
|
46
42
|
timer = null;
|
|
47
43
|
if (pending === null) return;
|
|
48
|
-
|
|
44
|
+
// Snapshot only at emission, not on every tool event. Completed records must
|
|
45
|
+
// not mutate a previously emitted frame while Pi is still consuming it.
|
|
46
|
+
const trace = pending.map(record => ({ ...record }));
|
|
49
47
|
pending = null;
|
|
50
|
-
|
|
48
|
+
lastSent = performance.now();
|
|
49
|
+
try {
|
|
50
|
+
onUpdate({ content: [{ type: "text", text: "" }], details: { trace, running: true } });
|
|
51
|
+
} catch {}
|
|
51
52
|
};
|
|
52
53
|
const emit = (trace) => {
|
|
53
|
-
if (timer === null && pending === null) {
|
|
54
|
-
send(trace);
|
|
55
|
-
timer = setTimeout(flush, PROGRESS_FRAME_MS);
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
54
|
pending = trace;
|
|
59
|
-
if (timer
|
|
55
|
+
if (timer !== null) return;
|
|
56
|
+
const wait = PROGRESS_FRAME_MS - (performance.now() - lastSent);
|
|
57
|
+
if (wait <= 0) send();
|
|
58
|
+
else timer = setTimeout(send, wait);
|
|
60
59
|
};
|
|
61
60
|
emit.flush = () => {
|
|
62
61
|
if (timer !== null) clearTimeout(timer);
|
|
@@ -66,10 +65,8 @@ function progressEmitter(onUpdate) {
|
|
|
66
65
|
return emit;
|
|
67
66
|
}
|
|
68
67
|
|
|
69
|
-
function sessionStats({ programs, returnedChars
|
|
70
|
-
|
|
71
|
-
const pct = total ? Math.round((collapsedChars / total) * 100) : 0;
|
|
72
|
-
return `this session: ${programs} programs · ~${Math.round(returnedChars / 4)} tokens returned · ~${Math.round(collapsedChars / 4)} already-seen tokens not re-sent (${pct}%, ${collapsedRuns} runs)`;
|
|
68
|
+
function sessionStats({ programs, returnedChars }) {
|
|
69
|
+
return `this session: ${programs} programs · ${returnedChars} output characters (not token counts)`;
|
|
73
70
|
}
|
|
74
71
|
|
|
75
72
|
function logsBlock(outcome, tail = "") {
|
|
@@ -89,21 +86,36 @@ const TOOL_DESCRIPTION = `Run one JavaScript program with four familiar commands
|
|
|
89
86
|
|
|
90
87
|
Native commands (async):
|
|
91
88
|
read(path|paths, offset?, limit?) → file text or text[]; read(directory) → directory entries
|
|
92
|
-
read("symbol or question") →
|
|
89
|
+
read("symbol or question") → locate and open source in one call, without an index; selected file text stays raw
|
|
90
|
+
read({query, resolve:true}) → {status,path,line,lines,text,complete,nextOffset?} for a direct resolve→edit handoff
|
|
93
91
|
read(path, {about: question}) → relevant file bodies, or source selection inside a directory
|
|
92
|
+
read({query, evidence:true}) → ranked evidence; read({path, outline:true}) → structural declarations
|
|
94
93
|
write(path, text) → write a file
|
|
95
|
-
edit(path, oldText, newText) → post-edit lines, checks, and references
|
|
94
|
+
edit(path, oldText, newText) → post-edit lines, checks, and references
|
|
95
|
+
edit(async () => {...}) → filesystem checkpoint: commit on success, rollback on throw; no shell commands, nesting, or concurrent outside commands
|
|
96
96
|
bash(command, {cwd?, timeoutMs?}) → bounded output; throws on non-zero exit
|
|
97
|
+
bash({command, args:[...]}) → literal argv without shell expansion of arguments
|
|
97
98
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
99
|
+
Only found selects and opens a file. Uncertain reads return ambiguous, not_found, or incomplete with no selected path. Use resolve:true for structured status checks; narrow the directory with path+about when uncertain.
|
|
100
|
+
Object arguments also work: read({path, offset?, limit?, about?, outline?, evidence?, resolve?}), edit({path, edits:[{oldText,newText}]}), edit({path,patch}), write({path,content}), bash({command,timeoutMs?}).
|
|
101
|
+
Independent read starts batch automatically. Mutations preserve submission order. Plain reads remain self-contained; oversized reads provide continuation offsets. Return only what the model needs. console.log is captured.`;
|
|
101
102
|
|
|
102
103
|
export default function piSupernova(pi) {
|
|
103
|
-
|
|
104
|
+
registerCodeMode(pi);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Shared entry used by both host adapters and direct engine integration.
|
|
108
|
+
export function registerCodeMode(pi) {
|
|
109
|
+
// Local cache residency cannot establish what remains in the model's context.
|
|
110
|
+
const config = { ...loadConfig(), seenWindow: 0 };
|
|
104
111
|
let cwd = process.cwd();
|
|
105
|
-
let catalog = [];
|
|
106
112
|
let programSeq = 0;
|
|
113
|
+
let stopped = false;
|
|
114
|
+
let warmTimer;
|
|
115
|
+
function cancelWarmTimer() {
|
|
116
|
+
if (warmTimer !== undefined) clearImmediate(warmTimer);
|
|
117
|
+
warmTimer = undefined;
|
|
118
|
+
}
|
|
107
119
|
|
|
108
120
|
const bridge = createHostBridge({
|
|
109
121
|
pi,
|
|
@@ -112,36 +124,21 @@ export default function piSupernova(pi) {
|
|
|
112
124
|
});
|
|
113
125
|
|
|
114
126
|
function refreshCatalog(target = bridge) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
.map(tool => {
|
|
119
|
-
const schema = tool.parameters;
|
|
120
|
-
try {
|
|
121
|
-
if (isFunction(schema?.toJsonSchema)) return { ...tool, parameters: schema.toJsonSchema() };
|
|
122
|
-
if (schema && !schema.type && pi.zod?.toJSONSchema && (schema._zod || schema._def)) {
|
|
123
|
-
return { ...tool, parameters: pi.zod.toJSONSchema(schema, { io: "input" }) };
|
|
124
|
-
}
|
|
125
|
-
return tool;
|
|
126
|
-
} catch (error) {
|
|
127
|
-
return { ...tool, parameters: undefined, schemaError: error.message };
|
|
128
|
-
}
|
|
129
|
-
});
|
|
130
|
-
catalog = buildCatalog(discoverable, config.excludeTools || []);
|
|
131
|
-
return catalog;
|
|
127
|
+
// Refresh executors and permissions, not a model-facing catalogue. Guest
|
|
128
|
+
// programs cannot dispatch arbitrary tools or consume their schemas.
|
|
129
|
+
return target.refreshTools();
|
|
132
130
|
}
|
|
133
131
|
|
|
134
|
-
function makeNovaApi(runBridge,
|
|
132
|
+
function makeNovaApi(runBridge, cancel) {
|
|
135
133
|
return {
|
|
136
|
-
search: async (query, limit) => searchCatalog(runCatalog, query, Number.isInteger(limit) ? limit : config.maxSearchResults),
|
|
137
|
-
describe: async (name) => describeTool(runCatalog, name),
|
|
138
134
|
call: (name, args) => runBridge.call(name, args),
|
|
139
135
|
callMany: (calls) => runBridge.callMany(calls),
|
|
140
|
-
speculateBegin: () => runBridge.beginSpeculation(),
|
|
141
|
-
speculateCommit: () => runBridge.commitSpeculation(),
|
|
142
|
-
speculateRollback: () => runBridge.rollbackSpeculation(),
|
|
143
|
-
names: () =>
|
|
136
|
+
speculateBegin: () => runBridge.barrier(() => runBridge.beginSpeculation()),
|
|
137
|
+
speculateCommit: () => runBridge.barrier(() => runBridge.commitSpeculation()),
|
|
138
|
+
speculateRollback: () => runBridge.barrier(() => runBridge.rollbackSpeculation()),
|
|
139
|
+
names: () => ["read", "edit", "write", "bash"],
|
|
144
140
|
batchRead: runBridge.supportsBatchRead(),
|
|
141
|
+
nativeArgv: runBridge.supportsNativeArgv?.() === true,
|
|
145
142
|
cancel,
|
|
146
143
|
};
|
|
147
144
|
}
|
|
@@ -152,12 +149,12 @@ export default function piSupernova(pi) {
|
|
|
152
149
|
description: TOOL_DESCRIPTION,
|
|
153
150
|
promptSnippet: "Use read, write, edit, and bash in one program",
|
|
154
151
|
promptGuidelines: [
|
|
155
|
-
"Use read, write, edit, and bash inside supernova. Start with read(question), or read(directory, {about: question}) for scoped source selection.
|
|
152
|
+
"Use read, write, edit, and bash inside supernova. Start with read(question), or read(directory, {about: question}) for scoped source selection. A source question already opens the selected file; do not issue a redundant read. Use read({query,resolve:true}) and check status before editing its path. Explicit about/outline/evidence reads remain available when needed. Return a compact value.",
|
|
156
153
|
],
|
|
157
154
|
parameters: Type.Object({
|
|
158
155
|
code: Type.String({ description: "JavaScript program: async body or arrow function." }),
|
|
159
156
|
timeoutMs: Type.Optional(Type.Integer({ minimum: 1000, description: "Hard timeout in ms." })),
|
|
160
|
-
}),
|
|
157
|
+
}, { required: ["code"] }),
|
|
161
158
|
// One self-owned result frame is shared by Pi and OMP; renderCall stays empty
|
|
162
159
|
// so separate call/result slots cannot duplicate the lifecycle card.
|
|
163
160
|
renderShell: "self",
|
|
@@ -165,6 +162,7 @@ export default function piSupernova(pi) {
|
|
|
165
162
|
renderCall: renderSupernovaCall,
|
|
166
163
|
renderResult: renderSupernovaResult,
|
|
167
164
|
async execute(_id, params, signal, onUpdate, ctx) {
|
|
165
|
+
cancelWarmTimer();
|
|
168
166
|
const runCwd = ctx?.cwd || cwd;
|
|
169
167
|
const runController = new AbortController();
|
|
170
168
|
const abortRun = () => runController.abort(signal?.reason);
|
|
@@ -182,18 +180,18 @@ export default function piSupernova(pi) {
|
|
|
182
180
|
const started = performance.now();
|
|
183
181
|
let outcome;
|
|
184
182
|
try {
|
|
185
|
-
|
|
183
|
+
refreshCatalog(runBridge);
|
|
186
184
|
runBridge.beginSpeculation();
|
|
187
185
|
outcome = await runGuestProgram({
|
|
188
186
|
code: params?.code,
|
|
189
|
-
nova: makeNovaApi(runBridge,
|
|
187
|
+
nova: makeNovaApi(runBridge, abortRun),
|
|
190
188
|
config: { ...config, timeoutMs: Number.isInteger(params?.timeoutMs) ? params.timeoutMs : config.timeoutMs },
|
|
191
189
|
signal: runController.signal,
|
|
192
190
|
onTimeout: abortRun,
|
|
193
191
|
});
|
|
194
192
|
runBridge.close();
|
|
195
193
|
if (outcome.ok) {
|
|
196
|
-
if (runBridge.getOverlayDepth() !== 1) throw new Error("program ended with an unfinished
|
|
194
|
+
if (runBridge.getOverlayDepth() !== 1) throw new Error("program ended with an unfinished edit checkpoint; await it before returning");
|
|
197
195
|
await runBridge.commitSpeculation();
|
|
198
196
|
}
|
|
199
197
|
else runBridge.rollbackSpeculation();
|
|
@@ -206,18 +204,36 @@ export default function piSupernova(pi) {
|
|
|
206
204
|
runBridge.setCallListener(null);
|
|
207
205
|
emitProgress.flush();
|
|
208
206
|
signal?.removeEventListener("abort", abortRun);
|
|
207
|
+
// Prepare one pristine worker during the model's next decision. Never
|
|
208
|
+
// recycle a worker that has executed arbitrary guest JavaScript.
|
|
209
|
+
cancelWarmTimer();
|
|
210
|
+
if (!stopped && !runController.signal.aborted) {
|
|
211
|
+
// Deliver the result before paying for another Worker constructor.
|
|
212
|
+
warmTimer = setImmediate(() => {
|
|
213
|
+
warmTimer = undefined;
|
|
214
|
+
if (!stopped && !runController.signal.aborted) warmGuestWorker(config).catch(() => {});
|
|
215
|
+
});
|
|
216
|
+
warmTimer.unref?.();
|
|
217
|
+
}
|
|
209
218
|
}
|
|
210
219
|
const trace = runBridge.getTrace();
|
|
211
220
|
const text = outcome.ok ? successText(outcome, call) : errorText(outcome, call);
|
|
212
|
-
|
|
221
|
+
const bounded = truncateChars(text, config.maxReturnChars, "output").text;
|
|
222
|
+
const visible = runBridge.ledger.dedupe(bounded, call);
|
|
223
|
+
if (!outcome.ok) throw new Error(visible);
|
|
224
|
+
const response = result(visible, {
|
|
213
225
|
ok: outcome.ok, error: outcome.error, wallMs: outcome.wallMs,
|
|
214
226
|
returnTruncated: outcome.returnTruncated, logTruncated: outcome.logTruncated,
|
|
215
227
|
logs: outcome.logs, result: outcome.result, trace,
|
|
216
228
|
});
|
|
229
|
+
if (outcome.images?.length) response.content.push(...outcome.images);
|
|
230
|
+
return response;
|
|
217
231
|
},
|
|
218
232
|
});
|
|
219
233
|
|
|
234
|
+
pi.on("session_shutdown", () => { stopped = true; cancelWarmTimer(); return stopWarmGuestWorker(); });
|
|
220
235
|
pi.on("session_start", (_event, ctx) => {
|
|
236
|
+
stopped = false;
|
|
221
237
|
if (ctx && isString(ctx.cwd) && ctx.cwd) cwd = ctx.cwd;
|
|
222
238
|
// A new session is a new model context: nothing has been seen yet.
|
|
223
239
|
bridge.bindCallContext(ctx);
|
|
@@ -232,12 +248,9 @@ export default function piSupernova(pi) {
|
|
|
232
248
|
handler: async (_args, ctx) => {
|
|
233
249
|
bridge.bindCallContext(ctx);
|
|
234
250
|
refreshCatalog();
|
|
235
|
-
const
|
|
236
|
-
const natives = Object.keys(bridge.natives).filter(name => bridge.isCallable(name) && !external.includes(name)).sort();
|
|
251
|
+
const commands = ["read", "edit", "write", "bash"].filter(bridge.isCallable);
|
|
237
252
|
const lines = [
|
|
238
|
-
`
|
|
239
|
-
`external tools: ${external.length ? external.join(", ") : "(none)"}`,
|
|
240
|
-
`native adapters: ${natives.join(", ")}`,
|
|
253
|
+
`Supernova CodeMode: ${commands.join(", ")}`,
|
|
241
254
|
`timeoutMs=${config.timeoutMs} maxCallResultChars=${config.maxCallResultChars} maxReturnChars=${config.maxReturnChars} maxBridgeCalls=${config.maxBridgeCalls} maxHeapMb=${config.maxHeapMb}`,
|
|
242
255
|
sessionStats(bridge.ledger.stats),
|
|
243
256
|
];
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-supernova",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "CodeMode for Pi and OMP
|
|
3
|
+
"version": "0.3.1",
|
|
4
|
+
"description": "One CodeMode invocation for Pi and OMP, with four guest commands, automatic read batching and source context.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "AdityaVG13",
|
|
7
7
|
"license": "MIT",
|
|
@@ -24,38 +24,15 @@
|
|
|
24
24
|
"exports": "./index.js",
|
|
25
25
|
"files": [
|
|
26
26
|
"index.js",
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"bottleneck.js",
|
|
30
|
-
"parallel.js",
|
|
31
|
-
"runtime.js",
|
|
32
|
-
"guest-worker.js",
|
|
33
|
-
"repo-index.js",
|
|
34
|
-
"evidence.js",
|
|
35
|
-
"fuzzy.js",
|
|
36
|
-
"outline.js",
|
|
37
|
-
"ledger.js",
|
|
38
|
-
"check.js",
|
|
39
|
-
"search.js",
|
|
40
|
-
"format.js",
|
|
41
|
-
"patch.js",
|
|
42
|
-
"vfs.js",
|
|
43
|
-
"workspace.js",
|
|
44
|
-
"config.js",
|
|
45
|
-
"config.default.json",
|
|
27
|
+
"src/",
|
|
28
|
+
"docs/",
|
|
46
29
|
"README.md",
|
|
47
|
-
"LICENSE"
|
|
48
|
-
"CHANGELOG.md",
|
|
49
|
-
"snap.js",
|
|
50
|
-
"diff.js",
|
|
51
|
-
"render.js",
|
|
52
|
-
"render-measure.js",
|
|
53
|
-
"omp-frame.js",
|
|
54
|
-
"surface.js",
|
|
55
|
-
"decode.js"
|
|
30
|
+
"LICENSE"
|
|
56
31
|
],
|
|
57
32
|
"scripts": {
|
|
58
|
-
"test": "node --test test
|
|
33
|
+
"test": "node --test tests/*/*.test.mjs",
|
|
34
|
+
"test:hosts": "node tests/hosts/verify.mjs",
|
|
35
|
+
"measure": "node tests/efficiency/measure.mjs",
|
|
59
36
|
"prepublishOnly": "npm test && node ../../scripts/preflight.mjs"
|
|
60
37
|
},
|
|
61
38
|
"pi": {
|
|
@@ -69,9 +46,13 @@
|
|
|
69
46
|
]
|
|
70
47
|
},
|
|
71
48
|
"peerDependencies": {
|
|
49
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
72
50
|
"typebox": "*"
|
|
73
51
|
},
|
|
74
52
|
"peerDependenciesMeta": {
|
|
53
|
+
"@earendil-works/pi-coding-agent": {
|
|
54
|
+
"optional": true
|
|
55
|
+
},
|
|
75
56
|
"typebox": {
|
|
76
57
|
"optional": true
|
|
77
58
|
}
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
|
|
2
|
-
import { isString, isObject } from "
|
|
2
|
+
import { isString, isObject } from "../shared/decode.js";
|
|
3
3
|
|
|
4
4
|
const NATIVE_TOOL_DEFINITIONS = [
|
|
5
5
|
{
|
|
6
6
|
name: "read",
|
|
7
|
-
description: "Read files
|
|
7
|
+
description: "Read files or directories. Source questions locate and open source directly; resolve returns structured source/status without guessing.",
|
|
8
8
|
parameters: { type: "object", properties: {
|
|
9
9
|
path: { anyOf: [{ type: "string" }, { type: "array", items: { type: "string" } }], description: "Workspace-relative file or directory, source question, or array of paths" },
|
|
10
10
|
target: { anyOf: [{ type: "string" }, { type: "array" }], description: "File path/query or array of paths" },
|
|
11
11
|
offset: { type: "number", description: "One-based starting line" },
|
|
12
12
|
limit: { type: "number", description: "Maximum lines to return" },
|
|
13
|
-
about: { type: "string", description: "Question or symbol: expand
|
|
13
|
+
about: { type: "string", description: "Question or symbol: expand file bodies, or locate and open source inside a directory" },
|
|
14
|
+
query: { type: "string", description: "Source question; optional path scopes the search directory" },
|
|
15
|
+
resolve: { type: "boolean", description: "Return structured source/status for a direct resolve-to-edit handoff" },
|
|
14
16
|
} },
|
|
15
17
|
},
|
|
16
18
|
{
|
|
@@ -161,7 +163,7 @@ export function searchCatalog(catalog, query, limit = 12) {
|
|
|
161
163
|
|
|
162
164
|
/** Optimal string alignment distance: insert/delete/substitute/adjacent-transpose cost 1. */
|
|
163
165
|
function editDistance(a, b) {
|
|
164
|
-
const rows = Array.from({ length: a.length + 1 }, (_, i) => [i, ...
|
|
166
|
+
const rows = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array.from({ length: b.length }, () => 0)]);
|
|
165
167
|
for (let j = 1; j <= b.length; j++) rows[0][j] = j;
|
|
166
168
|
for (let i = 1; i <= a.length; i++) {
|
|
167
169
|
for (let j = 1; j <= b.length; j++) {
|
|
@@ -198,7 +200,7 @@ function suggestNames(name, candidates, limit = 3) {
|
|
|
198
200
|
export function unknownToolMessage(name, candidates) {
|
|
199
201
|
const close = suggestNames(name, candidates);
|
|
200
202
|
const hint = close.length ? ` Did you mean ${close.map((c) => JSON.stringify(c)).join(", ")}?` : "";
|
|
201
|
-
return `unknown tool "${name}".${hint}
|
|
203
|
+
return `unknown tool "${name}".${hint} Check the command name and configured tool exclusions.`;
|
|
202
204
|
}
|
|
203
205
|
|
|
204
206
|
export function describeTool(catalog, name) {
|