cocoindex-code 0.2.33__tar.gz → 0.2.35__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.33 → cocoindex_code-0.2.35}/.gitignore +3 -0
- {cocoindex_code-0.2.33 → cocoindex_code-0.2.35}/PKG-INFO +35 -6
- {cocoindex_code-0.2.33 → cocoindex_code-0.2.35}/README.md +30 -1
- {cocoindex_code-0.2.33 → cocoindex_code-0.2.35}/pyproject.toml +5 -5
- {cocoindex_code-0.2.33 → cocoindex_code-0.2.35}/src/cocoindex_code/__init__.py +6 -0
- {cocoindex_code-0.2.33 → cocoindex_code-0.2.35}/src/cocoindex_code/_version.py +2 -2
- {cocoindex_code-0.2.33 → cocoindex_code-0.2.35}/src/cocoindex_code/cli.py +141 -45
- {cocoindex_code-0.2.33 → cocoindex_code-0.2.35}/src/cocoindex_code/client.py +32 -15
- {cocoindex_code-0.2.33 → cocoindex_code-0.2.35}/src/cocoindex_code/daemon.py +7 -1
- {cocoindex_code-0.2.33 → cocoindex_code-0.2.35}/src/cocoindex_code/protocol.py +6 -0
- {cocoindex_code-0.2.33 → cocoindex_code-0.2.35}/src/cocoindex_code/shared.py +6 -2
- {cocoindex_code-0.2.33 → cocoindex_code-0.2.35}/LICENSE +0 -0
- {cocoindex_code-0.2.33 → cocoindex_code-0.2.35}/src/cocoindex_code/__main__.py +0 -0
- {cocoindex_code-0.2.33 → cocoindex_code-0.2.35}/src/cocoindex_code/_daemon_paths.py +0 -0
- {cocoindex_code-0.2.33 → cocoindex_code-0.2.35}/src/cocoindex_code/chunking.py +0 -0
- {cocoindex_code-0.2.33 → cocoindex_code-0.2.35}/src/cocoindex_code/embedder_defaults.py +0 -0
- {cocoindex_code-0.2.33 → cocoindex_code-0.2.35}/src/cocoindex_code/embedder_params.py +0 -0
- {cocoindex_code-0.2.33 → cocoindex_code-0.2.35}/src/cocoindex_code/indexer.py +0 -0
- {cocoindex_code-0.2.33 → cocoindex_code-0.2.35}/src/cocoindex_code/litellm_embedder.py +0 -0
- {cocoindex_code-0.2.33 → cocoindex_code-0.2.35}/src/cocoindex_code/project.py +0 -0
- {cocoindex_code-0.2.33 → cocoindex_code-0.2.35}/src/cocoindex_code/query.py +0 -0
- {cocoindex_code-0.2.33 → cocoindex_code-0.2.35}/src/cocoindex_code/schema.py +0 -0
- {cocoindex_code-0.2.33 → cocoindex_code-0.2.35}/src/cocoindex_code/server.py +0 -0
- {cocoindex_code-0.2.33 → cocoindex_code-0.2.35}/src/cocoindex_code/settings.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.35
|
|
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.1.0,>=1.0.
|
|
20
|
+
Requires-Dist: cocoindex[litellm]<1.1.0,>=1.0.6
|
|
21
21
|
Requires-Dist: einops>=0.8.2
|
|
22
22
|
Requires-Dist: mcp>=1.0.0
|
|
23
23
|
Requires-Dist: msgspec>=0.19.0
|
|
@@ -29,7 +29,7 @@ Requires-Dist: questionary>=2.0.0
|
|
|
29
29
|
Requires-Dist: sqlite-vec>=0.1.0
|
|
30
30
|
Requires-Dist: typer>=0.9.0
|
|
31
31
|
Provides-Extra: dev
|
|
32
|
-
Requires-Dist: cocoindex[sentence-transformers]<1.1.0,>=1.0.
|
|
32
|
+
Requires-Dist: cocoindex[sentence-transformers]<1.1.0,>=1.0.6; extra == 'dev'
|
|
33
33
|
Requires-Dist: mypy>=1.0.0; extra == 'dev'
|
|
34
34
|
Requires-Dist: prek>=0.1.0; extra == 'dev'
|
|
35
35
|
Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
|
|
@@ -37,9 +37,9 @@ Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
|
|
|
37
37
|
Requires-Dist: pytest>=7.0.0; extra == 'dev'
|
|
38
38
|
Requires-Dist: ruff>=0.1.0; extra == 'dev'
|
|
39
39
|
Provides-Extra: embeddings-local
|
|
40
|
-
Requires-Dist: cocoindex[sentence-transformers]<1.1.0,>=1.0.
|
|
40
|
+
Requires-Dist: cocoindex[sentence-transformers]<1.1.0,>=1.0.6; extra == 'embeddings-local'
|
|
41
41
|
Provides-Extra: full
|
|
42
|
-
Requires-Dist: cocoindex[sentence-transformers]<1.1.0,>=1.0.
|
|
42
|
+
Requires-Dist: cocoindex[sentence-transformers]<1.1.0,>=1.0.6; extra == 'full'
|
|
43
43
|
Description-Content-Type: text/markdown
|
|
44
44
|
|
|
45
45
|
<p align="center">
|
|
@@ -568,6 +568,23 @@ envs:
|
|
|
568
568
|
|
|
569
569
|
</details>
|
|
570
570
|
|
|
571
|
+
<details>
|
|
572
|
+
<summary>OpenAI-compatible (custom endpoint)</summary>
|
|
573
|
+
|
|
574
|
+
Many providers (vLLM, LM Studio, LocalAI, Together, Fireworks, DeepInfra, …) expose an OpenAI-compatible embedding API. Use the `openai/` prefix and point `OPENAI_BASE_URL` at your endpoint:
|
|
575
|
+
|
|
576
|
+
```yaml
|
|
577
|
+
embedding:
|
|
578
|
+
model: openai/your-model-name
|
|
579
|
+
envs:
|
|
580
|
+
OPENAI_BASE_URL: https://your-endpoint/v1
|
|
581
|
+
OPENAI_API_KEY: your-api-key
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
Don't append `/embeddings` to the base URL — LiteLLM handles that.
|
|
585
|
+
|
|
586
|
+
</details>
|
|
587
|
+
|
|
571
588
|
<details>
|
|
572
589
|
<summary>Azure OpenAI</summary>
|
|
573
590
|
|
|
@@ -656,7 +673,7 @@ envs:
|
|
|
656
673
|
|
|
657
674
|
</details>
|
|
658
675
|
|
|
659
|
-
Any [LiteLLM-supported model](https://docs.litellm.ai/docs/embedding/supported_embedding) works. When using a LiteLLM model, set `provider: litellm` (or omit `provider` — LiteLLM is the default for non-`sentence-transformers` models).
|
|
676
|
+
Any [LiteLLM-supported model](https://docs.litellm.ai/docs/embedding/supported_embedding) works. When using a LiteLLM model, set `provider: litellm` (or omit `provider` — LiteLLM is the default for non-`sentence-transformers` models). For the full list of env vars each provider reads (API keys, base URLs, regions, …), see LiteLLM's [Setting API Keys](https://docs.litellm.ai/docs/set_keys).
|
|
660
677
|
|
|
661
678
|
### Local SentenceTransformers Models
|
|
662
679
|
|
|
@@ -776,6 +793,18 @@ If you previously configured `cocoindex-code` via environment variables, the `co
|
|
|
776
793
|
| `COCOINDEX_CODE_EXCLUDED_PATTERNS` | `exclude_patterns` in project `settings.yml` |
|
|
777
794
|
| `COCOINDEX_CODE_EXTRA_EXTENSIONS` | `include_patterns` + `language_overrides` in project `settings.yml` |
|
|
778
795
|
|
|
796
|
+
## Telemetry
|
|
797
|
+
|
|
798
|
+
`cocoindex-code` sends anonymous usage telemetry through CocoIndex so we can see how the tool is used in aggregate and prioritize improvements. The events identify themselves as `application: cocoindex-code`.
|
|
799
|
+
|
|
800
|
+
We **do not** collect your source code, file paths, queries, search results, embeddings, settings, or any other content from your codebase or environment.
|
|
801
|
+
|
|
802
|
+
To opt out, set:
|
|
803
|
+
|
|
804
|
+
```bash
|
|
805
|
+
export COCOINDEX_DISABLE_USAGE_TRACKING=1
|
|
806
|
+
```
|
|
807
|
+
|
|
779
808
|
## Large codebase / Enterprise
|
|
780
809
|
[CocoIndex](https://github.com/cocoindex-io/cocoindex) is an ultra efficient indexing engine that also works on large codebases at scale for enterprises. In enterprise scenarios it is a lot more efficient to share indexes with teammates when there are large or many repos. We also have advanced features like branch dedupe etc designed for enterprise users.
|
|
781
810
|
|
|
@@ -524,6 +524,23 @@ envs:
|
|
|
524
524
|
|
|
525
525
|
</details>
|
|
526
526
|
|
|
527
|
+
<details>
|
|
528
|
+
<summary>OpenAI-compatible (custom endpoint)</summary>
|
|
529
|
+
|
|
530
|
+
Many providers (vLLM, LM Studio, LocalAI, Together, Fireworks, DeepInfra, …) expose an OpenAI-compatible embedding API. Use the `openai/` prefix and point `OPENAI_BASE_URL` at your endpoint:
|
|
531
|
+
|
|
532
|
+
```yaml
|
|
533
|
+
embedding:
|
|
534
|
+
model: openai/your-model-name
|
|
535
|
+
envs:
|
|
536
|
+
OPENAI_BASE_URL: https://your-endpoint/v1
|
|
537
|
+
OPENAI_API_KEY: your-api-key
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
Don't append `/embeddings` to the base URL — LiteLLM handles that.
|
|
541
|
+
|
|
542
|
+
</details>
|
|
543
|
+
|
|
527
544
|
<details>
|
|
528
545
|
<summary>Azure OpenAI</summary>
|
|
529
546
|
|
|
@@ -612,7 +629,7 @@ envs:
|
|
|
612
629
|
|
|
613
630
|
</details>
|
|
614
631
|
|
|
615
|
-
Any [LiteLLM-supported model](https://docs.litellm.ai/docs/embedding/supported_embedding) works. When using a LiteLLM model, set `provider: litellm` (or omit `provider` — LiteLLM is the default for non-`sentence-transformers` models).
|
|
632
|
+
Any [LiteLLM-supported model](https://docs.litellm.ai/docs/embedding/supported_embedding) works. When using a LiteLLM model, set `provider: litellm` (or omit `provider` — LiteLLM is the default for non-`sentence-transformers` models). For the full list of env vars each provider reads (API keys, base URLs, regions, …), see LiteLLM's [Setting API Keys](https://docs.litellm.ai/docs/set_keys).
|
|
616
633
|
|
|
617
634
|
### Local SentenceTransformers Models
|
|
618
635
|
|
|
@@ -732,6 +749,18 @@ If you previously configured `cocoindex-code` via environment variables, the `co
|
|
|
732
749
|
| `COCOINDEX_CODE_EXCLUDED_PATTERNS` | `exclude_patterns` in project `settings.yml` |
|
|
733
750
|
| `COCOINDEX_CODE_EXTRA_EXTENSIONS` | `include_patterns` + `language_overrides` in project `settings.yml` |
|
|
734
751
|
|
|
752
|
+
## Telemetry
|
|
753
|
+
|
|
754
|
+
`cocoindex-code` sends anonymous usage telemetry through CocoIndex so we can see how the tool is used in aggregate and prioritize improvements. The events identify themselves as `application: cocoindex-code`.
|
|
755
|
+
|
|
756
|
+
We **do not** collect your source code, file paths, queries, search results, embeddings, settings, or any other content from your codebase or environment.
|
|
757
|
+
|
|
758
|
+
To opt out, set:
|
|
759
|
+
|
|
760
|
+
```bash
|
|
761
|
+
export COCOINDEX_DISABLE_USAGE_TRACKING=1
|
|
762
|
+
```
|
|
763
|
+
|
|
735
764
|
## Large codebase / Enterprise
|
|
736
765
|
[CocoIndex](https://github.com/cocoindex-io/cocoindex) is an ultra efficient indexing engine that also works on large codebases at scale for enterprises. In enterprise scenarios it is a lot more efficient to share indexes with teammates when there are large or many repos. We also have advanced features like branch dedupe etc designed for enterprise users.
|
|
737
766
|
|
|
@@ -23,7 +23,7 @@ classifiers = [
|
|
|
23
23
|
|
|
24
24
|
dependencies = [
|
|
25
25
|
"mcp>=1.0.0",
|
|
26
|
-
"cocoindex[litellm]>=1.0.
|
|
26
|
+
"cocoindex[litellm]>=1.0.6,<1.1.0",
|
|
27
27
|
"sqlite-vec>=0.1.0",
|
|
28
28
|
"pydantic>=2.0.0",
|
|
29
29
|
"numpy>=1.24.0",
|
|
@@ -39,7 +39,7 @@ dependencies = [
|
|
|
39
39
|
# `embeddings-local` is the primary feature extra: it pulls in
|
|
40
40
|
# `sentence-transformers` (via cocoindex) so local embeddings work without
|
|
41
41
|
# an API key.
|
|
42
|
-
embeddings-local = ["cocoindex[sentence-transformers]>=1.0.
|
|
42
|
+
embeddings-local = ["cocoindex[sentence-transformers]>=1.0.6,<1.1.0"]
|
|
43
43
|
# `full` is the umbrella "batteries-included" alias. Today it's just
|
|
44
44
|
# `embeddings-local`, but we expect to bundle more optional niceties under
|
|
45
45
|
# it over time — users who want everything can keep using `[full]` and pick
|
|
@@ -47,7 +47,7 @@ embeddings-local = ["cocoindex[sentence-transformers]>=1.0.0,<1.1.0"]
|
|
|
47
47
|
# `:full` image variant for consistency across install paths. Contents are
|
|
48
48
|
# inlined rather than self-referencing `cocoindex-code[embeddings-local]`
|
|
49
49
|
# to avoid resolver edge cases with older pip.
|
|
50
|
-
full = ["cocoindex[sentence-transformers]>=1.0.
|
|
50
|
+
full = ["cocoindex[sentence-transformers]>=1.0.6,<1.1.0"]
|
|
51
51
|
dev = [
|
|
52
52
|
"pytest>=7.0.0",
|
|
53
53
|
"pytest-asyncio>=0.21.0",
|
|
@@ -55,7 +55,7 @@ dev = [
|
|
|
55
55
|
"ruff>=0.1.0",
|
|
56
56
|
"mypy>=1.0.0",
|
|
57
57
|
"prek>=0.1.0",
|
|
58
|
-
"cocoindex[sentence-transformers]>=1.0.
|
|
58
|
+
"cocoindex[sentence-transformers]>=1.0.6,<1.1.0",
|
|
59
59
|
]
|
|
60
60
|
|
|
61
61
|
[project.scripts]
|
|
@@ -89,7 +89,7 @@ dev = [
|
|
|
89
89
|
"mypy>=1.0.0",
|
|
90
90
|
"prek>=0.1.0",
|
|
91
91
|
"types-pyyaml>=6.0.12.20250915",
|
|
92
|
-
"cocoindex[sentence-transformers]>=1.0.
|
|
92
|
+
"cocoindex[sentence-transformers]>=1.0.7,<1.1.0",
|
|
93
93
|
]
|
|
94
94
|
|
|
95
95
|
[tool.ruff]
|
|
@@ -3,8 +3,14 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import logging
|
|
6
|
+
import os
|
|
6
7
|
from typing import TYPE_CHECKING, Any
|
|
7
8
|
|
|
9
|
+
# Identify this application in cocoindex's telemetry payloads. Must be set
|
|
10
|
+
# before any `import cocoindex` runs (the value is read once at telemetry
|
|
11
|
+
# init time). See cocoindex-io/cocoindex#1992.
|
|
12
|
+
os.environ.setdefault("COCOINDEX_APPLICATION_FOR_TRACKING", "cocoindex-code")
|
|
13
|
+
|
|
8
14
|
logging.basicConfig(level=logging.WARNING)
|
|
9
15
|
|
|
10
16
|
from ._version import __version__ # noqa: E402
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.2.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 2,
|
|
21
|
+
__version__ = version = '0.2.35'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 35)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -321,12 +321,35 @@ def remove_from_gitignore(project_root: Path) -> None:
|
|
|
321
321
|
_LITELLM_MODELS_URL = "https://docs.litellm.ai/docs/embedding/supported_embedding"
|
|
322
322
|
|
|
323
323
|
|
|
324
|
+
def _st_model_rejection_reason(model: str) -> str | None:
|
|
325
|
+
"""Why ``model`` can't be a sentence-transformers model, or None if it's fine.
|
|
326
|
+
|
|
327
|
+
sentence-transformers loads HuggingFace model ids. An ``ollama/`` prefix is a
|
|
328
|
+
LiteLLM/Ollama route that ST tries (and fails) to resolve as a HuggingFace
|
|
329
|
+
repo — the user wants the litellm provider instead (issue #181). Real
|
|
330
|
+
HuggingFace ids that contain an ``org/`` slash (``Snowflake/...``,
|
|
331
|
+
``openai/...``) are left alone.
|
|
332
|
+
"""
|
|
333
|
+
if model.strip().lower().startswith("ollama/"):
|
|
334
|
+
return (
|
|
335
|
+
"ollama/… models run via litellm, not sentence-transformers — "
|
|
336
|
+
"go back and pick the litellm provider instead."
|
|
337
|
+
)
|
|
338
|
+
return None
|
|
339
|
+
|
|
340
|
+
|
|
324
341
|
def _resolve_embedding_choice(
|
|
325
342
|
litellm_model_flag: str | None,
|
|
326
343
|
st_installed: bool,
|
|
327
344
|
tty: bool,
|
|
345
|
+
previous: EmbeddingSettings | None = None,
|
|
328
346
|
) -> EmbeddingSettings:
|
|
329
|
-
"""Resolve the embedding settings per the init control-flow diagram.
|
|
347
|
+
"""Resolve the embedding settings per the init control-flow diagram.
|
|
348
|
+
|
|
349
|
+
On a retry, ``previous`` holds the choice from the last attempt; its
|
|
350
|
+
provider and model become the prompt defaults so the user only edits
|
|
351
|
+
what was wrong instead of retyping everything.
|
|
352
|
+
"""
|
|
330
353
|
if litellm_model_flag is not None:
|
|
331
354
|
return EmbeddingSettings(provider="litellm", model=litellm_model_flag)
|
|
332
355
|
|
|
@@ -349,14 +372,15 @@ def _resolve_embedding_choice(
|
|
|
349
372
|
"Embedding provider",
|
|
350
373
|
choices=[
|
|
351
374
|
questionary.Choice(
|
|
352
|
-
title="sentence-transformers (local, free)",
|
|
375
|
+
title="sentence-transformers (local, free — built-in HuggingFace models)",
|
|
353
376
|
value="sentence-transformers",
|
|
354
377
|
),
|
|
355
378
|
questionary.Choice(
|
|
356
|
-
title="litellm (
|
|
379
|
+
title="litellm (100+ providers — cloud APIs & local Ollama)",
|
|
357
380
|
value="litellm",
|
|
358
381
|
),
|
|
359
382
|
],
|
|
383
|
+
default=previous.provider if previous is not None else None,
|
|
360
384
|
).ask()
|
|
361
385
|
else:
|
|
362
386
|
_typer.echo(
|
|
@@ -369,10 +393,16 @@ def _resolve_embedding_choice(
|
|
|
369
393
|
raise _typer.Exit(code=1)
|
|
370
394
|
|
|
371
395
|
if provider == "sentence-transformers":
|
|
372
|
-
|
|
396
|
+
default_model = previous.model if previous is not None else DEFAULT_ST_MODEL
|
|
397
|
+
model = questionary.text(
|
|
398
|
+
"Model name",
|
|
399
|
+
default=default_model,
|
|
400
|
+
validate=lambda m: _st_model_rejection_reason(m) or True,
|
|
401
|
+
).ask()
|
|
373
402
|
elif provider == "litellm":
|
|
374
403
|
_typer.echo(f"See supported LiteLLM embedding models: {_LITELLM_MODELS_URL}")
|
|
375
|
-
|
|
404
|
+
default_model = previous.model if previous is not None else ""
|
|
405
|
+
model = questionary.text("Model name", default=default_model).ask()
|
|
376
406
|
else:
|
|
377
407
|
_typer.echo(f"Error: unknown provider {provider!r}", err=True)
|
|
378
408
|
raise _typer.Exit(code=1)
|
|
@@ -392,13 +422,14 @@ def _ok_fail_tag(ok: bool) -> str:
|
|
|
392
422
|
return _click.style("[FAIL]", fg="red", bold=True)
|
|
393
423
|
|
|
394
424
|
|
|
395
|
-
def _run_init_model_check(
|
|
396
|
-
"""Ask the daemon to test the embedding model; print results
|
|
425
|
+
def _run_init_model_check() -> bool:
|
|
426
|
+
"""Ask the daemon to test the embedding model; print results. Return True if all pass.
|
|
397
427
|
|
|
398
428
|
Drives the check via `DoctorRequest(project_root=None)`. The daemon loads
|
|
399
429
|
the model once and stays running, so the user's next `ccc index` starts
|
|
400
430
|
warm. Both DaemonStartError and generic exceptions are rendered as a
|
|
401
|
-
synthetic failed DoctorCheckResult — uniform failure-output shape.
|
|
431
|
+
synthetic failed DoctorCheckResult — uniform failure-output shape. The
|
|
432
|
+
caller decides what to show on failure (retry prompt / next-steps block).
|
|
402
433
|
"""
|
|
403
434
|
from rich.console import Console as _Console
|
|
404
435
|
from rich.live import Live as _Live
|
|
@@ -426,55 +457,101 @@ def _run_init_model_check(settings_path: Path) -> None:
|
|
|
426
457
|
)
|
|
427
458
|
]
|
|
428
459
|
|
|
429
|
-
|
|
460
|
+
ok = True
|
|
430
461
|
for r in results:
|
|
431
462
|
if r.name == "done":
|
|
432
463
|
continue
|
|
433
|
-
_print_doctor_result(r)
|
|
464
|
+
_print_doctor_result(r, verbose=False)
|
|
434
465
|
if not r.ok:
|
|
435
|
-
|
|
466
|
+
ok = False
|
|
467
|
+
return ok
|
|
436
468
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
469
|
+
|
|
470
|
+
def _print_init_next_steps(settings_path: Path) -> None:
|
|
471
|
+
"""Prominent recovery block shown after a failed init model check."""
|
|
472
|
+
import click as _click
|
|
473
|
+
|
|
474
|
+
display_path = format_path_for_display(settings_path)
|
|
475
|
+
_typer.echo(err=True)
|
|
476
|
+
_typer.echo(_click.style(" Next steps", bold=True), err=True)
|
|
477
|
+
_typer.echo(_click.style(f" {'─' * 38}", fg="bright_black"), err=True)
|
|
478
|
+
_typer.echo(
|
|
479
|
+
f" 1. Edit {_click.style(display_path, fg='cyan', bold=True)}\n"
|
|
480
|
+
" to change the model or add API keys under `envs:`.",
|
|
481
|
+
err=True,
|
|
482
|
+
)
|
|
483
|
+
_typer.echo(" 2. Run `ccc doctor` to verify.", err=True)
|
|
484
|
+
_typer.echo() # trailing blank before whatever init prints next
|
|
444
485
|
|
|
445
486
|
|
|
446
487
|
def _setup_user_settings_interactive(litellm_model_flag: str | None) -> None:
|
|
447
|
-
"""Interactive global-settings setup — only runs when settings are missing.
|
|
488
|
+
"""Interactive global-settings setup — only runs when settings are missing.
|
|
489
|
+
|
|
490
|
+
Loops until the configured model passes its check or the user chooses to
|
|
491
|
+
keep the current settings. On failure we offer a retry, but only when we
|
|
492
|
+
can actually re-prompt for a different model — i.e. interactive and not
|
|
493
|
+
pinned by ``--litellm-model``; otherwise we just print the next steps.
|
|
494
|
+
"""
|
|
448
495
|
from .embedder_defaults import lookup_defaults
|
|
449
496
|
from .shared import is_sentence_transformers_installed
|
|
450
497
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
tty=sys.stdin.isatty(),
|
|
455
|
-
)
|
|
498
|
+
st_installed = is_sentence_transformers_installed()
|
|
499
|
+
interactive = sys.stdin.isatty()
|
|
500
|
+
previous: EmbeddingSettings | None = None
|
|
456
501
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
502
|
+
while True:
|
|
503
|
+
embedding = _resolve_embedding_choice(
|
|
504
|
+
litellm_model_flag=litellm_model_flag,
|
|
505
|
+
st_installed=st_installed,
|
|
506
|
+
tty=interactive,
|
|
507
|
+
previous=previous,
|
|
508
|
+
)
|
|
509
|
+
previous = embedding # remembered as the defaults for a potential retry
|
|
463
510
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
511
|
+
# Apply curated defaults if the model is in our table.
|
|
512
|
+
indexing_defaults, query_defaults = lookup_defaults(embedding.provider, embedding.model)
|
|
513
|
+
defaults_applied = indexing_defaults is not None or query_defaults is not None
|
|
514
|
+
if defaults_applied:
|
|
515
|
+
embedding.indexing_params = indexing_defaults or {}
|
|
516
|
+
embedding.query_params = query_defaults or {}
|
|
467
517
|
|
|
468
|
-
|
|
518
|
+
path = save_initial_user_settings(embedding, defaults_applied=defaults_applied)
|
|
469
519
|
_typer.echo()
|
|
470
|
-
_typer.echo(f"
|
|
471
|
-
_typer.echo(f" indexing_params: {embedding.indexing_params}")
|
|
472
|
-
_typer.echo(f" query_params: {embedding.query_params}")
|
|
520
|
+
_typer.echo(f"Created user settings: {format_path_for_display(path)}")
|
|
473
521
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
522
|
+
if defaults_applied:
|
|
523
|
+
_typer.echo()
|
|
524
|
+
_typer.echo(f"Applied recommended defaults for {embedding.model}:")
|
|
525
|
+
_typer.echo(f" indexing_params: {embedding.indexing_params}")
|
|
526
|
+
_typer.echo(f" query_params: {embedding.query_params}")
|
|
527
|
+
|
|
528
|
+
_typer.echo()
|
|
529
|
+
_typer.echo(f"Testing embedding model: {embedding.provider} / {embedding.model}")
|
|
530
|
+
if _run_init_model_check():
|
|
531
|
+
_typer.echo()
|
|
532
|
+
return
|
|
533
|
+
|
|
534
|
+
# Model check failed. Retry only makes sense if we can re-prompt.
|
|
535
|
+
if interactive and litellm_model_flag is None:
|
|
536
|
+
import questionary
|
|
537
|
+
|
|
538
|
+
_typer.echo() # separate the failure output from the prompt below
|
|
539
|
+
choice = questionary.select(
|
|
540
|
+
"The embedding model couldn't be loaded. What would you like to do?",
|
|
541
|
+
choices=[
|
|
542
|
+
questionary.Choice(title="Try a different provider/model", value="retry"),
|
|
543
|
+
questionary.Choice(
|
|
544
|
+
title="Keep these settings and finish — I'll edit the file myself",
|
|
545
|
+
value="keep",
|
|
546
|
+
),
|
|
547
|
+
],
|
|
548
|
+
).ask()
|
|
549
|
+
if choice == "retry":
|
|
550
|
+
continue
|
|
551
|
+
# "keep" or None (cancelled) falls through to the next-steps block.
|
|
552
|
+
|
|
553
|
+
_print_init_next_steps(path)
|
|
554
|
+
return
|
|
478
555
|
|
|
479
556
|
|
|
480
557
|
@app.command()
|
|
@@ -692,7 +769,7 @@ def _print_error(msg: str) -> None:
|
|
|
692
769
|
_typer.echo(_click.style(f" ERROR: {msg}", fg="red"), err=True)
|
|
693
770
|
|
|
694
771
|
|
|
695
|
-
def _print_doctor_result(result: DoctorCheckResult) -> None:
|
|
772
|
+
def _print_doctor_result(result: DoctorCheckResult, *, verbose: bool = False) -> None:
|
|
696
773
|
import click as _click
|
|
697
774
|
|
|
698
775
|
if result.name == "done":
|
|
@@ -703,11 +780,27 @@ def _print_doctor_result(result: DoctorCheckResult) -> None:
|
|
|
703
780
|
_typer.echo(f" {line}")
|
|
704
781
|
for err in result.errors:
|
|
705
782
|
_typer.echo(_click.style(f" ERROR: {err}", fg="red"), err=True)
|
|
783
|
+
if result.traceback:
|
|
784
|
+
if verbose:
|
|
785
|
+
for line in result.traceback.splitlines():
|
|
786
|
+
_typer.echo(_click.style(f" {line}", fg="bright_black"), err=True)
|
|
787
|
+
else:
|
|
788
|
+
_typer.echo(
|
|
789
|
+
_click.style(" Run `ccc doctor -v` for the full traceback.", fg="bright_black"),
|
|
790
|
+
err=True,
|
|
791
|
+
)
|
|
706
792
|
|
|
707
793
|
|
|
708
794
|
@app.command()
|
|
709
795
|
@_catch_daemon_start_error
|
|
710
|
-
def doctor(
|
|
796
|
+
def doctor(
|
|
797
|
+
verbose: bool = _typer.Option(
|
|
798
|
+
False,
|
|
799
|
+
"-v",
|
|
800
|
+
"--verbose",
|
|
801
|
+
help="Show full exception tracebacks for failed checks.",
|
|
802
|
+
),
|
|
803
|
+
) -> None:
|
|
711
804
|
"""Check system health and report issues."""
|
|
712
805
|
from . import client as _client
|
|
713
806
|
from .settings import (
|
|
@@ -717,6 +810,9 @@ def doctor() -> None:
|
|
|
717
810
|
load_user_settings as _load_user_settings,
|
|
718
811
|
)
|
|
719
812
|
|
|
813
|
+
def _on_result(result: DoctorCheckResult) -> None:
|
|
814
|
+
_print_doctor_result(result, verbose=verbose)
|
|
815
|
+
|
|
720
816
|
# --- 1. Global settings (local, no daemon needed) ---
|
|
721
817
|
_print_section("Global Settings")
|
|
722
818
|
settings_path = user_settings_path()
|
|
@@ -770,7 +866,7 @@ def doctor() -> None:
|
|
|
770
866
|
try:
|
|
771
867
|
_client.doctor(
|
|
772
868
|
project_root=None,
|
|
773
|
-
on_result=
|
|
869
|
+
on_result=_on_result,
|
|
774
870
|
)
|
|
775
871
|
except Exception as e:
|
|
776
872
|
_print_error(f"Model check failed: {e}")
|
|
@@ -801,7 +897,7 @@ def doctor() -> None:
|
|
|
801
897
|
try:
|
|
802
898
|
_client.doctor(
|
|
803
899
|
project_root=str(project_root),
|
|
804
|
-
on_result=
|
|
900
|
+
on_result=_on_result,
|
|
805
901
|
)
|
|
806
902
|
except Exception as e:
|
|
807
903
|
_print_error(f"Project checks failed: {e}")
|
|
@@ -111,25 +111,33 @@ def _connect_and_handshake() -> Connection:
|
|
|
111
111
|
|
|
112
112
|
Returns the open connection for the caller to send exactly one request.
|
|
113
113
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
``
|
|
117
|
-
|
|
114
|
+
Automatically starts or restarts the daemon when it is absent or running
|
|
115
|
+
with stale global settings (e.g. a ``ccc init`` retry rewrote
|
|
116
|
+
``global_settings.yml`` after the daemon loaded it). A genuine *version*
|
|
117
|
+
mismatch after we have already reached a matching daemon means the binary
|
|
118
|
+
was replaced under us mid-session — that fails fast instead of looping on
|
|
119
|
+
restarts.
|
|
118
120
|
"""
|
|
119
121
|
global _daemon_ensured # noqa: PLW0603
|
|
120
122
|
|
|
121
|
-
if _daemon_ensured:
|
|
122
|
-
return _raw_connect_and_handshake()
|
|
123
|
-
|
|
124
|
-
# First connection — auto-start/restart as needed.
|
|
125
123
|
try:
|
|
126
124
|
conn = _raw_connect_and_handshake()
|
|
127
125
|
_daemon_ensured = True
|
|
128
126
|
return conn
|
|
129
|
-
except DaemonVersionError:
|
|
127
|
+
except DaemonVersionError as e:
|
|
128
|
+
# `resp.ok` is False only for a real version mismatch. Once we have
|
|
129
|
+
# ensured a matching daemon, a fresh version mismatch means the binary
|
|
130
|
+
# was swapped under us — fail fast. A settings-only restart request
|
|
131
|
+
# (resp.ok True, but the loaded settings mtime moved) is expected;
|
|
132
|
+
# restart the daemon below so it reloads them.
|
|
133
|
+
if _daemon_ensured and not e.resp.ok:
|
|
134
|
+
raise
|
|
130
135
|
stop_daemon()
|
|
131
136
|
except (ConnectionRefusedError, OSError):
|
|
132
|
-
|
|
137
|
+
# No daemon answered. Normal on the first call (start one below); if we
|
|
138
|
+
# had already ensured one it vanished mid-session — surface that.
|
|
139
|
+
if _daemon_ensured:
|
|
140
|
+
raise
|
|
133
141
|
|
|
134
142
|
if _is_daemon_supervised():
|
|
135
143
|
# Supervisor is responsible for (re)starting the daemon — just wait
|
|
@@ -192,10 +200,16 @@ class DaemonVersionError(RuntimeError):
|
|
|
192
200
|
|
|
193
201
|
def __init__(self, resp: HandshakeResponse) -> None:
|
|
194
202
|
self.resp = resp
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
203
|
+
if not resp.ok:
|
|
204
|
+
message = (
|
|
205
|
+
f"Daemon version mismatch (daemon={resp.daemon_version}, "
|
|
206
|
+
f"client={__version__}). Please retry — the daemon may need a restart."
|
|
207
|
+
)
|
|
208
|
+
else:
|
|
209
|
+
message = (
|
|
210
|
+
"Daemon is running with stale global settings and needs a restart. Please retry."
|
|
211
|
+
)
|
|
212
|
+
super().__init__(message)
|
|
199
213
|
|
|
200
214
|
|
|
201
215
|
class DaemonStartError(RuntimeError):
|
|
@@ -363,7 +377,10 @@ def doctor(
|
|
|
363
377
|
raise RuntimeError("Connection to daemon lost during doctor checks")
|
|
364
378
|
resp = decode_response(data)
|
|
365
379
|
if isinstance(resp, ErrorResponse):
|
|
366
|
-
|
|
380
|
+
detail = f"Daemon error: {resp.message}"
|
|
381
|
+
if resp.traceback:
|
|
382
|
+
detail += f"\n{resp.traceback}"
|
|
383
|
+
raise RuntimeError(detail)
|
|
367
384
|
if isinstance(resp, DoctorResponse):
|
|
368
385
|
results.append(resp.result)
|
|
369
386
|
if on_result is not None:
|
|
@@ -10,6 +10,7 @@ import signal
|
|
|
10
10
|
import sys
|
|
11
11
|
import threading
|
|
12
12
|
import time
|
|
13
|
+
import traceback
|
|
13
14
|
from collections.abc import AsyncIterator, Callable
|
|
14
15
|
from multiprocessing.connection import Connection, Listener
|
|
15
16
|
from pathlib import Path
|
|
@@ -250,7 +251,11 @@ async def handle_connection(
|
|
|
250
251
|
conn.send_bytes(encode_response(resp))
|
|
251
252
|
except Exception as exc:
|
|
252
253
|
logger.exception("Error during streaming response")
|
|
253
|
-
conn.send_bytes(
|
|
254
|
+
conn.send_bytes(
|
|
255
|
+
encode_response(
|
|
256
|
+
ErrorResponse(message=str(exc), traceback=traceback.format_exc())
|
|
257
|
+
)
|
|
258
|
+
)
|
|
254
259
|
else:
|
|
255
260
|
conn.send_bytes(encode_response(result))
|
|
256
261
|
except (EOFError, OSError, asyncio.CancelledError):
|
|
@@ -359,6 +364,7 @@ async def _check_model(
|
|
|
359
364
|
ok=False,
|
|
360
365
|
details=[params_detail],
|
|
361
366
|
errors=[result.error],
|
|
367
|
+
traceback=result.traceback,
|
|
362
368
|
)
|
|
363
369
|
|
|
364
370
|
|
|
@@ -154,6 +154,9 @@ class DoctorCheckResult(_msgspec.Struct):
|
|
|
154
154
|
ok: bool
|
|
155
155
|
details: list[str]
|
|
156
156
|
errors: list[str]
|
|
157
|
+
# Full formatted traceback for a failed check, shown by `ccc doctor` to aid
|
|
158
|
+
# debugging of daemon-side exceptions (e.g. a failing model check).
|
|
159
|
+
traceback: str | None = None
|
|
157
160
|
|
|
158
161
|
|
|
159
162
|
class DoctorResponse(_msgspec.Struct, tag="doctor"):
|
|
@@ -175,6 +178,9 @@ class DaemonEnvResponse(_msgspec.Struct, tag="daemon_env"):
|
|
|
175
178
|
|
|
176
179
|
class ErrorResponse(_msgspec.Struct, tag="error"):
|
|
177
180
|
message: str
|
|
181
|
+
# Full formatted traceback from the daemon, when the error originates from an
|
|
182
|
+
# unhandled exception. Surfaced by the CLI so daemon-side failures are debuggable.
|
|
183
|
+
traceback: str | None = None
|
|
178
184
|
|
|
179
185
|
|
|
180
186
|
Response = (
|
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import importlib.util
|
|
6
6
|
import logging
|
|
7
7
|
import pathlib
|
|
8
|
+
import traceback as _tb
|
|
8
9
|
from dataclasses import dataclass
|
|
9
10
|
from typing import TYPE_CHECKING, Annotated, Any, NamedTuple, Union
|
|
10
11
|
|
|
@@ -47,11 +48,14 @@ def is_sentence_transformers_installed() -> bool:
|
|
|
47
48
|
class EmbeddingCheckResult(NamedTuple):
|
|
48
49
|
"""Outcome of a single embed-test call. See `check_embedding`.
|
|
49
50
|
|
|
50
|
-
|
|
51
|
+
On success ``error is None`` and ``dim`` holds the embedding dimension. On
|
|
52
|
+
failure ``error`` holds a one-line summary and ``traceback`` the full
|
|
53
|
+
formatted traceback (for surfacing daemon-side stack traces in `ccc doctor`).
|
|
51
54
|
"""
|
|
52
55
|
|
|
53
56
|
dim: int | None
|
|
54
57
|
error: str | None
|
|
58
|
+
traceback: str | None = None
|
|
55
59
|
|
|
56
60
|
|
|
57
61
|
async def check_embedding(
|
|
@@ -73,7 +77,7 @@ async def check_embedding(
|
|
|
73
77
|
msg = " ".join(f"{type(e).__name__}: {e}".splitlines())
|
|
74
78
|
if len(msg) > 500:
|
|
75
79
|
msg = msg[:500] + "…"
|
|
76
|
-
return EmbeddingCheckResult(dim=None, error=msg)
|
|
80
|
+
return EmbeddingCheckResult(dim=None, error=msg, traceback=_tb.format_exc())
|
|
77
81
|
|
|
78
82
|
|
|
79
83
|
def create_embedder(
|
|
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
|