@colbymchenry/codegraph 0.3.1 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +681 -641
  3. package/dist/bin/codegraph.d.ts +7 -2
  4. package/dist/bin/codegraph.d.ts.map +1 -1
  5. package/dist/bin/codegraph.js +360 -140
  6. package/dist/bin/codegraph.js.map +1 -1
  7. package/dist/config.d.ts.map +1 -1
  8. package/dist/config.js +43 -10
  9. package/dist/config.js.map +1 -1
  10. package/dist/context/index.d.ts +17 -4
  11. package/dist/context/index.d.ts.map +1 -1
  12. package/dist/context/index.js +182 -15
  13. package/dist/context/index.js.map +1 -1
  14. package/dist/db/index.d.ts.map +1 -1
  15. package/dist/db/index.js +21 -0
  16. package/dist/db/index.js.map +1 -1
  17. package/dist/db/migrations.d.ts +1 -1
  18. package/dist/db/migrations.d.ts.map +1 -1
  19. package/dist/db/migrations.js +19 -12
  20. package/dist/db/migrations.js.map +1 -1
  21. package/dist/db/queries.d.ts +32 -1
  22. package/dist/db/queries.d.ts.map +1 -1
  23. package/dist/db/queries.js +271 -118
  24. package/dist/db/queries.js.map +1 -1
  25. package/dist/db/schema.sql +163 -149
  26. package/dist/directory.d.ts +13 -1
  27. package/dist/directory.d.ts.map +1 -1
  28. package/dist/directory.js +85 -19
  29. package/dist/directory.js.map +1 -1
  30. package/dist/errors.d.ts +1 -1
  31. package/dist/errors.d.ts.map +1 -1
  32. package/dist/errors.js +7 -1
  33. package/dist/errors.js.map +1 -1
  34. package/dist/extraction/grammars.d.ts +9 -4
  35. package/dist/extraction/grammars.d.ts.map +1 -1
  36. package/dist/extraction/grammars.js +133 -65
  37. package/dist/extraction/grammars.js.map +1 -1
  38. package/dist/extraction/index.d.ts +6 -0
  39. package/dist/extraction/index.d.ts.map +1 -1
  40. package/dist/extraction/index.js +209 -44
  41. package/dist/extraction/index.js.map +1 -1
  42. package/dist/extraction/tree-sitter.d.ts +67 -0
  43. package/dist/extraction/tree-sitter.d.ts.map +1 -1
  44. package/dist/extraction/tree-sitter.js +980 -38
  45. package/dist/extraction/tree-sitter.js.map +1 -1
  46. package/dist/graph/traversal.d.ts.map +1 -1
  47. package/dist/graph/traversal.js +6 -2
  48. package/dist/graph/traversal.js.map +1 -1
  49. package/dist/index.d.ts +6 -38
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +91 -73
  52. package/dist/index.js.map +1 -1
  53. package/dist/installer/banner.js +7 -7
  54. package/dist/installer/claude-md-template.js +32 -32
  55. package/dist/installer/config-writer.d.ts +9 -0
  56. package/dist/installer/config-writer.d.ts.map +1 -1
  57. package/dist/installer/config-writer.js +126 -17
  58. package/dist/installer/config-writer.js.map +1 -1
  59. package/dist/installer/index.d.ts.map +1 -1
  60. package/dist/installer/index.js +11 -9
  61. package/dist/installer/index.js.map +1 -1
  62. package/dist/mcp/index.d.ts +14 -3
  63. package/dist/mcp/index.d.ts.map +1 -1
  64. package/dist/mcp/index.js +109 -29
  65. package/dist/mcp/index.js.map +1 -1
  66. package/dist/mcp/tools.d.ts +66 -1
  67. package/dist/mcp/tools.d.ts.map +1 -1
  68. package/dist/mcp/tools.js +442 -49
  69. package/dist/mcp/tools.js.map +1 -1
  70. package/dist/mcp/transport.d.ts.map +1 -1
  71. package/dist/mcp/transport.js +2 -0
  72. package/dist/mcp/transport.js.map +1 -1
  73. package/dist/resolution/frameworks/index.d.ts +1 -0
  74. package/dist/resolution/frameworks/index.d.ts.map +1 -1
  75. package/dist/resolution/frameworks/index.js +5 -1
  76. package/dist/resolution/frameworks/index.js.map +1 -1
  77. package/dist/resolution/frameworks/svelte.d.ts +9 -0
  78. package/dist/resolution/frameworks/svelte.d.ts.map +1 -0
  79. package/dist/resolution/frameworks/svelte.js +268 -0
  80. package/dist/resolution/frameworks/svelte.js.map +1 -0
  81. package/dist/resolution/import-resolver.d.ts.map +1 -1
  82. package/dist/resolution/import-resolver.js +3 -4
  83. package/dist/resolution/import-resolver.js.map +1 -1
  84. package/dist/resolution/index.d.ts +15 -2
  85. package/dist/resolution/index.d.ts.map +1 -1
  86. package/dist/resolution/index.js +153 -13
  87. package/dist/resolution/index.js.map +1 -1
  88. package/dist/resolution/name-matcher.d.ts.map +1 -1
  89. package/dist/resolution/name-matcher.js +7 -20
  90. package/dist/resolution/name-matcher.js.map +1 -1
  91. package/dist/resolution/types.d.ts +4 -0
  92. package/dist/resolution/types.d.ts.map +1 -1
  93. package/dist/search/query-utils.d.ts +25 -0
  94. package/dist/search/query-utils.d.ts.map +1 -0
  95. package/dist/search/query-utils.js +124 -0
  96. package/dist/search/query-utils.js.map +1 -0
  97. package/dist/sentry.d.ts +22 -0
  98. package/dist/sentry.d.ts.map +1 -0
  99. package/dist/sentry.js +159 -0
  100. package/dist/sentry.js.map +1 -0
  101. package/dist/sync/index.d.ts +4 -2
  102. package/dist/sync/index.d.ts.map +1 -1
  103. package/dist/sync/index.js +3 -5
  104. package/dist/sync/index.js.map +1 -1
  105. package/dist/types.d.ts +7 -1
  106. package/dist/types.d.ts.map +1 -1
  107. package/dist/types.js +13 -0
  108. package/dist/types.js.map +1 -1
  109. package/dist/utils.d.ts +89 -2
  110. package/dist/utils.d.ts.map +1 -1
  111. package/dist/utils.js +252 -3
  112. package/dist/utils.js.map +1 -1
  113. package/dist/vectors/embedder.d.ts +1 -1
  114. package/dist/vectors/embedder.d.ts.map +1 -1
  115. package/dist/vectors/embedder.js +2 -2
  116. package/dist/vectors/embedder.js.map +1 -1
  117. package/dist/vectors/search.d.ts.map +1 -1
  118. package/dist/vectors/search.js +33 -32
  119. package/dist/vectors/search.js.map +1 -1
  120. package/package.json +74 -67
  121. package/scripts/patch-tree-sitter-dart.js +112 -0
  122. package/scripts/postinstall.js +71 -68
  123. package/dist/sync/git-hooks.d.ts +0 -66
  124. package/dist/sync/git-hooks.d.ts.map +0 -1
  125. package/dist/sync/git-hooks.js +0 -281
  126. package/dist/sync/git-hooks.js.map +0 -1
