cocoindex-code 0.2.6__tar.gz → 0.2.8__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.6 → cocoindex_code-0.2.8}/PKG-INFO +37 -1
- {cocoindex_code-0.2.6 → cocoindex_code-0.2.8}/README.md +36 -0
- {cocoindex_code-0.2.6 → cocoindex_code-0.2.8}/src/cocoindex_code/_version.py +2 -2
- {cocoindex_code-0.2.6 → cocoindex_code-0.2.8}/src/cocoindex_code/cli.py +67 -16
- {cocoindex_code-0.2.6 → cocoindex_code-0.2.8}/src/cocoindex_code/client.py +58 -10
- {cocoindex_code-0.2.6 → cocoindex_code-0.2.8}/src/cocoindex_code/config.py +4 -2
- {cocoindex_code-0.2.6 → cocoindex_code-0.2.8}/src/cocoindex_code/daemon.py +9 -1
- {cocoindex_code-0.2.6 → cocoindex_code-0.2.8}/src/cocoindex_code/project.py +9 -5
- {cocoindex_code-0.2.6 → cocoindex_code-0.2.8}/src/cocoindex_code/protocol.py +6 -0
- {cocoindex_code-0.2.6 → cocoindex_code-0.2.8}/src/cocoindex_code/settings.py +95 -13
- {cocoindex_code-0.2.6 → cocoindex_code-0.2.8}/.gitignore +0 -0
- {cocoindex_code-0.2.6 → cocoindex_code-0.2.8}/LICENSE +0 -0
- {cocoindex_code-0.2.6 → cocoindex_code-0.2.8}/pyproject.toml +0 -0
- {cocoindex_code-0.2.6 → cocoindex_code-0.2.8}/src/cocoindex_code/__init__.py +0 -0
- {cocoindex_code-0.2.6 → cocoindex_code-0.2.8}/src/cocoindex_code/__main__.py +0 -0
- {cocoindex_code-0.2.6 → cocoindex_code-0.2.8}/src/cocoindex_code/indexer.py +0 -0
- {cocoindex_code-0.2.6 → cocoindex_code-0.2.8}/src/cocoindex_code/query.py +0 -0
- {cocoindex_code-0.2.6 → cocoindex_code-0.2.8}/src/cocoindex_code/schema.py +0 -0
- {cocoindex_code-0.2.6 → cocoindex_code-0.2.8}/src/cocoindex_code/server.py +0 -0
- {cocoindex_code-0.2.6 → cocoindex_code-0.2.8}/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.8
|
|
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 |
|
|
@@ -456,8 +457,30 @@ embedding:
|
|
|
456
457
|
| xml | | `.xml` |
|
|
457
458
|
| yaml | | `.yaml`, `.yml` |
|
|
458
459
|
|
|
460
|
+
### Custom Database Location
|
|
461
|
+
|
|
462
|
+
By default, index databases (`cocoindex.db` and `target_sqlite.db`) live alongside settings in `<project>/.cocoindex_code/`. When running in Docker, you may want the databases on the container's native filesystem for performance (LMDB doesn't work well on mounted volumes) while keeping the source code and settings on a mounted volume.
|
|
463
|
+
|
|
464
|
+
Set `COCOINDEX_CODE_DB_PATH_MAPPING` to remap database locations by path prefix:
|
|
465
|
+
|
|
466
|
+
```bash
|
|
467
|
+
COCOINDEX_CODE_DB_PATH_MAPPING=/workspace=/db-files
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
With this mapping, a project at `/workspace/myrepo` stores its databases in `/db-files/myrepo/` instead of `/workspace/myrepo/.cocoindex_code/`. Settings files remain in the original location.
|
|
471
|
+
|
|
472
|
+
Multiple mappings are comma-separated and resolved in order (first match wins):
|
|
473
|
+
|
|
474
|
+
```bash
|
|
475
|
+
COCOINDEX_CODE_DB_PATH_MAPPING=/workspace=/db-files,/workspace2=/db-files2
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
Both source and target must be absolute paths. If no mapping matches, the default location is used.
|
|
479
|
+
|
|
459
480
|
## Troubleshooting
|
|
460
481
|
|
|
482
|
+
Run `ccc doctor` to diagnose common issues. It checks your settings, daemon health, embedding model, file matching, and index status — all in one command.
|
|
483
|
+
|
|
461
484
|
### `sqlite3.Connection object has no attribute enable_load_extension`
|
|
462
485
|
|
|
463
486
|
Some Python installations (e.g. the one pre-installed on macOS) ship with a SQLite library that doesn't enable extensions.
|
|
@@ -498,6 +521,19 @@ If you previously configured `cocoindex-code` via environment variables, the `co
|
|
|
498
521
|
|
|
499
522
|
If you need help with remote setup, please email our maintainer linghua@cocoindex.io, happy to help!
|
|
500
523
|
|
|
524
|
+
## Contributing
|
|
525
|
+
|
|
526
|
+
We welcome contributions! Before you start, please install the [pre-commit](https://pre-commit.com/) hooks so that linting, formatting, type checking, and tests run automatically before each commit:
|
|
527
|
+
|
|
528
|
+
```bash
|
|
529
|
+
pip install pre-commit
|
|
530
|
+
pre-commit install
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
This catches common issues — trailing whitespace, lint errors (Ruff), type errors (mypy), and test failures — before they reach CI.
|
|
534
|
+
|
|
535
|
+
For more details, see our [contributing guide](https://cocoindex.io/docs/contributing/guide).
|
|
536
|
+
|
|
501
537
|
## License
|
|
502
538
|
|
|
503
539
|
Apache-2.0
|
|
@@ -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 |
|
|
@@ -417,8 +418,30 @@ embedding:
|
|
|
417
418
|
| xml | | `.xml` |
|
|
418
419
|
| yaml | | `.yaml`, `.yml` |
|
|
419
420
|
|
|
421
|
+
### Custom Database Location
|
|
422
|
+
|
|
423
|
+
By default, index databases (`cocoindex.db` and `target_sqlite.db`) live alongside settings in `<project>/.cocoindex_code/`. When running in Docker, you may want the databases on the container's native filesystem for performance (LMDB doesn't work well on mounted volumes) while keeping the source code and settings on a mounted volume.
|
|
424
|
+
|
|
425
|
+
Set `COCOINDEX_CODE_DB_PATH_MAPPING` to remap database locations by path prefix:
|
|
426
|
+
|
|
427
|
+
```bash
|
|
428
|
+
COCOINDEX_CODE_DB_PATH_MAPPING=/workspace=/db-files
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
With this mapping, a project at `/workspace/myrepo` stores its databases in `/db-files/myrepo/` instead of `/workspace/myrepo/.cocoindex_code/`. Settings files remain in the original location.
|
|
432
|
+
|
|
433
|
+
Multiple mappings are comma-separated and resolved in order (first match wins):
|
|
434
|
+
|
|
435
|
+
```bash
|
|
436
|
+
COCOINDEX_CODE_DB_PATH_MAPPING=/workspace=/db-files,/workspace2=/db-files2
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
Both source and target must be absolute paths. If no mapping matches, the default location is used.
|
|
440
|
+
|
|
420
441
|
## Troubleshooting
|
|
421
442
|
|
|
443
|
+
Run `ccc doctor` to diagnose common issues. It checks your settings, daemon health, embedding model, file matching, and index status — all in one command.
|
|
444
|
+
|
|
422
445
|
### `sqlite3.Connection object has no attribute enable_load_extension`
|
|
423
446
|
|
|
424
447
|
Some Python installations (e.g. the one pre-installed on macOS) ship with a SQLite library that doesn't enable extensions.
|
|
@@ -459,6 +482,19 @@ If you previously configured `cocoindex-code` via environment variables, the `co
|
|
|
459
482
|
|
|
460
483
|
If you need help with remote setup, please email our maintainer linghua@cocoindex.io, happy to help!
|
|
461
484
|
|
|
485
|
+
## Contributing
|
|
486
|
+
|
|
487
|
+
We welcome contributions! Before you start, please install the [pre-commit](https://pre-commit.com/) hooks so that linting, formatting, type checking, and tests run automatically before each commit:
|
|
488
|
+
|
|
489
|
+
```bash
|
|
490
|
+
pip install pre-commit
|
|
491
|
+
pre-commit install
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
This catches common issues — trailing whitespace, lint errors (Ruff), type errors (mypy), and test failures — before they reach CI.
|
|
495
|
+
|
|
496
|
+
For more details, see our [contributing guide](https://cocoindex.io/docs/contributing/guide).
|
|
497
|
+
|
|
462
498
|
## License
|
|
463
499
|
|
|
464
500
|
Apache-2.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.8'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 2, 8)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
|
@@ -2,16 +2,21 @@
|
|
|
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,
|
|
12
16
|
default_user_settings,
|
|
13
17
|
find_parent_with_marker,
|
|
14
18
|
find_project_root,
|
|
19
|
+
resolve_db_dir,
|
|
15
20
|
save_project_settings,
|
|
16
21
|
save_user_settings,
|
|
17
22
|
user_settings_path,
|
|
@@ -35,8 +40,17 @@ app.add_typer(daemon_app, name="daemon")
|
|
|
35
40
|
def require_project_root() -> Path:
|
|
36
41
|
"""Find the project root by walking up from CWD.
|
|
37
42
|
|
|
38
|
-
|
|
43
|
+
Checks global settings first (more fundamental), then project settings.
|
|
44
|
+
Exits with code 1 if either check fails.
|
|
39
45
|
"""
|
|
46
|
+
gs_path = user_settings_path()
|
|
47
|
+
if not gs_path.is_file():
|
|
48
|
+
_typer.echo(
|
|
49
|
+
f"Error: Global settings not found: {gs_path}\n"
|
|
50
|
+
"Run `ccc init` to create it with default settings.",
|
|
51
|
+
err=True,
|
|
52
|
+
)
|
|
53
|
+
raise _typer.Exit(code=1)
|
|
40
54
|
root = find_project_root(Path.cwd())
|
|
41
55
|
if root is None:
|
|
42
56
|
_typer.echo(
|
|
@@ -48,6 +62,26 @@ def require_project_root() -> Path:
|
|
|
48
62
|
return root
|
|
49
63
|
|
|
50
64
|
|
|
65
|
+
_F = TypeVar("_F", bound=Callable[..., object])
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _catch_daemon_start_error(func: _F) -> _F:
|
|
69
|
+
"""Decorator that catches ``DaemonStartError`` and exits with a clean message.
|
|
70
|
+
|
|
71
|
+
Apply to any CLI command that may trigger daemon auto-start.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
@functools.wraps(func)
|
|
75
|
+
def wrapper(*args: object, **kwargs: object) -> object:
|
|
76
|
+
try:
|
|
77
|
+
return func(*args, **kwargs)
|
|
78
|
+
except DaemonStartError as e:
|
|
79
|
+
_typer.echo(f"Error: {e}", err=True)
|
|
80
|
+
raise _typer.Exit(code=1)
|
|
81
|
+
|
|
82
|
+
return wrapper # type: ignore[return-value]
|
|
83
|
+
|
|
84
|
+
|
|
51
85
|
def resolve_default_path(project_root: Path) -> str | None:
|
|
52
86
|
"""Compute default ``--path`` filter from CWD relative to project root."""
|
|
53
87
|
cwd = Path.cwd().resolve()
|
|
@@ -138,6 +172,9 @@ def _run_index_with_progress(project_root: str) -> None:
|
|
|
138
172
|
resp = _client.index(project_root, on_progress=_on_progress, on_waiting=_on_waiting)
|
|
139
173
|
except RuntimeError as e:
|
|
140
174
|
live.stop()
|
|
175
|
+
# Let DaemonStartError propagate to the decorator for consistent handling.
|
|
176
|
+
if isinstance(e, DaemonStartError):
|
|
177
|
+
raise
|
|
141
178
|
_typer.echo(f"Indexing failed: {e}", err=True)
|
|
142
179
|
raise _typer.Exit(code=1)
|
|
143
180
|
|
|
@@ -252,6 +289,12 @@ def init(
|
|
|
252
289
|
cwd = Path.cwd().resolve()
|
|
253
290
|
settings_file = _project_settings_path(cwd)
|
|
254
291
|
|
|
292
|
+
# Always ensure user settings exist
|
|
293
|
+
user_path = user_settings_path()
|
|
294
|
+
if not user_path.is_file():
|
|
295
|
+
save_user_settings(default_user_settings())
|
|
296
|
+
_typer.echo(f"Created user settings: {user_path}")
|
|
297
|
+
|
|
255
298
|
# Check if already initialized
|
|
256
299
|
if settings_file.is_file():
|
|
257
300
|
_typer.echo("Project already initialized.")
|
|
@@ -268,12 +311,6 @@ def init(
|
|
|
268
311
|
)
|
|
269
312
|
raise _typer.Exit(code=1)
|
|
270
313
|
|
|
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
314
|
# Create project settings
|
|
278
315
|
save_project_settings(cwd, default_project_settings())
|
|
279
316
|
_typer.echo(f"Created project settings: {settings_file}")
|
|
@@ -286,6 +323,7 @@ def init(
|
|
|
286
323
|
|
|
287
324
|
|
|
288
325
|
@app.command()
|
|
326
|
+
@_catch_daemon_start_error
|
|
289
327
|
def index() -> None:
|
|
290
328
|
"""Create/update index for the codebase."""
|
|
291
329
|
from . import client as _client
|
|
@@ -297,6 +335,7 @@ def index() -> None:
|
|
|
297
335
|
|
|
298
336
|
|
|
299
337
|
@app.command()
|
|
338
|
+
@_catch_daemon_start_error
|
|
300
339
|
def search(
|
|
301
340
|
query: list[str] = _typer.Argument(..., help="Search query"),
|
|
302
341
|
lang: list[str] = _typer.Option([], "--lang", help="Filter by language"),
|
|
@@ -333,6 +372,7 @@ def search(
|
|
|
333
372
|
|
|
334
373
|
|
|
335
374
|
@app.command()
|
|
375
|
+
@_catch_daemon_start_error
|
|
336
376
|
def status() -> None:
|
|
337
377
|
"""Show project status."""
|
|
338
378
|
from . import client as _client
|
|
@@ -350,10 +390,11 @@ def reset(
|
|
|
350
390
|
"""Reset project databases and optionally remove settings."""
|
|
351
391
|
project_root = require_project_root()
|
|
352
392
|
cocoindex_dir = project_root / ".cocoindex_code"
|
|
393
|
+
db_dir = resolve_db_dir(project_root)
|
|
353
394
|
|
|
354
395
|
db_files = [
|
|
355
|
-
|
|
356
|
-
|
|
396
|
+
db_dir / "cocoindex.db",
|
|
397
|
+
db_dir / "target_sqlite.db",
|
|
357
398
|
]
|
|
358
399
|
settings_file = cocoindex_dir / "settings.yml"
|
|
359
400
|
|
|
@@ -397,6 +438,12 @@ def reset(
|
|
|
397
438
|
f.unlink(missing_ok=True)
|
|
398
439
|
|
|
399
440
|
if all_:
|
|
441
|
+
# Remove db_dir if empty and different from cocoindex_dir
|
|
442
|
+
if db_dir != cocoindex_dir:
|
|
443
|
+
try:
|
|
444
|
+
db_dir.rmdir()
|
|
445
|
+
except OSError:
|
|
446
|
+
pass # Not empty or doesn't exist
|
|
400
447
|
# Remove .cocoindex_code/ if empty
|
|
401
448
|
try:
|
|
402
449
|
cocoindex_dir.rmdir()
|
|
@@ -446,6 +493,7 @@ def _print_doctor_result(result: DoctorCheckResult) -> None:
|
|
|
446
493
|
|
|
447
494
|
|
|
448
495
|
@app.command()
|
|
496
|
+
@_catch_daemon_start_error
|
|
449
497
|
def doctor() -> None:
|
|
450
498
|
"""Check system health and report issues."""
|
|
451
499
|
from . import client as _client
|
|
@@ -499,6 +547,10 @@ def doctor() -> None:
|
|
|
499
547
|
other_keys = [k for k in env_resp.env_names if k not in settings_keys]
|
|
500
548
|
if other_keys:
|
|
501
549
|
_typer.echo(f" Other env vars in daemon: {', '.join(sorted(other_keys))}")
|
|
550
|
+
if env_resp.db_path_mappings:
|
|
551
|
+
_typer.echo(" DB path mappings:")
|
|
552
|
+
for m in env_resp.db_path_mappings:
|
|
553
|
+
_typer.echo(f" {m.source} \u2192 {m.target}")
|
|
502
554
|
except Exception as e:
|
|
503
555
|
_print_error(f"Failed to get daemon env: {e}")
|
|
504
556
|
|
|
@@ -553,6 +605,7 @@ def doctor() -> None:
|
|
|
553
605
|
|
|
554
606
|
|
|
555
607
|
@app.command()
|
|
608
|
+
@_catch_daemon_start_error
|
|
556
609
|
def mcp() -> None:
|
|
557
610
|
"""Run as MCP server (stdio mode)."""
|
|
558
611
|
import asyncio
|
|
@@ -586,6 +639,7 @@ async def _bg_index(project_root: str) -> None:
|
|
|
586
639
|
|
|
587
640
|
|
|
588
641
|
@daemon_app.command("status")
|
|
642
|
+
@_catch_daemon_start_error
|
|
589
643
|
def daemon_status() -> None:
|
|
590
644
|
"""Show daemon status."""
|
|
591
645
|
from . import client as _client
|
|
@@ -603,6 +657,7 @@ def daemon_status() -> None:
|
|
|
603
657
|
|
|
604
658
|
|
|
605
659
|
@daemon_app.command("restart")
|
|
660
|
+
@_catch_daemon_start_error
|
|
606
661
|
def daemon_restart() -> None:
|
|
607
662
|
"""Restart the daemon."""
|
|
608
663
|
from .client import _wait_for_daemon, start_daemon, stop_daemon
|
|
@@ -611,13 +666,9 @@ def daemon_restart() -> None:
|
|
|
611
666
|
stop_daemon()
|
|
612
667
|
|
|
613
668
|
_typer.echo("Starting daemon...")
|
|
614
|
-
start_daemon()
|
|
615
|
-
|
|
616
|
-
|
|
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)
|
|
669
|
+
proc = start_daemon()
|
|
670
|
+
_wait_for_daemon(proc=proc)
|
|
671
|
+
_typer.echo("Daemon restarted.")
|
|
621
672
|
|
|
622
673
|
|
|
623
674
|
@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() ->
|
|
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(
|
|
473
|
-
|
|
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
|
-
|
|
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:
|
|
@@ -7,6 +7,8 @@ import os
|
|
|
7
7
|
from dataclasses import dataclass
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
|
|
10
|
+
from .settings import resolve_db_dir
|
|
11
|
+
|
|
10
12
|
_DEFAULT_MODEL = "sbert/sentence-transformers/all-MiniLM-L6-v2"
|
|
11
13
|
|
|
12
14
|
|
|
@@ -96,8 +98,8 @@ class Config:
|
|
|
96
98
|
_DEFAULT_MODEL,
|
|
97
99
|
)
|
|
98
100
|
|
|
99
|
-
# Index directory
|
|
100
|
-
index_dir = root
|
|
101
|
+
# Index directory: apply DB path mapping if configured
|
|
102
|
+
index_dir = resolve_db_dir(root)
|
|
101
103
|
|
|
102
104
|
# Device: auto-detect CUDA or use env override
|
|
103
105
|
device = os.environ.get("COCOINDEX_CODE_DEVICE")
|
|
@@ -48,6 +48,7 @@ from .protocol import (
|
|
|
48
48
|
from .settings import (
|
|
49
49
|
global_settings_mtime_us,
|
|
50
50
|
load_user_settings,
|
|
51
|
+
resolve_db_dir,
|
|
51
52
|
user_settings_dir,
|
|
52
53
|
)
|
|
53
54
|
from .shared import Embedder, create_embedder
|
|
@@ -345,7 +346,7 @@ async def _check_index_status(project_root_str: str) -> DoctorCheckResult:
|
|
|
345
346
|
from cocoindex.connectors import sqlite as coco_sqlite
|
|
346
347
|
|
|
347
348
|
project_root = Path(project_root_str)
|
|
348
|
-
db_path = project_root / "
|
|
349
|
+
db_path = resolve_db_dir(project_root) / "target_sqlite.db"
|
|
349
350
|
details = [f"Index: {db_path}"]
|
|
350
351
|
|
|
351
352
|
if not db_path.exists():
|
|
@@ -441,9 +442,16 @@ async def _dispatch(
|
|
|
441
442
|
return StopResponse(ok=True)
|
|
442
443
|
|
|
443
444
|
if isinstance(req, DaemonEnvRequest):
|
|
445
|
+
from .protocol import DbPathMappingEntry
|
|
446
|
+
from .settings import get_db_path_mappings
|
|
447
|
+
|
|
444
448
|
return DaemonEnvResponse(
|
|
445
449
|
env_names=sorted(os.environ.keys()),
|
|
446
450
|
settings_env_names=settings_env_names,
|
|
451
|
+
db_path_mappings=[
|
|
452
|
+
DbPathMappingEntry(source=str(m.source), target=str(m.target))
|
|
453
|
+
for m in get_db_path_mappings()
|
|
454
|
+
],
|
|
447
455
|
)
|
|
448
456
|
|
|
449
457
|
if isinstance(req, DoctorRequest):
|
|
@@ -21,6 +21,7 @@ from .protocol import (
|
|
|
21
21
|
SearchResult,
|
|
22
22
|
)
|
|
23
23
|
from .query import query_codebase
|
|
24
|
+
from .settings import resolve_db_dir
|
|
24
25
|
from .shared import (
|
|
25
26
|
CODEBASE_DIR,
|
|
26
27
|
EMBEDDER,
|
|
@@ -170,7 +171,7 @@ class Project:
|
|
|
170
171
|
offset: int = 0,
|
|
171
172
|
) -> list[SearchResult]:
|
|
172
173
|
"""Search within this project."""
|
|
173
|
-
target_db = self._project_root / "
|
|
174
|
+
target_db = resolve_db_dir(self._project_root) / "target_sqlite.db"
|
|
174
175
|
results = await query_codebase(
|
|
175
176
|
query=query,
|
|
176
177
|
target_sqlite_db_path=target_db,
|
|
@@ -254,11 +255,14 @@ class Project:
|
|
|
254
255
|
indexer loads them fresh from disk on every run so that user edits
|
|
255
256
|
take effect without restarting the daemon.
|
|
256
257
|
"""
|
|
257
|
-
|
|
258
|
-
|
|
258
|
+
settings_dir = project_root / ".cocoindex_code"
|
|
259
|
+
settings_dir.mkdir(parents=True, exist_ok=True)
|
|
259
260
|
|
|
260
|
-
|
|
261
|
-
|
|
261
|
+
db_dir = resolve_db_dir(project_root)
|
|
262
|
+
db_dir.mkdir(parents=True, exist_ok=True)
|
|
263
|
+
|
|
264
|
+
cocoindex_db_path = db_dir / "cocoindex.db"
|
|
265
|
+
target_sqlite_db_path = db_dir / "target_sqlite.db"
|
|
262
266
|
|
|
263
267
|
settings = coco.Settings.from_env(cocoindex_db_path)
|
|
264
268
|
|
|
@@ -158,9 +158,15 @@ class DoctorResponse(_msgspec.Struct, tag="doctor"):
|
|
|
158
158
|
final: bool = False
|
|
159
159
|
|
|
160
160
|
|
|
161
|
+
class DbPathMappingEntry(_msgspec.Struct):
|
|
162
|
+
source: str
|
|
163
|
+
target: str
|
|
164
|
+
|
|
165
|
+
|
|
161
166
|
class DaemonEnvResponse(_msgspec.Struct, tag="daemon_env"):
|
|
162
167
|
env_names: list[str]
|
|
163
168
|
settings_env_names: list[str]
|
|
169
|
+
db_path_mappings: list[DbPathMappingEntry] = []
|
|
164
170
|
|
|
165
171
|
|
|
166
172
|
class ErrorResponse(_msgspec.Struct, tag="error"):
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import os
|
|
5
6
|
from dataclasses import dataclass, field
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
from typing import Any
|
|
@@ -115,14 +116,89 @@ _SETTINGS_DIR_NAME = ".cocoindex_code"
|
|
|
115
116
|
_SETTINGS_FILE_NAME = "settings.yml" # project-level
|
|
116
117
|
_USER_SETTINGS_FILE_NAME = "global_settings.yml" # user-level
|
|
117
118
|
|
|
119
|
+
_ENV_DB_PATH_MAPPING = "COCOINDEX_CODE_DB_PATH_MAPPING"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclass
|
|
123
|
+
class DbPathMapping:
|
|
124
|
+
source: Path
|
|
125
|
+
target: Path
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
_db_path_mapping: list[DbPathMapping] | None = None
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _parse_db_path_mapping() -> list[DbPathMapping]:
|
|
132
|
+
"""Parse ``COCOINDEX_CODE_DB_PATH_MAPPING`` env var.
|
|
133
|
+
|
|
134
|
+
Format: ``/src1=/dst1,/src2=/dst2``
|
|
135
|
+
Both source and target must be absolute paths.
|
|
136
|
+
"""
|
|
137
|
+
raw = os.environ.get(_ENV_DB_PATH_MAPPING, "")
|
|
138
|
+
if not raw.strip():
|
|
139
|
+
return []
|
|
140
|
+
|
|
141
|
+
mappings: list[DbPathMapping] = []
|
|
142
|
+
for entry in raw.split(","):
|
|
143
|
+
entry = entry.strip()
|
|
144
|
+
if not entry:
|
|
145
|
+
continue
|
|
146
|
+
parts = entry.split("=", 1)
|
|
147
|
+
if len(parts) != 2 or not parts[0] or not parts[1]:
|
|
148
|
+
raise ValueError(
|
|
149
|
+
f"{_ENV_DB_PATH_MAPPING}: invalid entry {entry!r}, expected format 'source=target'"
|
|
150
|
+
)
|
|
151
|
+
source = Path(parts[0])
|
|
152
|
+
target = Path(parts[1])
|
|
153
|
+
if not source.is_absolute():
|
|
154
|
+
raise ValueError(
|
|
155
|
+
f"{_ENV_DB_PATH_MAPPING}: source path must be absolute, got {source!r}"
|
|
156
|
+
)
|
|
157
|
+
if not target.is_absolute():
|
|
158
|
+
raise ValueError(
|
|
159
|
+
f"{_ENV_DB_PATH_MAPPING}: target path must be absolute, got {target!r}"
|
|
160
|
+
)
|
|
161
|
+
mappings.append(DbPathMapping(source=source.resolve(), target=target.resolve()))
|
|
162
|
+
return mappings
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def resolve_db_dir(project_root: Path) -> Path:
|
|
166
|
+
"""Return the directory for database files given a project root.
|
|
167
|
+
|
|
168
|
+
Applies ``COCOINDEX_CODE_DB_PATH_MAPPING`` if set, otherwise falls back
|
|
169
|
+
to ``project_root / ".cocoindex_code"``.
|
|
170
|
+
"""
|
|
171
|
+
global _db_path_mapping # noqa: PLW0603
|
|
172
|
+
if _db_path_mapping is None:
|
|
173
|
+
_db_path_mapping = _parse_db_path_mapping()
|
|
174
|
+
|
|
175
|
+
resolved = project_root.resolve()
|
|
176
|
+
for mapping in _db_path_mapping:
|
|
177
|
+
if resolved == mapping.source or resolved.is_relative_to(mapping.source):
|
|
178
|
+
rel = resolved.relative_to(mapping.source)
|
|
179
|
+
return mapping.target / rel
|
|
180
|
+
return project_root / _SETTINGS_DIR_NAME
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def get_db_path_mappings() -> list[DbPathMapping]:
|
|
184
|
+
"""Return the parsed DB path mappings from ``COCOINDEX_CODE_DB_PATH_MAPPING``."""
|
|
185
|
+
global _db_path_mapping # noqa: PLW0603
|
|
186
|
+
if _db_path_mapping is None:
|
|
187
|
+
_db_path_mapping = _parse_db_path_mapping()
|
|
188
|
+
return list(_db_path_mapping)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _reset_db_path_mapping_cache() -> None:
|
|
192
|
+
"""Reset the cached mapping (for tests)."""
|
|
193
|
+
global _db_path_mapping # noqa: PLW0603
|
|
194
|
+
_db_path_mapping = None
|
|
195
|
+
|
|
118
196
|
|
|
119
197
|
def user_settings_dir() -> Path:
|
|
120
198
|
"""Return ``~/.cocoindex_code/``.
|
|
121
199
|
|
|
122
200
|
Respects ``COCOINDEX_CODE_DIR`` env var for overriding the base directory.
|
|
123
201
|
"""
|
|
124
|
-
import os
|
|
125
|
-
|
|
126
202
|
override = os.environ.get("COCOINDEX_CODE_DIR")
|
|
127
203
|
if override:
|
|
128
204
|
return Path(override)
|
|
@@ -240,7 +316,7 @@ def _user_settings_to_dict(settings: UserSettings) -> dict[str, Any]:
|
|
|
240
316
|
def _user_settings_from_dict(d: dict[str, Any]) -> UserSettings:
|
|
241
317
|
emb_dict = d.get("embedding")
|
|
242
318
|
if not emb_dict or "model" not in emb_dict:
|
|
243
|
-
raise ValueError("
|
|
319
|
+
raise ValueError("Must contain 'embedding' with at least 'model' field")
|
|
244
320
|
# Only pass keys that are present; provider uses dataclass default ("litellm") if omitted
|
|
245
321
|
emb_kwargs: dict[str, Any] = {"model": emb_dict["model"]}
|
|
246
322
|
if "provider" in emb_dict:
|
|
@@ -288,11 +364,14 @@ def load_user_settings() -> UserSettings:
|
|
|
288
364
|
path = user_settings_path()
|
|
289
365
|
if not path.is_file():
|
|
290
366
|
raise FileNotFoundError(f"User settings not found: {path}")
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
367
|
+
try:
|
|
368
|
+
with open(path) as f:
|
|
369
|
+
data = _yaml.safe_load(f)
|
|
370
|
+
if not data:
|
|
371
|
+
raise ValueError("File is empty")
|
|
372
|
+
return _user_settings_from_dict(data)
|
|
373
|
+
except Exception as e:
|
|
374
|
+
raise type(e)(f"Error loading {path}: {e}") from e
|
|
296
375
|
|
|
297
376
|
|
|
298
377
|
def save_user_settings(settings: UserSettings) -> Path:
|
|
@@ -312,11 +391,14 @@ def load_project_settings(project_root: Path) -> ProjectSettings:
|
|
|
312
391
|
path = project_settings_path(project_root)
|
|
313
392
|
if not path.is_file():
|
|
314
393
|
raise FileNotFoundError(f"Project settings not found: {path}")
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
394
|
+
try:
|
|
395
|
+
with open(path) as f:
|
|
396
|
+
data = _yaml.safe_load(f)
|
|
397
|
+
if not data:
|
|
398
|
+
return default_project_settings()
|
|
399
|
+
return _project_settings_from_dict(data)
|
|
400
|
+
except Exception as e:
|
|
401
|
+
raise type(e)(f"Error loading {path}: {e}") from e
|
|
320
402
|
|
|
321
403
|
|
|
322
404
|
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
|