Skip to content

Commit 05d38f2

Browse files
authored
feat: ccc doctor and upgrade cocoindex version (#110)
1 parent f6a03bf commit 05d38f2

8 files changed

Lines changed: 542 additions & 18 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ classifiers = [
2323

2424
dependencies = [
2525
"mcp>=1.0.0",
26-
"cocoindex[litellm]==1.0.0a35",
26+
"cocoindex[litellm]==1.0.0a37",
2727
"sentence-transformers>=2.2.0",
2828
"sqlite-vec>=0.1.0",
2929
"pydantic>=2.0.0",

src/cocoindex_code/cli.py

Lines changed: 138 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import typer as _typer
88

9-
from .protocol import IndexingProgress, ProjectStatusResponse, SearchResponse
9+
from .protocol import DoctorCheckResult, IndexingProgress, ProjectStatusResponse, SearchResponse
1010
from .settings import (
1111
default_project_settings,
1212
default_user_settings,
@@ -415,6 +415,143 @@ def reset(
415415
)
416416

417417

418+
def _print_section(name: str) -> None:
419+
import click as _click
420+
421+
_typer.echo()
422+
_typer.echo(_click.style(f" {name}", bold=True))
423+
_typer.echo(_click.style(f" {'─' * 38}", fg="bright_black"))
424+
425+
426+
def _print_error(msg: str) -> None:
427+
import click as _click
428+
429+
_typer.echo(_click.style(f" ERROR: {msg}", fg="red"), err=True)
430+
431+
432+
def _print_doctor_result(result: DoctorCheckResult) -> None:
433+
import click as _click
434+
435+
if result.name == "done":
436+
return
437+
if result.ok:
438+
tag = _click.style("[OK]", fg="green", bold=True)
439+
else:
440+
tag = _click.style("[FAIL]", fg="red", bold=True)
441+
_typer.echo(f"\n {tag} {result.name}")
442+
for line in result.details:
443+
_typer.echo(f" {line}")
444+
for err in result.errors:
445+
_typer.echo(_click.style(f" ERROR: {err}", fg="red"), err=True)
446+
447+
448+
@app.command()
449+
def doctor() -> None:
450+
"""Check system health and report issues."""
451+
from . import client as _client
452+
from .settings import (
453+
load_project_settings as _load_project_settings,
454+
)
455+
from .settings import (
456+
load_user_settings as _load_user_settings,
457+
)
458+
from .settings import (
459+
project_settings_path as _project_settings_path,
460+
)
461+
from .settings import (
462+
user_settings_path as _user_settings_path,
463+
)
464+
465+
# --- 1. Global settings (local, no daemon needed) ---
466+
_print_section("Global Settings")
467+
settings_path = _user_settings_path()
468+
_typer.echo(f" Settings: {settings_path}")
469+
try:
470+
user_settings = _load_user_settings()
471+
emb = user_settings.embedding
472+
device_str = f", device={emb.device}" if emb.device else ""
473+
_typer.echo(f" Embedding: provider={emb.provider}, model={emb.model}{device_str}")
474+
if user_settings.envs:
475+
_typer.echo(
476+
f" Env vars (from settings): {', '.join(sorted(user_settings.envs.keys()))}"
477+
)
478+
except (FileNotFoundError, ValueError) as e:
479+
_print_error(str(e))
480+
481+
# --- 2. Connect to daemon (handshake with auto-start/restart) ---
482+
_print_section("Daemon")
483+
daemon_ok = False
484+
try:
485+
status = _client.daemon_status()
486+
_typer.echo(f" Version: {status.version}")
487+
_typer.echo(f" Uptime: {status.uptime_seconds:.1f}s")
488+
_typer.echo(f" Loaded projects: {len(status.projects)}")
489+
daemon_ok = True
490+
except Exception as e:
491+
_print_error(f"Cannot connect to daemon: {e}")
492+
_typer.echo(" Remaining daemon-side checks will be skipped.")
493+
494+
# --- 3. Daemon environment (requires daemon) ---
495+
if daemon_ok:
496+
try:
497+
env_resp = _client.daemon_env()
498+
settings_keys = set(env_resp.settings_env_names)
499+
other_keys = [k for k in env_resp.env_names if k not in settings_keys]
500+
if other_keys:
501+
_typer.echo(f" Other env vars in daemon: {', '.join(sorted(other_keys))}")
502+
except Exception as e:
503+
_print_error(f"Failed to get daemon env: {e}")
504+
505+
# --- 4. Model check (daemon-side, global — before project checks) ---
506+
if daemon_ok:
507+
try:
508+
_client.doctor(
509+
project_root=None,
510+
on_result=_print_doctor_result,
511+
)
512+
except Exception as e:
513+
_print_error(f"Model check failed: {e}")
514+
515+
# --- 5. Detect project ---
516+
project_root = find_project_root(Path.cwd())
517+
518+
# --- 6. Project settings (local, no daemon needed) ---
519+
if project_root is not None:
520+
_print_section("Project Settings")
521+
ps_path = _project_settings_path(project_root)
522+
_typer.echo(f" Settings: {ps_path}")
523+
try:
524+
ps = _load_project_settings(project_root)
525+
_typer.echo(f" Include patterns ({len(ps.include_patterns)}):")
526+
_typer.echo(f" {', '.join(ps.include_patterns)}")
527+
_typer.echo(f" Exclude patterns ({len(ps.exclude_patterns)}):")
528+
_typer.echo(f" {', '.join(ps.exclude_patterns)}")
529+
if ps.language_overrides:
530+
_typer.echo(" Language overrides:")
531+
for lo in ps.language_overrides:
532+
_typer.echo(f" .{lo.ext} -> {lo.lang}")
533+
except (FileNotFoundError, ValueError) as e:
534+
_print_error(str(e))
535+
536+
# --- 7. Project daemon-side checks (file walk + index status) ---
537+
if daemon_ok and project_root is not None:
538+
try:
539+
_client.doctor(
540+
project_root=str(project_root),
541+
on_result=_print_doctor_result,
542+
)
543+
except Exception as e:
544+
_print_error(f"Project checks failed: {e}")
545+
546+
# --- 8. Log files ---
547+
_print_section("Log Files")
548+
from .daemon import daemon_dir as _daemon_dir
549+
550+
log_dir = _daemon_dir()
551+
_typer.echo(f" Daemon logs: {log_dir / 'daemon.log'}")
552+
_typer.echo(" Check logs above for further troubleshooting.")
553+
554+
418555
@app.command()
419556
def mcp() -> None:
420557
"""Run as MCP server (stdio mode)."""

src/cocoindex_code/client.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,12 @@
2020
from ._version import __version__
2121
from .daemon import _connection_family, daemon_pid_path, daemon_socket_path
2222
from .protocol import (
23+
DaemonEnvRequest,
24+
DaemonEnvResponse,
2325
DaemonStatusResponse,
26+
DoctorCheckResult,
27+
DoctorRequest,
28+
DoctorResponse,
2429
ErrorResponse,
2530
HandshakeRequest,
2631
HandshakeResponse,
@@ -259,6 +264,41 @@ def stop() -> StopResponse:
259264
return _send(StopRequest()) # type: ignore[return-value]
260265

261266

267+
def daemon_env() -> DaemonEnvResponse:
268+
"""Get environment variable names from the daemon."""
269+
return _send(DaemonEnvRequest()) # type: ignore[return-value]
270+
271+
272+
def doctor(
273+
project_root: str | None = None,
274+
on_result: Callable[[DoctorCheckResult], None] | None = None,
275+
) -> list[DoctorCheckResult]:
276+
"""Run doctor checks via daemon, streaming results to on_result callback."""
277+
conn = _connect_and_handshake()
278+
try:
279+
conn.send_bytes(encode_request(DoctorRequest(project_root=project_root)))
280+
results: list[DoctorCheckResult] = []
281+
while True:
282+
try:
283+
data = conn.recv_bytes()
284+
except EOFError:
285+
raise RuntimeError("Connection to daemon lost during doctor checks")
286+
resp = decode_response(data)
287+
if isinstance(resp, ErrorResponse):
288+
raise RuntimeError(f"Daemon error: {resp.message}")
289+
if isinstance(resp, DoctorResponse):
290+
results.append(resp.result)
291+
if on_result is not None:
292+
on_result(resp.result)
293+
if resp.final:
294+
break
295+
else:
296+
raise RuntimeError(f"Unexpected response: {type(resp).__name__}")
297+
return results
298+
finally:
299+
conn.close()
300+
301+
262302
# ---------------------------------------------------------------------------
263303
# Daemon lifecycle helpers
264304
# ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)