package/dist/mcp/tools.js CHANGED
@@ -4,13 +4,75 @@
4
4
  *
5
5
  * Defines the tools exposed by the CodeGraph MCP server.
6
6
  */
7
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
8
+ if (k2 === undefined) k2 = k;
9
+ var desc = Object.getOwnPropertyDescriptor(m, k);
10
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
11
+ desc = { enumerable: true, get: function() { return m[k]; } };
12
+ }
13
+ Object.defineProperty(o, k2, desc);
14
+ }) : (function(o, m, k, k2) {
15
+ if (k2 === undefined) k2 = k;
16
+ o[k2] = m[k];
17
+ }));
18
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
19
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
20
+ }) : function(o, v) {
21
+ o["default"] = v;
22
+ });
23
+ var __importStar = (this && this.__importStar) || (function () {
24
+ var ownKeys = function(o) {
25
+ ownKeys = Object.getOwnPropertyNames || function (o) {
26
+ var ar = [];
27
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
28
+ return ar;
29
+ };
30
+ return ownKeys(o);
31
+ };
32
+ return function (mod) {
33
+ if (mod && mod.__esModule) return mod;
34
+ var result = {};
35
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
36
+ __setModuleDefault(result, mod);
37
+ return result;
38
+ };
39
+ })();
7
40
  Object.defineProperty(exports, "__esModule", { value: true });
8
41
  exports.ToolHandler = exports.tools = void 0;
