cocoindex-code 0.2.6__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.
Files changed (20) hide show
  1. {cocoindex_code-0.2.6 → cocoindex_code-0.2.7}/PKG-INFO +4 -1
  2. {cocoindex_code-0.2.6 → cocoindex_code-0.2.7}/README.md +3 -0
  3. {cocoindex_code-0.2.6 → cocoindex_code-0.2.7}/src/cocoindex_code/_version.py +2 -2
  4. {cocoindex_code-0.2.6 → cocoindex_code-0.2.7}/src/cocoindex_code/cli.py +53 -14
  5. {cocoindex_code-0.2.6 → cocoindex_code-0.2.7}/src/cocoindex_code/client.py +58 -10
  6. {cocoindex_code-0.2.6 → cocoindex_code-0.2.7}/src/cocoindex_code/settings.py +17 -11
  7. {cocoindex_code-0.2.6 → cocoindex_code-0.2.7}/.gitignore +0 -0
  8. {cocoindex_code-0.2.6 → cocoindex_code-0.2.7}/LICENSE +0 -0
  9. {cocoindex_code-0.2.6 → cocoindex_code-0.2.7}/pyproject.toml +0 -0
  10. {cocoindex_code-0.2.6 → cocoindex_code-0.2.7}/src/cocoindex_code/__init__.py +0 -0
  11. {cocoindex_code-0.2.6 → cocoindex_code-0.2.7}/src/cocoindex_code/__main__.py +0 -0
  12. {cocoindex_code-0.2.6 → cocoindex_code-0.2.7}/src/cocoindex_code/config.py +0 -0
  13. {cocoindex_code-0.2.6 → cocoindex_code-0.2.7}/src/cocoindex_code/daemon.py +0 -0
  14. {cocoindex_code-0.2.6 → cocoindex_code-0.2.7}/src/cocoindex_code/indexer.py +0 -0
  15. {cocoindex_code-0.2.6 → cocoindex_code-0.2.7}/src/cocoindex_code/project.py +0 -0
  16. {cocoindex_code-0.2.6 → cocoindex_code-0.2.7}/src/cocoindex_code/protocol.py +0 -0
  17. {cocoindex_code-0.2.6 → cocoindex_code-0.2.7}/src/cocoindex_code/query.py +0 -0
  18. {cocoindex_code-0.2.6 → cocoindex_code-0.2.7}/src/cocoindex_code/schema.py +0 -0
  19. {cocoindex_code-0.2.6 → cocoindex_code-0.2.7}/src/cocoindex_code/server.py +0 -0
  20. {cocoindex_code-0.2.6 → 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.6
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
@@ -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.6'
32
- __version_tuple__ = version_tuple = (0, 2, 6)
31
+ __version__ = version = '0.2.7'
32
+ __version_tuple__ = version_tuple = (0, 2, 7)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -2,10 +2,14 @@
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
 
12
+ from .client import DaemonStartError
9
13
  from .protocol import DoctorCheckResult, IndexingProgress, ProjectStatusResponse, SearchResponse
10
14
  from .settings import (
11
15
  default_project_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
- Exits with code 1 if not found.
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
@@ -446,6 +485,7 @@ def _print_doctor_result(result: DoctorCheckResult) -> None:
446
485
 
447
486
 
448
487
  @app.command()
488
+ @_catch_daemon_start_error
449
489
  def doctor() -> None:
450
490
  """Check system health and report issues."""
451
491
  from . import client as _client
@@ -553,6 +593,7 @@ def doctor() -> None:
553
593
 
554
594
 
555
595
  @app.command()
596
+ @_catch_daemon_start_error
556
597
  def mcp() -> None:
557
598
  """Run as MCP server (stdio mode)."""
558
599
  import asyncio
@@ -586,6 +627,7 @@ async def _bg_index(project_root: str) -> None:
586
627
 
587
628
 
588
629
  @daemon_app.command("status")
630
+ @_catch_daemon_start_error
589
631
  def daemon_status() -> None:
590
632
  """Show daemon status."""
591
633
  from . import client as _client
@@ -603,6 +645,7 @@ def daemon_status() -> None:
603
645
 
604
646
 
605
647
  @daemon_app.command("restart")
648
+ @_catch_daemon_start_error
606
649
  def daemon_restart() -> None:
607
650
  """Restart the daemon."""
608
651
  from .client import _wait_for_daemon, start_daemon, stop_daemon
@@ -611,13 +654,9 @@ def daemon_restart() -> None:
611
654
  stop_daemon()
612
655
 
613
656
  _typer.echo("Starting daemon...")
614
- start_daemon()
615
- try:
616
- _wait_for_daemon()
617
- _typer.echo("Daemon restarted.")
618
- except TimeoutError:
619
- _typer.echo("Error: Daemon did not start in time.", err=True)
620
- raise _typer.Exit(code=1)
657
+ proc = start_daemon()
658
+ _wait_for_daemon(proc=proc)
659
+ _typer.echo("Daemon restarted.")
621
660
 
622
661
 
623
662
  @daemon_app.command("stop")
@@ -18,7 +18,7 @@ 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
23
  DaemonEnvRequest,
24
24
  DaemonEnvResponse,
@@ -84,8 +84,8 @@ def _connect_and_handshake() -> Connection:
84
84
  except (ConnectionRefusedError, OSError):
85
85
  pass
86
86
 
87
- start_daemon()
88
- _wait_for_daemon()
87
+ proc = start_daemon()
88
+ _wait_for_daemon(proc=proc)
89
89
 
90
90
  # Verify the fresh daemon is reachable
91
91
  for _attempt in range(10):
@@ -145,6 +145,27 @@ class DaemonVersionError(RuntimeError):
145
145
  )
146
146
 
147
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
+
148
169
  def _send(req: Request) -> Response:
149
170
  """Open connection, handshake, send one request, read one response, close."""
150
171
  conn = _connect_and_handshake()
@@ -316,8 +337,12 @@ def is_daemon_running() -> bool:
316
337
  return os.path.exists(daemon_socket_path())
317
338
 
318
339
 
319
- def start_daemon() -> None:
320
- """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
+ """
321
346
  from .daemon import daemon_dir
322
347
 
323
348
  daemon_dir().mkdir(parents=True, exist_ok=True)
@@ -332,7 +357,7 @@ def start_daemon() -> None:
332
357
  log_fd = open(log_path, "w")
333
358
  if sys.platform == "win32":
334
359
  _create_no_window = 0x08000000
335
- subprocess.Popen(
360
+ proc = subprocess.Popen(
336
361
  cmd,
337
362
  stdout=log_fd,
338
363
  stderr=log_fd,
@@ -340,7 +365,7 @@ def start_daemon() -> None:
340
365
  creationflags=_create_no_window,
341
366
  )
342
367
  else:
343
- subprocess.Popen(
368
+ proc = subprocess.Popen(
344
369
  cmd,
345
370
  start_new_session=True,
346
371
  stdout=log_fd,
@@ -348,6 +373,7 @@ def start_daemon() -> None:
348
373
  stdin=subprocess.DEVNULL,
349
374
  )
350
375
  log_fd.close()
376
+ return proc
351
377
 
352
378
 
353
379
  def _find_ccc_executable() -> str | None:
@@ -469,11 +495,27 @@ def _cleanup_stale_files(pid_path: Path, pid: int | None) -> None:
469
495
  pass
470
496
 
471
497
 
472
- def _wait_for_daemon(timeout: float = 30.0) -> None:
473
- """Wait for the daemon socket/pipe to become available."""
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
+ """
474
508
  deadline = time.monotonic() + timeout
475
509
  sock_path = daemon_socket_path()
476
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
+
477
519
  if sys.platform == "win32":
478
520
  try:
479
521
  conn = Client(sock_path, family=_connection_family())
@@ -485,7 +527,13 @@ def _wait_for_daemon(timeout: float = 30.0) -> None:
485
527
  if os.path.exists(sock_path):
486
528
  return
487
529
  time.sleep(0.2)
488
- raise TimeoutError("Daemon did not start in time")
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)
489
537
 
490
538
 
491
539
  def _needs_restart(resp: HandshakeResponse) -> bool:
@@ -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("global_settings.yml must contain 'embedding' with at least 'model' field")
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
- with open(path) as f:
292
- data = _yaml.safe_load(f)
293
- if not data:
294
- raise ValueError(f"User settings file is empty: {path}")
295
- return _user_settings_from_dict(data)
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
- with open(path) as f:
316
- data = _yaml.safe_load(f)
317
- if not data:
318
- return default_project_settings()
319
- return _project_settings_from_dict(data)
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