cocoindex-code 0.2.37__tar.gz → 0.2.39__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.37 → cocoindex_code-0.2.39}/PKG-INFO +86 -4
- {cocoindex_code-0.2.37 → cocoindex_code-0.2.39}/README.md +85 -3
- {cocoindex_code-0.2.37 → cocoindex_code-0.2.39}/src/cocoindex_code/_daemon_paths.py +50 -0
- {cocoindex_code-0.2.37 → cocoindex_code-0.2.39}/src/cocoindex_code/_version.py +2 -2
- {cocoindex_code-0.2.37 → cocoindex_code-0.2.39}/src/cocoindex_code/cli.py +59 -19
- {cocoindex_code-0.2.37 → cocoindex_code-0.2.39}/src/cocoindex_code/client.py +145 -21
- {cocoindex_code-0.2.37 → cocoindex_code-0.2.39}/src/cocoindex_code/daemon.py +172 -19
- {cocoindex_code-0.2.37 → cocoindex_code-0.2.39}/src/cocoindex_code/protocol.py +33 -0
- {cocoindex_code-0.2.37 → cocoindex_code-0.2.39}/src/cocoindex_code/server.py +50 -2
- {cocoindex_code-0.2.37 → cocoindex_code-0.2.39}/src/cocoindex_code/settings.py +18 -1
- {cocoindex_code-0.2.37 → cocoindex_code-0.2.39}/.gitignore +0 -0
- {cocoindex_code-0.2.37 → cocoindex_code-0.2.39}/LICENSE +0 -0
- {cocoindex_code-0.2.37 → cocoindex_code-0.2.39}/pyproject.toml +0 -0
- {cocoindex_code-0.2.37 → cocoindex_code-0.2.39}/src/cocoindex_code/__init__.py +0 -0
- {cocoindex_code-0.2.37 → cocoindex_code-0.2.39}/src/cocoindex_code/__main__.py +0 -0
- {cocoindex_code-0.2.37 → cocoindex_code-0.2.39}/src/cocoindex_code/chunking.py +0 -0
- {cocoindex_code-0.2.37 → cocoindex_code-0.2.39}/src/cocoindex_code/embedder_defaults.py +0 -0
- {cocoindex_code-0.2.37 → cocoindex_code-0.2.39}/src/cocoindex_code/embedder_params.py +0 -0
- {cocoindex_code-0.2.37 → cocoindex_code-0.2.39}/src/cocoindex_code/file_walk.py +0 -0
- {cocoindex_code-0.2.37 → cocoindex_code-0.2.39}/src/cocoindex_code/grep.py +0 -0
- {cocoindex_code-0.2.37 → cocoindex_code-0.2.39}/src/cocoindex_code/indexer.py +0 -0
- {cocoindex_code-0.2.37 → cocoindex_code-0.2.39}/src/cocoindex_code/litellm_embedder.py +0 -0
- {cocoindex_code-0.2.37 → cocoindex_code-0.2.39}/src/cocoindex_code/project.py +0 -0
- {cocoindex_code-0.2.37 → cocoindex_code-0.2.39}/src/cocoindex_code/query.py +0 -0
- {cocoindex_code-0.2.37 → cocoindex_code-0.2.39}/src/cocoindex_code/schema.py +0 -0
- {cocoindex_code-0.2.37 → cocoindex_code-0.2.39}/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.39
|
|
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
|
|
@@ -107,6 +107,8 @@ Next, set up your [coding agent integration](#coding-agent-integration) — or j
|
|
|
107
107
|
|
|
108
108
|
## Coding Agent Integration
|
|
109
109
|
|
|
110
|
+
This repository is a **single plugin marketplace** (`.claude-plugin/marketplace.json`) consumed by both **Claude Code** and **Grok** — same plugin id `cocoindex-code`, same `ccc` skill. Grok optionally activates the bundled hooks and MCP server with `--trust`; Claude Code users can install the same marketplace and rely on the skill alone or load hooks/MCP from the plugin as needed.
|
|
111
|
+
|
|
110
112
|
### Skill (Recommended)
|
|
111
113
|
|
|
112
114
|
Install the `ccc` skill so your coding agent automatically uses semantic search when needed:
|
|
@@ -126,11 +128,59 @@ Works with [Claude Code](https://docs.anthropic.com/en/docs/claude-code) and oth
|
|
|
126
128
|
For Claude Code users, this repository is also a [plugin marketplace](https://code.claude.com/docs/en/plugin-marketplaces). Install the skill from inside Claude Code with:
|
|
127
129
|
|
|
128
130
|
```text
|
|
129
|
-
/plugin marketplace add
|
|
131
|
+
/plugin marketplace add cocoindex-io/cocoindex-code
|
|
130
132
|
/plugin install cocoindex-code@cocoindex-code
|
|
131
133
|
```
|
|
132
134
|
|
|
133
|
-
This bundles the same `ccc` skill, with version pinning and `/plugin marketplace update` for updates.
|
|
135
|
+
This bundles the same `ccc` skill, with version pinning and `/plugin marketplace update` for updates. The repository also ships `hooks/hooks.json` and `.mcp.json` for Grok (and Claude Code plugin installs that load those files); Claude users who want skill-only search can rely on the skill alone and add MCP manually in the [MCP Server](#mcp-server) section below instead of using the bundled `.mcp.json`.
|
|
136
|
+
|
|
137
|
+
#### Grok plugin
|
|
138
|
+
|
|
139
|
+
For [Grok](https://github.com/xai-org/grok) users, install via Grok's plugin system. The plugin bundles three components:
|
|
140
|
+
|
|
141
|
+
| Component | Purpose |
|
|
142
|
+
|-----------|---------|
|
|
143
|
+
| **Skill** (`skills/ccc/`) | Agent runs `ccc search` / `ccc index` via the CLI (same as Claude Code above) |
|
|
144
|
+
| **Hook** (`hooks/hooks.json`) | `SessionStart` → incremental `ccc index` when `.cocoindex_code/` exists |
|
|
145
|
+
| **MCP** (`.mcp.json`) | `ccc mcp` stdio server — `search` tool with `refresh_index=true` by default |
|
|
146
|
+
|
|
147
|
+
Grok does **not** import Claude's `enabledPlugins` or plugin cache; install separately even if you already use cocoindex in Claude Code.
|
|
148
|
+
|
|
149
|
+
**Full install** (skill + hook + MCP):
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
grok plugin marketplace add cocoindex-io/cocoindex-code
|
|
153
|
+
grok plugin install cocoindex-io/cocoindex-code --trust
|
|
154
|
+
grok plugin enable cocoindex-code
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Prefer the GitHub shorthand (`cocoindex-io/cocoindex-code`) for install — `grok plugin install cocoindex-code` can fail when no marketplace plugin matches that bare name.
|
|
158
|
+
|
|
159
|
+
`--trust` is required so Grok activates the plugin's hooks and MCP server (skills load when the plugin is enabled).
|
|
160
|
+
|
|
161
|
+
**Skill-only** (match Claude Code — no auto-index hook, no MCP tool):
|
|
162
|
+
|
|
163
|
+
Install and enable as above, then disable the optional components:
|
|
164
|
+
|
|
165
|
+
1. **Hooks** — open `/hooks`, select the `SessionStart` hook from `cocoindex-code`, press `Space` to disable.
|
|
166
|
+
2. **MCP** — open `/mcps`, select `cocoindex-code`, press `Space` to disable; or persist in `~/.grok/config.toml`:
|
|
167
|
+
|
|
168
|
+
```toml
|
|
169
|
+
[mcp_servers.cocoindex-code]
|
|
170
|
+
enabled = false
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
The agent still owns indexing via the `ccc` skill (`ccc index` / `ccc search --refresh` when stale), same as Claude Code.
|
|
174
|
+
|
|
175
|
+
To avoid importing MCP servers from your Claude/Cursor user config (unrelated to this plugin):
|
|
176
|
+
|
|
177
|
+
```toml
|
|
178
|
+
[compat.claude]
|
|
179
|
+
mcps = false
|
|
180
|
+
|
|
181
|
+
[compat.cursor]
|
|
182
|
+
mcps = false
|
|
183
|
+
```
|
|
134
184
|
|
|
135
185
|
### MCP Server
|
|
136
186
|
|
|
@@ -178,6 +228,24 @@ Or use opencode.json:
|
|
|
178
228
|
```
|
|
179
229
|
</details>
|
|
180
230
|
|
|
231
|
+
<details>
|
|
232
|
+
<summary>Kilo Code</summary>
|
|
233
|
+
|
|
234
|
+
Add a local MCP server in `~/.config/kilo/kilo.jsonc`, `kilo.jsonc`, or `.kilo/kilo.jsonc`:
|
|
235
|
+
|
|
236
|
+
```json
|
|
237
|
+
{
|
|
238
|
+
"mcp": {
|
|
239
|
+
"cocoindex-code": {
|
|
240
|
+
"type": "local",
|
|
241
|
+
"command": ["ccc", "mcp"],
|
|
242
|
+
"enabled": true
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
</details>
|
|
248
|
+
|
|
181
249
|
Once configured, the agent automatically decides when semantic code search is helpful — finding code by description, exploring unfamiliar codebases, fuzzy/conceptual matches, or locating implementations without knowing exact names.
|
|
182
250
|
|
|
183
251
|
> **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.
|
|
@@ -215,7 +283,7 @@ ccc search "authentication logic" # search!
|
|
|
215
283
|
|
|
216
284
|
The background daemon starts automatically on first use.
|
|
217
285
|
|
|
218
|
-
> **Tip:** `ccc index` auto-initializes
|
|
286
|
+
> **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.
|
|
219
287
|
|
|
220
288
|
### CLI Reference
|
|
221
289
|
|
|
@@ -491,10 +559,15 @@ embedding:
|
|
|
491
559
|
|
|
492
560
|
envs: # extra environment variables for the daemon
|
|
493
561
|
OPENAI_API_KEY: your-key # only needed if not already in your shell environment
|
|
562
|
+
|
|
563
|
+
daemon:
|
|
564
|
+
idle_timeout_minutes: 180 # optional: exit the daemon after this long without client activity (default 180, 0 = never)
|
|
494
565
|
```
|
|
495
566
|
|
|
496
567
|
> **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.
|
|
497
568
|
|
|
569
|
+
> **Idle timeout:** the background daemon holds the embedding model in RAM, so it exits after `daemon.idle_timeout_minutes` without client activity and is restarted automatically on your next `ccc` command or MCP search. A live MCP session sends periodic heartbeats, so the daemon never idles out while your coding agent is connected. Set `0` to keep the daemon running forever.
|
|
570
|
+
|
|
498
571
|
> **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).
|
|
499
572
|
|
|
500
573
|
#### `indexing_params` / `query_params`
|
|
@@ -552,6 +625,14 @@ chunkers:
|
|
|
552
625
|
|
|
553
626
|
> `.cocoindex_code/` is automatically added to `.gitignore` during init.
|
|
554
627
|
|
|
628
|
+
After editing `include_patterns`, `exclude_patterns`, or `language_overrides`:
|
|
629
|
+
|
|
630
|
+
- Run `ccc doctor` to preview which files match.
|
|
631
|
+
- Run `ccc index` or `ccc search --refresh ...` to update the existing index.
|
|
632
|
+
- You do not need to delete the index or restart the daemon for these file-matching changes.
|
|
633
|
+
|
|
634
|
+
If you add or change custom `chunkers`, restart the daemon first so the chunker registry is reloaded, then run `ccc index`.
|
|
635
|
+
|
|
555
636
|
Use `chunkers` when you want to control how a file type is split into chunks before indexing.
|
|
556
637
|
|
|
557
638
|
`module: example_toml_chunker:toml_chunker` means:
|
|
@@ -750,6 +831,7 @@ embedding:
|
|
|
750
831
|
| cpp | c++ | `.cpp`, `.cc`, `.cxx`, `.h`, `.hpp` |
|
|
751
832
|
| csharp | csharp, cs | `.cs` |
|
|
752
833
|
| css | | `.css`, `.scss` |
|
|
834
|
+
| dart | | `.dart` |
|
|
753
835
|
| dtd | | `.dtd` |
|
|
754
836
|
| fortran | f, f90, f95, f03 | `.f`, `.f90`, `.f95`, `.f03` |
|
|
755
837
|
| go | golang | `.go` |
|
|
@@ -63,6 +63,8 @@ Next, set up your [coding agent integration](#coding-agent-integration) — or j
|
|
|
63
63
|
|
|
64
64
|
## Coding Agent Integration
|
|
65
65
|
|
|
66
|
+
This repository is a **single plugin marketplace** (`.claude-plugin/marketplace.json`) consumed by both **Claude Code** and **Grok** — same plugin id `cocoindex-code`, same `ccc` skill. Grok optionally activates the bundled hooks and MCP server with `--trust`; Claude Code users can install the same marketplace and rely on the skill alone or load hooks/MCP from the plugin as needed.
|
|
67
|
+
|
|
66
68
|
### Skill (Recommended)
|
|
67
69
|
|
|
68
70
|
Install the `ccc` skill so your coding agent automatically uses semantic search when needed:
|
|
@@ -82,11 +84,59 @@ Works with [Claude Code](https://docs.anthropic.com/en/docs/claude-code) and oth
|
|
|
82
84
|
For Claude Code users, this repository is also a [plugin marketplace](https://code.claude.com/docs/en/plugin-marketplaces). Install the skill from inside Claude Code with:
|
|
83
85
|
|
|
84
86
|
```text
|
|
85
|
-
/plugin marketplace add
|
|
87
|
+
/plugin marketplace add cocoindex-io/cocoindex-code
|
|
86
88
|
/plugin install cocoindex-code@cocoindex-code
|
|
87
89
|
```
|
|
88
90
|
|
|
89
|
-
This bundles the same `ccc` skill, with version pinning and `/plugin marketplace update` for updates.
|
|
91
|
+
This bundles the same `ccc` skill, with version pinning and `/plugin marketplace update` for updates. The repository also ships `hooks/hooks.json` and `.mcp.json` for Grok (and Claude Code plugin installs that load those files); Claude users who want skill-only search can rely on the skill alone and add MCP manually in the [MCP Server](#mcp-server) section below instead of using the bundled `.mcp.json`.
|
|
92
|
+
|
|
93
|
+
#### Grok plugin
|
|
94
|
+
|
|
95
|
+
For [Grok](https://github.com/xai-org/grok) users, install via Grok's plugin system. The plugin bundles three components:
|
|
96
|
+
|
|
97
|
+
| Component | Purpose |
|
|
98
|
+
|-----------|---------|
|
|
99
|
+
| **Skill** (`skills/ccc/`) | Agent runs `ccc search` / `ccc index` via the CLI (same as Claude Code above) |
|
|
100
|
+
| **Hook** (`hooks/hooks.json`) | `SessionStart` → incremental `ccc index` when `.cocoindex_code/` exists |
|
|
101
|
+
| **MCP** (`.mcp.json`) | `ccc mcp` stdio server — `search` tool with `refresh_index=true` by default |
|
|
102
|
+
|
|
103
|
+
Grok does **not** import Claude's `enabledPlugins` or plugin cache; install separately even if you already use cocoindex in Claude Code.
|
|
104
|
+
|
|
105
|
+
**Full install** (skill + hook + MCP):
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
grok plugin marketplace add cocoindex-io/cocoindex-code
|
|
109
|
+
grok plugin install cocoindex-io/cocoindex-code --trust
|
|
110
|
+
grok plugin enable cocoindex-code
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Prefer the GitHub shorthand (`cocoindex-io/cocoindex-code`) for install — `grok plugin install cocoindex-code` can fail when no marketplace plugin matches that bare name.
|
|
114
|
+
|
|
115
|
+
`--trust` is required so Grok activates the plugin's hooks and MCP server (skills load when the plugin is enabled).
|
|
116
|
+
|
|
117
|
+
**Skill-only** (match Claude Code — no auto-index hook, no MCP tool):
|
|
118
|
+
|
|
119
|
+
Install and enable as above, then disable the optional components:
|
|
120
|
+
|
|
121
|
+
1. **Hooks** — open `/hooks`, select the `SessionStart` hook from `cocoindex-code`, press `Space` to disable.
|
|
122
|
+
2. **MCP** — open `/mcps`, select `cocoindex-code`, press `Space` to disable; or persist in `~/.grok/config.toml`:
|
|
123
|
+
|
|
124
|
+
```toml
|
|
125
|
+
[mcp_servers.cocoindex-code]
|
|
126
|
+
enabled = false
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
The agent still owns indexing via the `ccc` skill (`ccc index` / `ccc search --refresh` when stale), same as Claude Code.
|
|
130
|
+
|
|
131
|
+
To avoid importing MCP servers from your Claude/Cursor user config (unrelated to this plugin):
|
|
132
|
+
|
|
133
|
+
```toml
|
|
134
|
+
[compat.claude]
|
|
135
|
+
mcps = false
|
|
136
|
+
|
|
137
|
+
[compat.cursor]
|
|
138
|
+
mcps = false
|
|
139
|
+
```
|
|
90
140
|
|
|
91
141
|
### MCP Server
|
|
92
142
|
|
|
@@ -134,6 +184,24 @@ Or use opencode.json:
|
|
|
134
184
|
```
|
|
135
185
|
</details>
|
|
136
186
|
|
|
187
|
+
<details>
|
|
188
|
+
<summary>Kilo Code</summary>
|
|
189
|
+
|
|
190
|
+
Add a local MCP server in `~/.config/kilo/kilo.jsonc`, `kilo.jsonc`, or `.kilo/kilo.jsonc`:
|
|
191
|
+
|
|
192
|
+
```json
|
|
193
|
+
{
|
|
194
|
+
"mcp": {
|
|
195
|
+
"cocoindex-code": {
|
|
196
|
+
"type": "local",
|
|
197
|
+
"command": ["ccc", "mcp"],
|
|
198
|
+
"enabled": true
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
</details>
|
|
204
|
+
|
|
137
205
|
Once configured, the agent automatically decides when semantic code search is helpful — finding code by description, exploring unfamiliar codebases, fuzzy/conceptual matches, or locating implementations without knowing exact names.
|
|
138
206
|
|
|
139
207
|
> **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.
|
|
@@ -171,7 +239,7 @@ ccc search "authentication logic" # search!
|
|
|
171
239
|
|
|
172
240
|
The background daemon starts automatically on first use.
|
|
173
241
|
|
|
174
|
-
> **Tip:** `ccc index` auto-initializes
|
|
242
|
+
> **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.
|
|
175
243
|
|
|
176
244
|
### CLI Reference
|
|
177
245
|
|
|
@@ -447,10 +515,15 @@ embedding:
|
|
|
447
515
|
|
|
448
516
|
envs: # extra environment variables for the daemon
|
|
449
517
|
OPENAI_API_KEY: your-key # only needed if not already in your shell environment
|
|
518
|
+
|
|
519
|
+
daemon:
|
|
520
|
+
idle_timeout_minutes: 180 # optional: exit the daemon after this long without client activity (default 180, 0 = never)
|
|
450
521
|
```
|
|
451
522
|
|
|
452
523
|
> **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.
|
|
453
524
|
|
|
525
|
+
> **Idle timeout:** the background daemon holds the embedding model in RAM, so it exits after `daemon.idle_timeout_minutes` without client activity and is restarted automatically on your next `ccc` command or MCP search. A live MCP session sends periodic heartbeats, so the daemon never idles out while your coding agent is connected. Set `0` to keep the daemon running forever.
|
|
526
|
+
|
|
454
527
|
> **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).
|
|
455
528
|
|
|
456
529
|
#### `indexing_params` / `query_params`
|
|
@@ -508,6 +581,14 @@ chunkers:
|
|
|
508
581
|
|
|
509
582
|
> `.cocoindex_code/` is automatically added to `.gitignore` during init.
|
|
510
583
|
|
|
584
|
+
After editing `include_patterns`, `exclude_patterns`, or `language_overrides`:
|
|
585
|
+
|
|
586
|
+
- Run `ccc doctor` to preview which files match.
|
|
587
|
+
- Run `ccc index` or `ccc search --refresh ...` to update the existing index.
|
|
588
|
+
- You do not need to delete the index or restart the daemon for these file-matching changes.
|
|
589
|
+
|
|
590
|
+
If you add or change custom `chunkers`, restart the daemon first so the chunker registry is reloaded, then run `ccc index`.
|
|
591
|
+
|
|
511
592
|
Use `chunkers` when you want to control how a file type is split into chunks before indexing.
|
|
512
593
|
|
|
513
594
|
`module: example_toml_chunker:toml_chunker` means:
|
|
@@ -706,6 +787,7 @@ embedding:
|
|
|
706
787
|
| cpp | c++ | `.cpp`, `.cc`, `.cxx`, `.h`, `.hpp` |
|
|
707
788
|
| csharp | csharp, cs | `.cs` |
|
|
708
789
|
| css | | `.css`, `.scss` |
|
|
790
|
+
| dart | | `.dart` |
|
|
709
791
|
| dtd | | `.dtd` |
|
|
710
792
|
| fortran | f, f90, f95, f03 | `.f`, `.f90`, `.f95`, `.f03` |
|
|
711
793
|
| go | golang | `.go` |
|
|
@@ -6,8 +6,11 @@ can import these without pulling in the full daemon stack.
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
+
import json
|
|
9
10
|
import os
|
|
10
11
|
import sys
|
|
12
|
+
import time
|
|
13
|
+
from dataclasses import dataclass
|
|
11
14
|
from pathlib import Path
|
|
12
15
|
|
|
13
16
|
from .settings import user_settings_dir
|
|
@@ -58,3 +61,50 @@ def daemon_pid_path() -> Path:
|
|
|
58
61
|
def daemon_log_path() -> Path:
|
|
59
62
|
"""Return the path for the daemon's log file."""
|
|
60
63
|
return daemon_runtime_dir() / "daemon.log"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def daemon_last_exit_path() -> Path:
|
|
67
|
+
"""Return the path for the daemon's graceful-exit marker file."""
|
|
68
|
+
return daemon_runtime_dir() / "last_exit"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass
|
|
72
|
+
class LastExitMarker:
|
|
73
|
+
"""Record of the most recent *graceful* daemon exit.
|
|
74
|
+
|
|
75
|
+
Written by the daemon's graceful shutdown path (``StopRequest``,
|
|
76
|
+
SIGTERM/SIGINT) and removed again at the next daemon startup. A crashed
|
|
77
|
+
daemon (SIGKILL, OOM, segfault) never writes it — that absence is how the
|
|
78
|
+
client tells a crash from a graceful exit.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
pid: int
|
|
82
|
+
reason: str
|
|
83
|
+
timestamp: float
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def write_last_exit_marker(pid: int, reason: str) -> None:
|
|
87
|
+
"""Persist the graceful-exit marker. Best-effort — never raises."""
|
|
88
|
+
payload = json.dumps({"pid": pid, "reason": reason, "timestamp": time.time()})
|
|
89
|
+
try:
|
|
90
|
+
daemon_last_exit_path().write_text(payload)
|
|
91
|
+
except OSError:
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def read_last_exit_marker() -> LastExitMarker | None:
|
|
96
|
+
"""Read the graceful-exit marker, or None when absent/unreadable."""
|
|
97
|
+
try:
|
|
98
|
+
data = json.loads(daemon_last_exit_path().read_text())
|
|
99
|
+
return LastExitMarker(
|
|
100
|
+
pid=int(data["pid"]),
|
|
101
|
+
reason=str(data["reason"]),
|
|
102
|
+
timestamp=float(data["timestamp"]),
|
|
103
|
+
)
|
|
104
|
+
except (OSError, ValueError, KeyError, TypeError):
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def clear_last_exit_marker() -> None:
|
|
109
|
+
"""Remove the marker (daemon startup) so it always refers to the most recent exit."""
|
|
110
|
+
daemon_last_exit_path().unlink(missing_ok=True)
|
|
@@ -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.39'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 39)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -10,6 +10,7 @@ from collections.abc import Callable
|
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
from typing import TYPE_CHECKING, TypeVar
|
|
12
12
|
|
|
13
|
+
import msgspec as _msgspec
|
|
13
14
|
import typer as _typer
|
|
14
15
|
|
|
15
16
|
if TYPE_CHECKING:
|
|
@@ -76,22 +77,31 @@ def _apply_host_cwd() -> None:
|
|
|
76
77
|
# ---------------------------------------------------------------------------
|
|
77
78
|
|
|
78
79
|
|
|
79
|
-
def require_project_root() -> Path:
|
|
80
|
+
def require_project_root(*, auto_init: bool = False) -> Path:
|
|
80
81
|
"""Find the project root by walking up from CWD.
|
|
81
82
|
|
|
82
83
|
Checks global settings first (more fundamental), then project settings.
|
|
83
|
-
|
|
84
|
+
With ``auto_init``, a missing project is initialized with default settings
|
|
85
|
+
instead of failing. Missing global settings run the same interactive model
|
|
86
|
+
setup as ``ccc init`` — but only on a TTY, since picking an embedding model
|
|
87
|
+
is a consequential choice; non-interactive runs (scripts, hooks, agents)
|
|
88
|
+
still exit with code 1 rather than silently committing to a default model.
|
|
84
89
|
"""
|
|
85
90
|
gs_path = user_settings_path()
|
|
86
91
|
if not gs_path.is_file():
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
92
|
+
if auto_init and sys.stdin.isatty():
|
|
93
|
+
_setup_user_settings_interactive(None)
|
|
94
|
+
else:
|
|
95
|
+
_typer.echo(
|
|
96
|
+
f"Error: Global settings not found: {format_path_for_display(gs_path)}\n"
|
|
97
|
+
"Run `ccc init` to create it with default settings.",
|
|
98
|
+
err=True,
|
|
99
|
+
)
|
|
100
|
+
raise _typer.Exit(code=1)
|
|
93
101
|
root = find_project_root(Path.cwd())
|
|
94
102
|
if root is None:
|
|
103
|
+
if auto_init:
|
|
104
|
+
return _auto_init_project(Path.cwd())
|
|
95
105
|
_typer.echo(
|
|
96
106
|
"Error: Not in an initialized project directory.\n"
|
|
97
107
|
"Run `ccc init` in your project root to get started.",
|
|
@@ -101,6 +111,24 @@ def require_project_root() -> Path:
|
|
|
101
111
|
return root
|
|
102
112
|
|
|
103
113
|
|
|
114
|
+
def _auto_init_project(cwd: Path) -> Path:
|
|
115
|
+
"""Create default project settings without an explicit ``ccc init``.
|
|
116
|
+
|
|
117
|
+
Anchors at the nearest parent git root when there is one, so running from
|
|
118
|
+
a repo subdirectory initializes the repo root rather than the subdirectory.
|
|
119
|
+
"""
|
|
120
|
+
root = find_parent_with_marker(cwd) or cwd
|
|
121
|
+
_create_project_settings(root)
|
|
122
|
+
return root
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _create_project_settings(root: Path) -> None:
|
|
126
|
+
"""Write default project settings at *root* and gitignore the settings dir."""
|
|
127
|
+
save_project_settings(root, default_project_settings())
|
|
128
|
+
add_to_gitignore(root)
|
|
129
|
+
_typer.echo(f"Created project settings: {format_path_for_display(project_settings_path(root))}")
|
|
130
|
+
|
|
131
|
+
|
|
104
132
|
_F = TypeVar("_F", bound=Callable[..., object])
|
|
105
133
|
|
|
106
134
|
|
|
@@ -599,12 +627,7 @@ def init(
|
|
|
599
627
|
)
|
|
600
628
|
raise _typer.Exit(code=1)
|
|
601
629
|
|
|
602
|
-
|
|
603
|
-
save_project_settings(cwd, default_project_settings())
|
|
604
|
-
_typer.echo(f"Created project settings: {format_path_for_display(settings_file)}")
|
|
605
|
-
|
|
606
|
-
# Add to .gitignore
|
|
607
|
-
add_to_gitignore(cwd)
|
|
630
|
+
_create_project_settings(cwd)
|
|
608
631
|
|
|
609
632
|
_typer.echo("You can edit the settings files to customize indexing behavior.")
|
|
610
633
|
_typer.echo("Run `ccc index` to build the index.")
|
|
@@ -616,7 +639,7 @@ def index() -> None:
|
|
|
616
639
|
"""Create/update index for the codebase."""
|
|
617
640
|
from . import client as _client
|
|
618
641
|
|
|
619
|
-
project_root = str(require_project_root())
|
|
642
|
+
project_root = str(require_project_root(auto_init=True))
|
|
620
643
|
print_project_header(project_root)
|
|
621
644
|
_run_index_with_progress(project_root)
|
|
622
645
|
print_index_stats(_client.project_status(project_root))
|
|
@@ -631,6 +654,7 @@ def search(
|
|
|
631
654
|
offset: int = _typer.Option(0, "--offset", help="Number of results to skip"),
|
|
632
655
|
limit: int = _typer.Option(10, "--limit", help="Maximum results to return"),
|
|
633
656
|
refresh: bool = _typer.Option(False, "--refresh", help="Refresh index before searching"),
|
|
657
|
+
json_output: bool = _typer.Option(False, "--json", help="Output results as JSON"),
|
|
634
658
|
) -> None:
|
|
635
659
|
"""Semantic search across the codebase."""
|
|
636
660
|
project_root = str(require_project_root())
|
|
@@ -656,7 +680,14 @@ def search(
|
|
|
656
680
|
limit=limit,
|
|
657
681
|
offset=offset,
|
|
658
682
|
)
|
|
659
|
-
|
|
683
|
+
if json_output:
|
|
684
|
+
sys.stdout.buffer.write(_msgspec.json.encode(resp))
|
|
685
|
+
sys.stdout.buffer.write(b"\n")
|
|
686
|
+
sys.stdout.buffer.flush()
|
|
687
|
+
if not resp.success:
|
|
688
|
+
raise _typer.Exit(code=1)
|
|
689
|
+
else:
|
|
690
|
+
print_search_results(resp)
|
|
660
691
|
|
|
661
692
|
|
|
662
693
|
@app.command()
|
|
@@ -994,11 +1025,18 @@ def mcp() -> None:
|
|
|
994
1025
|
project_root = str(require_project_root())
|
|
995
1026
|
|
|
996
1027
|
async def _run_mcp() -> None:
|
|
997
|
-
from .server import create_mcp_server
|
|
1028
|
+
from .server import create_mcp_server, run_heartbeat_loop
|
|
998
1029
|
|
|
999
1030
|
mcp_server = create_mcp_server(project_root)
|
|
1000
|
-
|
|
1001
|
-
|
|
1031
|
+
background_tasks = {
|
|
1032
|
+
asyncio.create_task(_bg_index(project_root)),
|
|
1033
|
+
asyncio.create_task(run_heartbeat_loop()),
|
|
1034
|
+
}
|
|
1035
|
+
try:
|
|
1036
|
+
await mcp_server.run_stdio_async()
|
|
1037
|
+
finally:
|
|
1038
|
+
for task in background_tasks:
|
|
1039
|
+
task.cancel()
|
|
1002
1040
|
|
|
1003
1041
|
asyncio.run(_run_mcp())
|
|
1004
1042
|
|
|
@@ -1028,6 +1066,8 @@ def daemon_status() -> None:
|
|
|
1028
1066
|
resp = _client.daemon_status()
|
|
1029
1067
|
_typer.echo(f"Daemon version: {resp.version}")
|
|
1030
1068
|
_typer.echo(f"Uptime: {resp.uptime_seconds:.1f}s")
|
|
1069
|
+
timeout_desc = f"{resp.idle_timeout_minutes}m" if resp.idle_timeout_minutes > 0 else "disabled"
|
|
1070
|
+
_typer.echo(f"Idle: {resp.idle_seconds:.1f}s (timeout: {timeout_desc})")
|
|
1031
1071
|
if resp.projects:
|
|
1032
1072
|
_typer.echo("Projects:")
|
|
1033
1073
|
for p in resp.projects:
|