codecortex 0.17.0__tar.gz → 0.18.0__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.
- {codecortex-0.17.0/src/codecortex.egg-info → codecortex-0.18.0}/PKG-INFO +5 -5
- {codecortex-0.17.0 → codecortex-0.18.0}/README.md +4 -4
- {codecortex-0.17.0 → codecortex-0.18.0/src/codecortex.egg-info}/PKG-INFO +5 -5
- {codecortex-0.17.0 → codecortex-0.18.0}/src/codecortex.egg-info/SOURCES.txt +4 -0
- codecortex-0.18.0/src/codeintel/__init__.py +1 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/auth.py +8 -1
- {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/config.py +24 -2
- {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/containment.py +15 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/indexer.py +160 -31
- codecortex-0.18.0/src/codeintel/paths.py +31 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/providers/semantic.py +108 -7
- codecortex-0.18.0/src/codeintel/redact.py +187 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/searcher.py +125 -32
- {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/semantic_db.py +59 -16
- {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_chunking.py +6 -6
- {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_config.py +63 -3
- {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_rbac.py +73 -0
- codecortex-0.18.0/tests/test_redaction_boundary.py +217 -0
- codecortex-0.18.0/tests/test_staleness.py +265 -0
- codecortex-0.18.0/tests/test_symbol_context.py +172 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_treesitter.py +5 -5
- codecortex-0.17.0/src/codeintel/__init__.py +0 -1
- codecortex-0.17.0/src/codeintel/redact.py +0 -113
- {codecortex-0.17.0 → codecortex-0.18.0}/LICENSE +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/pyproject.toml +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/setup.cfg +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/src/codecortex.egg-info/dependency_links.txt +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/src/codecortex.egg-info/entry_points.txt +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/src/codecortex.egg-info/requires.txt +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/src/codecortex.egg-info/top_level.txt +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/__main__.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/agent_prompt.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/cache.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/commands/__init__.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/commands/_common.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/commands/doctor.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/commands/gen_token.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/commands/graph.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/commands/index.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/commands/install.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/commands/map.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/commands/prompt.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/commands/query.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/commands/reset.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/commands/serve.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/commands/serve_http.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/commands/setup.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/commands/status.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/doctor.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/gateway.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/graph_backend.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/graph_render.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/graph_resolution.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/grapher.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/http_server.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/injector.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/installer.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/loc.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/logconfig.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/mapper.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/metrics.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/onboarding.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/outcome.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/policy.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/progress.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/provider.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/providers/__init__.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/providers/graph.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/providers/lsp.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/providers/none.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/py.typed +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/reindexer.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/reset.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/server.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/source_kind.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/term.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/verify.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/viewer/__init__.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/viewer/graph_template.html +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_agent_prompt.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_backend_reaper.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_cache.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_cli_commands.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_cli_help.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_cold_process.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_corpus.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_docs_ci_claims.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_docs_deadcode_withdrawal.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_doctor.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_e2e.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_engine_adoption.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_enterprise.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_gateway.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_graph_failure_population.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_graph_provider.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_graph_real.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_graph_stdin.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_grapher.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_hardening.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_http_auth.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_http_server.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_incompleteness.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_index_progress.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_indexer_binary.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_installer.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_integration.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_loc_census.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_lsp_provider.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_lsp_real.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_mapper.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_mcp_handshake.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_mcp_server.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_model_dimension.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_never_raise.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_onboarding.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_reindexer.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_rerank.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_reset.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_semantic_provider.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_source_kind.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_term.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_verify.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_verify_call.py +0 -0
- {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_version_skew.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codecortex
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.18.0
|
|
4
4
|
Summary: Local-first, MCP-native code-intelligence server — graph, LSP, and semantic search behind one safe code.query tool for coding agents.
|
|
5
5
|
Author: Shammai Hamilton
|
|
6
6
|
License-Expression: MIT
|
|
@@ -279,7 +279,7 @@ Then it launches the exact command it registered and drives a real MCP handshake
|
|
|
279
279
|
```text
|
|
280
280
|
v claude: registered at /Users/you/.claude.json
|
|
281
281
|
|
|
282
|
-
v verified: codeintel 0.
|
|
282
|
+
v verified: codeintel 0.18.0 — 4 tools (code.query, code.status, code.doctor, code.map)
|
|
283
283
|
```
|
|
284
284
|
|
|
285
285
|
If the command is not on `PATH`, or the server fails to start, install says so and exits non-zero
|
|
@@ -396,7 +396,7 @@ Full system docs live in [`docs/`](docs/) — start with the index:
|
|
|
396
396
|
| `codeintel install [--agent auto\|claude\|codex\|gemini\|zed\|all] [--no-verify] [--relative-command]` | Register codeintel with the agents installed on this machine (`auto`, the default), then prove it by completing a real MCP handshake against the registered command |
|
|
397
397
|
| `codeintel setup [project_root] [--all] [--index] [--warm] [--install-uv] [--install-deps] [--json]` | Prepare backends + index this repo (`--all` = one command: do everything automatable, idempotent); ends with a health report + **Next:** steps |
|
|
398
398
|
| `codeintel prompt [project_root] [--agent auto\|claude\|codex\|gemini\|zed] [--fresh] [--deep]` | Print a paste-to-your-agent setup prompt; probes health and emits only the outstanding steps (or "just restart me" when already healthy). `--fresh` = the full sequence from `pip install`, to send a friend |
|
|
399
|
-
| `codeintel index [project_root]` | Index a
|
|
399
|
+
| `codeintel index [project_root] [--quiet]` | Index a repo (semantic embeddings + best-effort graph & map refresh), with a live progress display; `--quiet` prints only the result line |
|
|
400
400
|
| `codeintel serve` | Start the MCP server (stdio transport) |
|
|
401
401
|
| `codeintel serve-http [--host HOST] [--port 8766] [--allow-remote] [--token TOKEN]` | Start the HTTP transport (loopback-only unless `--allow-remote`; `--token` requires a bearer token on every request) |
|
|
402
402
|
| `codeintel query --op OP --target TARGET [--engine auto] [--project-root DIR] [--json]` | Run a single query and print the result |
|
|
@@ -404,7 +404,7 @@ Full system docs live in [`docs/`](docs/) — start with the index:
|
|
|
404
404
|
| `codeintel doctor [project_root] [--deep] [--json]` | Diagnose per-engine health + repo index status, with a fix for each gap |
|
|
405
405
|
| `codeintel map [project_root]` | Generate the `CODE_INTEL.md` orientation file |
|
|
406
406
|
| `codeintel graph [project_root] [--html] [--out FILE] [--limit N]` | Emit the call graph as `{nodes,edges}` JSON, or `--html` a self-contained interactive viewer — see [docs/graph-viewer.md](docs/graph-viewer.md) |
|
|
407
|
-
| `codeintel reset [project_root] [--all] [--yes] [--json]` | Clear
|
|
407
|
+
| `codeintel reset [project_root] [--all] [--yes] [--json]` | Clear this repo's index — **both** semantic and graph — so it's as if never indexed; `--all` wipes every repo. Recovers from a corrupt/stale DB |
|
|
408
408
|
| `codeintel gen-token` | Print a secure random bearer token (for `serve-http` / RBAC `auth.toml`) |
|
|
409
409
|
|
|
410
410
|
Human-facing commands (`doctor`, `setup`, `prompt`, `reset`) honor `--no-color` / `NO_COLOR` and `--ascii`, and auto-degrade to plain text when piped.
|
|
@@ -444,7 +444,7 @@ Config is **validated on load** — an out-of-range number, a misspelled enum, o
|
|
|
444
444
|
| `CODEINTEL_HTTP_ACCESS_LOG=1` | One log line per HTTP request (method, path, status, latency) |
|
|
445
445
|
| `CODEINTEL_DEBUG=1` | Log the full traceback of any error the never-throw contract swallows (silent by default) — the switch for diagnosing an unexpected `null` |
|
|
446
446
|
| `CODEINTEL_REINDEX=off` | Disable the background reindexer; queries then index inline to stay fresh |
|
|
447
|
-
| `CODEINTEL_HOME` | Where
|
|
447
|
+
| `CODEINTEL_HOME` | Where per-machine state lives (default `~/.codeintel`) — the index cache, the global `config.toml`, and `auth.toml`. Set this when the process has **no resolvable home directory** — a container running as a UID with no passwd entry and no `$HOME`, which is common when an agent runs in one. Without it, `Path.home()` raises and every command fails somewhere far from the cause. |
|
|
448
448
|
|
|
449
449
|
## Privacy & dependencies
|
|
450
450
|
|
|
@@ -242,7 +242,7 @@ Then it launches the exact command it registered and drives a real MCP handshake
|
|
|
242
242
|
```text
|
|
243
243
|
v claude: registered at /Users/you/.claude.json
|
|
244
244
|
|
|
245
|
-
v verified: codeintel 0.
|
|
245
|
+
v verified: codeintel 0.18.0 — 4 tools (code.query, code.status, code.doctor, code.map)
|
|
246
246
|
```
|
|
247
247
|
|
|
248
248
|
If the command is not on `PATH`, or the server fails to start, install says so and exits non-zero
|
|
@@ -359,7 +359,7 @@ Full system docs live in [`docs/`](docs/) — start with the index:
|
|
|
359
359
|
| `codeintel install [--agent auto\|claude\|codex\|gemini\|zed\|all] [--no-verify] [--relative-command]` | Register codeintel with the agents installed on this machine (`auto`, the default), then prove it by completing a real MCP handshake against the registered command |
|
|
360
360
|
| `codeintel setup [project_root] [--all] [--index] [--warm] [--install-uv] [--install-deps] [--json]` | Prepare backends + index this repo (`--all` = one command: do everything automatable, idempotent); ends with a health report + **Next:** steps |
|
|
361
361
|
| `codeintel prompt [project_root] [--agent auto\|claude\|codex\|gemini\|zed] [--fresh] [--deep]` | Print a paste-to-your-agent setup prompt; probes health and emits only the outstanding steps (or "just restart me" when already healthy). `--fresh` = the full sequence from `pip install`, to send a friend |
|
|
362
|
-
| `codeintel index [project_root]` | Index a
|
|
362
|
+
| `codeintel index [project_root] [--quiet]` | Index a repo (semantic embeddings + best-effort graph & map refresh), with a live progress display; `--quiet` prints only the result line |
|
|
363
363
|
| `codeintel serve` | Start the MCP server (stdio transport) |
|
|
364
364
|
| `codeintel serve-http [--host HOST] [--port 8766] [--allow-remote] [--token TOKEN]` | Start the HTTP transport (loopback-only unless `--allow-remote`; `--token` requires a bearer token on every request) |
|
|
365
365
|
| `codeintel query --op OP --target TARGET [--engine auto] [--project-root DIR] [--json]` | Run a single query and print the result |
|
|
@@ -367,7 +367,7 @@ Full system docs live in [`docs/`](docs/) — start with the index:
|
|
|
367
367
|
| `codeintel doctor [project_root] [--deep] [--json]` | Diagnose per-engine health + repo index status, with a fix for each gap |
|
|
368
368
|
| `codeintel map [project_root]` | Generate the `CODE_INTEL.md` orientation file |
|
|
369
369
|
| `codeintel graph [project_root] [--html] [--out FILE] [--limit N]` | Emit the call graph as `{nodes,edges}` JSON, or `--html` a self-contained interactive viewer — see [docs/graph-viewer.md](docs/graph-viewer.md) |
|
|
370
|
-
| `codeintel reset [project_root] [--all] [--yes] [--json]` | Clear
|
|
370
|
+
| `codeintel reset [project_root] [--all] [--yes] [--json]` | Clear this repo's index — **both** semantic and graph — so it's as if never indexed; `--all` wipes every repo. Recovers from a corrupt/stale DB |
|
|
371
371
|
| `codeintel gen-token` | Print a secure random bearer token (for `serve-http` / RBAC `auth.toml`) |
|
|
372
372
|
|
|
373
373
|
Human-facing commands (`doctor`, `setup`, `prompt`, `reset`) honor `--no-color` / `NO_COLOR` and `--ascii`, and auto-degrade to plain text when piped.
|
|
@@ -407,7 +407,7 @@ Config is **validated on load** — an out-of-range number, a misspelled enum, o
|
|
|
407
407
|
| `CODEINTEL_HTTP_ACCESS_LOG=1` | One log line per HTTP request (method, path, status, latency) |
|
|
408
408
|
| `CODEINTEL_DEBUG=1` | Log the full traceback of any error the never-throw contract swallows (silent by default) — the switch for diagnosing an unexpected `null` |
|
|
409
409
|
| `CODEINTEL_REINDEX=off` | Disable the background reindexer; queries then index inline to stay fresh |
|
|
410
|
-
| `CODEINTEL_HOME` | Where
|
|
410
|
+
| `CODEINTEL_HOME` | Where per-machine state lives (default `~/.codeintel`) — the index cache, the global `config.toml`, and `auth.toml`. Set this when the process has **no resolvable home directory** — a container running as a UID with no passwd entry and no `$HOME`, which is common when an agent runs in one. Without it, `Path.home()` raises and every command fails somewhere far from the cause. |
|
|
411
411
|
|
|
412
412
|
## Privacy & dependencies
|
|
413
413
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codecortex
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.18.0
|
|
4
4
|
Summary: Local-first, MCP-native code-intelligence server — graph, LSP, and semantic search behind one safe code.query tool for coding agents.
|
|
5
5
|
Author: Shammai Hamilton
|
|
6
6
|
License-Expression: MIT
|
|
@@ -279,7 +279,7 @@ Then it launches the exact command it registered and drives a real MCP handshake
|
|
|
279
279
|
```text
|
|
280
280
|
v claude: registered at /Users/you/.claude.json
|
|
281
281
|
|
|
282
|
-
v verified: codeintel 0.
|
|
282
|
+
v verified: codeintel 0.18.0 — 4 tools (code.query, code.status, code.doctor, code.map)
|
|
283
283
|
```
|
|
284
284
|
|
|
285
285
|
If the command is not on `PATH`, or the server fails to start, install says so and exits non-zero
|
|
@@ -396,7 +396,7 @@ Full system docs live in [`docs/`](docs/) — start with the index:
|
|
|
396
396
|
| `codeintel install [--agent auto\|claude\|codex\|gemini\|zed\|all] [--no-verify] [--relative-command]` | Register codeintel with the agents installed on this machine (`auto`, the default), then prove it by completing a real MCP handshake against the registered command |
|
|
397
397
|
| `codeintel setup [project_root] [--all] [--index] [--warm] [--install-uv] [--install-deps] [--json]` | Prepare backends + index this repo (`--all` = one command: do everything automatable, idempotent); ends with a health report + **Next:** steps |
|
|
398
398
|
| `codeintel prompt [project_root] [--agent auto\|claude\|codex\|gemini\|zed] [--fresh] [--deep]` | Print a paste-to-your-agent setup prompt; probes health and emits only the outstanding steps (or "just restart me" when already healthy). `--fresh` = the full sequence from `pip install`, to send a friend |
|
|
399
|
-
| `codeintel index [project_root]` | Index a
|
|
399
|
+
| `codeintel index [project_root] [--quiet]` | Index a repo (semantic embeddings + best-effort graph & map refresh), with a live progress display; `--quiet` prints only the result line |
|
|
400
400
|
| `codeintel serve` | Start the MCP server (stdio transport) |
|
|
401
401
|
| `codeintel serve-http [--host HOST] [--port 8766] [--allow-remote] [--token TOKEN]` | Start the HTTP transport (loopback-only unless `--allow-remote`; `--token` requires a bearer token on every request) |
|
|
402
402
|
| `codeintel query --op OP --target TARGET [--engine auto] [--project-root DIR] [--json]` | Run a single query and print the result |
|
|
@@ -404,7 +404,7 @@ Full system docs live in [`docs/`](docs/) — start with the index:
|
|
|
404
404
|
| `codeintel doctor [project_root] [--deep] [--json]` | Diagnose per-engine health + repo index status, with a fix for each gap |
|
|
405
405
|
| `codeintel map [project_root]` | Generate the `CODE_INTEL.md` orientation file |
|
|
406
406
|
| `codeintel graph [project_root] [--html] [--out FILE] [--limit N]` | Emit the call graph as `{nodes,edges}` JSON, or `--html` a self-contained interactive viewer — see [docs/graph-viewer.md](docs/graph-viewer.md) |
|
|
407
|
-
| `codeintel reset [project_root] [--all] [--yes] [--json]` | Clear
|
|
407
|
+
| `codeintel reset [project_root] [--all] [--yes] [--json]` | Clear this repo's index — **both** semantic and graph — so it's as if never indexed; `--all` wipes every repo. Recovers from a corrupt/stale DB |
|
|
408
408
|
| `codeintel gen-token` | Print a secure random bearer token (for `serve-http` / RBAC `auth.toml`) |
|
|
409
409
|
|
|
410
410
|
Human-facing commands (`doctor`, `setup`, `prompt`, `reset`) honor `--no-color` / `NO_COLOR` and `--ascii`, and auto-degrade to plain text when piped.
|
|
@@ -444,7 +444,7 @@ Config is **validated on load** — an out-of-range number, a misspelled enum, o
|
|
|
444
444
|
| `CODEINTEL_HTTP_ACCESS_LOG=1` | One log line per HTTP request (method, path, status, latency) |
|
|
445
445
|
| `CODEINTEL_DEBUG=1` | Log the full traceback of any error the never-throw contract swallows (silent by default) — the switch for diagnosing an unexpected `null` |
|
|
446
446
|
| `CODEINTEL_REINDEX=off` | Disable the background reindexer; queries then index inline to stay fresh |
|
|
447
|
-
| `CODEINTEL_HOME` | Where
|
|
447
|
+
| `CODEINTEL_HOME` | Where per-machine state lives (default `~/.codeintel`) — the index cache, the global `config.toml`, and `auth.toml`. Set this when the process has **no resolvable home directory** — a container running as a UID with no passwd entry and no `$HOME`, which is common when an agent runs in one. Without it, `Path.home()` raises and every command fails somewhere far from the cause. |
|
|
448
448
|
|
|
449
449
|
## Privacy & dependencies
|
|
450
450
|
|
|
@@ -30,6 +30,7 @@ src/codeintel/mapper.py
|
|
|
30
30
|
src/codeintel/metrics.py
|
|
31
31
|
src/codeintel/onboarding.py
|
|
32
32
|
src/codeintel/outcome.py
|
|
33
|
+
src/codeintel/paths.py
|
|
33
34
|
src/codeintel/policy.py
|
|
34
35
|
src/codeintel/progress.py
|
|
35
36
|
src/codeintel/provider.py
|
|
@@ -104,11 +105,14 @@ tests/test_model_dimension.py
|
|
|
104
105
|
tests/test_never_raise.py
|
|
105
106
|
tests/test_onboarding.py
|
|
106
107
|
tests/test_rbac.py
|
|
108
|
+
tests/test_redaction_boundary.py
|
|
107
109
|
tests/test_reindexer.py
|
|
108
110
|
tests/test_rerank.py
|
|
109
111
|
tests/test_reset.py
|
|
110
112
|
tests/test_semantic_provider.py
|
|
111
113
|
tests/test_source_kind.py
|
|
114
|
+
tests/test_staleness.py
|
|
115
|
+
tests/test_symbol_context.py
|
|
112
116
|
tests/test_term.py
|
|
113
117
|
tests/test_treesitter.py
|
|
114
118
|
tests/test_verify.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.18.0"
|
|
@@ -26,6 +26,7 @@ import os
|
|
|
26
26
|
import pathlib
|
|
27
27
|
import tomllib # stdlib on Python 3.11+ (the project's minimum)
|
|
28
28
|
|
|
29
|
+
from codeintel.paths import codeintel_home
|
|
29
30
|
from codeintel.policy import TieringPolicy
|
|
30
31
|
|
|
31
32
|
logger = logging.getLogger("codeintel")
|
|
@@ -78,7 +79,13 @@ def _auth_config_path() -> pathlib.Path | None:
|
|
|
78
79
|
if env:
|
|
79
80
|
p = pathlib.Path(env)
|
|
80
81
|
return p if p.is_file() else None
|
|
81
|
-
|
|
82
|
+
try:
|
|
83
|
+
default = codeintel_home() / "auth.toml"
|
|
84
|
+
except Exception:
|
|
85
|
+
# No CODEINTEL_HOME and no resolvable home. `load_auth` promises never to raise, and its
|
|
86
|
+
# whole contract is "absent config → auth disabled"; a host with nowhere to look is the
|
|
87
|
+
# absent case, not a crash. (Previously this read `Path.home()` inline and raised.)
|
|
88
|
+
return None
|
|
82
89
|
return default if default.is_file() else None
|
|
83
90
|
|
|
84
91
|
|
|
@@ -4,6 +4,8 @@ import logging
|
|
|
4
4
|
import pathlib
|
|
5
5
|
import tomllib # stdlib since 3.11, which is this package's floor
|
|
6
6
|
|
|
7
|
+
from codeintel.paths import codeintel_home
|
|
8
|
+
|
|
7
9
|
logger = logging.getLogger("codeintel")
|
|
8
10
|
|
|
9
11
|
_DEFAULTS: dict = {
|
|
@@ -18,7 +20,12 @@ _DEFAULTS: dict = {
|
|
|
18
20
|
"model": "BAAI/bge-small-en-v1.5",
|
|
19
21
|
"chunk_strategy": "syntax", # syntax-aware (def/class boundaries) vs fixed line windows
|
|
20
22
|
"rerank": "on", # hybrid lexical+semantic rerank of search results
|
|
21
|
-
|
|
23
|
+
# Cosine candidates retrieved, fused and re-ranked before the top-k is returned. Was 30, but
|
|
24
|
+
# the semantic provider hardcoded its own `_display_k * 6` widening that overrode it, so the
|
|
25
|
+
# effective breadth has always been 60; 30 only ever described behaviour the code didn't have.
|
|
26
|
+
# The provider now honours this key, so the default is set to the value that was really in use
|
|
27
|
+
# — otherwise "fixing" the wiring would have silently narrowed everyone's retrieval by half.
|
|
28
|
+
"rerank_candidates": 60,
|
|
22
29
|
}
|
|
23
30
|
|
|
24
31
|
# Values restricted to a fixed set — anything else falls back to the default.
|
|
@@ -80,12 +87,27 @@ def _coerce(cfg: dict) -> dict:
|
|
|
80
87
|
return out
|
|
81
88
|
|
|
82
89
|
|
|
90
|
+
def _global_config() -> dict:
|
|
91
|
+
"""The machine-wide ``config.toml``, or ``{}`` when there isn't one.
|
|
92
|
+
|
|
93
|
+
Resolving the directory can RAISE (no ``CODEINTEL_HOME`` and no resolvable home — a container
|
|
94
|
+
UID with no passwd entry). That used to propagate straight out of ``load_config``: the call
|
|
95
|
+
read ``pathlib.Path.home()`` inline, so it blew up before ``_read_toml``'s handler could see
|
|
96
|
+
it, and every query on such a host died as an opaque ``provider-error``. "There is nowhere to
|
|
97
|
+
look for a global config" is not an error — it means there is no global config."""
|
|
98
|
+
try:
|
|
99
|
+
return _read_toml(codeintel_home() / "config.toml")
|
|
100
|
+
except Exception as exc:
|
|
101
|
+
logger.debug("no resolvable config directory (%s) — using defaults + project config", exc)
|
|
102
|
+
return {}
|
|
103
|
+
|
|
104
|
+
|
|
83
105
|
def load_config(project_root: str | None = None) -> dict:
|
|
84
106
|
"""Return the merged, validated config: defaults < global < project. Values that fail
|
|
85
107
|
validation fall back to their default (logged), so a bad config file never breaks a query."""
|
|
86
108
|
root = pathlib.Path(project_root) if project_root is not None else pathlib.Path.cwd()
|
|
87
109
|
|
|
88
|
-
global_cfg =
|
|
110
|
+
global_cfg = _global_config()
|
|
89
111
|
project_cfg = _read_toml(root / ".codeintel.toml")
|
|
90
112
|
|
|
91
113
|
merged = {**_DEFAULTS, **global_cfg, **project_cfg}
|
|
@@ -69,9 +69,24 @@ def open_contained(root_real: str, candidate: str | Path, **kwargs):
|
|
|
69
69
|
Raises ``ContainmentError`` rather than returning a sentinel, so a caller that wraps reads in a
|
|
70
70
|
broad ``except Exception`` cannot quietly downgrade an escape to "file not found" — the escape
|
|
71
71
|
is logged at WARNING here regardless of what the caller does with the exception.
|
|
72
|
+
|
|
73
|
+
A path that simply DOES NOT EXIST is reported as ``FileNotFoundError``, not as an escape.
|
|
74
|
+
``contained_path`` answers the single question "may this be read", and a deleted file is a
|
|
75
|
+
legitimate no — but it is also the most ordinary thing that can happen to an indexed file, and
|
|
76
|
+
reporting it as a refusal meant every deletion between indexing and a query logged a WARNING
|
|
77
|
+
asking whether a symlink had been "planted after indexing" and rendered the hit as
|
|
78
|
+
``[refused: resolves outside the indexed root]``. That is a false security alarm on a routine
|
|
79
|
+
event, and it drowns the real ones. The distinction is made HERE rather than by relaxing
|
|
80
|
+
``contained_path``, whose ``None`` also drives ``_cleanup_deleted``'s reconciliation of deleted
|
|
81
|
+
files — loosening it there would leave their rows in the index forever.
|
|
72
82
|
"""
|
|
73
83
|
safe = contained_path(root_real, candidate)
|
|
74
84
|
if safe is None:
|
|
85
|
+
# lexists(), not exists(): a dangling symlink still EXISTS as a link, and a link whose
|
|
86
|
+
# target vanished is a construct worth refusing loudly rather than calling "not found".
|
|
87
|
+
if not os.path.lexists(candidate):
|
|
88
|
+
logger.debug("indexed file no longer exists: %s", candidate)
|
|
89
|
+
raise FileNotFoundError(str(candidate))
|
|
75
90
|
logger.warning(
|
|
76
91
|
"refusing to read %s — it does not resolve to a file inside %s (symlink or hard link "
|
|
77
92
|
"planted after indexing?)", candidate, root_real,
|
|
@@ -11,6 +11,7 @@ from typing import TYPE_CHECKING
|
|
|
11
11
|
|
|
12
12
|
from codeintel.containment import contained_path, real_root
|
|
13
13
|
from codeintel.progress import ProgressSink, _Guard
|
|
14
|
+
from codeintel.semantic_db import MAX_CHUNK_CHARS, chunk_content_hash
|
|
14
15
|
from codeintel.source_kind import (
|
|
15
16
|
CODE_EXTS,
|
|
16
17
|
load_gitattributes_globs,
|
|
@@ -26,8 +27,10 @@ logger = logging.getLogger(__name__)
|
|
|
26
27
|
|
|
27
28
|
# Hard ceiling on the characters embedded for ONE chunk. Line-based splitting cannot bound a
|
|
28
29
|
# minified or generated single-line file, and the embedder's memory use scales with input size.
|
|
29
|
-
# Generous enough that no hand-written function is affected.
|
|
30
|
-
|
|
30
|
+
# Generous enough that no hand-written function is affected. Defined beside the schema so the
|
|
31
|
+
# searcher hashes an oversized chunk exactly the way the indexer stored it; re-exported here
|
|
32
|
+
# because this module is where the truncation is actually performed (and warned about).
|
|
33
|
+
_MAX_CHUNK_CHARS = MAX_CHUNK_CHARS
|
|
31
34
|
|
|
32
35
|
# Bytes examined when deciding whether a file is binary. A NUL in the first block is the classic
|
|
33
36
|
# signal and is what `git` itself uses.
|
|
@@ -228,6 +231,9 @@ class Indexer:
|
|
|
228
231
|
self._ts_parsers: dict = {} # per-instance tree-sitter parser cache (lang -> parser|None)
|
|
229
232
|
# Why the last index() failed, for callers that must SHOW a reason rather than log one.
|
|
230
233
|
self.last_error: str | None = None
|
|
234
|
+
# Rows whose content was unchanged but whose `chunk_end` this pass patched in place
|
|
235
|
+
# (pre-migration NULLs). Counted so the commit can be skipped when there were none.
|
|
236
|
+
self._backfilled = 0
|
|
231
237
|
|
|
232
238
|
def _get_embedder(self):
|
|
233
239
|
if self._embedder is None:
|
|
@@ -457,6 +463,77 @@ class Indexer:
|
|
|
457
463
|
spans.extend(members)
|
|
458
464
|
return spans
|
|
459
465
|
|
|
466
|
+
def _symbol_index_py(self, tree: ast.Module, n: int) -> list[tuple[int, int, str]]:
|
|
467
|
+
"""``(start, end, name)`` for EVERY def/class in the file, nested ones included.
|
|
468
|
+
|
|
469
|
+
Deliberately a full ``ast.walk`` rather than reusing ``_primary_spans``: that one emits
|
|
470
|
+
only the spans worth embedding (top-level defs, class members) and skips nested inner
|
|
471
|
+
functions, but a chunk can land inside one — and "which symbol am I looking at" should
|
|
472
|
+
answer with the innermost enclosing def, not the outermost. Separate from the span
|
|
473
|
+
pipeline on purpose, so chunk boundaries and symbol attribution stay independent."""
|
|
474
|
+
out: list[tuple[int, int, str]] = []
|
|
475
|
+
for node in ast.walk(tree):
|
|
476
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
|
477
|
+
lo, hi = self._node_span(node, n)
|
|
478
|
+
out.append((lo, hi, node.name))
|
|
479
|
+
return out
|
|
480
|
+
|
|
481
|
+
@staticmethod
|
|
482
|
+
def _ts_node_name(node) -> str | None:
|
|
483
|
+
"""The declared name of a tree-sitter def node, or None when it has no usable one.
|
|
484
|
+
|
|
485
|
+
Most def types carry a ``name`` field. Function-bound `const`/`let` (React components and
|
|
486
|
+
hooks — how most modern TS is written) instead nest the identifier one level down in a
|
|
487
|
+
``variable_declarator``, so an anonymous default export or an IIFE simply yields None and
|
|
488
|
+
the chunk is attributed to whatever encloses it."""
|
|
489
|
+
try:
|
|
490
|
+
named = node.child_by_field_name("name")
|
|
491
|
+
if named is not None and named.text:
|
|
492
|
+
return named.text.decode("utf-8", "replace")
|
|
493
|
+
if node.type in _TS_DECL_TYPES:
|
|
494
|
+
for child in node.children:
|
|
495
|
+
if child.type == "variable_declarator":
|
|
496
|
+
ident = child.child_by_field_name("name")
|
|
497
|
+
if ident is not None and ident.text:
|
|
498
|
+
return ident.text.decode("utf-8", "replace")
|
|
499
|
+
except Exception as exc:
|
|
500
|
+
logger.debug("tree-sitter node name extraction failed: %s", exc)
|
|
501
|
+
return None
|
|
502
|
+
|
|
503
|
+
def _symbol_index_ts(self, root, lang: str, n: int) -> list[tuple[int, int, str]]:
|
|
504
|
+
"""``_symbol_index_py`` for tree-sitter languages: every def node anywhere in the tree.
|
|
505
|
+
Iterative, so deep nesting cannot blow the stack (same reason ``nearest_defs`` is)."""
|
|
506
|
+
all_types = _TS_FUNC_TYPES.get(lang, set()) | _TS_CONTAINER_TYPES.get(lang, set())
|
|
507
|
+
if not all_types:
|
|
508
|
+
return []
|
|
509
|
+
arrow_ok = lang in _TS_ARROW_LANGS
|
|
510
|
+
out: list[tuple[int, int, str]] = []
|
|
511
|
+
stack = [root]
|
|
512
|
+
while stack:
|
|
513
|
+
node = stack.pop()
|
|
514
|
+
is_def = node.type in all_types or (
|
|
515
|
+
arrow_ok and node.type in _TS_DECL_TYPES and _ts_decl_is_function(node)
|
|
516
|
+
)
|
|
517
|
+
if is_def:
|
|
518
|
+
name = self._ts_node_name(node)
|
|
519
|
+
if name:
|
|
520
|
+
out.append((node.start_point[0], self._ts_end_line(node, n), name))
|
|
521
|
+
stack.extend(c for c in node.children if c.is_named)
|
|
522
|
+
return out
|
|
523
|
+
|
|
524
|
+
@staticmethod
|
|
525
|
+
def _enclosing_symbol(index: list[tuple[int, int, str]], start: int) -> str | None:
|
|
526
|
+
"""The INNERMOST definition containing line ``start``, or None at module level.
|
|
527
|
+
|
|
528
|
+
Innermost = the latest-starting span that still contains the line, so a method inside a
|
|
529
|
+
class reports the method (the useful answer) rather than the class."""
|
|
530
|
+
best_lo = -1
|
|
531
|
+
best_name = None
|
|
532
|
+
for lo, hi, name in index:
|
|
533
|
+
if lo <= start < hi and lo > best_lo:
|
|
534
|
+
best_lo, best_name = lo, name
|
|
535
|
+
return best_name
|
|
536
|
+
|
|
460
537
|
def _cover(self, primary: list[tuple[int, int]], n: int) -> list[tuple[int, int]]:
|
|
461
538
|
"""Gapless cover of ``[0, n)`` in file order: window-fill every gap between primary spans
|
|
462
539
|
and window-split any oversized def. The def-aligned *primary* spans are whole and mutually
|
|
@@ -479,13 +556,20 @@ class Indexer:
|
|
|
479
556
|
result.extend(self._window_spans(cursor, n))
|
|
480
557
|
return result
|
|
481
558
|
|
|
559
|
+
def _chunk_python_ast_full(
|
|
560
|
+
self, lines: list[str], source: str
|
|
561
|
+
) -> tuple[list[tuple[int, int]], list[tuple[int, int, str]]]:
|
|
562
|
+
"""Spans AND the symbol index from ONE parse. Both derive from the same tree, so they can
|
|
563
|
+
never describe different versions of the file."""
|
|
564
|
+
tree = ast.parse(source)
|
|
565
|
+
n = len(lines)
|
|
566
|
+
return self._cover(self._primary_spans(tree, n), n), self._symbol_index_py(tree, n)
|
|
567
|
+
|
|
482
568
|
def _chunk_python_ast(self, lines: list[str], source: str) -> list[tuple[int, int]]:
|
|
483
569
|
"""Parse ``source`` → a complete, non-overlapping, def-aligned cover of the file as
|
|
484
570
|
0-based half-open ``(start, end)`` spans. Raises on parse failure (``SyntaxError`` /
|
|
485
571
|
``ValueError`` on NUL bytes / ``RecursionError`` / …) so the caller falls back to windows."""
|
|
486
|
-
|
|
487
|
-
n = len(lines)
|
|
488
|
-
return self._cover(self._primary_spans(tree, n), n)
|
|
572
|
+
return self._chunk_python_ast_full(lines, source)[0]
|
|
489
573
|
|
|
490
574
|
def _get_ts_parser(self, lang: str):
|
|
491
575
|
"""Lazily load + cache (per instance) the tree-sitter parser for a language. Returns None
|
|
@@ -563,52 +647,67 @@ class Indexer:
|
|
|
563
647
|
emit(node)
|
|
564
648
|
return spans
|
|
565
649
|
|
|
566
|
-
def
|
|
567
|
-
"""
|
|
568
|
-
|
|
569
|
-
broken file still yields a partial tree (and thus useful spans) rather than raising."""
|
|
650
|
+
def _chunk_treesitter_full(self, lines: list[str], source: str, lang: str):
|
|
651
|
+
"""Spans AND the symbol index from ONE tree-sitter parse, or ``None`` when the parser is
|
|
652
|
+
unavailable (caller windows)."""
|
|
570
653
|
parser = self._get_ts_parser(lang)
|
|
571
654
|
if parser is None:
|
|
572
655
|
return None
|
|
573
656
|
tree = parser.parse(source.encode("utf-8", errors="replace"))
|
|
574
657
|
n = len(lines)
|
|
575
|
-
|
|
658
|
+
root = tree.root_node
|
|
659
|
+
return (
|
|
660
|
+
self._cover(self._primary_spans_ts(root, lang, n), n),
|
|
661
|
+
self._symbol_index_ts(root, lang, n),
|
|
662
|
+
)
|
|
663
|
+
|
|
664
|
+
def _chunk_treesitter(self, lines: list[str], source: str, lang: str):
|
|
665
|
+
"""Parse ``source`` with tree-sitter → a complete, def-aligned cover, or ``None`` when the
|
|
666
|
+
parser is unavailable (caller windows). tree-sitter is error-tolerant, so a syntactically
|
|
667
|
+
broken file still yields a partial tree (and thus useful spans) rather than raising."""
|
|
668
|
+
got = self._chunk_treesitter_full(lines, source, lang)
|
|
669
|
+
return None if got is None else got[0]
|
|
576
670
|
|
|
577
671
|
def _spans_for_file(
|
|
578
672
|
self, filepath: Path, lines: list[str], rel_path: str
|
|
579
|
-
) -> list[tuple[int, int]]:
|
|
673
|
+
) -> tuple[list[tuple[int, int]], list[tuple[int, int, str]]]:
|
|
580
674
|
"""Choose spans for one file under the syntax strategy: ``ast`` for ``.py``, tree-sitter for
|
|
581
675
|
the mapped languages (TS/JS/Go/Rust/Java/C/C++), fixed windows for everything else and on
|
|
582
|
-
any parse/grammar failure or when tree-sitter isn't installed.
|
|
676
|
+
any parse/grammar failure or when tree-sitter isn't installed.
|
|
677
|
+
|
|
678
|
+
Returns ``(spans, symbol_index)``. The index is empty for the windowed fallbacks — a file
|
|
679
|
+
we could not parse is a file whose symbols we do not know, and guessing them with a regex
|
|
680
|
+
is how a preview ends up confidently naming the wrong function."""
|
|
583
681
|
if self.chunk_strategy == "syntax":
|
|
584
682
|
suffix = filepath.suffix.lower()
|
|
585
683
|
if suffix == ".py":
|
|
586
684
|
try:
|
|
587
|
-
return self.
|
|
685
|
+
return self._chunk_python_ast_full(lines, "".join(lines))
|
|
588
686
|
except Exception as exc:
|
|
589
687
|
logger.debug("syntax chunking failed for %s (%s) — windowing", rel_path, exc)
|
|
590
688
|
else:
|
|
591
689
|
lang = _TS_LANG_BY_EXT.get(suffix)
|
|
592
690
|
if lang is not None:
|
|
593
691
|
try:
|
|
594
|
-
|
|
595
|
-
if
|
|
596
|
-
return
|
|
692
|
+
got = self._chunk_treesitter_full(lines, "".join(lines), lang)
|
|
693
|
+
if got is not None:
|
|
694
|
+
return got
|
|
597
695
|
except Exception as exc:
|
|
598
696
|
logger.debug("tree-sitter chunking failed for %s (%s) — windowing",
|
|
599
697
|
rel_path, exc)
|
|
600
|
-
return self._window_spans(0, len(lines))
|
|
698
|
+
return self._window_spans(0, len(lines)), []
|
|
601
699
|
|
|
602
700
|
# ---- materialisation -------------------------------------------------------------------
|
|
603
701
|
|
|
604
702
|
def _emit_spans(
|
|
605
703
|
self,
|
|
606
704
|
spans: list[tuple[int, int]],
|
|
705
|
+
symbols: list[tuple[int, int, str]],
|
|
607
706
|
lines: list[str],
|
|
608
707
|
rel_path: str,
|
|
609
708
|
project_key: str,
|
|
610
709
|
conn,
|
|
611
|
-
new_chunks: list[tuple[str, str, str, int, str]],
|
|
710
|
+
new_chunks: list[tuple[str, str, str, int, int, str, str | None]],
|
|
612
711
|
) -> tuple[set[str], bool]:
|
|
613
712
|
"""Materialise spans into new/changed chunk records — shared by both strategies, so the
|
|
614
713
|
whitespace-skip, hash-dedup, and per-file cap behave identically. Returns
|
|
@@ -651,29 +750,45 @@ class Indexer:
|
|
|
651
750
|
chunk_count += 1
|
|
652
751
|
continue
|
|
653
752
|
chunk_id = f"{project_key}:{rel_path}:{start}"
|
|
654
|
-
content_hash =
|
|
753
|
+
content_hash = chunk_content_hash(chunk_text)
|
|
754
|
+
symbol = self._enclosing_symbol(symbols, start)
|
|
655
755
|
keep_ids.add(chunk_id) # produced this pass — keep even when dedup skips re-embed
|
|
656
756
|
try:
|
|
657
757
|
row = conn.execute(
|
|
658
|
-
"SELECT content_hash FROM chunk_hashes
|
|
758
|
+
"SELECT content_hash, chunk_end, chunk_symbol FROM chunk_hashes"
|
|
759
|
+
" WHERE chunk_id = ?",
|
|
659
760
|
(chunk_id,),
|
|
660
761
|
).fetchone()
|
|
661
762
|
if row and row[0] == content_hash:
|
|
763
|
+
# Unchanged content: the stored embedding is still correct and must NOT be
|
|
764
|
+
# recomputed. But a row written before `chunk_end` existed carries NULL, and
|
|
765
|
+
# the searcher cannot verify staleness without it — so patch the span in
|
|
766
|
+
# place. This is what lets an existing 85k-chunk cache gain verification from
|
|
767
|
+
# one ordinary index pass instead of a full re-embed.
|
|
768
|
+
if row[1] != end or row[2] != symbol:
|
|
769
|
+
conn.execute(
|
|
770
|
+
"UPDATE chunk_hashes SET chunk_end = ?, chunk_symbol = ?"
|
|
771
|
+
" WHERE chunk_id = ?",
|
|
772
|
+
(end, symbol, chunk_id),
|
|
773
|
+
)
|
|
774
|
+
self._backfilled += 1
|
|
662
775
|
chunk_count += 1
|
|
663
776
|
continue
|
|
664
777
|
except Exception as exc:
|
|
665
778
|
logger.debug("hash check failed for %s: %s", chunk_id, exc)
|
|
666
|
-
new_chunks.append(
|
|
779
|
+
new_chunks.append(
|
|
780
|
+
(chunk_id, chunk_text, rel_path, start, end, content_hash, symbol)
|
|
781
|
+
)
|
|
667
782
|
chunk_count += 1
|
|
668
783
|
return keep_ids, True
|
|
669
784
|
|
|
670
785
|
def _collect_new_chunks(
|
|
671
786
|
self, root: Path, project_key: str, project_root_real: str
|
|
672
|
-
) -> list[tuple[str, str, str, int, str]]:
|
|
673
|
-
"""Walk files; return (chunk_id, text, rel_path, start, hash) for new/changed chunks, and
|
|
787
|
+
) -> list[tuple[str, str, str, int, int, str, str | None]]:
|
|
788
|
+
"""Walk files; return (chunk_id, text, rel_path, start, end, hash, symbol) for new/changed chunks, and
|
|
674
789
|
reconcile each fully-processed file (dropping rows for chunks it no longer produces)."""
|
|
675
790
|
conn = self.db.conn()
|
|
676
|
-
new_chunks: list[tuple[str, str, str, int, str]] = []
|
|
791
|
+
new_chunks: list[tuple[str, str, str, int, int, str, str | None]] = []
|
|
677
792
|
files_seen = 0
|
|
678
793
|
|
|
679
794
|
for filepath in self._walk_files(root):
|
|
@@ -699,9 +814,9 @@ class Indexer:
|
|
|
699
814
|
continue
|
|
700
815
|
|
|
701
816
|
rel_path = str(filepath.relative_to(root))
|
|
702
|
-
spans = self._spans_for_file(filepath, lines, rel_path)
|
|
817
|
+
spans, symbols = self._spans_for_file(filepath, lines, rel_path)
|
|
703
818
|
keep_ids, complete = self._emit_spans(
|
|
704
|
-
spans, lines, rel_path, project_key, conn, new_chunks
|
|
819
|
+
spans, symbols, lines, rel_path, project_key, conn, new_chunks
|
|
705
820
|
)
|
|
706
821
|
if complete:
|
|
707
822
|
# Reconcile only a fully-processed file: drop rows for defs/windows it no longer
|
|
@@ -716,13 +831,24 @@ class Indexer:
|
|
|
716
831
|
# out of scope for the indexer.
|
|
717
832
|
self.db.delete_file_orphans(project_root_real, rel_path, keep_ids)
|
|
718
833
|
|
|
834
|
+
# Commit any in-place `chunk_end` backfills. These ride outside `_embed_and_write`'s
|
|
835
|
+
# per-batch commits, and a pass that finds nothing new to embed returns before reaching
|
|
836
|
+
# them — so without this the backfill is computed and then dropped on every up-to-date
|
|
837
|
+
# repo, which is precisely the case it exists to serve.
|
|
838
|
+
if self._backfilled:
|
|
839
|
+
try:
|
|
840
|
+
conn.commit()
|
|
841
|
+
logger.debug("backfilled chunk_end for %d existing chunks", self._backfilled)
|
|
842
|
+
except Exception as exc:
|
|
843
|
+
logger.warning("committing chunk_end backfill failed: %s", exc)
|
|
844
|
+
|
|
719
845
|
# One final tick with the true totals so the committed scan line is accurate (the per-file
|
|
720
846
|
# ticks lag by the current file's chunks). The renderer records this even when throttled.
|
|
721
847
|
self._progress.scan(files_seen, len(new_chunks))
|
|
722
848
|
return new_chunks
|
|
723
849
|
|
|
724
850
|
def _embed_and_write(
|
|
725
|
-
self, new_chunks: list[tuple[str, str, str, int, str]], project_root_real: str
|
|
851
|
+
self, new_chunks: list[tuple[str, str, str, int, int, str, str | None]], project_root_real: str
|
|
726
852
|
) -> int:
|
|
727
853
|
total = len(new_chunks)
|
|
728
854
|
# Signal the model-load BEFORE materialising the embedder: on a cold cache _get_embedder()
|
|
@@ -744,7 +870,8 @@ class Indexer:
|
|
|
744
870
|
logger.warning("embedding batch %d failed: %s", i // batch_size, exc)
|
|
745
871
|
continue
|
|
746
872
|
|
|
747
|
-
for j, (chunk_id, _, rel_path, chunk_start, content_hash
|
|
873
|
+
for j, (chunk_id, _, rel_path, chunk_start, chunk_end, content_hash,
|
|
874
|
+
chunk_symbol) in enumerate(batch):
|
|
748
875
|
if j >= len(embeddings):
|
|
749
876
|
break
|
|
750
877
|
try:
|
|
@@ -770,9 +897,10 @@ class Indexer:
|
|
|
770
897
|
)
|
|
771
898
|
conn.execute(
|
|
772
899
|
"INSERT OR REPLACE INTO chunk_hashes"
|
|
773
|
-
"(chunk_id, project_root, file_path, chunk_start,
|
|
774
|
-
" VALUES (?, ?, ?, ?, ?)",
|
|
775
|
-
(chunk_id, project_root_real, rel_path, chunk_start,
|
|
900
|
+
"(chunk_id, project_root, file_path, chunk_start, chunk_end,"
|
|
901
|
+
" content_hash, chunk_symbol) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
902
|
+
(chunk_id, project_root_real, rel_path, chunk_start, chunk_end,
|
|
903
|
+
content_hash, chunk_symbol),
|
|
776
904
|
)
|
|
777
905
|
embedded_count += 1
|
|
778
906
|
except Exception as exc:
|
|
@@ -808,6 +936,7 @@ class Indexer:
|
|
|
808
936
|
return 0
|
|
809
937
|
|
|
810
938
|
project_key = _project_key(project_root_real)
|
|
939
|
+
self._backfilled = 0 # per-pass, so a reused Indexer can't carry a stale count forward
|
|
811
940
|
|
|
812
941
|
self._cleanup_deleted(root, project_root_real)
|
|
813
942
|
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Where codeintel keeps its per-machine state — one definition, used by every consumer.
|
|
2
|
+
|
|
3
|
+
``CODEINTEL_HOME`` exists because ``Path.home()`` raises when a process has no resolvable home
|
|
4
|
+
directory: a UID with no passwd entry and no ``$HOME``. That is not exotic for this tool. It is an
|
|
5
|
+
MCP server whose whole purpose is to be launched by coding agents, which are routinely run in
|
|
6
|
+
containers shaped exactly that way.
|
|
7
|
+
|
|
8
|
+
The override used to be honoured by the semantic cache alone. ``config.py`` and ``auth.py`` each
|
|
9
|
+
called ``Path.home()`` directly, so on such a host ``load_config()`` raised ``RuntimeError`` — and
|
|
10
|
+
kept raising with ``CODEINTEL_HOME`` set, because the override never reached them. The escape hatch
|
|
11
|
+
did not work in the one environment it was written for, and the failure surfaced several layers
|
|
12
|
+
away as a generic ``provider-error``. Anything resolving this directory now asks here.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
import pathlib
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def codeintel_home() -> pathlib.Path:
|
|
21
|
+
"""The per-machine state directory (cache, global config, auth).
|
|
22
|
+
|
|
23
|
+
Raises ``RuntimeError`` (from ``Path.home()``) when there is no override and no resolvable home
|
|
24
|
+
— deliberately, so a caller that must NAME the problem can. ``SemanticProvider.probe`` reports
|
|
25
|
+
it with the remediation that actually fixes it; callers for whom a missing file is merely
|
|
26
|
+
"no config" should catch it instead (see ``config.global_config_path``).
|
|
27
|
+
"""
|
|
28
|
+
override = os.environ.get("CODEINTEL_HOME", "").strip()
|
|
29
|
+
if override:
|
|
30
|
+
return pathlib.Path(override).expanduser()
|
|
31
|
+
return pathlib.Path.home() / ".codeintel"
|