@mcampa/ai-context-mcp 0.0.1-beta.05e8984

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.
@@ -0,0 +1,792 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { COLLECTION_LIMIT_MESSAGE, } from "@mcampa/ai-context-core";
4
+ import { ensureAbsolutePath, truncateContent, trackCodebasePath, } from "./utils.js";
5
+ export class ToolHandlers {
6
+ constructor(context, snapshotManager) {
7
+ this.indexingStats = null;
8
+ this.context = context;
9
+ this.snapshotManager = snapshotManager;
10
+ this.currentWorkspace = process.cwd();
11
+ console.log(`[WORKSPACE] Current workspace: ${this.currentWorkspace}`);
12
+ }
13
+ /**
14
+ * Sync indexed codebases from Zilliz Cloud collections
15
+ * This method fetches all collections from the vector database,
16
+ * gets the first document from each collection to extract codebasePath from metadata,
17
+ * and updates the snapshot with discovered codebases.
18
+ *
19
+ * Logic: Compare mcp-codebase-snapshot.json with zilliz cloud collections
20
+ * - If local snapshot has extra directories (not in cloud), remove them
21
+ * - If local snapshot is missing directories (exist in cloud), ignore them
22
+ */
23
+ async syncIndexedCodebasesFromCloud() {
24
+ try {
25
+ console.log(`[SYNC-CLOUD] ๐Ÿ”„ Syncing indexed codebases from Zilliz Cloud...`);
26
+ // Get all collections using the interface method
27
+ const vectorDb = this.context.getVectorDatabase();
28
+ // Use the new listCollections method from the interface
29
+ const collections = await vectorDb.listCollections();
30
+ console.log(`[SYNC-CLOUD] ๐Ÿ“‹ Found ${collections.length} collections in Zilliz Cloud`);
31
+ if (collections.length === 0) {
32
+ console.log(`[SYNC-CLOUD] โœ… No collections found in cloud`);
33
+ // If no collections in cloud, remove all local codebases
34
+ const localCodebases = this.snapshotManager.getIndexedCodebases();
35
+ if (localCodebases.length > 0) {
36
+ console.log(`[SYNC-CLOUD] ๐Ÿงน Removing ${localCodebases.length} local codebases as cloud has no collections`);
37
+ for (const codebasePath of localCodebases) {
38
+ this.snapshotManager.removeIndexedCodebase(codebasePath);
39
+ console.log(`[SYNC-CLOUD] โž– Removed local codebase: ${codebasePath}`);
40
+ }
41
+ this.snapshotManager.saveCodebaseSnapshot();
42
+ console.log(`[SYNC-CLOUD] ๐Ÿ’พ Updated snapshot to match empty cloud state`);
43
+ }
44
+ return;
45
+ }
46
+ const cloudCodebases = new Set();
47
+ // Check each collection for codebase path
48
+ for (const collectionName of collections) {
49
+ try {
50
+ // Skip collections that don't match the code_chunks pattern (support both legacy and new collections)
51
+ if (!collectionName.startsWith("code_chunks_") &&
52
+ !collectionName.startsWith("hybrid_code_chunks_")) {
53
+ console.log(`[SYNC-CLOUD] โญ๏ธ Skipping non-code collection: ${collectionName}`);
54
+ continue;
55
+ }
56
+ console.log(`[SYNC-CLOUD] ๐Ÿ” Checking collection: ${collectionName}`);
57
+ // Query the first document to get metadata
58
+ const results = await vectorDb.query(collectionName, "", // Empty filter to get all results
59
+ ["metadata"], // Only fetch metadata field
60
+ 1);
61
+ if (results && results.length > 0) {
62
+ const firstResult = results[0];
63
+ const metadataStr = firstResult.metadata;
64
+ if (metadataStr && typeof metadataStr === "string") {
65
+ try {
66
+ const metadata = JSON.parse(metadataStr);
67
+ const codebasePath = metadata.codebasePath;
68
+ if (codebasePath && typeof codebasePath === "string") {
69
+ console.log(`[SYNC-CLOUD] ๐Ÿ“ Found codebase path: ${codebasePath} in collection: ${collectionName}`);
70
+ cloudCodebases.add(codebasePath);
71
+ }
72
+ else {
73
+ console.warn(`[SYNC-CLOUD] โš ๏ธ No codebasePath found in metadata for collection: ${collectionName}`);
74
+ }
75
+ }
76
+ catch (parseError) {
77
+ console.warn(`[SYNC-CLOUD] โš ๏ธ Failed to parse metadata JSON for collection ${collectionName}:`, parseError);
78
+ }
79
+ }
80
+ else {
81
+ console.warn(`[SYNC-CLOUD] โš ๏ธ No metadata found in collection: ${collectionName}`);
82
+ }
83
+ }
84
+ else {
85
+ console.log(`[SYNC-CLOUD] โ„น๏ธ Collection ${collectionName} is empty`);
86
+ }
87
+ }
88
+ catch (collectionError) {
89
+ console.warn(`[SYNC-CLOUD] โš ๏ธ Error checking collection ${collectionName}:`, collectionError instanceof Error
90
+ ? collectionError.message
91
+ : String(collectionError));
92
+ // Continue with next collection
93
+ }
94
+ }
95
+ console.log(`[SYNC-CLOUD] ๐Ÿ“Š Found ${cloudCodebases.size} valid codebases in cloud`);
96
+ // Get current local codebases
97
+ const localCodebases = new Set(this.snapshotManager.getIndexedCodebases());
98
+ console.log(`[SYNC-CLOUD] ๐Ÿ“Š Found ${localCodebases.size} local codebases in snapshot`);
99
+ let hasChanges = false;
100
+ // Remove local codebases that don't exist in cloud
101
+ for (const localCodebase of localCodebases) {
102
+ if (!cloudCodebases.has(localCodebase)) {
103
+ this.snapshotManager.removeIndexedCodebase(localCodebase);
104
+ hasChanges = true;
105
+ console.log(`[SYNC-CLOUD] โž– Removed local codebase (not in cloud): ${localCodebase}`);
106
+ }
107
+ }
108
+ // Note: We don't add cloud codebases that are missing locally (as per user requirement)
109
+ console.log(`[SYNC-CLOUD] โ„น๏ธ Skipping addition of cloud codebases not present locally (per sync policy)`);
110
+ if (hasChanges) {
111
+ this.snapshotManager.saveCodebaseSnapshot();
112
+ console.log(`[SYNC-CLOUD] ๐Ÿ’พ Updated snapshot to match cloud state`);
113
+ }
114
+ else {
115
+ console.log(`[SYNC-CLOUD] โœ… Local snapshot already matches cloud state`);
116
+ }
117
+ console.log(`[SYNC-CLOUD] โœ… Cloud sync completed successfully`);
118
+ }
119
+ catch (error) {
120
+ console.error(`[SYNC-CLOUD] โŒ Error syncing codebases from cloud:`, String(error));
121
+ // Don't throw - this is not critical for the main functionality
122
+ }
123
+ }
124
+ async handleIndexCodebase(args) {
125
+ const { path: codebasePath, force, splitter, customExtensions, ignorePatterns, } = args;
126
+ const forceReindex = force || false;
127
+ const splitterType = splitter || "ast"; // Default to AST
128
+ const customFileExtensions = customExtensions || [];
129
+ const customIgnorePatterns = ignorePatterns || [];
130
+ try {
131
+ // Sync indexed codebases from cloud first
132
+ await this.syncIndexedCodebasesFromCloud();
133
+ // Validate splitter parameter
134
+ if (splitterType !== "ast" && splitterType !== "langchain") {
135
+ return {
136
+ content: [
137
+ {
138
+ type: "text",
139
+ text: `Error: Invalid splitter type '${splitterType}'. Must be 'ast' or 'langchain'.`,
140
+ },
141
+ ],
142
+ isError: true,
143
+ };
144
+ }
145
+ // Force absolute path resolution - warn if relative path provided
146
+ const absolutePath = ensureAbsolutePath(codebasePath);
147
+ // Validate path exists
148
+ if (!fs.existsSync(absolutePath)) {
149
+ return {
150
+ content: [
151
+ {
152
+ type: "text",
153
+ text: `Error: Path '${absolutePath}' does not exist. Original input: '${codebasePath}'`,
154
+ },
155
+ ],
156
+ isError: true,
157
+ };
158
+ }
159
+ // Check if it's a directory
160
+ const stat = fs.statSync(absolutePath);
161
+ if (!stat.isDirectory()) {
162
+ return {
163
+ content: [
164
+ {
165
+ type: "text",
166
+ text: `Error: Path '${absolutePath}' is not a directory`,
167
+ },
168
+ ],
169
+ isError: true,
170
+ };
171
+ }
172
+ // Check if already indexing
173
+ if (this.snapshotManager.getIndexingCodebases().includes(absolutePath)) {
174
+ return {
175
+ content: [
176
+ {
177
+ type: "text",
178
+ text: `Codebase '${absolutePath}' is already being indexed in the background. Please wait for completion.`,
179
+ },
180
+ ],
181
+ isError: true,
182
+ };
183
+ }
184
+ //Check if the snapshot and cloud index are in sync
185
+ if (this.snapshotManager.getIndexedCodebases().includes(absolutePath) !==
186
+ (await this.context.hasIndex())) {
187
+ console.warn(`[INDEX-VALIDATION] โŒ Snapshot and cloud index mismatch: ${absolutePath}`);
188
+ }
189
+ // Check if already indexed (unless force is true)
190
+ if (!forceReindex &&
191
+ this.snapshotManager.getIndexedCodebases().includes(absolutePath)) {
192
+ return {
193
+ content: [
194
+ {
195
+ type: "text",
196
+ text: `Codebase '${absolutePath}' is already indexed. Use force=true to re-index.`,
197
+ },
198
+ ],
199
+ isError: true,
200
+ };
201
+ }
202
+ // If force reindex and codebase is already indexed, remove it
203
+ if (forceReindex) {
204
+ if (this.snapshotManager.getIndexedCodebases().includes(absolutePath)) {
205
+ console.log(`[FORCE-REINDEX] ๐Ÿ”„ Removing '${absolutePath}' from indexed list for re-indexing`);
206
+ this.snapshotManager.removeIndexedCodebase(absolutePath);
207
+ }
208
+ if (await this.context.hasIndex()) {
209
+ console.log(`[FORCE-REINDEX] ๐Ÿ”„ Clearing index for '${absolutePath}'`);
210
+ await this.context.clearIndex(absolutePath);
211
+ }
212
+ }
213
+ // CRITICAL: Pre-index collection creation validation
214
+ try {
215
+ console.log(`[INDEX-VALIDATION] ๐Ÿ” Validating collection creation capability`);
216
+ const canCreateCollection = await this.context
217
+ .getVectorDatabase()
218
+ .checkCollectionLimit();
219
+ if (!canCreateCollection) {
220
+ console.error(`[INDEX-VALIDATION] โŒ Collection limit validation failed: ${absolutePath}`);
221
+ // CRITICAL: Immediately return the COLLECTION_LIMIT_MESSAGE to MCP client
222
+ return {
223
+ content: [
224
+ {
225
+ type: "text",
226
+ text: COLLECTION_LIMIT_MESSAGE,
227
+ },
228
+ ],
229
+ isError: true,
230
+ };
231
+ }
232
+ console.log(`[INDEX-VALIDATION] โœ… Collection creation validation completed`);
233
+ }
234
+ catch (validationError) {
235
+ // Handle other collection creation errors
236
+ console.error(`[INDEX-VALIDATION] โŒ Collection creation validation failed:`, validationError);
237
+ return {
238
+ content: [
239
+ {
240
+ type: "text",
241
+ text: `Error validating collection creation: ${validationError instanceof Error
242
+ ? validationError.message
243
+ : String(validationError)}`,
244
+ },
245
+ ],
246
+ isError: true,
247
+ };
248
+ }
249
+ // Add custom extensions if provided
250
+ if (customFileExtensions.length > 0) {
251
+ console.log(`[CUSTOM-EXTENSIONS] Adding ${customFileExtensions.length} custom extensions: ${customFileExtensions.join(", ")}`);
252
+ this.context.addCustomExtensions(customFileExtensions);
253
+ }
254
+ // Add custom ignore patterns if provided (before loading file-based patterns)
255
+ if (customIgnorePatterns.length > 0) {
256
+ console.log(`[IGNORE-PATTERNS] Adding ${customIgnorePatterns.length} custom ignore patterns: ${customIgnorePatterns.join(", ")}`);
257
+ this.context.addCustomIgnorePatterns(customIgnorePatterns);
258
+ }
259
+ // Check current status and log if retrying after failure
260
+ const currentStatus = this.snapshotManager.getCodebaseStatus(absolutePath);
261
+ if (currentStatus === "indexfailed") {
262
+ const failedInfo = this.snapshotManager.getCodebaseInfo(absolutePath);
263
+ const errorMessage = failedInfo?.status === "indexfailed"
264
+ ? failedInfo.errorMessage
265
+ : "Unknown error";
266
+ console.log(`[BACKGROUND-INDEX] Retrying indexing for previously failed codebase. Previous error: ${errorMessage}`);
267
+ }
268
+ // Set to indexing status and save snapshot immediately
269
+ this.snapshotManager.setCodebaseIndexing(absolutePath, 0);
270
+ this.snapshotManager.saveCodebaseSnapshot();
271
+ // Track the codebase path for syncing
272
+ trackCodebasePath(absolutePath);
273
+ // Start background indexing - now safe to proceed
274
+ this.startBackgroundIndexing(absolutePath, forceReindex, splitterType);
275
+ const pathInfo = codebasePath !== absolutePath
276
+ ? `\nNote: Input path '${codebasePath}' was resolved to absolute path '${absolutePath}'`
277
+ : "";
278
+ const extensionInfo = customFileExtensions.length > 0
279
+ ? `\nUsing ${customFileExtensions.length} custom extensions: ${customFileExtensions.join(", ")}`
280
+ : "";
281
+ const ignoreInfo = customIgnorePatterns.length > 0
282
+ ? `\nUsing ${customIgnorePatterns.length} custom ignore patterns: ${customIgnorePatterns.join(", ")}`
283
+ : "";
284
+ return {
285
+ content: [
286
+ {
287
+ type: "text",
288
+ text: `Started background indexing for codebase '${absolutePath}' using ${splitterType.toUpperCase()} splitter.${pathInfo}${extensionInfo}${ignoreInfo}\n\nIndexing is running in the background. You can search the codebase while indexing is in progress, but results may be incomplete until indexing completes.`,
289
+ },
290
+ ],
291
+ };
292
+ }
293
+ catch (error) {
294
+ // Enhanced error handling to prevent MCP service crash
295
+ console.error("Error in handleIndexCodebase:", error);
296
+ // Ensure we always return a proper MCP response, never throw
297
+ return {
298
+ content: [
299
+ {
300
+ type: "text",
301
+ text: `Error starting indexing: ${String(error)}`,
302
+ },
303
+ ],
304
+ isError: true,
305
+ };
306
+ }
307
+ }
308
+ async startBackgroundIndexing(codebasePath, forceReindex, splitterType) {
309
+ const absolutePath = codebasePath;
310
+ let lastSaveTime = 0; // Track last save timestamp
311
+ try {
312
+ console.log(`[BACKGROUND-INDEX] Starting background indexing for: ${absolutePath}`);
313
+ // Note: If force reindex, collection was already cleared during validation phase
314
+ if (forceReindex) {
315
+ console.log(`[BACKGROUND-INDEX] โ„น๏ธ Force reindex mode - collection was already cleared during validation`);
316
+ }
317
+ // Use the existing Context instance for indexing.
318
+ const contextForThisTask = this.context;
319
+ if (splitterType !== "ast") {
320
+ console.warn(`[BACKGROUND-INDEX] Non-AST splitter '${splitterType}' requested; falling back to AST splitter`);
321
+ }
322
+ // Load ignore patterns from files first (including .ignore, .gitignore, etc.)
323
+ await this.context.getLoadedIgnorePatterns(absolutePath);
324
+ // Initialize file synchronizer with proper ignore patterns (including project-specific patterns)
325
+ const { FileSynchronizer } = await import("@mcampa/ai-context-core");
326
+ const ignorePatterns = this.context.getIgnorePatterns() || [];
327
+ console.log(`[BACKGROUND-INDEX] Using ignore patterns: ${ignorePatterns.join(", ")}`);
328
+ const synchronizer = new FileSynchronizer(absolutePath, ignorePatterns);
329
+ await synchronizer.initialize();
330
+ // Store synchronizer in the context (let context manage collection names)
331
+ await this.context.getPreparedCollection(absolutePath);
332
+ const collectionName = this.context.getCollectionName();
333
+ this.context.setSynchronizer(collectionName, synchronizer);
334
+ if (contextForThisTask !== this.context) {
335
+ contextForThisTask.setSynchronizer(collectionName, synchronizer);
336
+ }
337
+ console.log(`[BACKGROUND-INDEX] Starting indexing with ${splitterType} splitter for: ${absolutePath}`);
338
+ // Log embedding provider information before indexing
339
+ const embeddingProvider = this.context.getEmbedding();
340
+ console.log(`[BACKGROUND-INDEX] ๐Ÿง  Using embedding provider: ${embeddingProvider.getProvider()} with dimension: ${embeddingProvider.getDimension()}`);
341
+ // Start indexing with the appropriate context and progress tracking
342
+ console.log(`[BACKGROUND-INDEX] ๐Ÿš€ Beginning codebase indexing process...`);
343
+ const stats = await contextForThisTask.indexCodebase(absolutePath, (progress) => {
344
+ // Update progress in snapshot manager using new method
345
+ this.snapshotManager.setCodebaseIndexing(absolutePath, progress.percentage);
346
+ // Save snapshot periodically (every 2 seconds to avoid too frequent saves)
347
+ const currentTime = Date.now();
348
+ if (currentTime - lastSaveTime >= 2000) {
349
+ // 2 seconds = 2000ms
350
+ this.snapshotManager.saveCodebaseSnapshot();
351
+ lastSaveTime = currentTime;
352
+ console.log(`[BACKGROUND-INDEX] ๐Ÿ’พ Saved progress snapshot at ${progress.percentage.toFixed(1)}%`);
353
+ }
354
+ console.log(`[BACKGROUND-INDEX] Progress: ${progress.phase} - ${progress.percentage}% (${progress.current}/${progress.total})`);
355
+ });
356
+ console.log(`[BACKGROUND-INDEX] โœ… Indexing completed successfully! Files: ${stats.indexedFiles}, Chunks: ${stats.totalChunks}`);
357
+ // Set codebase to indexed status with complete statistics
358
+ this.snapshotManager.setCodebaseIndexed(absolutePath, stats);
359
+ this.indexingStats = {
360
+ indexedFiles: stats.indexedFiles,
361
+ totalChunks: stats.totalChunks,
362
+ };
363
+ // Save snapshot after updating codebase lists
364
+ this.snapshotManager.saveCodebaseSnapshot();
365
+ let message = `Background indexing completed for '${absolutePath}' using ${splitterType.toUpperCase()} splitter.\nIndexed ${stats.indexedFiles} files, ${stats.totalChunks} chunks.`;
366
+ if (stats.status === "limit_reached") {
367
+ message += `\nโš ๏ธ Warning: Indexing stopped because the chunk limit (450,000) was reached. The index may be incomplete.`;
368
+ }
369
+ console.log(`[BACKGROUND-INDEX] ${message}`);
370
+ }
371
+ catch (error) {
372
+ console.error(`[BACKGROUND-INDEX] Error during indexing for ${absolutePath}:`, error);
373
+ // Get the last attempted progress
374
+ const lastProgress = this.snapshotManager.getIndexingProgress(absolutePath);
375
+ // Set codebase to failed status with error information
376
+ const errorMessage = String(error);
377
+ this.snapshotManager.setCodebaseIndexFailed(absolutePath, errorMessage, lastProgress);
378
+ this.snapshotManager.saveCodebaseSnapshot();
379
+ // Log error but don't crash MCP service - indexing errors are handled gracefully
380
+ console.error(`[BACKGROUND-INDEX] Indexing failed for ${absolutePath}: ${errorMessage}`);
381
+ }
382
+ }
383
+ async handleSearchCode(args) {
384
+ const { path: codebasePath, query, limit = 10, extensionFilter } = args;
385
+ const resultLimit = limit || 10;
386
+ try {
387
+ // Sync indexed codebases from cloud first
388
+ await this.syncIndexedCodebasesFromCloud();
389
+ // Force absolute path resolution - warn if relative path provided
390
+ const absolutePath = ensureAbsolutePath(codebasePath);
391
+ // Validate path exists
392
+ if (!fs.existsSync(absolutePath)) {
393
+ return {
394
+ content: [
395
+ {
396
+ type: "text",
397
+ text: `Error: Path '${absolutePath}' does not exist. Original input: '${codebasePath}'`,
398
+ },
399
+ ],
400
+ isError: true,
401
+ };
402
+ }
403
+ // Check if it's a directory
404
+ const stat = fs.statSync(absolutePath);
405
+ if (!stat.isDirectory()) {
406
+ return {
407
+ content: [
408
+ {
409
+ type: "text",
410
+ text: `Error: Path '${absolutePath}' is not a directory`,
411
+ },
412
+ ],
413
+ isError: true,
414
+ };
415
+ }
416
+ trackCodebasePath(absolutePath);
417
+ // Check if this codebase is indexed or being indexed
418
+ const isIndexed = this.snapshotManager
419
+ .getIndexedCodebases()
420
+ .includes(absolutePath);
421
+ const isIndexing = this.snapshotManager
422
+ .getIndexingCodebases()
423
+ .includes(absolutePath);
424
+ if (!isIndexed && !isIndexing) {
425
+ return {
426
+ content: [
427
+ {
428
+ type: "text",
429
+ text: `Error: Codebase '${absolutePath}' is not indexed. Please index it first using the index_codebase tool.`,
430
+ },
431
+ ],
432
+ isError: true,
433
+ };
434
+ }
435
+ // Show indexing status if codebase is being indexed
436
+ let indexingStatusMessage = "";
437
+ if (isIndexing) {
438
+ indexingStatusMessage = `\nโš ๏ธ **Indexing in Progress**: This codebase is currently being indexed in the background. Search results may be incomplete until indexing completes.`;
439
+ }
440
+ console.log(`[SEARCH] Searching in codebase: ${absolutePath}`);
441
+ console.log(`[SEARCH] Query: "${query}"`);
442
+ console.log(`[SEARCH] Indexing status: ${isIndexing ? "In Progress" : "Completed"}`);
443
+ // Log embedding provider information before search
444
+ const embeddingProvider = this.context.getEmbedding();
445
+ console.log(`[SEARCH] ๐Ÿง  Using embedding provider: ${embeddingProvider.getProvider()} for search`);
446
+ console.log(`[SEARCH] ๐Ÿ” Generating embeddings for query using ${embeddingProvider.getProvider()}...`);
447
+ // Build filter expression from extensionFilter list
448
+ let filterExpr = undefined;
449
+ if (Array.isArray(extensionFilter) && extensionFilter.length > 0) {
450
+ const cleaned = extensionFilter
451
+ .filter((v) => typeof v === "string")
452
+ .map((v) => v.trim())
453
+ .filter((v) => v.length > 0);
454
+ const invalid = cleaned.filter((e) => !(e.startsWith(".") && e.length > 1 && !/\s/.test(e)));
455
+ if (invalid.length > 0) {
456
+ return {
457
+ content: [
458
+ {
459
+ type: "text",
460
+ text: `Error: Invalid file extensions in extensionFilter: ${JSON.stringify(invalid)}. Use proper extensions like '.ts', '.py'.`,
461
+ },
462
+ ],
463
+ isError: true,
464
+ };
465
+ }
466
+ const quoted = cleaned.map((e) => `'${e}'`).join(", ");
467
+ filterExpr = `fileExtension in [${quoted}]`;
468
+ }
469
+ // Search in the specified codebase
470
+ const searchResults = await this.context.semanticSearch(query, Math.min(resultLimit, 50), 0.3, filterExpr);
471
+ console.log(`[SEARCH] โœ… Search completed! Found ${searchResults.length} results using ${embeddingProvider.getProvider()} embeddings`);
472
+ if (searchResults.length === 0) {
473
+ let noResultsMessage = `No results found for query: "${query}" in codebase '${absolutePath}'`;
474
+ if (isIndexing) {
475
+ noResultsMessage += `\n\nNote: This codebase is still being indexed. Try searching again after indexing completes, or the query may not match any indexed content.`;
476
+ }
477
+ return {
478
+ content: [
479
+ {
480
+ type: "text",
481
+ text: noResultsMessage,
482
+ },
483
+ ],
484
+ };
485
+ }
486
+ // Format results
487
+ const formattedResults = searchResults
488
+ .map((result, index) => {
489
+ const location = `${result.relativePath}:${result.startLine}-${result.endLine}`;
490
+ const context = truncateContent(result.content, 5000);
491
+ const codebaseInfo = path.basename(absolutePath);
492
+ return (`${index + 1}. Code snippet (${result.language}) [${codebaseInfo}]\n` +
493
+ ` Location: ${location}\n` +
494
+ ` Rank: ${index + 1}\n` +
495
+ ` Context: \n\`\`\`${result.language}\n${context}\n\`\`\`\n`);
496
+ })
497
+ .join("\n");
498
+ let resultMessage = `Found ${searchResults.length} results for query: "${query}" in codebase '${absolutePath}'${indexingStatusMessage}\n\n${formattedResults}`;
499
+ if (isIndexing) {
500
+ resultMessage += `\n\n๐Ÿ’ก **Tip**: This codebase is still being indexed. More results may become available as indexing progresses.`;
501
+ }
502
+ return {
503
+ content: [
504
+ {
505
+ type: "text",
506
+ text: resultMessage,
507
+ },
508
+ ],
509
+ };
510
+ }
511
+ catch (error) {
512
+ // Check if this is the collection limit error
513
+ // Handle both direct string throws and Error objects containing the message
514
+ const errorMessage = typeof error === "string"
515
+ ? error
516
+ : error instanceof Error
517
+ ? error.message
518
+ : String(error);
519
+ if (errorMessage === COLLECTION_LIMIT_MESSAGE ||
520
+ errorMessage.includes(COLLECTION_LIMIT_MESSAGE)) {
521
+ // Return the collection limit message as a successful response
522
+ // This ensures LLM treats it as final answer, not as retryable error
523
+ return {
524
+ content: [
525
+ {
526
+ type: "text",
527
+ text: COLLECTION_LIMIT_MESSAGE,
528
+ },
529
+ ],
530
+ };
531
+ }
532
+ return {
533
+ content: [
534
+ {
535
+ type: "text",
536
+ text: `Error searching code: ${errorMessage} Please check if the codebase has been indexed first.`,
537
+ },
538
+ ],
539
+ isError: true,
540
+ };
541
+ }
542
+ }
543
+ async handleClearIndex(args) {
544
+ const { path: codebasePath } = args;
545
+ if (this.snapshotManager.getIndexedCodebases().length === 0 &&
546
+ this.snapshotManager.getIndexingCodebases().length === 0) {
547
+ return {
548
+ content: [
549
+ {
550
+ type: "text",
551
+ text: "No codebases are currently indexed or being indexed.",
552
+ },
553
+ ],
554
+ };
555
+ }
556
+ if (!codebasePath) {
557
+ return {
558
+ content: [
559
+ {
560
+ type: "text",
561
+ text: "Error: Path is required for clearing index.",
562
+ },
563
+ ],
564
+ isError: true,
565
+ };
566
+ }
567
+ try {
568
+ // Force absolute path resolution - warn if relative path provided
569
+ const absolutePath = ensureAbsolutePath(codebasePath);
570
+ // Validate path exists
571
+ if (!fs.existsSync(absolutePath)) {
572
+ return {
573
+ content: [
574
+ {
575
+ type: "text",
576
+ text: `Error: Path '${absolutePath}' does not exist. Original input: '${codebasePath}'`,
577
+ },
578
+ ],
579
+ isError: true,
580
+ };
581
+ }
582
+ // Check if it's a directory
583
+ const stat = fs.statSync(absolutePath);
584
+ if (!stat.isDirectory()) {
585
+ return {
586
+ content: [
587
+ {
588
+ type: "text",
589
+ text: `Error: Path '${absolutePath}' is not a directory`,
590
+ },
591
+ ],
592
+ isError: true,
593
+ };
594
+ }
595
+ // Check if this codebase is indexed or being indexed
596
+ const isIndexed = this.snapshotManager
597
+ .getIndexedCodebases()
598
+ .includes(absolutePath);
599
+ const isIndexing = this.snapshotManager
600
+ .getIndexingCodebases()
601
+ .includes(absolutePath);
602
+ if (!isIndexed && !isIndexing) {
603
+ return {
604
+ content: [
605
+ {
606
+ type: "text",
607
+ text: `Error: Codebase '${absolutePath}' is not indexed or being indexed.`,
608
+ },
609
+ ],
610
+ isError: true,
611
+ };
612
+ }
613
+ console.log(`[CLEAR] Clearing codebase: ${absolutePath}`);
614
+ try {
615
+ await this.context.clearIndex(absolutePath);
616
+ console.log(`[CLEAR] Successfully cleared index for: ${absolutePath}`);
617
+ }
618
+ catch (error) {
619
+ const errorMsg = `Failed to clear ${absolutePath}: ${String(error)}`;
620
+ console.error(`[CLEAR] ${errorMsg}`);
621
+ return {
622
+ content: [
623
+ {
624
+ type: "text",
625
+ text: errorMsg,
626
+ },
627
+ ],
628
+ isError: true,
629
+ };
630
+ }
631
+ // Completely remove the cleared codebase from snapshot
632
+ this.snapshotManager.removeCodebaseCompletely(absolutePath);
633
+ // Reset indexing stats if this was the active codebase
634
+ this.indexingStats = null;
635
+ // Save snapshot after clearing index
636
+ this.snapshotManager.saveCodebaseSnapshot();
637
+ let resultText = `Successfully cleared codebase '${absolutePath}'`;
638
+ const remainingIndexed = this.snapshotManager.getIndexedCodebases().length;
639
+ const remainingIndexing = this.snapshotManager.getIndexingCodebases().length;
640
+ if (remainingIndexed > 0 || remainingIndexing > 0) {
641
+ resultText += `\n${remainingIndexed} other indexed codebase(s) and ${remainingIndexing} indexing codebase(s) remain`;
642
+ }
643
+ return {
644
+ content: [
645
+ {
646
+ type: "text",
647
+ text: resultText,
648
+ },
649
+ ],
650
+ };
651
+ }
652
+ catch (error) {
653
+ // Check if this is the collection limit error
654
+ // Handle both direct string throws and Error objects containing the message
655
+ const errorMessage = typeof error === "string"
656
+ ? error
657
+ : error instanceof Error
658
+ ? error.message
659
+ : String(error);
660
+ if (errorMessage === COLLECTION_LIMIT_MESSAGE ||
661
+ errorMessage.includes(COLLECTION_LIMIT_MESSAGE)) {
662
+ // Return the collection limit message as a successful response
663
+ // This ensures LLM treats it as final answer, not as retryable error
664
+ return {
665
+ content: [
666
+ {
667
+ type: "text",
668
+ text: COLLECTION_LIMIT_MESSAGE,
669
+ },
670
+ ],
671
+ };
672
+ }
673
+ return {
674
+ content: [
675
+ {
676
+ type: "text",
677
+ text: `Error clearing index: ${errorMessage}`,
678
+ },
679
+ ],
680
+ isError: true,
681
+ };
682
+ }
683
+ }
684
+ async handleGetIndexingStatus(args) {
685
+ const { path: codebasePath } = args;
686
+ try {
687
+ // Force absolute path resolution
688
+ const absolutePath = ensureAbsolutePath(codebasePath);
689
+ // Validate path exists
690
+ if (!fs.existsSync(absolutePath)) {
691
+ return {
692
+ content: [
693
+ {
694
+ type: "text",
695
+ text: `Error: Path '${absolutePath}' does not exist. Original input: '${codebasePath}'`,
696
+ },
697
+ ],
698
+ isError: true,
699
+ };
700
+ }
701
+ // Check if it's a directory
702
+ const stat = fs.statSync(absolutePath);
703
+ if (!stat.isDirectory()) {
704
+ return {
705
+ content: [
706
+ {
707
+ type: "text",
708
+ text: `Error: Path '${absolutePath}' is not a directory`,
709
+ },
710
+ ],
711
+ isError: true,
712
+ };
713
+ }
714
+ // Check indexing status using new status system
715
+ const status = this.snapshotManager.getCodebaseStatus(absolutePath);
716
+ const info = this.snapshotManager.getCodebaseInfo(absolutePath);
717
+ let statusMessage = "";
718
+ switch (status) {
719
+ case "indexed":
720
+ if (info && info.status === "indexed") {
721
+ statusMessage = `โœ… Codebase '${absolutePath}' is fully indexed and ready for search.`;
722
+ statusMessage += `\n๐Ÿ“Š Statistics: ${info.indexedFiles} files, ${info.totalChunks} chunks`;
723
+ statusMessage += `\n๐Ÿ“… Status: ${info.indexStatus}`;
724
+ statusMessage += `\n๐Ÿ• Last updated: ${new Date(info.lastUpdated).toLocaleString()}`;
725
+ }
726
+ else {
727
+ statusMessage = `โœ… Codebase '${absolutePath}' is fully indexed and ready for search.`;
728
+ }
729
+ break;
730
+ case "indexing":
731
+ if (info && info.status === "indexing") {
732
+ const progressPercentage = info.indexingPercentage || 0;
733
+ statusMessage = `๐Ÿ”„ Codebase '${absolutePath}' is currently being indexed. Progress: ${progressPercentage.toFixed(1)}%`;
734
+ // Add more detailed status based on progress
735
+ if (progressPercentage < 10) {
736
+ statusMessage += " (Preparing and scanning files...)";
737
+ }
738
+ else if (progressPercentage < 100) {
739
+ statusMessage +=
740
+ " (Processing files and generating embeddings...)";
741
+ }
742
+ statusMessage += `\n๐Ÿ• Last updated: ${new Date(info.lastUpdated).toLocaleString()}`;
743
+ }
744
+ else {
745
+ statusMessage = `๐Ÿ”„ Codebase '${absolutePath}' is currently being indexed.`;
746
+ }
747
+ break;
748
+ case "indexfailed":
749
+ if (info && info.status === "indexfailed") {
750
+ statusMessage = `โŒ Codebase '${absolutePath}' indexing failed.`;
751
+ statusMessage += `\n๐Ÿšจ Error: ${info.errorMessage}`;
752
+ if (info.lastAttemptedPercentage !== undefined) {
753
+ statusMessage += `\n๐Ÿ“Š Failed at: ${info.lastAttemptedPercentage.toFixed(1)}% progress`;
754
+ }
755
+ statusMessage += `\n๐Ÿ• Failed at: ${new Date(info.lastUpdated).toLocaleString()}`;
756
+ statusMessage += `\n๐Ÿ’ก You can retry indexing by running the index_codebase command again.`;
757
+ }
758
+ else {
759
+ statusMessage = `โŒ Codebase '${absolutePath}' indexing failed. You can retry indexing.`;
760
+ }
761
+ break;
762
+ case "not_found":
763
+ default:
764
+ statusMessage = `โŒ Codebase '${absolutePath}' is not indexed. Please use the index_codebase tool to index it first.`;
765
+ break;
766
+ }
767
+ const pathInfo = codebasePath !== absolutePath
768
+ ? `\nNote: Input path '${codebasePath}' was resolved to absolute path '${absolutePath}'`
769
+ : "";
770
+ return {
771
+ content: [
772
+ {
773
+ type: "text",
774
+ text: statusMessage + pathInfo,
775
+ },
776
+ ],
777
+ };
778
+ }
779
+ catch (error) {
780
+ return {
781
+ content: [
782
+ {
783
+ type: "text",
784
+ text: `Error getting indexing status: ${String(error)}`,
785
+ },
786
+ ],
787
+ isError: true,
788
+ };
789
+ }
790
+ }
791
+ }
792
+ //# sourceMappingURL=handlers.js.map