UNPKG

socraticode

Version:

SocratiCode — MCP server for local codebase indexing, semantic search, and code dependency graphs. All private, all local via Docker.

643 lines • 35.4 kB
// SPDX-License-Identifier: AGPL-3.0-only // Copyright (C) 2026 Giancarlo Erra - Altaire Limited import path from "node:path"; import { projectIdFromPath } from "../config.js"; import { getWatcherMode, mergeExtraExtensions, SOCRATICODE_VERSION } from "../constants.js"; import { awaitGraphBuild, describeGraphBuilder, describeUnresolvedSymbolEdges, ensureDynamicLanguages, findCircularDependencies, generateMermaidDiagram, getAstGrepLang, getDynamicLanguageStatus, getExistingGraph, getFileDependencies, getGraphBuildProgress, getGraphStats, getGraphStatus, getLastGraphBuildCompleted, getOrBuildGraph, isGraphBuildInProgress, isImportResolutionLow, rebuildGraph, removeGraph } from "../services/code-graph.js"; import { withGitRefreshNotice } from "../services/git-refresh.js"; import { detectEntryPoints } from "../services/graph-entrypoints.js"; import { getCallFlow, getImpactRadius, getSymbolContextResult, listSymbols, looksLikeFilePath, } from "../services/graph-impact.js"; import { openInBrowser, writeInteractiveGraphFile } from "../services/graph-visualize-browser.js"; import { buildInteractiveGraphHtml } from "../services/graph-visualize-html.js"; import { logger } from "../services/logger.js"; import { dropSymbolGraphCacheGeneration, getSymbolGraphCache, } from "../services/symbol-graph-cache.js"; import { StorageReadError, SymbolGraphGenerationChangedError, } from "../services/symbol-graph-store.js"; import { ensureWatcherStarted } from "../services/watcher.js"; /** * Tools that answer purely from the persisted symbol graph. If the last build * could not persist it, they read whatever cache survived, so their answers may * be stale or absent with nothing on screen to say why — the "0 callers, no * explanation" report behind #89. They get the failure prepended. */ const SYMBOL_GRAPH_TOOLS = new Set([ "codebase_impact", "codebase_flow", "codebase_symbol", "codebase_symbols", ]); class ExplicitGraphBuildRequiredError extends Error { } /** * Default mode keeps the historical lazy-build behaviour. Snapshot modes may * read a cached or persisted graph, but must not create one as a side effect of * a query. */ async function getGraphForRead(projectPath) { const watcherMode = getWatcherMode(); if (watcherMode === "auto") return getOrBuildGraph(projectPath); const graph = await getExistingGraph(projectPath); if (graph) return graph; throw new ExplicitGraphBuildRequiredError(`No code graph found for: ${projectPath}\n` + `Automatic graph creation is disabled by SOCRATICODE_WATCHER=${watcherMode}. ` + "Run codebase_graph_build to create it explicitly."); } function hasRelevantGrammarFailure(failed, target, file, symbolId) { if (failed.length === 0) return false; const hintedFile = symbolId?.split("::")[0] ?? file ?? (looksLikeFilePath(target) ? target : undefined); if (!hintedFile) return true; const grammar = getAstGrepLang(path.extname(hintedFile).toLowerCase()); if (!grammar) return true; return failed.some(({ name }) => name.toLowerCase() === String(grammar).toLowerCase()); } /** Add Git freshness to graph reads while keeping explicit graph mutations separate. */ export async function handleGraphTool(name, args) { if (name === "codebase_graph_build" || name === "codebase_graph_remove") return handleGraphResult(name, args); const projectPath = args.projectPath || process.cwd(); return withGitRefreshNotice(projectPath, () => handleGraphResult(name, args)); } /** Dispatch a graph request with persisted-generation retries and visible storage errors. */ async function handleGraphResult(name, args) { for (let attempt = 0; attempt < 2; attempt++) { try { const result = await dispatchGraphTool(name, args); if (!SYMBOL_GRAPH_TOOLS.has(name)) return result; const resolved = path.resolve(args.projectPath || process.cwd()); const symbolGraphError = getLastGraphBuildCompleted(resolved)?.symbolGraphError; if (!symbolGraphError) return result; return [ `WARNING: the last graph build could not persist the symbol graph: ${symbolGraphError}`, "Results below may be stale or incomplete until a rebuild succeeds.", "", result, ].join("\n"); } catch (err) { if (err instanceof ExplicitGraphBuildRequiredError) return err.message; if (err instanceof SymbolGraphGenerationChangedError && SYMBOL_GRAPH_TOOLS.has(name) && attempt === 0) { dropSymbolGraphCacheGeneration(err.projectId, err.generation); continue; } if (err instanceof StorageReadError) { return `Storage error: ${err.message}`; } throw err; } } return "Storage error: symbol graph generation changed repeatedly while the query was starting"; } async function dispatchGraphTool(name, args) { const projectPath = path.resolve(args.projectPath || process.cwd()); // Auto-start watcher on any graph interaction (fire-and-forget) ensureWatcherStarted(projectPath); switch (name) { case "codebase_graph_build": { const resolved = path.resolve(projectPath); // Concurrency guard: if already building, show progress if (isGraphBuildInProgress(resolved)) { const progress = getGraphBuildProgress(resolved); const lines = [ `⚠ Graph build already in progress for: ${resolved}`, ]; if (progress) { const elapsed = ((Date.now() - progress.startedAt) / 1000).toFixed(0); const pct = progress.filesTotal > 0 ? ` (${Math.round((progress.filesProcessed / progress.filesTotal) * 100)}%)` : ""; const skippedNote = progress.filesSkipped ? ` — ${progress.filesSkipped} skipped` : ""; lines.push(`Phase: ${progress.phase}`); lines.push(`Progress: ${progress.filesProcessed}/${progress.filesTotal} files${pct}${skippedNote}`); lines.push(`Elapsed: ${elapsed}s`); } lines.push("", "Call codebase_graph_status to check progress."); return lines.join("\n"); } // Fire-and-forget: start graph build in the background const extraExts = mergeExtraExtensions(args.extraExtensions); rebuildGraph(resolved, extraExts.size > 0 ? extraExts : undefined) .then((graph) => { logger.info("Background graph build completed", { projectPath: resolved, nodes: graph.nodes.length, edges: graph.edges.length, filesSkipped: getLastGraphBuildCompleted(resolved)?.filesSkipped ?? 0, }); }) .catch((err) => { const message = err instanceof Error ? err.message : String(err); logger.error("Background graph build failed", { projectPath: resolved, error: message }); }); return [ `Graph build started in the background for: ${resolved}`, "", "IMPORTANT: The graph is now building asynchronously.", "Call codebase_graph_status to check progress. Keep calling it periodically until the build completes.", "Once complete, you can use codebase_graph_query, codebase_graph_stats, etc. to explore the graph.", ].join("\n"); } case "codebase_graph_query": { const filePath = args.filePath; const graph = await getGraphForRead(projectPath); const deps = getFileDependencies(graph, filePath); const lines = [`Dependencies for: ${filePath}\n`]; if (deps.imports.length === 0 && deps.importedBy.length === 0) { lines.push("No dependency information found for this file."); lines.push("Make sure codebase_graph_build has been run and the file path is relative."); } else { if (deps.imports.length > 0) { lines.push(`Imports (${deps.imports.length}):`); for (const imp of deps.imports) { lines.push(` → ${imp}`); } } if (deps.importedBy.length > 0) { lines.push(`\nImported by (${deps.importedBy.length}):`); for (const dep of deps.importedBy) { lines.push(` ← ${dep}`); } } } return lines.join("\n"); } case "codebase_graph_stats": { const graph = await getGraphForRead(projectPath); if (graph.nodes.length === 0) { return "No graph data available. Run codebase_graph_build first."; } const stats = getGraphStats(graph); const lines = [ `Code Graph Statistics for: ${projectPath}\n`, `Total files: ${stats.totalFiles}`, `Total dependency edges: ${stats.totalEdges}`, `Average dependencies per file: ${stats.avgDependencies.toFixed(1)}`, `Circular dependency chains: ${stats.circularDeps}`, ]; if (Object.keys(stats.languageBreakdown).length > 0) { lines.push("", "Languages:"); for (const [lang, count] of Object.entries(stats.languageBreakdown).sort((a, b) => b[1] - a[1])) { lines.push(` ${lang}: ${count} files`); } } lines.push("", `Most connected files (top 10):`); for (const f of stats.mostConnected) { lines.push(` ${f.file}: ${f.connections} connections`); } if (stats.orphans.length > 0) { lines.push(""); lines.push(`Orphan files (no dependencies, showing first 20):`); for (const f of stats.orphans.slice(0, 20)) { lines.push(` ${f}`); } if (stats.orphans.length > 20) { lines.push(` ... and ${stats.orphans.length - 20} more`); } } return lines.join("\n"); } case "codebase_graph_circular": { const graph = await getGraphForRead(projectPath); const cycles = findCircularDependencies(graph); if (cycles.length === 0) { return "No circular dependencies found."; } const lines = [`Found ${cycles.length} circular dependency chain(s):\n`]; for (let i = 0; i < Math.min(cycles.length, 20); i++) { lines.push(`Cycle ${i + 1}: ${cycles[i].join(" → ")}`); } if (cycles.length > 20) { lines.push(`\n... and ${cycles.length - 20} more cycles`); } return lines.join("\n"); } case "codebase_graph_visualize": { const graph = await getGraphForRead(projectPath); if (graph.nodes.length === 0) { return "No graph data available. Run codebase_graph_build first."; } const rawMode = String(args.mode ?? "mermaid").toLowerCase(); if (rawMode !== "mermaid" && rawMode !== "interactive") { return `Invalid mode "${rawMode}". Must be "mermaid" (default — returns Mermaid text) or "interactive" (generates a self-contained HTML page and opens it in the browser).`; } if (rawMode === "interactive") { const projectId = projectIdFromPath(projectPath); const projectName = path.basename(projectPath); const { html, stats } = await buildInteractiveGraphHtml({ projectPath, projectName, projectId, graph }); const file = await writeInteractiveGraphFile(projectId, html); const noOpenRequested = args.open === false || args.open === "false"; const openResult = noOpenRequested ? { opened: false } : await openInBrowser(file); const lines = [ `Interactive dependency graph for: ${projectPath}`, `File written: ${file}`, `Stats: ${stats.files} files · ${stats.fileEdges} edges · symbol view: ${stats.symbolMode}${stats.symbolMode === "full" ? ` (${stats.symbols} symbols / ${stats.symbolEdges} calls)` : ""}`, ]; if (noOpenRequested) { lines.push("", "Browser auto-open skipped (open=false). Open the file above manually."); } else if (openResult.opened) { lines.push("", "Opened in your default browser. If nothing appears, open the file path above manually."); } else { lines.push("", `Could not auto-open a browser (${openResult.error}). Open the file path above manually.`); } lines.push("", "Interactions:", " • Click a node → sidebar with file/symbol details, symbols list, and action buttons.", " • Right-click a node → highlight its blast radius (reverse-transitive closure).", " • Toggle Files ↔ Symbols at the top (Symbols disabled on gigantic repos — use codebase_impact for those).", " • Search box filters nodes live; layout dropdown switches between Dagre / force / concentric / grid.", " • Export PNG button produces a shareable image of the current view."); return lines.join("\n"); } const mermaid = generateMermaidDiagram(graph); return [ `Dependency graph for: ${projectPath}`, `(${graph.nodes.length} files, ${graph.edges.length} edges)`, "", "```mermaid", mermaid, "```", ].join("\n"); } case "codebase_graph_remove": { // Wait for any in-flight graph build to finish before removing if (isGraphBuildInProgress(projectPath)) { logger.info("Waiting for in-flight graph build to finish before removing graph", { projectPath }); await awaitGraphBuild(projectPath); } await removeGraph(projectPath); return `Removed code graph for: ${projectPath}`; } case "codebase_graph_status": { const resolved = path.resolve(projectPath); // Trigger grammar registration so the diagnostic block below reflects // the real loader state. Idempotent and cheap after the first call. ensureDynamicLanguages(); const grammarStatus = getDynamicLanguageStatus(); const BUILTIN_AST_LANGUAGES = [ "css", "html", "javascript", "tsx", "typescript", ]; const renderGrammarBlock = () => { const block = ["", "AST grammars:"]; block.push(` Built-in (ast-grep, ${BUILTIN_AST_LANGUAGES.length}): ${BUILTIN_AST_LANGUAGES.join(", ")}`); if (grammarStatus.loaded.length > 0) { block.push(` Dynamic loaded (${grammarStatus.loaded.length}): ${grammarStatus.loaded.join(", ")}`); } if (grammarStatus.failed.length > 0) { block.push(` Dynamic failed (${grammarStatus.failed.length}):`); for (const f of grammarStatus.failed) { block.push(` - ${f.name}: ${f.error}`); } block.push(" Symbols and imports for failed languages will be empty until the underlying load error is resolved."); } return block; }; // Show in-flight build progress if building if (isGraphBuildInProgress(resolved)) { const progress = getGraphBuildProgress(resolved); if (!progress) return "No progress data available."; const elapsed = ((Date.now() - progress.startedAt) / 1000).toFixed(0); const pct = progress.filesTotal > 0 ? Math.round((progress.filesProcessed / progress.filesTotal) * 100) : 0; const buildingLines = [ `Code Graph Status for: ${resolved}`, "", `Status: BUILDING`, `Phase: ${progress.phase}`, `Progress: ${progress.filesProcessed}/${progress.filesTotal} files (${pct}%)${progress.filesSkipped ? ` — ${progress.filesSkipped} skipped` : ""}`, `Elapsed: ${elapsed}s`, ...renderGrammarBlock(), "", "The graph is being built in the background.", "Call codebase_graph_status again to check progress.", ]; return buildingLines.join("\n"); } // Show last completed build info if available const lastBuild = getLastGraphBuildCompleted(resolved); const graphInfo = await getGraphStatus(resolved); if (!graphInfo) { const lines = [`No code graph found for: ${resolved}`]; if (lastBuild?.error) { lines.push(`Last build failed: ${lastBuild.error}`); } lines.push("Run codebase_graph_build or codebase_index to create one."); lines.push(...renderGrammarBlock()); return lines.join("\n"); } // READY is emitted on graph existence alone, so a graph that resolved // almost none of the imports it captured looks exactly like a healthy // one, and consumers that gate on status send users to tools that answer // empty — which reads as "nothing depends on this" rather than "the // resolver failed" (issue #107). Stated directly beneath the edge count // it qualifies, so the number is never read on its own. Deliberately not // a DEGRADED status: a hard state would false-positive on legitimately // orphan-heavy projects. const renderImportResolutionBlock = () => { // Captured before the predicate rather than asserted after it: the // predicate already rejects an absent count, but TypeScript cannot // narrow through the call, and an assertion here would keep compiling // while printing "undefined" if the predicate ever started accepting // one. const captured = graphInfo.importCount; if (captured === undefined) return []; if (!isImportResolutionLow(graphInfo.edgeCount, captured)) return []; const pct = ((graphInfo.edgeCount / captured) * 100).toFixed(1); return [ `Import resolution: ${graphInfo.edgeCount} of ${captured} captured imports resolved to project files (${pct}%)`, " Most imports did not resolve, so codebase_graph_query, codebase_graph_stats and codebase_impact will under-report dependencies — an empty answer there means unresolved, not independent.", " Expected when a project's imports are mostly external (stdlib, third-party); otherwise the resolver may not support this project's layout.", ]; }; const ago = ((Date.now() - new Date(graphInfo.lastBuiltAt).getTime()) / 1000).toFixed(0); const lines = [ `Code Graph Status for: ${resolved}`, "", `Status: READY`, `Files (nodes): ${graphInfo.nodeCount}`, `Dependencies (edges): ${graphInfo.edgeCount}`, ...renderImportResolutionBlock(), `Last built: ${graphInfo.lastBuiltAt} (${ago}s ago)`, // Which build produced the graph being served, next to when it was // produced. A persisted graph outlives the binary that wrote it, so // after an upgrade every other signal here — READY, and the current // version from codebase_about — describes the server rather than the // artifact, and a graph cut before a resolver shipped reads as that // resolver being broken (issue #120). ...describeGraphBuilder(graphInfo.builtByVersion, SOCRATICODE_VERSION), `In-memory cache: ${graphInfo.cached ? "yes" : "no (will load from storage on next query)"}`, ]; if (lastBuild) { lines.push(`Last build duration: ${(lastBuild.durationMs / 1000).toFixed(1)}s`); if (lastBuild.filesSkipped) { lines.push(`Files skipped: ${lastBuild.filesSkipped}`); } if (lastBuild.symbolGraphError) { // The file-import graph above is real; the symbol half is not. Say so // here rather than letting codebase_impact answer "0 callers" as if // that were a finding. lines.push(`Symbol graph FAILED to persist: ${lastBuild.symbolGraphError}`, " Symbol-level tools (codebase_impact, codebase_flow, codebase_symbol) will be incomplete or empty until a rebuild succeeds."); } } if (graphInfo.symbol) { const sm = graphInfo.symbol; lines.push(""); lines.push("Symbol graph (Impact Analysis):"); lines.push(` Files: ${sm.fileCount}`); lines.push(` Symbols: ${sm.symbolCount}`); lines.push(` Call edges: ${sm.edgeCount}`); lines.push(...describeUnresolvedSymbolEdges(sm.unresolvedEdgePct)); } lines.push(...renderGrammarBlock()); return lines.join("\n"); } case "codebase_impact": { const target = args.target?.trim() ?? ""; const file = args.file?.trim(); const symbolId = args.symbolId?.trim(); if (!target && !symbolId) return "Missing required argument: target or symbolId"; const depth = typeof args.depth === "number" ? args.depth : 3; const limit = typeof args.limit === "number" ? args.limit : undefined; const projectId = projectIdFromPath(projectPath); const cache = await getSymbolGraphCache(projectId); if (!cache) { return "No symbol graph found. Run codebase_graph_build (or codebase_index) first."; } ensureDynamicLanguages(); const grammarStatus = getDynamicLanguageStatus(); const isIncomplete = hasRelevantGrammarFailure(grammarStatus.failed, target, file, symbolId) || (cache.meta.schemaVersion ? cache.meta.schemaVersion < 2 : true); const result = await getImpactRadius(cache, target || symbolId, depth, { file, symbolId, isIncomplete, limit }); if (result.status === "graph_upgrade_required") { return result.message ?? "The symbol graph requires an upgrade. Rebuild with codebase_graph_build."; } if (result.status === "storage_error") { return `Storage error: ${result.message}`; } if (result.status === "not_found") { return result.message ?? `Target '${target || symbolId}' was not found in the graph.`; } if (result.status === "ambiguous") { const lines = [ `Target '${target}' is ambiguous:`, result.message ?? "", ]; if (result.candidates && result.candidates.length > 0) { lines.push(""); if (limit !== undefined && (result.totalCandidates ?? 0) > limit) { lines.push(showingLine(result.candidates.length, result.totalCandidates ?? 0, "candidates for", target), ""); } lines.push("Candidates:"); for (const c of result.candidates) { lines.push(` - [${c.kind}] ${c.qualifiedName} in ${c.file}:${c.line} (ID: ${c.id})`); } } return lines.join("\n").trimEnd(); } if (result.status === "unsupported_or_incomplete") { return result.message ?? `Graph analysis for '${target || symbolId}' is incomplete.`; } const lines = [ `Blast radius for ${result.targetKind}: ${result.target}`, `Depth: ${result.depth} Total impacted files: ${result.totalFiles}`, "", ]; if (result.totalFiles === 0) { lines.push("No callers or references found — nothing else depends on this."); } else { for (const [hop, files] of result.filesByDepth.entries()) { lines.push(`Hop ${hop} (${files.length} files):`); for (const f of files) lines.push(` - ${f}`); lines.push(""); } } return lines.join("\n").trimEnd(); } case "codebase_flow": { const projectId = projectIdFromPath(projectPath); const cache = await getSymbolGraphCache(projectId); if (!cache) { return "No symbol graph found. Run codebase_graph_build (or codebase_index) first."; } const entrypoint = args.entrypoint?.trim(); // Zero-arg mode → ranked entry-point list if (!entrypoint) { const release = cache.acquireReader(); const readerToken = release.token; try { // Build a fresh detection using the file graph + per-file payloads from the cache. // For efficiency we only list entry points by walking known symbols via the name index. const fileGraph = await getGraphForRead(projectPath); const nameIndex = await cache.getNameIndex(readerToken); const seenFiles = new Set(); const payloads = []; for (const refs of nameIndex.values()) { for (const ref of refs) { if (seenFiles.has(ref.file)) continue; seenFiles.add(ref.file); const p = await cache.getFilePayload(ref.file, readerToken); if (p) payloads.push(p); } } const entries = detectEntryPoints(fileGraph, payloads); if (entries.length === 0) { return "No entry points detected. The codebase may not have orphan files, conventional main() functions, or framework routes."; } const lines = [`Detected ${entries.length} entry point(s):`, ""]; for (const e of entries.slice(0, 50)) { lines.push(` ${e.name} (${e.file}${e.line ? `:${e.line}` : ""}) — ${e.reason}`); } if (entries.length > 50) lines.push(` ... and ${entries.length - 50} more`); lines.push("", "Pass `entrypoint` to trace forward call flow from any of these."); return lines.join("\n"); } finally { release(); } } // Hold one generation across name resolution and the complete traversal. const release = cache.acquireReader(); const readerToken = release.token; try { const nameIndex = await cache.getNameIndex(readerToken); let refs = nameIndex.get(entrypoint) ?? []; const fileHint = args.file?.trim(); if (fileHint) refs = refs.filter((r) => r.file === fileHint); if (refs.length === 0) { return `No symbol named "${entrypoint}" found${fileHint ? ` in ${fileHint}` : ""}.`; } if (refs.length > 1) { const limit = typeof args.limit === "number" ? args.limit : undefined; const listed = limit === undefined ? refs : refs.slice(0, limit); const lines = [`Symbol "${entrypoint}" is ambiguous (${refs.length} matches). Pass \`file\` to disambiguate:`, ""]; if (listed.length < refs.length) lines.push(showingLine(listed.length, refs.length, "candidates for", entrypoint), ""); for (const r of listed) lines.push(` - ${r.file}`); return lines.join("\n"); } const depth = typeof args.depth === "number" ? args.depth : 5; const tree = await getCallFlow(cache, refs[0].id, depth, readerToken); if (!tree) return `Could not load symbol "${entrypoint}".`; const lines = [`Call flow from ${tree.symbolName} (${tree.file}:${tree.line})`, ""]; renderFlowTree(tree, "", true, lines); return lines.join("\n"); } finally { release(); } } case "codebase_symbol": { const symName = args.name?.trim(); if (!symName) return "Missing required argument: name"; const fileHint = args.file?.trim(); const projectId = projectIdFromPath(projectPath); const cache = await getSymbolGraphCache(projectId); if (!cache) { return "No symbol graph found. Run codebase_graph_build (or codebase_index) first."; } const limit = typeof args.limit === "number" ? args.limit : undefined; const { contexts: ctxs, total, fileCount } = await getSymbolContextResult(cache, symName, fileHint, limit); if (ctxs.length === 0) { return `No symbol named "${symName}" found${fileHint ? ` in ${fileHint}` : ""}.`; } const lines = []; if (limit !== undefined && total > limit) { const narrow = fileCount > 1 ? (fileHint ? NARROW_BY_LONGER_FILE : NARROW_BY_FILE) : undefined; lines.push(showingLine(ctxs.length, total, "definitions of", symName, narrow), ""); } for (const ctx of ctxs) { lines.push(`Symbol: ${ctx.symbol.qualifiedName} (${ctx.symbol.kind})`); lines.push(`Defined: ${ctx.symbol.file}:${ctx.symbol.line}–${ctx.symbol.endLine} [${ctx.symbol.language}]`); lines.push(""); lines.push(`Callers (${ctx.callers.length}):`); if (ctx.callers.length === 0) lines.push(" (none — possibly an entry point or unused)"); else for (const c of ctx.callers.slice(0, 30)) lines.push(` ← ${c.file}:${c.line}${c.kind ? ` (${c.kind})` : ""}`); if (ctx.callers.length > 30) lines.push(` ... and ${ctx.callers.length - 30} more`); lines.push(""); lines.push(`Callees (${ctx.callees.length}):`); if (ctx.callees.length === 0) lines.push(" (none)"); else for (const c of ctx.callees.slice(0, 30)) { lines.push(` → ${c.name} [${c.confidence}${c.resolved.length > 0 ? `, ${c.resolved.length} candidate(s)` : ""}${c.kind ? `, ${c.kind}` : ""}]`); } if (ctx.callees.length > 30) lines.push(` ... and ${ctx.callees.length - 30} more`); lines.push("---"); } return lines.join("\n").replace(/---\n?$/, "").trimEnd(); } case "codebase_symbols": { const file = args.file?.trim(); const query = args.query?.trim(); const limit = typeof args.limit === "number" ? args.limit : 200; const projectId = projectIdFromPath(projectPath); const cache = await getSymbolGraphCache(projectId); if (!cache) { return "No symbol graph found. Run codebase_graph_build (or codebase_index) first."; } const symbols = await listSymbols(cache, { file, query, limit }); if (symbols.length === 0) { return file ? `No symbols found in ${file}.` : query ? `No symbols matching "${query}".` : "No symbols found."; } const lines = [ file ? `Symbols in ${file} (${symbols.length}):` : `Symbols matching "${query ?? "*"}" (${symbols.length}):`, "", ]; for (const s of symbols) { lines.push(` ${s.kind.padEnd(11)} ${s.qualifiedName.padEnd(40)} ${s.file}:${s.line}`); } return lines.join("\n"); } default: return `Unknown tool: ${name}`; } } const NARROW_BY_FILE = "Pass file to narrow to one file"; const NARROW_BY_LONGER_FILE = "Pass a longer file path to narrow to one file"; /** Format the notice that only `shown` of `total` matches are listed; `narrow`, when given, comes before the larger-limit advice. */ function showingLine(shown, total, noun, name, narrow) { return `Showing ${shown} of ${total} ${noun} "${name}". ${narrow ? `${narrow}, or pass` : "Pass"} a larger limit to see more.`; } /** Render a FlowNode subtree using ASCII tree characters. */ function renderFlowTree(node, prefix, isLast, out) { const branch = isLast ? "└── " : "├── "; const suffix = node.truncatedReason ? ` [truncated: ${node.truncatedReason}]` : ""; out.push(`${prefix}${branch}${node.symbolName} (${node.file}:${node.line})${suffix}`); const childPrefix = prefix + (isLast ? " " : "│ "); for (let i = 0; i < node.children.length; i++) { renderFlowTree(node.children[i], childPrefix, i === node.children.length - 1, out); } } // Mark deprecated import as used to satisfy lint when no other reference exists void looksLikeFilePath; //# sourceMappingURL=graph-tools.js.map