@sean.holung/minicode 0.3.4 → 0.3.6

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 (72) hide show
  1. package/README.md +25 -47
  2. package/dist/scripts/run-benchmarks.js +73 -28
  3. package/dist/src/agent/config.js +51 -66
  4. package/dist/src/agent/editable-config.js +50 -58
  5. package/dist/src/agent/home-env.js +74 -0
  6. package/dist/src/benchmark/runner.js +142 -59
  7. package/dist/src/cli/config-slash-command.js +15 -13
  8. package/dist/src/indexer/project-index.js +49 -13
  9. package/dist/src/serve/agent-bridge.js +99 -31
  10. package/dist/src/serve/mcp-server.js +70 -21
  11. package/dist/src/serve/server.js +198 -8
  12. package/dist/src/session/session-preview.js +14 -0
  13. package/dist/src/shared/graph-search.js +80 -0
  14. package/dist/src/shared/graph-selection.js +40 -0
  15. package/dist/src/shared/graph-symbols.js +82 -0
  16. package/dist/src/shared/symbol-resolution.js +33 -0
  17. package/dist/src/tools/find-path.js +15 -6
  18. package/dist/src/tools/find-references.js +7 -2
  19. package/dist/src/tools/get-dependencies.js +8 -3
  20. package/dist/src/tools/read-symbol.js +9 -3
  21. package/dist/src/tools/registry.js +4 -1
  22. package/dist/src/tools/search-code-map.js +18 -3
  23. package/dist/src/web/app.js +646 -87
  24. package/dist/src/web/index.html +68 -6
  25. package/dist/src/web/style.css +208 -1
  26. package/dist/tests/benchmark-harness.test.js +100 -0
  27. package/dist/tests/config-api.test.js +5 -5
  28. package/dist/tests/config-integration.test.js +130 -56
  29. package/dist/tests/config-slash-command.test.js +12 -11
  30. package/dist/tests/config.test.js +12 -4
  31. package/dist/tests/editable-config.test.js +15 -12
  32. package/dist/tests/file-tools.test.js +34 -1
  33. package/dist/tests/find-path.test.js +43 -2
  34. package/dist/tests/find-references.test.js +49 -0
  35. package/dist/tests/get-dependencies.test.js +23 -0
  36. package/dist/tests/graph-onboarding.test.js +10 -1
  37. package/dist/tests/graph-search.test.js +66 -0
  38. package/dist/tests/graph-selection.test.js +58 -0
  39. package/dist/tests/graph-symbols.test.js +45 -0
  40. package/dist/tests/home-env.test.js +56 -0
  41. package/dist/tests/indexer.test.js +6 -0
  42. package/dist/tests/read-symbol.test.js +35 -0
  43. package/dist/tests/request-tracker.test.js +15 -0
  44. package/dist/tests/run-benchmarks.test.js +117 -33
  45. package/dist/tests/search-code-map.test.js +2 -0
  46. package/dist/tests/serve.integration.test.js +338 -9
  47. package/dist/tests/session-preview.test.js +56 -0
  48. package/dist/tests/session-ui.test.js +4 -0
  49. package/dist/tests/settings-ui.test.js +18 -0
  50. package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.d.ts.map +1 -1
  51. package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.js +2 -1
  52. package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.js.map +1 -1
  53. package/node_modules/@minicode/agent-sdk/dist/src/index.d.ts +1 -1
  54. package/node_modules/@minicode/agent-sdk/dist/src/index.d.ts.map +1 -1
  55. package/node_modules/@minicode/agent-sdk/dist/src/index.js.map +1 -1
  56. package/node_modules/@minicode/agent-sdk/dist/src/indexer/types.d.ts +3 -0
  57. package/node_modules/@minicode/agent-sdk/dist/src/indexer/types.d.ts.map +1 -1
  58. package/node_modules/@minicode/agent-sdk/dist/src/tools/registry.d.ts +3 -0
  59. package/node_modules/@minicode/agent-sdk/dist/src/tools/registry.d.ts.map +1 -1
  60. package/node_modules/@minicode/agent-sdk/dist/src/tools/registry.js +4 -1
  61. package/node_modules/@minicode/agent-sdk/dist/src/tools/registry.js.map +1 -1
  62. package/node_modules/@minicode/agent-sdk/dist/src/tools/run-command.d.ts +11 -1
  63. package/node_modules/@minicode/agent-sdk/dist/src/tools/run-command.d.ts.map +1 -1
  64. package/node_modules/@minicode/agent-sdk/dist/src/tools/run-command.js +4 -1
  65. package/node_modules/@minicode/agent-sdk/dist/src/tools/run-command.js.map +1 -1
  66. package/node_modules/@minicode/agent-sdk/dist/src/tools/search.d.ts.map +1 -1
  67. package/node_modules/@minicode/agent-sdk/dist/src/tools/search.js +16 -8
  68. package/node_modules/@minicode/agent-sdk/dist/src/tools/search.js.map +1 -1
  69. package/node_modules/@minicode/agent-sdk/dist/tests/file-tools.test.js +19 -2
  70. package/node_modules/@minicode/agent-sdk/dist/tests/file-tools.test.js.map +1 -1
  71. package/node_modules/@minicode/agent-sdk/dist/tsconfig.tsbuildinfo +1 -1
  72. package/package.json +1 -1
