Skip to content
51 changes: 36 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -287,31 +287,52 @@ Structured filters available: `framework`, `language`, `componentType`, `layer`
!.codebase-context/memory.json
```

## CLI Access (Vendor-Neutral)
## CLI Reference

You can manage team memory directly from the terminal without any AI agent:
All MCP tools are available as CLI commands — no AI agent required. Useful for scripting, debugging, and CI workflows.

Set `CODEBASE_ROOT` to your project root, or run from the project directory.

```bash
# List all memories
npx codebase-context memory list
# Search the indexed codebase
npx codebase-context search --query "authentication middleware"
npx codebase-context search --query "auth" --intent edit --limit 5

# Filter by category or type
npx codebase-context memory list --category conventions --type convention
# Project structure, frameworks, and dependencies
npx codebase-context metadata

# Search memories
npx codebase-context memory list --query "auth"
# Index state and progress
npx codebase-context status

# Add a memory
npx codebase-context memory add --type convention --category tooling --memory "Use pnpm, not npm" --reason "Workspace support and speed"
# Re-index the codebase
npx codebase-context reindex
npx codebase-context reindex --incremental --reason "added new service"

# Remove a memory
npx codebase-context memory remove <id>
# Style guide rules
npx codebase-context style-guide
npx codebase-context style-guide --query "naming" --category patterns

# Team patterns (DI, state, testing, etc.)
npx codebase-context patterns
npx codebase-context patterns --category testing

# JSON output for scripting
npx codebase-context memory list --json
# Symbol references
npx codebase-context refs --symbol "UserService"
npx codebase-context refs --symbol "handleLogin" --limit 20

# Circular dependency detection
npx codebase-context cycles
npx codebase-context cycles --scope src/features

# Memory management
npx codebase-context memory list
npx codebase-context memory list --category conventions --type convention
npx codebase-context memory list --query "auth" --json
npx codebase-context memory add --type convention --category tooling --memory "Use pnpm, not npm" --reason "Workspace support and speed"
npx codebase-context memory remove <id>
```

Set `CODEBASE_ROOT` to point to your project, or run from the project directory.
All commands accept `--json` for raw JSON output suitable for piping and scripting.

## Tip: Ensuring your AI Agent recalls memory:

Expand Down
29 changes: 29 additions & 0 deletions docs/capabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,35 @@

Technical reference for what `codebase-context` ships today. For the user-facing overview, see [README.md](../README.md).

## CLI Reference

All 10 MCP tools are exposed as CLI subcommands. Set `CODEBASE_ROOT` or run from the project directory.

| Command | Flags | Maps to |
|---|---|---|
| `search --query <q>` | `--intent explore\|edit\|refactor\|migrate`, `--limit <n>`, `--lang <l>`, `--framework <f>`, `--layer <l>` | `search_codebase` |
| `metadata` | — | `get_codebase_metadata` |
| `status` | — | `get_indexing_status` |
| `reindex` | `--incremental`, `--reason <r>` | `refresh_index` |
| `style-guide` | `--query <q>`, `--category <c>` | `get_style_guide` |
| `patterns` | `--category all\|di\|state\|testing\|libraries` | `get_team_patterns` |
| `refs --symbol <name>` | `--limit <n>` | `get_symbol_references` |
| `cycles` | `--scope <path>` | `detect_circular_dependencies` |
| `memory list` | `--category`, `--type`, `--query`, `--json` | — |
| `memory add` | `--type`, `--category`, `--memory`, `--reason` | `remember` |
| `memory remove <id>` | — | — |

All commands accept `--json` for raw JSON output. Errors go to stderr with exit code 1.

```bash
# Quick examples
npx codebase-context status
npx codebase-context search --query "auth middleware" --intent edit
npx codebase-context refs --symbol "UserService" --limit 10
npx codebase-context cycles --scope src/features
npx codebase-context reindex --incremental
```

## Tool Surface

10 MCP tools + 1 optional resource (`codebase://context`). **Migration:** `get_component_usage` was removed; use `get_symbol_references` for symbol usage evidence.
Expand Down
272 changes: 271 additions & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,288 @@
/**
* CLI subcommands for codebase-context.
* Memory list/add/remove — vendor-neutral access without any AI agent.
* search/metadata/status/reindex/style-guide/patterns/refs/cycles — all MCP tools.
*/

