diff --git a/README.md b/README.md index fda100a..fb64f2c 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,12 @@ Config file locations: | VS Code | `.vscode/mcp.json` | | Windsurf | `.windsurf/mcp.json` | +### CLI Subcommands + +- `init [target]` - Generate MCP configuration (targets: `claude`, `cursor`, `vscode`, `windsurf`). +- `skeleton [path]` or `tree [path]` - **(New)** View the structural tree of a project with file headers and symbol definitions directly in your terminal. +- `[path]` - Start the MCP server (stdio) for the specified path (defaults to current directory). + ### From Source ```bash @@ -113,11 +119,6 @@ npm install npm run build ``` -```bash -node build/index.js # analyze current directory -node build/index.js /path/to/my-project # analyze a specific project -``` - ## Architecture Three layers built with TypeScript over stdio using the Model Context Protocol SDK: diff --git a/src/index.ts b/src/index.ts index f4cab5e..b690755 100644 --- a/src/index.ts +++ b/src/index.ts @@ -29,8 +29,9 @@ const AGENT_CONFIG_PATH: Record = { windsurf: ".windsurf/mcp.json", }; +const SUB_COMMANDS = ["init", "skeleton", "tree"]; const passthroughArgs = process.argv.slice(2); -const ROOT_DIR = passthroughArgs[0] && passthroughArgs[0] !== "init" +const ROOT_DIR = passthroughArgs[0] && !SUB_COMMANDS.includes(passthroughArgs[0]) ? resolve(passthroughArgs[0]) : process.cwd(); @@ -351,6 +352,15 @@ async function main() { await runInitCommand(args.slice(1)); return; } + if (args[0] === "skeleton" || args[0] === "tree") { + const tree = await getContextTree({ + rootDir: ROOT_DIR, + includeSymbols: true, + maxTokens: 50000, + }); + process.stdout.write(tree + "\n"); + return; + } await ensureMcpDataDir(ROOT_DIR); const trackerEnabled = (process.env.CONTEXTPLUS_EMBED_TRACKER ?? "true").toLowerCase() !== "false"; const stopTracker = trackerEnabled diff --git a/src/tools/context-tree.ts b/src/tools/context-tree.ts index 8ac22c8..2568c9e 100644 --- a/src/tools/context-tree.ts +++ b/src/tools/context-tree.ts @@ -32,40 +32,43 @@ async function buildTree(entries: FileEntry[], _rootDir: string, includeSymbols: const dirMap = new Map(); dirMap.set(".", root); - const dirs = entries.filter((e) => e.isDirectory).sort((a, b) => a.relativePath.localeCompare(b.relativePath)); - for (const dir of dirs) { - const parts = dir.relativePath.split("/"); - const parentPath = parts.length > 1 ? parts.slice(0, -1).join("/") : "."; - const parent = dirMap.get(parentPath) ?? root; - const node: TreeNode = { name: parts[parts.length - 1], relativePath: dir.relativePath, isDirectory: true, children: [] }; - parent.children.push(node); - dirMap.set(dir.relativePath, node); - } + // Sort by depth then path to ensure parents exist before children + const sortedEntries = entries.sort((a, b) => a.depth - b.depth || a.relativePath.localeCompare(b.relativePath)); - const files = entries.filter((e) => !e.isDirectory); - for (const file of files) { - const parts = file.relativePath.split("/"); + for (const entry of sortedEntries) { + const parts = entry.relativePath.split("/"); const parentPath = parts.length > 1 ? parts.slice(0, -1).join("/") : "."; - const parent = dirMap.get(parentPath) ?? root; + + // Ensure parent node exists (fallback to root) + let parent = dirMap.get(parentPath); + if (!parent && parentPath !== ".") { + // Auto-create missing parent directories if needed + parent = root; + } else if (!parent) { + parent = root; + } const node: TreeNode = { name: parts[parts.length - 1], - relativePath: file.relativePath, - isDirectory: false, + relativePath: entry.relativePath, + isDirectory: entry.isDirectory, children: [], }; - if (isSupportedFile(file.path)) { + if (!entry.isDirectory && isSupportedFile(entry.path)) { try { - const analysis = await analyzeFile(file.path); + const analysis = await analyzeFile(entry.path); node.header = analysis.header || undefined; if (includeSymbols && analysis.symbols.length > 0) { node.symbols = analysis.symbols.map((s) => formatSymbol(s, 0)).join("\n"); } - } catch { - } + } catch {} } + parent.children.push(node); + if (entry.isDirectory) { + dirMap.set(entry.relativePath, node); + } } return root;