Skip to content
Open
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
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,11 @@ code-review-graph build # Parse entire codebase
code-review-graph update # Incremental update (changed files only)
code-review-graph status # Graph statistics
code-review-graph watch # Auto-update on file changes
code-review-graph watch --json-events # Emit machine-readable watch events
code-review-graph web # Start local browser graph explorer
axon web # Same web explorer via the short alias
axon-web # Dedicated web explorer entry point
code-review-graph lsp # Start Language Server Protocol server
code-review-graph visualize # Generate interactive HTML graph
code-review-graph visualize --format graphml # Export as GraphML
code-review-graph visualize --format svg # Export as SVG
Expand Down Expand Up @@ -321,6 +326,24 @@ full config reference and all available options.

</details>

<details>
<summary><strong>Browser explorer and editor integrations</strong></summary>
<br>

Build the graph, then start the local web UI:

```bash
code-review-graph build
axon web --repo . --host 127.0.0.1 --port 8765
# Open http://127.0.0.1:8765/
```

`axon-web --repo . --host 127.0.0.1 --port 8765` is equivalent. The browser explorer reads the local SQLite graph, supports search, node inspection, query and impact endpoints, receives live update notifications from the graph database, and shows estimated source-vs-graph token savings. It also records local aggregate telemetry for web API operations so the dashboard can show observed request count plus estimated cumulative savings.

For editor integrations, `code-review-graph lsp` starts the stdio Language Server Protocol server. The VS Code extension also supports continuous indexing through `code-review-graph watch --json-events`.

</details>

<details>
<summary><strong>28 MCP tools</strong></summary>
<br>
Expand Down
22 changes: 22 additions & 0 deletions code-review-graph-vscode/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ Open the Command Palette (`Ctrl+Shift+P`) and run **Code Graph: Build Graph**.

The graph database is stored locally at `.code-review-graph/graph.db` and updates automatically on file save.

For continuous background indexing, run **Code Graph: Watch Mode**. The extension starts `code-review-graph watch --json-events`, streams progress to the **Code Graph Watch** output channel, and stops the watcher when the extension deactivates.

## Commands

| Command | Description |
Expand All @@ -62,6 +64,26 @@ The graph database is stored locally at `.code-review-graph/graph.db` and update
| `Code Graph: Compute Embeddings` | Generate vector embeddings for semantic search |
| `Code Graph: Watch Mode` | Run graph in watch mode for continuous updates |

## Browser UI and LSP

The Python backend also ships a standalone browser explorer:

```bash
code-review-graph build
axon web --repo . --host 127.0.0.1 --port 8765
# Open http://127.0.0.1:8765/
```

`axon-web --repo . --host 127.0.0.1 --port 8765` starts the same UI.

The browser dashboard includes graph size, estimated source-vs-graph token savings, observed web API request count, and estimated cumulative/average savings. Those estimates use the standard chars/4 approximation and are not model-provider billing data.

Editor integrations can use the stdio Language Server Protocol server:

```bash
code-review-graph lsp --repo .
```

## Settings

| Setting | Default | Description |
Expand Down
11 changes: 8 additions & 3 deletions code-review-graph-vscode/src/backend/cli.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { execFile } from 'child_process';
import { execFile, spawn } from 'child_process';
import type { ChildProcess } from 'child_process';
import { promisify } from 'util';
import * as vscode from 'vscode';

