Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,18 +106,19 @@ 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
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:
Expand Down
12 changes: 11 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@ const AGENT_CONFIG_PATH: Record<AgentTarget, string> = {
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();

Expand Down Expand Up @@ -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
Expand Down
41 changes: 22 additions & 19 deletions src/tools/context-tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,40 +32,43 @@ async function buildTree(entries: FileEntry[], _rootDir: string, includeSymbols:
const dirMap = new Map<string, TreeNode>();
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;
Expand Down