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.
Files changed (26) hide show
  1. {cocoindex_code-0.2.37 → cocoindex_code-0.2.39}/PKG-INFO +86 -4
  2. {cocoindex_code-0.2.37 → cocoindex_code-0.2.39}/README.md +85 -3
  3. {cocoindex_code-0.2.37 → cocoindex_code-0.2.39}/src/cocoindex_code/_daemon_paths.py +50 -0
  4. {cocoindex_code-0.2.37 → cocoindex_code-0.2.39}/src/cocoindex_code/_version.py +2 -2
  5. {cocoindex_code-0.2.37 → cocoindex_code-0.2.39}/src/cocoindex_code/cli.py +59 -19
  6. {cocoindex_code-0.2.37 → cocoindex_code-0.2.39}/src/cocoindex_code/client.py +145 -21
  7. {cocoindex_code-0.2.37 → cocoindex_code-0.2.39}/src/cocoindex_code/daemon.py +172 -19
  8. {cocoindex_code-0.2.37 → cocoindex_code-0.2.39}/src/cocoindex_code/protocol.py +33 -0
  9. {cocoindex_code-0.2.37 → cocoindex_code-0.2.39}/src/cocoindex_code/server.py +50 -2
  10. {cocoindex_code-0.2.37 → cocoindex_code-0.2.39}/src/cocoindex_code/settings.py +18 -1
  11. {cocoindex_code-0.2.37 → cocoindex_code-0.2.39}/.gitignore +0 -0
  12. {cocoindex_code-0.2.37 → cocoindex_code-0.2.39}/LICENSE +0 -0
  13. {cocoindex_code-0.2.37 → cocoindex_code-0.2.39}/pyproject.toml +0 -0
  14. {cocoindex_code-0.2.37 → cocoindex_code-0.2.39}/src/cocoindex_code/__init__.py +0 -0
  15. {cocoindex_code-0.2.37 → cocoindex_code-0.2.39}/src/cocoindex_code/__main__.py +0 -0
  16. {cocoindex_code-0.2.37 → cocoindex_code-0.2.39}/src/cocoindex_code/chunking.py +0 -0
  17. {cocoindex_code-0.2.37 → cocoindex_code-0.2.39}/src/cocoindex_code/embedder_defaults.py +0 -0
  18. {cocoindex_code-0.2.37 → cocoindex_code-0.2.39}/src/cocoindex_code/embedder_params.py +0 -0
  19. {cocoindex_code-0.2.37 → cocoindex_code-0.2.39}/src/cocoindex_code/file_walk.py +0 -0
  20. {cocoindex_code-0.2.37 → cocoindex_code-0.2.39}/src/cocoindex_code/grep.py +0 -0
  21. {cocoindex_code-0.2.37 → cocoindex_code-0.2.39}/src/cocoindex_code/indexer.py +0 -0
  22. {cocoindex_code-0.2.37 → cocoindex_code-0.2.39}/src/cocoindex_code/litellm_embedder.py +0 -0
  23. {cocoindex_code-0.2.37 → cocoindex_code-0.2.39}/src/cocoindex_code/project.py +0 -0
  24. {cocoindex_code-0.2.37 → cocoindex_code-0.2.39}/src/cocoindex_code/query.py +0 -0
  25. {cocoindex_code-0.2.37 → cocoindex_code-0.2.39}/src/cocoindex_code/schema.py +0 -0
  26. {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.37
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 Roxabi/cocoindex-code
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 if you haven't run `ccc init` yet, so you can skip straight to indexing.
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 Roxabi/cocoindex-code
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 if you haven't run `ccc init` yet, so you can skip straight to indexing.
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.37'
22
- __version_tuple__ = version_tuple = (0, 2, 37)
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
- Exits with code 1 if either check fails.
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
- _typer.echo(
88
- f"Error: Global settings not found: {format_path_for_display(gs_path)}\n"
89
- "Run `ccc init` to create it with default settings.",
90
- err=True,
91
- )
92
- raise _typer.Exit(code=1)
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
- # Create project settings
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
- print_search_results(resp)
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
- asyncio.create_task(_bg_index(project_root))
1001
- await mcp_server.run_stdio_async()
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: