cocoindex-code 0.2.42__tar.gz → 0.2.43__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.42 → cocoindex_code-0.2.43}/PKG-INFO +33 -25
- {cocoindex_code-0.2.42 → cocoindex_code-0.2.43}/README.md +32 -24
- {cocoindex_code-0.2.42 → cocoindex_code-0.2.43}/src/cocoindex_code/_daemon_paths.py +40 -0
- {cocoindex_code-0.2.42 → cocoindex_code-0.2.43}/src/cocoindex_code/_version.py +2 -2
- cocoindex_code-0.2.43/src/cocoindex_code/chunking.py +54 -0
- {cocoindex_code-0.2.42 → cocoindex_code-0.2.43}/src/cocoindex_code/cli.py +28 -8
- {cocoindex_code-0.2.42 → cocoindex_code-0.2.43}/src/cocoindex_code/client.py +15 -9
- {cocoindex_code-0.2.42 → cocoindex_code-0.2.43}/src/cocoindex_code/daemon.py +123 -30
- {cocoindex_code-0.2.42 → cocoindex_code-0.2.43}/src/cocoindex_code/indexer.py +15 -7
- {cocoindex_code-0.2.42 → cocoindex_code-0.2.43}/src/cocoindex_code/project.py +42 -5
- {cocoindex_code-0.2.42 → cocoindex_code-0.2.43}/src/cocoindex_code/server.py +26 -5
- {cocoindex_code-0.2.42 → cocoindex_code-0.2.43}/src/cocoindex_code/settings.py +48 -0
- {cocoindex_code-0.2.42 → cocoindex_code-0.2.43}/src/cocoindex_code/shared.py +2 -1
- cocoindex_code-0.2.42/src/cocoindex_code/chunking.py +0 -29
- {cocoindex_code-0.2.42 → cocoindex_code-0.2.43}/.gitignore +0 -0
- {cocoindex_code-0.2.42 → cocoindex_code-0.2.43}/LICENSE +0 -0
- {cocoindex_code-0.2.42 → cocoindex_code-0.2.43}/pyproject.toml +0 -0
- {cocoindex_code-0.2.42 → cocoindex_code-0.2.43}/src/cocoindex_code/__init__.py +0 -0
- {cocoindex_code-0.2.42 → cocoindex_code-0.2.43}/src/cocoindex_code/__main__.py +0 -0
- {cocoindex_code-0.2.42 → cocoindex_code-0.2.43}/src/cocoindex_code/embedder_defaults.py +0 -0
- {cocoindex_code-0.2.42 → cocoindex_code-0.2.43}/src/cocoindex_code/embedder_params.py +0 -0
- {cocoindex_code-0.2.42 → cocoindex_code-0.2.43}/src/cocoindex_code/file_walk.py +0 -0
- {cocoindex_code-0.2.42 → cocoindex_code-0.2.43}/src/cocoindex_code/grep.py +0 -0
- {cocoindex_code-0.2.42 → cocoindex_code-0.2.43}/src/cocoindex_code/litellm_embedder.py +0 -0
- {cocoindex_code-0.2.42 → cocoindex_code-0.2.43}/src/cocoindex_code/protocol.py +0 -0
- {cocoindex_code-0.2.42 → cocoindex_code-0.2.43}/src/cocoindex_code/query.py +0 -0
- {cocoindex_code-0.2.42 → cocoindex_code-0.2.43}/src/cocoindex_code/schema.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: cocoindex-code
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.43
|
|
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
|
|
@@ -47,15 +47,15 @@ Description-Content-Type: text/markdown
|
|
|
47
47
|
</p>
|
|
48
48
|
|
|
49
49
|
|
|
50
|
-
<h1 align="center">AST-based semantic code search
|
|
50
|
+
<h1 align="center">AST-based semantic code search</h1>
|
|
51
51
|
|
|
52
52
|

