Make codegraph_explore language-agnostic

- Pattern generation now uses multiple naming conventions:
  PascalCase, snake_case, and camelCase
- Common suffixes/prefixes work across languages (Service,
  Handler, Controller, handle_, create_, etc.)
- Removed JS/React-specific patterns like useBundle, SwapDialog
- Bump version to 0.1.6

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Colby McHenry
2026-01-19 02:53:50 -06:00
co-authored by Claude Opus 4.5
parent 0c168c4d8b
commit 43cee17dc3
2 changed files with 168 additions and 118 deletions
+167 -117
View File
@@ -276,7 +276,7 @@ export class ToolHandler {
// Detect if this looks like a feature request (vs bug fix or exploration)
const isFeatureQuery = this.looksLikeFeatureRequest(task);
const reminder = isFeatureQuery
? '\n\n---\n**Note:** This is code context only. For new features, consider asking the user about UX preferences, edge cases, and acceptance criteria before implementing.'
? '\n\n⚠️ **Ask user:** UX preferences, edge cases, acceptance criteria'
: '';
// buildContext returns string when format is 'markdown'
@@ -447,22 +447,21 @@ export class ToolHandler {
}
/**
* Handle codegraph_explore - the "sub-agent" that does intensive exploration
* and returns a condensed brief
* Handle codegraph_explore - deep exploration that finds existing implementations
* and returns actionable insights
*/
private async handleExplore(args: Record<string, unknown>): Promise<ToolResult> {
const task = args.task as string;
const focus = args.focus as string | undefined;
const keywordsArg = args.keywords as string | undefined;
// Phase 1: Extract search terms
const keywords = this.extractKeywords(task, keywordsArg);
// Phase 2: Find relevant symbols (internal, not returned directly)
// Phase 2: Find relevant symbols
const symbolMap = new Map<string, Node>();
const fileSet = new Set<string>();
for (const keyword of keywords.slice(0, 5)) { // Limit to 5 keywords
for (const keyword of keywords.slice(0, 5)) {
const results = this.cg.searchNodes(keyword, { limit: 10 });
for (const r of results) {
if (!symbolMap.has(r.node.id)) {
@@ -472,56 +471,196 @@ export class ToolHandler {
}
}
// Phase 3: Analyze call relationships for top symbols
// Phase 3: Look for EXISTING implementations (key improvement)
// Search for common patterns that indicate feature already exists
const existingImplementations: string[] = [];
const searchPatterns = this.generateExistingPatternSearches(keywords);
for (const pattern of searchPatterns) {
const results = this.cg.searchNodes(pattern, { limit: 5 });
for (const r of results) {
const node = r.node;
// Check if this looks like an implementation (not just a type)
if (['function', 'method', 'component'].includes(node.kind)) {
const loc = `${node.filePath}:${node.startLine}`;
existingImplementations.push(`${node.name} (${node.kind}) - ${loc}`);
symbolMap.set(node.id, node);
fileSet.add(node.filePath);
}
}
}
// Phase 4: Analyze architecture - find similar features to understand patterns
const architectureInsights: string[] = [];
const allFunctions = Array.from(symbolMap.values())
.filter(n => n.kind === 'function' || n.kind === 'method' || n.kind === 'component');
// Look for hooks, handlers, dialogs, cards - common UI patterns
const hooks = allFunctions.filter(n => n.name.startsWith('use'));
const handlers = allFunctions.filter(n => n.name.startsWith('handle'));
const dialogs = Array.from(symbolMap.values()).filter(n =>
n.name.toLowerCase().includes('dialog') || n.name.toLowerCase().includes('modal'));
const apiRoutes = Array.from(symbolMap.values()).filter(n =>
n.filePath.includes('/api/') || n.kind === 'route');
if (hooks.length > 0) {
architectureInsights.push(`Hooks: ${hooks.slice(0, 3).map(h => h.name).join(', ')}`);
}
if (handlers.length > 0) {
architectureInsights.push(`Handlers: ${handlers.slice(0, 3).map(h => h.name).join(', ')}`);
}
if (dialogs.length > 0) {
architectureInsights.push(`Dialogs: ${dialogs.slice(0, 3).map(d => d.name).join(', ')}`);
}
if (apiRoutes.length > 0) {
architectureInsights.push(`API: ${apiRoutes.slice(0, 3).map(r => r.filePath.split('/api/')[1] || r.name).join(', ')}`);
}
// Phase 5: Trace call graphs to understand data flow
const callGraphInsights: string[] = [];
const topSymbols = Array.from(symbolMap.values())
.filter(n => n.kind === 'function' || n.kind === 'method' || n.kind === 'component')
.slice(0, 5);
const topSymbols = allFunctions.slice(0, 5);
for (const symbol of topSymbols) {
const callers = this.cg.getCallers(symbol.id);
const callees = this.cg.getCallees(symbol.id);
if (callers.length > 0 || callees.length > 0) {
const callerNames = callers.slice(0, 3).map(c => c.node.name).join(', ');
const calleeNames = callees.slice(0, 3).map(c => c.node.name).join(', ');
const callerNames = callers.slice(0, 2).map(c => c.node.name).join(', ');
const calleeNames = callees.slice(0, 2).map(c => c.node.name).join(', ');
let insight = `**${symbol.name}**`;
if (callers.length > 0) insight += ` called by: ${callerNames}${callers.length > 3 ? '...' : ''}`;
if (callees.length > 0) insight += ` calls: ${calleeNames}${callees.length > 3 ? '...' : ''}`;
let insight = symbol.name;
if (callers.length > 0) insight += `${callerNames}`;
if (callees.length > 0) insight += `${calleeNames}`;
callGraphInsights.push(insight);
}
}
// Phase 4: Identify key entry points and patterns
// Phase 6: Identify key types
const interfaces = Array.from(symbolMap.values())
.filter(n => n.kind === 'interface' || n.kind === 'type_alias');
const components = Array.from(symbolMap.values()).filter(n => n.kind === 'component');
const routes = Array.from(symbolMap.values()).filter(n => n.kind === 'route');
const interfaces = Array.from(symbolMap.values()).filter(n => n.kind === 'interface' || n.kind === 'type_alias');
const functions = Array.from(symbolMap.values()).filter(n => n.kind === 'function' || n.kind === 'method');
// Phase 5: Build condensed brief
const brief = this.buildExploreBrief({
// Phase 7: Build compact brief with insights
const brief = this.buildExploreBriefV2({
task,
focus,
keywords,
files: Array.from(fileSet),
components,
routes,
interfaces,
functions,
existingImplementations,
architectureInsights,
callGraphInsights,
interfaces,
components,
totalSymbols: symbolMap.size,
});
// Add feature request reminder if applicable
const isFeatureQuery = this.looksLikeFeatureRequest(task);
const reminder = isFeatureQuery
? '\n\n---\n**Before implementing:** Clarify with the user: UX preferences, edge cases, error handling, and acceptance criteria.'
? '\n\n⚠️ **Ask user:** UX preferences, edge cases, acceptance criteria'
: '';
return this.textResult(brief + reminder);
}
/**
* Generate search patterns to find existing implementations
* Language-agnostic: generates patterns for multiple naming conventions
*/
private generateExistingPatternSearches(keywords: string[]): string[] {
const patterns: string[] = [];
for (const keyword of keywords.slice(0, 3)) {
// Skip very short or common words
if (keyword.length < 3) continue;
const lower = keyword.toLowerCase();
const capitalized = keyword.charAt(0).toUpperCase() + keyword.slice(1).toLowerCase();
// The keyword itself in various cases
patterns.push(lower);
patterns.push(capitalized);
// Common suffixes (work across most languages)
// These patterns find: SwapService, swap_service, SwapHandler, etc.
const suffixes = ['Service', 'Handler', 'Controller', 'Manager', 'Helper', 'Util', 'Utils'];
for (const suffix of suffixes) {
patterns.push(`${capitalized}${suffix}`); // PascalCase: SwapService
patterns.push(`${lower}_${suffix.toLowerCase()}`); // snake_case: swap_service
}
// Common prefixes (work across most languages)
const prefixes = ['handle', 'process', 'do', 'execute', 'perform', 'run'];
for (const prefix of prefixes) {
patterns.push(`${prefix}_${lower}`); // snake_case: handle_swap
patterns.push(`${prefix}${capitalized}`); // camelCase: handleSwap
}
// Common action patterns
patterns.push(`create_${lower}`);
patterns.push(`update_${lower}`);
patterns.push(`delete_${lower}`);
patterns.push(`get_${lower}`);
patterns.push(`create${capitalized}`);
patterns.push(`update${capitalized}`);
patterns.push(`delete${capitalized}`);
patterns.push(`get${capitalized}`);
}
return [...new Set(patterns)];
}
/**
* Build compact brief with existing implementation focus
*/
private buildExploreBriefV2(data: {
task: string;
files: string[];
existingImplementations: string[];
architectureInsights: string[];
callGraphInsights: string[];
interfaces: Node[];
components: Node[];
totalSymbols: number;
}): string {
const lines: string[] = [];
// Stats
lines.push(`**${data.totalSymbols} symbols in ${data.files.length} files**`);
// MOST IMPORTANT: Existing implementations found
if (data.existingImplementations.length > 0) {
lines.push('');
lines.push('🔍 **Existing implementations found:**');
for (const impl of data.existingImplementations.slice(0, 5)) {
lines.push(` - ${impl}`);
}
}
// Architecture patterns
if (data.architectureInsights.length > 0) {
lines.push('');
lines.push(`**Patterns:** ${data.architectureInsights.join(' | ')}`);
}
// Data flow (compact)
if (data.callGraphInsights.length > 0) {
lines.push(`**Flow:** ${data.callGraphInsights.slice(0, 3).join(' | ')}`);
}
// Key types
if (data.interfaces.length > 0) {
const types = data.interfaces.slice(0, 4).map(t => `${t.name}:${t.startLine}`);
lines.push(`**Types:** ${types.join(', ')}`);
}
// Key files to read
if (data.files.length > 0) {
lines.push('');
lines.push(`**Read:** ${data.files.slice(0, 3).join(', ')}`);
}
return lines.join('\n');
}
/**
* Extract keywords from task description
*/
@@ -559,95 +698,6 @@ export class ToolHandler {
return [...new Set(keywords)];
}
/**
* Build a condensed exploration brief
*/
private buildExploreBrief(data: {
task: string;
focus?: string;
keywords: string[];
files: string[];
components: Node[];
routes: Node[];
interfaces: Node[];
functions: Node[];
callGraphInsights: string[];
totalSymbols: number;
}): string {
const lines: string[] = [
'## Exploration Brief',
'',
`**Task:** ${data.task}`,
`**Found:** ${data.totalSymbols} relevant symbols across ${data.files.length} files`,
'',
];
// Key files (grouped by directory)
if (data.files.length > 0) {
lines.push('### Key Files');
const topFiles = data.files.slice(0, 10);
for (const file of topFiles) {
lines.push(`- ${file}`);
}
if (data.files.length > 10) {
lines.push(`- ... and ${data.files.length - 10} more`);
}
lines.push('');
}
// Entry points
const entryPoints: string[] = [];
if (data.components.length > 0) {
entryPoints.push(`**Components:** ${data.components.slice(0, 5).map(n => `${n.name} (${n.filePath}:${n.startLine})`).join(', ')}`);
}
if (data.routes.length > 0) {
entryPoints.push(`**Routes:** ${data.routes.slice(0, 5).map(n => `${n.name} (${n.filePath}:${n.startLine})`).join(', ')}`);
}
if (entryPoints.length > 0) {
lines.push('### Entry Points');
lines.push(...entryPoints);
lines.push('');
}
// Key types/interfaces
if (data.interfaces.length > 0) {
lines.push('### Key Types');
for (const iface of data.interfaces.slice(0, 5)) {
lines.push(`- **${iface.name}** - ${iface.filePath}:${iface.startLine}`);
}
lines.push('');
}
// Key functions
if (data.functions.length > 0) {
lines.push('### Key Functions');
for (const fn of data.functions.slice(0, 8)) {
const sig = fn.signature ? ` - \`${fn.signature.slice(0, 60)}${fn.signature.length > 60 ? '...' : ''}\`` : '';
lines.push(`- **${fn.name}** (${fn.filePath}:${fn.startLine})${sig}`);
}
lines.push('');
}
// Call graph insights
if (data.callGraphInsights.length > 0) {
lines.push('### Data Flow');
for (const insight of data.callGraphInsights.slice(0, 6)) {
lines.push(`- ${insight}`);
}
lines.push('');
}
// Suggested files to read (actionable)
lines.push('### Suggested Next Steps');
lines.push('Read these files for implementation details:');
const suggestedFiles = data.files.slice(0, 3);
for (const file of suggestedFiles) {
lines.push(`1. \`${file}\``);
}
return lines.join('\n');
}
// =========================================================================
// Formatting helpers (compact by default to reduce context usage)
// =========================================================================