42
+ const index_1 = __importStar(require("../index"));
43
+ const crypto_1 = require("crypto");
44
+ const fs_1 = require("fs");
45
+ const utils_1 = require("../utils");
46
+ const os_1 = require("os");
47
+ const path_1 = require("path");
48
+ /**
49
+ * Mark a Claude session as having consulted MCP tools.
50
+ * This enables Grep/Glob/Bash commands that would otherwise be blocked.
51
+ */
52
+ function markSessionConsulted(sessionId) {
53
+ try {
54
+ const hash = (0, crypto_1.createHash)('md5').update(sessionId).digest('hex').slice(0, 16);
55
+ const markerPath = (0, path_1.join)((0, os_1.tmpdir)(), `codegraph-consulted-${hash}`);
56
+ (0, fs_1.writeFileSync)(markerPath, new Date().toISOString(), 'utf8');
57
+ }
58
+ catch {
59
+ // Silently fail - don't break MCP on marker write failure
60
+ }
61
+ }
62
+ /**
63
+ * Common projectPath property for cross-project queries
64
+ */
65
+ const projectPathProperty = {
66
+ type: 'string',
67
+ description: 'Path to a different project with .codegraph/ initialized. If omitted, uses current project. Use this to query other codebases.',
68
+ };
9
69
  /**
10
70
  * All CodeGraph MCP tools
11
71
  *
12
72
  * Designed for minimal context usage - use codegraph_context as the primary tool,
13
73
  * and only use other tools for targeted follow-up queries.
74
+ *
75
+ * All tools support cross-project queries via the optional `projectPath` parameter.
14
76
  */
15
77
  exports.tools = [
16
78
  {
@@ -33,6 +95,7 @@ exports.tools = [
33
95
  description: 'Maximum results (default: 10)',
34
96
  default: 10,
35
97
  },
98
+ projectPath: projectPathProperty,
36
99
  },
37
100
  required: ['query'],
38
101
  },
@@ -57,6 +120,7 @@ exports.tools = [
57
120
  description: 'Include code snippets for key symbols (default: true)',
58
121
  default: true,
59
122
  },
123
+ projectPath: projectPathProperty,
60
124
  },
61
125
  required: ['task'],
62
126
  },
@@ -76,6 +140,7 @@ exports.tools = [
76
140
  description: 'Maximum number of callers to return (default: 20)',
77
141
  default: 20,
78
142
  },
143
+ projectPath: projectPathProperty,
79
144
  },
80
145
  required: ['symbol'],
81
146
  },
@@ -95,6 +160,7 @@ exports.tools = [
95
160
  description: 'Maximum number of callees to return (default: 20)',
96
161
  default: 20,
97
162
  },
163
+ projectPath: projectPathProperty,
98
164
  },
99
165
  required: ['symbol'],
100
166
  },
@@ -114,6 +180,7 @@ exports.tools = [
114
180
  description: 'How many levels of dependencies to traverse (default: 2)',
115
181
  default: 2,
116
182
  },
183
+ projectPath: projectPathProperty,
117
184
  },
118
185
  required: ['symbol'],
119
186
  },
@@ -133,6 +200,7 @@ exports.tools = [
133
200
  description: 'Include full source code (default: false to minimize context)',
134
201
  default: false,
135
202
  },
203
+ projectPath: projectPathProperty,
136
204
  },
137
205
  required: ['symbol'],
138
206
  },
@@ -142,18 +210,128 @@ exports.tools = [
142
210
  description: 'Get the status of the CodeGraph index, including statistics about indexed files, nodes, and edges.',
143
211
  inputSchema: {
144
212
  type: 'object',
145
- properties: {},
213
+ properties: {
214
+ projectPath: projectPathProperty,
215
+ },
216
+ },
217
+ },
218
+ {
219
+ name: 'codegraph_files',
220
+ description: 'REQUIRED for file/folder exploration. Get the project file structure from the CodeGraph index. Returns a tree view of all indexed files with metadata (language, symbol count). Much faster than Glob/filesystem scanning. Use this FIRST when exploring project structure, finding files, or understanding codebase organization.',
221
+ inputSchema: {
222
+ type: 'object',
223
+ properties: {
224
+ path: {
225
+ type: 'string',
226
+ description: 'Filter to files under this directory path (e.g., "src/components"). Returns all files if not specified.',
227
+ },
228
+ pattern: {
229
+ type: 'string',
230
+ description: 'Filter files matching this glob pattern (e.g., "*.tsx", "**/*.test.ts")',
231
+ },
232
+ format: {
233
+ type: 'string',
234
+ description: 'Output format: "tree" (hierarchical, default), "flat" (simple list), "grouped" (by language)',
235
+ enum: ['tree', 'flat', 'grouped'],
236
+ default: 'tree',
237
+ },
238
+ includeMetadata: {
239
+ type: 'boolean',
240
+ description: 'Include file metadata like language and symbol count (default: true)',
241
+ default: true,
242
+ },
243
+ maxDepth: {
244
+ type: 'number',
245
+ description: 'Maximum directory depth to show (default: unlimited)',
246
+ },
247
+ projectPath: projectPathProperty,
248
+ },
146
249
  },
147
250
  },