import path from 'path';
import { promises as fs } from 'fs';
import type { Memory, MemoryCategory, MemoryType } from './types/index.js';
import { CODEBASE_CONTEXT_DIRNAME, MEMORY_FILENAME } from './constants/codebase-context.js';
import {
CODEBASE_CONTEXT_DIRNAME,
MEMORY_FILENAME,
INTELLIGENCE_FILENAME,
KEYWORD_INDEX_FILENAME,
VECTOR_DB_DIRNAME
} from './constants/codebase-context.js';
import {
appendMemoryFile,
readMemoriesFile,
removeMemory,
filterMemories,
withConfidence
} from './memory/store.js';
import { CodebaseIndexer } from './core/indexer.js';
import { dispatchTool } from './tools/index.js';
import type { ToolContext } from './tools/index.js';
import type { IndexState } from './tools/types.js';
import { analyzerRegistry } from './core/analyzer-registry.js';
import { AngularAnalyzer } from './analyzers/angular/index.js';
import { GenericAnalyzer } from './analyzers/generic/index.js';

analyzerRegistry.register(new AngularAnalyzer());
analyzerRegistry.register(new GenericAnalyzer());

const CLI_COMMANDS = [
'memory',
'search',
'metadata',
'status',
'reindex',
'style-guide',
'patterns',
'refs',
'cycles'
] as const;

type CliCommand = (typeof CLI_COMMANDS)[number];

function printUsage(): void {
console.log('codebase-context <command> [options]');
console.log('');
console.log('Commands:');
console.log(' memory <list|add|remove> Memory CRUD');
console.log(
' search --query <q> Search the indexed codebase'
);
console.log(' [--intent explore|edit|refactor|migrate]');
console.log(' [--limit <n>] [--lang <l>] [--framework <f>] [--layer <l>]');
console.log(' metadata Project structure, frameworks, deps');
console.log(' status Index state and progress');
console.log(' reindex [--incremental] [--reason <r>] Re-index the codebase');
console.log(' style-guide [--query <q>] [--category <c>] Style guide rules');
console.log(
' patterns [--category all|di|state|testing|libraries] Team patterns'
);
console.log(' refs --symbol <name> [--limit <n>] Symbol references');
console.log(' cycles [--scope <path>] Circular dependency detection');
console.log('');
console.log('Global flags:');
console.log(' --json Output raw JSON (default: human-readable)');
console.log(' --help Show this help');
console.log('');
console.log('Environment:');
console.log(' CODEBASE_ROOT Project root path (default: cwd)');
}

async function initToolContext(): Promise<ToolContext> {
const rootPath = path.resolve(process.env.CODEBASE_ROOT || process.cwd());

const paths = {
baseDir: path.join(rootPath, CODEBASE_CONTEXT_DIRNAME),
memory: path.join(rootPath, CODEBASE_CONTEXT_DIRNAME, MEMORY_FILENAME),
intelligence: path.join(rootPath, CODEBASE_CONTEXT_DIRNAME, INTELLIGENCE_FILENAME),
keywordIndex: path.join(rootPath, CODEBASE_CONTEXT_DIRNAME, KEYWORD_INDEX_FILENAME),
vectorDb: path.join(rootPath, CODEBASE_CONTEXT_DIRNAME, VECTOR_DB_DIRNAME)
};

// Check if index exists to determine initial status
let indexExists = false;
try {
await fs.access(paths.keywordIndex);
indexExists = true;
} catch {
// no index on disk
}

const indexState: IndexState = {
status: indexExists ? 'ready' : 'idle'
};

const performIndexing = async (incrementalOnly?: boolean): Promise<void> => {
indexState.status = 'indexing';
const mode = incrementalOnly ? 'incremental' : 'full';
console.error(`Indexing (${mode}): ${rootPath}`);

try {
let lastLoggedProgress = { phase: '', percentage: -1 };
const indexer = new CodebaseIndexer({
rootPath,
incrementalOnly,
onProgress: (progress) => {
const shouldLog =
progress.phase !== lastLoggedProgress.phase ||
(progress.percentage % 10 === 0 &&
progress.percentage !== lastLoggedProgress.percentage);
if (shouldLog) {
console.error(`[${progress.phase}] ${progress.percentage}%`);
lastLoggedProgress = { phase: progress.phase, percentage: progress.percentage };
}
}
});

indexState.indexer = indexer;
const stats = await indexer.index();
indexState.status = 'ready';
indexState.lastIndexed = new Date();
indexState.stats = stats;

console.error(
`Complete: ${stats.indexedFiles} files, ${stats.totalChunks} chunks in ${(
stats.duration / 1000
).toFixed(2)}s`
);
} catch (error) {
indexState.status = 'error';
indexState.error = error instanceof Error ? error.message : String(error);
console.error('Indexing failed:', indexState.error);
}
};

return { indexState, paths, rootPath, performIndexing };
}