@@ -48,22 +48,26 @@ function buildAdjacencyTo(edges) {
48
48
  }
49
49
  return map;
50
50
  }
51
- function resolveSymbol(name, symbols) {
51
+ function resolveSymbols(name, symbols) {
52
52
  const direct = symbols.get(name);
53
53
  if (direct)
54
- return direct;
54
+ return [direct];
55
55
  const matches = [...symbols.values()].filter((sym) => getSymbolLookupNames(sym).includes(name));
56
56
  if (matches.length === 0) {
57
- return undefined;
57
+ return [];
58
58
  }
59
59
  matches.sort((a, b) => Number(b.exported) - Number(a.exported) ||
60
60
  a.filePath.localeCompare(b.filePath) ||
61
61
  a.startLine - b.startLine ||
62
62
  a.qualifiedName.localeCompare(b.qualifiedName));
63
- return matches[0];
63
+ return matches;
64
+ }
65
+ function resolveSymbol(name, symbols) {
66
+ return resolveSymbols(name, symbols)[0];
64
67
  }
65
68
  export function createProjectIndex(symbols, files, dependencyEdges, plugins, projectFiles, workspaceRoot) {
66
69
  let adjacencyFrom = buildAdjacencyFrom(dependencyEdges);
70
+ const root = path.resolve(workspaceRoot);
67
71
  function rebuildSymbolsMap() {
68
72
  const normalizedSymbols = normalizeIndexedSymbols(files);
69
73
  symbols.clear();
@@ -71,6 +75,42 @@ export function createProjectIndex(symbols, files, dependencyEdges, plugins, pro
71
75
  symbols.set(qualifiedName, symbol);
72
76
  }
73
77
  }
78
+ function rebuildDependencyEdges() {
79
+ for (const p of plugins) {
80
+ if (p.resolveDependencies) {
81
+ const allSymbols = [...symbols.values()];
82
+ const edges = p.resolveDependencies(allSymbols, projectFiles);
83
+ dependencyEdges.splice(0, dependencyEdges.length, ...edges);
84
+ adjacencyFrom = buildAdjacencyFrom(dependencyEdges);
85
+ break;
86
+ }
87
+ }
88
+ }
89
+ async function refreshFromWorkspace() {
90
+ const validExtensions = new Set(plugins.flatMap((p) => p.extensions));
91
+ const sourceFiles = [];
92
+ await collectSourceFiles(root, root, sourceFiles, validExtensions);
93
+ files.clear();
94
+ projectFiles.clear();
95
+ for (const relPath of sourceFiles) {
96
+ const plugin = getPluginForFile(relPath, plugins);
97
+ if (!plugin)
98
+ continue;
99
+ const absPath = path.join(root, relPath);
100
+ let content;
101
+ try {
102
+ content = await readFile(absPath, "utf8");
103
+ }
104
+ catch {
105
+ continue;
106
+ }
107
+ projectFiles.set(relPath, content);
108
+ const extracted = plugin.indexFile(relPath, content);
109
+ files.set(relPath, extracted);
110
+ }
111
+ rebuildSymbolsMap();
112
+ rebuildDependencyEdges();
113
+ }
74
114
  return {
75
115
  symbols,
76
116
  files,
@@ -81,6 +121,9 @@ export function createProjectIndex(symbols, files, dependencyEdges, plugins, pro
81
121
  getSymbol(name) {
82
122
  return resolveSymbol(name, symbols);
83
123
  },
124
+ getSymbolMatches(name) {
125
+ return resolveSymbols(name, symbols);
126
+ },
84
127
  getSymbolsInFile(filePath) {
85
128
  return files.get(filePath) ?? [];
86
129
  },
@@ -226,16 +269,9 @@ export function createProjectIndex(symbols, files, dependencyEdges, plugins, pro
226
269
  const extracted = plugin.indexFile(relPath, content);
227
270
  files.set(relPath, extracted);
228
271
  rebuildSymbolsMap();
229
- for (const p of plugins) {
230
- if (p.resolveDependencies) {
231
- const allSymbols = [...symbols.values()];
232
- const edges = p.resolveDependencies(allSymbols, projectFiles);
233
- dependencyEdges.splice(0, dependencyEdges.length, ...edges);
234
- adjacencyFrom = buildAdjacencyFrom(dependencyEdges);
235
- break;
236
- }
237
- }
272
+ rebuildDependencyEdges();
238
273
  },
274
+ refreshFromWorkspace,
239
275
  };
240
276
  }
241
277
  /**
@@ -13,9 +13,11 @@ import { getSymbolDisplayName } from "../indexer/symbol-names.js";
13
13
  export class AgentBridge {
14
14
  agent;
15
15
  config;
16
+ baseConfig;
16
17
  modelClient;
17
18
  projectIndex;
18
- buildAgent;
19
+ toolRegistry;
20
+ sessionOpenRouterConnected = false;
19
21
  busy = false;
20
22
  abortController = null;
21
23
  broadcast;
@@ -43,9 +45,8 @@ export class AgentBridge {
43
45
  }
44
46
  async init() {
45
47
  const config = await loadAgentConfig();
46
- let modelClient;
47
48
  try {
48
- modelClient = createModelClient(config);
49
+ this.modelClient = createModelClient(config);
49
50
  }
50
51
  catch {
51
52
  // Model client may fail to initialize if API keys are missing.
@@ -75,27 +76,46 @@ export class AgentBridge {
75
76
  return this.appendAnnotationsToResult(name, input, result);
76
77
  };
77
78
  this.config = config;
79
+ this.baseConfig = AgentBridge.cloneConfig(config);
78
80
  this.projectIndex = projectIndex;
79
- if (modelClient) {
80
- this.modelClient = modelClient;
81
- this.buildAgent = (session, onUiUpdate) => {
82
- return new CodingAgent({
83
- config,
84
- modelClient,
85
- toolRegistry,
86
- verbose: this.verbose,
87
- ...(session ? { session } : {}),
88
- ...(projectIndex !== undefined
89
- ? { getCodeMap: (focusSymbols) => projectIndex.getCodeMap(undefined, focusSymbols) }
90
- : {}),
91
- onUiUpdate: onUiUpdate ?? ((event) => {
92
- this.emit(event);
93
- }),
94
- getSystemPromptSuffix: () => this.buildAnnotationSuffix(),
95
- });
96
- };
97
- this.agent = this.buildAgent();
81
+ this.toolRegistry = toolRegistry;
82
+ if (this.modelClient) {
83
+ this.agent = this.createAgent();
84
+ }
85
+ }
86
+ static cloneConfig(config) {
87
+ return {
88
+ ...config,
89
+ commandDenylist: [...config.commandDenylist],
90
+ };
91
+ }
92
+ static applyConfig(target, source) {
93
+ const targetRecord = target;
94
+ for (const key of Object.keys(targetRecord)) {
95
+ if (!(key in source)) {
96
+ delete targetRecord[key];
97
+ }
98
98
  }
99
+ Object.assign(targetRecord, AgentBridge.cloneConfig(source));
100
+ }
101
+ createAgent(session, onUiUpdate) {
102
+ if (!this.modelClient || !this.toolRegistry) {
103
+ throw new Error("Agent runtime is not initialized.");
104
+ }
105
+ return new CodingAgent({
106
+ config: this.config,
107
+ modelClient: this.modelClient,
108
+ toolRegistry: this.toolRegistry,
109
+ verbose: this.verbose,
110
+ ...(session ? { session } : {}),
111
+ ...(this.projectIndex !== undefined
112
+ ? { getCodeMap: (focusSymbols) => this.projectIndex.getCodeMap(undefined, focusSymbols) }
113
+ : {}),
114
+ onUiUpdate: onUiUpdate ?? ((event) => {
115
+ this.emit(event);
116
+ }),
117
+ getSystemPromptSuffix: () => this.buildAnnotationSuffix(),
118
+ });
99
119
  }
100
120
  // ── File watcher for automatic reindexing ──
101
121
  static WATCH_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx"]);
@@ -171,6 +191,45 @@ export class AgentBridge {
171
191
  getConfig() {
172
192
  return this.config;
173
193
  }
194
+ isOpenRouterSessionConnected() {
195
+ return this.sessionOpenRouterConnected;
196
+ }
197
+ connectOpenRouter(apiKey) {
198
+ const trimmedKey = apiKey.trim();
199
+ if (!trimmedKey) {
200
+ throw new Error("OpenRouter OAuth exchange did not return an API key.");
201
+ }
202
+ if (this.busy) {
203
+ throw new Error("busy");
204
+ }
205
+ const currentSession = this.agent?.getSession();
206
+ this.config.modelProvider = "openai-compatible";
207
+ this.config.openAiBaseUrl = "https://openrouter.ai/api/v1";
208
+ this.config.openAiApiKey = trimmedKey;
209
+ this.sessionOpenRouterConnected = true;
210
+ this.modelClient = createModelClient(this.config);
211
+ this.agent = this.createAgent(currentSession);
212
+ }
213
+ disconnectOpenRouter() {
214
+ if (this.busy) {
215
+ throw new Error("busy");
216
+ }
217
+ if (!this.sessionOpenRouterConnected) {
218
+ return false;
219
+ }
220
+ const currentSession = this.agent?.getSession();
221
+ AgentBridge.applyConfig(this.config, this.baseConfig);
222
+ this.sessionOpenRouterConnected = false;
223
+ try {
224
+ this.modelClient = createModelClient(this.config);
225
+ this.agent = this.createAgent(currentSession);
226
+ }
227
+ catch {
228
+ this.modelClient = undefined;
229
+ this.agent = undefined;
230
+ }
231
+ return true;
232
+ }
174
233
  requireAgent() {
175
234
  if (!this.agent) {
176
235
  throw new Error("Agent is not configured. Set MODEL and provider settings in ~/.minicode/.env");
@@ -230,13 +289,13 @@ export class AgentBridge {
230
289
  return saveSession(agent.getSession(), label, annotationsObj);
231
290
  }
232
291
  async loadSess(label) {
233
- if (!this.buildAgent) {
292
+ if (!this.modelClient) {
234
293
  throw new Error("Agent is not configured.");
235
294
  }
236
295
  const result = (await loadSessionByLabel(label)) ?? (await loadSession(label));
237
296
  if (!result)
238
297
  return null;
239
- this.agent = this.buildAgent(result.session);
298
+ this.agent = this.createAgent(result.session);
240
299
  // Restore annotations from saved session
241
300
  this.annotations.clear();
242
301
  if (result.annotations) {
@@ -276,10 +335,18 @@ export class AgentBridge {
276
335
  return undefined;
277
336
  return this.projectIndex.getSymbol(name);
278
337
  }
338
+ getSymbolMatches(name) {
339
+ if (!this.projectIndex)
340
+ return [];
341
+ return this.projectIndex.getSymbolMatches(name);
342
+ }
279
343
  getDependencies(symbolName, depth) {
280
344
  if (!this.projectIndex)
281
345
  return undefined;
282
- const cone = this.projectIndex.getDependencyCone(symbolName, depth);
346
+ const matches = this.projectIndex.getSymbolMatches(symbolName);
347
+ if (matches.length !== 1)
348
+ return undefined;
349
+ const cone = this.projectIndex.getDependencyCone(matches[0].qualifiedName, depth);
283
350
  if (cone.length === 0)
284
351
  return undefined;
285
352
  return cone.map((sym) => ({
@@ -293,9 +360,10 @@ export class AgentBridge {
293
360
  getReferences(symbolName) {
294
361
  if (!this.projectIndex)
295
362
  return undefined;
296
- const sym = this.projectIndex.getSymbol(symbolName);
297
- if (!sym)
363
+ const matches = this.projectIndex.getSymbolMatches(symbolName);
364
+ if (matches.length !== 1)
298
365
  return undefined;
366
+ const sym = matches[0];
299
367
  // Find all edges pointing TO this symbol
300
368
  const refs = this.projectIndex.dependencyEdges
301
369
  .filter((e) => e.to === sym.qualifiedName || e.to === sym.name)
@@ -464,12 +532,12 @@ export class AgentBridge {
464
532
  async explainSymbol(name, onEvent, signal) {
465
533
  if (!this.projectIndex)
466
534
  throw new Error("No project index");
467
- if (!this.buildAgent)
535
+ if (!this.modelClient)
468
536
  throw new Error("Agent is not configured.");
469
537
  const sym = this.projectIndex.getSymbol(name);
470
538
  if (!sym)
471
539
  throw new Error(`Symbol "${name}" not found`);
472
- const explainAgent = this.buildAgent(undefined, onEvent);
540
+ const explainAgent = this.createAgent(undefined, onEvent);
473
541
  const prompt = `Explain "${sym.name}" (${sym.kind} in ${sym.filePath}).
474
542
  Use read_symbol, get_dependencies, find_references to gather context.
475
543
  Explain what it does, how it works, what depends on it, and key design decisions.
@@ -482,12 +550,12 @@ Be concise but thorough.`;
482
550
  const report = this.getStructuralAnalysis();
483
551
  if (!report)
484
552
  throw new Error("No project index");
485
- if (!this.buildAgent)
553
+ if (!this.modelClient)
486
554
  throw new Error("Agent is not configured.");
487
555
  const finding = report.findings.find((item) => item.id === findingId);
488
556
  if (!finding)
489
557
  throw new Error(`Structural finding "${findingId}" not found`);
490
- const explainAgent = this.buildAgent(undefined, onEvent);
558
+ const explainAgent = this.createAgent(undefined, onEvent);
491
559
  const affectedSymbols = finding.symbols.length > 0
492
560
  ? finding.symbols.slice(0, 8).join(", ")
493
561
  : "(none)";
@@ -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]);