148
251
  ];
149
252
  /**
150
253
  * Tool handler that executes tools against a CodeGraph instance
254
+ *
255
+ * Supports cross-project queries via the projectPath parameter.
256
+ * Other projects are opened on-demand and cached for performance.
151
257
  */
152
258
  class ToolHandler {
153
259
  cg;
260
+ // Cache of opened CodeGraph instances for cross-project queries
261
+ projectCache = new Map();
154
262
  constructor(cg) {
155
263
  this.cg = cg;
156
264
  }
265
+ /**
266
+ * Update the default CodeGraph instance (e.g. after lazy initialization)
267
+ */
268
+ setDefaultCodeGraph(cg) {
269
+ this.cg = cg;
270
+ }
271
+ /**
272
+ * Whether a default CodeGraph instance is available
273
+ */
274
+ hasDefaultCodeGraph() {
275
+ return this.cg !== null;
276
+ }
277
+ /**
278
+ * Get CodeGraph instance for a project
279
+ *
280
+ * If projectPath is provided, opens that project's CodeGraph (cached).
281
+ * Otherwise returns the default CodeGraph instance.
282
+ *
283
+ * Walks up parent directories to find the nearest .codegraph/ folder,
284
+ * similar to how git finds .git/ directories.
285
+ */
286
+ getCodeGraph(projectPath) {
287
+ if (!projectPath) {
288
+ if (!this.cg) {
289
+ throw new Error('CodeGraph not initialized for this project. Run \'codegraph init\' first.');
290
+ }
291
+ return this.cg;
292
+ }
293
+ // Check cache first (using original path as key)
294
+ if (this.projectCache.has(projectPath)) {
295
+ return this.projectCache.get(projectPath);
296
+ }
297
+ // Walk up parent directories to find nearest .codegraph/
298
+ const resolvedRoot = (0, index_1.findNearestCodeGraphRoot)(projectPath);
299
+ if (!resolvedRoot) {
300
+ throw new Error(`CodeGraph not initialized in ${projectPath}. Run 'codegraph init' in that project first.`);
301
+ }
302
+ // Check if we already have this resolved root cached (different path, same project)
303
+ if (this.projectCache.has(resolvedRoot)) {
304
+ const cg = this.projectCache.get(resolvedRoot);
305
+ // Cache under original path too for faster future lookups
306
+ this.projectCache.set(projectPath, cg);
307
+ return cg;
308
+ }
309
+ // Open and cache under both paths
310
+ const cg = index_1.default.openSync(resolvedRoot);
311
+ this.projectCache.set(resolvedRoot, cg);
312
+ if (projectPath !== resolvedRoot) {
313
+ this.projectCache.set(projectPath, cg);
314
+ }
315
+ return cg;
316
+ }
317
+ /**
318
+ * Close all cached project connections
319
+ */
320
+ closeAll() {
321
+ for (const cg of this.projectCache.values()) {
322
+ cg.close();
323
+ }
324
+ this.projectCache.clear();
325
+ }
326
+ /**
327
+ * Validate that a value is a non-empty string
328
+ */
329
+ validateString(value, name) {
330
+ if (typeof value !== 'string' || value.length === 0) {
331
+ return this.errorResult(`${name} must be a non-empty string`);
332
+ }
333
+ return value;
334
+ }
157
335
  /**
158
336
  * Execute a tool by name
159
337
  */
@@ -173,12 +351,19 @@ class ToolHandler {
173
351
  case 'codegraph_node':
174
352
  return await this.handleNode(args);
175
353
  case 'codegraph_status':
176
- return await this.handleStatus();
354
+ return await this.handleStatus(args);
355
+ case 'codegraph_files':
356
+ return await this.handleFiles(args);
177
357
  default:
178
358
  return this.errorResult(`Unknown tool: ${toolName}`);
179
359
  }
180
360
  }
