Add codegraph_explore tool and expand exclude patterns
- Add codegraph_explore MCP tool for deep exploration with condensed output - Expand default exclude patterns for framework build outputs (.next, .nuxt, .expo, etc.) - Increase node ID hash length from 16 to 32 chars to prevent collisions - Add feature request detection with UX clarification reminders - Update README with MCP tools reference and best practices - Update CLAUDE.md with context usage guidelines Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
b1c6d672db
commit
0c168c4d8b
@@ -19,6 +19,9 @@ import { getParser, detectLanguage, isLanguageSupported } from './grammars';
|
||||
|
||||
/**
|
||||
* Generate a unique node ID
|
||||
*
|
||||
* Uses a 32-character (128-bit) hash to avoid collisions when indexing
|
||||
* large codebases with many files containing similar symbols.
|
||||
*/
|
||||
export function generateNodeId(
|
||||
filePath: string,
|
||||
@@ -30,7 +33,7 @@ export function generateNodeId(
|
||||
.createHash('sha256')
|
||||
.update(`${filePath}:${kind}:${name}:${line}`)
|
||||
.digest('hex')
|
||||
.substring(0, 16);
|
||||
.substring(0, 32);
|
||||
return `${kind}:${hash}`;
|
||||
}
|
||||
|
||||
|
||||
+264
-3
@@ -71,7 +71,7 @@ export const tools: ToolDefinition[] = [
|
||||
},
|
||||
{
|
||||
name: 'codegraph_context',
|
||||
description: 'PRIMARY TOOL: Build comprehensive context for a task. Returns entry points, related symbols, and key code - often enough to understand the codebase without additional tool calls.',
|
||||
description: 'PRIMARY TOOL: Build comprehensive context for a task. Returns entry points, related symbols, and key code - often enough to understand the codebase without additional tool calls. NOTE: This provides CODE context, not product requirements. For new features, still clarify UX/behavior questions with the user before implementing.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@@ -177,6 +177,29 @@ export const tools: ToolDefinition[] = [
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'codegraph_explore',
|
||||
description: 'RECOMMENDED FOR COMPLEX TASKS: Deep exploration that returns a condensed brief. Internally performs multiple searches, call graph analysis, and impact assessment - then synthesizes results into a compact summary. Use this instead of multiple codegraph_* calls to keep your context clean. Returns: key files, critical functions, data flow summary, and suggested approach.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
task: {
|
||||
type: 'string',
|
||||
description: 'Detailed description of the feature, bug, or task to explore',
|
||||
},
|
||||
focus: {
|
||||
type: 'string',
|
||||
description: 'Optional focus area: "architecture" (structure & patterns), "implementation" (specific code), or "impact" (what would change). Default: auto-detect.',
|
||||
enum: ['architecture', 'implementation', 'impact'],
|
||||
},
|
||||
keywords: {
|
||||
type: 'string',
|
||||
description: 'Optional comma-separated keywords to search for (e.g., "bundle,swap,subscription")',
|
||||
},
|
||||
},
|
||||
required: ['task'],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -205,6 +228,8 @@ export class ToolHandler {
|
||||
return await this.handleNode(args);
|
||||
case 'codegraph_status':
|
||||
return await this.handleStatus();
|
||||
case 'codegraph_explore':
|
||||
return await this.handleExplore(args);
|
||||
default:
|
||||
return this.errorResult(`Unknown tool: ${toolName}`);
|
||||
}
|
||||
@@ -248,13 +273,47 @@ export class ToolHandler {
|
||||
format: 'markdown',
|
||||
});
|
||||
|
||||
// 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.'
|
||||
: '';
|
||||
|
||||
// buildContext returns string when format is 'markdown'
|
||||
if (typeof context === 'string') {
|
||||
return this.textResult(context);
|
||||
return this.textResult(context + reminder);
|
||||
}
|
||||
|
||||
// If it returns TaskContext, format it
|
||||
return this.textResult(this.formatTaskContext(context));
|
||||
return this.textResult(this.formatTaskContext(context) + reminder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Heuristic to detect if a query looks like a feature request
|
||||
*/
|
||||
private looksLikeFeatureRequest(task: string): boolean {
|
||||
const featureKeywords = [
|
||||
'add', 'create', 'implement', 'build', 'enable', 'allow',
|
||||
'new feature', 'support for', 'ability to', 'want to',
|
||||
'should be able', 'need to add', 'swap', 'edit', 'modify'
|
||||
];
|
||||
const bugKeywords = [
|
||||
'fix', 'bug', 'error', 'broken', 'crash', 'issue', 'problem',
|
||||
'not working', 'fails', 'undefined', 'null'
|
||||
];
|
||||
const explorationKeywords = [
|
||||
'how does', 'where is', 'what is', 'find', 'show me',
|
||||
'explain', 'understand', 'explore'
|
||||
];
|
||||
|
||||
const lowerTask = task.toLowerCase();
|
||||
|
||||
// If it's clearly a bug or exploration, not a feature
|
||||
if (bugKeywords.some(k => lowerTask.includes(k))) return false;
|
||||
if (explorationKeywords.some(k => lowerTask.includes(k))) return false;
|
||||
|
||||
// If it matches feature keywords, it's likely a feature request
|
||||
return featureKeywords.some(k => lowerTask.includes(k));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -387,6 +446,208 @@ export class ToolHandler {
|
||||
return this.textResult(lines.join('\n'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle codegraph_explore - the "sub-agent" that does intensive exploration
|
||||
* and returns a condensed brief
|
||||
*/
|
||||
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)
|
||||
const symbolMap = new Map<string, Node>();
|
||||
const fileSet = new Set<string>();
|
||||
|
||||
for (const keyword of keywords.slice(0, 5)) { // Limit to 5 keywords
|
||||
const results = this.cg.searchNodes(keyword, { limit: 10 });
|
||||
for (const r of results) {
|
||||
if (!symbolMap.has(r.node.id)) {
|
||||
symbolMap.set(r.node.id, r.node);
|
||||
fileSet.add(r.node.filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 3: Analyze call relationships for top symbols
|
||||
const callGraphInsights: string[] = [];
|
||||
const topSymbols = Array.from(symbolMap.values())
|
||||
.filter(n => n.kind === 'function' || n.kind === 'method' || n.kind === 'component')
|
||||
.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(', ');
|
||||
|
||||
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 ? '...' : ''}`;
|
||||
callGraphInsights.push(insight);
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 4: Identify key entry points and patterns
|
||||
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({
|
||||
task,
|
||||
focus,
|
||||
keywords,
|
||||
files: Array.from(fileSet),
|
||||
components,
|
||||
routes,
|
||||
interfaces,
|
||||
functions,
|
||||
callGraphInsights,
|
||||
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.'
|
||||
: '';
|
||||
|
||||
return this.textResult(brief + reminder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract keywords from task description
|
||||
*/
|
||||
private extractKeywords(task: string, explicitKeywords?: string): string[] {
|
||||
const keywords: string[] = [];
|
||||
|
||||
// Add explicit keywords first
|
||||
if (explicitKeywords) {
|
||||
keywords.push(...explicitKeywords.split(',').map(k => k.trim()).filter(Boolean));
|
||||
}
|
||||
|
||||
// Extract likely code identifiers from task (camelCase, PascalCase, snake_case)
|
||||
const identifierPattern = /\b([A-Z][a-zA-Z0-9]*|[a-z][a-zA-Z0-9]*[A-Z][a-zA-Z0-9]*|[a-z]+_[a-z_]+)\b/g;
|
||||
const matches = task.match(identifierPattern) || [];
|
||||
keywords.push(...matches);
|
||||
|
||||
// Extract quoted terms
|
||||
const quotedPattern = /"([^"]+)"|'([^']+)'/g;
|
||||
let match;
|
||||
while ((match = quotedPattern.exec(task)) !== null) {
|
||||
const quoted = match[1] || match[2];
|
||||
if (quoted) keywords.push(quoted);
|
||||
}
|
||||
|
||||
// Extract domain-specific terms (nouns that might be code concepts)
|
||||
const commonTerms = task.toLowerCase()
|
||||
.split(/\s+/)
|
||||
.filter(word =>
|
||||
word.length > 3 &&
|
||||
!['this', 'that', 'with', 'from', 'have', 'been', 'will', 'would', 'could', 'should', 'when', 'where', 'what', 'which', 'their', 'there', 'these', 'those', 'about', 'into', 'then', 'than', 'some', 'other', 'after', 'before'].includes(word)
|
||||
);
|
||||
keywords.push(...commonTerms);
|
||||
|
||||
// Deduplicate and return
|
||||
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)
|
||||
// =========================================================================
|
||||
|
||||
+117
-8
@@ -498,21 +498,130 @@ export const DEFAULT_CONFIG: CodeGraphConfig = {
|
||||
'**/*.rb',
|
||||
],
|
||||
exclude: [
|
||||
// Version control
|
||||
'**/.git/**',
|
||||
|
||||
// Dependencies
|
||||
'**/node_modules/**',
|
||||
'**/vendor/**',
|
||||
'**/Pods/**',
|
||||
|
||||
// Generic build outputs
|
||||
'**/dist/**',
|
||||
'**/build/**',
|
||||
'**/.git/**',
|
||||
'**/vendor/**',
|
||||
'**/__pycache__/**',
|
||||
'**/target/**',
|
||||
'**/*.min.js',
|
||||
'**/*.bundle.js',
|
||||
'**/Pods/**',
|
||||
'**/.gradle/**',
|
||||
'**/out/**',
|
||||
'**/bin/**',
|
||||
'**/obj/**',
|
||||
'**/target/**',
|
||||
|
||||
// JavaScript/TypeScript
|
||||
'**/*.min.js',
|
||||
'**/*.bundle.js',
|
||||
'**/.next/**',
|
||||
'**/.nuxt/**',
|
||||
'**/.svelte-kit/**',
|
||||
'**/.output/**',
|
||||
'**/.turbo/**',
|
||||
'**/.cache/**',
|
||||
'**/.parcel-cache/**',
|
||||
'**/.vite/**',
|
||||
'**/.astro/**',
|
||||
'**/.docusaurus/**',
|
||||
'**/.gatsby/**',
|
||||
'**/.webpack/**',
|
||||
'**/.nx/**',
|
||||
'**/.yarn/cache/**',
|
||||
'**/.pnpm-store/**',
|
||||
'**/storybook-static/**',
|
||||
|
||||
// React Native / Expo
|
||||
'**/.expo/**',
|
||||
'**/web-build/**',
|
||||
'**/ios/Pods/**',
|
||||
'**/ios/build/**',
|
||||
'**/android/build/**',
|
||||
'**/android/.gradle/**',
|
||||
|
||||
// Python
|
||||
'**/__pycache__/**',
|
||||
'**/.venv/**',
|
||||
'**/venv/**',
|
||||
'**/.pytest_cache/**',
|
||||
'**/.mypy_cache/**',
|
||||
'**/.ruff_cache/**',
|
||||
'**/.tox/**',
|
||||
'**/.nox/**',
|
||||
'**/*.egg-info/**',
|
||||
'**/.eggs/**',
|
||||
|
||||
// Go
|
||||
'**/go/pkg/mod/**',
|
||||
|
||||
// Rust
|
||||
'**/target/debug/**',
|
||||
'**/target/release/**',
|
||||
|
||||
// Java/Kotlin/Gradle
|
||||
'**/.gradle/**',
|
||||
'**/.m2/**',
|
||||
'**/generated-sources/**',
|
||||
'**/.kotlin/**',
|
||||
|
||||
// C#/.NET
|
||||
'**/.vs/**',
|
||||
'**/.nuget/**',
|
||||
'**/artifacts/**',
|
||||
'**/publish/**',
|
||||
|
||||
// C/C++
|
||||
'**/cmake-build-*/**',
|
||||
'**/CMakeFiles/**',
|
||||
'**/bazel-*/**',
|
||||
'**/vcpkg_installed/**',
|
||||
'**/.conan/**',
|
||||
'**/Debug/**',
|
||||
'**/Release/**',
|
||||
'**/x64/**',
|
||||
|
||||
// Swift/iOS/Xcode
|
||||
'**/DerivedData/**',
|
||||
'**/.build/**',
|
||||
'**/.swiftpm/**',
|
||||
'**/xcuserdata/**',
|
||||
'**/Carthage/Build/**',
|
||||
'**/SourcePackages/**',
|
||||
|
||||
// PHP
|
||||
'**/.composer/**',
|
||||
'**/storage/framework/**',
|
||||
'**/bootstrap/cache/**',
|
||||
|
||||
// Ruby
|
||||
'**/.bundle/**',
|
||||
'**/tmp/cache/**',
|
||||
'**/public/assets/**',
|
||||
'**/public/packs/**',
|
||||
'**/.yardoc/**',
|
||||
|
||||
// Testing/Coverage
|
||||
'**/coverage/**',
|
||||
'**/htmlcov/**',
|
||||
'**/.nyc_output/**',
|
||||
'**/test-results/**',
|
||||
'**/.coverage/**',
|
||||
|
||||
// IDE/Editor
|
||||
'**/.idea/**',
|
||||
|
||||
// Logs and temp
|
||||
'**/logs/**',
|
||||
'**/tmp/**',
|
||||
'**/temp/**',
|
||||
|
||||
// Documentation build output
|
||||
'**/_build/**',
|
||||
'**/docs/_build/**',
|
||||
'**/site/**',
|
||||
],
|
||||
languages: [],
|
||||
frameworks: [],
|
||||
|
||||
Reference in New Issue
Block a user