@sean.holung/minicode 0.3.4 → 0.3.5
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 +5 -3
- package/dist/scripts/run-benchmarks.js +73 -28
- package/dist/src/benchmark/runner.js +142 -59
- package/dist/src/indexer/project-index.js +49 -13
- package/dist/src/serve/agent-bridge.js +12 -3
- package/dist/src/serve/mcp-server.js +70 -21
- package/dist/src/serve/server.js +37 -4
- package/dist/src/shared/graph-symbols.js +82 -0
- package/dist/src/shared/symbol-resolution.js +33 -0
- package/dist/src/tools/find-path.js +15 -6
- package/dist/src/tools/find-references.js +7 -2
- package/dist/src/tools/get-dependencies.js +8 -3
- package/dist/src/tools/read-symbol.js +9 -3
- package/dist/src/tools/registry.js +4 -1
- package/dist/src/tools/search-code-map.js +18 -3
- package/dist/src/web/app.js +154 -33
- package/dist/tests/benchmark-harness.test.js +100 -0
- package/dist/tests/file-tools.test.js +34 -1
- package/dist/tests/find-path.test.js +43 -2
- package/dist/tests/find-references.test.js +49 -0
- package/dist/tests/get-dependencies.test.js +23 -0
- package/dist/tests/graph-symbols.test.js +45 -0
- package/dist/tests/indexer.test.js +6 -0
- package/dist/tests/read-symbol.test.js +35 -0
- package/dist/tests/request-tracker.test.js +15 -0
- package/dist/tests/run-benchmarks.test.js +117 -33
- package/dist/tests/search-code-map.test.js +2 -0
- package/dist/tests/serve.integration.test.js +109 -3
- package/dist/tests/session-ui.test.js +2 -0
- package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.d.ts.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.js +2 -1
- package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.js.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/index.d.ts +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/index.d.ts.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/index.js.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/indexer/types.d.ts +3 -0
- package/node_modules/@minicode/agent-sdk/dist/src/indexer/types.d.ts.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/tools/registry.d.ts +3 -0
- package/node_modules/@minicode/agent-sdk/dist/src/tools/registry.d.ts.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/tools/registry.js +4 -1
- package/node_modules/@minicode/agent-sdk/dist/src/tools/registry.js.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/tools/run-command.d.ts +11 -1
- package/node_modules/@minicode/agent-sdk/dist/src/tools/run-command.d.ts.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/tools/run-command.js +4 -1
- package/node_modules/@minicode/agent-sdk/dist/src/tools/run-command.js.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/tools/search.d.ts.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/tools/search.js +16 -8
- package/node_modules/@minicode/agent-sdk/dist/src/tools/search.js.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/tests/file-tools.test.js +19 -2
- package/node_modules/@minicode/agent-sdk/dist/tests/file-tools.test.js.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -10,6 +10,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
10
10
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
11
11
|
import { z } from "zod";
|
|
12
12
|
import { getSymbolDisplayName } from "../indexer/symbol-names.js";
|
|
13
|
+
import { formatAmbiguousSymbolMatches, resolveSymbolInput, } from "../shared/symbol-resolution.js";
|
|
13
14
|
/** Active transports keyed by session ID. */
|
|
14
15
|
const transports = new Map();
|
|
15
16
|
/**
|
|
@@ -44,12 +45,19 @@ function createMcpServer(bridge, emit) {
|
|
|
44
45
|
// ── Tools ──
|
|
45
46
|
server.tool("read_symbol", "Read a specific function, class, interface, or type by name from the AST index. Returns source code, signature, dependencies, references, and annotations in one call — much more targeted than reading an entire file. PREFERRED over read_file for .ts/.tsx/.js/.jsx when you know the symbol name.", { name: z.string().describe("The symbol name or qualified name (e.g. 'Session' or 'Session.trim')") }, async ({ name }) => {
|
|
46
47
|
return wrapToolCall("read_symbol", { name }, async () => {
|
|
47
|
-
const
|
|
48
|
-
if (
|
|
48
|
+
const resolution = resolveSymbolInput(bridge, name);
|
|
49
|
+
if (resolution.status === "missing") {
|
|
49
50
|
return { content: [{ type: "text", text: `Symbol "${name}" not found in the project index.` }], isError: true };
|
|
50
51
|
}
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
if (resolution.status === "ambiguous") {
|
|
53
|
+
return {
|
|
54
|
+
content: [{ type: "text", text: formatAmbiguousSymbolMatches("read_symbol", name, resolution.matches) }],
|
|
55
|
+
isError: true,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const sym = resolution.symbol;
|
|
59
|
+
const deps = bridge.getDependencies(sym.qualifiedName, 1);
|
|
60
|
+
const refs = bridge.getReferences(sym.qualifiedName);
|
|
53
61
|
const lines = [
|
|
54
62
|
`## ${sym.kind}: ${getSymbolDisplayName(sym)}`,
|
|
55
63
|
`File: ${sym.filePath}:${sym.startLine}`,
|
|
@@ -85,14 +93,28 @@ function createMcpServer(bridge, emit) {
|
|
|
85
93
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
86
94
|
});
|
|
87
95
|
});
|
|
88
|
-
server.tool("find_references", "Find all symbols that call, import, or reference a given symbol. Essential for understanding impact before making changes.", { name: z.string().describe("The symbol name to find references for") }, async ({ name }) => {
|
|
96
|
+
server.tool("find_references", "Find all symbols that call, import, or reference a given symbol. Essential for understanding impact before making changes.", { name: z.string().describe("The symbol name or qualified name to find references for") }, async ({ name }) => {
|
|
89
97
|
return wrapToolCall("find_references", { name }, async () => {
|
|
90
|
-
const
|
|
98
|
+
const resolution = resolveSymbolInput(bridge, name);
|
|
99
|
+
if (resolution.status === "missing") {
|
|
100
|
+
return { content: [{ type: "text", text: `Symbol "${name}" not found.` }], isError: true };
|
|
101
|
+
}
|
|
102
|
+
if (resolution.status === "ambiguous") {
|
|
103
|
+
return {
|
|
104
|
+
content: [{ type: "text", text: formatAmbiguousSymbolMatches("find_references", name, resolution.matches) }],
|
|
105
|
+
isError: true,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
const symbol = resolution.symbol;
|
|
109
|
+
const refs = bridge.getReferences(symbol.qualifiedName);
|
|
91
110
|
if (!refs || refs.length === 0) {
|
|
92
111
|
return { content: [{ type: "text", text: `No references found for "${name}".` }] };
|
|
93
112
|
}
|
|
94
|
-
const lines = [
|
|
95
|
-
|
|
113
|
+
const lines = [
|
|
114
|
+
`References to ${getSymbolDisplayName(symbol)}:`,
|
|
115
|
+
...refs.map((r) => ` - ${r.name ?? r.from} (${r.kind})`),
|
|
116
|
+
];
|
|
117
|
+
const annotations = bridge.getAnnotationsForSymbol(symbol.qualifiedName);
|
|
96
118
|
if (annotations.length > 0) {
|
|
97
119
|
lines.push("", `[User annotation: ${annotations.join("; ")}]`);
|
|
98
120
|
}
|
|
@@ -100,19 +122,30 @@ function createMcpServer(bridge, emit) {
|
|
|
100
122
|
});
|
|
101
123
|
});
|
|
102
124
|
server.tool("get_dependencies", "Get the dependency cone of a symbol — everything it calls, imports, extends, or references. Essential for understanding implementation and data flow.", {
|
|
103
|
-
name: z.string().describe("The symbol name to get dependencies for"),
|
|
125
|
+
name: z.string().describe("The symbol name or qualified name to get dependencies for"),
|
|
104
126
|
depth: z.number().optional().default(2).describe("How many levels deep to traverse (default: 2)"),
|
|
105
127
|
}, async ({ name, depth }) => {
|
|
106
128
|
return wrapToolCall("get_dependencies", { name, depth }, async () => {
|
|
107
|
-
const
|
|
129
|
+
const resolution = resolveSymbolInput(bridge, name);
|
|
130
|
+
if (resolution.status === "missing") {
|
|
131
|
+
return { content: [{ type: "text", text: `Symbol "${name}" not found.` }], isError: true };
|
|
132
|
+
}
|
|
133
|
+
if (resolution.status === "ambiguous") {
|
|
134
|
+
return {
|
|
135
|
+
content: [{ type: "text", text: formatAmbiguousSymbolMatches("get_dependencies", name, resolution.matches) }],
|
|
136
|
+
isError: true,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
const symbol = resolution.symbol;
|
|
140
|
+
const deps = bridge.getDependencies(symbol.qualifiedName, depth);
|
|
108
141
|
if (!deps || deps.length === 0) {
|
|
109
142
|
return { content: [{ type: "text", text: `No dependencies found for "${name}".` }] };
|
|
110
143
|
}
|
|
111
144
|
const lines = [
|
|
112
|
-
`Dependencies of
|
|
145
|
+
`Dependencies of ${getSymbolDisplayName(symbol)} (depth=${depth}):`,
|
|
113
146
|
...deps.map((d) => ` - ${d.qualifiedName} (${d.kind}) — ${d.filePath}`),
|
|
114
147
|
];
|
|
115
|
-
const annotations = bridge.getAnnotationsForSymbol(
|
|
148
|
+
const annotations = bridge.getAnnotationsForSymbol(symbol.qualifiedName);
|
|
116
149
|
if (annotations.length > 0) {
|
|
117
150
|
lines.push("", `[User annotation: ${annotations.join("; ")}]`);
|
|
118
151
|
}
|
|
@@ -140,29 +173,41 @@ function createMcpServer(bridge, emit) {
|
|
|
140
173
|
}
|
|
141
174
|
const lines = [
|
|
142
175
|
`Found ${matches.length} symbol(s) matching "${query}":`,
|
|
143
|
-
...matches.map((s) => ` - ${s.name} (${s.kind}) — ${s.filePath}:${s.startLine}${s.signature ? `\n ${s.signature}` : ""}`),
|
|
176
|
+
...matches.map((s) => ` - ${s.name} (${s.kind}) — ${s.filePath}:${s.startLine} — qualified: ${s.qualifiedName}${s.signature ? `\n ${s.signature}` : ""}`),
|
|
144
177
|
];
|
|
145
178
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
146
179
|
});
|
|
147
180
|
});
|
|
148
181
|
server.tool("find_path", "Find the shortest dependency path between two symbols, or trace a symbol back to an entry point. Useful for understanding how code connects.", {
|
|
149
|
-
from: z.string().describe("Source symbol name"),
|
|
150
|
-
to: z.string().optional().describe("Target symbol name. If omitted, traces back to the nearest entry point."),
|
|
182
|
+
from: z.string().describe("Source symbol name or qualified name"),
|
|
183
|
+
to: z.string().optional().describe("Target symbol name or qualified name. If omitted, traces back to the nearest entry point."),
|
|
151
184
|
}, async ({ from, to }) => {
|
|
152
185
|
return wrapToolCall("find_path", { from, to }, async () => {
|
|
153
186
|
if (!bridge.hasIndex()) {
|
|
154
187
|
return { content: [{ type: "text", text: "No project index available." }], isError: true };
|
|
155
188
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
if (!fromSym) {
|
|
189
|
+
const fromResolution = resolveSymbolInput(bridge, from);
|
|
190
|
+
if (fromResolution.status === "missing") {
|
|
159
191
|
return { content: [{ type: "text", text: `Symbol "${from}" not found.` }], isError: true };
|
|
160
192
|
}
|
|
193
|
+
if (fromResolution.status === "ambiguous") {
|
|
194
|
+
return {
|
|
195
|
+
content: [{ type: "text", text: formatAmbiguousSymbolMatches("find_path", from, fromResolution.matches) }],
|
|
196
|
+
isError: true,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
const fromSym = fromResolution.symbol;
|
|
161
200
|
if (to) {
|
|
162
|
-
const
|
|
163
|
-
if (
|
|
201
|
+
const toResolution = resolveSymbolInput(bridge, to);
|
|
202
|
+
if (toResolution.status === "missing") {
|
|
164
203
|
return { content: [{ type: "text", text: `Symbol "${to}" not found.` }], isError: true };
|
|
165
204
|
}
|
|
205
|
+
if (toResolution.status === "ambiguous") {
|
|
206
|
+
return {
|
|
207
|
+
content: [{ type: "text", text: formatAmbiguousSymbolMatches("find_path", to, toResolution.matches) }],
|
|
208
|
+
isError: true,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
166
211
|
}
|
|
167
212
|
// Delegate to the find_path tool via the tool registry
|
|
168
213
|
// We need the actual ProjectIndex for this, so access it through the bridge's getGraph
|
|
@@ -189,7 +234,11 @@ function createMcpServer(bridge, emit) {
|
|
|
189
234
|
}
|
|
190
235
|
const startId = fromSym.qualifiedName;
|
|
191
236
|
if (to) {
|
|
192
|
-
const
|
|
237
|
+
const toResolution = resolveSymbolInput(bridge, to);
|
|
238
|
+
if (toResolution.status !== "resolved") {
|
|
239
|
+
return { content: [{ type: "text", text: `Symbol "${to}" could not be resolved.` }], isError: true };
|
|
240
|
+
}
|
|
241
|
+
const toSym = toResolution.symbol;
|
|
193
242
|
const endId = toSym.qualifiedName;
|
|
194
243
|
// BFS shortest path
|
|
195
244
|
const visited = new Set([startId]);
|
package/dist/src/serve/server.js
CHANGED
|
@@ -9,6 +9,7 @@ import { handleChatCompletions, handleModels } from "./openai-compat.js";
|
|
|
9
9
|
import { formatConfigForDisplay, getConfigMissing } from "../agent/config.js";
|
|
10
10
|
import { applyPersistedConfigUpdates, buildStructuredConfigPayload } from "../agent/editable-config.js";
|
|
11
11
|
import { sortModelsAlphabetically } from "../model-utils.js";
|
|
12
|
+
import { serializeSymbolMatch } from "../shared/symbol-resolution.js";
|
|
12
13
|
import { handleMcpRequest } from "./mcp-server.js";
|
|
13
14
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14
15
|
// Resolve web dir: always serve from dist/src/web (built by scripts/build-web.mjs)
|
|
@@ -195,7 +196,19 @@ export function createRequestHandler(bridge, emit, options = {}) {
|
|
|
195
196
|
const name = decodeURIComponent(pathname.slice("/api/symbols/".length, -"/dependencies".length));
|
|
196
197
|
const depthParam = url.searchParams.get("depth");
|
|
197
198
|
const depth = depthParam ? Number(depthParam) : undefined;
|
|
198
|
-
const
|
|
199
|
+
const matches = bridge.getSymbolMatches(name);
|
|
200
|
+
if (matches.length === 0) {
|
|
201
|
+
sendJson(res, 404, { error: `Symbol "${name}" not found` });
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (matches.length > 1) {
|
|
205
|
+
sendJson(res, 409, {
|
|
206
|
+
error: `Symbol "${name}" is ambiguous`,
|
|
207
|
+
candidates: matches.map(serializeSymbolMatch),
|
|
208
|
+
});
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const result = bridge.getDependencies(matches[0].qualifiedName, depth);
|
|
199
212
|
if (!result) {
|
|
200
213
|
sendJson(res, 404, { error: `Symbol "${name}" not found` });
|
|
201
214
|
return;
|
|
@@ -205,7 +218,19 @@ export function createRequestHandler(bridge, emit, options = {}) {
|
|
|
205
218
|
}
|
|
206
219
|
if (pathname.startsWith("/api/symbols/") && pathname.endsWith("/references") && method === "GET") {
|
|
207
220
|
const name = decodeURIComponent(pathname.slice("/api/symbols/".length, -"/references".length));
|
|
208
|
-
const
|
|
221
|
+
const matches = bridge.getSymbolMatches(name);
|
|
222
|
+
if (matches.length === 0) {
|
|
223
|
+
sendJson(res, 404, { error: `Symbol "${name}" not found` });
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (matches.length > 1) {
|
|
227
|
+
sendJson(res, 409, {
|
|
228
|
+
error: `Symbol "${name}" is ambiguous`,
|
|
229
|
+
candidates: matches.map(serializeSymbolMatch),
|
|
230
|
+
});
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
const result = bridge.getReferences(matches[0].qualifiedName);
|
|
209
234
|
if (!result) {
|
|
210
235
|
sendJson(res, 404, { error: `Symbol "${name}" not found` });
|
|
211
236
|
return;
|
|
@@ -215,11 +240,19 @@ export function createRequestHandler(bridge, emit, options = {}) {
|
|
|
215
240
|
}
|
|
216
241
|
if (pathname.startsWith("/api/symbols/") && pathname.endsWith("/source") && method === "GET") {
|
|
217
242
|
const name = decodeURIComponent(pathname.slice("/api/symbols/".length, -"/source".length));
|
|
218
|
-
const
|
|
219
|
-
if (
|
|
243
|
+
const matches = bridge.getSymbolMatches(name);
|
|
244
|
+
if (matches.length === 0) {
|
|
220
245
|
sendJson(res, 404, { error: `Symbol "${name}" not found` });
|
|
221
246
|
return;
|
|
222
247
|
}
|
|
248
|
+
if (matches.length > 1) {
|
|
249
|
+
sendJson(res, 409, {
|
|
250
|
+
error: `Symbol "${name}" is ambiguous`,
|
|
251
|
+
candidates: matches.map(serializeSymbolMatch),
|
|
252
|
+
});
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
const sym = matches[0];
|
|
223
256
|
try {
|
|
224
257
|
const fileContent = await readFile(path.resolve(config.workspaceRoot, sym.filePath), "utf8");
|
|
225
258
|
const lines = fileContent.split(/\r?\n/);
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
function dedupe(values) {
|
|
2
|
+
return [...new Set(values.filter((value) => value.length > 0))];
|
|
3
|
+
}
|
|
4
|
+
function stripCollisionSuffix(value) {
|
|
5
|
+
const hashIndex = value.indexOf("#");
|
|
6
|
+
return hashIndex >= 0 ? value.slice(0, hashIndex) : value;
|
|
7
|
+
}
|
|
8
|
+
function stripDisplayKindSuffix(value) {
|
|
9
|
+
return value.replace(/\s+\([^()]+\)$/, "");
|
|
10
|
+
}
|
|
11
|
+
export function getGraphNodeId(node, fallbackId = "") {
|
|
12
|
+
return node.qualifiedName || node.id || fallbackId || node.name || "";
|
|
13
|
+
}
|
|
14
|
+
export function getGraphNodeLabel(node, fallbackId = "") {
|
|
15
|
+
const label = node.name?.trim();
|
|
16
|
+
if (label && label.length > 0) {
|
|
17
|
+
return label;
|
|
18
|
+
}
|
|
19
|
+
const id = getGraphNodeId(node, fallbackId);
|
|
20
|
+
return id.split(".").pop() || id;
|
|
21
|
+
}
|
|
22
|
+
export function getGraphNodeAliases(node, fallbackId = "") {
|
|
23
|
+
const id = getGraphNodeId(node, fallbackId);
|
|
24
|
+
const label = getGraphNodeLabel(node, fallbackId);
|
|
25
|
+
const shortId = id.split(".").pop() || id;
|
|
26
|
+
return dedupe([
|
|
27
|
+
id,
|
|
28
|
+
node.id ?? "",
|
|
29
|
+
node.qualifiedName ?? "",
|
|
30
|
+
label,
|
|
31
|
+
stripDisplayKindSuffix(label),
|
|
32
|
+
shortId,
|
|
33
|
+
stripCollisionSuffix(shortId),
|
|
34
|
+
stripCollisionSuffix(id),
|
|
35
|
+
]);
|
|
36
|
+
}
|
|
37
|
+
function compareLabels(a, b) {
|
|
38
|
+
return a.localeCompare(b, undefined, { sensitivity: "base" });
|
|
39
|
+
}
|
|
40
|
+
export function compareGraphNodeIds(a, b, nodes) {
|
|
41
|
+
const nodeA = nodes.get(a);
|
|
42
|
+
const nodeB = nodes.get(b);
|
|
43
|
+
const exportedA = nodeA ? Number(!!nodeA.exported) : 0;
|
|
44
|
+
const exportedB = nodeB ? Number(!!nodeB.exported) : 0;
|
|
45
|
+
if (exportedA !== exportedB) {
|
|
46
|
+
return exportedB - exportedA;
|
|
47
|
+
}
|
|
48
|
+
const labelA = getGraphNodeLabel(nodeA ?? {}, a);
|
|
49
|
+
const labelB = getGraphNodeLabel(nodeB ?? {}, b);
|
|
50
|
+
const labelComparison = compareLabels(labelA, labelB);
|
|
51
|
+
if (labelComparison !== 0) {
|
|
52
|
+
return labelComparison;
|
|
53
|
+
}
|
|
54
|
+
return compareLabels(a, b);
|
|
55
|
+
}
|
|
56
|
+
export function matchesGraphNodeQuery(query, node, fallbackId = "") {
|
|
57
|
+
const normalizedQuery = query.trim().toLowerCase();
|
|
58
|
+
if (normalizedQuery.length === 0) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
return getGraphNodeAliases(node, fallbackId).some((alias) => alias.toLowerCase().includes(normalizedQuery));
|
|
62
|
+
}
|
|
63
|
+
export function resolveGraphNodeIds(nodes, symbolName) {
|
|
64
|
+
const query = symbolName.trim();
|
|
65
|
+
if (query.length === 0) {
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
if (nodes.has(query)) {
|
|
69
|
+
return [query];
|
|
70
|
+
}
|
|
71
|
+
const exactMatches = [...nodes.entries()]
|
|
72
|
+
.filter(([id, node]) => getGraphNodeAliases(node, id).includes(query))
|
|
73
|
+
.map(([id]) => id)
|
|
74
|
+
.sort((a, b) => compareGraphNodeIds(a, b, nodes));
|
|
75
|
+
if (exactMatches.length > 0) {
|
|
76
|
+
return exactMatches;
|
|
77
|
+
}
|
|
78
|
+
return [...nodes.entries()]
|
|
79
|
+
.filter(([id, node]) => matchesGraphNodeQuery(query, node, id))
|
|
80
|
+
.map(([id]) => id)
|
|
81
|
+
.sort((a, b) => compareGraphNodeIds(a, b, nodes));
|
|
82
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { getSymbolDisplayName } from "../indexer/symbol-names.js";
|
|
2
|
+
export function resolveSymbolInput(projectIndex, name) {
|
|
3
|
+
const matches = projectIndex.getSymbolMatches(name);
|
|
4
|
+
if (matches.length === 0) {
|
|
5
|
+
return { status: "missing" };
|
|
6
|
+
}
|
|
7
|
+
if (matches.length > 1) {
|
|
8
|
+
return { status: "ambiguous", matches };
|
|
9
|
+
}
|
|
10
|
+
return { status: "resolved", symbol: matches[0] };
|
|
11
|
+
}
|
|
12
|
+
export function formatSymbolMatch(match) {
|
|
13
|
+
return `${getSymbolDisplayName(match)} (${match.kind}) — ${match.filePath}:${match.startLine} — qualified: ${match.qualifiedName}`;
|
|
14
|
+
}
|
|
15
|
+
export function formatAmbiguousSymbolMatches(toolName, name, matches) {
|
|
16
|
+
return [
|
|
17
|
+
`Symbol "${name}" is ambiguous; ${matches.length} matches were found.`,
|
|
18
|
+
`Re-run ${toolName} with one of these qualified or disambiguated names:`,
|
|
19
|
+
"",
|
|
20
|
+
...matches.map((match) => `- ${formatSymbolMatch(match)}`),
|
|
21
|
+
].join("\n");
|
|
22
|
+
}
|
|
23
|
+
export function serializeSymbolMatch(match) {
|
|
24
|
+
return {
|
|
25
|
+
name: getSymbolDisplayName(match),
|
|
26
|
+
qualifiedName: match.qualifiedName,
|
|
27
|
+
kind: match.kind,
|
|
28
|
+
filePath: match.filePath,
|
|
29
|
+
startLine: match.startLine,
|
|
30
|
+
endLine: match.endLine,
|
|
31
|
+
signature: match.signature,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { expectNonEmptyString, expectOptionalNumber } from "@minicode/agent-sdk";
|
|
2
2
|
import { getSymbolDisplayName } from "../indexer/symbol-names.js";
|
|
3
|
+
import { formatAmbiguousSymbolMatches, resolveSymbolInput, } from "../shared/symbol-resolution.js";
|
|
3
4
|
export function createFindPathTool(projectIndex) {
|
|
4
5
|
return {
|
|
5
6
|
name: "find_path",
|
|
@@ -27,17 +28,25 @@ export function createFindPathTool(projectIndex) {
|
|
|
27
28
|
const from = expectNonEmptyString(input, "from");
|
|
28
29
|
const to = typeof input.to === "string" && input.to.length > 0 ? input.to : undefined;
|
|
29
30
|
const maxDepth = expectOptionalNumber(input, "max_depth");
|
|
30
|
-
const
|
|
31
|
-
if (
|
|
31
|
+
const fromResolution = resolveSymbolInput(projectIndex, from);
|
|
32
|
+
if (fromResolution.status === "missing") {
|
|
32
33
|
return `Symbol "${from}" not found in the project index.`;
|
|
33
34
|
}
|
|
35
|
+
if (fromResolution.status === "ambiguous") {
|
|
36
|
+
return formatAmbiguousSymbolMatches("find_path", from, fromResolution.matches);
|
|
37
|
+
}
|
|
38
|
+
const fromSymbol = fromResolution.symbol;
|
|
34
39
|
if (to) {
|
|
35
40
|
// Path between two symbols
|
|
36
|
-
const
|
|
37
|
-
if (
|
|
41
|
+
const toResolution = resolveSymbolInput(projectIndex, to);
|
|
42
|
+
if (toResolution.status === "missing") {
|
|
38
43
|
return `Symbol "${to}" not found in the project index.`;
|
|
39
44
|
}
|
|
40
|
-
|
|
45
|
+
if (toResolution.status === "ambiguous") {
|
|
46
|
+
return formatAmbiguousSymbolMatches("find_path", to, toResolution.matches);
|
|
47
|
+
}
|
|
48
|
+
const toSymbol = toResolution.symbol;
|
|
49
|
+
const path = projectIndex.findPath(fromSymbol.qualifiedName, toSymbol.qualifiedName, maxDepth ?? 10);
|
|
41
50
|
if (path.length === 0) {
|
|
42
51
|
return `No path found between "${from}" and "${to}".`;
|
|
43
52
|
}
|
|
@@ -53,7 +62,7 @@ export function createFindPathTool(projectIndex) {
|
|
|
53
62
|
}
|
|
54
63
|
else {
|
|
55
64
|
// Trace to entry point(s)
|
|
56
|
-
const paths = projectIndex.findPathToEntryPoint(
|
|
65
|
+
const paths = projectIndex.findPathToEntryPoint(fromSymbol.qualifiedName, maxDepth ?? 20);
|
|
57
66
|
if (paths.length === 0) {
|
|
58
67
|
return `No entry point paths found for "${from}". It may itself be an entry point.`;
|
|
59
68
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { expectNonEmptyString, expectOptionalNumber } from "@minicode/agent-sdk";
|
|
2
2
|
import { getSymbolDisplayName } from "../indexer/symbol-names.js";
|
|
3
|
+
import { formatAmbiguousSymbolMatches, resolveSymbolInput, } from "../shared/symbol-resolution.js";
|
|
3
4
|
const DEFAULT_LIMIT = 50;
|
|
4
5
|
export function createFindReferencesTool(projectIndex) {
|
|
5
6
|
return {
|
|
@@ -28,10 +29,14 @@ export function createFindReferencesTool(projectIndex) {
|
|
|
28
29
|
const name = expectNonEmptyString(input, "name");
|
|
29
30
|
const skip = Math.max(0, expectOptionalNumber(input, "skip") ?? 0);
|
|
30
31
|
const limit = Math.max(1, Math.min(100, expectOptionalNumber(input, "limit") ?? DEFAULT_LIMIT));
|
|
31
|
-
const
|
|
32
|
-
if (
|
|
32
|
+
const resolution = resolveSymbolInput(projectIndex, name);
|
|
33
|
+
if (resolution.status === "missing") {
|
|
33
34
|
return `Symbol "${name}" not found in the project index.`;
|
|
34
35
|
}
|
|
36
|
+
if (resolution.status === "ambiguous") {
|
|
37
|
+
return formatAmbiguousSymbolMatches("find_references", name, resolution.matches);
|
|
38
|
+
}
|
|
39
|
+
const symbol = resolution.symbol;
|
|
35
40
|
const refs = projectIndex.dependencyEdges.filter((e) => e.to === symbol.qualifiedName || e.to === symbol.name);
|
|
36
41
|
if (refs.length === 0) {
|
|
37
42
|
return `No references found for "${name}".`;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { expectNonEmptyString, expectOptionalNumber } from "@minicode/agent-sdk";
|
|
2
2
|
import { getSymbolDisplayName } from "../indexer/symbol-names.js";
|
|
3
|
+
import { formatAmbiguousSymbolMatches, resolveSymbolInput, } from "../shared/symbol-resolution.js";
|
|
3
4
|
export function createGetDependenciesTool(projectIndex) {
|
|
4
5
|
return {
|
|
5
6
|
name: "get_dependencies",
|
|
@@ -32,11 +33,15 @@ export function createGetDependenciesTool(projectIndex) {
|
|
|
32
33
|
const depth = expectOptionalNumber(input, "depth") ?? 1;
|
|
33
34
|
const skip = Math.max(0, expectOptionalNumber(input, "skip") ?? 0);
|
|
34
35
|
const limit = Math.max(1, Math.min(100, expectOptionalNumber(input, "limit") ?? 50));
|
|
35
|
-
const
|
|
36
|
-
if (
|
|
36
|
+
const resolution = resolveSymbolInput(projectIndex, name);
|
|
37
|
+
if (resolution.status === "missing") {
|
|
37
38
|
return `Symbol "${name}" not found in the project index.`;
|
|
38
39
|
}
|
|
39
|
-
|
|
40
|
+
if (resolution.status === "ambiguous") {
|
|
41
|
+
return formatAmbiguousSymbolMatches("get_dependencies", name, resolution.matches);
|
|
42
|
+
}
|
|
43
|
+
const symbol = resolution.symbol;
|
|
44
|
+
const cone = projectIndex.getDependencyCone(symbol.qualifiedName, depth);
|
|
40
45
|
const shown = cone.slice(skip, skip + limit);
|
|
41
46
|
const lines = shown.map((s) => {
|
|
42
47
|
const header = `${s.kind} ${getSymbolDisplayName(s)}`;
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { readFile, stat } from "node:fs/promises";
|
|
2
2
|
import { resolveWorkspacePath, validateFileReadSize, expectNonEmptyString, expectOptionalBoolean, } from "@minicode/agent-sdk";
|
|
3
3
|
import { getSymbolDisplayName } from "../indexer/symbol-names.js";
|
|
4
|
+
import { formatAmbiguousSymbolMatches, resolveSymbolInput, } from "../shared/symbol-resolution.js";
|
|
4
5
|
const LEADING_CONTEXT_LINES = 3;
|
|
5
6
|
export function createReadSymbolTool(config, projectIndex) {
|
|
6
7
|
return {
|
|
7
8
|
name: "read_symbol",
|
|
8
9
|
description: "Read a specific function, class, or type definition by name. " +
|
|
9
10
|
"Returns the symbol's source code, referenced types, callers, and callees. " +
|
|
11
|
+
"If a bare name matches multiple symbols, returns disambiguation candidates. " +
|
|
10
12
|
"PREFER this over read_file for .ts/.tsx/.js/.jsx — use the code map to find symbol names.",
|
|
11
13
|
inputSchema: {
|
|
12
14
|
type: "object",
|
|
@@ -26,10 +28,14 @@ export function createReadSymbolTool(config, projectIndex) {
|
|
|
26
28
|
execute: async (input) => {
|
|
27
29
|
const name = expectNonEmptyString(input, "name");
|
|
28
30
|
const includeBody = expectOptionalBoolean(input, "includeBody") ?? true;
|
|
29
|
-
const
|
|
30
|
-
if (
|
|
31
|
+
const resolution = resolveSymbolInput(projectIndex, name);
|
|
32
|
+
if (resolution.status === "missing") {
|
|
31
33
|
return `Symbol "${name}" not found in the project index. Try using search to find it, or use read_file to read the full file.`;
|
|
32
34
|
}
|
|
35
|
+
if (resolution.status === "ambiguous") {
|
|
36
|
+
return formatAmbiguousSymbolMatches("read_symbol", name, resolution.matches);
|
|
37
|
+
}
|
|
38
|
+
const symbol = resolution.symbol;
|
|
33
39
|
const filePath = resolveWorkspacePath(symbol.filePath, config.workspaceRoot);
|
|
34
40
|
const fileStat = await stat(filePath);
|
|
35
41
|
if (!fileStat.isFile()) {
|
|
@@ -75,7 +81,7 @@ export function createReadSymbolTool(config, projectIndex) {
|
|
|
75
81
|
if (calls.length > 0) {
|
|
76
82
|
parts.push("", "## Calls", "", calls.map((s) => `- ${s}`).join("\n"));
|
|
77
83
|
}
|
|
78
|
-
const cone = projectIndex.getDependencyCone(
|
|
84
|
+
const cone = projectIndex.getDependencyCone(symbol.qualifiedName, 1);
|
|
79
85
|
const typeRefs = cone.filter((s) => s.qualifiedName !== symbol.qualifiedName &&
|
|
80
86
|
(s.kind === "interface" || s.kind === "type"));
|
|
81
87
|
if (typeRefs.length > 0) {
|
|
@@ -14,6 +14,9 @@ export function createToolRegistry(config, projectIndex) {
|
|
|
14
14
|
? {
|
|
15
15
|
afterWrite: (relPath, content) => projectIndex.reindexFile(relPath, content),
|
|
16
16
|
afterEdit: (relPath, content) => projectIndex.reindexFile(relPath, content),
|
|
17
|
+
afterCommand: async () => {
|
|
18
|
+
await projectIndex.refreshFromWorkspace();
|
|
19
|
+
},
|
|
17
20
|
}
|
|
18
21
|
: undefined;
|
|
19
22
|
const tools = [
|
|
@@ -22,7 +25,7 @@ export function createToolRegistry(config, projectIndex) {
|
|
|
22
25
|
createEditFileTool(config, hooks ? { afterEdit: hooks.afterEdit } : undefined),
|
|
23
26
|
createSearchTool(config),
|
|
24
27
|
createListFilesTool(config),
|
|
25
|
-
createRunCommandTool(config),
|
|
28
|
+
createRunCommandTool(config, hooks ? { afterCommand: hooks.afterCommand } : undefined),
|
|
26
29
|
];
|
|
27
30
|
if (projectIndex) {
|
|
28
31
|
tools.splice(1, 0, createReadSymbolTool(config, projectIndex));
|
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
import { expectNonEmptyString, expectOptionalNumber } from "@minicode/agent-sdk";
|
|
2
2
|
import { getSymbolDisplayName, getSymbolLookupNames } from "../indexer/symbol-names.js";
|
|
3
3
|
const DEFAULT_LIMIT = 30;
|
|
4
|
+
function compareMatchedSymbols(pattern, a, b) {
|
|
5
|
+
const lowerPattern = pattern.toLowerCase();
|
|
6
|
+
const aDisplay = getSymbolDisplayName(a).toLowerCase();
|
|
7
|
+
const bDisplay = getSymbolDisplayName(b).toLowerCase();
|
|
8
|
+
const aExact = Number(aDisplay === lowerPattern || a.qualifiedName.toLowerCase() === lowerPattern || a.name.toLowerCase() === lowerPattern);
|
|
9
|
+
const bExact = Number(bDisplay === lowerPattern || b.qualifiedName.toLowerCase() === lowerPattern || b.name.toLowerCase() === lowerPattern);
|
|
10
|
+
if (aExact !== bExact) {
|
|
11
|
+
return bExact - aExact;
|
|
12
|
+
}
|
|
13
|
+
return Number(b.exported) - Number(a.exported) ||
|
|
14
|
+
aDisplay.localeCompare(bDisplay) ||
|
|
15
|
+
a.filePath.localeCompare(b.filePath) ||
|
|
16
|
+
a.startLine - b.startLine ||
|
|
17
|
+
a.qualifiedName.localeCompare(b.qualifiedName);
|
|
18
|
+
}
|
|
4
19
|
function matchesPattern(text, pattern) {
|
|
5
20
|
const lowerText = text.toLowerCase();
|
|
6
21
|
const lowerPattern = pattern.toLowerCase();
|
|
@@ -11,7 +26,7 @@ export function createSearchCodeMapTool(projectIndex) {
|
|
|
11
26
|
name: "search_code_map",
|
|
12
27
|
description: "Search the full project index for symbols by name or substring. " +
|
|
13
28
|
"Use when the code map is truncated and you need to find a symbol not listed. " +
|
|
14
|
-
"Returns qualified names and file paths; use read_symbol with the result.",
|
|
29
|
+
"Returns disambiguated display names, qualified names, and file paths; use read_symbol with the result.",
|
|
15
30
|
inputSchema: {
|
|
16
31
|
type: "object",
|
|
17
32
|
properties: {
|
|
@@ -53,9 +68,9 @@ export function createSearchCodeMapTool(projectIndex) {
|
|
|
53
68
|
return false;
|
|
54
69
|
}
|
|
55
70
|
return true;
|
|
56
|
-
});
|
|
71
|
+
}).sort((a, b) => compareMatchedSymbols(pattern, a, b));
|
|
57
72
|
const shown = matches.slice(skip, skip + limit);
|
|
58
|
-
const lines = shown.map((s) => `- ${getSymbolDisplayName(s)} (${s.kind}) — ${s.filePath}`);
|
|
73
|
+
const lines = shown.map((s) => `- ${getSymbolDisplayName(s)} (${s.kind}) — ${s.filePath}:${s.startLine} — qualified: ${s.qualifiedName}`);
|
|
59
74
|
const remaining = matches.length - skip - shown.length;
|
|
60
75
|
const footer = remaining > 0
|
|
61
76
|
? `\n... and ${remaining} more (use skip: ${skip + limit}, limit: ${limit} for next page)`
|