@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.
Files changed (52) hide show
  1. package/README.md +5 -3
  2. package/dist/scripts/run-benchmarks.js +73 -28
  3. package/dist/src/benchmark/runner.js +142 -59
  4. package/dist/src/indexer/project-index.js +49 -13
  5. package/dist/src/serve/agent-bridge.js +12 -3
  6. package/dist/src/serve/mcp-server.js +70 -21
  7. package/dist/src/serve/server.js +37 -4
  8. package/dist/src/shared/graph-symbols.js +82 -0
  9. package/dist/src/shared/symbol-resolution.js +33 -0
  10. package/dist/src/tools/find-path.js +15 -6
  11. package/dist/src/tools/find-references.js +7 -2
  12. package/dist/src/tools/get-dependencies.js +8 -3
  13. package/dist/src/tools/read-symbol.js +9 -3
  14. package/dist/src/tools/registry.js +4 -1
  15. package/dist/src/tools/search-code-map.js +18 -3
  16. package/dist/src/web/app.js +154 -33
  17. package/dist/tests/benchmark-harness.test.js +100 -0
  18. package/dist/tests/file-tools.test.js +34 -1
  19. package/dist/tests/find-path.test.js +43 -2
  20. package/dist/tests/find-references.test.js +49 -0
  21. package/dist/tests/get-dependencies.test.js +23 -0
  22. package/dist/tests/graph-symbols.test.js +45 -0
  23. package/dist/tests/indexer.test.js +6 -0
  24. package/dist/tests/read-symbol.test.js +35 -0
  25. package/dist/tests/request-tracker.test.js +15 -0
  26. package/dist/tests/run-benchmarks.test.js +117 -33
  27. package/dist/tests/search-code-map.test.js +2 -0
  28. package/dist/tests/serve.integration.test.js +109 -3
  29. package/dist/tests/session-ui.test.js +2 -0
  30. package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.d.ts.map +1 -1
  31. package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.js +2 -1
  32. package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.js.map +1 -1
  33. package/node_modules/@minicode/agent-sdk/dist/src/index.d.ts +1 -1
  34. package/node_modules/@minicode/agent-sdk/dist/src/index.d.ts.map +1 -1
  35. package/node_modules/@minicode/agent-sdk/dist/src/index.js.map +1 -1
  36. package/node_modules/@minicode/agent-sdk/dist/src/indexer/types.d.ts +3 -0
  37. package/node_modules/@minicode/agent-sdk/dist/src/indexer/types.d.ts.map +1 -1
  38. package/node_modules/@minicode/agent-sdk/dist/src/tools/registry.d.ts +3 -0
  39. package/node_modules/@minicode/agent-sdk/dist/src/tools/registry.d.ts.map +1 -1
  40. package/node_modules/@minicode/agent-sdk/dist/src/tools/registry.js +4 -1
  41. package/node_modules/@minicode/agent-sdk/dist/src/tools/registry.js.map +1 -1
  42. package/node_modules/@minicode/agent-sdk/dist/src/tools/run-command.d.ts +11 -1
  43. package/node_modules/@minicode/agent-sdk/dist/src/tools/run-command.d.ts.map +1 -1
  44. package/node_modules/@minicode/agent-sdk/dist/src/tools/run-command.js +4 -1
  45. package/node_modules/@minicode/agent-sdk/dist/src/tools/run-command.js.map +1 -1
  46. package/node_modules/@minicode/agent-sdk/dist/src/tools/search.d.ts.map +1 -1
  47. package/node_modules/@minicode/agent-sdk/dist/src/tools/search.js +16 -8
  48. package/node_modules/@minicode/agent-sdk/dist/src/tools/search.js.map +1 -1
  49. package/node_modules/@minicode/agent-sdk/dist/tests/file-tools.test.js +19 -2
  50. package/node_modules/@minicode/agent-sdk/dist/tests/file-tools.test.js.map +1 -1
  51. package/node_modules/@minicode/agent-sdk/dist/tsconfig.tsbuildinfo +1 -1
  52. 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 sym = bridge.getSymbol(name);
48
- if (!sym) {
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
- const deps = bridge.getDependencies(name, 1);
52
- const refs = bridge.getReferences(name);
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 refs = bridge.getReferences(name);
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 = [`References to "${name}":`, ...refs.map((r) => ` - ${r.name ?? r.from} (${r.kind})`)];
95
- const annotations = bridge.getAnnotationsForSymbol(name);
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 deps = bridge.getDependencies(name, depth);
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 "${name}" (depth=${depth}):`,
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(name);
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
- // Use the bridge's project index via getSymbol to verify symbols exist
157
- const fromSym = bridge.getSymbol(from);
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 toSym = bridge.getSymbol(to);
163
- if (!toSym) {
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 toSym = bridge.getSymbol(to);
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]);
@@ -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 result = bridge.getDependencies(name, depth);
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 result = bridge.getReferences(name);
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 sym = bridge.getSymbol(name);
219
- if (!sym) {
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 fromSymbol = projectIndex.getSymbol(from);
31
- if (!fromSymbol) {
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 toSymbol = projectIndex.getSymbol(to);
37
- if (!toSymbol) {
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
- const path = projectIndex.findPath(from, to, maxDepth ?? 10);
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(from, maxDepth ?? 20);
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 symbol = projectIndex.getSymbol(name);
32
- if (!symbol) {
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 symbol = projectIndex.getSymbol(name);
36
- if (!symbol) {
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
- const cone = projectIndex.getDependencyCone(name, depth);
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 symbol = projectIndex.getSymbol(name);
30
- if (!symbol) {
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(name, 1);
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)`