181
361
  catch (err) {
362
+ try {
363
+ const { captureException } = require('../sentry');
364
+ captureException(err, { tool: toolName });
365
+ }
366
+ catch { }
182
367
  return this.errorResult(`Tool execution failed: ${err instanceof Error ? err.message : String(err)}`);
183
368
  }
184
369
  }
@@ -186,10 +371,14 @@ class ToolHandler {
186
371
  * Handle codegraph_search
187
372
  */
188
373
  async handleSearch(args) {
189
- const query = args.query;
374
+ const query = this.validateString(args.query, 'query');
375
+ if (typeof query !== 'string')
376
+ return query;
377
+ const cg = this.getCodeGraph(args.projectPath);
190
378
  const kind = args.kind;
191
- const limit = args.limit || 10;
192
- const results = this.cg.searchNodes(query, {
379
+ const rawLimit = Number(args.limit) || 10;
380
+ const limit = (0, utils_1.clamp)(rawLimit, 1, 100);
381
+ const results = cg.searchNodes(query, {
193
382
  limit,
194
383
  kinds: kind ? [kind] : undefined,
195
384
  });
@@ -197,16 +386,24 @@ class ToolHandler {
197
386
  return this.textResult(`No results found for "${query}"`);
198
387
  }
199
388
  const formatted = this.formatSearchResults(results);
200
- return this.textResult(formatted);
389
+ return this.textResult(this.truncateOutput(formatted));
201
390
  }
202
391
  /**
203
392
  * Handle codegraph_context
204
393
  */
205
394
  async handleContext(args) {
206
- const task = args.task;
395
+ const task = this.validateString(args.task, 'task');
396
+ if (typeof task !== 'string')
397
+ return task;
398
+ // Mark session as consulted (enables Grep/Glob/Bash)
399
+ const sessionId = process.env.CLAUDE_SESSION_ID;
400
+ if (sessionId) {
401
+ markSessionConsulted(sessionId);
402
+ }
403
+ const cg = this.getCodeGraph(args.projectPath);
207
404
  const maxNodes = args.maxNodes || 20;
208
405
  const includeCode = args.includeCode !== false;
209
- const context = await this.cg.buildContext(task, {
406
+ const context = await cg.buildContext(task, {
210
407
  maxNodes,
211
408
  includeCode,
212
409
  format: 'markdown',
@@ -253,85 +450,88 @@ class ToolHandler {
253
450
  * Handle codegraph_callers
254
451
  */
255
452
  async handleCallers(args) {
256
- const symbol = args.symbol;
257
- const limit = args.limit || 20;
258
- // First find the node by name
259
- const results = this.cg.searchNodes(symbol, { limit: 1 });
260
- if (results.length === 0 || !results[0]) {
453
+ const symbol = this.validateString(args.symbol, 'symbol');
454
+ if (typeof symbol !== 'string')
455
+ return symbol;
456
+ const cg = this.getCodeGraph(args.projectPath);
457
+ const limit = (0, utils_1.clamp)(args.limit || 20, 1, 100);
458
+ const match = this.findSymbol(cg, symbol);
459
+ if (!match) {
261
460
  return this.textResult(`Symbol "${symbol}" not found in the codebase`);
262
461
  }
263
- const node = results[0].node;
264
- const callers = this.cg.getCallers(node.id);
462
+ const callers = cg.getCallers(match.node.id);
265
463
  if (callers.length === 0) {
266
- return this.textResult(`No callers found for "${symbol}"`);
464
+ return this.textResult(`No callers found for "${symbol}"${match.note}`);
267
465
  }
268
- // Extract just the nodes from the { node, edge } tuples
269
466
  const callerNodes = callers.slice(0, limit).map(c => c.node);
270
- const formatted = this.formatNodeList(callerNodes, `Callers of ${symbol}`);
271
- return this.textResult(formatted);
467
+ const formatted = this.formatNodeList(callerNodes, `Callers of ${symbol}`) + match.note;
468
+ return this.textResult(this.truncateOutput(formatted));
272
469
  }
273
470
  /**
274
471
  * Handle codegraph_callees
275
472
  */
276
473
  async handleCallees(args) {
277
- const symbol = args.symbol;
278
- const limit = args.limit || 20;
279
- // First find the node by name
280
- const results = this.cg.searchNodes(symbol, { limit: 1 });
281
- if (results.length === 0 || !results[0]) {
474
+ const symbol = this.validateString(args.symbol, 'symbol');
475
+ if (typeof symbol !== 'string')
476
+ return symbol;
477
+ const cg = this.getCodeGraph(args.projectPath);
478
+ const limit = (0, utils_1.clamp)(args.limit || 20, 1, 100);
479
+ const match = this.findSymbol(cg, symbol);
480
+ if (!match) {
282
481
  return this.textResult(`Symbol "${symbol}" not found in the codebase`);
283
482
  }
284
- const node = results[0].node;
285
- const callees = this.cg.getCallees(node.id);
483
+ const callees = cg.getCallees(match.node.id);
286
484
  if (callees.length === 0) {
287
- return this.textResult(`No callees found for "${symbol}"`);
485
+ return this.textResult(`No callees found for "${symbol}"${match.note}`);
288
486
  }
289
- // Extract just the nodes from the { node, edge } tuples
290
487
  const calleeNodes = callees.slice(0, limit).map(c => c.node);
291
- const formatted = this.formatNodeList(calleeNodes, `Callees of ${symbol}`);
292
- return this.textResult(formatted);
488
+ const formatted = this.formatNodeList(calleeNodes, `Callees of ${symbol}`) + match.note;
489
+ return this.textResult(this.truncateOutput(formatted));
293
490
  }
294
491
  /**
295
492
  * Handle codegraph_impact
296
493
  */
297
494
  async handleImpact(args) {
298
- const symbol = args.symbol;
299
- const depth = args.depth || 2;
300
- // First find the node by name
301
- const results = this.cg.searchNodes(symbol, { limit: 1 });
302
- if (results.length === 0 || !results[0]) {
495
+ const symbol = this.validateString(args.symbol, 'symbol');
496
+ if (typeof symbol !== 'string')
497
+ return symbol;
498
+ const cg = this.getCodeGraph(args.projectPath);
499
+ const depth = (0, utils_1.clamp)(args.depth || 2, 1, 10);
500
+ const match = this.findSymbol(cg, symbol);
501
+ if (!match) {
303
502
  return this.textResult(`Symbol "${symbol}" not found in the codebase`);
304
503
  }
305
- const node = results[0].node;
306
- const impact = this.cg.getImpactRadius(node.id, depth);
307
- const formatted = this.formatImpact(symbol, impact);
308
- return this.textResult(formatted);
504
+ const impact = cg.getImpactRadius(match.node.id, depth);
505
+ const formatted = this.formatImpact(symbol, impact) + match.note;
506
+ return this.textResult(this.truncateOutput(formatted));
309
507
  }
310
508
  /**
311
509
  * Handle codegraph_node
312
510
  */
313
511
  async handleNode(args) {
314
- const symbol = args.symbol;
512
+ const symbol = this.validateString(args.symbol, 'symbol');
513
+ if (typeof symbol !== 'string')
514
+ return symbol;
515
+ const cg = this.getCodeGraph(args.projectPath);
315
516
  // Default to false to minimize context usage
316
517
  const includeCode = args.includeCode === true;
317
- // Find the node by name
318
- const results = this.cg.searchNodes(symbol, { limit: 1 });
319
- if (results.length === 0 || !results[0]) {
518
+ const match = this.findSymbol(cg, symbol);
519
+ if (!match) {
320
520
  return this.textResult(`Symbol "${symbol}" not found in the codebase`);
321
521
  }
322
- const node = results[0].node;
323
522
  let code = null;
324
523
  if (includeCode) {
325
- code = await this.cg.getCode(node.id);
524
+ code = await cg.getCode(match.node.id);
326
525
  }
327
- const formatted = this.formatNodeDetails(node, code);
328
- return this.textResult(formatted);
526
+ const formatted = this.formatNodeDetails(match.node, code) + match.note;
527
+ return this.textResult(this.truncateOutput(formatted));
329
528
  }
330
529
  /**
331
530
  * Handle codegraph_status
332
531
  */
333
- async handleStatus() {
334
- const stats = this.cg.getStats();
532
+ async handleStatus(args) {
533
+ const cg = this.getCodeGraph(args.projectPath);
534
+ const stats = cg.getStats();
335
535
  const lines = [
336
536
  '## CodeGraph Status',
337
537
  '',
@@ -355,6 +555,199 @@ class ToolHandler {
355
555
  }
356
556
  return this.textResult(lines.join('\n'));
357
557
  }
558
+ /**
559
+ * Handle codegraph_files - get project file structure from the index
560
+ */
561
+ async handleFiles(args) {
562
+ const cg = this.getCodeGraph(args.projectPath);
563
+ const pathFilter = args.path;
564
+ const pattern = args.pattern;
565
+ const format = args.format || 'tree';
566
+ const includeMetadata = args.includeMetadata !== false;
567
+ const maxDepth = args.maxDepth != null ? (0, utils_1.clamp)(args.maxDepth, 1, 20) : undefined;
568
+ // Get all files from the index
569
+ const allFiles = cg.getFiles();
570
+ if (allFiles.length === 0) {
571
+ return this.textResult('No files indexed. Run `codegraph index` first.');
572
+ }
573
+ // Filter by path prefix
574
+ let files = pathFilter
575
+ ? allFiles.filter(f => f.path.startsWith(pathFilter) || f.path.startsWith('./' + pathFilter))
576
+ : allFiles;
577
+ // Filter by glob pattern
578
+ if (pattern) {
579
+ const regex = this.globToRegex(pattern);
580
+ files = files.filter(f => regex.test(f.path));
581
+ }
582
+ if (files.length === 0) {
583
+ return this.textResult(`No files found matching the criteria.`);
584
+ }
585
+ // Format output
586
+ let output;
587
+ switch (format) {
588
+ case 'flat':
589
+ output = this.formatFilesFlat(files, includeMetadata);
590
+ break;
591
+ case 'grouped':
592
+ output = this.formatFilesGrouped(files, includeMetadata);
593
+ break;
594
+ case 'tree':
595
+ default:
596
+ output = this.formatFilesTree(files, includeMetadata, maxDepth);
597
+ break;
598
+ }
599
+ return this.textResult(this.truncateOutput(output));
600
+ }
601
+ /**
602
+ * Convert glob pattern to regex
603
+ */
604
+ globToRegex(pattern) {
605
+ const escaped = pattern
606
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&') // Escape special regex chars except * and ?
607
+ .replace(/\*\*/g, '{{GLOBSTAR}}') // Temp placeholder for **
608
+ .replace(/\*/g, '[^/]*') // * matches anything except /
609
+ .replace(/\?/g, '[^/]') // ? matches single char except /
610
+ .replace(/\{\{GLOBSTAR\}\}/g, '.*'); // ** matches anything including /
611
+ return new RegExp(escaped);
612
+ }
613
+ /**
614
+ * Format files as a flat list
615
+ */
616
+ formatFilesFlat(files, includeMetadata) {
617
+ const lines = [`## Files (${files.length})`, ''];
618
+ for (const file of files.sort((a, b) => a.path.localeCompare(b.path))) {
619
+ if (includeMetadata) {
620
+ lines.push(`- ${file.path} (${file.language}, ${file.nodeCount} symbols)`);
621
+ }
622
+ else {
623
+ lines.push(`- ${file.path}`);
624
+ }
625
+ }
626
+ return lines.join('\n');
627
+ }
628
+ /**
629
+ * Format files grouped by language
630
+ */
631
+ formatFilesGrouped(files, includeMetadata) {
632
+ const byLang = new Map();
633
+ for (const file of files) {
634
+ const existing = byLang.get(file.language) || [];
635
+ existing.push(file);
636
+ byLang.set(file.language, existing);
637
+ }
638
+ const lines = [`## Files by Language (${files.length} total)`, ''];
639
+ // Sort languages by file count (descending)
640
+ const sortedLangs = [...byLang.entries()].sort((a, b) => b[1].length - a[1].length);
641
+ for (const [lang, langFiles] of sortedLangs) {
642
+ lines.push(`### ${lang} (${langFiles.length})`);
643
+ for (const file of langFiles.sort((a, b) => a.path.localeCompare(b.path))) {
644
+ if (includeMetadata) {
645
+ lines.push(`- ${file.path} (${file.nodeCount} symbols)`);
646
+ }
647
+ else {
648
+ lines.push(`- ${file.path}`);
649
+ }
650
+ }
651
+ lines.push('');
652
+ }
653
+ return lines.join('\n');
654
+ }
655
+ /**
656
+ * Format files as a tree structure
657
+ */
658
+ formatFilesTree(files, includeMetadata, maxDepth) {
659
+ const root = { name: '', children: new Map() };
660
+ for (const file of files) {
661
+ const parts = file.path.split('/');
662
+ let current = root;
663
+ for (let i = 0; i < parts.length; i++) {
664
+ const part = parts[i];
665
+ if (!part)
666
+ continue;
667
+ if (!current.children.has(part)) {
668
+ current.children.set(part, { name: part, children: new Map() });
669
+ }
670
+ current = current.children.get(part);
671
+ // If this is the last part, it's a file
672
+ if (i === parts.length - 1) {
673
+ current.file = { language: file.language, nodeCount: file.nodeCount };
674
+ }
675
+ }
676
+ }
677
+ // Render tree
678
+ const lines = [`## Project Structure (${files.length} files)`, ''];
679
+ const renderNode = (node, prefix, isLast, depth) => {
680
+ if (maxDepth !== undefined && depth > maxDepth)
681
+ return;
682
+ const connector = isLast ? '└── ' : '├── ';
683
+ const childPrefix = isLast ? ' ' : '│ ';
684
+ if (node.name) {
685
+ let line = prefix + connector + node.name;
686
+ if (node.file && includeMetadata) {
687
+ line += ` (${node.file.language}, ${node.file.nodeCount} symbols)`;
688
+ }
689
+ lines.push(line);
690
+ }
691
+ const children = [...node.children.values()];
692
+ // Sort: directories first, then files, both alphabetically
693
+ children.sort((a, b) => {
694
+ const aIsDir = a.children.size > 0 && !a.file;
695
+ const bIsDir = b.children.size > 0 && !b.file;
696
+ if (aIsDir !== bIsDir)
697
+ return aIsDir ? -1 : 1;
698
+ return a.name.localeCompare(b.name);
699
+ });
700
+ for (let i = 0; i < children.length; i++) {
701
+ const child = children[i];
702
+ const nextPrefix = node.name ? prefix + childPrefix : prefix;
703
+ renderNode(child, nextPrefix, i === children.length - 1, depth + 1);
704
+ }
705
+ };
706
+ renderNode(root, '', true, 0);
707
+ return lines.join('\n');
708
+ }
709
+ // =========================================================================
710
+ // Symbol resolution helpers
711
+ // =========================================================================
712
+ /**
713
+ * Find a symbol by name, handling disambiguation when multiple matches exist.
714
+ * Returns the best match and a note about alternatives if any.
715
+ */
716
+ findSymbol(cg, symbol) {
717
+ const results = cg.searchNodes(symbol, { limit: 10 });
718
+ if (results.length === 0 || !results[0]) {
719
+ return null;
720
+ }
721
+ // If only one result, or first is an exact name match, use it directly
722
+ const exactMatches = results.filter(r => r.node.name === symbol);
723
+ if (exactMatches.length === 1) {
724
+ return { node: exactMatches[0].node, note: '' };
725
+ }
726
+ if (exactMatches.length > 1) {
727
+ // Multiple exact matches - pick first, note the others
728
+ const picked = exactMatches[0].node;
729
+ const others = exactMatches.slice(1).map(r => `${r.node.name} (${r.node.kind}) at ${r.node.filePath}:${r.node.startLine}`);
730
+ const note = `\n\n> **Note:** ${exactMatches.length} symbols named "${symbol}". Showing results for \`${picked.filePath}:${picked.startLine}\`. Others: ${others.join(', ')}`;
731
+ return { node: picked, note };
732
+ }
733
+ // No exact match, use best fuzzy match
734
+ return { node: results[0].node, note: '' };
735
+ }
736
+ /**
737
+ * Maximum output length to prevent context bloat (characters)
738
+ */
739
+ MAX_OUTPUT_LENGTH = 15000;
740
+ /**
741
+ * Truncate output if it exceeds the maximum length
742
+ */
743
+ truncateOutput(text) {
744
+ if (text.length <= this.MAX_OUTPUT_LENGTH)
745
+ return text;
746
+ const truncated = text.slice(0, this.MAX_OUTPUT_LENGTH);
747
+ const lastNewline = truncated.lastIndexOf('\n');
748
+ const cutPoint = lastNewline > this.MAX_OUTPUT_LENGTH * 0.8 ? lastNewline : this.MAX_OUTPUT_LENGTH;
749
+ return truncated.slice(0, cutPoint) + '\n\n... (output truncated)';
750
+ }
358
751
  // =========================================================================
359
752
  // Formatting helpers (compact by default to reduce context usage)
360
753
  // =========================================================================