Expand Down Expand Up @@ -81,9 +82,13 @@ export class CliWrapper {

/**
* Start the watch daemon for continuous file monitoring.
* The caller owns the returned long-running process.
*/
async watchGraph(workspaceRoot: string): Promise<CliResult> {
return this.exec(['watch'], workspaceRoot);
startWatchGraph(workspaceRoot: string): ChildProcess {
return spawn(this.cliPath, ['watch', '--json-events'], {
cwd: workspaceRoot,
stdio: ['ignore', 'pipe', 'pipe'],
});
}

/**
Expand Down
16 changes: 13 additions & 3 deletions code-review-graph-vscode/src/backend/sqlite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,9 +182,19 @@ export class SqliteReader {
}

constructor(dbPath: string) {
this.db = new Database(dbPath, { readonly: true });
this.db.pragma('journal_mode = WAL');
this.db.pragma('busy_timeout = 5000');
const db = new Database(dbPath, { readonly: true });
try {
db.pragma('journal_mode = WAL');
} catch (err: unknown) {
const code = typeof err === 'object' && err !== null && 'code' in err
? String((err as { code?: unknown }).code)
: '';
if (code !== 'SQLITE_READONLY') {
throw err;
}
}
db.pragma('busy_timeout = 5000');
this.db = db;
}

/**
Expand Down
46 changes: 40 additions & 6 deletions code-review-graph-vscode/src/extension.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as vscode from "vscode";
import * as path from "node:path";
import * as fs from "node:fs";
import type { ChildProcess } from "node:child_process";

import { SqliteReader } from "./backend/sqlite";
import type { GraphNode } from "./backend/sqlite";
Expand All @@ -19,6 +20,8 @@ import { ScmDecorationProvider } from "./features/scmDecorations";
let sqliteReader: SqliteReader | undefined;
let autoUpdateTimer: ReturnType<typeof setTimeout> | undefined;
let scmDecorationProvider: ScmDecorationProvider | undefined;
let watchProcess: ChildProcess | undefined;
let watchOutput: vscode.OutputChannel | undefined;

/**
* Locate the graph database file in the workspace.
Expand Down Expand Up @@ -554,13 +557,37 @@ function registerCommands(
return;
}

vscode.window.showInformationMessage("Code Graph: Watch mode started.");
const result = await cli.watchGraph(workspaceRoot);
if (!result.success) {
vscode.window.showErrorMessage(
`Code Graph: Watch failed. ${result.stderr}`
);
if (watchProcess) {
watchProcess.kill();
watchProcess = undefined;
vscode.window.showInformationMessage("Code Graph: Watch mode stopped.");
return;
}

watchOutput ??= vscode.window.createOutputChannel("Code Graph Watch");
watchOutput.appendLine(`Starting watch mode in ${workspaceRoot}`);
watchProcess = cli.startWatchGraph(workspaceRoot);

watchProcess.stdout?.on("data", (data) => {
watchOutput?.append(data.toString());
});
watchProcess.stderr?.on("data", (data) => {
watchOutput?.append(data.toString());
});
watchProcess.on("error", (error) => {
watchProcess = undefined;
vscode.window.showErrorMessage(`Code Graph: Watch failed. ${error.message}`);
});
watchProcess.on("close", (code, signal) => {
const expectedStop = signal === "SIGTERM" || signal === "SIGKILL";
watchProcess = undefined;
watchOutput?.appendLine(`Watch stopped with code=${code} signal=${signal ?? ""}`);
if (code && !expectedStop) {
vscode.window.showWarningMessage(`Code Graph: Watch stopped with code ${code}.`);
}
});

vscode.window.showInformationMessage("Code Graph: Watch mode started.");
})
);

Expand Down Expand Up @@ -978,6 +1005,13 @@ export function deactivate(): void {
autoUpdateTimer = undefined;
}

if (watchProcess) {
watchProcess.kill();
watchProcess = undefined;
}
watchOutput?.dispose();
watchOutput = undefined;

sqliteReader?.close();
sqliteReader = undefined;
}
49 changes: 48 additions & 1 deletion code_review_graph/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
code-review-graph status
code-review-graph serve [--auto-watch] [--http] [--host ADDR] [--port PORT]
code-review-graph mcp [--auto-watch]
code-review-graph lsp
code-review-graph web
code-review-graph visualize
code-review-graph wiki
code-review-graph detect-changes [--base BASE] [--brief]
Expand Down Expand Up @@ -103,6 +105,8 @@ def _print_banner() -> None:
{g}update{r} Incremental update {d}(changed files only){r}
{g}watch{r} Auto-update on file changes
{g}status{r} Show graph statistics
{g}web{r} Start axon-web browser explorer
{g}lsp{r} Start graph Language Server Protocol server
{g}visualize{r} Generate interactive HTML graph
{g}wiki{r} Generate markdown wiki from communities
{g}detect-changes{r} Analyze change impact {d}(risk-scored review){r}
Expand Down Expand Up @@ -502,6 +506,10 @@ def main() -> None:
default=None,
help="External directory to store graph database (useful for network shares)"
)
watch_cmd.add_argument(
"--json-events", action="store_true",
help="Print one JSON event per graph update",
)

# status
status_cmd = sub.add_parser("status", help="Show graph statistics")
Expand Down Expand Up @@ -709,6 +717,20 @@ def main() -> None:
help="Repository path or alias to remove",
)

# lsp
lsp_cmd = sub.add_parser("lsp", help="Start LSP server (stdio transport)")
lsp_cmd.add_argument("--repo", default=None, help="Repository root (auto-detected)")

# web
web_cmd = sub.add_parser("web", help="Start axon-web browser explorer")
web_cmd.add_argument("--repo", default=None, help="Repository root (auto-detected)")
web_cmd.add_argument("--host", default="127.0.0.1", help="Bind host")
web_cmd.add_argument("--port", type=int, default=8765, help="Bind port")
web_cmd.add_argument(
"--open", action="store_true", dest="open_browser",
help="Open the browser after starting the server",
)

args = ap.parse_args()

if args.version:
Expand Down Expand Up @@ -773,6 +795,21 @@ def main() -> None:
handler(args)
return

if args.command == "lsp":
from .lsp import main as lsp_main
lsp_main(repo_root=args.repo)
return

if args.command == "web":
from .web import run_web
run_web(
repo_root=args.repo,
host=getattr(args, "host", "127.0.0.1"),
port=getattr(args, "port", 8765),
open_browser=getattr(args, "open_browser", False),
)
return

if args.command == "eval":
from .eval.reporter import generate_full_report, generate_readme_tables
from .eval.runner import run_eval
Expand Down Expand Up @@ -986,7 +1023,17 @@ def main() -> None:
from .postprocessing import run_post_processing

try:
watch(repo_root, store, on_files_updated=run_post_processing)
if getattr(args, "json_events", False):
def _emit(event):
print(json.dumps(event), flush=True)
watch(
repo_root,
store,
on_files_updated=run_post_processing,
event_sink=_emit,
)
else:
watch(repo_root, store, on_files_updated=run_post_processing)
except RuntimeError as exc:
print(f"Error: {exc}", file=sys.stderr)
sys.exit(1)
Expand Down
33 changes: 32 additions & 1 deletion code_review_graph/incremental.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import threading
import time
from pathlib import Path, PurePosixPath
from typing import Callable, Optional
from typing import Any, Callable, Optional

from .graph import GraphStore
from .parser import CodeParser
Expand Down Expand Up @@ -1068,6 +1068,7 @@ def watch(
repo_root: Path,
store: GraphStore,
on_files_updated: Optional[Callable] = None,
event_sink: Callable[[dict[str, Any]], None] | None = None,
) -> None:
"""Watch for file changes and auto-update the graph.

Expand All @@ -1080,6 +1081,8 @@ def watch(
batch of file updates completes. Receives the store as its
only argument. Used by the CLI to run post-processing
(FTS, flows, communities) after watch updates.
event_sink: Optional callback that receives structured update/error
events. Used by editor integrations.
"""
import threading

Expand Down Expand Up @@ -1134,8 +1137,21 @@ def on_deleted(self, event):
store.remove_file_data(event.src_path)
store.commit()
logger.info("Removed: %s", rel)
if event_sink:
event_sink({
"event": "removed",
"file": rel,
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%S"),
})
except Exception as e:
logger.error("Error removing %s: %s", rel, e)
if event_sink:
event_sink({
"event": "error",
"file": rel,
"error": str(e),
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%S"),
})

def _schedule(self, abs_path: str):
"""Add file to pending set and reset the debounce timer."""
Expand Down Expand Up @@ -1186,9 +1202,24 @@ def _update_file(self, abs_path: str) -> bool:
len(nodes),
len(edges),
)
if event_sink:
event_sink({
"event": "updated",
"file": rel,
"nodes": len(nodes),
"edges": len(edges),
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%S"),
})
return True
except Exception as e:
logger.error("Error updating %s: %s", abs_path, e)
if event_sink:
event_sink({
"event": "error",
"file": str(path),
"error": str(e),
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%S"),
})
return False

handler = GraphUpdateHandler()
Expand Down
Loading