@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.
- package/LICENSE +24 -0
- package/README.md +688 -0
- package/dist/config.d.ts +55 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +172 -0
- package/dist/config.js.map +1 -0
- package/dist/embedding.d.ts +5 -0
- package/dist/embedding.d.ts.map +1 -0
- package/dist/embedding.js +77 -0
- package/dist/embedding.js.map +1 -0
- package/dist/handlers.d.ts +74 -0
- package/dist/handlers.d.ts.map +1 -0
- package/dist/handlers.js +686 -0
- package/dist/handlers.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +280 -0
- package/dist/index.js.map +1 -0
- package/dist/snapshot.d.ts +95 -0
- package/dist/snapshot.d.ts.map +1 -0
- package/dist/snapshot.js +439 -0
- package/dist/snapshot.js.map +1 -0
- package/dist/sync.d.ts +11 -0
- package/dist/sync.d.ts.map +1 -0
- package/dist/sync.js +123 -0
- package/dist/sync.js.map +1 -0
- package/dist/utils.d.ts +10 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +27 -0
- package/dist/utils.js.map +1 -0
- package/package.json +42 -0
package/dist/handlers.js
ADDED
@@ -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
|