Navegador
feat: expand CLI with function/class/query commands, clean JSON output for agent use
Commit
e72710e6001cfc982fc8104fd9fa18ce5e5326cfe2188b1a7b7109f8db6c994e
Parent
24e2a9ecc802de6…
2 files changed
+121
-40
+2
-2
+121
-40
| --- navegador/cli/commands.py | ||
| +++ navegador/cli/commands.py | ||
| @@ -1,7 +1,10 @@ | ||
| 1 | 1 | """ |
| 2 | 2 | Navegador CLI — ingest repos, load context, serve MCP. |
| 3 | + | |
| 4 | +All commands support --format json for clean stdout output suitable for | |
| 5 | +piping to agents or other tools without MCP overhead. | |
| 3 | 6 | """ |
| 4 | 7 | |
| 5 | 8 | import asyncio |
| 6 | 9 | import logging |
| 7 | 10 | |
| @@ -9,15 +12,34 @@ | ||
| 9 | 12 | from rich.console import Console |
| 10 | 13 | from rich.table import Table |
| 11 | 14 | |
| 12 | 15 | console = Console() |
| 13 | 16 | |
| 17 | +DB_OPTION = click.option( | |
| 18 | + "--db", default=".navegador/graph.db", show_default=True, help="Graph DB path." | |
| 19 | +) | |
| 20 | +FMT_OPTION = click.option( | |
| 21 | + "--format", "fmt", | |
| 22 | + type=click.Choice(["markdown", "json"]), | |
| 23 | + default="markdown", | |
| 24 | + show_default=True, | |
| 25 | + help="Output format. Use json for agent/pipe consumption.", | |
| 26 | +) | |
| 27 | + | |
| 14 | 28 | |
| 15 | 29 | def _get_store(db: str): |
| 16 | 30 | from navegador.graph import GraphStore |
| 17 | 31 | return GraphStore.sqlite(db) |
| 18 | 32 | |
| 33 | + | |
| 34 | +def _emit(text: str, fmt: str) -> None: | |
| 35 | + """Print text — raw to stdout for json, rich for markdown.""" | |
| 36 | + if fmt == "json": | |
| 37 | + click.echo(text) | |
| 38 | + else: | |
| 39 | + console.print(text) | |
| 40 | + | |
| 19 | 41 | |
| 20 | 42 | @click.group() |
| 21 | 43 | @click.version_option(package_name="navegador") |
| 22 | 44 | def main(): |
| 23 | 45 | """Navegador — AST + knowledge graph context engine for AI coding agents.""" |
| @@ -24,56 +46,99 @@ | ||
| 24 | 46 | logging.basicConfig(level=logging.WARNING) |
| 25 | 47 | |
| 26 | 48 | |
| 27 | 49 | @main.command() |
| 28 | 50 | @click.argument("repo_path", type=click.Path(exists=True)) |
| 29 | -@click.option("--db", default=".navegador/graph.db", show_default=True, help="Graph DB path.") | |
| 51 | +@DB_OPTION | |
| 30 | 52 | @click.option("--clear", is_flag=True, help="Clear existing graph before ingesting.") |
| 31 | -def ingest(repo_path: str, db: str, clear: bool): | |
| 53 | +@click.option("--json", "as_json", is_flag=True, help="Output stats as JSON.") | |
| 54 | +def ingest(repo_path: str, db: str, clear: bool, as_json: bool): | |
| 32 | 55 | """Ingest a repository into the navegador graph.""" |
| 56 | + import json | |
| 57 | + | |
| 33 | 58 | from navegador.ingestion import RepoIngester |
| 34 | 59 | |
| 35 | 60 | store = _get_store(db) |
| 36 | 61 | ingester = RepoIngester(store) |
| 37 | 62 | |
| 38 | - with console.status(f"[bold]Ingesting[/bold] {repo_path}..."): | |
| 63 | + if as_json: | |
| 39 | 64 | stats = ingester.ingest(repo_path, clear=clear) |
| 40 | - | |
| 41 | - table = Table(title="Ingestion complete", show_header=True) | |
| 42 | - table.add_column("Metric", style="cyan") | |
| 43 | - table.add_column("Count", justify="right", style="green") | |
| 44 | - for k, v in stats.items(): | |
| 45 | - table.add_row(k.capitalize(), str(v)) | |
| 46 | - console.print(table) | |
| 65 | + click.echo(json.dumps(stats, indent=2)) | |
| 66 | + else: | |
| 67 | + with console.status(f"[bold]Ingesting[/bold] {repo_path}..."): | |
| 68 | + stats = ingester.ingest(repo_path, clear=clear) | |
| 69 | + table = Table(title="Ingestion complete", show_header=True) | |
| 70 | + table.add_column("Metric", style="cyan") | |
| 71 | + table.add_column("Count", justify="right", style="green") | |
| 72 | + for k, v in stats.items(): | |
| 73 | + table.add_row(k.capitalize(), str(v)) | |
| 74 | + console.print(table) | |
| 47 | 75 | |
| 48 | 76 | |
| 49 | 77 | @main.command() |
| 50 | 78 | @click.argument("file_path") |
| 51 | -@click.option("--db", default=".navegador/graph.db", show_default=True) | |
| 52 | -@click.option("--format", "fmt", type=click.Choice(["markdown", "json"]), default="markdown") | |
| 79 | +@DB_OPTION | |
| 80 | +@FMT_OPTION | |
| 53 | 81 | def context(file_path: str, db: str, fmt: str): |
| 54 | - """Load and print context for a file.""" | |
| 82 | + """Load context for a file — all symbols and their relationships.""" | |
| 83 | + from navegador.context import ContextLoader | |
| 84 | + | |
| 85 | + loader = ContextLoader(_get_store(db)) | |
| 86 | + bundle = loader.load_file(file_path) | |
| 87 | + _emit(bundle.to_json() if fmt == "json" else bundle.to_markdown(), fmt) | |
| 88 | + | |
| 89 | + | |
| 90 | +@main.command() | |
| 91 | +@click.argument("name") | |
| 92 | +@click.option("--file", "file_path", default="", help="File path to narrow the search.") | |
| 93 | +@click.option("--depth", default=2, show_default=True, help="Call graph traversal depth.") | |
| 94 | +@DB_OPTION | |
| 95 | +@FMT_OPTION | |
| 96 | +def function(name: str, file_path: str, db: str, depth: int, fmt: str): | |
| 97 | + """Load context for a function — callers, callees, signature.""" | |
| 98 | + from navegador.context import ContextLoader | |
| 99 | + | |
| 100 | + loader = ContextLoader(_get_store(db)) | |
| 101 | + bundle = loader.load_function(name, file_path=file_path, depth=depth) | |
| 102 | + _emit(bundle.to_json() if fmt == "json" else bundle.to_markdown(), fmt) | |
| 103 | + | |
| 104 | + | |
| 105 | +@main.command("class") | |
| 106 | +@click.argument("name") | |
| 107 | +@click.option("--file", "file_path", default="", help="File path to narrow the search.") | |
| 108 | +@DB_OPTION | |
| 109 | +@FMT_OPTION | |
| 110 | +def class_(name: str, file_path: str, db: str, fmt: str): | |
| 111 | + """Load context for a class — methods, inheritance, subclasses.""" | |
| 55 | 112 | from navegador.context import ContextLoader |
| 56 | 113 | |
| 57 | - store = _get_store(db) | |
| 58 | - loader = ContextLoader(store) | |
| 59 | - bundle = loader.load_file(file_path) | |
| 60 | - output = bundle.to_markdown() if fmt == "markdown" else bundle.to_json() | |
| 61 | - console.print(output) | |
| 114 | + loader = ContextLoader(_get_store(db)) | |
| 115 | + bundle = loader.load_class(name, file_path=file_path) | |
| 116 | + _emit(bundle.to_json() if fmt == "json" else bundle.to_markdown(), fmt) | |
| 62 | 117 | |
| 63 | 118 | |
| 64 | 119 | @main.command() |
| 65 | 120 | @click.argument("query") |
| 66 | -@click.option("--db", default=".navegador/graph.db", show_default=True) | |
| 121 | +@DB_OPTION | |
| 67 | 122 | @click.option("--limit", default=20, show_default=True) |
| 68 | -def search(query: str, db: str, limit: int): | |
| 69 | - """Search for symbols (functions, classes) by name.""" | |
| 123 | +@FMT_OPTION | |
| 124 | +def search(query: str, db: str, limit: int, fmt: str): | |
| 125 | + """Search for symbols (functions, classes, methods) by name.""" | |
| 126 | + import json | |
| 127 | + | |
| 70 | 128 | from navegador.context import ContextLoader |
| 71 | 129 | |
| 72 | - store = _get_store(db) | |
| 73 | - loader = ContextLoader(store) | |
| 130 | + loader = ContextLoader(_get_store(db)) | |
| 74 | 131 | results = loader.search(query, limit=limit) |
| 132 | + | |
| 133 | + if fmt == "json": | |
| 134 | + click.echo(json.dumps([ | |
| 135 | + {"type": r.type, "name": r.name, "file_path": r.file_path, | |
| 136 | + "line_start": r.line_start, "docstring": r.docstring} | |
| 137 | + for r in results | |
| 138 | + ], indent=2)) | |
| 139 | + return | |
| 75 | 140 | |
| 76 | 141 | if not results: |
| 77 | 142 | console.print("[yellow]No results.[/yellow]") |
| 78 | 143 | return |
| 79 | 144 | |
| @@ -86,38 +151,54 @@ | ||
| 86 | 151 | table.add_row(r.type, r.name, r.file_path, str(r.line_start or "")) |
| 87 | 152 | console.print(table) |
| 88 | 153 | |
| 89 | 154 | |
| 90 | 155 | @main.command() |
| 91 | -@click.option("--db", default=".navegador/graph.db", show_default=True) | |
| 92 | -def stats(db: str): | |
| 156 | +@click.argument("cypher") | |
| 157 | +@DB_OPTION | |
| 158 | +def query(cypher: str, db: str): | |
| 159 | + """Run a raw Cypher query and print results as JSON.""" | |
| 160 | + import json | |
| 161 | + | |
| 162 | + store = _get_store(db) | |
| 163 | + result = store.query(cypher) | |
| 164 | + rows = result.result_set or [] | |
| 165 | + click.echo(json.dumps(rows, default=str, indent=2)) | |
| 166 | + | |
| 167 | + | |
| 168 | +@main.command() | |
| 169 | +@DB_OPTION | |
| 170 | +@click.option("--json", "as_json", is_flag=True, help="Output as JSON.") | |
| 171 | +def stats(db: str, as_json: bool): | |
| 93 | 172 | """Show graph statistics.""" |
| 173 | + import json | |
| 174 | + | |
| 94 | 175 | store = _get_store(db) |
| 95 | - table = Table(title="Graph stats", show_header=True) | |
| 96 | - table.add_column("Metric", style="cyan") | |
| 97 | - table.add_column("Count", justify="right", style="green") | |
| 98 | - table.add_row("Nodes", str(store.node_count())) | |
| 99 | - table.add_row("Edges", str(store.edge_count())) | |
| 100 | - console.print(table) | |
| 176 | + data = {"nodes": store.node_count(), "edges": store.edge_count()} | |
| 177 | + | |
| 178 | + if as_json: | |
| 179 | + click.echo(json.dumps(data, indent=2)) | |
| 180 | + else: | |
| 181 | + table = Table(title="Graph stats", show_header=True) | |
| 182 | + table.add_column("Metric", style="cyan") | |
| 183 | + table.add_column("Count", justify="right", style="green") | |
| 184 | + for k, v in data.items(): | |
| 185 | + table.add_row(k.capitalize(), str(v)) | |
| 186 | + console.print(table) | |
| 101 | 187 | |
| 102 | 188 | |
| 103 | 189 | @main.command() |
| 104 | -@click.option("--db", default=".navegador/graph.db", show_default=True) | |
| 105 | -@click.option("--host", default="127.0.0.1", show_default=True) | |
| 106 | -@click.option("--port", default=8765, show_default=True) | |
| 107 | -def mcp(db: str, host: str, port: int): | |
| 108 | - """Start the MCP server for AI agent integration.""" | |
| 190 | +@DB_OPTION | |
| 191 | +def mcp(db: str): | |
| 192 | + """Start the MCP server for AI agent integration (stdio).""" | |
| 109 | 193 | from mcp.server.stdio import stdio_server # type: ignore[import] |
| 110 | 194 | |
| 111 | 195 | from navegador.mcp import create_mcp_server |
| 112 | 196 | |
| 113 | - def store_factory(): | |
| 114 | - return _get_store(db) | |
| 115 | - | |
| 116 | - server = create_mcp_server(store_factory) | |
| 197 | + server = create_mcp_server(lambda: _get_store(db)) | |
| 117 | 198 | console.print("[green]Navegador MCP server running[/green] (stdio)") |
| 118 | 199 | |
| 119 | 200 | async def _run(): |
| 120 | 201 | async with stdio_server() as (read_stream, write_stream): |
| 121 | 202 | await server.run(read_stream, write_stream, server.create_initialization_options()) |
| 122 | 203 | |
| 123 | 204 | asyncio.run(_run()) |
| 124 | 205 |
| --- navegador/cli/commands.py | |
| +++ navegador/cli/commands.py | |
| @@ -1,7 +1,10 @@ | |
| 1 | """ |
| 2 | Navegador CLI — ingest repos, load context, serve MCP. |
| 3 | """ |
| 4 | |
| 5 | import asyncio |
| 6 | import logging |
| 7 | |
| @@ -9,15 +12,34 @@ | |
| 9 | from rich.console import Console |
| 10 | from rich.table import Table |
| 11 | |
| 12 | console = Console() |
| 13 | |
| 14 | |
| 15 | def _get_store(db: str): |
| 16 | from navegador.graph import GraphStore |
| 17 | return GraphStore.sqlite(db) |
| 18 | |
| 19 | |
| 20 | @click.group() |
| 21 | @click.version_option(package_name="navegador") |
| 22 | def main(): |
| 23 | """Navegador — AST + knowledge graph context engine for AI coding agents.""" |
| @@ -24,56 +46,99 @@ | |
| 24 | logging.basicConfig(level=logging.WARNING) |
| 25 | |
| 26 | |
| 27 | @main.command() |
| 28 | @click.argument("repo_path", type=click.Path(exists=True)) |
| 29 | @click.option("--db", default=".navegador/graph.db", show_default=True, help="Graph DB path.") |
| 30 | @click.option("--clear", is_flag=True, help="Clear existing graph before ingesting.") |
| 31 | def ingest(repo_path: str, db: str, clear: bool): |
| 32 | """Ingest a repository into the navegador graph.""" |
| 33 | from navegador.ingestion import RepoIngester |
| 34 | |
| 35 | store = _get_store(db) |
| 36 | ingester = RepoIngester(store) |
| 37 | |
| 38 | with console.status(f"[bold]Ingesting[/bold] {repo_path}..."): |
| 39 | stats = ingester.ingest(repo_path, clear=clear) |
| 40 | |
| 41 | table = Table(title="Ingestion complete", show_header=True) |
| 42 | table.add_column("Metric", style="cyan") |
| 43 | table.add_column("Count", justify="right", style="green") |
| 44 | for k, v in stats.items(): |
| 45 | table.add_row(k.capitalize(), str(v)) |
| 46 | console.print(table) |
| 47 | |
| 48 | |
| 49 | @main.command() |
| 50 | @click.argument("file_path") |
| 51 | @click.option("--db", default=".navegador/graph.db", show_default=True) |
| 52 | @click.option("--format", "fmt", type=click.Choice(["markdown", "json"]), default="markdown") |
| 53 | def context(file_path: str, db: str, fmt: str): |
| 54 | """Load and print context for a file.""" |
| 55 | from navegador.context import ContextLoader |
| 56 | |
| 57 | store = _get_store(db) |
| 58 | loader = ContextLoader(store) |
| 59 | bundle = loader.load_file(file_path) |
| 60 | output = bundle.to_markdown() if fmt == "markdown" else bundle.to_json() |
| 61 | console.print(output) |
| 62 | |
| 63 | |
| 64 | @main.command() |
| 65 | @click.argument("query") |
| 66 | @click.option("--db", default=".navegador/graph.db", show_default=True) |
| 67 | @click.option("--limit", default=20, show_default=True) |
| 68 | def search(query: str, db: str, limit: int): |
| 69 | """Search for symbols (functions, classes) by name.""" |
| 70 | from navegador.context import ContextLoader |
| 71 | |
| 72 | store = _get_store(db) |
| 73 | loader = ContextLoader(store) |
| 74 | results = loader.search(query, limit=limit) |
| 75 | |
| 76 | if not results: |
| 77 | console.print("[yellow]No results.[/yellow]") |
| 78 | return |
| 79 | |
| @@ -86,38 +151,54 @@ | |
| 86 | table.add_row(r.type, r.name, r.file_path, str(r.line_start or "")) |
| 87 | console.print(table) |
| 88 | |
| 89 | |
| 90 | @main.command() |
| 91 | @click.option("--db", default=".navegador/graph.db", show_default=True) |
| 92 | def stats(db: str): |
| 93 | """Show graph statistics.""" |
| 94 | store = _get_store(db) |
| 95 | table = Table(title="Graph stats", show_header=True) |
| 96 | table.add_column("Metric", style="cyan") |
| 97 | table.add_column("Count", justify="right", style="green") |
| 98 | table.add_row("Nodes", str(store.node_count())) |
| 99 | table.add_row("Edges", str(store.edge_count())) |
| 100 | console.print(table) |
| 101 | |
| 102 | |
| 103 | @main.command() |
| 104 | @click.option("--db", default=".navegador/graph.db", show_default=True) |
| 105 | @click.option("--host", default="127.0.0.1", show_default=True) |
| 106 | @click.option("--port", default=8765, show_default=True) |
| 107 | def mcp(db: str, host: str, port: int): |
| 108 | """Start the MCP server for AI agent integration.""" |
| 109 | from mcp.server.stdio import stdio_server # type: ignore[import] |
| 110 | |
| 111 | from navegador.mcp import create_mcp_server |
| 112 | |
| 113 | def store_factory(): |
| 114 | return _get_store(db) |
| 115 | |
| 116 | server = create_mcp_server(store_factory) |
| 117 | console.print("[green]Navegador MCP server running[/green] (stdio)") |
| 118 | |
| 119 | async def _run(): |
| 120 | async with stdio_server() as (read_stream, write_stream): |
| 121 | await server.run(read_stream, write_stream, server.create_initialization_options()) |
| 122 | |
| 123 | asyncio.run(_run()) |
| 124 |
| --- navegador/cli/commands.py | |
| +++ navegador/cli/commands.py | |
| @@ -1,7 +1,10 @@ | |
| 1 | """ |
| 2 | Navegador CLI — ingest repos, load context, serve MCP. |
| 3 | |
| 4 | All commands support --format json for clean stdout output suitable for |
| 5 | piping to agents or other tools without MCP overhead. |
| 6 | """ |
| 7 | |
| 8 | import asyncio |
| 9 | import logging |
| 10 | |
| @@ -9,15 +12,34 @@ | |
| 12 | from rich.console import Console |
| 13 | from rich.table import Table |
| 14 | |
| 15 | console = Console() |
| 16 | |
| 17 | DB_OPTION = click.option( |
| 18 | "--db", default=".navegador/graph.db", show_default=True, help="Graph DB path." |
| 19 | ) |
| 20 | FMT_OPTION = click.option( |
| 21 | "--format", "fmt", |
| 22 | type=click.Choice(["markdown", "json"]), |
| 23 | default="markdown", |
| 24 | show_default=True, |
| 25 | help="Output format. Use json for agent/pipe consumption.", |
| 26 | ) |
| 27 | |
| 28 | |
| 29 | def _get_store(db: str): |
| 30 | from navegador.graph import GraphStore |
| 31 | return GraphStore.sqlite(db) |
| 32 | |
| 33 | |
| 34 | def _emit(text: str, fmt: str) -> None: |
| 35 | """Print text — raw to stdout for json, rich for markdown.""" |
| 36 | if fmt == "json": |
| 37 | click.echo(text) |
| 38 | else: |
| 39 | console.print(text) |
| 40 | |
| 41 | |
| 42 | @click.group() |
| 43 | @click.version_option(package_name="navegador") |
| 44 | def main(): |
| 45 | """Navegador — AST + knowledge graph context engine for AI coding agents.""" |
| @@ -24,56 +46,99 @@ | |
| 46 | logging.basicConfig(level=logging.WARNING) |
| 47 | |
| 48 | |
| 49 | @main.command() |
| 50 | @click.argument("repo_path", type=click.Path(exists=True)) |
| 51 | @DB_OPTION |
| 52 | @click.option("--clear", is_flag=True, help="Clear existing graph before ingesting.") |
| 53 | @click.option("--json", "as_json", is_flag=True, help="Output stats as JSON.") |
| 54 | def ingest(repo_path: str, db: str, clear: bool, as_json: bool): |
| 55 | """Ingest a repository into the navegador graph.""" |
| 56 | import json |
| 57 | |
| 58 | from navegador.ingestion import RepoIngester |
| 59 | |
| 60 | store = _get_store(db) |
| 61 | ingester = RepoIngester(store) |
| 62 | |
| 63 | if as_json: |
| 64 | stats = ingester.ingest(repo_path, clear=clear) |
| 65 | click.echo(json.dumps(stats, indent=2)) |
| 66 | else: |
| 67 | with console.status(f"[bold]Ingesting[/bold] {repo_path}..."): |
| 68 | stats = ingester.ingest(repo_path, clear=clear) |
| 69 | table = Table(title="Ingestion complete", show_header=True) |
| 70 | table.add_column("Metric", style="cyan") |
| 71 | table.add_column("Count", justify="right", style="green") |
| 72 | for k, v in stats.items(): |
| 73 | table.add_row(k.capitalize(), str(v)) |
| 74 | console.print(table) |
| 75 | |
| 76 | |
| 77 | @main.command() |
| 78 | @click.argument("file_path") |
| 79 | @DB_OPTION |
| 80 | @FMT_OPTION |
| 81 | def context(file_path: str, db: str, fmt: str): |
| 82 | """Load context for a file — all symbols and their relationships.""" |
| 83 | from navegador.context import ContextLoader |
| 84 | |
| 85 | loader = ContextLoader(_get_store(db)) |
| 86 | bundle = loader.load_file(file_path) |
| 87 | _emit(bundle.to_json() if fmt == "json" else bundle.to_markdown(), fmt) |
| 88 | |
| 89 | |
| 90 | @main.command() |
| 91 | @click.argument("name") |
| 92 | @click.option("--file", "file_path", default="", help="File path to narrow the search.") |
| 93 | @click.option("--depth", default=2, show_default=True, help="Call graph traversal depth.") |
| 94 | @DB_OPTION |
| 95 | @FMT_OPTION |
| 96 | def function(name: str, file_path: str, db: str, depth: int, fmt: str): |
| 97 | """Load context for a function — callers, callees, signature.""" |
| 98 | from navegador.context import ContextLoader |
| 99 | |
| 100 | loader = ContextLoader(_get_store(db)) |
| 101 | bundle = loader.load_function(name, file_path=file_path, depth=depth) |
| 102 | _emit(bundle.to_json() if fmt == "json" else bundle.to_markdown(), fmt) |
| 103 | |
| 104 | |
| 105 | @main.command("class") |
| 106 | @click.argument("name") |
| 107 | @click.option("--file", "file_path", default="", help="File path to narrow the search.") |
| 108 | @DB_OPTION |
| 109 | @FMT_OPTION |
| 110 | def class_(name: str, file_path: str, db: str, fmt: str): |
| 111 | """Load context for a class — methods, inheritance, subclasses.""" |
| 112 | from navegador.context import ContextLoader |
| 113 | |
| 114 | loader = ContextLoader(_get_store(db)) |
| 115 | bundle = loader.load_class(name, file_path=file_path) |
| 116 | _emit(bundle.to_json() if fmt == "json" else bundle.to_markdown(), fmt) |
| 117 | |
| 118 | |
| 119 | @main.command() |
| 120 | @click.argument("query") |
| 121 | @DB_OPTION |
| 122 | @click.option("--limit", default=20, show_default=True) |
| 123 | @FMT_OPTION |
| 124 | def search(query: str, db: str, limit: int, fmt: str): |
| 125 | """Search for symbols (functions, classes, methods) by name.""" |
| 126 | import json |
| 127 | |
| 128 | from navegador.context import ContextLoader |
| 129 | |
| 130 | loader = ContextLoader(_get_store(db)) |
| 131 | results = loader.search(query, limit=limit) |
| 132 | |
| 133 | if fmt == "json": |
| 134 | click.echo(json.dumps([ |
| 135 | {"type": r.type, "name": r.name, "file_path": r.file_path, |
| 136 | "line_start": r.line_start, "docstring": r.docstring} |
| 137 | for r in results |
| 138 | ], indent=2)) |
| 139 | return |
| 140 | |
| 141 | if not results: |
| 142 | console.print("[yellow]No results.[/yellow]") |
| 143 | return |
| 144 | |
| @@ -86,38 +151,54 @@ | |
| 151 | table.add_row(r.type, r.name, r.file_path, str(r.line_start or "")) |
| 152 | console.print(table) |
| 153 | |
| 154 | |
| 155 | @main.command() |
| 156 | @click.argument("cypher") |
| 157 | @DB_OPTION |
| 158 | def query(cypher: str, db: str): |
| 159 | """Run a raw Cypher query and print results as JSON.""" |
| 160 | import json |
| 161 | |
| 162 | store = _get_store(db) |
| 163 | result = store.query(cypher) |
| 164 | rows = result.result_set or [] |
| 165 | click.echo(json.dumps(rows, default=str, indent=2)) |
| 166 | |
| 167 | |
| 168 | @main.command() |
| 169 | @DB_OPTION |
| 170 | @click.option("--json", "as_json", is_flag=True, help="Output as JSON.") |
| 171 | def stats(db: str, as_json: bool): |
| 172 | """Show graph statistics.""" |
| 173 | import json |
| 174 | |
| 175 | store = _get_store(db) |
| 176 | data = {"nodes": store.node_count(), "edges": store.edge_count()} |
| 177 | |
| 178 | if as_json: |
| 179 | click.echo(json.dumps(data, indent=2)) |
| 180 | else: |
| 181 | table = Table(title="Graph stats", show_header=True) |
| 182 | table.add_column("Metric", style="cyan") |
| 183 | table.add_column("Count", justify="right", style="green") |
| 184 | for k, v in data.items(): |
| 185 | table.add_row(k.capitalize(), str(v)) |
| 186 | console.print(table) |
| 187 | |
| 188 | |
| 189 | @main.command() |
| 190 | @DB_OPTION |
| 191 | def mcp(db: str): |
| 192 | """Start the MCP server for AI agent integration (stdio).""" |
| 193 | from mcp.server.stdio import stdio_server # type: ignore[import] |
| 194 | |
| 195 | from navegador.mcp import create_mcp_server |
| 196 | |
| 197 | server = create_mcp_server(lambda: _get_store(db)) |
| 198 | console.print("[green]Navegador MCP server running[/green] (stdio)") |
| 199 | |
| 200 | async def _run(): |
| 201 | async with stdio_server() as (read_stream, write_stream): |
| 202 | await server.run(read_stream, write_stream, server.create_initialization_options()) |
| 203 | |
| 204 | asyncio.run(_run()) |
| 205 |
+2
-2
| --- navegador/context/loader.py | ||
| +++ navegador/context/loader.py | ||
| @@ -114,11 +114,11 @@ | ||
| 114 | 114 | target=target, |
| 115 | 115 | nodes=nodes, |
| 116 | 116 | metadata={"depth": depth, "query": "file_contents"}, |
| 117 | 117 | ) |
| 118 | 118 | |
| 119 | - def load_function(self, name: str, file_path: str, depth: int = 2) -> ContextBundle: | |
| 119 | + def load_function(self, name: str, file_path: str = "", depth: int = 2) -> ContextBundle: | |
| 120 | 120 | """Load context for a function — its callers and callees.""" |
| 121 | 121 | target = ContextNode(type="Function", name=name, file_path=file_path) |
| 122 | 122 | nodes: list[ContextNode] = [] |
| 123 | 123 | edges: list[dict[str, str]] = [] |
| 124 | 124 | |
| @@ -137,11 +137,11 @@ | ||
| 137 | 137 | edges.append({"from": row[1], "type": "CALLS", "to": name}) |
| 138 | 138 | |
| 139 | 139 | return ContextBundle(target=target, nodes=nodes, edges=edges, |
| 140 | 140 | metadata={"depth": depth, "query": "function_context"}) |
| 141 | 141 | |
| 142 | - def load_class(self, name: str, file_path: str) -> ContextBundle: | |
| 142 | + def load_class(self, name: str, file_path: str = "") -> ContextBundle: | |
| 143 | 143 | """Load context for a class — its methods, parent classes, and subclasses.""" |
| 144 | 144 | target = ContextNode(type="Class", name=name, file_path=file_path) |
| 145 | 145 | nodes: list[ContextNode] = [] |
| 146 | 146 | edges: list[dict[str, str]] = [] |
| 147 | 147 | |
| 148 | 148 |
| --- navegador/context/loader.py | |
| +++ navegador/context/loader.py | |
| @@ -114,11 +114,11 @@ | |
| 114 | target=target, |
| 115 | nodes=nodes, |
| 116 | metadata={"depth": depth, "query": "file_contents"}, |
| 117 | ) |
| 118 | |
| 119 | def load_function(self, name: str, file_path: str, depth: int = 2) -> ContextBundle: |
| 120 | """Load context for a function — its callers and callees.""" |
| 121 | target = ContextNode(type="Function", name=name, file_path=file_path) |
| 122 | nodes: list[ContextNode] = [] |
| 123 | edges: list[dict[str, str]] = [] |
| 124 | |
| @@ -137,11 +137,11 @@ | |
| 137 | edges.append({"from": row[1], "type": "CALLS", "to": name}) |
| 138 | |
| 139 | return ContextBundle(target=target, nodes=nodes, edges=edges, |
| 140 | metadata={"depth": depth, "query": "function_context"}) |
| 141 | |
| 142 | def load_class(self, name: str, file_path: str) -> ContextBundle: |
| 143 | """Load context for a class — its methods, parent classes, and subclasses.""" |
| 144 | target = ContextNode(type="Class", name=name, file_path=file_path) |
| 145 | nodes: list[ContextNode] = [] |
| 146 | edges: list[dict[str, str]] = [] |
| 147 | |
| 148 |
| --- navegador/context/loader.py | |
| +++ navegador/context/loader.py | |
| @@ -114,11 +114,11 @@ | |
| 114 | target=target, |
| 115 | nodes=nodes, |
| 116 | metadata={"depth": depth, "query": "file_contents"}, |
| 117 | ) |
| 118 | |
| 119 | def load_function(self, name: str, file_path: str = "", depth: int = 2) -> ContextBundle: |
| 120 | """Load context for a function — its callers and callees.""" |
| 121 | target = ContextNode(type="Function", name=name, file_path=file_path) |
| 122 | nodes: list[ContextNode] = [] |
| 123 | edges: list[dict[str, str]] = [] |
| 124 | |
| @@ -137,11 +137,11 @@ | |
| 137 | edges.append({"from": row[1], "type": "CALLS", "to": name}) |
| 138 | |
| 139 | return ContextBundle(target=target, nodes=nodes, edges=edges, |
| 140 | metadata={"depth": depth, "query": "function_context"}) |
| 141 | |
| 142 | def load_class(self, name: str, file_path: str = "") -> ContextBundle: |
| 143 | """Load context for a class — its methods, parent classes, and subclasses.""" |
| 144 | target = ContextNode(type="Class", name=name, file_path=file_path) |
| 145 | nodes: list[ContextNode] = [] |
| 146 | edges: list[dict[str, str]] = [] |
| 147 | |
| 148 |