#!/usr/bin/env node // Heap re-spawn removed — only analyze.ts needs the 8GB heap (via its own ensureHeap()). // Removing it from here improves MCP server startup time significantly. import { Command } from 'commander'; import { createRequire } from 'node:module'; import { createLazyAction } from './lazy-action.js'; import { registerGroupCommands } from './group.js'; const _require = createRequire(import.meta.url); const pkg = _require('../../package.json'); const program = new Command(); program.name('gitnexus').description('GitNexus local CLI and MCP server').version(pkg.version); program .command('setup') .description('One-time setup: configure MCP for Cursor, Claude Code, OpenCode, Codex') .action(createLazyAction(() => import('./setup.js'), 'setupCommand')); program .command('analyze [path]') .description('Index a repository (full analysis)') .option('-f, --force', 'Force full re-index even if up to date') .option( '--embeddings [limit]', 'Enable embedding generation for semantic search (off by default). ' + 'Optional [limit] overrides the 50,000-node safety cap; pass 0 to disable the cap entirely.', ) .option( '--drop-embeddings', 'Drop existing embeddings on rebuild. By default, an `analyze` without `--embeddings` ' + 'preserves any embeddings already present in the index.', ) .option( '--skills', 'Generate repo-specific skill files from detected communities ' + '(no-op when --index-only is also set).', ) .option('--skip-agents-md', 'Skip updating the gitnexus section in AGENTS.md and CLAUDE.md') .option('--no-stats', 'Omit volatile file/symbol counts from AGENTS.md and CLAUDE.md') .option( '--skip-skills', 'Skip installing standard GitNexus skill files under .claude/skills/gitnexus/. ' + 'Does not suppress community skills from --skills (those use .claude/skills/generated/). ' + 'Use --index-only to skip all AI-context file injection.', ) .option('--index-only', 'Pure index mode: skip all file injection (AGENTS.md, CLAUDE.md, skills)') .option( '--skip-git', 'Treat the provided path/cwd as the index root and skip parent git-root discovery', ) .option( '--name ', 'Register this repo under a custom name in ~/.gitnexus/registry.json ' + '(disambiguates repos whose paths share a basename, e.g. two different .../app folders)', ) .option( '--allow-duplicate-name', 'Register this repo even if another path already uses the same --name alias. ' + 'Leaves `-r ` ambiguous for the two paths; use -r to disambiguate.', ) .option('-v, --verbose', 'Enable verbose ingestion warnings (default: false)') .option( '--max-file-size ', 'Skip files larger than this (KB). Default: 512. Hard cap: 32768 (tree-sitter limit).', ) .option( '--worker-timeout ', 'Worker sub-batch idle timeout before retry/fallback. Default: 30.', ) .option('--embedding-threads ', 'Limit local ONNX embedding CPU threads') .option('--embedding-batch-size ', 'Number of nodes per embedding batch') .option('--embedding-sub-batch-size ', 'Number of chunks per embedding model call') .option('--embedding-device ', 'Embedding device: auto, cpu, dml, cuda, or wasm') .addHelpText( 'after', '\nEnvironment variables:\n' + ' GITNEXUS_NO_GITIGNORE=1 Skip .gitignore parsing (still reads .gitnexusignore)\n' + ' GITNEXUS_MAX_FILE_SIZE=N Override large-file skip threshold (KB). Default 512, max 32768.\n' + ' GITNEXUS_WORKER_SUB_BATCH_TIMEOUT_MS=N Worker idle timeout in milliseconds. Default 30000.\n' + ' GITNEXUS_WORKER_SUB_BATCH_MAX_BYTES=N Worker job byte budget. Default 8388608.\n' + ' GITNEXUS_EMBEDDING_THREADS=N Limit local ONNX CPU threads for --embeddings.\n' + ' GITNEXUS_SEMANTIC_EXACT_SCAN_LIMIT=N Max embedding chunks for exact-scan fallback. Default 10000.\n' + '\nTip: `.gitnexusignore` supports `.gitignore`-style negation. Add e.g.\n' + ' `!__tests__/` to index a directory that is auto-filtered by default (#771).', ) .action(createLazyAction(() => import('./analyze.js'), 'analyzeCommand')); program .command('index [path...]') .description( 'Register an existing .gitnexus/ folder into the global registry (no re-analysis needed)', ) .option('-f, --force', 'Register even if meta.json is missing (stats will be empty)') .option('--allow-non-git', 'Allow registering folders that are not Git repositories') .action(createLazyAction(() => import('./index-repo.js'), 'indexCommand')); program .command('serve') .description('Start local HTTP server for web UI connection') .option('-p, --port ', 'Port number', '4747') .option('--host ', 'Bind address (default: 127.0.0.1, use 0.0.0.0 for remote access)') .action(createLazyAction(() => import('./serve.js'), 'serveCommand')); program .command('mcp') .description('Start MCP server (stdio) — serves all indexed repos') .action(createLazyAction(() => import('./mcp.js'), 'mcpCommand')); program .command('list') .description('List all indexed repositories') .action(createLazyAction(() => import('./list.js'), 'listCommand')); program .command('status') .description('Show index status for current repo') .action(createLazyAction(() => import('./status.js'), 'statusCommand')); program .command('doctor') .description('Show runtime platform capabilities and embedding configuration') .action(createLazyAction(() => import('./doctor.js'), 'doctorCommand')); program .command('clean') .description('Delete GitNexus index for current repo') .option('-f, --force', 'Skip confirmation prompt') .option('--all', 'Clean all indexed repos') .action(createLazyAction(() => import('./clean.js'), 'cleanCommand')); program .command('remove ') .description( 'Delete the GitNexus index for a registered repo (by alias, name, or absolute path). ' + 'Unlike `clean`, does not require being inside the repo. Idempotent on unknown targets.', ) .option('-f, --force', 'Skip confirmation prompt') .action(createLazyAction(() => import('./remove.js'), 'removeCommand')); program .command('wiki [path]') .description('Generate repository wiki from knowledge graph') .option('-f, --force', 'Force full regeneration even if up to date') .option('--provider ', 'LLM provider: openai or cursor (default: openai)') .option('--model ', 'LLM model or Azure deployment name (default: minimax/minimax-m2.5)') .option( '--base-url ', 'LLM API base URL. Azure v1: https://{resource}.openai.azure.com/openai/v1', ) .option('--api-key ', 'LLM API key or Azure api-key (saved to ~/.gitnexus/config.json)') .option( '--api-version ', 'Azure api-version query param, e.g. 2024-10-21 (legacy Azure API only)', ) .option( '--reasoning-model', 'Mark deployment as reasoning model (o1/o3/o4-mini) — strips temperature, uses max_completion_tokens', ) .option('--no-reasoning-model', 'Disable reasoning model mode (overrides saved config)') .option('--concurrency ', 'Parallel LLM calls (default: 3)', '3') .option('--timeout ', 'Per-attempt LLM request timeout in seconds (default: 60)') .option('--retries ', 'Max LLM retry attempts per request (default: 3)') .option('--gist', 'Publish wiki as a public GitHub Gist after generation') .option('-v, --verbose', 'Enable verbose output (show LLM commands and responses)') .option('--review', 'Stop after grouping to review module structure before generating pages') .action(createLazyAction(() => import('./wiki.js'), 'wikiCommand')); program .command('augment ') .description('Augment a search pattern with knowledge graph context (used by hooks)') .action(createLazyAction(() => import('./augment.js'), 'augmentCommand')); program .command('publish [path]') .description( 'Notify the understand-quickly registry that this repo has a fresh GitNexus index. ' + 'Opt-in: requires UNDERSTAND_QUICKLY_TOKEN (fine-grained PAT with ' + '`Repository dispatches: write` on looptech-ai/understand-quickly). ' + 'No-op without the token. See https://github.com/looptech-ai/understand-quickly.', ) .option('--id ', 'Override the registry id (defaults to the origin remote)') .option('--skip-git', 'Treat cwd as the repo root and skip parent git-root discovery') .action(createLazyAction(() => import('./publish.js'), 'publishCommand')); // ─── Direct Tool Commands (no MCP overhead) ──────────────────────── // These invoke LocalBackend directly for use in eval, scripts, and CI. program .command('query ') .description('Search the knowledge graph for execution flows related to a concept') .option('-r, --repo ', 'Target repository (omit if only one indexed)') .option('-c, --context ', 'Task context to improve ranking') .option('-g, --goal ', 'What you want to find') .option('-l, --limit ', 'Max processes to return (default: 5)') .option('--content', 'Include full symbol source code') .action(createLazyAction(() => import('./tool.js'), 'queryCommand')); program .command('context [name]') .description('360-degree view of a code symbol: callers, callees, processes') .option('-r, --repo ', 'Target repository') .option('-u, --uid ', 'Direct symbol UID (zero-ambiguity lookup)') .option('-f, --file ', 'File path to disambiguate common names') .option('--content', 'Include full symbol source code') .action(createLazyAction(() => import('./tool.js'), 'contextCommand')); program .command('impact ') .description('Blast radius analysis: what breaks if you change a symbol') .option('-d, --direction ', 'upstream (dependants) or downstream (dependencies)', 'upstream') .option('-r, --repo ', 'Target repository') .option('--depth ', 'Max relationship depth (default: 3)') .option('--include-tests', 'Include test files in results') .action(createLazyAction(() => import('./tool.js'), 'impactCommand')); program .command('cypher ') .description('Execute raw Cypher query against the knowledge graph') .option('-r, --repo ', 'Target repository') .action(createLazyAction(() => import('./tool.js'), 'cypherCommand')); program .command('detect-changes') .alias('detect_changes') .description('Map git diff hunks to indexed symbols and affected execution flows') .option('-s, --scope ', 'What to analyze: unstaged, staged, all, or compare', 'unstaged') .option('-b, --base-ref ', 'Branch/commit for compare scope (e.g. main)') .option('-r, --repo ', 'Target repository') .action(createLazyAction(() => import('./tool.js'), 'detectChangesCommand')); // ─── Eval Server (persistent daemon for SWE-bench) ───────────────── program .command('eval-server') .description('Start lightweight HTTP server for fast tool calls during evaluation') .option('-p, --port ', 'Port number', '4848') .option('--idle-timeout ', 'Auto-shutdown after N seconds idle (0 = disabled)', '0') .action(createLazyAction(() => import('./eval-server.js'), 'evalServerCommand')); registerGroupCommands(program); program.parse(process.argv);