codegraph_explore now reads code internally (sub-agent behavior)

Instead of returning file paths for Claude to read separately,
codegraph_explore now:
- Reads relevant files internally using fs
- Extracts code snippets for key symbols (functions, types, APIs)
- Includes the code directly in the response
- Returns a synthesis with data flow and key code included

This matches native explore agent behavior where file reading
happens in the sub-agent context, keeping main context clean.

Bump version to 0.1.8

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Colby McHenry
2026-01-19 03:16:05 -06:00
co-authored by Claude Opus 4.5
parent 0ecd195f15
commit ef6f2ae50b
2 changed files with 167 additions and 129 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@colbymchenry/codegraph",
"version": "0.1.7",
"version": "0.1.8",
"description": "A local-first code intelligence system that builds a semantic knowledge graph from any codebase",
"main": "dist/index.js",
"types": "dist/index.d.ts",
+162 -124
View File
@@ -4,6 +4,8 @@
* Defines the tools exposed by the CodeGraph MCP server.
*/
import * as fs from 'fs';
import * as path from 'path';
import CodeGraph from '../index';
import type { Node, SearchResult, Subgraph, TaskContext, NodeKind } from '../types';
@@ -179,7 +181,7 @@ export const tools: ToolDefinition[] = [
},
{
name: 'codegraph_explore',
description: 'RECOMMENDED FOR COMPLEX TASKS: Deep exploration that returns a condensed brief. Internally performs searches, call graph analysis, and checks for EXISTING implementations. IMPORTANT: If results show existing implementations, READ those files before planning - the feature may already exist. For feature requests, use AskUserQuestion to clarify requirements BEFORE making a plan.',
description: 'RECOMMENDED FOR COMPLEX TASKS: Deep exploration that READS CODE INTERNALLY and returns synthesis with key snippets. You do NOT need to make separate Read calls - the code is included in the response. Checks for existing implementations, traces data flow, and includes relevant code. For feature requests, use AskUserQuestion to clarify requirements BEFORE planning.',
inputSchema: {
type: 'object',
properties: {
@@ -447,12 +449,13 @@ export class ToolHandler {
}
/**
* Handle codegraph_explore - deep exploration that finds existing implementations
* and returns actionable insights
* Handle codegraph_explore - deep exploration with internal file reading
* Returns synthesized context so Claude doesn't need separate Read calls
*/
private async handleExplore(args: Record<string, unknown>): Promise<ToolResult> {
const task = args.task as string;
const keywordsArg = args.keywords as string | undefined;
const projectRoot = this.cg.getProjectRoot();
// Phase 1: Extract search terms
const keywords = this.extractKeywords(task, keywordsArg);
@@ -471,93 +474,193 @@ export class ToolHandler {
}
}
// Phase 3: Look for EXISTING implementations (key improvement)
// Search for common patterns that indicate feature already exists
const existingImplementations: string[] = [];
// Phase 3: Look for EXISTING implementations
const existingImplNodes: Node[] = [];
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}`);
if (!symbolMap.has(node.id)) {
symbolMap.set(node.id, node);
fileSet.add(node.filePath);
}
existingImplNodes.push(node);
}
}
}
// 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');
// Phase 4: Categorize symbols by type
const allSymbols = Array.from(symbolMap.values());
const functions = allSymbols.filter(n => n.kind === 'function' || n.kind === 'method');
const components = allSymbols.filter(n => n.kind === 'component');
const types = allSymbols.filter(n => n.kind === 'interface' || n.kind === 'type_alias');
const apiRoutes = allSymbols.filter(n => n.filePath.includes('/api/') || n.kind === 'route');
// 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');
// Phase 5: READ CODE INTERNALLY for key symbols (the sub-agent behavior)
// Prioritize: existing implementations > API routes > types > functions
const codeSnippets: Array<{ node: Node; code: string }> = [];
const maxSnippets = 8;
const maxCodeLength = 1500; // chars per snippet
if (hooks.length > 0) {
architectureInsights.push(`Hooks: ${hooks.slice(0, 3).map(h => h.name).join(', ')}`);
const priorityNodes = [
...existingImplNodes.slice(0, 3),
...apiRoutes.slice(0, 2),
...types.slice(0, 2),
...components.slice(0, 2),
...functions.filter(n => !existingImplNodes.includes(n)).slice(0, 1),
];
for (const node of priorityNodes) {
if (codeSnippets.length >= maxSnippets) break;
const code = this.extractNodeCode(projectRoot, node, maxCodeLength);
if (code) {
codeSnippets.push({ node, code });
}
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 = allFunctions.slice(0, 5);
for (const symbol of topSymbols) {
const callers = this.cg.getCallers(symbol.id);
const callees = this.cg.getCallees(symbol.id);
// Phase 6: Trace call graphs for data flow understanding
const dataFlowInsights: string[] = [];
for (const node of existingImplNodes.slice(0, 3)) {
const callers = this.cg.getCallers(node.id);
const callees = this.cg.getCallees(node.id);
if (callers.length > 0 || callees.length > 0) {
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 += `${callerNames}`;
if (callees.length > 0) insight += `${calleeNames}`;
callGraphInsights.push(insight);
let flow = `${node.name}`;
if (callers.length > 0) flow = `${callers.slice(0, 2).map(c => c.node.name).join(', ')}${flow}`;
if (callees.length > 0) flow = `${flow}${callees.slice(0, 2).map(c => c.node.name).join(', ')}`;
dataFlowInsights.push(flow);
}
}
// 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');
// Phase 7: Build compact brief with insights
// Phase 7: Build comprehensive synthesis
const isFeatureQuery = this.looksLikeFeatureRequest(task);
const hasExistingImpl = existingImplementations.length > 0;
const hasExistingImpl = existingImplNodes.length > 0;
const brief = this.buildExploreBriefV2({
const synthesis = this.buildExploreSynthesis({
task,
files: Array.from(fileSet),
existingImplementations,
architectureInsights,
callGraphInsights,
interfaces,
components,
existingImplNodes,
codeSnippets,
types,
apiRoutes,
dataFlowInsights,
totalSymbols: symbolMap.size,
totalFiles: fileSet.size,
isFeatureQuery,
hasExistingImpl,
});
return this.textResult(brief);
return this.textResult(synthesis);
}
/**
* Extract code from a node's source file
*/
private extractNodeCode(projectRoot: string, node: Node, maxLength: number): string | null {
const filePath = path.join(projectRoot, node.filePath);
if (!fs.existsSync(filePath)) {
return null;
}
try {
const content = fs.readFileSync(filePath, 'utf-8');
const lines = content.split('\n');
const startIdx = Math.max(0, node.startLine - 1);
const endIdx = Math.min(lines.length, node.endLine);
let code = lines.slice(startIdx, endIdx).join('\n');
// Truncate if too long
if (code.length > maxLength) {
code = code.slice(0, maxLength) + '\n// ... truncated ...';
}
return code;
} catch {
return null;
}
}
/**
* Build comprehensive synthesis with code included
*/
private buildExploreSynthesis(data: {
task: string;
existingImplNodes: Node[];
codeSnippets: Array<{ node: Node; code: string }>;
types: Node[];
apiRoutes: Node[];
dataFlowInsights: string[];
totalSymbols: number;
totalFiles: number;
isFeatureQuery: boolean;
hasExistingImpl: boolean;
}): string {
const lines: string[] = [];
// Critical warnings at TOP
if (data.hasExistingImpl) {
lines.push('⚠️ **EXISTING IMPLEMENTATIONS FOUND** - Review code below before planning. Feature may already exist.');
lines.push('');
}
if (data.isFeatureQuery) {
lines.push('📋 **BEFORE PLANNING:** Use `AskUserQuestion` to clarify UX preferences and requirements.');
lines.push('');
}
// Summary stats
lines.push(`**Explored ${data.totalSymbols} symbols across ${data.totalFiles} files**`);
lines.push('');
// Data flow (if available)
if (data.dataFlowInsights.length > 0) {
lines.push('**Data Flow:**');
for (const flow of data.dataFlowInsights.slice(0, 3)) {
lines.push(` ${flow}`);
}
lines.push('');
}
// Key types (signatures only, no code)
if (data.types.length > 0) {
lines.push('**Key Types:**');
for (const t of data.types.slice(0, 4)) {
lines.push(` - \`${t.name}\` (${t.filePath}:${t.startLine})`);
}
lines.push('');
}
// API routes (signatures only)
if (data.apiRoutes.length > 0) {
lines.push('**API Routes:**');
for (const r of data.apiRoutes.slice(0, 3)) {
const routePath = r.filePath.split('/api/')[1] || r.filePath;
lines.push(` - \`/api/${routePath}\` (${r.name})`);
}
lines.push('');
}
// CODE SNIPPETS - the key sub-agent behavior
if (data.codeSnippets.length > 0) {
lines.push('---');
lines.push('**Key Code (read internally):**');
lines.push('');
for (const { node, code } of data.codeSnippets) {
lines.push(`### ${node.name} (${node.kind}) - ${node.filePath}:${node.startLine}`);
lines.push('```' + (node.language || 'typescript'));
lines.push(code);
lines.push('```');
lines.push('');
}
}
return lines.join('\n');
}
/**
@@ -607,71 +710,6 @@ export class ToolHandler {
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;
isFeatureQuery: boolean;
hasExistingImpl: boolean;
}): string {
const lines: string[] = [];
// CRITICAL: Action items at TOP (not bottom) so Claude sees them first
if (data.hasExistingImpl) {
lines.push('⚠️ **STOP: Similar implementations exist!** Read the files below to check if this feature already exists before planning.');
lines.push('');
}
if (data.isFeatureQuery) {
lines.push('📋 **BEFORE PLANNING:** Use `AskUserQuestion` to clarify UX preferences, edge cases, and acceptance criteria.');
lines.push('');
}
// Stats
lines.push(`**${data.totalSymbols} symbols in ${data.files.length} files**`);
// Existing implementations - now with context that these need verification
if (data.existingImplementations.length > 0) {
lines.push('');
lines.push('🔍 **Existing implementations to verify:**');
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
*/