|
|
53
53
|
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
CocoIndex Code provides AST-based semantic search for your codebase, built on the [CocoIndex](https://github.com/cocoindex-io/cocoindex) Rust data transformation engine. Use it from the CLI or connect Claude, Codex, Cursor, and other coding agents through a [Skill](#skill-recommended) or [MCP](#mcp-server).
|
|
56
56
|
|
|
57
|
-
-
|
|
58
|
-
-
|
|
57
|
+
- 70% token savings.
|
|
58
|
+
- 1 min setup with default settings.
|
|
59
59
|
|
|
60
60
|
<div align="center">
|
|
61
61
|
|
|
@@ -69,7 +69,7 @@ A lightweight, effective **(AST-based)** semantic code search tool for your code
|
|
|
69
69
|
[](https://github.com/cocoindex-io/cocoindex/actions/workflows/release.yml)
|
|
70
70
|
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
Star [CocoIndex](https://github.com/cocoindex-io/cocoindex) if you find it useful.
|
|
73
73
|
|
|
74
74
|
[Deutsch](https://readme-i18n.com/cocoindex-io/cocoindex-code?lang=de) |
|
|
75
75
|
[English](https://readme-i18n.com/cocoindex-io/cocoindex-code?lang=en) |
|
|
@@ -84,7 +84,9 @@ A lightweight, effective **(AST-based)** semantic code search tool for your code
|
|
|
84
84
|
</div>
|
|
85
85
|
|
|
86
86
|
|
|
87
|
-
|
|
87
|
+
<a id="get-started--zero-config-lets-go"></a>
|
|
88
|
+
|
|
89
|
+
## Get Started
|
|
88
90
|
|
|
89
91
|
### Install
|
|
90
92
|
|
|
@@ -117,7 +119,7 @@ Install the `ccc` skill so your coding agent automatically uses semantic search
|
|
|
117
119
|
npx skills add cocoindex-io/cocoindex-code
|
|
118
120
|
```
|
|
119
121
|
|
|
120
|
-
|
|
122
|
+
The skill teaches the agent to handle initialization, indexing, and searching, so you don't need to run `ccc init` or `ccc index` yourself. The agent keeps the index up to date as you work.
|
|
121
123
|
|
|
122
124
|
The agent uses semantic search automatically when it would be helpful. You can also nudge it explicitly — just ask it to search the codebase, e.g. *"find how user sessions are managed"*, or type `/ccc` to invoke the skill directly.
|
|
123
125
|
|
|
@@ -141,7 +143,7 @@ For [Grok](https://github.com/xai-org/grok) users, install via Grok's plugin sys
|
|
|
141
143
|
| Component | Purpose |
|
|
142
144
|
|-----------|---------|
|
|
143
145
|
| **Skill** (`skills/ccc/`) | Agent runs `ccc search` / `ccc index` via the CLI (same as Claude Code above) |
|
|
144
|
-
| **Hook** (`hooks/hooks.json`) | `SessionStart` + `PostToolUse` (Edit/Write/…) → incremental `ccc index` when `.cocoindex_code
|
|
146
|
+
| **Hook** (`hooks/hooks.json`) | `SessionStart` + `PostToolUse` (Edit/Write/…) → incremental `ccc index` when `.cocoindex_code/settings.yml` exists (never in the directory holding ccc's user settings, `~` by default) |
|
|
145
147
|
| **MCP** (`.mcp.json`) | `ccc mcp` stdio server — `search` tool with `refresh_index=true` by default |
|
|
146
148
|
|
|
147
149
|
Grok does **not** import Claude's `enabledPlugins` or plugin cache; install separately even if you already use cocoindex in Claude Code.
|
|
@@ -196,7 +198,7 @@ Then restart the session (`/reload-plugins` does not reload extensions). Require
|
|
|
196
198
|
| Component | Purpose |
|
|
197
199
|
|-----------|---------|
|
|
198
200
|
| **Skill** (`skills/ccc/`) | Agent runs `ccc search` / `ccc index` via the CLI |
|
|
199
|
-
| **Hook** (`extensions/ccc-index.ts` via `package.json#omp.extensions`) | `session_start` + post-edit `tool_result` → incremental `ccc index` when `.cocoindex_code
|
|
201
|
+
| **Hook** (`extensions/ccc-index.ts` via `package.json#omp.extensions`) | `session_start` + post-edit `tool_result` → incremental `ccc index` when `.cocoindex_code/settings.yml` exists (never in the directory holding ccc's user settings, `~` by default) |
|
|
200
202
|
| **MCP** (`.mcp.json`) | `ccc mcp` stdio server |
|
|
201
203
|
|
|
202
204
|
OMP does **not** execute Claude `hooks/hooks.json` command hooks. The TypeScript extension is the OMP equivalent of the Claude/Grok SessionStart + PostToolUse pair.
|
|
@@ -205,6 +207,9 @@ Skill-only (no MCP, no auto-index): install the skill via `npx skills add cocoin
|
|
|
205
207
|
|
|
206
208
|
### MCP Server
|
|
207
209
|
|
|
210
|
+
<!-- Ownership marker for the official MCP registry. It must match "name" in server.json. Keep the next line. -->
|
|
211
|
+
<!-- mcp-name: io.github.cocoindex-io/cocoindex-code -->
|
|
212
|
+
|
|
208
213
|
Alternatively, use `ccc mcp` to run as an MCP server:
|
|
209
214
|
|
|
210
215
|
<details>
|
|
@@ -284,7 +289,7 @@ Prefer the marketplace install above. Manual project MCP (`.omp/mcp.json`):
|
|
|
284
289
|
```
|
|
285
290
|
</details>
|
|
286
291
|
|
|
287
|
-
Once configured, the agent
|
|
292
|
+
Once configured, the agent can use semantic search to find code by description, explore unfamiliar codebases, and locate implementations without exact names.
|
|
288
293
|
|
|
289
294
|
> **Note:** The `cocoindex-code` command (without subcommand) still works as an MCP server for backward compatibility. It auto-creates settings from environment variables on first run.
|
|
290
295
|
|
|
@@ -323,6 +328,8 @@ The background daemon starts automatically on first use.
|
|
|
323
328
|
|
|
324
329
|
> **Tip:** You can skip `ccc init` and go straight to `ccc index` — it auto-initializes new projects with default settings. If global settings are missing too (first use on the machine), it walks you through the same model setup as `ccc init` when run interactively; non-interactive runs (scripts, hooks) still require a one-time `ccc init` first.
|
|
325
330
|
|
|
331
|
+
> **Note:** `ccc index` never auto-initializes the directory that holds ccc's user settings directory: your home directory by default (`~/.cocoindex_code` holds the global settings and daemon files), or the parent of `$COCOINDEX_CODE_DIR` when that directory is named `.cocoindex_code`. A project there would cover every repo below it. The editor hooks skip that directory even if it is already a project. If an older ccc made your home directory a project (`~/.cocoindex_code/settings.yml` exists), run `ccc reset --all` in your home directory: it removes that project's settings and indexes and keeps your global settings. To index your whole home directory on purpose, run `ccc init` there.
|
|
332
|
+
|
|
326
333
|
### CLI Reference
|
|
327
334
|
|
|
328
335
|
| Command | Description |
|
|
@@ -475,8 +482,8 @@ ccc() {
|
|
|
475
482
|
}
|
|
476
483
|
```
|
|
477
484
|
|
|
478
|
-
Now `cd` into
|
|
479
|
-
`ccc search ...`, `ccc status
|
|
485
|
+
Now `cd` into a project under your workspace and run `ccc init`, `ccc index`,
|
|
486
|
+
`ccc search ...`, or `ccc status`.
|
|
480
487
|
|
|
481
488
|
### Connect your coding agent
|
|
482
489
|
|
|
@@ -565,11 +572,11 @@ docker build -t cocoindex-code:local -f docker/Dockerfile .
|
|
|
565
572
|
```
|
|
566
573
|
|
|
567
574
|
## Features
|
|
568
|
-
- **Semantic Code Search**: Find relevant code using natural language queries when grep doesn't work well
|
|
569
|
-
- **
|
|
575
|
+
- **Semantic Code Search**: Find relevant code using natural language queries when grep doesn't work well.
|
|
576
|
+
- **Incremental indexing**: The [Rust indexing engine](https://github.com/cocoindex-io/cocoindex) re-indexes only changed files.
|
|
570
577
|
- **Multi-Language Support**: Python, JavaScript/TypeScript, Rust, Go, Java, C/C++, C#, SQL, Shell, and more.
|
|
571
|
-
- **Embedded**:
|
|
572
|
-
- **
|
|
578
|
+
- **Embedded storage**: No database setup required.
|
|
579
|
+
- **Embedding providers**: Local SentenceTransformers via the `[full]` extra, with no API key, or 100+ cloud providers via LiteLLM.
|
|
573
580
|
|
|
574
581
|
## Configuration
|
|
575
582
|
|
|
@@ -685,8 +692,9 @@ After editing `include_patterns`, `exclude_patterns`, `max_file_size`, or `langu
|
|
|
685
692
|
- Run `ccc doctor` to preview which files match.
|
|
686
693
|
- Run `ccc index` or `ccc search --refresh ...` to update the existing index.
|
|
687
694
|
- You do not need to delete the index or restart the daemon for these file-matching changes.
|
|
695
|
+
- After any `language_overrides` change, the next run re-chunks all files, including files whose content did not change; the run reports them as reprocessed. Chunks whose text did not change keep their cached embeddings.
|
|
688
696
|
|
|
689
|
-
If you add or change custom `chunkers`, restart the daemon
|
|
697
|
+
If you add or change custom `chunkers`, or edit a chunker module, restart the daemon (`ccc daemon restart`), then run `ccc index`. The daemon imports chunkers once at startup, so until it restarts it keeps running the old chunker code. The first run after the restart re-chunks all files, not only the ones the changed entry applies to; chunks whose text did not change keep their cached embeddings. A chunker counts as changed when its `module:` value changes or when the file of the module it names changes. Edits to other modules that file imports are not detected: after such an edit, also change the chunker's own module file (for example, bump a version comment in it).
|
|
690
698
|
|
|
691
699
|
Use `chunkers` when you want to control how a file type is split into chunks before indexing.
|
|
692
700
|
|
|
@@ -938,7 +946,7 @@ Both source and target must be absolute paths. If no mapping matches, the defaul
|
|
|
938
946
|
|
|
939
947
|
## Troubleshooting
|
|
940
948
|
|
|
941
|
-
Run `ccc doctor` to
|
|
949
|
+
Run `ccc doctor` to check your settings, daemon health, embedding model, file matching, and index status.
|
|
942
950
|
|
|
943
951
|
### `sqlite3.Connection object has no attribute enable_load_extension`
|
|
944
952
|
|
|
@@ -950,7 +958,7 @@ Some Python installations (e.g. the one pre-installed on macOS) ship with a SQLi
|
|
|
950
958
|
brew install python3
|
|
951
959
|
```
|
|
952
960
|
|
|
953
|
-
Then re-install cocoindex-code (see [Get Started](#get-started
|
|
961
|
+
Then re-install cocoindex-code (see [Get Started](#get-started) for install options):
|
|
954
962
|
|
|
955
963
|
Using pipx:
|
|
956
964
|
```bash
|
|
@@ -965,7 +973,7 @@ uv tool install --upgrade cocoindex-code
|
|
|
965
973
|
|
|
966
974
|
### `MDB_MAP_FULL: Environment mapsize limit reached`
|
|
967
975
|
|
|
968
|
-
The index is stored in an LMDB database whose maximum size is fixed when the daemon starts. The default ceiling is **4 GiB**, which
|
|
976
|
+
The index is stored in an LMDB database whose maximum size is fixed when the daemon starts. The default ceiling is **4 GiB**, which codebases with tens of thousands of files can exhaust, especially with high-dimensional embedding models like `nomic-ai/CodeRankEmbed`.
|
|
969
977
|
|
|
970
978
|
Raise the ceiling with the `COCOINDEX_LMDB_MAP_SIZE` environment variable (value in **bytes**). LMDB only grows the file as data is written, so a high limit doesn't pre-allocate disk — it's safe to set it generously:
|
|
971
979
|
|
|
@@ -1015,15 +1023,15 @@ export COCOINDEX_DISABLE_USAGE_TRACKING=1
|
|
|
1015
1023
|
```
|
|
1016
1024
|
|
|
1017
1025
|
## Large codebase / Enterprise
|
|
1018
|
-
[CocoIndex](https://github.com/cocoindex-io/cocoindex)
|
|
1026
|
+
Teams indexing large or multiple repositories can share [CocoIndex](https://github.com/cocoindex-io/cocoindex) indexes across developers. Enterprise features include deduplication across branches.
|
|
1019
1027
|
|
|
1020
|
-
>
|
|
1028
|
+
> If indexing fails with `MDB_MAP_FULL`, raise the LMDB map size. See [`MDB_MAP_FULL: Environment mapsize limit reached`](#mdb_map_full-environment-mapsize-limit-reached) under Troubleshooting.
|
|
1021
1029
|
|
|
1022
|
-
|
|
1030
|
+
For help with remote setup, email linghua@cocoindex.io.
|
|
1023
1031
|
|
|
1024
1032
|
## Contributing
|
|
1025
1033
|
|
|
1026
|
-
We welcome contributions
|
|
1034
|
+
We welcome contributions. This project uses [uv](https://docs.astral.sh/uv/getting-started/installation/) for development. CI checks lint, formatting, types, and tests for every PR. Run the same checks locally before opening a PR; failed pre-commit checks are the most common cause of red CI on incoming PRs.
|
|
1027
1035
|
|
|
1028
1036
|
### 1. Install the dev dependencies
|
|
1029
1037
|
|
|
@@ -3,15 +3,15 @@
|
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
<h1 align="center">AST-based semantic code search
|
|
6
|
+
<h1 align="center">AST-based semantic code search</h1>
|
|
7
7
|
|
|
8
8
|

|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
CocoIndex Code provides AST-based semantic search for your codebase, built on the [CocoIndex](https://github.com/cocoindex-io/cocoindex) Rust data transformation engine. Use it from the CLI or connect Claude, Codex, Cursor, and other coding agents through a [Skill](#skill-recommended) or [MCP](#mcp-server).
|
|
12
12
|
|
|
13
|
-
-
|
|
14
|
-
-
|
|
13
|
+
- 70% token savings.
|
|
14
|
+
- 1 min setup with default settings.
|
|
15
15
|
|
|
16
16
|
<div align="center">
|
|
17
17
|
|
|
@@ -25,7 +25,7 @@ A lightweight, effective **(AST-based)** semantic code search tool for your code
|
|
|
25
25
|
[](https://github.com/cocoindex-io/cocoindex/actions/workflows/release.yml)
|
|
26
26
|
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
Star [CocoIndex](https://github.com/cocoindex-io/cocoindex) if you find it useful.
|
|
29
29
|
|
|
30
30
|
[Deutsch](https://readme-i18n.com/cocoindex-io/cocoindex-code?lang=de) |
|
|
31
31
|
[English](https://readme-i18n.com/cocoindex-io/cocoindex-code?lang=en) |
|
|
@@ -40,7 +40,9 @@ A lightweight, effective **(AST-based)** semantic code search tool for your code
|
|
|
40
40
|
</div>
|
|
41
41
|
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
<a id="get-started--zero-config-lets-go"></a>
|
|
44
|
+
|
|
45
|
+
## Get Started
|
|
44
46
|
|
|
45
47
|
### Install
|
|
46
48
|
|
|
@@ -73,7 +75,7 @@ Install the `ccc` skill so your coding agent automatically uses semantic search
|
|
|
73
75
|
npx skills add cocoindex-io/cocoindex-code
|
|
74
76
|
```
|
|
75
77
|
|
|
76
|
-
|
|
78
|
+
The skill teaches the agent to handle initialization, indexing, and searching, so you don't need to run `ccc init` or `ccc index` yourself. The agent keeps the index up to date as you work.
|
|
77
79
|
|
|
78
80
|
The agent uses semantic search automatically when it would be helpful. You can also nudge it explicitly — just ask it to search the codebase, e.g. *"find how user sessions are managed"*, or type `/ccc` to invoke the skill directly.
|
|
79
81
|
|
|
@@ -97,7 +99,7 @@ For [Grok](https://github.com/xai-org/grok) users, install via Grok's plugin sys
|
|
|
97
99
|
| Component | Purpose |
|
|
98
100
|
|-----------|---------|
|
|
99
101
|
| **Skill** (`skills/ccc/`) | Agent runs `ccc search` / `ccc index` via the CLI (same as Claude Code above) |
|
|
100
|
-
| **Hook** (`hooks/hooks.json`) | `SessionStart` + `PostToolUse` (Edit/Write/…) → incremental `ccc index` when `.cocoindex_code
|
|
102
|
+
| **Hook** (`hooks/hooks.json`) | `SessionStart` + `PostToolUse` (Edit/Write/…) → incremental `ccc index` when `.cocoindex_code/settings.yml` exists (never in the directory holding ccc's user settings, `~` by default) |
|
|
101
103
|
| **MCP** (`.mcp.json`) | `ccc mcp` stdio server — `search` tool with `refresh_index=true` by default |
|
|
102
104
|
|
|
103
105
|
Grok does **not** import Claude's `enabledPlugins` or plugin cache; install separately even if you already use cocoindex in Claude Code.
|
|
@@ -152,7 +154,7 @@ Then restart the session (`/reload-plugins` does not reload extensions). Require
|
|
|
152
154
|
| Component | Purpose |
|
|
153
155
|
|-----------|---------|
|
|
154
156
|
| **Skill** (`skills/ccc/`) | Agent runs `ccc search` / `ccc index` via the CLI |
|
|
155
|
-
| **Hook** (`extensions/ccc-index.ts` via `package.json#omp.extensions`) | `session_start` + post-edit `tool_result` → incremental `ccc index` when `.cocoindex_code
|
|
157
|
+
| **Hook** (`extensions/ccc-index.ts` via `package.json#omp.extensions`) | `session_start` + post-edit `tool_result` → incremental `ccc index` when `.cocoindex_code/settings.yml` exists (never in the directory holding ccc's user settings, `~` by default) |
|
|
156
158
|
| **MCP** (`.mcp.json`) | `ccc mcp` stdio server |
|
|
157
159
|
|
|
158
160
|
OMP does **not** execute Claude `hooks/hooks.json` command hooks. The TypeScript extension is the OMP equivalent of the Claude/Grok SessionStart + PostToolUse pair.
|
|
@@ -161,6 +163,9 @@ Skill-only (no MCP, no auto-index): install the skill via `npx skills add cocoin
|
|
|
161
163
|
|
|
162
164
|
### MCP Server
|
|
163
165
|
|
|
166
|
+
<!-- Ownership marker for the official MCP registry. It must match "name" in server.json. Keep the next line. -->
|
|
167
|
+
<!-- mcp-name: io.github.cocoindex-io/cocoindex-code -->
|
|
168
|
+
|
|
164
169
|
Alternatively, use `ccc mcp` to run as an MCP server:
|
|
165
170
|
|
|
166
171
|
<details>
|
|
@@ -240,7 +245,7 @@ Prefer the marketplace install above. Manual project MCP (`.omp/mcp.json`):
|
|
|
240
245
|
```
|
|
241
246
|
</details>
|
|
242
247
|
|
|
243
|
-
Once configured, the agent
|
|
248
|
+
Once configured, the agent can use semantic search to find code by description, explore unfamiliar codebases, and locate implementations without exact names.
|
|
244
249
|
|
|
245
250
|
> **Note:** The `cocoindex-code` command (without subcommand) still works as an MCP server for backward compatibility. It auto-creates settings from environment variables on first run.
|
|
246
251
|
|
|
@@ -279,6 +284,8 @@ The background daemon starts automatically on first use.
|
|
|
279
284
|
|
|
280
285
|
> **Tip:** You can skip `ccc init` and go straight to `ccc index` — it auto-initializes new projects with default settings. If global settings are missing too (first use on the machine), it walks you through the same model setup as `ccc init` when run interactively; non-interactive runs (scripts, hooks) still require a one-time `ccc init` first.
|
|
281
286
|
|
|
287
|
+
> **Note:** `ccc index` never auto-initializes the directory that holds ccc's user settings directory: your home directory by default (`~/.cocoindex_code` holds the global settings and daemon files), or the parent of `$COCOINDEX_CODE_DIR` when that directory is named `.cocoindex_code`. A project there would cover every repo below it. The editor hooks skip that directory even if it is already a project. If an older ccc made your home directory a project (`~/.cocoindex_code/settings.yml` exists), run `ccc reset --all` in your home directory: it removes that project's settings and indexes and keeps your global settings. To index your whole home directory on purpose, run `ccc init` there.
|
|
288
|
+
|
|
282
289
|
### CLI Reference
|
|
283
290
|
|
|
284
291
|
| Command | Description |
|
|
@@ -431,8 +438,8 @@ ccc() {
|
|
|
431
438
|
}
|
|
432
439
|
```
|
|
433
440
|
|
|
434
|
-
Now `cd` into
|
|
435
|
-
`ccc search ...`, `ccc status
|
|
441
|
+
Now `cd` into a project under your workspace and run `ccc init`, `ccc index`,
|
|
442
|
+
`ccc search ...`, or `ccc status`.
|
|
436
443
|
|
|
437
444
|
### Connect your coding agent
|
|
438
445
|
|
|
@@ -521,11 +528,11 @@ docker build -t cocoindex-code:local -f docker/Dockerfile .
|
|
|
521
528
|
```
|
|
522
529
|
|
|
523
530
|
## Features
|
|
524
|
-
- **Semantic Code Search**: Find relevant code using natural language queries when grep doesn't work well
|
|
525
|
-
- **
|
|
531
|
+
- **Semantic Code Search**: Find relevant code using natural language queries when grep doesn't work well.
|
|
532
|
+
- **Incremental indexing**: The [Rust indexing engine](https://github.com/cocoindex-io/cocoindex) re-indexes only changed files.
|
|
526
533
|
- **Multi-Language Support**: Python, JavaScript/TypeScript, Rust, Go, Java, C/C++, C#, SQL, Shell, and more.
|
|
527
|
-
- **Embedded**:
|
|
528
|
-
- **
|
|
534
|
+
- **Embedded storage**: No database setup required.
|
|
535
|
+
- **Embedding providers**: Local SentenceTransformers via the `[full]` extra, with no API key, or 100+ cloud providers via LiteLLM.
|
|
529
536
|
|
|
530
537
|
## Configuration
|
|
531
538
|
|
|
@@ -641,8 +648,9 @@ After editing `include_patterns`, `exclude_patterns`, `max_file_size`, or `langu
|
|
|
641
648
|
- Run `ccc doctor` to preview which files match.
|
|
642
649
|
- Run `ccc index` or `ccc search --refresh ...` to update the existing index.
|
|
643
650
|
- You do not need to delete the index or restart the daemon for these file-matching changes.
|
|
651
|
+
- After any `language_overrides` change, the next run re-chunks all files, including files whose content did not change; the run reports them as reprocessed. Chunks whose text did not change keep their cached embeddings.
|
|
644
652
|
|
|
645
|
-
If you add or change custom `chunkers`, restart the daemon
|
|
653
|
+
If you add or change custom `chunkers`, or edit a chunker module, restart the daemon (`ccc daemon restart`), then run `ccc index`. The daemon imports chunkers once at startup, so until it restarts it keeps running the old chunker code. The first run after the restart re-chunks all files, not only the ones the changed entry applies to; chunks whose text did not change keep their cached embeddings. A chunker counts as changed when its `module:` value changes or when the file of the module it names changes. Edits to other modules that file imports are not detected: after such an edit, also change the chunker's own module file (for example, bump a version comment in it).
|
|
646
654
|
|
|
647
655
|
Use `chunkers` when you want to control how a file type is split into chunks before indexing.
|
|
648
656
|
|
|
@@ -894,7 +902,7 @@ Both source and target must be absolute paths. If no mapping matches, the defaul
|
|
|
894
902
|
|
|
895
903
|
## Troubleshooting
|
|
896
904
|
|
|
897
|
-
Run `ccc doctor` to
|
|
905
|
+
Run `ccc doctor` to check your settings, daemon health, embedding model, file matching, and index status.
|
|
898
906
|
|
|
899
907
|
### `sqlite3.Connection object has no attribute enable_load_extension`
|
|
900
908
|
|
|
@@ -906,7 +914,7 @@ Some Python installations (e.g. the one pre-installed on macOS) ship with a SQLi
|
|
|
906
914
|
brew install python3
|
|
907
915
|
```
|
|
908
916
|
|
|
909
|
-
Then re-install cocoindex-code (see [Get Started](#get-started
|
|
917
|
+
Then re-install cocoindex-code (see [Get Started](#get-started) for install options):
|
|
910
918
|
|
|
911
919
|
Using pipx:
|
|
912
920
|
```bash
|
|
@@ -921,7 +929,7 @@ uv tool install --upgrade cocoindex-code
|
|
|
921
929
|
|
|
922
930
|
### `MDB_MAP_FULL: Environment mapsize limit reached`
|
|
923
931
|
|
|
924
|
-
The index is stored in an LMDB database whose maximum size is fixed when the daemon starts. The default ceiling is **4 GiB**, which
|
|
932
|
+
The index is stored in an LMDB database whose maximum size is fixed when the daemon starts. The default ceiling is **4 GiB**, which codebases with tens of thousands of files can exhaust, especially with high-dimensional embedding models like `nomic-ai/CodeRankEmbed`.
|
|
925
933
|
|
|
926
934
|
Raise the ceiling with the `COCOINDEX_LMDB_MAP_SIZE` environment variable (value in **bytes**). LMDB only grows the file as data is written, so a high limit doesn't pre-allocate disk — it's safe to set it generously:
|
|
927
935
|
|
|
@@ -971,15 +979,15 @@ export COCOINDEX_DISABLE_USAGE_TRACKING=1
|
|
|
971
979
|
```
|
|
972
980
|
|
|
973
981
|
## Large codebase / Enterprise
|
|
974
|
-
[CocoIndex](https://github.com/cocoindex-io/cocoindex)
|
|
982
|
+
Teams indexing large or multiple repositories can share [CocoIndex](https://github.com/cocoindex-io/cocoindex) indexes across developers. Enterprise features include deduplication across branches.
|
|
975
983
|
|
|
976
|
-
>
|
|
984
|
+
> If indexing fails with `MDB_MAP_FULL`, raise the LMDB map size. See [`MDB_MAP_FULL: Environment mapsize limit reached`](#mdb_map_full-environment-mapsize-limit-reached) under Troubleshooting.
|
|
977
985
|
|
|
978
|
-
|
|
986
|
+
For help with remote setup, email linghua@cocoindex.io.
|
|
979
987
|
|
|
980
988
|
## Contributing
|
|
981
989
|
|
|
982
|
-
We welcome contributions
|
|
990
|
+
We welcome contributions. This project uses [uv](https://docs.astral.sh/uv/getting-started/installation/) for development. CI checks lint, formatting, types, and tests for every PR. Run the same checks locally before opening a PR; failed pre-commit checks are the most common cause of red CI on incoming PRs.
|
|
983
991
|
|
|
984
992
|
### 1. Install the dev dependencies
|
|
985
993
|
|
|
@@ -14,6 +14,7 @@ import tempfile
|
|
|
14
14
|
import time
|
|
15
15
|
from dataclasses import dataclass
|
|
16
16
|
from pathlib import Path
|
|
17
|
+
from typing import NamedTuple
|
|
17
18
|
|
|
18
19
|
from .settings import user_settings_dir
|
|
19
20
|
|
|
@@ -79,6 +80,45 @@ def daemon_socket_path() -> str:
|
|
|
79
80
|
return str(short)
|
|
80
81
|
|
|
81
82
|
|
|
83
|
+
class SocketIdentity(NamedTuple):
|
|
84
|
+
"""Identifies one socket file, telling a daemon's own apart from one a
|
|
85
|
+
replacement daemon has since bound at the same path (issue #288).
|
|
86
|
+
|
|
87
|
+
``ctime_ns`` is part of it because ext4/overlayfs hand a freed inode
|
|
88
|
+
number straight to the next file: once the original socket is closed and
|
|
89
|
+
unlinked, a replacement's can have the same ``(dev, ino)``.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
dev: int
|
|
93
|
+
ino: int
|
|
94
|
+
ctime_ns: int
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def socket_identity(path: str) -> SocketIdentity | None:
|
|
98
|
+
"""Return the identity of the socket file at *path*, or None if absent. POSIX only."""
|
|
99
|
+
try:
|
|
100
|
+
st = os.stat(path)
|
|
101
|
+
except OSError:
|
|
102
|
+
return None
|
|
103
|
+
return SocketIdentity(st.st_dev, st.st_ino, st.st_ctime_ns)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def unlink_socket_if_owned(path: str, identity: SocketIdentity | None) -> None:
|
|
107
|
+
"""Remove the socket file at *path* only if it is still *identity*.
|
|
108
|
+
|
|
109
|
+
Leaves alone a socket a replacement daemon has bound there since. Not
|
|
110
|
+
atomic — POSIX has no unlink-if-inode — so a replacement binding between
|
|
111
|
+
the stat and the unlink is still lost, but that window is microseconds.
|
|
112
|
+
Best-effort — never raises.
|
|
113
|
+
"""
|
|
114
|
+
if identity is None or socket_identity(path) != identity:
|
|
115
|
+
return
|
|
116
|
+
try:
|
|
117
|
+
os.unlink(path)
|
|
118
|
+
except OSError:
|
|
119
|
+
pass
|
|
120
|
+
|
|
121
|
+
|
|
82
122
|
def daemon_pid_path() -> Path:
|
|
83
123
|
"""Return the path for the daemon's PID file."""
|
|
84
124
|
return daemon_runtime_dir() / "daemon.pid"
|
|
@@ -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.43'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 43)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Public API for writing custom chunkers.
|
|
2
|
+
|
|
3
|
+
Example usage::
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from cocoindex_code.chunking import Chunk, ChunkerFn, TextPosition
|
|
7
|
+
|
|
8
|
+
def my_chunker(path: Path, content: str) -> tuple[str | None, list[Chunk]]:
|
|
9
|
+
pos = TextPosition(byte_offset=0, char_offset=0, line=1, column=0)
|
|
10
|
+
return "mylang", [Chunk(text=content, start=pos, end=pos)]
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import dataclasses as _dataclasses
|
|
16
|
+
import pathlib as _pathlib
|
|
17
|
+
from collections.abc import Callable as _Callable
|
|
18
|
+
|
|
19
|
+
import cocoindex as _coco
|
|
20
|
+
from cocoindex.resources.chunk import Chunk, TextPosition
|
|
21
|
+
|
|
22
|
+
# Callable alias (not Protocol) — consistent with codebase style.
|
|
23
|
+
# language_override=None keeps the language detected by detect_code_language.
|
|
24
|
+
# path is not resolved (no syscall); call path.resolve() inside the chunker if needed.
|
|
25
|
+
ChunkerFn = _Callable[[_pathlib.Path, str], tuple[str | None, list[Chunk]]]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@_dataclasses.dataclass(frozen=True)
|
|
29
|
+
class LoadedChunker:
|
|
30
|
+
"""A chunker the daemon imported from a ``settings.yml`` entry.
|
|
31
|
+
|
|
32
|
+
Its memo key is ``(spec, module_sha256)``, never ``fn`` itself: cocoindex
|
|
33
|
+
fingerprints a function by its module and name only, so an edit to its body
|
|
34
|
+
would go unnoticed, and it cannot fingerprint lambdas or closures at all.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
fn: ChunkerFn
|
|
38
|
+
#: The ``"module.path:callable"`` string from ``settings.yml``.
|
|
39
|
+
spec: str
|
|
40
|
+
#: sha256 of the module file named in ``spec``, as it was when imported. Helper
|
|
41
|
+
#: modules that file imports are not covered.
|
|
42
|
+
module_sha256: str
|
|
43
|
+
|
|
44
|
+
def __coco_memo_key__(self) -> tuple[str, str]:
|
|
45
|
+
return (self.spec, self.module_sha256)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# Keyed by file suffix (e.g. ".toml"). Adding, removing or changing an entry
|
|
49
|
+
# re-processes all files.
|
|
50
|
+
CHUNKER_REGISTRY = _coco.ContextKey[dict[str, LoadedChunker]](
|
|
51
|
+
"chunker_registry", detect_change=True
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
__all__ = ["Chunk", "ChunkerFn", "CHUNKER_REGISTRY", "LoadedChunker", "TextPosition"]
|
|
@@ -26,6 +26,7 @@ from ._version import __version__
|
|
|
26
26
|
from .settings import (
|
|
27
27
|
DEFAULT_ST_MODEL,
|
|
28
28
|
EmbeddingSettings,
|
|
29
|
+
auto_init_refusal,
|
|
29
30
|
cocoindex_db_path,
|
|
30
31
|
default_project_settings,
|
|
31
32
|
find_parent_with_marker,
|
|
@@ -38,6 +39,7 @@ from .settings import (
|
|
|
38
39
|
save_project_settings,
|
|
39
40
|
target_sqlite_db_path,
|
|
40
41
|
user_settings_path,
|
|
42
|
+
user_settings_root_note,
|
|
41
43
|
)
|
|
42
44
|
|
|
43
45
|
app = _typer.Typer(
|
|
@@ -81,13 +83,19 @@ def _apply_host_cwd() -> None:
|
|
|
81
83
|
def require_project_root(*, auto_init: bool = False) -> Path:
|
|
82
84
|
"""Find the project root by walking up from CWD.
|
|
83
85
|
|
|
84
|
-
|
|
86
|
+
Reports missing global settings before a missing project (more fundamental).
|
|
85
87
|
With ``auto_init``, a missing project is initialized with default settings
|
|
86
|
-
instead of failing
|
|
88
|
+
instead of failing, unless that would be the user settings root (refused
|
|
89
|
+
first). Missing global settings run the same interactive model
|
|
87
90
|
setup as ``ccc init`` — but only on a TTY, since picking an embedding model
|
|
88
91
|
is a consequential choice; non-interactive runs (scripts, hooks, agents)
|
|
89
92
|
still exit with code 1 rather than silently committing to a default model.
|
|
90
93
|
"""
|
|
94
|
+
cwd = Path.cwd()
|
|
95
|
+
root = find_project_root(cwd)
|
|
96
|
+
# Decide where auto-init would write, and whether it may, before the interactive
|
|
97
|
+
# model setup below, so a refusal never comes after a wasted prompt.
|
|
98
|
+
new_root = _auto_init_root(cwd) if root is None and auto_init else None
|
|
91
99
|
gs_path = user_settings_path()
|
|
92
100
|
if not gs_path.is_file():
|
|
93
101
|
if auto_init and sys.stdin.isatty():
|
|
@@ -99,27 +107,39 @@ def require_project_root(*, auto_init: bool = False) -> Path:
|
|
|
99
107
|
err=True,
|
|
100
108
|
)
|
|
101
109
|
raise _typer.Exit(code=1)
|
|
102
|
-
|
|
110
|
+
if new_root is not None:
|
|
111
|
+
_create_project_settings(new_root)
|
|
112
|
+
return new_root
|
|
103
113
|
if root is None:
|
|
104
|
-
if auto_init:
|
|
105
|
-
return _auto_init_project(Path.cwd())
|
|
106
114
|
_typer.echo(
|
|
107
115
|
"Error: Not in an initialized project directory.\n"
|
|
108
116
|
"Run `ccc init` in your project root to get started.",
|
|
109
117
|
err=True,
|
|
110
118
|
)
|
|
111
119
|
raise _typer.Exit(code=1)
|
|
120
|
+
_note_if_user_settings_root(root)
|
|
112
121
|
return root
|
|
113
122
|
|
|
114
123
|
|
|
115
|
-
def
|
|
116
|
-
"""
|
|
124
|
+
def _note_if_user_settings_root(root: Path) -> None:
|
|
125
|
+
"""Tell the user when a repo below the user settings root resolves to it."""
|
|
126
|
+
note = user_settings_root_note(root, Path.cwd())
|
|
127
|
+
if note is not None:
|
|
128
|
+
_typer.echo(f"Note: {note}", err=True)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _auto_init_root(cwd: Path) -> Path:
|
|
132
|
+
"""Return where ``ccc index`` may create default project settings, or exit.
|
|
117
133
|
|
|
118
134
|
Anchors at the nearest parent git root when there is one, so running from
|
|
119
135
|
a repo subdirectory initializes the repo root rather than the subdirectory.
|
|
136
|
+
Never the user settings root (see ``auto_init_refusal``).
|
|
120
137
|
"""
|
|
121
138
|
root = find_parent_with_marker(cwd) or cwd
|
|
122
|
-
|
|
139
|
+
refusal = auto_init_refusal(root)
|
|
140
|
+
if refusal is not None:
|
|
141
|
+
_typer.echo(f"Error: {refusal}", err=True)
|
|
142
|
+
raise _typer.Exit(code=1)
|
|
123
143
|
return root
|
|
124
144
|
|
|
125
145
|
|
|
@@ -21,12 +21,15 @@ from typing import NamedTuple
|
|
|
21
21
|
import msgspec
|
|
22
22
|
|
|
23
23
|
from ._daemon_paths import (
|
|
24
|
+
SocketIdentity,
|
|
24
25
|
connection_family,
|
|
25
26
|
daemon_log_path,
|
|
26
27
|
daemon_pid_path,
|
|
27
28
|
daemon_runtime_dir,
|
|
28
29
|
daemon_socket_path,
|
|
29
30
|
read_last_exit_marker,
|
|
31
|
+
socket_identity,
|
|
32
|
+
unlink_socket_if_owned,
|
|
30
33
|
)
|
|
31
34
|
from ._version import __version__
|
|
32
35
|
from .protocol import (
|
|
@@ -642,6 +645,9 @@ def stop_daemon() -> None:
|
|
|
642
645
|
pid = None
|
|
643
646
|
except (FileNotFoundError, ValueError):
|
|
644
647
|
pass
|
|
648
|
+
# The socket file of the daemon being stopped. Cleanup removes only that
|
|
649
|
+
# one, never a replacement daemon's bound there meanwhile (issue #288).
|
|
650
|
+
sock_identity = socket_identity(daemon_socket_path()) if sys.platform != "win32" else None
|
|
645
651
|
|
|
646
652
|
# 1) Graceful StopRequest via socket (bypass auto-start)
|
|
647
653
|
try:
|
|
@@ -677,17 +683,17 @@ def stop_daemon() -> None:
|
|
|
677
683
|
except (ProcessLookupError, PermissionError):
|
|
678
684
|
pass
|
|
679
685
|
|
|
680
|
-
_cleanup_stale_files(pid_path, pid)
|
|
686
|
+
_cleanup_stale_files(pid_path, pid, sock_identity)
|
|
681
687
|
|
|
682
688
|
|
|
683
|
-
def _cleanup_stale_files(
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
689
|
+
def _cleanup_stale_files(
|
|
690
|
+
pid_path: Path, pid: int | None, sock_identity: SocketIdentity | None
|
|
691
|
+
) -> None:
|
|
692
|
+
"""Remove socket and PID file after the daemon has exited.
|
|
693
|
+
|
|
694
|
+
The socket file is removed only while it is still *sock_identity*.
|
|
695
|
+
"""
|
|
696
|
+
unlink_socket_if_owned(daemon_socket_path(), sock_identity)
|
|
691
697
|
if pid is not None:
|
|
692
698
|
try:
|
|
693
699
|
stored = pid_path.read_text().strip()
|
|
@@ -3,19 +3,25 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
|
+
import contextvars
|
|
7
|
+
import hashlib
|
|
6
8
|
import importlib
|
|
7
9
|
import logging
|
|
8
10
|
import os
|
|
9
11
|
import signal
|
|
12
|
+
import socket
|
|
10
13
|
import sys
|
|
11
14
|
import threading
|
|
12
15
|
import time
|
|
13
16
|
import traceback
|
|
17
|
+
import weakref
|
|
14
18
|
from collections.abc import AsyncIterator, Callable
|
|
19
|
+
from concurrent.futures import Future, ThreadPoolExecutor
|
|
15
20
|
from datetime import timedelta
|
|
16
21
|
from multiprocessing.connection import Client, Connection, Listener
|
|
17
22
|
from pathlib import Path
|
|
18
|
-
from
|
|
23
|
+
from types import ModuleType
|
|
24
|
+
from typing import Any, ParamSpec, TypeVar
|
|
19
25
|
|
|
20
26
|
from ._daemon_paths import (
|
|
21
27
|
clear_last_exit_marker,
|
|
@@ -24,10 +30,12 @@ from ._daemon_paths import (
|
|
|
24
30
|
daemon_pid_path,
|
|
25
31
|
daemon_runtime_dir,
|
|
26
32
|
daemon_socket_path,
|
|
33
|
+
socket_identity,
|
|
34
|
+
unlink_socket_if_owned,
|
|
27
35
|
write_last_exit_marker,
|
|
28
36
|
)
|
|
29
37
|
from ._version import __version__
|
|
30
|
-
from .chunking import
|
|
38
|
+
from .chunking import LoadedChunker
|
|
31
39
|
from .embedder_params import resolve_embedder_params
|
|
32
40
|
from .project import Project
|
|
33
41
|
from .protocol import (
|
|
@@ -99,22 +107,41 @@ def _build_backward_compat_warning(
|
|
|
99
107
|
)
|
|
100
108
|
|
|
101
109
|
|
|
102
|
-
|
|
103
|
-
|
|
110
|
+
# sha256 of each chunker module's file, taken when the daemon first got the module.
|
|
111
|
+
# importlib returns the already loaded module on later imports (another project, or
|
|
112
|
+
# the same one after `ccc reset`), and that module still runs the code that was
|
|
113
|
+
# loaded, whatever the file holds now.
|
|
114
|
+
_chunker_module_sha256: weakref.WeakKeyDictionary[ModuleType, str] = weakref.WeakKeyDictionary()
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _module_sha256(mod: ModuleType) -> str:
|
|
118
|
+
digest = _chunker_module_sha256.get(mod)
|
|
119
|
+
if digest is None:
|
|
120
|
+
# Builtin modules have no file; their code only changes with Python itself.
|
|
121
|
+
file = getattr(mod, "__file__", None)
|
|
122
|
+
digest = hashlib.sha256(Path(file).read_bytes()).hexdigest() if file else ""
|
|
123
|
+
_chunker_module_sha256[mod] = digest
|
|
124
|
+
return digest
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _resolve_chunker_registry(mappings: list[ChunkerMapping]) -> dict[str, LoadedChunker]:
|
|
128
|
+
"""Import the chunkers from ``ChunkerMapping`` settings entries, keyed by suffix.
|
|
104
129
|
|
|
105
130
|
Each ``mapping.module`` must be a ``"module.path:callable"`` string importable
|
|
106
|
-
from the current environment.
|
|
131
|
+
from the current environment. Each chunker records that string and a hash of
|
|
132
|
+
the module file it names, which is what makes a chunker edit re-process files.
|
|
107
133
|
"""
|
|
108
|
-
registry: dict[str,
|
|
134
|
+
registry: dict[str, LoadedChunker] = {}
|
|
109
135
|
for cm in mappings:
|
|
110
136
|
module_path, _, attr = cm.module.partition(":")
|
|
111
137
|
if not attr:
|
|
112
138
|
raise ValueError(f"chunker module {cm.module!r} must use 'module.path:callable' format")
|
|
113
139
|
mod = importlib.import_module(module_path)
|
|
140
|
+
module_sha256 = _module_sha256(mod)
|
|
114
141
|
fn = getattr(mod, attr)
|
|
115
142
|
if not callable(fn):
|
|
116
143
|
raise ValueError(f"chunker {cm.module!r}: {attr!r} is not callable")
|
|
117
|
-
registry[f".{cm.ext}"] = fn
|
|
144
|
+
registry[f".{cm.ext}"] = LoadedChunker(fn, spec=cm.module, module_sha256=module_sha256)
|
|
118
145
|
return registry
|
|
119
146
|
|
|
120
147
|
|
|
@@ -161,13 +188,12 @@ class ProjectRegistry:
|
|
|
161
188
|
if project_root not in self._projects:
|
|
162
189
|
root = Path(project_root)
|
|
163
190
|
project_settings = load_project_settings(root)
|
|
164
|
-
chunker_registry = _resolve_chunker_registry(project_settings.chunkers)
|
|
165
191
|
project = await Project.create(
|
|
166
192
|
root,
|
|
167
193
|
self._embedder,
|
|
168
194
|
indexing_params=self.indexing_params,
|
|
169
195
|
query_params=self.query_params,
|
|
170
|
-
chunker_registry=
|
|
196
|
+
chunker_registry=_resolve_chunker_registry(project_settings.chunkers),
|
|
171
197
|
clear_mps_cache_after_index=self._clear_mps_cache_after_index,
|
|
172
198
|
)
|
|
173
199
|
self._projects[project_root] = project
|
|
@@ -613,6 +639,77 @@ async def _dispatch(
|
|
|
613
639
|
# Daemon main
|
|
614
640
|
# ---------------------------------------------------------------------------
|
|
615
641
|
|
|
642
|
+
_P = ParamSpec("_P")
|
|
643
|
+
_T = TypeVar("_T")
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
class EmptyContextThreadPoolExecutor(ThreadPoolExecutor):
|
|
647
|
+
"""Thread pool whose worker threads start in an empty ``contextvars`` context.
|
|
648
|
+
|
|
649
|
+
``submit()`` starts a worker thread when no idle one is free. On free-threaded
|
|
650
|
+
Python 3.14 (``sys.flags.thread_inherit_context``) that thread starts in a copy
|
|
651
|
+
of the submitter's context and keeps it for its whole life. A copy taken inside
|
|
652
|
+
a CocoIndex component references the project's CocoIndex environment, which is
|
|
653
|
+
only released once nothing references it, so it would outlive the project and
|
|
654
|
+
opening the project again in this process (as after ``ccc reset``) would fail.
|
|
655
|
+
Submitting from an empty context gives worker threads the empty context they
|
|
656
|
+
start with on GIL builds.
|
|
657
|
+
"""
|
|
658
|
+
|
|
659
|
+
def submit(self, fn: Callable[_P, _T], /, *args: _P.args, **kwargs: _P.kwargs) -> Future[_T]:
|
|
660
|
+
return contextvars.Context().run(super().submit, fn, *args, **kwargs)
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
if sys.platform != "win32": # Windows uses named pipes via Listener
|
|
664
|
+
|
|
665
|
+
class _UnixListener:
|
|
666
|
+
"""POSIX stand-in for ``multiprocessing.connection.Listener`` that removes
|
|
667
|
+
its socket file on ``close()`` only while the path still names it.
|
|
668
|
+
|
|
669
|
+
The stdlib listener unlinks its address on ``close()`` unconditionally, so
|
|
670
|
+
a displaced daemon shutting down would delete the socket a replacement
|
|
671
|
+
daemon had since bound at the same path, cutting clients off from it
|
|
672
|
+
(issue #288).
|
|
673
|
+
"""
|
|
674
|
+
|
|
675
|
+
def __init__(self, path: str) -> None:
|
|
676
|
+
# Take over the path from a crashed or displaced daemon's socket file,
|
|
677
|
+
# which would otherwise make bind() fail.
|
|
678
|
+
try:
|
|
679
|
+
os.unlink(path)
|
|
680
|
+
except FileNotFoundError:
|
|
681
|
+
pass
|
|
682
|
+
self._path = path
|
|
683
|
+
self._sock = socket.socket(socket.AF_UNIX)
|
|
684
|
+
try:
|
|
685
|
+
self._sock.setblocking(True)
|
|
686
|
+
self._sock.bind(path)
|
|
687
|
+
self._sock.listen(1) # same backlog as the stdlib Listener
|
|
688
|
+
except OSError:
|
|
689
|
+
self._sock.close()
|
|
690
|
+
raise
|
|
691
|
+
# A replacement binding the path in the instant between bind() and
|
|
692
|
+
# this stat would be recorded as ours; bind() itself can't report the
|
|
693
|
+
# file's identity, and the window is microseconds.
|
|
694
|
+
self._identity = socket_identity(path)
|
|
695
|
+
|
|
696
|
+
def accept(self) -> Connection:
|
|
697
|
+
conn, _ = self._sock.accept()
|
|
698
|
+
conn.setblocking(True)
|
|
699
|
+
return Connection(conn.detach())
|
|
700
|
+
|
|
701
|
+
def close(self) -> None:
|
|
702
|
+
# Unlink while the socket is still open: an open socket pins its
|
|
703
|
+
# inode, so a replacement's file can't have been given the same one.
|
|
704
|
+
unlink_socket_if_owned(self._path, self._identity)
|
|
705
|
+
# On Linux only shutdown() wakes a thread blocked in accept(); macOS
|
|
706
|
+
# wakes it on close() and rejects shutdown() of a listening socket.
|
|
707
|
+
try:
|
|
708
|
+
self._sock.shutdown(socket.SHUT_RDWR)
|
|
709
|
+
except OSError:
|
|
710
|
+
pass
|
|
711
|
+
self._sock.close()
|
|
712
|
+
|
|
616
713
|
|
|
617
714
|
def run_daemon(
|
|
618
715
|
*,
|
|
@@ -695,16 +792,14 @@ def run_daemon(
|
|
|
695
792
|
)
|
|
696
793
|
|
|
697
794
|
sock_path = daemon_socket_path()
|
|
698
|
-
if sys.platform
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
pass
|
|
703
|
-
|
|
704
|
-
listener = Listener(sock_path, family=connection_family())
|
|
795
|
+
if sys.platform == "win32":
|
|
796
|
+
listener = Listener(sock_path, family=connection_family())
|
|
797
|
+
else:
|
|
798
|
+
listener = _UnixListener(sock_path)
|
|
705
799
|
logger.info("Listening on %s", sock_path)
|
|
706
800
|
|
|
707
801
|
loop = asyncio.new_event_loop()
|
|
802
|
+
loop.set_default_executor(EmptyContextThreadPoolExecutor())
|
|
708
803
|
tasks: set[asyncio.Task[Any]] = set()
|
|
709
804
|
|
|
710
805
|
reaper = IdleReaper(
|
|
@@ -816,14 +911,17 @@ def run_daemon(
|
|
|
816
911
|
# absence is how the client detects a crash.
|
|
817
912
|
write_last_exit_marker(pid=os.getpid(), reason=shutdown_reason or "unknown")
|
|
818
913
|
|
|
819
|
-
# 1. Stop accepting new connections
|
|
820
|
-
#
|
|
821
|
-
# (
|
|
822
|
-
#
|
|
823
|
-
#
|
|
824
|
-
#
|
|
825
|
-
#
|
|
826
|
-
#
|
|
914
|
+
# 1. Stop accepting new connections and remove the socket file (if a
|
|
915
|
+
# replacement daemon hasn't taken over the path). On Windows a
|
|
916
|
+
# blocked accept() holds an open pipe instance that listener.close()
|
|
917
|
+
# can't release (it only closes the queued next-instance handle),
|
|
918
|
+
# which would keep the named pipe alive after exit — wake it with a
|
|
919
|
+
# dummy connection so the accept thread closes it and exits. POSIX
|
|
920
|
+
# doesn't need the wake-up (listener.close() makes accept() raise),
|
|
921
|
+
# but it's harmless there and keeps this path exercised on every
|
|
922
|
+
# platform rather than only on Windows CI. If a replacement daemon
|
|
923
|
+
# owns the path, the wake-up reaches it instead: one extra
|
|
924
|
+
# connection, which only counts as activity there.
|
|
827
925
|
shutting_down.set()
|
|
828
926
|
try:
|
|
829
927
|
Client(sock_path, family=connection_family()).close()
|
|
@@ -843,12 +941,7 @@ def run_daemon(
|
|
|
843
941
|
registry.close_all()
|
|
844
942
|
loop.close()
|
|
845
943
|
|
|
846
|
-
# 4. Remove
|
|
847
|
-
if sys.platform != "win32":
|
|
848
|
-
try:
|
|
849
|
-
Path(sock_path).unlink(missing_ok=True)
|
|
850
|
-
except Exception:
|
|
851
|
-
pass
|
|
944
|
+
# 4. Remove the PID file.
|
|
852
945
|
try:
|
|
853
946
|
stored = pid_path.read_text().strip()
|
|
854
947
|
if stored == str(os.getpid()):
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
from collections.abc import Mapping
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
|
|
7
8
|
import cocoindex as coco
|
|
@@ -35,8 +36,13 @@ splitter = RecursiveSplitter()
|
|
|
35
36
|
async def process_file(
|
|
36
37
|
file: localfs.File,
|
|
37
38
|
table: sqlite.TableTarget[CodeChunk],
|
|
39
|
+
language_overrides: Mapping[str, str],
|
|
38
40
|
) -> None:
|
|
39
|
-
"""Process a single file: chunk, embed, and store.
|
|
41
|
+
"""Process a single file: chunk, embed, and store.
|
|
42
|
+
|
|
43
|
+
``language_overrides`` maps a file suffix (``".inc"``) to a language. It is an
|
|
44
|
+
argument rather than read from settings here so that it is part of the memo key.
|
|
45
|
+
"""
|
|
40
46
|
embedder = coco.use_context(EMBEDDER)
|
|
41
47
|
indexing_params = coco.use_context(INDEXING_EMBED_PARAMS)
|
|
42
48
|
|
|
@@ -49,11 +55,8 @@ async def process_file(
|
|
|
49
55
|
return
|
|
50
56
|
|
|
51
57
|
suffix = file.file_path.path.suffix
|
|
52
|
-
project_root = coco.use_context(CODEBASE_DIR)
|
|
53
|
-
ps = load_project_settings(project_root)
|
|
54
|
-
ext_lang_map = {f".{lo.ext}": lo.lang for lo in ps.language_overrides}
|
|
55
58
|
language = (
|
|
56
|
-
|
|
59
|
+
language_overrides.get(suffix)
|
|
57
60
|
or detect_code_language(filename=file.file_path.path.name)
|
|
58
61
|
or "text"
|
|
59
62
|
)
|
|
@@ -61,7 +64,7 @@ async def process_file(
|
|
|
61
64
|
chunker_registry = coco.use_context(CHUNKER_REGISTRY)
|
|
62
65
|
chunker = chunker_registry.get(suffix)
|
|
63
66
|
if chunker is not None:
|
|
64
|
-
language_override, chunks = chunker(Path(file.file_path.path), content)
|
|
67
|
+
language_override, chunks = chunker.fn(Path(file.file_path.path), content)
|
|
65
68
|
if language_override is not None:
|
|
66
69
|
language = language_override
|
|
67
70
|
else:
|
|
@@ -120,6 +123,11 @@ async def indexer_main() -> None:
|
|
|
120
123
|
path_matcher=matcher,
|
|
121
124
|
)
|
|
122
125
|
|
|
126
|
+
language_overrides = {f".{lo.ext}": lo.lang for lo in ps.language_overrides}
|
|
123
127
|
await coco.mount_each(
|
|
124
|
-
coco.component_subpath(coco.Symbol("process_file")),
|
|
128
|
+
coco.component_subpath(coco.Symbol("process_file")),
|
|
129
|
+
process_file,
|
|
130
|
+
files.items(),
|
|
131
|
+
table,
|
|
132
|
+
language_overrides,
|
|
125
133
|
)
|
|
@@ -3,16 +3,19 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
|
+
import contextvars
|
|
6
7
|
import logging
|
|
7
8
|
import sqlite3
|
|
8
|
-
|
|
9
|
+
import weakref
|
|
10
|
+
from collections.abc import AsyncIterator, Callable, Mapping
|
|
11
|
+
from concurrent.futures import Future, ThreadPoolExecutor
|
|
9
12
|
from pathlib import Path
|
|
10
|
-
from typing import Any
|
|
13
|
+
from typing import Any, ParamSpec, TypeVar
|
|
11
14
|
|
|
12
15
|
import cocoindex as coco
|
|
13
16
|
from cocoindex.connectors import sqlite as coco_sqlite
|
|
14
17
|
|
|
15
|
-
from .chunking import CHUNKER_REGISTRY,
|
|
18
|
+
from .chunking import CHUNKER_REGISTRY, LoadedChunker
|
|
16
19
|
from .indexer import indexer_main
|
|
17
20
|
from .protocol import (
|
|
18
21
|
IndexingProgress,
|
|
@@ -45,6 +48,36 @@ from .shared import (
|
|
|
45
48
|
|
|
46
49
|
logger = logging.getLogger(__name__)
|
|
47
50
|
|
|
51
|
+
_P = ParamSpec("_P")
|
|
52
|
+
_T = TypeVar("_T")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class _EmptyContextThreadPoolExecutor(ThreadPoolExecutor):
|
|
56
|
+
"""A ``ThreadPoolExecutor`` whose worker threads start with an empty context.
|
|
57
|
+
|
|
58
|
+
On free-threaded Python a new thread starts with a copy of the context of the code
|
|
59
|
+
that started it (``sys.flags.thread_inherit_context``). The event loop's default
|
|
60
|
+
executor starts its workers lazily, often inside a CocoIndex component (the LocalFS
|
|
61
|
+
walker calls ``run_in_executor``). Such a worker keeps that component's context, which
|
|
62
|
+
references the project's ``Environment`` and through it the LMDB env, for as long as
|
|
63
|
+
the loop lives. The env then stays open after the project is closed, and loading the
|
|
64
|
+
project again fails with "environment already open in this program".
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def submit(self, fn: Callable[_P, _T], /, *args: _P.args, **kwargs: _P.kwargs) -> Future[_T]:
|
|
68
|
+
# Worker threads are started from submit().
|
|
69
|
+
return contextvars.Context().run(super().submit, fn, *args, **kwargs)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# Loops whose default executor is already an _EmptyContextThreadPoolExecutor.
|
|
73
|
+
_loops_with_empty_context_executor: weakref.WeakSet[asyncio.AbstractEventLoop] = weakref.WeakSet()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _use_empty_context_default_executor(loop: asyncio.AbstractEventLoop) -> None:
|
|
77
|
+
if loop not in _loops_with_empty_context_executor:
|
|
78
|
+
loop.set_default_executor(_EmptyContextThreadPoolExecutor(thread_name_prefix="asyncio"))
|
|
79
|
+
_loops_with_empty_context_executor.add(loop)
|
|
80
|
+
|
|
48
81
|
|
|
49
82
|
class Project:
|
|
50
83
|
_env: coco.Environment
|
|
@@ -281,7 +314,7 @@ class Project:
|
|
|
281
314
|
embedder: Embedder,
|
|
282
315
|
indexing_params: dict[str, Any],
|
|
283
316
|
query_params: dict[str, Any],
|
|
284
|
-
chunker_registry:
|
|
317
|
+
chunker_registry: Mapping[str, LoadedChunker] | None = None,
|
|
285
318
|
clear_mps_cache_after_index: bool = False,
|
|
286
319
|
) -> Project:
|
|
287
320
|
"""Create a project with explicit embedder and per-call params.
|
|
@@ -299,7 +332,7 @@ class Project:
|
|
|
299
332
|
query_params: Extra kwargs spread into ``embedder.embed()`` for the
|
|
300
333
|
query side.
|
|
301
334
|
chunker_registry: Optional mapping of file suffix (e.g. ``".toml"``)
|
|
302
|
-
to a ``
|
|
335
|
+
to a ``LoadedChunker``. When a suffix matches, the registered
|
|
303
336
|
chunker is called instead of the built-in splitter.
|
|
304
337
|
clear_mps_cache_after_index: Whether to release unused MPS allocator
|
|
305
338
|
memory in CocoIndex's GPU subprocess after each index run.
|
|
@@ -315,6 +348,10 @@ class Project:
|
|
|
315
348
|
|
|
316
349
|
settings = coco.Settings.from_env(cocoindex_db)
|
|
317
350
|
|
|
351
|
+
# The environment runs its components on the running loop. Keep that loop's
|
|
352
|
+
# executor threads from holding on to the environment once this project is closed.
|
|
353
|
+
_use_empty_context_default_executor(asyncio.get_running_loop())
|
|
354
|
+
|
|
318
355
|
context = coco.ContextProvider()
|
|
319
356
|
context.provide(CODEBASE_DIR, project_root)
|
|
320
357
|
context.provide(SQLITE_DB, coco_sqlite.connect(str(target_sqlite_db), load_vec=True))
|
|
@@ -18,7 +18,7 @@ from mcp.server.mcpserver import MCPServer
|
|
|
18
18
|
from pydantic import BaseModel, Field
|
|
19
19
|
|
|
20
20
|
from ._version import __version__
|
|
21
|
-
from .settings import DaemonSettings, load_user_settings
|
|
21
|
+
from .settings import DaemonSettings, load_user_settings, user_settings_root_note
|
|
22
22
|
|
|
23
23
|
_MCP_INSTRUCTIONS = (
|
|
24
24
|
"Code search and codebase understanding tools."
|
|
@@ -56,6 +56,13 @@ class SearchResultModel(BaseModel):
|
|
|
56
56
|
message: str | None = None
|
|
57
57
|
|
|
58
58
|
|
|
59
|
+
def _with_note(message: str | None, note: str | None) -> str | None:
|
|
60
|
+
"""Append *note* to a result message; MCP clients hide the server's stderr."""
|
|
61
|
+
if note is None:
|
|
62
|
+
return message
|
|
63
|
+
return f"{message}\nNote: {note}" if message else f"Note: {note}"
|
|
64
|
+
|
|
65
|
+
|
|
59
66
|
# === Daemon-backed MCP server factory ===
|
|
60
67
|
|
|
61
68
|
|
|
@@ -125,7 +132,9 @@ def create_mcp_server(project_root: str) -> MCPServer:
|
|
|
125
132
|
from . import client as _client
|
|
126
133
|
|
|
127
134
|
loop = asyncio.get_event_loop()
|
|
135
|
+
note: str | None = None
|
|
128
136
|
try:
|
|
137
|
+
note = user_settings_root_note(Path(project_root), Path.cwd())
|
|
129
138
|
if refresh_index:
|
|
130
139
|
await loop.run_in_executor(None, lambda: _client.index(project_root))
|
|
131
140
|
resp = await loop.run_in_executor(
|
|
@@ -154,10 +163,12 @@ def create_mcp_server(project_root: str) -> MCPServer:
|
|
|
154
163
|
],
|
|
155
164
|
total_returned=resp.total_returned,
|
|
156
165
|
offset=resp.offset,
|
|
157
|
-
message=resp.message,
|
|
166
|
+
message=_with_note(resp.message, note),
|
|
158
167
|
)
|
|
159
168
|
except Exception as e:
|
|
160
|
-
return SearchResultModel(
|
|
169
|
+
return SearchResultModel(
|
|
170
|
+
success=False, message=_with_note(f"Query failed: {e!s}", note)
|
|
171
|
+
)
|
|
161
172
|
|
|
162
173
|
return mcp
|
|
163
174
|
|
|
@@ -225,10 +236,12 @@ def main() -> None:
|
|
|
225
236
|
Auto-detects/creates settings from env vars, then delegates to daemon.
|
|
226
237
|
"""
|
|
227
238
|
import argparse
|
|
239
|
+
import sys
|
|
228
240
|
|
|
229
241
|
from .settings import (
|
|
230
242
|
EmbeddingSettings,
|
|
231
243
|
LanguageOverride,
|
|
244
|
+
auto_init_refusal,
|
|
232
245
|
default_project_settings,
|
|
233
246
|
default_user_settings,
|
|
234
247
|
find_legacy_project_root,
|
|
@@ -251,12 +264,14 @@ def main() -> None:
|
|
|
251
264
|
# --- Discover project root ---
|
|
252
265
|
cwd = Path.cwd()
|
|
253
266
|
project_root = find_project_root(cwd)
|
|
267
|
+
root_from_env = False
|
|
254
268
|
|
|
255
269
|
if project_root is None:
|
|
256
270
|
# Try env var
|
|
257
271
|
env_root = os.environ.get("COCOINDEX_CODE_ROOT_PATH")
|
|
258
272
|
if env_root:
|
|
259
273
|
project_root = Path(env_root).resolve()
|
|
274
|
+
root_from_env = True
|
|
260
275
|
else:
|
|
261
276
|
# Use marker-based discovery
|
|
262
277
|
legacy_root = find_legacy_project_root(cwd)
|
|
@@ -265,6 +280,14 @@ def main() -> None:
|
|
|
265
280
|
# --- Auto-create project settings if needed ---
|
|
266
281
|
proj_settings_file = project_settings_path(project_root)
|
|
267
282
|
if not proj_settings_file.is_file():
|
|
283
|
+
# COCOINDEX_CODE_ROOT_PATH is a deliberate choice, like `ccc init`: only a
|
|
284
|
+
# root found by discovery is refused.
|
|
285
|
+
refusal = None if root_from_env else auto_init_refusal(project_root)
|
|
286
|
+
if refusal is not None:
|
|
287
|
+
sys.exit(
|
|
288
|
+
f"Error: {refusal} To serve a project from here, set"
|
|
289
|
+
" COCOINDEX_CODE_ROOT_PATH to its directory."
|
|
290
|
+
)
|
|
268
291
|
ps = default_project_settings()
|
|
269
292
|
|
|
270
293
|
# Migrate COCOINDEX_CODE_EXCLUDED_PATTERNS
|
|
@@ -320,8 +343,6 @@ def main() -> None:
|
|
|
320
343
|
from .protocol import IndexingProgress
|
|
321
344
|
|
|
322
345
|
if args.command == "index":
|
|
323
|
-
import sys
|
|
324
|
-
|
|
325
346
|
from rich.console import Console
|
|
326
347
|
from rich.live import Live
|
|
327
348
|
from rich.spinner import Spinner
|
|
@@ -354,6 +354,54 @@ def project_settings_path(project_root: Path) -> Path:
|
|
|
354
354
|
return project_root / _SETTINGS_DIR_NAME / _SETTINGS_FILE_NAME
|
|
355
355
|
|
|
356
356
|
|
|
357
|
+
def is_user_settings_root(directory: Path) -> bool:
|
|
358
|
+
"""Return whether *directory*'s ``.cocoindex_code`` is the user settings dir.
|
|
359
|
+
|
|
360
|
+
That is ``$HOME`` by default, ``/x`` with ``COCOINDEX_CODE_DIR=/x/.cocoindex_code``
|
|
361
|
+
(the Docker image's layout), and no directory with ``COCOINDEX_CODE_DIR=/x/ccc``.
|
|
362
|
+
A project there covers every directory below it, so ccc never creates one
|
|
363
|
+
automatically; ``ccc init`` run there by hand still does.
|
|
364
|
+
"""
|
|
365
|
+
return (directory / _SETTINGS_DIR_NAME).resolve() == user_settings_dir().resolve()
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def auto_init_refusal(directory: Path) -> str | None:
|
|
369
|
+
"""Return why *directory* must not be initialized automatically, or ``None``."""
|
|
370
|
+
if not is_user_settings_root(directory):
|
|
371
|
+
return None
|
|
372
|
+
shown = format_path_for_display(directory)
|
|
373
|
+
return (
|
|
374
|
+
f"{shown} has no project settings, and ccc does not create them there"
|
|
375
|
+
f" automatically: its {_SETTINGS_DIR_NAME} directory is ccc's user settings"
|
|
376
|
+
" directory, so a project there would cover every directory below it."
|
|
377
|
+
" Run ccc from inside a project instead. To index all of"
|
|
378
|
+
f" {shown}, run `ccc init` there."
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def user_settings_root_note(root: Path, start: Path) -> str | None:
|
|
383
|
+
"""Explain why a repo containing *start* resolves to the user settings root.
|
|
384
|
+
|
|
385
|
+
Such a project is usually left over from an older ccc that auto-initialized
|
|
386
|
+
``$HOME``: every repo below it without settings of its own resolves to it.
|
|
387
|
+
Returns ``None`` unless *root* is the user settings root and *start* lies in
|
|
388
|
+
a repo below it.
|
|
389
|
+
"""
|
|
390
|
+
if not is_user_settings_root(root):
|
|
391
|
+
return None
|
|
392
|
+
repo = find_parent_with_marker(start)
|
|
393
|
+
if repo is None or repo == root:
|
|
394
|
+
return None
|
|
395
|
+
shown_root = format_path_for_display(root)
|
|
396
|
+
shown_repo = format_path_for_display(repo)
|
|
397
|
+
return (
|
|
398
|
+
f"{shown_repo} has no ccc project of its own, so the project in use is"
|
|
399
|
+
f" {shown_root}, whose .cocoindex_code directory is also ccc's user settings"
|
|
400
|
+
f" directory. If {shown_root} is not meant to be a project, run"
|
|
401
|
+
f" `ccc reset --all` there, then `ccc init` in {shown_repo}."
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
|
|
357
405
|
def find_project_root(start: Path) -> Path | None:
|
|
358
406
|
"""Walk up from *start* looking for ``.cocoindex_code/settings.yml``.
|
|
359
407
|
|
|
@@ -37,7 +37,8 @@ Embedder = Union[
|
|
|
37
37
|
EMBEDDER = coco.ContextKey[Embedder]("embedder", detect_change=True)
|
|
38
38
|
SQLITE_DB = coco.ContextKey[sqlite.ManagedConnection]("index_db")
|
|
39
39
|
CODEBASE_DIR = coco.ContextKey[pathlib.Path]("codebase")
|
|
40
|
-
INDEXING_EMBED_PARAMS = coco.ContextKey[dict[str, Any]]("indexing_embed_params")
|
|
40
|
+
INDEXING_EMBED_PARAMS = coco.ContextKey[dict[str, Any]]("indexing_embed_params", detect_change=True)
|
|
41
|
+
# Read only at query time, outside any memoized function: nothing to invalidate.
|
|
41
42
|
QUERY_EMBED_PARAMS = coco.ContextKey[dict[str, Any]]("query_embed_params")
|
|
42
43
|
|
|
43
44
|
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
"""Public API for writing custom chunkers.
|
|
2
|
-
|
|
3
|
-
Example usage::
|
|
4
|
-
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
from cocoindex_code.chunking import Chunk, ChunkerFn, TextPosition
|
|
7
|
-
|
|
8
|
-
def my_chunker(path: Path, content: str) -> tuple[str | None, list[Chunk]]:
|
|
9
|
-
pos = TextPosition(byte_offset=0, char_offset=0, line=1, column=0)
|
|
10
|
-
return "mylang", [Chunk(text=content, start=pos, end=pos)]
|
|
11
|
-
"""
|
|
12
|
-
|
|
13
|
-
from __future__ import annotations
|
|
14
|
-
|
|
15
|
-
import pathlib as _pathlib
|
|
16
|
-
from collections.abc import Callable as _Callable
|
|
17
|
-
|
|
18
|
-
import cocoindex as _coco
|
|
19
|
-
from cocoindex.resources.chunk import Chunk, TextPosition
|
|
20
|
-
|
|
21
|
-
# Callable alias (not Protocol) — consistent with codebase style.
|
|
22
|
-
# language_override=None keeps the language detected by detect_code_language.
|
|
23
|
-
# path is not resolved (no syscall); call path.resolve() inside the chunker if needed.
|
|
24
|
-
ChunkerFn = _Callable[[_pathlib.Path, str], tuple[str | None, list[Chunk]]]
|
|
25
|
-
|
|
26
|
-
# tracked=False: callables are not fingerprint-able; daemon restart re-indexes anyway.
|
|
27
|
-
CHUNKER_REGISTRY = _coco.ContextKey[dict[str, ChunkerFn]]("chunker_registry")
|
|
28
|
-
|
|
29
|
-
__all__ = ["Chunk", "ChunkerFn", "CHUNKER_REGISTRY", "TextPosition"]
|
|
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
|