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
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { relativeSlash } from "../fs/workspace.js";
|
|
2
|
+
|
|
3
|
+
export function outlineOptions(params, references, config) {
|
|
4
|
+
const options = { references };
|
|
5
|
+
|
|
6
|
+
if (Number.isInteger(params?.maxChars) && params.maxChars > 0) options.maxChars = Math.min(params.maxChars, config.maxCallResultChars ?? 65536);
|
|
7
|
+
|
|
8
|
+
return options;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Outline lines carry their own line numbers (" 330 text"); provenance follows them. */
|
|
12
|
+
export function recordOutlineOrigins(ledger, rel, outlineText) {
|
|
13
|
+
for (const line of outlineText.split("\n")) {
|
|
14
|
+
const m = /^\s*(\d+) (.*)$/.exec(line);
|
|
15
|
+
|
|
16
|
+
if (m && !/ … \d+ lines$/.test(line)) ledger.recordOrigin(rel, Number(m[1]), [line]);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Where else a name appears (declaration line excluded), for outlines and edit results. */
|
|
21
|
+
export function createReferenceFinder(index, vfs) {
|
|
22
|
+
return async function referenceFinder(cwd, targetPath) {
|
|
23
|
+
let files;
|
|
24
|
+
|
|
25
|
+
try { files = [...new Set([...await index.files(cwd), ...vfs.getOverlayPaths()])]; }
|
|
26
|
+
catch { return () => []; }
|
|
27
|
+
|
|
28
|
+
if (!index.canScan(files)) return () => [];
|
|
29
|
+
|
|
30
|
+
return (name, excludeLine) => {
|
|
31
|
+
if (!name || name.length < 3) return [];
|
|
32
|
+
const escaped = name.replace(/[$]/g, (c) => "\\" + c);
|
|
33
|
+
const regex = new RegExp("\\b" + escaped + "\\b");
|
|
34
|
+
|
|
35
|
+
return index
|
|
36
|
+
.grepRows(files, regex, cwd, file => vfs.getOverlay(file))
|
|
37
|
+
.filter((r) => !(r.line === excludeLine && r.rel === relativeSlash(cwd, targetPath)))
|
|
38
|
+
.map((r) => r.rel + ":" + r.line);
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { isString } from "../shared/decode.js";
|
|
3
|
+
import { buildWriteDiff } from "../fs/diff.js";
|
|
4
|
+
import { quickCheck } from "../fs/check.js";
|
|
5
|
+
import { resolveWorkspacePath, relativeSlash } from "../fs/workspace.js";
|
|
6
|
+
import {
|
|
7
|
+
textResult, contentLineInfo, boundedWriteDiff,
|
|
8
|
+
WRITE_DIFF_MAX_READ_BYTES, WRITE_APPEND_MAX_READ_BYTES, QUICK_CHECK_MAX_CHARS,
|
|
9
|
+
writeSnapshot,
|
|
10
|
+
} from "../fs/text-ops.js";
|
|
11
|
+
|
|
12
|
+
const READ_ARTIFACT_MARK = /\[read truncated;|…\[[^\]\n]*truncated[^\]\n]*\]…/u;
|
|
13
|
+
|
|
14
|
+
function assertWriteAppendFlag(append) {
|
|
15
|
+
if (append !== undefined && append !== true && append !== false) throw new Error("write append must be a boolean");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function assertWriteArtifactsFlag(allowReadArtifacts) {
|
|
19
|
+
if (allowReadArtifacts !== undefined && allowReadArtifacts !== true && allowReadArtifacts !== false) throw new Error("write allowReadArtifacts must be a boolean");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function writeDiffFor(target, prevText, content, removedLines) {
|
|
23
|
+
if (removedLines === undefined && content.length <= WRITE_DIFF_MAX_READ_BYTES) {
|
|
24
|
+
return buildWriteDiff(target, prevText, content);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return boundedWriteDiff(target, content, removedLines ?? contentLineInfo(prevText).count);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function writeCheckWarning(content, target) {
|
|
31
|
+
if (content.length > QUICK_CHECK_MAX_CHARS) return "";
|
|
32
|
+
const check = quickCheck(content, path.extname(target));
|
|
33
|
+
|
|
34
|
+
return check && !check.ok ? "\ncheck: " + check.message : "";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function writeOutcome(rel, target, content, speculative, prevText, removedLines) {
|
|
38
|
+
const diff = writeDiffFor(target, prevText, content, removedLines);
|
|
39
|
+
const tag = speculative ? " (speculative)" : "";
|
|
40
|
+
|
|
41
|
+
return textResult(`wrote ${rel}${tag}${writeCheckWarning(content, target)}`, { path: target, speculative, diff });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function createWrite(ctx) {
|
|
45
|
+
const { getCwd, vfs, index } = ctx;
|
|
46
|
+
|
|
47
|
+
function assertWriteParams(params) {
|
|
48
|
+
if (!isString(params?.content)) throw new Error("write requires string content");
|
|
49
|
+
assertWriteAppendFlag(params.append);
|
|
50
|
+
assertWriteArtifactsFlag(params.allowReadArtifacts);
|
|
51
|
+
const content = String(params.content);
|
|
52
|
+
|
|
53
|
+
if (params.allowReadArtifacts !== true && READ_ARTIFACT_MARK.test(content)) {
|
|
54
|
+
throw new Error("refusing to write truncated read output; use edit() or reconstruct complete source windows. Set allowReadArtifacts:true only to intentionally write literal truncation-marker text");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return content;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function applyAppend(target, content, snap) {
|
|
61
|
+
let { previous: prevText, overlay, existingBytes } = snap;
|
|
62
|
+
|
|
63
|
+
if (existingBytes > WRITE_APPEND_MAX_READ_BYTES) throw new Error("append input exceeds " + WRITE_APPEND_MAX_READ_BYTES + " bytes; stream it with bash redirection instead");
|
|
64
|
+
|
|
65
|
+
if (existingBytes !== undefined && existingBytes > WRITE_DIFF_MAX_READ_BYTES) {
|
|
66
|
+
try { prevText = overlay !== undefined ? overlay : await vfs.read(target, { maxBytes: WRITE_APPEND_MAX_READ_BYTES, preserveRead: true }); }
|
|
67
|
+
catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return { content: prevText + content, prevText, removedLines: undefined };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function write(params, signal) {
|
|
74
|
+
const cwd = getCwd();
|
|
75
|
+
const target = await resolveWorkspacePath(cwd, params?.path, "write", false);
|
|
76
|
+
|
|
77
|
+
if (signal?.aborted) throw new Error("aborted");
|
|
78
|
+
let content = assertWriteParams(params);
|
|
79
|
+
const snap = await writeSnapshot(vfs, target, signal);
|
|
80
|
+
let { previous: prevText, removedLines } = snap;
|
|
81
|
+
|
|
82
|
+
if (params.append === true) {
|
|
83
|
+
const appended = await applyAppend(target, content, snap);
|
|
84
|
+
content = appended.content;
|
|
85
|
+
prevText = appended.prevText;
|
|
86
|
+
removedLines = appended.removedLines;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const { speculative } = await vfs.write(target, content);
|
|
90
|
+
index.touch(relativeSlash(cwd, target));
|
|
91
|
+
|
|
92
|
+
return writeOutcome(relativeSlash(cwd, target), target, content, speculative, prevText, removedLines);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return { write };
|
|
96
|
+
}
|
package/src/bridge/catalog.js
CHANGED
|
@@ -1,257 +1,67 @@
|
|
|
1
|
+
import { isString } from "../shared/decode.js";
|
|
1
2
|
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
name: "read",
|
|
7
|
-
description: "Read files, images or directories. JSON selectors project full documents within output budgets. Source questions locate and open source directly; resolve returns structured source/status without guessing.",
|
|
8
|
-
parameters: { type: "object", properties: {
|
|
9
|
-
path: { anyOf: [{ type: "string" }, { type: "array", items: { type: "string" } }], description: "Workspace-relative file or directory, source question, or array of paths" },
|
|
10
|
-
target: { anyOf: [{ type: "string" }, { type: "array" }], description: "File path/query or array of paths" },
|
|
11
|
-
offset: { type: "number", description: "One-based starting line" },
|
|
12
|
-
limit: { type: "number", description: "Maximum lines to return" },
|
|
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" },
|
|
16
|
-
complete: { type: "boolean", description: "Fail unless the entire requested file fits without clipping" },
|
|
17
|
-
json: { anyOf: [{ type: "boolean" }, { type: "string" }, { type: "array", items: { type: "string" } }], description: "Parse the complete JSON input (up to 16 MiB), then select .field, .items[0:3], or quoted keys. A selector array returns an array of values; true selects the root. Oversized selections fail, never clip." },
|
|
18
|
-
} },
|
|
19
|
-
},
|
|
20
|
-
{
|
|
21
|
-
name: "write", description: "Write UTF-8 content to a workspace file.",
|
|
22
|
-
parameters: { type: "object", properties: { path: { type: "string" }, content: { type: "string" }, allowReadArtifacts: { type: "boolean", description: "Explicit opt-in for intentionally writing literal truncation-marker text" } }, required: ["path", "content"] },
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
name: "edit", description: "Apply unique text replacements to a workspace file; returns the post-edit lines, a structural check, and references to changed declarations.",
|
|
26
|
-
parameters: { type: "object", properties: {
|
|
27
|
-
path: { type: "string" }, oldText: { type: "string" }, newText: { type: "string" }, edits: { type: "array", description: "[{oldText, newText}] for several replacements in one call" },
|
|
28
|
-
}, required: ["path"] },
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
name: "apply_patch", description: "Apply a unified diff to one workspace file.",
|
|
32
|
-
parameters: { type: "object", properties: { path: { type: "string" }, patch: { type: "string" } }, required: ["patch"] },
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
name: "snap", description: "Select source with found, ambiguous, not_found, or incomplete status. The read command uses the same engine.",
|
|
36
|
-
parameters: { type: "object", properties: {
|
|
37
|
-
query: { type: "string", description: "Source concept to resolve" },
|
|
38
|
-
path: { type: "string", description: "Optional workspace search root; explicitly targeting a hidden directory includes its hidden files, but Git metadata is always excluded" },
|
|
39
|
-
}, required: ["query"] },
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
name: "evidence", description: "Top-K source spans (with path and line provenance) that answer a concept question; read these instead of whole files.",
|
|
43
|
-
parameters: { type: "object", properties: {
|
|
44
|
-
query: { type: "string", description: "Concept, symbol, or question" },
|
|
45
|
-
path: { type: "string", description: "Optional search root" },
|
|
46
|
-
k: { type: "number", description: "Main spans to return (default 5)" },
|
|
47
|
-
maxChars: { type: "number", description: "Total text budget (default 6000)" },
|
|
48
|
-
}, required: ["query"] },
|
|
49
|
-
},
|
|
50
|
-
{
|
|
51
|
-
name: "surface", description: "Extract a structural outline from a workspace source file.",
|
|
52
|
-
parameters: { type: "object", properties: { path: { type: "string" } }, required: ["path"] },
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
name: "bash", description: "Run a shell command inside the workspace and capture bounded output.",
|
|
56
|
-
parameters: { type: "object", properties: { command: { type: "string" }, cwd: { type: "string" }, timeoutMs: { type: "number" } }, required: ["command"] },
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
name: "grep", description: "Search file contents. Smart-case regex; definition lines first (marked *); fuzzy fallback when nothing matches literally.",
|
|
60
|
-
parameters: { type: "object", properties: {
|
|
61
|
-
pattern: { type: "string" }, path: { type: "string" }, glob: { type: "string" }, caseSensitive: { type: "boolean" }, limit: { type: "number" },
|
|
62
|
-
}, required: ["pattern"] },
|
|
63
|
-
},
|
|
64
|
-
{
|
|
65
|
-
name: "glob", description: "Find files: a glob pattern, or free text for typo-tolerant, frecency-ranked path search.",
|
|
66
|
-
parameters: { type: "object", properties: { pattern: { type: "string" } }, required: ["pattern"] },
|
|
67
|
-
},
|
|
68
|
-
{
|
|
69
|
-
name: "find", description: "List workspace files, optionally constrained by path and pattern.",
|
|
70
|
-
parameters: { type: "object", properties: { path: { type: "string" }, pattern: { type: "string" }, glob: { type: "string" } } },
|
|
71
|
-
},
|
|
72
|
-
{
|
|
73
|
-
name: "ls", description: "List direct entries in a workspace directory.",
|
|
74
|
-
parameters: { type: "object", properties: { path: { type: "string" } } },
|
|
75
|
-
},
|
|
76
|
-
];
|
|
77
|
-
|
|
78
|
-
export function mergeNativeToolDefinitions(tools, capturedNames = []) {
|
|
79
|
-
const nativeByName = new Map(NATIVE_TOOL_DEFINITIONS.map((tool) => [tool.name, tool]));
|
|
80
|
-
const captured = new Set(capturedNames);
|
|
81
|
-
const seen = new Set();
|
|
82
|
-
const merged = [];
|
|
83
|
-
|
|
84
|
-
for (const tool of tools || []) {
|
|
85
|
-
const fallback = nativeByName.get(tool?.name);
|
|
86
|
-
merged.push(fallback && !captured.has(tool.name)
|
|
87
|
-
? { ...tool, ...fallback, sourceInfo: { path: "<native:" + tool.name + ">" } }
|
|
88
|
-
: tool);
|
|
89
|
-
|
|
90
|
-
if (tool?.name) seen.add(tool.name);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
for (const fallback of NATIVE_TOOL_DEFINITIONS) {
|
|
94
|
-
if (!seen.has(fallback.name)) {
|
|
95
|
-
merged.push({ ...fallback, sourceInfo: { path: "<native:" + fallback.name + ">" } });
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return merged;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function sourcePathOf(tool) {
|
|
103
|
-
if (tool.sourceInfo && isString(tool.sourceInfo.path)) return tool.sourceInfo.path;
|
|
104
|
-
|
|
105
|
-
if (isString(tool.extensionPath)) return tool.extensionPath;
|
|
106
|
-
|
|
107
|
-
if (isString(tool.sourcePath)) return tool.sourcePath;
|
|
108
|
-
|
|
109
|
-
return undefined;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function normalizeTool(tool) {
|
|
113
|
-
if (!tool || !isObject(tool)) return null;
|
|
114
|
-
const name = isString(tool.name) ? tool.name : "";
|
|
115
|
-
|
|
116
|
-
if (!name) return null;
|
|
117
|
-
const description = isString(tool.description) ? tool.description : "";
|
|
118
|
-
|
|
119
|
-
return {
|
|
120
|
-
name,
|
|
121
|
-
nameLower: name.toLowerCase(),
|
|
122
|
-
description,
|
|
123
|
-
descLower: description.toLowerCase(),
|
|
124
|
-
parameters: tool.parameters,
|
|
125
|
-
schemaError: tool.schemaError,
|
|
126
|
-
sourcePath: sourcePathOf(tool),
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
export function buildCatalog(tools, excludeNames = []) {
|
|
131
|
-
const exclude = new Set(excludeNames);
|
|
132
|
-
const rows = [];
|
|
133
|
-
|
|
134
|
-
for (const tool of tools || []) {
|
|
135
|
-
const row = normalizeTool(tool);
|
|
136
|
-
|
|
137
|
-
if (!row || exclude.has(row.name)) continue;
|
|
138
|
-
rows.push(row);
|
|
139
|
-
}
|
|
3
|
+
/** Optimal string alignment distance: insert/delete/substitute/adjacent-transpose cost 1. */
|
|
4
|
+
function osaCell(a, b, rows, i, j) {
|
|
5
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
6
|
+
let best = Math.min(rows[i - 1][j] + 1, rows[i][j - 1] + 1, rows[i - 1][j - 1] + cost);
|
|
140
7
|
|
|
141
|
-
|
|
8
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) best = Math.min(best, rows[i - 2][j - 2] + 1);
|
|
142
9
|
|
|
143
|
-
return
|
|
10
|
+
return best;
|
|
144
11
|
}
|
|
145
12
|
|
|
146
|
-
function
|
|
147
|
-
|
|
148
|
-
.toLowerCase()
|
|
149
|
-
.split(/[^a-z0-9_]+/g)
|
|
150
|
-
.filter((t) => t.length > 1);
|
|
151
|
-
}
|
|
13
|
+
function editDistance(a, b) {
|
|
14
|
+
const rows = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array.from({ length: b.length }, () => 0)]);
|
|
152
15
|
|
|
153
|
-
|
|
154
|
-
if (tokens.length === 0) return 1;
|
|
155
|
-
const name = row.nameLower || row.name.toLowerCase();
|
|
156
|
-
const desc = row.descLower || row.description.toLowerCase();
|
|
157
|
-
let score = 0;
|
|
16
|
+
for (let j = 1; j <= b.length; j++) rows[0][j] = j;
|
|
158
17
|
|
|
159
|
-
for (
|
|
160
|
-
|
|
161
|
-
else if (name.includes(token)) score += 5;
|
|
162
|
-
else if (desc.includes(token)) score += 2;
|
|
18
|
+
for (let i = 1; i < rows.length; i++) {
|
|
19
|
+
for (let j = 1; j <= b.length; j++) rows[i][j] = osaCell(a, b, rows, i, j);
|
|
163
20
|
}
|
|
164
21
|
|
|
165
|
-
return
|
|
22
|
+
return rows[a.length][b.length];
|
|
166
23
|
}
|
|
167
24
|
|
|
168
|
-
|
|
169
|
-
const
|
|
170
|
-
const scored = [];
|
|
25
|
+
function scoreName(needle, candidate, maxDistance) {
|
|
26
|
+
const lower = candidate.toLowerCase();
|
|
171
27
|
|
|
172
|
-
|
|
173
|
-
|
|
28
|
+
if (lower === needle) return null;
|
|
29
|
+
const distance = lower.includes(needle) || needle.includes(lower) ? 1 : editDistance(needle, lower);
|
|
174
30
|
|
|
175
|
-
|
|
176
|
-
scored.push({ name: row.name, description: row.description.slice(0, 160), score });
|
|
177
|
-
}
|
|
31
|
+
if (distance > maxDistance) return null;
|
|
178
32
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
return scored.slice(0, Math.max(1, limit)).map(({ score: _s, ...hit }) => hit);
|
|
33
|
+
return { candidate, distance };
|
|
182
34
|
}
|
|
183
35
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
for (let i = 1; i <= a.length; i++) {
|
|
191
|
-
for (let j = 1; j <= b.length; j++) {
|
|
192
|
-
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
193
|
-
let best = Math.min(rows[i - 1][j] + 1, rows[i][j - 1] + 1, rows[i - 1][j - 1] + cost);
|
|
194
|
-
|
|
195
|
-
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) best = Math.min(best, rows[i - 2][j - 2] + 1);
|
|
196
|
-
rows[i][j] = best;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
return rows[a.length][b.length];
|
|
36
|
+
function bySuggestionRank(needle, a, b) {
|
|
37
|
+
return (
|
|
38
|
+
a.distance - b.distance ||
|
|
39
|
+
Math.abs(a.candidate.length - needle.length) - Math.abs(b.candidate.length - needle.length) ||
|
|
40
|
+
a.candidate.localeCompare(b.candidate)
|
|
41
|
+
);
|
|
201
42
|
}
|
|
202
43
|
|
|
203
|
-
/** Closest tool names for a mistyped name: substring hits first, then a length-scaled edit distance. */
|
|
204
44
|
function suggestNames(name, candidates, limit = 3) {
|
|
205
|
-
const needle = String(name || "").toLowerCase();
|
|
45
|
+
const needle = String(name || "").toLowerCase().slice(0, 128);
|
|
206
46
|
|
|
207
47
|
if (!needle) return [];
|
|
208
48
|
const maxDistance = Math.max(1, Math.floor(needle.length / 3));
|
|
209
49
|
const scored = [];
|
|
210
50
|
|
|
211
51
|
for (const candidate of candidates) {
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
if (lower === needle) continue;
|
|
215
|
-
const distance = lower.includes(needle) || needle.includes(lower) ? 1 : editDistance(needle, lower);
|
|
52
|
+
const hit = scoreName(needle, candidate, maxDistance);
|
|
216
53
|
|
|
217
|
-
if (
|
|
54
|
+
if (hit) scored.push(hit);
|
|
218
55
|
}
|
|
219
56
|
|
|
220
|
-
scored.sort(
|
|
221
|
-
(a, b) =>
|
|
222
|
-
a.distance - b.distance ||
|
|
223
|
-
Math.abs(a.candidate.length - needle.length) - Math.abs(b.candidate.length - needle.length) ||
|
|
224
|
-
a.candidate.localeCompare(b.candidate),
|
|
225
|
-
);
|
|
57
|
+
scored.sort((a, b) => bySuggestionRank(needle, a, b));
|
|
226
58
|
|
|
227
59
|
return scored.slice(0, limit).map((s) => s.candidate);
|
|
228
60
|
}
|
|
229
61
|
|
|
230
62
|
export function unknownToolMessage(name, candidates) {
|
|
231
|
-
const close = suggestNames(name, candidates);
|
|
63
|
+
const close = suggestNames(name, candidates.filter(isString));
|
|
232
64
|
const hint = close.length ? ` Did you mean ${close.map((c) => JSON.stringify(c)).join(", ")}?` : "";
|
|
233
65
|
|
|
234
66
|
return `unknown tool "${name}".${hint} Check the command name and configured tool exclusions.`;
|
|
235
67
|
}
|
|
236
|
-
|
|
237
|
-
export function describeTool(catalog, name) {
|
|
238
|
-
const row = catalog.find((t) => t.name === name);
|
|
239
|
-
|
|
240
|
-
if (!row) {
|
|
241
|
-
return { ok: false, error: unknownToolMessage(name, catalog.map((t) => t.name)) };
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
if (!isObject(row.parameters)) return { ok: false, name, error: "tool schema unavailable: " + (row.schemaError ?? name) };
|
|
245
|
-
|
|
246
|
-
if (!row._described) {
|
|
247
|
-
row._described = {
|
|
248
|
-
ok: true,
|
|
249
|
-
name: row.name,
|
|
250
|
-
description: row.description,
|
|
251
|
-
parameters: row.parameters,
|
|
252
|
-
sourcePath: row.sourcePath,
|
|
253
|
-
};
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
return row._described;
|
|
257
|
-
}
|