function extractText(result: { content?: Array<{ type: string; text: string }> }): string {
return result.content?.[0]?.text ?? '';
}

function formatJson(json: string, useJson: boolean): void {
if (useJson) {
console.log(json);
return;
}
// Pretty-print already-formatted JSON as-is (it's already readable)
console.log(json);
}

export async function handleCliCommand(argv: string[]): Promise<void> {
const command = argv[0] as CliCommand | '--help' | undefined;

if (!command || command === '--help') {
printUsage();
return;
}

if (command === 'memory') {
return handleMemoryCli(argv.slice(1));
}

const useJson = argv.includes('--json');

// Parse flags into a map
const flags: Record<string, string | boolean> = {};
for (let i = 1; i < argv.length; i++) {
const arg = argv[i];
if (arg === '--json') continue;
if (arg.startsWith('--')) {
const key = arg.slice(2);
const next = argv[i + 1];
if (next && !next.startsWith('--')) {
flags[key] = next;
i++;
} else {
flags[key] = true;
}
}
}

const ctx = await initToolContext();

let toolName: string;
let toolArgs: Record<string, unknown> = {};

switch (command) {
case 'search': {
if (!flags['query']) {
console.error('Error: --query is required');
console.error('Usage: codebase-context search --query <text> [--intent <i>] [--limit <n>]');
process.exit(1);
}
toolName = 'search_codebase';
toolArgs = {
query: flags['query'],
...(flags['intent'] ? { intent: flags['intent'] } : {}),
...(flags['limit'] ? { limit: Number(flags['limit']) } : {}),
...(flags['lang'] ? { filters: { language: flags['lang'] } } : {}),
...(flags['framework'] ? { filters: { framework: flags['framework'] } } : {}),
...(flags['layer'] ? { filters: { layer: flags['layer'] } } : {})
};
break;
}
case 'metadata': {
toolName = 'get_codebase_metadata';
break;
}
case 'status': {
toolName = 'get_indexing_status';
break;
}
case 'reindex': {
toolName = 'refresh_index';
toolArgs = {
...(flags['incremental'] ? { incrementalOnly: true } : {}),
...(flags['reason'] ? { reason: flags['reason'] } : {})
};
// For CLI, reindex must be awaited (fire-and-forget won't work in a process that exits)
await ctx.performIndexing(Boolean(flags['incremental']));
const statusResult = await dispatchTool('get_indexing_status', {}, ctx);
formatJson(extractText(statusResult), useJson);
return;
}
case 'style-guide': {
toolName = 'get_style_guide';
toolArgs = {
...(flags['query'] ? { query: flags['query'] } : {}),
...(flags['category'] ? { category: flags['category'] } : {})
};
break;
}
case 'patterns': {
toolName = 'get_team_patterns';
toolArgs = {
...(flags['category'] ? { category: flags['category'] } : {})
};
break;
}
case 'refs': {
if (!flags['symbol']) {
console.error('Error: --symbol is required');
console.error('Usage: codebase-context refs --symbol <name> [--limit <n>]');
process.exit(1);
}
toolName = 'get_symbol_references';
toolArgs = {
symbol: flags['symbol'],
...(flags['limit'] ? { limit: Number(flags['limit']) } : {})
};
break;
}
case 'cycles': {
toolName = 'detect_circular_dependencies';
toolArgs = {
...(flags['scope'] ? { scope: flags['scope'] } : {})
};
break;
}
default: {
console.error(`Unknown command: ${command}`);
console.error('');
printUsage();
process.exit(1);
}
}

try {
const result = await dispatchTool(toolName, toolArgs, ctx);
if (result.isError) {
console.error(extractText(result));
process.exit(1);
}
formatJson(extractText(result), useJson);
} catch (error) {
console.error('Error:', error instanceof Error ? error.message : String(error));
process.exit(1);
}
}

export async function handleMemoryCli(args: string[]): Promise<void> {
// Resolve project root: use CODEBASE_ROOT env or cwd (argv[2] is "memory", not a path)
Expand Down
Loading
Loading