cocoindex-code 0.2.23__tar.gz → 0.2.24__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.23 → cocoindex_code-0.2.24}/.gitignore +4 -0
- {cocoindex_code-0.2.23 → cocoindex_code-0.2.24}/PKG-INFO +89 -27
- {cocoindex_code-0.2.23 → cocoindex_code-0.2.24}/README.md +88 -26
- {cocoindex_code-0.2.23 → cocoindex_code-0.2.24}/pyproject.toml +4 -1
- cocoindex_code-0.2.24/src/cocoindex_code/_daemon_paths.py +60 -0
- {cocoindex_code-0.2.23 → cocoindex_code-0.2.24}/src/cocoindex_code/_version.py +2 -2
- {cocoindex_code-0.2.23 → cocoindex_code-0.2.24}/src/cocoindex_code/cli.py +47 -14
- {cocoindex_code-0.2.23 → cocoindex_code-0.2.24}/src/cocoindex_code/client.py +40 -14
- {cocoindex_code-0.2.23 → cocoindex_code-0.2.24}/src/cocoindex_code/daemon.py +54 -20
- {cocoindex_code-0.2.23 → cocoindex_code-0.2.24}/src/cocoindex_code/protocol.py +1 -0
- {cocoindex_code-0.2.23 → cocoindex_code-0.2.24}/src/cocoindex_code/settings.py +83 -29
- cocoindex_code-0.2.23/src/cocoindex_code/_daemon_paths.py +0 -44
- {cocoindex_code-0.2.23 → cocoindex_code-0.2.24}/LICENSE +0 -0
- {cocoindex_code-0.2.23 → cocoindex_code-0.2.24}/src/cocoindex_code/__init__.py +0 -0
- {cocoindex_code-0.2.23 → cocoindex_code-0.2.24}/src/cocoindex_code/__main__.py +0 -0
- {cocoindex_code-0.2.23 → cocoindex_code-0.2.24}/src/cocoindex_code/chunking.py +0 -0
- {cocoindex_code-0.2.23 → cocoindex_code-0.2.24}/src/cocoindex_code/indexer.py +0 -0
- {cocoindex_code-0.2.23 → cocoindex_code-0.2.24}/src/cocoindex_code/litellm_embedder.py +0 -0
- {cocoindex_code-0.2.23 → cocoindex_code-0.2.24}/src/cocoindex_code/project.py +0 -0
- {cocoindex_code-0.2.23 → cocoindex_code-0.2.24}/src/cocoindex_code/query.py +0 -0
- {cocoindex_code-0.2.23 → cocoindex_code-0.2.24}/src/cocoindex_code/schema.py +0 -0
- {cocoindex_code-0.2.23 → cocoindex_code-0.2.24}/src/cocoindex_code/server.py +0 -0
- {cocoindex_code-0.2.23 → cocoindex_code-0.2.24}/src/cocoindex_code/shared.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cocoindex-code
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.24
|
|
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
|
|
@@ -242,33 +242,79 @@ The recommended approach is a **persistent container**: start it once, and use
|
|
|
242
242
|
`docker exec` to run CLI commands or connect MCP sessions to it. The daemon
|
|
243
243
|
inside stays warm across sessions, so the embedding model is loaded only once.
|
|
244
244
|
|
|
245
|
-
###
|
|
245
|
+
### Quick start — `docker compose up -d`
|
|
246
|
+
|
|
247
|
+
Grab [`docker/docker-compose.yml`](./docker/docker-compose.yml) from this repo and run:
|
|
248
|
+
|
|
249
|
+
```bash
|
|
250
|
+
# macOS / Windows
|
|
251
|
+
docker compose up -d
|
|
252
|
+
|
|
253
|
+
# Linux (aligns file ownership on bind-mounted paths with your host user)
|
|
254
|
+
PUID=$(id -u) PGID=$(id -g) docker compose up -d
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
By default your home directory is mounted into the container (set
|
|
258
|
+
`COCOINDEX_HOST_WORKSPACE` to narrow this to a specific code folder). Index
|
|
259
|
+
data and the embedding model cache persist in a Docker volume across
|
|
260
|
+
restarts. Your global settings file at `$HOME/.cocoindex_code/global_settings.yml`
|
|
261
|
+
is visible and editable on the host; edits take effect on your next `ccc` command.
|
|
262
|
+
|
|
263
|
+
> **GHCR:** to pull from GitHub Container Registry instead of Docker Hub,
|
|
264
|
+
> change the `image:` line in your copy of `docker-compose.yml` to
|
|
265
|
+
> `ghcr.io/cocoindex-io/cocoindex-code:latest`.
|
|
266
|
+
|
|
267
|
+
### Or: `docker run`
|
|
268
|
+
|
|
269
|
+
<details>
|
|
270
|
+
<summary>Docker Desktop (macOS / Windows)</summary>
|
|
246
271
|
|
|
247
272
|
```bash
|
|
248
273
|
docker run -d --name cocoindex-code \
|
|
249
|
-
--volume "$
|
|
250
|
-
--volume cocoindex-
|
|
251
|
-
|
|
252
|
-
|
|
274
|
+
--volume "$HOME:/workspace" \
|
|
275
|
+
--volume cocoindex-data:/var/cocoindex \
|
|
276
|
+
-e COCOINDEX_CODE_HOST_PATH_MAPPING="/workspace=$HOME" \
|
|
277
|
+
cocoindex/cocoindex-code:latest
|
|
253
278
|
```
|
|
279
|
+
</details>
|
|
254
280
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
- `cocoindex-model-cache` — persists the embedding model across image upgrades
|
|
281
|
+
<details>
|
|
282
|
+
<summary>Linux (with <code>PUID</code>/<code>PGID</code>)</summary>
|
|
258
283
|
|
|
259
|
-
|
|
284
|
+
```bash
|
|
285
|
+
docker run -d --name cocoindex-code \
|
|
286
|
+
-e PUID=$(id -u) -e PGID=$(id -g) \
|
|
287
|
+
--volume "$HOME:/workspace" \
|
|
288
|
+
--volume cocoindex-data:/var/cocoindex \
|
|
289
|
+
-e COCOINDEX_CODE_HOST_PATH_MAPPING="/workspace=$HOME" \
|
|
290
|
+
cocoindex/cocoindex-code:latest
|
|
291
|
+
```
|
|
292
|
+
</details>
|
|
293
|
+
|
|
294
|
+
### Shell wrapper for `ccc` commands
|
|
295
|
+
|
|
296
|
+
Paste this into `~/.bashrc` / `~/.zshrc` so `ccc` feels native on the host
|
|
297
|
+
and picks up the right project based on your current directory:
|
|
260
298
|
|
|
261
299
|
```bash
|
|
262
|
-
|
|
300
|
+
ccc() {
|
|
301
|
+
docker exec -it -e COCOINDEX_CODE_HOST_CWD="$PWD" cocoindex-code ccc "$@"
|
|
302
|
+
}
|
|
263
303
|
```
|
|
264
304
|
|
|
265
|
-
|
|
305
|
+
Now `cd` into any project under your workspace and run `ccc init`, `ccc index`,
|
|
306
|
+
`ccc search ...`, `ccc status`, etc. — it just works.
|
|
307
|
+
|
|
308
|
+
### Connect your coding agent
|
|
266
309
|
|
|
267
310
|
<details>
|
|
268
311
|
<summary>Claude Code</summary>
|
|
269
312
|
|
|
313
|
+
Register MCP from inside the target project so `$PWD` points there:
|
|
314
|
+
|
|
270
315
|
```bash
|
|
271
|
-
claude mcp add cocoindex-code -- docker exec -i
|
|
316
|
+
claude mcp add cocoindex-code -- docker exec -i \
|
|
317
|
+
-e COCOINDEX_CODE_HOST_CWD="$PWD" cocoindex-code ccc mcp
|
|
272
318
|
```
|
|
273
319
|
|
|
274
320
|
Or via `.mcp.json`:
|
|
@@ -279,40 +325,50 @@ Or via `.mcp.json`:
|
|
|
279
325
|
"cocoindex-code": {
|
|
280
326
|
"type": "stdio",
|
|
281
327
|
"command": "docker",
|
|
282
|
-
"args": [
|
|
328
|
+
"args": [
|
|
329
|
+
"exec",
|
|
330
|
+
"-i",
|
|
331
|
+
"-e",
|
|
332
|
+
"COCOINDEX_CODE_HOST_CWD=${PWD}",
|
|
333
|
+
"cocoindex-code",
|
|
334
|
+
"ccc",
|
|
335
|
+
"mcp"
|
|
336
|
+
]
|
|
283
337
|
}
|
|
284
338
|
}
|
|
285
339
|
}
|
|
286
340
|
```
|
|
341
|
+
|
|
342
|
+
> Note: use `-i` (not `-it`). The `-t` flag allocates a terminal, which
|
|
343
|
+
> interferes with MCP's JSON messaging over stdin/stdout — only add it for
|
|
344
|
+
> interactive `ccc` commands like `ccc init`.
|
|
287
345
|
</details>
|
|
288
346
|
|
|
289
347
|
<details>
|
|
290
348
|
<summary>Codex</summary>
|
|
291
349
|
|
|
292
350
|
```bash
|
|
293
|
-
codex mcp add cocoindex-code -- docker exec -i
|
|
351
|
+
codex mcp add cocoindex-code -- docker exec -i \
|
|
352
|
+
-e COCOINDEX_CODE_HOST_CWD="$PWD" cocoindex-code ccc mcp
|
|
294
353
|
```
|
|
295
354
|
</details>
|
|
296
355
|
|
|
297
|
-
###
|
|
356
|
+
### Upgrading from an older image
|
|
298
357
|
|
|
299
|
-
|
|
358
|
+
Earlier images used separate `cocoindex-db` and `cocoindex-model-cache`
|
|
359
|
+
volumes; the current image consolidates them into a single `cocoindex-data`
|
|
360
|
+
volume. Before pulling the new image, drop the old container and volumes —
|
|
361
|
+
indexes rebuild on your next `ccc index`, and the embedding model is
|
|
362
|
+
re-populated automatically on first start:
|
|
300
363
|
|
|
301
364
|
```bash
|
|
302
|
-
docker
|
|
303
|
-
docker
|
|
304
|
-
docker exec -it cocoindex-code ccc status
|
|
305
|
-
```
|
|
306
|
-
|
|
307
|
-
Or set an alias on your host so it feels native:
|
|
308
|
-
|
|
309
|
-
```bash
|
|
310
|
-
alias ccc='docker exec -it cocoindex-code ccc'
|
|
365
|
+
docker rm -f cocoindex-code
|
|
366
|
+
docker volume rm cocoindex-db cocoindex-model-cache
|
|
311
367
|
```
|
|
312
368
|
|
|
313
369
|
### Configuration via environment variables
|
|
314
370
|
|
|
315
|
-
Pass configuration to `docker run` with `-e`:
|
|
371
|
+
Pass configuration to `docker run` / compose with `-e`:
|
|
316
372
|
|
|
317
373
|
```bash
|
|
318
374
|
# Extra extensions (e.g. Typesafe Config, SBT build files)
|
|
@@ -325,6 +381,10 @@ Pass configuration to `docker run` with `-e`:
|
|
|
325
381
|
-e VOYAGE_API_KEY=your-key
|
|
326
382
|
```
|
|
327
383
|
|
|
384
|
+
> **Security note:** mounting `$HOME` gives the container read/write access
|
|
385
|
+
> to everything under it. If that's too broad, bind-mount a narrower
|
|
386
|
+
> directory instead (`COCOINDEX_HOST_WORKSPACE=/path/to/code`).
|
|
387
|
+
|
|
328
388
|
### Build the image locally
|
|
329
389
|
|
|
330
390
|
```bash
|
|
@@ -359,6 +419,8 @@ envs: # extra environment variabl
|
|
|
359
419
|
|
|
360
420
|
> **Note:** The daemon inherits your shell environment. If an API key (e.g. `OPENAI_API_KEY`) is already set as an environment variable, you don't need to duplicate it in `envs`. The `envs` field is only for values that aren't in your environment.
|
|
361
421
|
|
|
422
|
+
> **Custom location:** set `COCOINDEX_CODE_DIR` to place `global_settings.yml` somewhere other than `~/.cocoindex_code/` — useful if you want the file to live alongside your projects (e.g. on a synced folder).
|
|
423
|
+
|
|
362
424
|
### Project Settings (`<project>/.cocoindex_code/settings.yml`)
|
|
363
425
|
|
|
364
426
|
Per-project. Controls which files to index.
|
|
@@ -198,33 +198,79 @@ The recommended approach is a **persistent container**: start it once, and use
|
|
|
198
198
|
`docker exec` to run CLI commands or connect MCP sessions to it. The daemon
|
|
199
199
|
inside stays warm across sessions, so the embedding model is loaded only once.
|
|
200
200
|
|
|
201
|
-
###
|
|
201
|
+
### Quick start — `docker compose up -d`
|
|
202
|
+
|
|
203
|
+
Grab [`docker/docker-compose.yml`](./docker/docker-compose.yml) from this repo and run:
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
# macOS / Windows
|
|
207
|
+
docker compose up -d
|
|
208
|
+
|
|
209
|
+
# Linux (aligns file ownership on bind-mounted paths with your host user)
|
|
210
|
+
PUID=$(id -u) PGID=$(id -g) docker compose up -d
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
By default your home directory is mounted into the container (set
|
|
214
|
+
`COCOINDEX_HOST_WORKSPACE` to narrow this to a specific code folder). Index
|
|
215
|
+
data and the embedding model cache persist in a Docker volume across
|
|
216
|
+
restarts. Your global settings file at `$HOME/.cocoindex_code/global_settings.yml`
|
|
217
|
+
is visible and editable on the host; edits take effect on your next `ccc` command.
|
|
218
|
+
|
|
219
|
+
> **GHCR:** to pull from GitHub Container Registry instead of Docker Hub,
|
|
220
|
+
> change the `image:` line in your copy of `docker-compose.yml` to
|
|
221
|
+
> `ghcr.io/cocoindex-io/cocoindex-code:latest`.
|
|
222
|
+
|
|
223
|
+
### Or: `docker run`
|
|
224
|
+
|
|
225
|
+
<details>
|
|
226
|
+
<summary>Docker Desktop (macOS / Windows)</summary>
|
|
202
227
|
|
|
203
228
|
```bash
|
|
204
229
|
docker run -d --name cocoindex-code \
|
|
205
|
-
--volume "$
|
|
206
|
-
--volume cocoindex-
|
|
207
|
-
|
|
208
|
-
|
|
230
|
+
--volume "$HOME:/workspace" \
|
|
231
|
+
--volume cocoindex-data:/var/cocoindex \
|
|
232
|
+
-e COCOINDEX_CODE_HOST_PATH_MAPPING="/workspace=$HOME" \
|
|
233
|
+
cocoindex/cocoindex-code:latest
|
|
209
234
|
```
|
|
235
|
+
</details>
|
|
210
236
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
- `cocoindex-model-cache` — persists the embedding model across image upgrades
|
|
237
|
+
<details>
|
|
238
|
+
<summary>Linux (with <code>PUID</code>/<code>PGID</code>)</summary>
|
|
214
239
|
|
|
215
|
-
|
|
240
|
+
```bash
|
|
241
|
+
docker run -d --name cocoindex-code \
|
|
242
|
+
-e PUID=$(id -u) -e PGID=$(id -g) \
|
|
243
|
+
--volume "$HOME:/workspace" \
|
|
244
|
+
--volume cocoindex-data:/var/cocoindex \
|
|
245
|
+
-e COCOINDEX_CODE_HOST_PATH_MAPPING="/workspace=$HOME" \
|
|
246
|
+
cocoindex/cocoindex-code:latest
|
|
247
|
+
```
|
|
248
|
+
</details>
|
|
249
|
+
|
|
250
|
+
### Shell wrapper for `ccc` commands
|
|
251
|
+
|
|
252
|
+
Paste this into `~/.bashrc` / `~/.zshrc` so `ccc` feels native on the host
|
|
253
|
+
and picks up the right project based on your current directory:
|
|
216
254
|
|
|
217
255
|
```bash
|
|
218
|
-
|
|
256
|
+
ccc() {
|
|
257
|
+
docker exec -it -e COCOINDEX_CODE_HOST_CWD="$PWD" cocoindex-code ccc "$@"
|
|
258
|
+
}
|
|
219
259
|
```
|
|
220
260
|
|
|
221
|
-
|
|
261
|
+
Now `cd` into any project under your workspace and run `ccc init`, `ccc index`,
|
|
262
|
+
`ccc search ...`, `ccc status`, etc. — it just works.
|
|
263
|
+
|
|
264
|
+
### Connect your coding agent
|
|
222
265
|
|
|
223
266
|
<details>
|
|
224
267
|
<summary>Claude Code</summary>
|
|
225
268
|
|
|
269
|
+
Register MCP from inside the target project so `$PWD` points there:
|
|
270
|
+
|
|
226
271
|
```bash
|
|
227
|
-
claude mcp add cocoindex-code -- docker exec -i
|
|
272
|
+
claude mcp add cocoindex-code -- docker exec -i \
|
|
273
|
+
-e COCOINDEX_CODE_HOST_CWD="$PWD" cocoindex-code ccc mcp
|
|
228
274
|
```
|
|
229
275
|
|
|
230
276
|
Or via `.mcp.json`:
|
|
@@ -235,40 +281,50 @@ Or via `.mcp.json`:
|
|
|
235
281
|
"cocoindex-code": {
|
|
236
282
|
"type": "stdio",
|
|
237
283
|
"command": "docker",
|
|
238
|
-
"args": [
|
|
284
|
+
"args": [
|
|
285
|
+
"exec",
|
|
286
|
+
"-i",
|
|
287
|
+
"-e",
|
|
288
|
+
"COCOINDEX_CODE_HOST_CWD=${PWD}",
|
|
289
|
+
"cocoindex-code",
|
|
290
|
+
"ccc",
|
|
291
|
+
"mcp"
|
|
292
|
+
]
|
|
239
293
|
}
|
|
240
294
|
}
|
|
241
295
|
}
|
|
242
296
|
```
|
|
297
|
+
|
|
298
|
+
> Note: use `-i` (not `-it`). The `-t` flag allocates a terminal, which
|
|
299
|
+
> interferes with MCP's JSON messaging over stdin/stdout — only add it for
|
|
300
|
+
> interactive `ccc` commands like `ccc init`.
|
|
243
301
|
</details>
|
|
244
302
|
|
|
245
303
|
<details>
|
|
246
304
|
<summary>Codex</summary>
|
|
247
305
|
|
|
248
306
|
```bash
|
|
249
|
-
codex mcp add cocoindex-code -- docker exec -i
|
|
307
|
+
codex mcp add cocoindex-code -- docker exec -i \
|
|
308
|
+
-e COCOINDEX_CODE_HOST_CWD="$PWD" cocoindex-code ccc mcp
|
|
250
309
|
```
|
|
251
310
|
</details>
|
|
252
311
|
|
|
253
|
-
###
|
|
312
|
+
### Upgrading from an older image
|
|
254
313
|
|
|
255
|
-
|
|
314
|
+
Earlier images used separate `cocoindex-db` and `cocoindex-model-cache`
|
|
315
|
+
volumes; the current image consolidates them into a single `cocoindex-data`
|
|
316
|
+
volume. Before pulling the new image, drop the old container and volumes —
|
|
317
|
+
indexes rebuild on your next `ccc index`, and the embedding model is
|
|
318
|
+
re-populated automatically on first start:
|
|
256
319
|
|
|
257
320
|
```bash
|
|
258
|
-
docker
|
|
259
|
-
docker
|
|
260
|
-
docker exec -it cocoindex-code ccc status
|
|
261
|
-
```
|
|
262
|
-
|
|
263
|
-
Or set an alias on your host so it feels native:
|
|
264
|
-
|
|
265
|
-
```bash
|
|
266
|
-
alias ccc='docker exec -it cocoindex-code ccc'
|
|
321
|
+
docker rm -f cocoindex-code
|
|
322
|
+
docker volume rm cocoindex-db cocoindex-model-cache
|
|
267
323
|
```
|
|
268
324
|
|
|
269
325
|
### Configuration via environment variables
|
|
270
326
|
|
|
271
|
-
Pass configuration to `docker run` with `-e`:
|
|
327
|
+
Pass configuration to `docker run` / compose with `-e`:
|
|
272
328
|
|
|
273
329
|
```bash
|
|
274
330
|
# Extra extensions (e.g. Typesafe Config, SBT build files)
|
|
@@ -281,6 +337,10 @@ Pass configuration to `docker run` with `-e`:
|
|
|
281
337
|
-e VOYAGE_API_KEY=your-key
|
|
282
338
|
```
|
|
283
339
|
|
|
340
|
+
> **Security note:** mounting `$HOME` gives the container read/write access
|
|
341
|
+
> to everything under it. If that's too broad, bind-mount a narrower
|
|
342
|
+
> directory instead (`COCOINDEX_HOST_WORKSPACE=/path/to/code`).
|
|
343
|
+
|
|
284
344
|
### Build the image locally
|
|
285
345
|
|
|
286
346
|
```bash
|
|
@@ -315,6 +375,8 @@ envs: # extra environment variabl
|
|
|
315
375
|
|
|
316
376
|
> **Note:** The daemon inherits your shell environment. If an API key (e.g. `OPENAI_API_KEY`) is already set as an environment variable, you don't need to duplicate it in `envs`. The `envs` field is only for values that aren't in your environment.
|
|
317
377
|
|
|
378
|
+
> **Custom location:** set `COCOINDEX_CODE_DIR` to place `global_settings.yml` somewhere other than `~/.cocoindex_code/` — useful if you want the file to live alongside your projects (e.g. on a synced folder).
|
|
379
|
+
|
|
318
380
|
### Project Settings (`<project>/.cocoindex_code/settings.yml`)
|
|
319
381
|
|
|
320
382
|
Per-project. Controls which files to index.
|
|
@@ -105,5 +105,8 @@ explicit_package_bases = true
|
|
|
105
105
|
testpaths = ["tests"]
|
|
106
106
|
python_files = ["test_*.py"]
|
|
107
107
|
python_functions = ["test_*"]
|
|
108
|
-
addopts = "-v --tb=short"
|
|
108
|
+
addopts = "-v --tb=short -m 'not docker_e2e'"
|
|
109
109
|
asyncio_mode = "auto"
|
|
110
|
+
markers = [
|
|
111
|
+
"docker_e2e: requires Docker; builds the image and runs containerized E2E tests. Run with: pytest -m docker_e2e",
|
|
112
|
+
]
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Daemon filesystem paths and connection helpers.
|
|
2
|
+
|
|
3
|
+
Lightweight module with no cocoindex dependency so that the CLI client
|
|
4
|
+
can import these without pulling in the full daemon stack.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from .settings import user_settings_dir
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def daemon_runtime_dir() -> Path:
|
|
17
|
+
"""Return the directory that holds daemon runtime artifacts.
|
|
18
|
+
|
|
19
|
+
Holds ``daemon.sock``, ``daemon.pid``, ``daemon.log``. Kept separate from
|
|
20
|
+
the user-settings dir so that (e.g. in Docker) the socket can live on the
|
|
21
|
+
container's native filesystem while ``global_settings.yml`` lives on a
|
|
22
|
+
bind mount.
|
|
23
|
+
|
|
24
|
+
Override with ``COCOINDEX_CODE_RUNTIME_DIR``. Defaults to
|
|
25
|
+
:func:`user_settings_dir` for backward compatibility — non-Docker users
|
|
26
|
+
see identical behavior to before the split.
|
|
27
|
+
"""
|
|
28
|
+
override = os.environ.get("COCOINDEX_CODE_RUNTIME_DIR")
|
|
29
|
+
if override:
|
|
30
|
+
return Path(override)
|
|
31
|
+
return user_settings_dir()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def connection_family() -> str:
|
|
35
|
+
"""Return the multiprocessing connection family for this platform."""
|
|
36
|
+
return "AF_PIPE" if sys.platform == "win32" else "AF_UNIX"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def daemon_socket_path() -> str:
|
|
40
|
+
"""Return the daemon socket/pipe address."""
|
|
41
|
+
if sys.platform == "win32":
|
|
42
|
+
import hashlib
|
|
43
|
+
|
|
44
|
+
# Hash the runtime dir so COCOINDEX_CODE_RUNTIME_DIR (or the
|
|
45
|
+
# COCOINDEX_CODE_DIR fallback) overrides produce unique pipe names,
|
|
46
|
+
# preventing conflicts between different daemon instances (tests,
|
|
47
|
+
# users, etc.)
|
|
48
|
+
dir_hash = hashlib.md5(str(daemon_runtime_dir()).encode()).hexdigest()[:12]
|
|
49
|
+
return rf"\\.\pipe\cocoindex_code_{dir_hash}"
|
|
50
|
+
return str(daemon_runtime_dir() / "daemon.sock")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def daemon_pid_path() -> Path:
|
|
54
|
+
"""Return the path for the daemon's PID file."""
|
|
55
|
+
return daemon_runtime_dir() / "daemon.pid"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def daemon_log_path() -> Path:
|
|
59
|
+
"""Return the path for the daemon's log file."""
|
|
60
|
+
return daemon_runtime_dir() / "daemon.log"
|
|
@@ -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.24'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 24)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import functools
|
|
6
|
+
import os
|
|
6
7
|
import sys
|
|
7
8
|
from collections.abc import Callable
|
|
8
9
|
from pathlib import Path
|
|
@@ -19,6 +20,8 @@ from .settings import (
|
|
|
19
20
|
default_project_settings,
|
|
20
21
|
find_parent_with_marker,
|
|
21
22
|
find_project_root,
|
|
23
|
+
format_path_for_display,
|
|
24
|
+
normalize_input_path,
|
|
22
25
|
project_settings_path,
|
|
23
26
|
resolve_db_dir,
|
|
24
27
|
save_initial_user_settings,
|
|
@@ -37,6 +40,29 @@ daemon_app = _typer.Typer(name="daemon", help="Manage the daemon process.")
|
|
|
37
40
|
app.add_typer(daemon_app, name="daemon")
|
|
38
41
|
|
|
39
42
|
|
|
43
|
+
@app.callback()
|
|
44
|
+
def _apply_host_cwd() -> None:
|
|
45
|
+
"""Honor ``COCOINDEX_CODE_HOST_CWD`` when forwarded from a ``docker exec`` wrapper.
|
|
46
|
+
|
|
47
|
+
The env var carries the host shell's pwd verbatim. We normalize it through
|
|
48
|
+
the host path mapping to container form and ``chdir`` there so
|
|
49
|
+
cwd-driven discovery (``find_project_root`` etc.) sees the user's real
|
|
50
|
+
project subtree. Unset → no-op.
|
|
51
|
+
"""
|
|
52
|
+
host_cwd = os.environ.get("COCOINDEX_CODE_HOST_CWD")
|
|
53
|
+
if not host_cwd:
|
|
54
|
+
return
|
|
55
|
+
target = normalize_input_path(host_cwd)
|
|
56
|
+
try:
|
|
57
|
+
os.chdir(target)
|
|
58
|
+
except OSError as e:
|
|
59
|
+
_typer.echo(
|
|
60
|
+
f"Warning: COCOINDEX_CODE_HOST_CWD={host_cwd!r} → {target!r} "
|
|
61
|
+
f"is not accessible: {e}. Continuing with cwd={os.getcwd()!r}.",
|
|
62
|
+
err=True,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
40
66
|
# ---------------------------------------------------------------------------
|
|
41
67
|
# Shared CLI helpers
|
|
42
68
|
# ---------------------------------------------------------------------------
|
|
@@ -51,7 +77,7 @@ def require_project_root() -> Path:
|
|
|
51
77
|
gs_path = user_settings_path()
|
|
52
78
|
if not gs_path.is_file():
|
|
53
79
|
_typer.echo(
|
|
54
|
-
f"Error: Global settings not found: {gs_path}\n"
|
|
80
|
+
f"Error: Global settings not found: {format_path_for_display(gs_path)}\n"
|
|
55
81
|
"Run `ccc init` to create it with default settings.",
|
|
56
82
|
err=True,
|
|
57
83
|
)
|
|
@@ -112,7 +138,7 @@ def _format_progress(progress: IndexingProgress) -> str:
|
|
|
112
138
|
|
|
113
139
|
def print_project_header(project_root: str) -> None:
|
|
114
140
|
"""Print the project root directory."""
|
|
115
|
-
_typer.echo(f"Project: {project_root}")
|
|
141
|
+
_typer.echo(f"Project: {format_path_for_display(project_root)}")
|
|
116
142
|
|
|
117
143
|
|
|
118
144
|
def print_index_stats(status: ProjectStatusResponse) -> None:
|
|
@@ -400,8 +426,9 @@ def _run_init_model_check(settings_path: Path) -> None:
|
|
|
400
426
|
failed = True
|
|
401
427
|
|
|
402
428
|
if failed:
|
|
429
|
+
display_path = format_path_for_display(settings_path)
|
|
403
430
|
_typer.echo(
|
|
404
|
-
f"You can edit {
|
|
431
|
+
f"You can edit {display_path} to change the model or add API keys\n"
|
|
405
432
|
"under `envs:`. Then run `ccc doctor` to verify.",
|
|
406
433
|
err=True,
|
|
407
434
|
)
|
|
@@ -419,7 +446,7 @@ def _setup_user_settings_interactive(litellm_model_flag: str | None) -> None:
|
|
|
419
446
|
|
|
420
447
|
path = save_initial_user_settings(embedding)
|
|
421
448
|
_typer.echo()
|
|
422
|
-
_typer.echo(f"Created user settings: {path}")
|
|
449
|
+
_typer.echo(f"Created user settings: {format_path_for_display(path)}")
|
|
423
450
|
|
|
424
451
|
_typer.echo()
|
|
425
452
|
_typer.echo(f"Testing embedding model: {embedding.provider} / {embedding.model}")
|
|
@@ -443,8 +470,9 @@ def init(
|
|
|
443
470
|
user_path = user_settings_path()
|
|
444
471
|
if user_path.is_file():
|
|
445
472
|
if litellm_model is not None:
|
|
473
|
+
display_path = format_path_for_display(user_path)
|
|
446
474
|
_typer.echo(
|
|
447
|
-
f"Error: global settings already exist at {
|
|
475
|
+
f"Error: global settings already exist at {display_path}.\n"
|
|
448
476
|
"Edit that file or remove it before passing `--litellm-model`.",
|
|
449
477
|
err=True,
|
|
450
478
|
)
|
|
@@ -461,8 +489,9 @@ def init(
|
|
|
461
489
|
if not force:
|
|
462
490
|
parent = find_parent_with_marker(cwd)
|
|
463
491
|
if parent is not None and parent != cwd:
|
|
492
|
+
display_parent = format_path_for_display(parent)
|
|
464
493
|
_typer.echo(
|
|
465
|
-
f"Warning: A parent directory has a project marker: {
|
|
494
|
+
f"Warning: A parent directory has a project marker: {display_parent}\n"
|
|
466
495
|
"You might want to run `ccc init` there instead.\n"
|
|
467
496
|
"Use `ccc init -f` to initialize here anyway."
|
|
468
497
|
)
|
|
@@ -470,7 +499,7 @@ def init(
|
|
|
470
499
|
|
|
471
500
|
# Create project settings
|
|
472
501
|
save_project_settings(cwd, default_project_settings())
|
|
473
|
-
_typer.echo(f"Created project settings: {settings_file}")
|
|
502
|
+
_typer.echo(f"Created project settings: {format_path_for_display(settings_file)}")
|
|
474
503
|
|
|
475
504
|
# Add to .gitignore
|
|
476
505
|
add_to_gitignore(cwd)
|
|
@@ -538,10 +567,10 @@ def status() -> None:
|
|
|
538
567
|
project_root = str(project_root_path)
|
|
539
568
|
print_project_header(project_root)
|
|
540
569
|
|
|
541
|
-
_typer.echo(f"Settings: {project_settings_path(project_root_path)}")
|
|
570
|
+
_typer.echo(f"Settings: {format_path_for_display(project_settings_path(project_root_path))}")
|
|
542
571
|
db_path = target_sqlite_db_path(project_root_path)
|
|
543
572
|
if db_path.exists():
|
|
544
|
-
_typer.echo(f"Index DB: {db_path}")
|
|
573
|
+
_typer.echo(f"Index DB: {format_path_for_display(db_path)}")
|
|
545
574
|
|
|
546
575
|
print_index_stats(_client.project_status(project_root))
|
|
547
576
|
|
|
@@ -576,7 +605,7 @@ def reset(
|
|
|
576
605
|
if to_delete:
|
|
577
606
|
_typer.echo("The following files will be deleted:")
|
|
578
607
|
for f in to_delete:
|
|
579
|
-
_typer.echo(f" {f}")
|
|
608
|
+
_typer.echo(f" {format_path_for_display(f)}")
|
|
580
609
|
|
|
581
610
|
# Confirm
|
|
582
611
|
if not force:
|
|
@@ -668,7 +697,7 @@ def doctor() -> None:
|
|
|
668
697
|
# --- 1. Global settings (local, no daemon needed) ---
|
|
669
698
|
_print_section("Global Settings")
|
|
670
699
|
settings_path = user_settings_path()
|
|
671
|
-
_typer.echo(f" Settings: {settings_path}")
|
|
700
|
+
_typer.echo(f" Settings: {format_path_for_display(settings_path)}")
|
|
672
701
|
try:
|
|
673
702
|
user_settings = _load_user_settings()
|
|
674
703
|
emb = user_settings.embedding
|
|
@@ -706,6 +735,10 @@ def doctor() -> None:
|
|
|
706
735
|
_typer.echo(" DB path mappings:")
|
|
707
736
|
for m in env_resp.db_path_mappings:
|
|
708
737
|
_typer.echo(f" {m.source} \u2192 {m.target}")
|
|
738
|
+
if env_resp.host_path_mappings:
|
|
739
|
+
_typer.echo(" Host path mappings:")
|
|
740
|
+
for m in env_resp.host_path_mappings:
|
|
741
|
+
_typer.echo(f" {m.source} \u2192 {m.target}")
|
|
709
742
|
except Exception as e:
|
|
710
743
|
_print_error(f"Failed to get daemon env: {e}")
|
|
711
744
|
|
|
@@ -726,7 +759,7 @@ def doctor() -> None:
|
|
|
726
759
|
if project_root is not None:
|
|
727
760
|
_print_section("Project Settings")
|
|
728
761
|
ps_path = project_settings_path(project_root)
|
|
729
|
-
_typer.echo(f" Settings: {ps_path}")
|
|
762
|
+
_typer.echo(f" Settings: {format_path_for_display(ps_path)}")
|
|
730
763
|
try:
|
|
731
764
|
ps = _load_project_settings(project_root)
|
|
732
765
|
_typer.echo(f" Include patterns ({len(ps.include_patterns)}):")
|
|
@@ -754,7 +787,7 @@ def doctor() -> None:
|
|
|
754
787
|
_print_section("Log Files")
|
|
755
788
|
from ._daemon_paths import daemon_log_path as _daemon_log_path
|
|
756
789
|
|
|
757
|
-
_typer.echo(f" Daemon logs: {_daemon_log_path()}")
|
|
790
|
+
_typer.echo(f" Daemon logs: {format_path_for_display(_daemon_log_path())}")
|
|
758
791
|
_typer.echo(" Check logs above for further troubleshooting.")
|
|
759
792
|
|
|
760
793
|
|
|
@@ -805,7 +838,7 @@ def daemon_status() -> None:
|
|
|
805
838
|
_typer.echo("Projects:")
|
|
806
839
|
for p in resp.projects:
|
|
807
840
|
state = "indexing" if p.indexing else "idle"
|
|
808
|
-
_typer.echo(f" {p.project_root} [{state}]")
|
|
841
|
+
_typer.echo(f" {format_path_for_display(p.project_root)} [{state}]")
|
|
809
842
|
else:
|
|
810
843
|
_typer.echo("No projects loaded.")
|
|
811
844
|
|
|
@@ -19,9 +19,9 @@ from pathlib import Path
|
|
|
19
19
|
|
|
20
20
|
from ._daemon_paths import (
|
|
21
21
|
connection_family,
|
|
22
|
-
daemon_dir,
|
|
23
22
|
daemon_log_path,
|
|
24
23
|
daemon_pid_path,
|
|
24
|
+
daemon_runtime_dir,
|
|
25
25
|
daemon_socket_path,
|
|
26
26
|
)
|
|
27
27
|
from ._version import __version__
|
|
@@ -53,6 +53,7 @@ from .protocol import (
|
|
|
53
53
|
decode_response,
|
|
54
54
|
encode_request,
|
|
55
55
|
)
|
|
56
|
+
from .settings import normalize_input_path
|
|
56
57
|
|
|
57
58
|
logger = logging.getLogger(__name__)
|
|
58
59
|
|
|
@@ -65,6 +66,14 @@ logger = logging.getLogger(__name__)
|
|
|
65
66
|
_daemon_ensured = False
|
|
66
67
|
|
|
67
68
|
|
|
69
|
+
def _is_daemon_supervised() -> bool:
|
|
70
|
+
"""True when an external supervisor (Docker entrypoint loop, systemd, …) owns
|
|
71
|
+
daemon respawn. The client in that mode calls ``stop_daemon`` but never
|
|
72
|
+
``start_daemon`` — it just waits for the socket to reappear.
|
|
73
|
+
"""
|
|
74
|
+
return os.environ.get("COCOINDEX_CODE_DAEMON_SUPERVISED") == "1"
|
|
75
|
+
|
|
76
|
+
|
|
68
77
|
def _connect_and_handshake() -> Connection:
|
|
69
78
|
"""Connect to the daemon and perform the version handshake.
|
|
70
79
|
|
|
@@ -90,8 +99,13 @@ def _connect_and_handshake() -> Connection:
|
|
|
90
99
|
except (ConnectionRefusedError, OSError):
|
|
91
100
|
pass
|
|
92
101
|
|
|
93
|
-
|
|
94
|
-
|
|
102
|
+
if _is_daemon_supervised():
|
|
103
|
+
# Supervisor is responsible for (re)starting the daemon — just wait
|
|
104
|
+
# for the socket to reappear.
|
|
105
|
+
_wait_for_daemon()
|
|
106
|
+
else:
|
|
107
|
+
proc = start_daemon()
|
|
108
|
+
_wait_for_daemon(proc=proc)
|
|
95
109
|
|
|
96
110
|
# Verify the fresh daemon is reachable
|
|
97
111
|
for _attempt in range(10):
|
|
@@ -199,6 +213,7 @@ def index(
|
|
|
199
213
|
on_waiting: Callable[[], None] | None = None,
|
|
200
214
|
) -> IndexResponse:
|
|
201
215
|
"""Request indexing with streaming progress. Blocks until complete."""
|
|
216
|
+
project_root = normalize_input_path(project_root)
|
|
202
217
|
conn = _connect_and_handshake()
|
|
203
218
|
try:
|
|
204
219
|
conn.send_bytes(encode_request(IndexRequest(project_root=project_root)))
|
|
@@ -240,6 +255,7 @@ def search(
|
|
|
240
255
|
progress), calls *on_waiting* (if provided) then continues reading
|
|
241
256
|
until the final ``SearchResponse``.
|
|
242
257
|
"""
|
|
258
|
+
project_root = normalize_input_path(project_root)
|
|
243
259
|
conn = _connect_and_handshake()
|
|
244
260
|
try:
|
|
245
261
|
conn.send_bytes(
|
|
@@ -274,7 +290,7 @@ def search(
|
|
|
274
290
|
|
|
275
291
|
|
|
276
292
|
def project_status(project_root: str) -> ProjectStatusResponse:
|
|
277
|
-
return _send(ProjectStatusRequest(project_root=project_root)) # type: ignore[return-value]
|
|
293
|
+
return _send(ProjectStatusRequest(project_root=normalize_input_path(project_root))) # type: ignore[return-value]
|
|
278
294
|
|
|
279
295
|
|
|
280
296
|
def daemon_status() -> DaemonStatusResponse:
|
|
@@ -284,7 +300,7 @@ def daemon_status() -> DaemonStatusResponse:
|
|
|
284
300
|
|
|
285
301
|
|
|
286
302
|
def remove_project(project_root: str) -> RemoveProjectResponse:
|
|
287
|
-
return _send(RemoveProjectRequest(project_root=project_root)) # type: ignore[return-value]
|
|
303
|
+
return _send(RemoveProjectRequest(project_root=normalize_input_path(project_root))) # type: ignore[return-value]
|
|
288
304
|
|
|
289
305
|
|
|
290
306
|
def stop() -> StopResponse:
|
|
@@ -301,6 +317,8 @@ def doctor(
|
|
|
301
317
|
on_result: Callable[[DoctorCheckResult], None] | None = None,
|
|
302
318
|
) -> list[DoctorCheckResult]:
|
|
303
319
|
"""Run doctor checks via daemon, streaming results to on_result callback."""
|
|
320
|
+
if project_root is not None:
|
|
321
|
+
project_root = normalize_input_path(project_root)
|
|
304
322
|
conn = _connect_and_handshake()
|
|
305
323
|
try:
|
|
306
324
|
conn.send_bytes(encode_request(DoctorRequest(project_root=project_root)))
|
|
@@ -349,7 +367,7 @@ def start_daemon() -> subprocess.Popen[bytes]:
|
|
|
349
367
|
Returns the ``Popen`` object so callers can detect early process death
|
|
350
368
|
(via ``proc.poll()``) instead of waiting for a full timeout.
|
|
351
369
|
"""
|
|
352
|
-
|
|
370
|
+
daemon_runtime_dir().mkdir(parents=True, exist_ok=True)
|
|
353
371
|
log_path = daemon_log_path()
|
|
354
372
|
|
|
355
373
|
ccc_path = _find_ccc_executable()
|
|
@@ -508,18 +526,16 @@ def _wait_for_daemon(
|
|
|
508
526
|
If *proc* is given, polls the process each iteration. When the process
|
|
509
527
|
exits before the socket appears, raises ``DaemonStartError`` immediately
|
|
510
528
|
with the daemon log content — no need to wait for the full timeout.
|
|
529
|
+
|
|
530
|
+
Socket existence is checked *before* ``proc.poll()`` so that races with a
|
|
531
|
+
supervisor (e.g. the Docker entrypoint restart loop) don't spuriously raise
|
|
532
|
+
``DaemonStartError``: if the supervisor wins the bind and our subprocess
|
|
533
|
+
exits because the socket is already in use, the socket is still ready — we
|
|
534
|
+
should return success, not flag a failure.
|
|
511
535
|
"""
|
|
512
536
|
deadline = time.monotonic() + timeout
|
|
513
537
|
sock_path = daemon_socket_path()
|
|
514
538
|
while time.monotonic() < deadline:
|
|
515
|
-
# Check if the daemon process died before the socket appeared.
|
|
516
|
-
if proc is not None and proc.poll() is not None:
|
|
517
|
-
log = _read_daemon_log()
|
|
518
|
-
msg = "Daemon process exited before it became ready."
|
|
519
|
-
if log:
|
|
520
|
-
msg += f"\n\nDaemon log:\n{log}"
|
|
521
|
-
raise DaemonStartError(msg, log=log)
|
|
522
|
-
|
|
523
539
|
if sys.platform == "win32":
|
|
524
540
|
try:
|
|
525
541
|
conn = Client(sock_path, family=connection_family())
|
|
@@ -530,6 +546,16 @@ def _wait_for_daemon(
|
|
|
530
546
|
else:
|
|
531
547
|
if os.path.exists(sock_path):
|
|
532
548
|
return
|
|
549
|
+
|
|
550
|
+
# Daemon socket not yet up — if we spawned a subprocess that already
|
|
551
|
+
# exited, bail out with its log.
|
|
552
|
+
if proc is not None and proc.poll() is not None:
|
|
553
|
+
log = _read_daemon_log()
|
|
554
|
+
msg = "Daemon process exited before it became ready."
|
|
555
|
+
if log:
|
|
556
|
+
msg += f"\n\nDaemon log:\n{log}"
|
|
557
|
+
raise DaemonStartError(msg, log=log)
|
|
558
|
+
|
|
533
559
|
time.sleep(0.2)
|
|
534
560
|
|
|
535
561
|
# Timeout — also include log for diagnostics.
|
|
@@ -17,9 +17,9 @@ from typing import Any
|
|
|
17
17
|
|
|
18
18
|
from ._daemon_paths import (
|
|
19
19
|
connection_family,
|
|
20
|
-
daemon_dir,
|
|
21
20
|
daemon_log_path,
|
|
22
21
|
daemon_pid_path,
|
|
22
|
+
daemon_runtime_dir,
|
|
23
23
|
daemon_socket_path,
|
|
24
24
|
)
|
|
25
25
|
from ._version import __version__
|
|
@@ -56,10 +56,13 @@ from .protocol import (
|
|
|
56
56
|
)
|
|
57
57
|
from .settings import (
|
|
58
58
|
ChunkerMapping,
|
|
59
|
+
format_path_for_display,
|
|
60
|
+
get_host_path_mappings,
|
|
59
61
|
global_settings_mtime_us,
|
|
60
62
|
load_project_settings,
|
|
61
63
|
load_user_settings,
|
|
62
64
|
target_sqlite_db_path,
|
|
65
|
+
user_settings_path,
|
|
63
66
|
)
|
|
64
67
|
from .shared import Embedder, check_embedding, create_embedder
|
|
65
68
|
|
|
@@ -91,17 +94,28 @@ def _resolve_chunker_registry(mappings: list[ChunkerMapping]) -> dict[str, _Chun
|
|
|
91
94
|
|
|
92
95
|
|
|
93
96
|
class ProjectRegistry:
|
|
94
|
-
"""Cache of loaded projects, keyed by project root path.
|
|
97
|
+
"""Cache of loaded projects, keyed by project root path.
|
|
98
|
+
|
|
99
|
+
``_embedder`` is ``None`` when the daemon is running in "no-settings mode"
|
|
100
|
+
(started before ``global_settings.yml`` existed). In that state
|
|
101
|
+
``get_project`` raises an error pointing the user at ``ccc init``; the
|
|
102
|
+
daemon still serves handshakes so the client can detect the mtime
|
|
103
|
+
mismatch once the file is created and trigger a supervisor respawn.
|
|
104
|
+
"""
|
|
95
105
|
|
|
96
106
|
_projects: dict[str, Project]
|
|
97
|
-
_embedder: Embedder
|
|
107
|
+
_embedder: Embedder | None
|
|
98
108
|
|
|
99
|
-
def __init__(self, embedder: Embedder) -> None:
|
|
109
|
+
def __init__(self, embedder: Embedder | None) -> None:
|
|
100
110
|
self._projects = {}
|
|
101
111
|
self._embedder = embedder
|
|
102
112
|
|
|
103
113
|
async def get_project(self, project_root: str) -> Project:
|
|
104
114
|
"""Get or create a Project for the given root. Lazy initialization."""
|
|
115
|
+
if self._embedder is None:
|
|
116
|
+
raise RuntimeError(
|
|
117
|
+
"Daemon has no global settings loaded. Run `ccc init` to set up cocoindex-code."
|
|
118
|
+
)
|
|
105
119
|
if project_root not in self._projects:
|
|
106
120
|
root = Path(project_root)
|
|
107
121
|
project_settings = load_project_settings(root)
|
|
@@ -260,8 +274,19 @@ async def _handle_doctor(
|
|
|
260
274
|
)
|
|
261
275
|
|
|
262
276
|
|
|
263
|
-
async def _check_model(embedder: Embedder) -> DoctorCheckResult:
|
|
264
|
-
"""Test the embedding model by embedding a short string.
|
|
277
|
+
async def _check_model(embedder: Embedder | None) -> DoctorCheckResult:
|
|
278
|
+
"""Test the embedding model by embedding a short string.
|
|
279
|
+
|
|
280
|
+
Returns a failed result when the embedder is ``None`` (daemon running in
|
|
281
|
+
no-settings mode).
|
|
282
|
+
"""
|
|
283
|
+
if embedder is None:
|
|
284
|
+
return DoctorCheckResult(
|
|
285
|
+
name="Model Check",
|
|
286
|
+
ok=False,
|
|
287
|
+
details=[],
|
|
288
|
+
errors=["Daemon has no global settings loaded. Run `ccc init` to set up."],
|
|
289
|
+
)
|
|
265
290
|
result = await check_embedding(embedder)
|
|
266
291
|
if result.error is None:
|
|
267
292
|
return DoctorCheckResult(
|
|
@@ -340,7 +365,7 @@ async def _check_index_status(project_root_str: str) -> DoctorCheckResult:
|
|
|
340
365
|
|
|
341
366
|
project_root = Path(project_root_str)
|
|
342
367
|
db_path = target_sqlite_db_path(project_root)
|
|
343
|
-
details = [f"Index: {db_path}"]
|
|
368
|
+
details = [f"Index: {format_path_for_display(db_path)}"]
|
|
344
369
|
|
|
345
370
|
if not db_path.exists():
|
|
346
371
|
details.append("Index not created yet.")
|
|
@@ -445,6 +470,10 @@ async def _dispatch(
|
|
|
445
470
|
DbPathMappingEntry(source=str(m.source), target=str(m.target))
|
|
446
471
|
for m in get_db_path_mappings()
|
|
447
472
|
],
|
|
473
|
+
host_path_mappings=[
|
|
474
|
+
DbPathMappingEntry(source=str(m.source), target=str(m.target))
|
|
475
|
+
for m in get_host_path_mappings()
|
|
476
|
+
],
|
|
448
477
|
)
|
|
449
478
|
|
|
450
479
|
if isinstance(req, DoctorRequest):
|
|
@@ -468,19 +497,24 @@ def run_daemon() -> None:
|
|
|
468
497
|
to serve connections, and performs cleanup when shutdown is requested via
|
|
469
498
|
``StopRequest`` or a signal (SIGTERM / SIGINT).
|
|
470
499
|
"""
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
#
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
#
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
500
|
+
daemon_runtime_dir().mkdir(parents=True, exist_ok=True)
|
|
501
|
+
|
|
502
|
+
# No-settings mode: start even when global_settings.yml is missing so the
|
|
503
|
+
# client can complete its handshake, detect the mtime mismatch once
|
|
504
|
+
# `ccc init` writes the file, and trigger a supervisor respawn. The
|
|
505
|
+
# alternative (auto-creating defaults) would skip the interactive
|
|
506
|
+
# provider/model picker in `ccc init`.
|
|
507
|
+
settings_mtime_us = global_settings_mtime_us() # None when file is missing
|
|
508
|
+
embedder: Embedder | None
|
|
509
|
+
if user_settings_path().is_file():
|
|
510
|
+
user_settings = load_user_settings()
|
|
511
|
+
settings_env_keys = list(user_settings.envs.keys())
|
|
512
|
+
for key, value in user_settings.envs.items():
|
|
513
|
+
os.environ[key] = value
|
|
514
|
+
embedder = create_embedder(user_settings.embedding)
|
|
515
|
+
else:
|
|
516
|
+
settings_env_keys = []
|
|
517
|
+
embedder = None
|
|
484
518
|
|
|
485
519
|
# Write PID file
|
|
486
520
|
pid_path = daemon_pid_path()
|
|
@@ -167,6 +167,7 @@ class DaemonEnvResponse(_msgspec.Struct, tag="daemon_env"):
|
|
|
167
167
|
env_names: list[str]
|
|
168
168
|
settings_env_names: list[str]
|
|
169
169
|
db_path_mappings: list[DbPathMappingEntry] = []
|
|
170
|
+
host_path_mappings: list[DbPathMappingEntry] = []
|
|
170
171
|
|
|
171
172
|
|
|
172
173
|
class ErrorResponse(_msgspec.Struct, tag="error"):
|
|
@@ -151,51 +151,68 @@ _SETTINGS_FILE_NAME = "settings.yml" # project-level
|
|
|
151
151
|
_USER_SETTINGS_FILE_NAME = "global_settings.yml" # user-level
|
|
152
152
|
|
|
153
153
|
_ENV_DB_PATH_MAPPING = "COCOINDEX_CODE_DB_PATH_MAPPING"
|
|
154
|
+
_ENV_HOST_PATH_MAPPING = "COCOINDEX_CODE_HOST_PATH_MAPPING"
|
|
154
155
|
|
|
155
156
|
|
|
156
157
|
@dataclass
|
|
157
|
-
class
|
|
158
|
+
class PathMapping:
|
|
158
159
|
source: Path
|
|
159
160
|
target: Path
|
|
160
161
|
|
|
161
162
|
|
|
162
|
-
|
|
163
|
+
def _parse_path_mapping(env_var: str) -> list[PathMapping]:
|
|
164
|
+
"""Parse a ``source=target[,source=target...]`` env var.
|
|
163
165
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
"""Parse ``COCOINDEX_CODE_DB_PATH_MAPPING`` env var.
|
|
167
|
-
|
|
168
|
-
Format: ``/src1=/dst1,/src2=/dst2``
|
|
169
|
-
Both source and target must be absolute paths.
|
|
166
|
+
Both source and target must be absolute paths. Returns an empty list when
|
|
167
|
+
the env var is unset or blank. Raises ``ValueError`` on malformed entries.
|
|
170
168
|
"""
|
|
171
|
-
raw = os.environ.get(
|
|
169
|
+
raw = os.environ.get(env_var, "")
|
|
172
170
|
if not raw.strip():
|
|
173
171
|
return []
|
|
174
172
|
|
|
175
|
-
mappings: list[
|
|
173
|
+
mappings: list[PathMapping] = []
|
|
176
174
|
for entry in raw.split(","):
|
|
177
175
|
entry = entry.strip()
|
|
178
176
|
if not entry:
|
|
179
177
|
continue
|
|
180
178
|
parts = entry.split("=", 1)
|
|
181
179
|
if len(parts) != 2 or not parts[0] or not parts[1]:
|
|
182
|
-
raise ValueError(
|
|
183
|
-
f"{_ENV_DB_PATH_MAPPING}: invalid entry {entry!r}, expected format 'source=target'"
|
|
184
|
-
)
|
|
180
|
+
raise ValueError(f"{env_var}: invalid entry {entry!r}, expected format 'source=target'")
|
|
185
181
|
source = Path(parts[0])
|
|
186
182
|
target = Path(parts[1])
|
|
187
183
|
if not source.is_absolute():
|
|
188
|
-
raise ValueError(
|
|
189
|
-
f"{_ENV_DB_PATH_MAPPING}: source path must be absolute, got {source!r}"
|
|
190
|
-
)
|
|
184
|
+
raise ValueError(f"{env_var}: source path must be absolute, got {source!r}")
|
|
191
185
|
if not target.is_absolute():
|
|
192
|
-
raise ValueError(
|
|
193
|
-
|
|
194
|
-
)
|
|
195
|
-
mappings.append(DbPathMapping(source=source.resolve(), target=target.resolve()))
|
|
186
|
+
raise ValueError(f"{env_var}: target path must be absolute, got {target!r}")
|
|
187
|
+
mappings.append(PathMapping(source=source.resolve(), target=target.resolve()))
|
|
196
188
|
return mappings
|
|
197
189
|
|
|
198
190
|
|
|
191
|
+
def _apply_mapping(mappings: list[PathMapping], path: str | Path, reverse: bool = False) -> str:
|
|
192
|
+
"""Rewrite ``path`` through ``mappings``. First prefix match wins.
|
|
193
|
+
|
|
194
|
+
``reverse=False``: rewrites source-prefix → target-prefix (forward).
|
|
195
|
+
``reverse=True``: rewrites target-prefix → source-prefix (reverse).
|
|
196
|
+
|
|
197
|
+
Relative paths and absolute paths with no matching prefix are returned
|
|
198
|
+
unchanged (as ``str``).
|
|
199
|
+
"""
|
|
200
|
+
p = Path(path)
|
|
201
|
+
if not p.is_absolute():
|
|
202
|
+
return str(path)
|
|
203
|
+
resolved = p.resolve()
|
|
204
|
+
for m in mappings:
|
|
205
|
+
src, dst = (m.target, m.source) if reverse else (m.source, m.target)
|
|
206
|
+
if resolved == src or resolved.is_relative_to(src):
|
|
207
|
+
rel = resolved.relative_to(src)
|
|
208
|
+
return str(dst / rel) if str(rel) != "." else str(dst)
|
|
209
|
+
return str(path)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
_db_path_mapping: list[PathMapping] | None = None
|
|
213
|
+
_host_path_mapping: list[PathMapping] | None = None
|
|
214
|
+
|
|
215
|
+
|
|
199
216
|
def resolve_db_dir(project_root: Path) -> Path:
|
|
200
217
|
"""Return the directory for database files given a project root.
|
|
201
218
|
|
|
@@ -204,7 +221,7 @@ def resolve_db_dir(project_root: Path) -> Path:
|
|
|
204
221
|
"""
|
|
205
222
|
global _db_path_mapping # noqa: PLW0603
|
|
206
223
|
if _db_path_mapping is None:
|
|
207
|
-
_db_path_mapping =
|
|
224
|
+
_db_path_mapping = _parse_path_mapping(_ENV_DB_PATH_MAPPING)
|
|
208
225
|
|
|
209
226
|
resolved = project_root.resolve()
|
|
210
227
|
for mapping in _db_path_mapping:
|
|
@@ -214,20 +231,52 @@ def resolve_db_dir(project_root: Path) -> Path:
|
|
|
214
231
|
return project_root / _SETTINGS_DIR_NAME
|
|
215
232
|
|
|
216
233
|
|
|
217
|
-
def get_db_path_mappings() -> list[
|
|
234
|
+
def get_db_path_mappings() -> list[PathMapping]:
|
|
218
235
|
"""Return the parsed DB path mappings from ``COCOINDEX_CODE_DB_PATH_MAPPING``."""
|
|
219
236
|
global _db_path_mapping # noqa: PLW0603
|
|
220
237
|
if _db_path_mapping is None:
|
|
221
|
-
_db_path_mapping =
|
|
238
|
+
_db_path_mapping = _parse_path_mapping(_ENV_DB_PATH_MAPPING)
|
|
222
239
|
return list(_db_path_mapping)
|
|
223
240
|
|
|
224
241
|
|
|
242
|
+
def get_host_path_mappings() -> list[PathMapping]:
|
|
243
|
+
"""Return the parsed host path mappings from ``COCOINDEX_CODE_HOST_PATH_MAPPING``."""
|
|
244
|
+
global _host_path_mapping # noqa: PLW0603
|
|
245
|
+
if _host_path_mapping is None:
|
|
246
|
+
_host_path_mapping = _parse_path_mapping(_ENV_HOST_PATH_MAPPING)
|
|
247
|
+
return list(_host_path_mapping)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def format_path_for_display(p: str | Path) -> str:
|
|
251
|
+
"""Translate a container path to its host equivalent for user-facing output.
|
|
252
|
+
|
|
253
|
+
No-op when ``COCOINDEX_CODE_HOST_PATH_MAPPING`` is unset or when ``p`` is a
|
|
254
|
+
relative path / unmatched absolute path.
|
|
255
|
+
"""
|
|
256
|
+
return _apply_mapping(get_host_path_mappings(), p, reverse=False)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def normalize_input_path(p: str | Path) -> str:
|
|
260
|
+
"""Translate a host path back to its container form before using it internally.
|
|
261
|
+
|
|
262
|
+
Inverse of :func:`format_path_for_display`. No-op when the env var is unset
|
|
263
|
+
or when ``p`` is relative / unmatched.
|
|
264
|
+
"""
|
|
265
|
+
return _apply_mapping(get_host_path_mappings(), p, reverse=True)
|
|
266
|
+
|
|
267
|
+
|
|
225
268
|
def _reset_db_path_mapping_cache() -> None:
|
|
226
269
|
"""Reset the cached mapping (for tests)."""
|
|
227
270
|
global _db_path_mapping # noqa: PLW0603
|
|
228
271
|
_db_path_mapping = None
|
|
229
272
|
|
|
230
273
|
|
|
274
|
+
def _reset_host_path_mapping_cache() -> None:
|
|
275
|
+
"""Reset the cached mapping (for tests)."""
|
|
276
|
+
global _host_path_mapping # noqa: PLW0603
|
|
277
|
+
_host_path_mapping = None
|
|
278
|
+
|
|
279
|
+
|
|
231
280
|
_TARGET_SQLITE_DB_NAME = "target_sqlite.db"
|
|
232
281
|
_COCOINDEX_DB_NAME = "cocoindex.db"
|
|
233
282
|
|
|
@@ -295,22 +344,27 @@ def find_legacy_project_root(start: Path) -> Path | None:
|
|
|
295
344
|
|
|
296
345
|
|
|
297
346
|
def find_parent_with_marker(start: Path) -> Path | None:
|
|
298
|
-
"""Walk up from *start* looking for
|
|
347
|
+
"""Walk up from *start* looking for an initialized project or a git repo.
|
|
348
|
+
|
|
349
|
+
Match criteria: ``.cocoindex_code/settings.yml`` (a real project marker —
|
|
350
|
+
distinct from a workspace-root ``.cocoindex_code/global_settings.yml``
|
|
351
|
+
which should not trigger this check) or ``.git/``.
|
|
299
352
|
|
|
300
|
-
Returns the first directory found, or ``None``.
|
|
301
|
-
|
|
302
|
-
|
|
353
|
+
Returns the first directory found, or ``None``. Does not consider the home
|
|
354
|
+
directory or above, to avoid false positives on CI runners where ~/.git
|
|
355
|
+
may exist.
|
|
303
356
|
"""
|
|
304
357
|
home = Path.home().resolve()
|
|
305
358
|
current = start.resolve()
|
|
306
359
|
while True:
|
|
307
|
-
# Stop before reaching the home directory (home itself is not a project root)
|
|
308
360
|
if current == home:
|
|
309
361
|
return None
|
|
310
362
|
parent = current.parent
|
|
311
363
|
if parent == current:
|
|
312
364
|
return None
|
|
313
|
-
if (current / _SETTINGS_DIR_NAME).
|
|
365
|
+
if (current / _SETTINGS_DIR_NAME / _SETTINGS_FILE_NAME).is_file() or (
|
|
366
|
+
current / ".git"
|
|
367
|
+
).is_dir():
|
|
314
368
|
return current
|
|
315
369
|
current = parent
|
|
316
370
|
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
"""Daemon filesystem paths and connection helpers.
|
|
2
|
-
|
|
3
|
-
Lightweight module with no cocoindex dependency so that the CLI client
|
|
4
|
-
can import these without pulling in the full daemon stack.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
|
|
9
|
-
import sys
|
|
10
|
-
from pathlib import Path
|
|
11
|
-
|
|
12
|
-
from .settings import user_settings_dir
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def daemon_dir() -> Path:
|
|
16
|
-
"""Return the daemon directory (``~/.cocoindex_code/``)."""
|
|
17
|
-
return user_settings_dir()
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def connection_family() -> str:
|
|
21
|
-
"""Return the multiprocessing connection family for this platform."""
|
|
22
|
-
return "AF_PIPE" if sys.platform == "win32" else "AF_UNIX"
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def daemon_socket_path() -> str:
|
|
26
|
-
"""Return the daemon socket/pipe address."""
|
|
27
|
-
if sys.platform == "win32":
|
|
28
|
-
import hashlib
|
|
29
|
-
|
|
30
|
-
# Hash the daemon dir so COCOINDEX_CODE_DIR overrides create unique pipe names,
|
|
31
|
-
# preventing conflicts between different daemon instances (tests, users, etc.)
|
|
32
|
-
dir_hash = hashlib.md5(str(daemon_dir()).encode()).hexdigest()[:12]
|
|
33
|
-
return rf"\\.\pipe\cocoindex_code_{dir_hash}"
|
|
34
|
-
return str(daemon_dir() / "daemon.sock")
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def daemon_pid_path() -> Path:
|
|
38
|
-
"""Return the path for the daemon's PID file."""
|
|
39
|
-
return daemon_dir() / "daemon.pid"
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def daemon_log_path() -> Path:
|
|
43
|
-
"""Return the path for the daemon's log file."""
|
|
44
|
-
return daemon_dir() / "daemon.log"
|
|
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
|