cocoindex-code 0.2.5__tar.gz → 0.2.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cocoindex_code-0.2.5 → cocoindex_code-0.2.6}/PKG-INFO +2 -2
- {cocoindex_code-0.2.5 → cocoindex_code-0.2.6}/pyproject.toml +1 -1
- {cocoindex_code-0.2.5 → cocoindex_code-0.2.6}/src/cocoindex_code/_version.py +2 -2
- {cocoindex_code-0.2.5 → cocoindex_code-0.2.6}/src/cocoindex_code/cli.py +138 -1
- {cocoindex_code-0.2.5 → cocoindex_code-0.2.6}/src/cocoindex_code/client.py +40 -0
- {cocoindex_code-0.2.5 → cocoindex_code-0.2.6}/src/cocoindex_code/daemon.py +166 -4
- {cocoindex_code-0.2.5 → cocoindex_code-0.2.6}/src/cocoindex_code/protocol.py +30 -0
- {cocoindex_code-0.2.5 → cocoindex_code-0.2.6}/.gitignore +0 -0
- {cocoindex_code-0.2.5 → cocoindex_code-0.2.6}/LICENSE +0 -0
- {cocoindex_code-0.2.5 → cocoindex_code-0.2.6}/README.md +0 -0
- {cocoindex_code-0.2.5 → cocoindex_code-0.2.6}/src/cocoindex_code/__init__.py +0 -0
- {cocoindex_code-0.2.5 → cocoindex_code-0.2.6}/src/cocoindex_code/__main__.py +0 -0
- {cocoindex_code-0.2.5 → cocoindex_code-0.2.6}/src/cocoindex_code/config.py +0 -0
- {cocoindex_code-0.2.5 → cocoindex_code-0.2.6}/src/cocoindex_code/indexer.py +0 -0
- {cocoindex_code-0.2.5 → cocoindex_code-0.2.6}/src/cocoindex_code/project.py +0 -0
- {cocoindex_code-0.2.5 → cocoindex_code-0.2.6}/src/cocoindex_code/query.py +0 -0
- {cocoindex_code-0.2.5 → cocoindex_code-0.2.6}/src/cocoindex_code/schema.py +0 -0
- {cocoindex_code-0.2.5 → cocoindex_code-0.2.6}/src/cocoindex_code/server.py +0 -0
- {cocoindex_code-0.2.5 → cocoindex_code-0.2.6}/src/cocoindex_code/settings.py +0 -0
- {cocoindex_code-0.2.5 → cocoindex_code-0.2.6}/src/cocoindex_code/shared.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cocoindex-code
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.6
|
|
4
4
|
Summary: MCP server for indexing and querying codebases using CocoIndex
|
|
5
5
|
Project-URL: Homepage, https://github.com/cocoindex-io/cocoindex-code
|
|
6
6
|
Project-URL: Repository, https://github.com/cocoindex-io/cocoindex-code
|
|
@@ -17,7 +17,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.13
|
|
18
18
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
19
|
Requires-Python: >=3.11
|
|
20
|
-
Requires-Dist: cocoindex[litellm]==1.0.
|
|
20
|
+
Requires-Dist: cocoindex[litellm]==1.0.0a37
|
|
21
21
|
Requires-Dist: einops>=0.8.2
|
|
22
22
|
Requires-Dist: mcp>=1.0.0
|
|
23
23
|
Requires-Dist: msgspec>=0.19.0
|
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '0.2.
|
|
32
|
-
__version_tuple__ = version_tuple = (0, 2,
|
|
31
|
+
__version__ = version = '0.2.6'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 2, 6)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
|
@@ -6,7 +6,7 @@ from pathlib import Path
|
|
|
6
6
|
|
|
7
7
|
import typer as _typer
|
|
8
8
|
|
|
9
|
-
from .protocol import IndexingProgress, ProjectStatusResponse, SearchResponse
|
|
9
|
+
from .protocol import DoctorCheckResult, IndexingProgress, ProjectStatusResponse, SearchResponse
|
|
10
10
|
from .settings import (
|
|
11
11
|
default_project_settings,
|
|
12
12
|
default_user_settings,
|
|
@@ -415,6 +415,143 @@ def reset(
|
|
|
415
415
|
)
|
|
416
416
|
|
|
417
417
|
|
|
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
|
+
|
|
418
555
|
@app.command()
|
|
419
556
|
def mcp() -> None:
|
|
420
557
|
"""Run as MCP server (stdio mode)."""
|
|
@@ -20,7 +20,12 @@ from pathlib import Path
|
|
|
20
20
|
from ._version import __version__
|
|
21
21
|
from .daemon import _connection_family, daemon_pid_path, daemon_socket_path
|
|
22
22
|
from .protocol import (
|
|
23
|
+
DaemonEnvRequest,
|
|
24
|
+
DaemonEnvResponse,
|
|
23
25
|
DaemonStatusResponse,
|
|
26
|
+
DoctorCheckResult,
|
|
27
|
+
DoctorRequest,
|
|
28
|
+
DoctorResponse,
|
|
24
29
|
ErrorResponse,
|
|
25
30
|
HandshakeRequest,
|
|
26
31
|
HandshakeResponse,
|
|
@@ -259,6 +264,41 @@ def stop() -> StopResponse:
|
|
|
259
264
|
return _send(StopRequest()) # type: ignore[return-value]
|
|
260
265
|
|
|
261
266
|
|
|
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
|
+
|
|
262
302
|
# ---------------------------------------------------------------------------
|
|
263
303
|
# Daemon lifecycle helpers
|
|
264
304
|
# ---------------------------------------------------------------------------
|
|
@@ -17,9 +17,15 @@ from typing import Any
|
|
|
17
17
|
from ._version import __version__
|
|
18
18
|
from .project import Project
|
|
19
19
|
from .protocol import (
|
|
20
|
+
DaemonEnvRequest,
|
|
21
|
+
DaemonEnvResponse,
|
|
20
22
|
DaemonProjectInfo,
|
|
21
23
|
DaemonStatusRequest,
|
|
22
24
|
DaemonStatusResponse,
|
|
25
|
+
DoctorCheckResult,
|
|
26
|
+
DoctorRequest,
|
|
27
|
+
DoctorResponse,
|
|
28
|
+
DoctorStreamResponse,
|
|
23
29
|
ErrorResponse,
|
|
24
30
|
HandshakeRequest,
|
|
25
31
|
HandshakeResponse,
|
|
@@ -151,6 +157,7 @@ async def handle_connection(
|
|
|
151
157
|
start_time: float,
|
|
152
158
|
on_shutdown: Callable[[], None],
|
|
153
159
|
settings_mtime_us: int | None,
|
|
160
|
+
settings_env_names: list[str],
|
|
154
161
|
) -> None:
|
|
155
162
|
"""Handle a single client connection (per-request model).
|
|
156
163
|
|
|
@@ -186,7 +193,7 @@ async def handle_connection(
|
|
|
186
193
|
data = await loop.run_in_executor(None, conn.recv_bytes)
|
|
187
194
|
req = decode_request(data)
|
|
188
195
|
|
|
189
|
-
result = await _dispatch(req, registry, start_time, on_shutdown)
|
|
196
|
+
result = await _dispatch(req, registry, start_time, on_shutdown, settings_env_names)
|
|
190
197
|
if isinstance(result, AsyncIterator):
|
|
191
198
|
try:
|
|
192
199
|
async for resp in result:
|
|
@@ -231,17 +238,161 @@ async def _search_with_wait(
|
|
|
231
238
|
yield ErrorResponse(message=str(e))
|
|
232
239
|
|
|
233
240
|
|
|
241
|
+
async def _handle_doctor(
|
|
242
|
+
req: DoctorRequest,
|
|
243
|
+
registry: ProjectRegistry,
|
|
244
|
+
) -> AsyncIterator[DoctorStreamResponse]:
|
|
245
|
+
"""Run doctor checks sequentially, yielding results as they complete.
|
|
246
|
+
|
|
247
|
+
When ``project_root`` is None, only the model check runs (global scope).
|
|
248
|
+
When ``project_root`` is set, only project-specific checks run (file walk + index status).
|
|
249
|
+
The CLI calls this twice — once without project, once with — so that global checks
|
|
250
|
+
appear before project settings in the output.
|
|
251
|
+
"""
|
|
252
|
+
if req.project_root is None:
|
|
253
|
+
# Global-scope checks
|
|
254
|
+
yield DoctorResponse(result=await _check_model(registry._embedder))
|
|
255
|
+
else:
|
|
256
|
+
# Project-scope checks
|
|
257
|
+
yield DoctorResponse(result=await _check_file_walk(req.project_root))
|
|
258
|
+
yield DoctorResponse(result=await _check_index_status(req.project_root))
|
|
259
|
+
|
|
260
|
+
# Final marker
|
|
261
|
+
yield DoctorResponse(
|
|
262
|
+
result=DoctorCheckResult(name="done", ok=True, details=[], errors=[]),
|
|
263
|
+
final=True,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
async def _check_model(embedder: Embedder) -> DoctorCheckResult:
|
|
268
|
+
"""Test the embedding model by embedding a short string."""
|
|
269
|
+
try:
|
|
270
|
+
vec = await embedder.embed("hello world")
|
|
271
|
+
dim = len(vec)
|
|
272
|
+
return DoctorCheckResult(
|
|
273
|
+
name="Model Check",
|
|
274
|
+
ok=True,
|
|
275
|
+
details=[f"Embedding dimension: {dim}"],
|
|
276
|
+
errors=[],
|
|
277
|
+
)
|
|
278
|
+
except Exception as e:
|
|
279
|
+
return DoctorCheckResult(
|
|
280
|
+
name="Model Check",
|
|
281
|
+
ok=False,
|
|
282
|
+
details=[],
|
|
283
|
+
errors=[str(e)],
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
async def _check_file_walk(project_root_str: str) -> DoctorCheckResult:
|
|
288
|
+
"""Walk project files and report counts + gitignore paths."""
|
|
289
|
+
from pathlib import PurePath
|
|
290
|
+
|
|
291
|
+
from cocoindex.resources.file import PatternFilePathMatcher
|
|
292
|
+
|
|
293
|
+
from .indexer import GitignoreAwareMatcher
|
|
294
|
+
from .settings import load_gitignore_spec, load_project_settings
|
|
295
|
+
|
|
296
|
+
project_root = Path(project_root_str)
|
|
297
|
+
try:
|
|
298
|
+
ps = load_project_settings(project_root)
|
|
299
|
+
except FileNotFoundError as e:
|
|
300
|
+
return DoctorCheckResult(name="File Walk", ok=False, details=[], errors=[str(e)])
|
|
301
|
+
|
|
302
|
+
gitignore_spec = load_gitignore_spec(project_root)
|
|
303
|
+
base_matcher = PatternFilePathMatcher(
|
|
304
|
+
included_patterns=ps.include_patterns,
|
|
305
|
+
excluded_patterns=ps.exclude_patterns,
|
|
306
|
+
)
|
|
307
|
+
matcher = GitignoreAwareMatcher(base_matcher, gitignore_spec, project_root)
|
|
308
|
+
|
|
309
|
+
counts_by_ext: dict[str, int] = {}
|
|
310
|
+
gitignore_dirs: list[str] = []
|
|
311
|
+
total = 0
|
|
312
|
+
|
|
313
|
+
def _walk() -> None:
|
|
314
|
+
nonlocal total
|
|
315
|
+
for dirpath_str, dirnames, filenames in os.walk(project_root):
|
|
316
|
+
dirpath = Path(dirpath_str)
|
|
317
|
+
rel_dir = PurePath(dirpath.relative_to(project_root))
|
|
318
|
+
if rel_dir != PurePath(".") and not matcher.is_dir_included(rel_dir):
|
|
319
|
+
dirnames.clear()
|
|
320
|
+
continue
|
|
321
|
+
|
|
322
|
+
if (dirpath / ".gitignore").is_file():
|
|
323
|
+
gitignore_dirs.append(str(rel_dir))
|
|
324
|
+
|
|
325
|
+
for fname in filenames:
|
|
326
|
+
rel_path = rel_dir / fname if rel_dir != PurePath(".") else PurePath(fname)
|
|
327
|
+
if matcher.is_file_included(rel_path):
|
|
328
|
+
total += 1
|
|
329
|
+
ext = PurePath(fname).suffix or "(no ext)"
|
|
330
|
+
counts_by_ext[ext] = counts_by_ext.get(ext, 0) + 1
|
|
331
|
+
|
|
332
|
+
await asyncio.get_event_loop().run_in_executor(None, _walk)
|
|
333
|
+
|
|
334
|
+
details = [f"Total matched files: {total}"]
|
|
335
|
+
for ext, count in sorted(counts_by_ext.items(), key=lambda x: -x[1]):
|
|
336
|
+
details.append(f" {ext}: {count}")
|
|
337
|
+
if gitignore_dirs:
|
|
338
|
+
details.append(f"Loaded .gitignore from: {', '.join(gitignore_dirs)}")
|
|
339
|
+
|
|
340
|
+
return DoctorCheckResult(name="File Walk", ok=True, details=details, errors=[])
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
async def _check_index_status(project_root_str: str) -> DoctorCheckResult:
|
|
344
|
+
"""Check index status by querying target_sqlite.db directly."""
|
|
345
|
+
from cocoindex.connectors import sqlite as coco_sqlite
|
|
346
|
+
|
|
347
|
+
project_root = Path(project_root_str)
|
|
348
|
+
db_path = project_root / ".cocoindex_code" / "target_sqlite.db"
|
|
349
|
+
details = [f"Index: {db_path}"]
|
|
350
|
+
|
|
351
|
+
if not db_path.exists():
|
|
352
|
+
details.append("Index not created yet.")
|
|
353
|
+
return DoctorCheckResult(name="Index Status", ok=True, details=details, errors=[])
|
|
354
|
+
|
|
355
|
+
try:
|
|
356
|
+
conn = coco_sqlite.connect(str(db_path), load_vec=True)
|
|
357
|
+
try:
|
|
358
|
+
with conn.readonly() as db:
|
|
359
|
+
total_chunks = db.execute("SELECT COUNT(*) FROM code_chunks_vec").fetchone()[0]
|
|
360
|
+
file_rows = db.execute("SELECT DISTINCT file_path FROM code_chunks_vec").fetchall()
|
|
361
|
+
total_files = len(file_rows)
|
|
362
|
+
lang_rows = db.execute(
|
|
363
|
+
"SELECT language, COUNT(*) FROM code_chunks_vec GROUP BY language"
|
|
364
|
+
).fetchall()
|
|
365
|
+
languages = {row[0]: row[1] for row in lang_rows}
|
|
366
|
+
finally:
|
|
367
|
+
conn.close()
|
|
368
|
+
|
|
369
|
+
details.append(f"Chunks: {total_chunks}")
|
|
370
|
+
details.append(f"Files: {total_files}")
|
|
371
|
+
if languages:
|
|
372
|
+
details.append("Languages:")
|
|
373
|
+
for lang, count in sorted(languages.items(), key=lambda x: -x[1]):
|
|
374
|
+
details.append(f" {lang}: {count} chunks")
|
|
375
|
+
return DoctorCheckResult(name="Index Status", ok=True, details=details, errors=[])
|
|
376
|
+
except Exception as e:
|
|
377
|
+
return DoctorCheckResult(name="Index Status", ok=False, details=details, errors=[str(e)])
|
|
378
|
+
|
|
379
|
+
|
|
234
380
|
async def _dispatch(
|
|
235
381
|
req: Request,
|
|
236
382
|
registry: ProjectRegistry,
|
|
237
383
|
start_time: float,
|
|
238
384
|
on_shutdown: Callable[[], None],
|
|
239
|
-
|
|
385
|
+
settings_env_names: list[str],
|
|
386
|
+
) -> (
|
|
387
|
+
Response
|
|
388
|
+
| AsyncIterator[IndexStreamResponse]
|
|
389
|
+
| AsyncIterator[SearchStreamResponse]
|
|
390
|
+
| AsyncIterator[DoctorStreamResponse]
|
|
391
|
+
):
|
|
240
392
|
"""Dispatch a request to the appropriate handler.
|
|
241
393
|
|
|
242
394
|
Returns a single Response for most requests, or an AsyncIterator for
|
|
243
|
-
streaming requests (IndexRequest,
|
|
244
|
-
load-time indexing).
|
|
395
|
+
streaming requests (IndexRequest, SearchRequest when waiting, DoctorRequest).
|
|
245
396
|
"""
|
|
246
397
|
try:
|
|
247
398
|
if isinstance(req, IndexRequest):
|
|
@@ -289,6 +440,15 @@ async def _dispatch(
|
|
|
289
440
|
on_shutdown()
|
|
290
441
|
return StopResponse(ok=True)
|
|
291
442
|
|
|
443
|
+
if isinstance(req, DaemonEnvRequest):
|
|
444
|
+
return DaemonEnvResponse(
|
|
445
|
+
env_names=sorted(os.environ.keys()),
|
|
446
|
+
settings_env_names=settings_env_names,
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
if isinstance(req, DoctorRequest):
|
|
450
|
+
return _handle_doctor(req, registry)
|
|
451
|
+
|
|
292
452
|
return ErrorResponse(message=f"Unknown request type: {type(req).__name__}")
|
|
293
453
|
except Exception as e:
|
|
294
454
|
logger.exception("Error dispatching request")
|
|
@@ -314,6 +474,7 @@ def run_daemon() -> None:
|
|
|
314
474
|
settings_mtime_us = global_settings_mtime_us()
|
|
315
475
|
|
|
316
476
|
# Set environment variables from settings
|
|
477
|
+
settings_env_keys = list(user_settings.envs.keys())
|
|
317
478
|
for key, value in user_settings.envs.items():
|
|
318
479
|
os.environ[key] = value
|
|
319
480
|
|
|
@@ -363,6 +524,7 @@ def run_daemon() -> None:
|
|
|
363
524
|
start_time,
|
|
364
525
|
_request_shutdown,
|
|
365
526
|
settings_mtime_us,
|
|
527
|
+
settings_env_keys,
|
|
366
528
|
)
|
|
367
529
|
)
|
|
368
530
|
tasks.add(task)
|
|
@@ -42,6 +42,14 @@ class StopRequest(_msgspec.Struct, tag="stop"):
|
|
|
42
42
|
pass
|
|
43
43
|
|
|
44
44
|
|
|
45
|
+
class DoctorRequest(_msgspec.Struct, tag="doctor"):
|
|
46
|
+
project_root: str | None = None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class DaemonEnvRequest(_msgspec.Struct, tag="daemon_env"):
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
|
|
45
53
|
Request = (
|
|
46
54
|
HandshakeRequest
|
|
47
55
|
| IndexRequest
|
|
@@ -50,6 +58,8 @@ Request = (
|
|
|
50
58
|
| DaemonStatusRequest
|
|
51
59
|
| RemoveProjectRequest
|
|
52
60
|
| StopRequest
|
|
61
|
+
| DoctorRequest
|
|
62
|
+
| DaemonEnvRequest
|
|
53
63
|
)
|
|
54
64
|
|
|
55
65
|
# ---------------------------------------------------------------------------
|
|
@@ -136,6 +146,23 @@ class StopResponse(_msgspec.Struct, tag="stop"):
|
|
|
136
146
|
ok: bool
|
|
137
147
|
|
|
138
148
|
|
|
149
|
+
class DoctorCheckResult(_msgspec.Struct):
|
|
150
|
+
name: str
|
|
151
|
+
ok: bool
|
|
152
|
+
details: list[str]
|
|
153
|
+
errors: list[str]
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class DoctorResponse(_msgspec.Struct, tag="doctor"):
|
|
157
|
+
result: DoctorCheckResult
|
|
158
|
+
final: bool = False
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class DaemonEnvResponse(_msgspec.Struct, tag="daemon_env"):
|
|
162
|
+
env_names: list[str]
|
|
163
|
+
settings_env_names: list[str]
|
|
164
|
+
|
|
165
|
+
|
|
139
166
|
class ErrorResponse(_msgspec.Struct, tag="error"):
|
|
140
167
|
message: str
|
|
141
168
|
|
|
@@ -150,11 +177,14 @@ Response = (
|
|
|
150
177
|
| DaemonStatusResponse
|
|
151
178
|
| RemoveProjectResponse
|
|
152
179
|
| StopResponse
|
|
180
|
+
| DoctorResponse
|
|
181
|
+
| DaemonEnvResponse
|
|
153
182
|
| ErrorResponse
|
|
154
183
|
)
|
|
155
184
|
|
|
156
185
|
IndexStreamResponse = IndexProgressUpdate | IndexWaitingNotice | IndexResponse | ErrorResponse
|
|
157
186
|
SearchStreamResponse = IndexWaitingNotice | SearchResponse | ErrorResponse
|
|
187
|
+
DoctorStreamResponse = DoctorResponse | ErrorResponse
|
|
158
188
|
|
|
159
189
|
# ---------------------------------------------------------------------------
|
|
160
190
|
# Encode / decode helpers (msgpack binary)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|