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
116 changes: 97 additions & 19 deletions code_review_graph/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
code-review-graph build [--base BASE]
code-review-graph update [--base BASE]
code-review-graph watch
code-review-graph daemon <start|stop|status>
code-review-graph status
code-review-graph serve [--http] [--host ADDR] [--port PORT]
code-review-graph serve [--auto-watch] [--http] [--host ADDR] [--port PORT]
code-review-graph mcp [--auto-watch]
code-review-graph visualize
code-review-graph wiki
code-review-graph detect-changes [--base BASE] [--brief]
Expand Down Expand Up @@ -87,6 +89,7 @@ def _print_banner() -> None:
{g}build{r} Full graph build {d}(parse all files){r}
{g}update{r} Incremental update {d}(changed files only){r}
{g}watch{r} Auto-update on file changes
{g}daemon{r} Offline watch daemon management
{g}status{r} Show graph statistics
{g}visualize{r} Generate interactive HTML graph
{g}wiki{r} Generate markdown wiki from communities
Expand Down Expand Up @@ -428,6 +431,20 @@ def main() -> None:
watch_cmd = sub.add_parser("watch", help="Watch for changes and auto-update")
watch_cmd.add_argument("--repo", default=None, help="Repository root (auto-detected)")

# daemon
daemon_cmd = sub.add_parser("daemon", help="Manage offline watch daemon")
daemon_cmd.add_argument(
"action",
choices=["start", "stop", "status"],
help="Daemon action to perform",
)
daemon_cmd.add_argument("--repo", default=None, help="Repository root (auto-detected)")
daemon_cmd.add_argument(
"--foreground",
action="store_true",
help="Run in foreground (recommended with process managers/services)",
)

# status
status_cmd = sub.add_parser("status", help="Show graph statistics")
status_cmd.add_argument("--repo", default=None, help="Repository root (auto-detected)")
Expand Down Expand Up @@ -497,12 +514,17 @@ def main() -> None:
detect_cmd.add_argument("--brief", action="store_true", help="Show brief summary only")
detect_cmd.add_argument("--repo", default=None, help="Repository root (auto-detected)")

# serve
# serve / mcp
serve_cmd = sub.add_parser(
"serve",
help="Start MCP server (stdio by default, or HTTP on localhost with --http)",
)
serve_cmd.add_argument("--repo", default=None, help="Repository root (auto-detected)")
serve_cmd.add_argument(
"--auto-watch",
action="store_true",
help="Start filesystem watch in a daemon thread while MCP server runs",
)
serve_cmd.add_argument(
"--tools", default=None,
help=(
Expand Down Expand Up @@ -531,6 +553,14 @@ def main() -> None:
help="Port for --http (default: 5555)",
)

mcp_cmd = sub.add_parser("mcp", help="Alias for serve")
mcp_cmd.add_argument("--repo", default=None, help="Repository root (auto-detected)")
mcp_cmd.add_argument(
"--auto-watch",
action="store_true",
help="Start filesystem watch in a daemon thread while MCP server runs",
)

args = ap.parse_args()

if args.version:
Expand All @@ -541,25 +571,34 @@ def main() -> None:
_print_banner()
return

if args.command == "serve":
if args.command in ("serve", "mcp"):
from .main import main as serve_main

if args.port is not None and not args.http:
serve_cmd.error("--port requires --http")
if args.host is not None and not args.http:
serve_cmd.error("--host requires --http")
if args.http:
host = args.host if args.host is not None else "127.0.0.1"
port = args.port if args.port is not None else 5555
serve_main(
repo_root=args.repo,
transport="streamable-http",
host=host,
port=port,
tools=args.tools,
)
auto_watch = getattr(args, "auto_watch", False)
if args.command == "serve":
if getattr(args, "port", None) is not None and not getattr(args, "http", False):
serve_cmd.error("--port requires --http")
if getattr(args, "host", None) is not None and not getattr(args, "http", False):
serve_cmd.error("--host requires --http")
if getattr(args, "http", False):
host = args.host if args.host is not None else "127.0.0.1"
port = args.port if args.port is not None else 5555
serve_main(
repo_root=args.repo,
auto_watch=auto_watch,
transport="streamable-http",
host=host,
port=port,
tools=getattr(args, "tools", None),
)
else:
serve_main(
repo_root=args.repo,
auto_watch=auto_watch,
tools=getattr(args, "tools", None),
)
else:
serve_main(repo_root=args.repo, tools=args.tools)
serve_main(repo_root=args.repo, auto_watch=auto_watch)
return

if args.command == "eval":
Expand Down Expand Up @@ -642,9 +681,36 @@ def main() -> None:
find_project_root,
find_repo_root,
get_db_path,
get_watch_daemon_status,
run_watch_daemon_foreground,
start_watch_daemon,
stop_watch_daemon,
watch,
)

if args.command == "daemon":
repo_root = Path(args.repo) if args.repo else find_project_root()
action = args.action
if action == "start":
if getattr(args, "foreground", False):
run_watch_daemon_foreground(repo_root)
else:
result = start_watch_daemon(repo_root)
print(result["message"])
return
if action == "stop":
result = stop_watch_daemon(repo_root)
print(result["message"])
return
status = get_watch_daemon_status(repo_root)
if status["running"]:
print(f"Watch daemon is running (pid={status['pid']}).")
else:
print("Watch daemon is not running.")
print(f"PID file: {status['pid_file']}")
print(f"Lock file: {status['lock_file']}")
return

if args.command == "postprocess":
repo_root = Path(args.repo) if args.repo else find_project_root()
db_path = get_db_path(repo_root)
Expand Down Expand Up @@ -768,7 +834,15 @@ def main() -> None:
elif args.command == "watch":
from .postprocessing import run_post_processing

watch(repo_root, store, on_files_updated=run_post_processing)
try:
watch(repo_root, store, on_files_updated=run_post_processing)
except RuntimeError as exc:
logging.error(
"%s. If daemon mode is running, stop it with "
"'code-review-graph daemon stop' first.",
exc,
)
sys.exit(1)

elif args.command == "visualize":
from .incremental import get_data_dir
Expand Down Expand Up @@ -867,3 +941,7 @@ def main() -> None:

finally:
store.close()


if __name__ == "__main__":
main()
7 changes: 4 additions & 3 deletions code_review_graph/embeddings.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from __future__ import annotations

import hashlib
import importlib
import logging
import os
import sqlite3
Expand All @@ -19,7 +20,7 @@
import time
from abc import ABC, abstractmethod
from pathlib import Path
from typing import Any
from typing import Any, cast
from urllib.parse import urlparse

from .graph import GraphNode, GraphStore, node_to_dict
Expand Down Expand Up @@ -99,8 +100,8 @@ def name(self) -> str:
class GoogleEmbeddingProvider(EmbeddingProvider):
def __init__(self, api_key: str, model: str = "gemini-embedding-001") -> None:
try:
from google import genai
self._client = genai.Client(api_key=api_key)
genai = importlib.import_module("google.genai")
self._client = cast(Any, genai).Client(api_key=api_key)
self.model = model
self._dimension: int | None = None
except ImportError:
Expand Down
Loading