cocoindex-code 0.2.5__tar.gz → 0.2.7__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.7}/PKG-INFO +5 -2
- {cocoindex_code-0.2.5 → cocoindex_code-0.2.7}/README.md +3 -0
- {cocoindex_code-0.2.5 → cocoindex_code-0.2.7}/pyproject.toml +1 -1
- {cocoindex_code-0.2.5 → cocoindex_code-0.2.7}/src/cocoindex_code/_version.py +2 -2
- {cocoindex_code-0.2.5 → cocoindex_code-0.2.7}/src/cocoindex_code/cli.py +191 -15
- {cocoindex_code-0.2.5 → cocoindex_code-0.2.7}/src/cocoindex_code/client.py +98 -10
- {cocoindex_code-0.2.5 → cocoindex_code-0.2.7}/src/cocoindex_code/daemon.py +166 -4
- {cocoindex_code-0.2.5 → cocoindex_code-0.2.7}/src/cocoindex_code/protocol.py +30 -0
- {cocoindex_code-0.2.5 → cocoindex_code-0.2.7}/src/cocoindex_code/settings.py +17 -11
- {cocoindex_code-0.2.5 → cocoindex_code-0.2.7}/.gitignore +0 -0
- {cocoindex_code-0.2.5 → cocoindex_code-0.2.7}/LICENSE +0 -0
- {cocoindex_code-0.2.5 → cocoindex_code-0.2.7}/src/cocoindex_code/__init__.py +0 -0
- {cocoindex_code-0.2.5 → cocoindex_code-0.2.7}/src/cocoindex_code/__main__.py +0 -0
- {cocoindex_code-0.2.5 → cocoindex_code-0.2.7}/src/cocoindex_code/config.py +0 -0
- {cocoindex_code-0.2.5 → cocoindex_code-0.2.7}/src/cocoindex_code/indexer.py +0 -0
- {cocoindex_code-0.2.5 → cocoindex_code-0.2.7}/src/cocoindex_code/project.py +0 -0
- {cocoindex_code-0.2.5 → cocoindex_code-0.2.7}/src/cocoindex_code/query.py +0 -0
- {cocoindex_code-0.2.5 → cocoindex_code-0.2.7}/src/cocoindex_code/schema.py +0 -0
- {cocoindex_code-0.2.5 → cocoindex_code-0.2.7}/src/cocoindex_code/server.py +0 -0
- {cocoindex_code-0.2.5 → cocoindex_code-0.2.7}/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.7
|
|
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
|
|
@@ -208,6 +208,7 @@ The background daemon starts automatically on first use.
|
|
|
208
208
|
| `ccc search <query>` | Semantic search across the codebase |
|
|
209
209
|
| `ccc status` | Show index stats (chunk count, file count, language breakdown) |
|
|
210
210
|
| `ccc mcp` | Run as MCP server in stdio mode |
|
|
211
|
+
| `ccc doctor` | Run diagnostics — checks settings, daemon, model, file matching, and index health |
|
|
211
212
|
| `ccc reset` | Delete index databases. `--all` also removes settings. `-f` skips confirmation. |
|
|
212
213
|
| `ccc daemon status` | Show daemon version, uptime, and loaded projects |
|
|
213
214
|
| `ccc daemon restart` | Restart the background daemon |
|
|
@@ -458,6 +459,8 @@ embedding:
|
|
|
458
459
|
|
|
459
460
|
## Troubleshooting
|
|
460
461
|
|
|
462
|
+
Run `ccc doctor` to diagnose common issues. It checks your settings, daemon health, embedding model, file matching, and index status — all in one command.
|
|
463
|
+
|
|
461
464
|
### `sqlite3.Connection object has no attribute enable_load_extension`
|
|
462
465
|
|
|
463
466
|
Some Python installations (e.g. the one pre-installed on macOS) ship with a SQLite library that doesn't enable extensions.
|
|
@@ -169,6 +169,7 @@ The background daemon starts automatically on first use.
|
|
|
169
169
|
| `ccc search <query>` | Semantic search across the codebase |
|
|
170
170
|
| `ccc status` | Show index stats (chunk count, file count, language breakdown) |
|
|
171
171
|
| `ccc mcp` | Run as MCP server in stdio mode |
|
|
172
|
+
| `ccc doctor` | Run diagnostics — checks settings, daemon, model, file matching, and index health |
|
|
172
173
|
| `ccc reset` | Delete index databases. `--all` also removes settings. `-f` skips confirmation. |
|
|
173
174
|
| `ccc daemon status` | Show daemon version, uptime, and loaded projects |
|
|
174
175
|
| `ccc daemon restart` | Restart the background daemon |
|
|
@@ -419,6 +420,8 @@ embedding:
|
|
|
419
420
|
|
|
420
421
|
## Troubleshooting
|
|
421
422
|
|
|
423
|
+
Run `ccc doctor` to diagnose common issues. It checks your settings, daemon health, embedding model, file matching, and index status — all in one command.
|
|
424
|
+
|
|
422
425
|
### `sqlite3.Connection object has no attribute enable_load_extension`
|
|
423
426
|
|
|
424
427
|
Some Python installations (e.g. the one pre-installed on macOS) ship with a SQLite library that doesn't enable extensions.
|
|
@@ -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.7'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 2, 7)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
|
@@ -2,11 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import functools
|
|
6
|
+
from collections.abc import Callable
|
|
5
7
|
from pathlib import Path
|
|
8
|
+
from typing import TypeVar
|
|
6
9
|
|
|
7
10
|
import typer as _typer
|
|
8
11
|
|
|
9
|
-
from .
|
|
12
|
+
from .client import DaemonStartError
|
|
13
|
+
from .protocol import DoctorCheckResult, IndexingProgress, ProjectStatusResponse, SearchResponse
|
|
10
14
|
from .settings import (
|
|
11
15
|
default_project_settings,
|
|
12
16
|
default_user_settings,
|
|
@@ -35,8 +39,17 @@ app.add_typer(daemon_app, name="daemon")
|
|
|
35
39
|
def require_project_root() -> Path:
|
|
36
40
|
"""Find the project root by walking up from CWD.
|
|
37
41
|
|
|
38
|
-
|
|
42
|
+
Checks global settings first (more fundamental), then project settings.
|
|
43
|
+
Exits with code 1 if either check fails.
|
|
39
44
|
"""
|
|
45
|
+
gs_path = user_settings_path()
|
|
46
|
+
if not gs_path.is_file():
|
|
47
|
+
_typer.echo(
|
|
48
|
+
f"Error: Global settings not found: {gs_path}\n"
|
|
49
|
+
"Run `ccc init` to create it with default settings.",
|
|
50
|
+
err=True,
|
|
51
|
+
)
|
|
52
|
+
raise _typer.Exit(code=1)
|
|
40
53
|
root = find_project_root(Path.cwd())
|
|
41
54
|
if root is None:
|
|
42
55
|
_typer.echo(
|
|
@@ -48,6 +61,26 @@ def require_project_root() -> Path:
|
|
|
48
61
|
return root
|
|
49
62
|
|
|
50
63
|
|
|
64
|
+
_F = TypeVar("_F", bound=Callable[..., object])
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _catch_daemon_start_error(func: _F) -> _F:
|
|
68
|
+
"""Decorator that catches ``DaemonStartError`` and exits with a clean message.
|
|
69
|
+
|
|
70
|
+
Apply to any CLI command that may trigger daemon auto-start.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
@functools.wraps(func)
|
|
74
|
+
def wrapper(*args: object, **kwargs: object) -> object:
|
|
75
|
+
try:
|
|
76
|
+
return func(*args, **kwargs)
|
|
77
|
+
except DaemonStartError as e:
|
|
78
|
+
_typer.echo(f"Error: {e}", err=True)
|
|
79
|
+
raise _typer.Exit(code=1)
|
|
80
|
+
|
|
81
|
+
return wrapper # type: ignore[return-value]
|
|
82
|
+
|
|
83
|
+
|
|
51
84
|
def resolve_default_path(project_root: Path) -> str | None:
|
|
52
85
|
"""Compute default ``--path`` filter from CWD relative to project root."""
|
|
53
86
|
cwd = Path.cwd().resolve()
|
|
@@ -138,6 +171,9 @@ def _run_index_with_progress(project_root: str) -> None:
|
|
|
138
171
|
resp = _client.index(project_root, on_progress=_on_progress, on_waiting=_on_waiting)
|
|
139
172
|
except RuntimeError as e:
|
|
140
173
|
live.stop()
|
|
174
|
+
# Let DaemonStartError propagate to the decorator for consistent handling.
|
|
175
|
+
if isinstance(e, DaemonStartError):
|
|
176
|
+
raise
|
|
141
177
|
_typer.echo(f"Indexing failed: {e}", err=True)
|
|
142
178
|
raise _typer.Exit(code=1)
|
|
143
179
|
|
|
@@ -252,6 +288,12 @@ def init(
|
|
|
252
288
|
cwd = Path.cwd().resolve()
|
|
253
289
|
settings_file = _project_settings_path(cwd)
|
|
254
290
|
|
|
291
|
+
# Always ensure user settings exist
|
|
292
|
+
user_path = user_settings_path()
|
|
293
|
+
if not user_path.is_file():
|
|
294
|
+
save_user_settings(default_user_settings())
|
|
295
|
+
_typer.echo(f"Created user settings: {user_path}")
|
|
296
|
+
|
|
255
297
|
# Check if already initialized
|
|
256
298
|
if settings_file.is_file():
|
|
257
299
|
_typer.echo("Project already initialized.")
|
|
@@ -268,12 +310,6 @@ def init(
|
|
|
268
310
|
)
|
|
269
311
|
raise _typer.Exit(code=1)
|
|
270
312
|
|
|
271
|
-
# Create user settings if missing
|
|
272
|
-
user_path = user_settings_path()
|
|
273
|
-
if not user_path.is_file():
|
|
274
|
-
save_user_settings(default_user_settings())
|
|
275
|
-
_typer.echo(f"Created user settings: {user_path}")
|
|
276
|
-
|
|
277
313
|
# Create project settings
|
|
278
314
|
save_project_settings(cwd, default_project_settings())
|
|
279
315
|
_typer.echo(f"Created project settings: {settings_file}")
|
|
@@ -286,6 +322,7 @@ def init(
|
|
|
286
322
|
|
|
287
323
|
|
|
288
324
|
@app.command()
|
|
325
|
+
@_catch_daemon_start_error
|
|
289
326
|
def index() -> None:
|
|
290
327
|
"""Create/update index for the codebase."""
|
|
291
328
|
from . import client as _client
|
|
@@ -297,6 +334,7 @@ def index() -> None:
|
|
|
297
334
|
|
|
298
335
|
|
|
299
336
|
@app.command()
|
|
337
|
+
@_catch_daemon_start_error
|
|
300
338
|
def search(
|
|
301
339
|
query: list[str] = _typer.Argument(..., help="Search query"),
|
|
302
340
|
lang: list[str] = _typer.Option([], "--lang", help="Filter by language"),
|
|
@@ -333,6 +371,7 @@ def search(
|
|
|
333
371
|
|
|
334
372
|
|
|
335
373
|
@app.command()
|
|
374
|
+
@_catch_daemon_start_error
|
|
336
375
|
def status() -> None:
|
|
337
376
|
"""Show project status."""
|
|
338
377
|
from . import client as _client
|
|
@@ -415,7 +454,146 @@ def reset(
|
|
|
415
454
|
)
|
|
416
455
|
|
|
417
456
|
|
|
457
|
+
def _print_section(name: str) -> None:
|
|
458
|
+
import click as _click
|
|
459
|
+
|
|
460
|
+
_typer.echo()
|
|
461
|
+
_typer.echo(_click.style(f" {name}", bold=True))
|
|
462
|
+
_typer.echo(_click.style(f" {'─' * 38}", fg="bright_black"))
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def _print_error(msg: str) -> None:
|
|
466
|
+
import click as _click
|
|
467
|
+
|
|
468
|
+
_typer.echo(_click.style(f" ERROR: {msg}", fg="red"), err=True)
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def _print_doctor_result(result: DoctorCheckResult) -> None:
|
|
472
|
+
import click as _click
|
|
473
|
+
|
|
474
|
+
if result.name == "done":
|
|
475
|
+
return
|
|
476
|
+
if result.ok:
|
|
477
|
+
tag = _click.style("[OK]", fg="green", bold=True)
|
|
478
|
+
else:
|
|
479
|
+
tag = _click.style("[FAIL]", fg="red", bold=True)
|
|
480
|
+
_typer.echo(f"\n {tag} {result.name}")
|
|
481
|
+
for line in result.details:
|
|
482
|
+
_typer.echo(f" {line}")
|
|
483
|
+
for err in result.errors:
|
|
484
|
+
_typer.echo(_click.style(f" ERROR: {err}", fg="red"), err=True)
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
@app.command()
|
|
488
|
+
@_catch_daemon_start_error
|
|
489
|
+
def doctor() -> None:
|
|
490
|
+
"""Check system health and report issues."""
|
|
491
|
+
from . import client as _client
|
|
492
|
+
from .settings import (
|
|
493
|
+
load_project_settings as _load_project_settings,
|
|
494
|
+
)
|
|
495
|
+
from .settings import (
|
|
496
|
+
load_user_settings as _load_user_settings,
|
|
497
|
+
)
|
|
498
|
+
from .settings import (
|
|
499
|
+
project_settings_path as _project_settings_path,
|
|
500
|
+
)
|
|
501
|
+
from .settings import (
|
|
502
|
+
user_settings_path as _user_settings_path,
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
# --- 1. Global settings (local, no daemon needed) ---
|
|
506
|
+
_print_section("Global Settings")
|
|
507
|
+
settings_path = _user_settings_path()
|
|
508
|
+
_typer.echo(f" Settings: {settings_path}")
|
|
509
|
+
try:
|
|
510
|
+
user_settings = _load_user_settings()
|
|
511
|
+
emb = user_settings.embedding
|
|
512
|
+
device_str = f", device={emb.device}" if emb.device else ""
|
|
513
|
+
_typer.echo(f" Embedding: provider={emb.provider}, model={emb.model}{device_str}")
|
|
514
|
+
if user_settings.envs:
|
|
515
|
+
_typer.echo(
|
|
516
|
+
f" Env vars (from settings): {', '.join(sorted(user_settings.envs.keys()))}"
|
|
517
|
+
)
|
|
518
|
+
except (FileNotFoundError, ValueError) as e:
|
|
519
|
+
_print_error(str(e))
|
|
520
|
+
|
|
521
|
+
# --- 2. Connect to daemon (handshake with auto-start/restart) ---
|
|
522
|
+
_print_section("Daemon")
|
|
523
|
+
daemon_ok = False
|
|
524
|
+
try:
|
|
525
|
+
status = _client.daemon_status()
|
|
526
|
+
_typer.echo(f" Version: {status.version}")
|
|
527
|
+
_typer.echo(f" Uptime: {status.uptime_seconds:.1f}s")
|
|
528
|
+
_typer.echo(f" Loaded projects: {len(status.projects)}")
|
|
529
|
+
daemon_ok = True
|
|
530
|
+
except Exception as e:
|
|
531
|
+
_print_error(f"Cannot connect to daemon: {e}")
|
|
532
|
+
_typer.echo(" Remaining daemon-side checks will be skipped.")
|
|
533
|
+
|
|
534
|
+
# --- 3. Daemon environment (requires daemon) ---
|
|
535
|
+
if daemon_ok:
|
|
536
|
+
try:
|
|
537
|
+
env_resp = _client.daemon_env()
|
|
538
|
+
settings_keys = set(env_resp.settings_env_names)
|
|
539
|
+
other_keys = [k for k in env_resp.env_names if k not in settings_keys]
|
|
540
|
+
if other_keys:
|
|
541
|
+
_typer.echo(f" Other env vars in daemon: {', '.join(sorted(other_keys))}")
|
|
542
|
+
except Exception as e:
|
|
543
|
+
_print_error(f"Failed to get daemon env: {e}")
|
|
544
|
+
|
|
545
|
+
# --- 4. Model check (daemon-side, global — before project checks) ---
|
|
546
|
+
if daemon_ok:
|
|
547
|
+
try:
|
|
548
|
+
_client.doctor(
|
|
549
|
+
project_root=None,
|
|
550
|
+
on_result=_print_doctor_result,
|
|
551
|
+
)
|
|
552
|
+
except Exception as e:
|
|
553
|
+
_print_error(f"Model check failed: {e}")
|
|
554
|
+
|
|
555
|
+
# --- 5. Detect project ---
|
|
556
|
+
project_root = find_project_root(Path.cwd())
|
|
557
|
+
|
|
558
|
+
# --- 6. Project settings (local, no daemon needed) ---
|
|
559
|
+
if project_root is not None:
|
|
560
|
+
_print_section("Project Settings")
|
|
561
|
+
ps_path = _project_settings_path(project_root)
|
|
562
|
+
_typer.echo(f" Settings: {ps_path}")
|
|
563
|
+
try:
|
|
564
|
+
ps = _load_project_settings(project_root)
|
|
565
|
+
_typer.echo(f" Include patterns ({len(ps.include_patterns)}):")
|
|
566
|
+
_typer.echo(f" {', '.join(ps.include_patterns)}")
|
|
567
|
+
_typer.echo(f" Exclude patterns ({len(ps.exclude_patterns)}):")
|
|
568
|
+
_typer.echo(f" {', '.join(ps.exclude_patterns)}")
|
|
569
|
+
if ps.language_overrides:
|
|
570
|
+
_typer.echo(" Language overrides:")
|
|
571
|
+
for lo in ps.language_overrides:
|
|
572
|
+
_typer.echo(f" .{lo.ext} -> {lo.lang}")
|
|
573
|
+
except (FileNotFoundError, ValueError) as e:
|
|
574
|
+
_print_error(str(e))
|
|
575
|
+
|
|
576
|
+
# --- 7. Project daemon-side checks (file walk + index status) ---
|
|
577
|
+
if daemon_ok and project_root is not None:
|
|
578
|
+
try:
|
|
579
|
+
_client.doctor(
|
|
580
|
+
project_root=str(project_root),
|
|
581
|
+
on_result=_print_doctor_result,
|
|
582
|
+
)
|
|
583
|
+
except Exception as e:
|
|
584
|
+
_print_error(f"Project checks failed: {e}")
|
|
585
|
+
|
|
586
|
+
# --- 8. Log files ---
|
|
587
|
+
_print_section("Log Files")
|
|
588
|
+
from .daemon import daemon_dir as _daemon_dir
|
|
589
|
+
|
|
590
|
+
log_dir = _daemon_dir()
|
|
591
|
+
_typer.echo(f" Daemon logs: {log_dir / 'daemon.log'}")
|
|
592
|
+
_typer.echo(" Check logs above for further troubleshooting.")
|
|
593
|
+
|
|
594
|
+
|
|
418
595
|
@app.command()
|
|
596
|
+
@_catch_daemon_start_error
|
|
419
597
|
def mcp() -> None:
|
|
420
598
|
"""Run as MCP server (stdio mode)."""
|
|
421
599
|
import asyncio
|
|
@@ -449,6 +627,7 @@ async def _bg_index(project_root: str) -> None:
|
|
|
449
627
|
|
|
450
628
|
|
|
451
629
|
@daemon_app.command("status")
|
|
630
|
+
@_catch_daemon_start_error
|
|
452
631
|
def daemon_status() -> None:
|
|
453
632
|
"""Show daemon status."""
|
|
454
633
|
from . import client as _client
|
|
@@ -466,6 +645,7 @@ def daemon_status() -> None:
|
|
|
466
645
|
|
|
467
646
|
|
|
468
647
|
@daemon_app.command("restart")
|
|
648
|
+
@_catch_daemon_start_error
|
|
469
649
|
def daemon_restart() -> None:
|
|
470
650
|
"""Restart the daemon."""
|
|
471
651
|
from .client import _wait_for_daemon, start_daemon, stop_daemon
|
|
@@ -474,13 +654,9 @@ def daemon_restart() -> None:
|
|
|
474
654
|
stop_daemon()
|
|
475
655
|
|
|
476
656
|
_typer.echo("Starting daemon...")
|
|
477
|
-
start_daemon()
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
_typer.echo("Daemon restarted.")
|
|
481
|
-
except TimeoutError:
|
|
482
|
-
_typer.echo("Error: Daemon did not start in time.", err=True)
|
|
483
|
-
raise _typer.Exit(code=1)
|
|
657
|
+
proc = start_daemon()
|
|
658
|
+
_wait_for_daemon(proc=proc)
|
|
659
|
+
_typer.echo("Daemon restarted.")
|
|
484
660
|
|
|
485
661
|
|
|
486
662
|
@daemon_app.command("stop")
|
|
@@ -18,9 +18,14 @@ from multiprocessing.connection import Client, Connection
|
|
|
18
18
|
from pathlib import Path
|
|
19
19
|
|
|
20
20
|
from ._version import __version__
|
|
21
|
-
from .daemon import _connection_family, daemon_pid_path, daemon_socket_path
|
|
21
|
+
from .daemon import _connection_family, daemon_log_path, 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,
|
|
@@ -79,8 +84,8 @@ def _connect_and_handshake() -> Connection:
|
|
|
79
84
|
except (ConnectionRefusedError, OSError):
|
|
80
85
|
pass
|
|
81
86
|
|
|
82
|
-
start_daemon()
|
|
83
|
-
_wait_for_daemon()
|
|
87
|
+
proc = start_daemon()
|
|
88
|
+
_wait_for_daemon(proc=proc)
|
|
84
89
|
|
|
85
90
|
# Verify the fresh daemon is reachable
|
|
86
91
|
for _attempt in range(10):
|
|
@@ -140,6 +145,27 @@ class DaemonVersionError(RuntimeError):
|
|
|
140
145
|
)
|
|
141
146
|
|
|
142
147
|
|
|
148
|
+
class DaemonStartError(RuntimeError):
|
|
149
|
+
"""Raised when the daemon process fails to start.
|
|
150
|
+
|
|
151
|
+
Carries the daemon log content so callers can display it to the user.
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
def __init__(self, message: str, log: str | None = None) -> None:
|
|
155
|
+
self.log = log
|
|
156
|
+
super().__init__(message)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _read_daemon_log() -> str | None:
|
|
160
|
+
"""Read the daemon log file, returning its content or None."""
|
|
161
|
+
log_path = daemon_log_path()
|
|
162
|
+
try:
|
|
163
|
+
content = log_path.read_text().strip()
|
|
164
|
+
return content if content else None
|
|
165
|
+
except (FileNotFoundError, OSError):
|
|
166
|
+
return None
|
|
167
|
+
|
|
168
|
+
|
|
143
169
|
def _send(req: Request) -> Response:
|
|
144
170
|
"""Open connection, handshake, send one request, read one response, close."""
|
|
145
171
|
conn = _connect_and_handshake()
|
|
@@ -259,6 +285,41 @@ def stop() -> StopResponse:
|
|
|
259
285
|
return _send(StopRequest()) # type: ignore[return-value]
|
|
260
286
|
|
|
261
287
|
|
|
288
|
+
def daemon_env() -> DaemonEnvResponse:
|
|
289
|
+
"""Get environment variable names from the daemon."""
|
|
290
|
+
return _send(DaemonEnvRequest()) # type: ignore[return-value]
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def doctor(
|
|
294
|
+
project_root: str | None = None,
|
|
295
|
+
on_result: Callable[[DoctorCheckResult], None] | None = None,
|
|
296
|
+
) -> list[DoctorCheckResult]:
|
|
297
|
+
"""Run doctor checks via daemon, streaming results to on_result callback."""
|
|
298
|
+
conn = _connect_and_handshake()
|
|
299
|
+
try:
|
|
300
|
+
conn.send_bytes(encode_request(DoctorRequest(project_root=project_root)))
|
|
301
|
+
results: list[DoctorCheckResult] = []
|
|
302
|
+
while True:
|
|
303
|
+
try:
|
|
304
|
+
data = conn.recv_bytes()
|
|
305
|
+
except EOFError:
|
|
306
|
+
raise RuntimeError("Connection to daemon lost during doctor checks")
|
|
307
|
+
resp = decode_response(data)
|
|
308
|
+
if isinstance(resp, ErrorResponse):
|
|
309
|
+
raise RuntimeError(f"Daemon error: {resp.message}")
|
|
310
|
+
if isinstance(resp, DoctorResponse):
|
|
311
|
+
results.append(resp.result)
|
|
312
|
+
if on_result is not None:
|
|
313
|
+
on_result(resp.result)
|
|
314
|
+
if resp.final:
|
|
315
|
+
break
|
|
316
|
+
else:
|
|
317
|
+
raise RuntimeError(f"Unexpected response: {type(resp).__name__}")
|
|
318
|
+
return results
|
|
319
|
+
finally:
|
|
320
|
+
conn.close()
|
|
321
|
+
|
|
322
|
+
|
|
262
323
|
# ---------------------------------------------------------------------------
|
|
263
324
|
# Daemon lifecycle helpers
|
|
264
325
|
# ---------------------------------------------------------------------------
|
|
@@ -276,8 +337,12 @@ def is_daemon_running() -> bool:
|
|
|
276
337
|
return os.path.exists(daemon_socket_path())
|
|
277
338
|
|
|
278
339
|
|
|
279
|
-
def start_daemon() ->
|
|
280
|
-
"""Start the daemon as a background process.
|
|
340
|
+
def start_daemon() -> subprocess.Popen[bytes]:
|
|
341
|
+
"""Start the daemon as a background process.
|
|
342
|
+
|
|
343
|
+
Returns the ``Popen`` object so callers can detect early process death
|
|
344
|
+
(via ``proc.poll()``) instead of waiting for a full timeout.
|
|
345
|
+
"""
|
|
281
346
|
from .daemon import daemon_dir
|
|
282
347
|
|
|
283
348
|
daemon_dir().mkdir(parents=True, exist_ok=True)
|
|
@@ -292,7 +357,7 @@ def start_daemon() -> None:
|
|
|
292
357
|
log_fd = open(log_path, "w")
|
|
293
358
|
if sys.platform == "win32":
|
|
294
359
|
_create_no_window = 0x08000000
|
|
295
|
-
subprocess.Popen(
|
|
360
|
+
proc = subprocess.Popen(
|
|
296
361
|
cmd,
|
|
297
362
|
stdout=log_fd,
|
|
298
363
|
stderr=log_fd,
|
|
@@ -300,7 +365,7 @@ def start_daemon() -> None:
|
|
|
300
365
|
creationflags=_create_no_window,
|
|
301
366
|
)
|
|
302
367
|
else:
|
|
303
|
-
subprocess.Popen(
|
|
368
|
+
proc = subprocess.Popen(
|
|
304
369
|
cmd,
|
|
305
370
|
start_new_session=True,
|
|
306
371
|
stdout=log_fd,
|
|
@@ -308,6 +373,7 @@ def start_daemon() -> None:
|
|
|
308
373
|
stdin=subprocess.DEVNULL,
|
|
309
374
|
)
|
|
310
375
|
log_fd.close()
|
|
376
|
+
return proc
|
|
311
377
|
|
|
312
378
|
|
|
313
379
|
def _find_ccc_executable() -> str | None:
|
|
@@ -429,11 +495,27 @@ def _cleanup_stale_files(pid_path: Path, pid: int | None) -> None:
|
|
|
429
495
|
pass
|
|
430
496
|
|
|
431
497
|
|
|
432
|
-
def _wait_for_daemon(
|
|
433
|
-
|
|
498
|
+
def _wait_for_daemon(
|
|
499
|
+
timeout: float = 30.0,
|
|
500
|
+
proc: subprocess.Popen[bytes] | None = None,
|
|
501
|
+
) -> None:
|
|
502
|
+
"""Wait for the daemon socket/pipe to become available.
|
|
503
|
+
|
|
504
|
+
If *proc* is given, polls the process each iteration. When the process
|
|
505
|
+
exits before the socket appears, raises ``DaemonStartError`` immediately
|
|
506
|
+
with the daemon log content — no need to wait for the full timeout.
|
|
507
|
+
"""
|
|
434
508
|
deadline = time.monotonic() + timeout
|
|
435
509
|
sock_path = daemon_socket_path()
|
|
436
510
|
while time.monotonic() < deadline:
|
|
511
|
+
# Check if the daemon process died before the socket appeared.
|
|
512
|
+
if proc is not None and proc.poll() is not None:
|
|
513
|
+
log = _read_daemon_log()
|
|
514
|
+
msg = "Daemon process exited before it became ready."
|
|
515
|
+
if log:
|
|
516
|
+
msg += f"\n\nDaemon log:\n{log}"
|
|
517
|
+
raise DaemonStartError(msg, log=log)
|
|
518
|
+
|
|
437
519
|
if sys.platform == "win32":
|
|
438
520
|
try:
|
|
439
521
|
conn = Client(sock_path, family=_connection_family())
|
|
@@ -445,7 +527,13 @@ def _wait_for_daemon(timeout: float = 30.0) -> None:
|
|
|
445
527
|
if os.path.exists(sock_path):
|
|
446
528
|
return
|
|
447
529
|
time.sleep(0.2)
|
|
448
|
-
|
|
530
|
+
|
|
531
|
+
# Timeout — also include log for diagnostics.
|
|
532
|
+
log = _read_daemon_log()
|
|
533
|
+
msg = "Daemon did not start in time."
|
|
534
|
+
if log:
|
|
535
|
+
msg += f"\n\nDaemon log:\n{log}"
|
|
536
|
+
raise DaemonStartError(msg, log=log)
|
|
449
537
|
|
|
450
538
|
|
|
451
539
|
def _needs_restart(resp: HandshakeResponse) -> bool:
|
|
@@ -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)
|
|
@@ -240,7 +240,7 @@ def _user_settings_to_dict(settings: UserSettings) -> dict[str, Any]:
|
|
|
240
240
|
def _user_settings_from_dict(d: dict[str, Any]) -> UserSettings:
|
|
241
241
|
emb_dict = d.get("embedding")
|
|
242
242
|
if not emb_dict or "model" not in emb_dict:
|
|
243
|
-
raise ValueError("
|
|
243
|
+
raise ValueError("Must contain 'embedding' with at least 'model' field")
|
|
244
244
|
# Only pass keys that are present; provider uses dataclass default ("litellm") if omitted
|
|
245
245
|
emb_kwargs: dict[str, Any] = {"model": emb_dict["model"]}
|
|
246
246
|
if "provider" in emb_dict:
|
|
@@ -288,11 +288,14 @@ def load_user_settings() -> UserSettings:
|
|
|
288
288
|
path = user_settings_path()
|
|
289
289
|
if not path.is_file():
|
|
290
290
|
raise FileNotFoundError(f"User settings not found: {path}")
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
291
|
+
try:
|
|
292
|
+
with open(path) as f:
|
|
293
|
+
data = _yaml.safe_load(f)
|
|
294
|
+
if not data:
|
|
295
|
+
raise ValueError("File is empty")
|
|
296
|
+
return _user_settings_from_dict(data)
|
|
297
|
+
except Exception as e:
|
|
298
|
+
raise type(e)(f"Error loading {path}: {e}") from e
|
|
296
299
|
|
|
297
300
|
|
|
298
301
|
def save_user_settings(settings: UserSettings) -> Path:
|
|
@@ -312,11 +315,14 @@ def load_project_settings(project_root: Path) -> ProjectSettings:
|
|
|
312
315
|
path = project_settings_path(project_root)
|
|
313
316
|
if not path.is_file():
|
|
314
317
|
raise FileNotFoundError(f"Project settings not found: {path}")
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
318
|
+
try:
|
|
319
|
+
with open(path) as f:
|
|
320
|
+
data = _yaml.safe_load(f)
|
|
321
|
+
if not data:
|
|
322
|
+
return default_project_settings()
|
|
323
|
+
return _project_settings_from_dict(data)
|
|
324
|
+
except Exception as e:
|
|
325
|
+
raise type(e)(f"Error loading {path}: {e}") from e
|
|
320
326
|
|
|
321
327
|
|
|
322
328
|
def save_project_settings(project_root: Path, settings: ProjectSettings) -> Path:
|
|
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
|