@pleaseai/context-please-mcp 0.1.0

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