erosolar-cli 1.7.367 → 1.7.368

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/capabilities/learnCapability.d.ts +2 -0
  2. package/dist/capabilities/learnCapability.d.ts.map +1 -1
  3. package/dist/capabilities/learnCapability.js +9 -2
  4. package/dist/capabilities/learnCapability.js.map +1 -1
  5. package/dist/shell/interactiveShell.d.ts +2 -0
  6. package/dist/shell/interactiveShell.d.ts.map +1 -1
  7. package/dist/shell/interactiveShell.js +32 -15
  8. package/dist/shell/interactiveShell.js.map +1 -1
  9. package/dist/shell/terminalInput.d.ts +32 -20
  10. package/dist/shell/terminalInput.d.ts.map +1 -1
  11. package/dist/shell/terminalInput.js +203 -257
  12. package/dist/shell/terminalInput.js.map +1 -1
  13. package/dist/shell/terminalInputAdapter.d.ts.map +1 -1
  14. package/dist/shell/terminalInputAdapter.js +5 -2
  15. package/dist/shell/terminalInputAdapter.js.map +1 -1
  16. package/dist/subagents/taskRunner.d.ts.map +1 -1
  17. package/dist/subagents/taskRunner.js +7 -25
  18. package/dist/subagents/taskRunner.js.map +1 -1
  19. package/dist/tools/learnTools.js +127 -4
  20. package/dist/tools/learnTools.js.map +1 -1
  21. package/dist/tools/localExplore.d.ts +169 -0
  22. package/dist/tools/localExplore.d.ts.map +1 -0
  23. package/dist/tools/localExplore.js +1141 -0
  24. package/dist/tools/localExplore.js.map +1 -0
  25. package/dist/ui/ShellUIAdapter.d.ts +27 -0
  26. package/dist/ui/ShellUIAdapter.d.ts.map +1 -1
  27. package/dist/ui/ShellUIAdapter.js +175 -9
  28. package/dist/ui/ShellUIAdapter.js.map +1 -1
  29. package/dist/ui/compactRenderer.d.ts +139 -0
  30. package/dist/ui/compactRenderer.d.ts.map +1 -0
  31. package/dist/ui/compactRenderer.js +398 -0
  32. package/dist/ui/compactRenderer.js.map +1 -0
  33. package/dist/ui/inPlaceUpdater.d.ts +181 -0
  34. package/dist/ui/inPlaceUpdater.d.ts.map +1 -0
  35. package/dist/ui/inPlaceUpdater.js +515 -0
  36. package/dist/ui/inPlaceUpdater.js.map +1 -0
  37. package/dist/ui/theme.d.ts +108 -3
  38. package/dist/ui/theme.d.ts.map +1 -1
  39. package/dist/ui/theme.js +124 -3
  40. package/dist/ui/theme.js.map +1 -1
  41. package/dist/ui/toolDisplay.d.ts +44 -7
  42. package/dist/ui/toolDisplay.d.ts.map +1 -1
  43. package/dist/ui/toolDisplay.js +168 -84
  44. package/dist/ui/toolDisplay.js.map +1 -1
  45. package/dist/ui/unified/index.d.ts +11 -0
  46. package/dist/ui/unified/index.d.ts.map +1 -1
  47. package/dist/ui/unified/index.js +16 -0
  48. package/dist/ui/unified/index.js.map +1 -1
  49. package/dist/ui/unified/layout.d.ts.map +1 -1
  50. package/dist/ui/unified/layout.js +32 -47
  51. package/dist/ui/unified/layout.js.map +1 -1
  52. package/package.json +1 -1
@@ -0,0 +1,1141 @@
1
+ /**
2
+ * Local Explore Engine - Claude Code-style codebase exploration without API calls.
3
+ *
4
+ * This module implements an offline exploration system that mimics how Claude Code's
5
+ * explore agent works, but without requiring an Anthropic API key. It uses:
6
+ *
7
+ * 1. Codebase indexing - Build a searchable index of files, functions, classes, imports
8
+ * 2. Query processing - Parse natural language questions and map to search strategies
9
+ * 3. Smart search - Combine glob, grep, and structure analysis
10
+ * 4. Answer generation - Format results in a Claude Code-like way
11
+ * 5. Caching - Store the index for fast subsequent queries
12
+ */
13
+ import { readFileSync, existsSync, readdirSync, statSync, writeFileSync, mkdirSync } from 'node:fs';
14
+ import { join, relative, extname, basename, dirname } from 'node:path';
15
+ import { createHash } from 'node:crypto';
16
+ import { homedir } from 'node:os';
17
+ import { buildError } from '../core/errors.js';
18
+ // =====================================================
19
+ // Constants
20
+ // =====================================================
21
+ const INDEX_VERSION = 1;
22
+ const CACHE_DIR = join(homedir(), '.erosolar', 'explore-cache');
23
+ const IGNORED_DIRS = new Set([
24
+ 'node_modules', '.git', '.svn', '.hg', 'dist', 'build', 'out',
25
+ '.next', '.nuxt', '.output', 'coverage', '.nyc_output', '.cache',
26
+ '.turbo', '.vercel', '.netlify', '__pycache__', '.pytest_cache',
27
+ '.mypy_cache', '.ruff_cache', 'venv', '.venv', 'env', '.env',
28
+ 'target', 'vendor', '.idea', '.vscode',
29
+ ]);
30
+ const LANGUAGE_MAP = {
31
+ '.ts': 'TypeScript', '.tsx': 'TypeScript React', '.js': 'JavaScript',
32
+ '.jsx': 'JavaScript React', '.mjs': 'JavaScript', '.cjs': 'JavaScript',
33
+ '.py': 'Python', '.rs': 'Rust', '.go': 'Go', '.java': 'Java',
34
+ '.kt': 'Kotlin', '.rb': 'Ruby', '.php': 'PHP', '.cs': 'C#',
35
+ '.cpp': 'C++', '.c': 'C', '.h': 'C/C++ Header', '.swift': 'Swift',
36
+ '.vue': 'Vue', '.svelte': 'Svelte', '.md': 'Markdown',
37
+ '.json': 'JSON', '.yaml': 'YAML', '.yml': 'YAML', '.toml': 'TOML',
38
+ };
39
+ // Query type detection patterns
40
+ const QUERY_PATTERNS = [
41
+ { pattern: /where\s+(?:is|are|can\s+i\s+find)\s+.*(file|module|component)/i, type: 'find_file' },
42
+ { pattern: /find\s+(?:the\s+)?(?:file|module|component)/i, type: 'find_file' },
43
+ { pattern: /which\s+file/i, type: 'find_file' },
44
+ { pattern: /where\s+(?:is|are)\s+(?:the\s+)?(\w+)\s+(?:defined|declared|implemented)/i, type: 'find_symbol' },
45
+ { pattern: /find\s+(?:the\s+)?(?:function|class|type|interface)\s+/i, type: 'find_symbol' },
46
+ { pattern: /definition\s+of/i, type: 'find_symbol' },
47
+ { pattern: /where\s+(?:is\s+)?(\w+)\s+used/i, type: 'find_usage' },
48
+ { pattern: /who\s+(?:uses|calls|imports)/i, type: 'find_usage' },
49
+ { pattern: /usages?\s+of/i, type: 'find_usage' },
50
+ { pattern: /how\s+(?:does|do|is)/i, type: 'understand' },
51
+ { pattern: /explain\s+(?:the\s+)?(?:how|what)/i, type: 'explain' },
52
+ { pattern: /what\s+(?:is|are|does)/i, type: 'explain' },
53
+ { pattern: /architecture|structure|organization|layout/i, type: 'architecture' },
54
+ { pattern: /dependencies|imports|relationships/i, type: 'dependencies' },
55
+ { pattern: /./, type: 'search' }, // Default fallback
56
+ ];
57
+ // =====================================================
58
+ // Local Explore Engine
59
+ // =====================================================
60
+ export class LocalExploreEngine {
61
+ workingDir;
62
+ index = null;
63
+ indexPath;
64
+ constructor(workingDir) {
65
+ this.workingDir = workingDir;
66
+ const hash = createHash('md5').update(workingDir).digest('hex').slice(0, 12);
67
+ this.indexPath = join(CACHE_DIR, `index-${hash}.json`);
68
+ }
69
+ /**
70
+ * Initialize or load the codebase index.
71
+ * Returns true if a fresh index was built.
72
+ */
73
+ async initialize(forceRebuild = false) {
74
+ if (!forceRebuild && this.tryLoadCache()) {
75
+ return { rebuilt: false, fileCount: this.index.files.length };
76
+ }
77
+ this.index = await this.buildIndex();
78
+ this.saveCache();
79
+ return { rebuilt: true, fileCount: this.index.files.length };
80
+ }
81
+ /**
82
+ * Process a natural language query and return exploration results.
83
+ */
84
+ async explore(query) {
85
+ const startTime = Date.now();
86
+ if (!this.index) {
87
+ await this.initialize();
88
+ }
89
+ const parsedQuery = this.parseQuery(query);
90
+ const result = await this.executeQuery(parsedQuery);
91
+ result.timeTaken = Date.now() - startTime;
92
+ return result;
93
+ }
94
+ /**
95
+ * Get a quick overview of the codebase.
96
+ */
97
+ getOverview() {
98
+ if (!this.index) {
99
+ return 'Index not loaded. Call initialize() first.';
100
+ }
101
+ const idx = this.index;
102
+ const lines = [
103
+ `# Codebase Overview: ${basename(idx.rootDir)}`,
104
+ '',
105
+ `**Files indexed:** ${idx.files.length}`,
106
+ `**Last indexed:** ${idx.createdAt}`,
107
+ '',
108
+ '## Quick Access',
109
+ '',
110
+ ];
111
+ if (idx.quickLookup.entryPoints.length) {
112
+ lines.push(`**Entry Points:** ${idx.quickLookup.entryPoints.slice(0, 5).join(', ')}`);
113
+ }
114
+ if (idx.quickLookup.configFiles.length) {
115
+ lines.push(`**Config Files:** ${idx.quickLookup.configFiles.slice(0, 5).join(', ')}`);
116
+ }
117
+ if (idx.quickLookup.testFiles.length) {
118
+ lines.push(`**Test Files:** ${idx.quickLookup.testFiles.length} files`);
119
+ }
120
+ lines.push('', '## Architecture');
121
+ if (idx.patterns.architecture.length) {
122
+ lines.push(`**Detected:** ${idx.patterns.architecture.join(', ')}`);
123
+ }
124
+ if (idx.patterns.designPatterns.length) {
125
+ lines.push(`**Patterns:** ${idx.patterns.designPatterns.slice(0, 5).join(', ')}`);
126
+ }
127
+ return lines.join('\n');
128
+ }
129
+ // =====================================================
130
+ // Private Methods
131
+ // =====================================================
132
+ tryLoadCache() {
133
+ try {
134
+ if (!existsSync(this.indexPath)) {
135
+ return false;
136
+ }
137
+ const content = readFileSync(this.indexPath, 'utf-8');
138
+ const cached = JSON.parse(content);
139
+ // Check version and hash
140
+ if (cached.version !== INDEX_VERSION) {
141
+ return false;
142
+ }
143
+ // Quick validation: check if file count roughly matches
144
+ const currentFileCount = this.countFiles(this.workingDir, 0, 4);
145
+ const cachedFileCount = cached.files.length;
146
+ const drift = Math.abs(currentFileCount - cachedFileCount);
147
+ // Allow 10% drift before rebuilding
148
+ if (drift > cachedFileCount * 0.1 && drift > 10) {
149
+ return false;
150
+ }
151
+ // Restore Map objects from JSON
152
+ this.index = {
153
+ ...cached,
154
+ symbols: {
155
+ byName: new Map(Object.entries(cached.symbols.byName || {})),
156
+ byKind: new Map(Object.entries(cached.symbols.byKind || {})),
157
+ byFile: new Map(Object.entries(cached.symbols.byFile || {})),
158
+ },
159
+ imports: {
160
+ imports: new Map(Object.entries(cached.imports.imports || {})),
161
+ importedBy: new Map(Object.entries(cached.imports.importedBy || {})),
162
+ },
163
+ };
164
+ return true;
165
+ }
166
+ catch {
167
+ return false;
168
+ }
169
+ }
170
+ saveCache() {
171
+ try {
172
+ mkdirSync(CACHE_DIR, { recursive: true });
173
+ // Convert Maps to objects for JSON serialization
174
+ const serializable = {
175
+ ...this.index,
176
+ symbols: {
177
+ byName: Object.fromEntries(this.index.symbols.byName),
178
+ byKind: Object.fromEntries(this.index.symbols.byKind),
179
+ byFile: Object.fromEntries(this.index.symbols.byFile),
180
+ },
181
+ imports: {
182
+ imports: Object.fromEntries(this.index.imports.imports),
183
+ importedBy: Object.fromEntries(this.index.imports.importedBy),
184
+ },
185
+ };
186
+ writeFileSync(this.indexPath, JSON.stringify(serializable, null, 2));
187
+ }
188
+ catch {
189
+ // Ignore cache save errors
190
+ }
191
+ }
192
+ countFiles(dir, depth, maxDepth) {
193
+ if (depth >= maxDepth)
194
+ return 0;
195
+ let count = 0;
196
+ try {
197
+ const entries = readdirSync(dir, { withFileTypes: true });
198
+ for (const entry of entries) {
199
+ if (entry.name.startsWith('.') || IGNORED_DIRS.has(entry.name))
200
+ continue;
201
+ if (entry.isDirectory()) {
202
+ count += this.countFiles(join(dir, entry.name), depth + 1, maxDepth);
203
+ }
204
+ else if (entry.isFile() && LANGUAGE_MAP[extname(entry.name).toLowerCase()]) {
205
+ count++;
206
+ }
207
+ }
208
+ }
209
+ catch {
210
+ // Ignore errors
211
+ }
212
+ return count;
213
+ }
214
+ async buildIndex() {
215
+ const files = [];
216
+ const symbolsByName = new Map();
217
+ const symbolsByKind = new Map();
218
+ const symbolsByFile = new Map();
219
+ const importsMap = new Map();
220
+ const importedByMap = new Map();
221
+ const quickLookup = {
222
+ entryPoints: [],
223
+ configFiles: [],
224
+ testFiles: [],
225
+ componentFiles: [],
226
+ typeFiles: [],
227
+ utilityFiles: [],
228
+ routeFiles: [],
229
+ modelFiles: [],
230
+ };
231
+ const patterns = {
232
+ architecture: [],
233
+ designPatterns: [],
234
+ namingConventions: [],
235
+ testPatterns: [],
236
+ componentPatterns: [],
237
+ };
238
+ // Collect all files
239
+ const allFiles = this.collectFiles(this.workingDir, 0, 6);
240
+ // Index each file
241
+ for (const filePath of allFiles) {
242
+ try {
243
+ const indexed = this.indexFile(filePath);
244
+ if (!indexed)
245
+ continue;
246
+ files.push(indexed);
247
+ // Build symbol index
248
+ const relPath = indexed.path;
249
+ const fileSymbols = [];
250
+ for (const sym of [...indexed.symbols.functions, ...indexed.symbols.classes,
251
+ ...indexed.symbols.interfaces, ...indexed.symbols.types]) {
252
+ const location = {
253
+ file: relPath,
254
+ line: sym.line,
255
+ kind: sym.kind,
256
+ signature: sym.signature,
257
+ };
258
+ // By name
259
+ const existing = symbolsByName.get(sym.name.toLowerCase()) || [];
260
+ existing.push(location);
261
+ symbolsByName.set(sym.name.toLowerCase(), existing);
262
+ // By kind
263
+ const byKind = symbolsByKind.get(sym.kind) || [];
264
+ byKind.push(location);
265
+ symbolsByKind.set(sym.kind, byKind);
266
+ fileSymbols.push(sym.name);
267
+ }
268
+ symbolsByFile.set(relPath, fileSymbols);
269
+ // Build import graph
270
+ importsMap.set(relPath, indexed.imports);
271
+ for (const imp of indexed.imports) {
272
+ const existing = importedByMap.get(imp) || [];
273
+ existing.push(relPath);
274
+ importedByMap.set(imp, existing);
275
+ }
276
+ // Categorize files
277
+ this.categorizeFile(indexed, quickLookup);
278
+ }
279
+ catch {
280
+ // Skip files we can't index
281
+ }
282
+ }
283
+ // Detect patterns
284
+ this.detectPatterns(files, patterns);
285
+ // Create hash for cache validation
286
+ const hash = createHash('md5')
287
+ .update(files.map(f => f.path).sort().join('\n'))
288
+ .digest('hex');
289
+ return {
290
+ version: INDEX_VERSION,
291
+ createdAt: new Date().toISOString(),
292
+ rootDir: this.workingDir,
293
+ hash,
294
+ files,
295
+ symbols: {
296
+ byName: symbolsByName,
297
+ byKind: symbolsByKind,
298
+ byFile: symbolsByFile,
299
+ },
300
+ imports: {
301
+ imports: importsMap,
302
+ importedBy: importedByMap,
303
+ },
304
+ patterns,
305
+ quickLookup,
306
+ };
307
+ }
308
+ collectFiles(dir, depth, maxDepth) {
309
+ if (depth >= maxDepth)
310
+ return [];
311
+ const files = [];
312
+ try {
313
+ const entries = readdirSync(dir, { withFileTypes: true });
314
+ for (const entry of entries) {
315
+ if (entry.name.startsWith('.') || IGNORED_DIRS.has(entry.name))
316
+ continue;
317
+ const fullPath = join(dir, entry.name);
318
+ if (entry.isDirectory()) {
319
+ files.push(...this.collectFiles(fullPath, depth + 1, maxDepth));
320
+ }
321
+ else if (entry.isFile()) {
322
+ const ext = extname(entry.name).toLowerCase();
323
+ if (LANGUAGE_MAP[ext]) {
324
+ files.push(fullPath);
325
+ }
326
+ }
327
+ }
328
+ }
329
+ catch {
330
+ // Ignore errors
331
+ }
332
+ return files;
333
+ }
334
+ indexFile(filePath) {
335
+ try {
336
+ const stat = statSync(filePath);
337
+ if (stat.size > 500000)
338
+ return null; // Skip large files
339
+ const content = readFileSync(filePath, 'utf-8');
340
+ const lines = content.split('\n');
341
+ const ext = extname(filePath).toLowerCase();
342
+ const language = LANGUAGE_MAP[ext] || 'Unknown';
343
+ const relPath = relative(this.workingDir, filePath);
344
+ const symbols = this.extractSymbols(content, ext);
345
+ const imports = this.extractImports(content, ext);
346
+ const exports = this.extractExports(content, ext);
347
+ const purpose = this.inferPurpose(basename(filePath), content, symbols);
348
+ const tags = this.generateTags(relPath, content, symbols, purpose);
349
+ return {
350
+ path: relPath,
351
+ language,
352
+ size: stat.size,
353
+ lineCount: lines.length,
354
+ lastModified: stat.mtimeMs,
355
+ symbols,
356
+ imports,
357
+ exports,
358
+ purpose,
359
+ tags,
360
+ };
361
+ }
362
+ catch {
363
+ return null;
364
+ }
365
+ }
366
+ extractSymbols(content, ext) {
367
+ const symbols = {
368
+ functions: [],
369
+ classes: [],
370
+ interfaces: [],
371
+ types: [],
372
+ constants: [],
373
+ variables: [],
374
+ };
375
+ if (!['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'].includes(ext)) {
376
+ return symbols; // Only analyze JS/TS for now
377
+ }
378
+ const lines = content.split('\n');
379
+ // Extract functions
380
+ const funcRegex = /(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/g;
381
+ let match;
382
+ while ((match = funcRegex.exec(content)) !== null) {
383
+ const line = content.substring(0, match.index).split('\n').length;
384
+ symbols.functions.push({
385
+ name: match[1] || '',
386
+ line,
387
+ kind: 'function',
388
+ isExported: content.substring(match.index - 20, match.index).includes('export'),
389
+ signature: `${match[1]}(${match[2]})`,
390
+ });
391
+ }
392
+ // Arrow functions
393
+ const arrowRegex = /(?:export\s+)?(?:const|let)\s+(\w+)\s*=\s*(?:async\s+)?\(?([^)=]*)\)?\s*=>/g;
394
+ while ((match = arrowRegex.exec(content)) !== null) {
395
+ const line = content.substring(0, match.index).split('\n').length;
396
+ symbols.functions.push({
397
+ name: match[1] || '',
398
+ line,
399
+ kind: 'function',
400
+ isExported: content.substring(match.index - 20, match.index).includes('export'),
401
+ signature: `${match[1]}(${match[2] || ''})`,
402
+ });
403
+ }
404
+ // Classes
405
+ const classRegex = /(?:export\s+)?class\s+(\w+)(?:\s+extends\s+(\w+))?/g;
406
+ while ((match = classRegex.exec(content)) !== null) {
407
+ const line = content.substring(0, match.index).split('\n').length;
408
+ symbols.classes.push({
409
+ name: match[1] || '',
410
+ line,
411
+ kind: 'class',
412
+ isExported: content.substring(match.index - 20, match.index).includes('export'),
413
+ signature: match[2] ? `class ${match[1]} extends ${match[2]}` : `class ${match[1]}`,
414
+ });
415
+ }
416
+ // Interfaces
417
+ const interfaceRegex = /(?:export\s+)?interface\s+(\w+)(?:\s+extends\s+([^{]+))?/g;
418
+ while ((match = interfaceRegex.exec(content)) !== null) {
419
+ const line = content.substring(0, match.index).split('\n').length;
420
+ symbols.interfaces.push({
421
+ name: match[1] || '',
422
+ line,
423
+ kind: 'interface',
424
+ isExported: content.substring(match.index - 20, match.index).includes('export'),
425
+ signature: `interface ${match[1]}`,
426
+ });
427
+ }
428
+ // Types
429
+ const typeRegex = /(?:export\s+)?type\s+(\w+)\s*=/g;
430
+ while ((match = typeRegex.exec(content)) !== null) {
431
+ const line = content.substring(0, match.index).split('\n').length;
432
+ symbols.types.push({
433
+ name: match[1] || '',
434
+ line,
435
+ kind: 'type',
436
+ isExported: content.substring(match.index - 20, match.index).includes('export'),
437
+ signature: `type ${match[1]}`,
438
+ });
439
+ }
440
+ // Constants
441
+ const constRegex = /(?:export\s+)?const\s+([A-Z][A-Z_0-9]+)\s*=/g;
442
+ while ((match = constRegex.exec(content)) !== null) {
443
+ const line = content.substring(0, match.index).split('\n').length;
444
+ symbols.constants.push({
445
+ name: match[1] || '',
446
+ line,
447
+ kind: 'const',
448
+ isExported: content.substring(match.index - 20, match.index).includes('export'),
449
+ });
450
+ }
451
+ return symbols;
452
+ }
453
+ extractImports(content, ext) {
454
+ const imports = [];
455
+ if (!['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'].includes(ext)) {
456
+ return imports;
457
+ }
458
+ // ES6 imports
459
+ const importRegex = /import\s+.*?\s+from\s+['"]([^'"]+)['"]/g;
460
+ let match;
461
+ while ((match = importRegex.exec(content)) !== null) {
462
+ const source = match[1] || '';
463
+ if (source.startsWith('.')) {
464
+ // Resolve relative imports
465
+ imports.push(source);
466
+ }
467
+ }
468
+ return imports;
469
+ }
470
+ extractExports(content, ext) {
471
+ const exports = [];
472
+ if (!['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'].includes(ext)) {
473
+ return exports;
474
+ }
475
+ // Named exports
476
+ const namedExportRegex = /export\s+(?:const|let|var|function|class|interface|type|enum)\s+(\w+)/g;
477
+ let match;
478
+ while ((match = namedExportRegex.exec(content)) !== null) {
479
+ if (match[1])
480
+ exports.push(match[1]);
481
+ }
482
+ // Default export
483
+ if (/export\s+default/.test(content)) {
484
+ exports.push('default');
485
+ }
486
+ return exports;
487
+ }
488
+ inferPurpose(filename, content, symbols) {
489
+ const lower = filename.toLowerCase();
490
+ if (lower.includes('test') || lower.includes('spec'))
491
+ return 'Test file';
492
+ if (lower === 'index.ts' || lower === 'index.js')
493
+ return 'Module entry point';
494
+ if (lower.includes('config'))
495
+ return 'Configuration';
496
+ if (lower.includes('type') || lower.includes('interface'))
497
+ return 'Type definitions';
498
+ if (lower.includes('util') || lower.includes('helper'))
499
+ return 'Utilities';
500
+ if (lower.includes('hook'))
501
+ return 'React hooks';
502
+ if (lower.includes('context'))
503
+ return 'Context provider';
504
+ if (lower.includes('store') || lower.includes('reducer'))
505
+ return 'State management';
506
+ if (lower.includes('service'))
507
+ return 'Business logic';
508
+ if (lower.includes('api') || lower.includes('client'))
509
+ return 'API client';
510
+ if (lower.includes('route'))
511
+ return 'Routes';
512
+ if (lower.includes('middleware'))
513
+ return 'Middleware';
514
+ if (lower.includes('model') || lower.includes('entity'))
515
+ return 'Data models';
516
+ if (lower.includes('schema'))
517
+ return 'Schema definitions';
518
+ if (lower.includes('component'))
519
+ return 'UI component';
520
+ if (symbols.classes.length > 0)
521
+ return `Class: ${symbols.classes[0]?.name}`;
522
+ if (symbols.interfaces.length > 0)
523
+ return `Types/Interfaces`;
524
+ if (symbols.functions.length > 0)
525
+ return `Functions: ${symbols.functions.slice(0, 3).map(f => f.name).join(', ')}`;
526
+ return 'General module';
527
+ }
528
+ generateTags(path, content, symbols, purpose) {
529
+ const tags = [];
530
+ // Add purpose-based tags
531
+ tags.push(purpose.toLowerCase().split(':')[0] || '');
532
+ // Add path-based tags
533
+ const parts = path.split('/');
534
+ for (const part of parts.slice(0, -1)) {
535
+ if (part && !part.startsWith('.')) {
536
+ tags.push(part.toLowerCase());
537
+ }
538
+ }
539
+ // Add symbol-based tags
540
+ for (const sym of symbols.functions) {
541
+ if (sym.isExported)
542
+ tags.push(sym.name.toLowerCase());
543
+ }
544
+ for (const sym of symbols.classes) {
545
+ if (sym.isExported)
546
+ tags.push(sym.name.toLowerCase());
547
+ }
548
+ // Add content-based tags
549
+ if (/useEffect|useState|useCallback/.test(content))
550
+ tags.push('react', 'hooks');
551
+ if (/async\s+function|await\s+/.test(content))
552
+ tags.push('async');
553
+ if (/express|fastify|koa/.test(content))
554
+ tags.push('server', 'http');
555
+ if (/import.*prisma|import.*mongoose/.test(content))
556
+ tags.push('database');
557
+ if (/import.*zod|import.*yup|import.*joi/.test(content))
558
+ tags.push('validation');
559
+ if (/describe\s*\(|it\s*\(|test\s*\(/.test(content))
560
+ tags.push('test');
561
+ return [...new Set(tags.filter(Boolean))];
562
+ }
563
+ categorizeFile(file, lookup) {
564
+ const lower = file.path.toLowerCase();
565
+ const name = basename(file.path).toLowerCase();
566
+ // Entry points
567
+ if (['index.ts', 'index.js', 'main.ts', 'main.js', 'app.ts', 'app.js'].includes(name)) {
568
+ lookup.entryPoints.push(file.path);
569
+ }
570
+ // Config files
571
+ if (name.includes('config') || name.includes('settings') || name.includes('.rc')) {
572
+ lookup.configFiles.push(file.path);
573
+ }
574
+ // Test files
575
+ if (lower.includes('test') || lower.includes('spec') || lower.includes('__tests__')) {
576
+ lookup.testFiles.push(file.path);
577
+ }
578
+ // Component files
579
+ if (lower.includes('component') || lower.includes('components/') ||
580
+ file.language.includes('React')) {
581
+ lookup.componentFiles.push(file.path);
582
+ }
583
+ // Type files
584
+ if (lower.includes('type') || lower.includes('interface') || name.endsWith('.d.ts')) {
585
+ lookup.typeFiles.push(file.path);
586
+ }
587
+ // Utility files
588
+ if (lower.includes('util') || lower.includes('helper') || lower.includes('lib/')) {
589
+ lookup.utilityFiles.push(file.path);
590
+ }
591
+ // Route files
592
+ if (lower.includes('route') || lower.includes('api/')) {
593
+ lookup.routeFiles.push(file.path);
594
+ }
595
+ // Model files
596
+ if (lower.includes('model') || lower.includes('entity') || lower.includes('schema')) {
597
+ lookup.modelFiles.push(file.path);
598
+ }
599
+ }
600
+ detectPatterns(files, patterns) {
601
+ const dirs = new Set();
602
+ for (const file of files) {
603
+ const dir = dirname(file.path);
604
+ dirs.add(dir.split('/')[0] || dir);
605
+ }
606
+ // Detect architecture patterns
607
+ if (dirs.has('src') && (dirs.has('components') || dirs.has('views'))) {
608
+ patterns.architecture.push('Component-based');
609
+ }
610
+ if (dirs.has('api') || dirs.has('routes')) {
611
+ patterns.architecture.push('API-driven');
612
+ }
613
+ if (dirs.has('services') && dirs.has('controllers')) {
614
+ patterns.architecture.push('Layered');
615
+ }
616
+ if (dirs.has('domain') || dirs.has('entities')) {
617
+ patterns.architecture.push('Domain-driven');
618
+ }
619
+ // Detect design patterns
620
+ const allContent = files.slice(0, 50).map(f => {
621
+ try {
622
+ return readFileSync(join(this.workingDir, f.path), 'utf-8');
623
+ }
624
+ catch {
625
+ return '';
626
+ }
627
+ }).join('\n');
628
+ if (/singleton|getInstance/i.test(allContent))
629
+ patterns.designPatterns.push('Singleton');
630
+ if (/factory|create\w+/i.test(allContent))
631
+ patterns.designPatterns.push('Factory');
632
+ if (/observer|subscribe|emit/i.test(allContent))
633
+ patterns.designPatterns.push('Observer');
634
+ if (/strategy|\bstrategy\b/i.test(allContent))
635
+ patterns.designPatterns.push('Strategy');
636
+ if (/decorator|@\w+/i.test(allContent))
637
+ patterns.designPatterns.push('Decorator');
638
+ // Detect naming conventions
639
+ const fileNames = files.map(f => basename(f.path, extname(f.path)));
640
+ if (fileNames.some(n => /[a-z]+-[a-z]+/.test(n)))
641
+ patterns.namingConventions.push('kebab-case');
642
+ if (fileNames.some(n => /[a-z]+[A-Z]/.test(n)))
643
+ patterns.namingConventions.push('camelCase');
644
+ if (fileNames.some(n => /^[A-Z][a-z]+[A-Z]/.test(n)))
645
+ patterns.namingConventions.push('PascalCase');
646
+ if (fileNames.some(n => /[a-z]+_[a-z]+/.test(n)))
647
+ patterns.namingConventions.push('snake_case');
648
+ }
649
+ parseQuery(query) {
650
+ const normalized = query.trim().toLowerCase();
651
+ let type = 'search';
652
+ for (const { pattern, type: queryType } of QUERY_PATTERNS) {
653
+ if (pattern.test(query)) {
654
+ type = queryType;
655
+ break;
656
+ }
657
+ }
658
+ // Extract keywords
659
+ const stopWords = new Set(['the', 'a', 'an', 'is', 'are', 'where', 'what', 'how', 'which', 'find', 'show', 'get', 'me', 'i', 'can', 'do', 'does', 'for', 'to', 'in', 'of']);
660
+ const keywords = normalized
661
+ .replace(/[^\w\s]/g, ' ')
662
+ .split(/\s+/)
663
+ .filter(w => w.length > 2 && !stopWords.has(w));
664
+ // Extract filters from query
665
+ const filters = { limit: 10 };
666
+ // File pattern filter
667
+ const fileMatch = query.match(/(?:in|from|file)\s+([^\s]+\.[a-z]+)/i);
668
+ if (fileMatch) {
669
+ filters.filePattern = fileMatch[1];
670
+ }
671
+ // Language filter
672
+ const langMatch = query.match(/(typescript|javascript|python|rust|go)/i);
673
+ if (langMatch) {
674
+ filters.language = langMatch[1];
675
+ }
676
+ // Symbol kind filter
677
+ const kindMatch = query.match(/(function|class|type|interface|constant)/i);
678
+ if (kindMatch && kindMatch[1]) {
679
+ filters.symbolKind = kindMatch[1].toLowerCase();
680
+ }
681
+ return {
682
+ type,
683
+ keywords,
684
+ filters,
685
+ originalQuery: query,
686
+ };
687
+ }
688
+ async executeQuery(query) {
689
+ const result = {
690
+ query: query.originalQuery,
691
+ queryType: query.type,
692
+ files: [],
693
+ symbols: [],
694
+ answer: '',
695
+ suggestions: [],
696
+ timeTaken: 0,
697
+ };
698
+ switch (query.type) {
699
+ case 'find_file':
700
+ this.findFiles(query, result);
701
+ break;
702
+ case 'find_symbol':
703
+ this.findSymbols(query, result);
704
+ break;
705
+ case 'find_usage':
706
+ this.findUsages(query, result);
707
+ break;
708
+ case 'architecture':
709
+ this.explainArchitecture(query, result);
710
+ break;
711
+ case 'dependencies':
712
+ this.showDependencies(query, result);
713
+ break;
714
+ case 'understand':
715
+ case 'explain':
716
+ this.explainConcept(query, result);
717
+ break;
718
+ case 'search':
719
+ default:
720
+ this.generalSearch(query, result);
721
+ break;
722
+ }
723
+ // Generate suggestions
724
+ this.generateSuggestions(query, result);
725
+ return result;
726
+ }
727
+ findFiles(query, result) {
728
+ const matches = [];
729
+ for (const file of this.index.files) {
730
+ let relevance = 0;
731
+ for (const keyword of query.keywords) {
732
+ const keywordLower = keyword.toLowerCase();
733
+ // Match in path
734
+ if (file.path.toLowerCase().includes(keywordLower)) {
735
+ relevance += 10;
736
+ }
737
+ // Match in tags
738
+ if (file.tags.some(t => t.includes(keywordLower))) {
739
+ relevance += 5;
740
+ }
741
+ // Match in purpose
742
+ if (file.purpose.toLowerCase().includes(keywordLower)) {
743
+ relevance += 3;
744
+ }
745
+ // Match symbol names
746
+ const allSymbols = [...file.symbols.functions, ...file.symbols.classes];
747
+ if (allSymbols.some(s => s.name.toLowerCase().includes(keywordLower))) {
748
+ relevance += 7;
749
+ }
750
+ }
751
+ // Apply filters
752
+ if (query.filters.filePattern && !file.path.includes(query.filters.filePattern)) {
753
+ relevance = 0;
754
+ }
755
+ if (query.filters.language && !file.language.toLowerCase().includes(query.filters.language.toLowerCase())) {
756
+ relevance = 0;
757
+ }
758
+ if (relevance > 0) {
759
+ matches.push({
760
+ path: file.path,
761
+ relevance,
762
+ reason: file.purpose,
763
+ });
764
+ }
765
+ }
766
+ // Sort by relevance and limit
767
+ matches.sort((a, b) => b.relevance - a.relevance);
768
+ result.files = matches.slice(0, query.filters.limit || 10);
769
+ // Generate answer
770
+ if (result.files.length > 0) {
771
+ const topFiles = result.files.slice(0, 5);
772
+ result.answer = [
773
+ `Found ${result.files.length} relevant file(s) for "${query.keywords.join(' ')}":`,
774
+ '',
775
+ ...topFiles.map(f => `• **${f.path}** - ${f.reason}`),
776
+ result.files.length > 5 ? `\n...and ${result.files.length - 5} more files` : '',
777
+ ].join('\n');
778
+ }
779
+ else {
780
+ result.answer = `No files found matching "${query.keywords.join(' ')}". Try different keywords or check the spelling.`;
781
+ }
782
+ }
783
+ findSymbols(query, result) {
784
+ const matches = [];
785
+ for (const keyword of query.keywords) {
786
+ const keywordLower = keyword.toLowerCase();
787
+ const locations = this.index.symbols.byName.get(keywordLower) || [];
788
+ for (const loc of locations) {
789
+ // Check kind filter
790
+ if (query.filters.symbolKind && loc.kind !== query.filters.symbolKind) {
791
+ continue;
792
+ }
793
+ matches.push({
794
+ name: keyword,
795
+ file: loc.file,
796
+ line: loc.line,
797
+ kind: loc.kind,
798
+ relevance: 10,
799
+ });
800
+ }
801
+ }
802
+ // Also search partial matches
803
+ for (const [name, locations] of this.index.symbols.byName) {
804
+ for (const keyword of query.keywords) {
805
+ if (name.includes(keyword.toLowerCase()) && !matches.some(m => m.name === name)) {
806
+ for (const loc of locations) {
807
+ if (query.filters.symbolKind && loc.kind !== query.filters.symbolKind)
808
+ continue;
809
+ matches.push({
810
+ name,
811
+ file: loc.file,
812
+ line: loc.line,
813
+ kind: loc.kind,
814
+ relevance: 5,
815
+ });
816
+ }
817
+ }
818
+ }
819
+ }
820
+ matches.sort((a, b) => b.relevance - a.relevance);
821
+ result.symbols = matches.slice(0, query.filters.limit || 10);
822
+ // Generate answer
823
+ if (result.symbols.length > 0) {
824
+ const topSymbols = result.symbols.slice(0, 5);
825
+ result.answer = [
826
+ `Found ${result.symbols.length} symbol(s) matching "${query.keywords.join(' ')}":`,
827
+ '',
828
+ ...topSymbols.map(s => `• **${s.name}** (${s.kind}) at ${s.file}:${s.line}`),
829
+ result.symbols.length > 5 ? `\n...and ${result.symbols.length - 5} more symbols` : '',
830
+ ].join('\n');
831
+ }
832
+ else {
833
+ result.answer = `No symbols found matching "${query.keywords.join(' ')}".`;
834
+ }
835
+ }
836
+ findUsages(query, result) {
837
+ for (const keyword of query.keywords) {
838
+ const keywordLower = keyword.toLowerCase();
839
+ // Find files that import this symbol
840
+ for (const file of this.index.files) {
841
+ // Check if file content mentions the keyword
842
+ const fullPath = join(this.workingDir, file.path);
843
+ try {
844
+ const content = readFileSync(fullPath, 'utf-8');
845
+ if (content.toLowerCase().includes(keywordLower)) {
846
+ const lines = content.split('\n');
847
+ const matchingLines = lines
848
+ .map((line, i) => ({ line, num: i + 1 }))
849
+ .filter(({ line }) => line.toLowerCase().includes(keywordLower))
850
+ .slice(0, 3);
851
+ result.files.push({
852
+ path: file.path,
853
+ relevance: matchingLines.length * 3,
854
+ reason: `Used on line${matchingLines.length > 1 ? 's' : ''} ${matchingLines.map(l => l.num).join(', ')}`,
855
+ snippets: matchingLines.map(l => l.line.trim()),
856
+ });
857
+ }
858
+ }
859
+ catch {
860
+ // Skip unreadable files
861
+ }
862
+ }
863
+ }
864
+ result.files.sort((a, b) => b.relevance - a.relevance);
865
+ result.files = result.files.slice(0, query.filters.limit || 10);
866
+ if (result.files.length > 0) {
867
+ result.answer = [
868
+ `Found ${result.files.length} file(s) using "${query.keywords.join(' ')}":`,
869
+ '',
870
+ ...result.files.slice(0, 5).map(f => `• **${f.path}** - ${f.reason}`),
871
+ ].join('\n');
872
+ }
873
+ else {
874
+ result.answer = `No usages found for "${query.keywords.join(' ')}".`;
875
+ }
876
+ }
877
+ explainArchitecture(_query, result) {
878
+ const patterns = this.index.patterns;
879
+ const lookup = this.index.quickLookup;
880
+ const lines = [
881
+ '# Architecture Overview',
882
+ '',
883
+ ];
884
+ if (patterns.architecture.length) {
885
+ lines.push(`**Architectural Style:** ${patterns.architecture.join(', ')}`);
886
+ lines.push('');
887
+ }
888
+ if (patterns.designPatterns.length) {
889
+ lines.push(`**Design Patterns:** ${patterns.designPatterns.join(', ')}`);
890
+ lines.push('');
891
+ }
892
+ lines.push('## Key Areas');
893
+ lines.push('');
894
+ if (lookup.entryPoints.length) {
895
+ lines.push(`**Entry Points:** ${lookup.entryPoints.slice(0, 3).join(', ')}`);
896
+ }
897
+ if (lookup.componentFiles.length) {
898
+ lines.push(`**Components:** ${lookup.componentFiles.length} files`);
899
+ }
900
+ if (lookup.routeFiles.length) {
901
+ lines.push(`**Routes/API:** ${lookup.routeFiles.length} files`);
902
+ }
903
+ if (lookup.modelFiles.length) {
904
+ lines.push(`**Models:** ${lookup.modelFiles.length} files`);
905
+ }
906
+ if (lookup.testFiles.length) {
907
+ lines.push(`**Tests:** ${lookup.testFiles.length} files`);
908
+ }
909
+ if (patterns.namingConventions.length) {
910
+ lines.push('');
911
+ lines.push(`**Naming Conventions:** ${patterns.namingConventions.join(', ')}`);
912
+ }
913
+ result.answer = lines.join('\n');
914
+ }
915
+ showDependencies(query, result) {
916
+ // Find the most relevant file
917
+ const targetFile = query.keywords.length > 0
918
+ ? this.index.files.find(f => query.keywords.some(k => f.path.toLowerCase().includes(k.toLowerCase())))
919
+ : this.index.quickLookup.entryPoints[0];
920
+ if (!targetFile) {
921
+ result.answer = 'Specify a file to show dependencies for.';
922
+ return;
923
+ }
924
+ const filePath = typeof targetFile === 'string' ? targetFile : targetFile.path;
925
+ const imports = this.index.imports.imports.get(filePath) || [];
926
+ const importedBy = this.index.imports.importedBy.get(filePath) || [];
927
+ const lines = [
928
+ `# Dependencies for ${filePath}`,
929
+ '',
930
+ '## Imports (dependencies)',
931
+ imports.length ? imports.map(i => `• ${i}`).join('\n') : 'No local imports',
932
+ '',
933
+ '## Imported by (dependents)',
934
+ importedBy.length ? importedBy.map(i => `• ${i}`).join('\n') : 'Not imported by other files',
935
+ ];
936
+ result.answer = lines.join('\n');
937
+ }
938
+ explainConcept(query, result) {
939
+ // Combine file and symbol search for conceptual queries
940
+ this.findFiles(query, result);
941
+ this.findSymbols(query, result);
942
+ // Enhanced answer
943
+ const allMatches = result.files.length + result.symbols.length;
944
+ if (allMatches > 0) {
945
+ const parts = [
946
+ `Found ${allMatches} relevant result(s) for "${query.keywords.join(' ')}":`,
947
+ '',
948
+ ];
949
+ if (result.symbols.length > 0) {
950
+ parts.push('**Symbols:**');
951
+ for (const s of result.symbols.slice(0, 3)) {
952
+ parts.push(`• ${s.name} (${s.kind}) at ${s.file}:${s.line}`);
953
+ }
954
+ parts.push('');
955
+ }
956
+ if (result.files.length > 0) {
957
+ parts.push('**Files:**');
958
+ for (const f of result.files.slice(0, 3)) {
959
+ parts.push(`• ${f.path} - ${f.reason}`);
960
+ }
961
+ }
962
+ result.answer = parts.join('\n');
963
+ }
964
+ }
965
+ generalSearch(query, result) {
966
+ // Combine all search methods
967
+ this.findFiles(query, result);
968
+ const fileMatches = [...result.files];
969
+ result.files = [];
970
+ this.findSymbols(query, result);
971
+ const symbolMatches = [...result.symbols];
972
+ // Merge results
973
+ result.files = fileMatches;
974
+ result.symbols = symbolMatches;
975
+ // Generate combined answer
976
+ const parts = [];
977
+ if (symbolMatches.length > 0) {
978
+ parts.push(`**Symbols (${symbolMatches.length}):**`);
979
+ for (const s of symbolMatches.slice(0, 5)) {
980
+ parts.push(` • ${s.name} (${s.kind}) → ${s.file}:${s.line}`);
981
+ }
982
+ }
983
+ if (fileMatches.length > 0) {
984
+ if (parts.length)
985
+ parts.push('');
986
+ parts.push(`**Files (${fileMatches.length}):**`);
987
+ for (const f of fileMatches.slice(0, 5)) {
988
+ parts.push(` • ${f.path} - ${f.reason}`);
989
+ }
990
+ }
991
+ if (parts.length === 0) {
992
+ result.answer = `No results found for "${query.keywords.join(' ')}". Try different terms.`;
993
+ }
994
+ else {
995
+ result.answer = parts.join('\n');
996
+ }
997
+ }
998
+ generateSuggestions(query, result) {
999
+ result.suggestions = [];
1000
+ if (result.files.length > 0) {
1001
+ result.suggestions.push(`Explore: ${result.files[0]?.path}`);
1002
+ }
1003
+ if (query.type === 'find_file' && result.files.length === 0) {
1004
+ result.suggestions.push('Try broader search terms');
1005
+ result.suggestions.push('Check the architecture overview: "show architecture"');
1006
+ }
1007
+ if (query.type === 'find_symbol') {
1008
+ result.suggestions.push('Find usages: "where is X used"');
1009
+ }
1010
+ // Suggest related queries based on patterns
1011
+ const patterns = this.index.patterns;
1012
+ if (patterns.designPatterns.length > 0) {
1013
+ result.suggestions.push(`Explore pattern: ${patterns.designPatterns[0]}`);
1014
+ }
1015
+ }
1016
+ }
1017
+ // =====================================================
1018
+ // Tool Definition
1019
+ // =====================================================
1020
+ export function createLocalExploreTool(workingDir) {
1021
+ const engine = new LocalExploreEngine(workingDir);
1022
+ let initialized = false;
1023
+ return {
1024
+ name: 'explore',
1025
+ description: `Explore and understand the codebase using natural language queries.
1026
+ This tool works entirely offline without requiring an API key.
1027
+
1028
+ Example queries:
1029
+ - "Where is authentication handled?"
1030
+ - "Find the UserService class"
1031
+ - "Show architecture overview"
1032
+ - "Where is handleSubmit used?"
1033
+ - "What are the entry points?"
1034
+ - "Show dependencies for src/app.ts"
1035
+
1036
+ Returns file locations, symbol definitions, and explanations.`,
1037
+ parameters: {
1038
+ type: 'object',
1039
+ properties: {
1040
+ query: {
1041
+ type: 'string',
1042
+ description: 'Natural language query about the codebase',
1043
+ },
1044
+ rebuild: {
1045
+ type: 'boolean',
1046
+ description: 'Force rebuild of the codebase index (default: false)',
1047
+ },
1048
+ },
1049
+ required: ['query'],
1050
+ additionalProperties: false,
1051
+ },
1052
+ cacheable: false,
1053
+ handler: async (args) => {
1054
+ try {
1055
+ const query = args['query'];
1056
+ const rebuild = args['rebuild'] === true;
1057
+ if (!query || !query.trim()) {
1058
+ return 'Error: query must be a non-empty string';
1059
+ }
1060
+ // Initialize on first use
1061
+ if (!initialized || rebuild) {
1062
+ const { rebuilt, fileCount } = await engine.initialize(rebuild);
1063
+ initialized = true;
1064
+ if (rebuilt) {
1065
+ return `Indexed ${fileCount} files. Now exploring: "${query}"\n\n${(await engine.explore(query)).answer}`;
1066
+ }
1067
+ }
1068
+ // Handle special queries
1069
+ if (query.toLowerCase() === 'overview' || query.toLowerCase() === 'help') {
1070
+ return engine.getOverview();
1071
+ }
1072
+ // Execute exploration
1073
+ const result = await engine.explore(query);
1074
+ // Format output
1075
+ const lines = [result.answer];
1076
+ if (result.suggestions.length > 0 && result.files.length + result.symbols.length < 3) {
1077
+ lines.push('');
1078
+ lines.push('**Suggestions:**');
1079
+ for (const s of result.suggestions.slice(0, 3)) {
1080
+ lines.push(`• ${s}`);
1081
+ }
1082
+ }
1083
+ lines.push('');
1084
+ lines.push(`_Query: "${query}" | Type: ${result.queryType} | Time: ${result.timeTaken}ms_`);
1085
+ return lines.join('\n');
1086
+ }
1087
+ catch (error) {
1088
+ return buildError('exploring codebase', error, { workingDir });
1089
+ }
1090
+ },
1091
+ };
1092
+ }
1093
+ /**
1094
+ * Create all local exploration tools (explore + index management).
1095
+ */
1096
+ export function createLocalExploreTools(workingDir) {
1097
+ return [
1098
+ createLocalExploreTool(workingDir),
1099
+ createIndexTool(workingDir),
1100
+ ];
1101
+ }
1102
+ function createIndexTool(workingDir) {
1103
+ const engine = new LocalExploreEngine(workingDir);
1104
+ return {
1105
+ name: 'explore_index',
1106
+ description: 'Manage the codebase index for exploration. Use action "rebuild" to force re-index, "status" to check index state.',
1107
+ parameters: {
1108
+ type: 'object',
1109
+ properties: {
1110
+ action: {
1111
+ type: 'string',
1112
+ enum: ['rebuild', 'status'],
1113
+ description: 'Action to perform on the index',
1114
+ },
1115
+ },
1116
+ required: ['action'],
1117
+ additionalProperties: false,
1118
+ },
1119
+ cacheable: false,
1120
+ handler: async (args) => {
1121
+ try {
1122
+ const action = args['action'];
1123
+ if (action === 'rebuild') {
1124
+ const { fileCount } = await engine.initialize(true);
1125
+ return `✓ Rebuilt codebase index: ${fileCount} files analyzed`;
1126
+ }
1127
+ if (action === 'status') {
1128
+ const { rebuilt, fileCount } = await engine.initialize(false);
1129
+ return rebuilt
1130
+ ? `Built new index: ${fileCount} files`
1131
+ : `Using cached index: ${fileCount} files`;
1132
+ }
1133
+ return 'Unknown action. Use "rebuild" or "status".';
1134
+ }
1135
+ catch (error) {
1136
+ return buildError('managing index', error, { workingDir });
1137
+ }
1138
+ },
1139
+ };
1140
+ }
1141
+ //# sourceMappingURL=localExplore.js.map