codecortex 0.2.0__py3-none-any.whl

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.
@@ -0,0 +1,206 @@
1
+ Metadata-Version: 2.4
2
+ Name: codecortex
3
+ Version: 0.2.0
4
+ Summary: Local-first, MCP-native code-intelligence server — graph, LSP, and semantic search behind one safe code.query tool for coding agents.
5
+ Author: Shammai Hamilton
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/hamilton-sky/codeintel
8
+ Project-URL: Repository, https://github.com/hamilton-sky/codeintel
9
+ Project-URL: Issues, https://github.com/hamilton-sky/codeintel/issues
10
+ Project-URL: Changelog, https://github.com/hamilton-sky/codeintel/blob/main/CHANGELOG.md
11
+ Keywords: mcp,model-context-protocol,code-intelligence,code-search,llm,agents,lsp,semantic-search,knowledge-graph,static-analysis,developer-tools
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Environment :: Console
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Classifier: Topic :: Software Development :: Quality Assurance
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.11
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Requires-Dist: mcp>=1.0
27
+ Requires-Dist: sqlite-vec>=0.1
28
+ Requires-Dist: fastembed>=0.3
29
+ Provides-Extra: dev
30
+ Requires-Dist: pytest>=8; extra == "dev"
31
+ Requires-Dist: numpy>=1.24; extra == "dev"
32
+ Dynamic: license-file
33
+
34
+ # codeintel
35
+
36
+ A unified code-intelligence gateway — graph + LSP + semantic — that gives any coding agent a single safe API to search, trace, and understand any codebase.
37
+
38
+ [![CI](https://github.com/hamilton-sky/codeintel/actions/workflows/ci.yml/badge.svg)](https://github.com/hamilton-sky/codeintel/actions/workflows/ci.yml)
39
+
40
+ ## Quickstart
41
+
42
+ ```bash
43
+ pip install codecortex
44
+ ```
45
+
46
+ This installs the `codeintel` CLI; the **semantic** engine works out of the box. The **graph**
47
+ and **LSP** engines use external backends (`codebase-memory-mcp`, and serena via `uvx`) —
48
+ run `codeintel doctor` to see what's available and how to enable the rest. (On PyPI the
49
+ distribution is `codecortex` because `codeintel` was taken; the CLI and import stay `codeintel`.)
50
+
51
+ Or from source:
52
+
53
+ ```bash
54
+ git clone https://github.com/hamilton-sky/codeintel.git
55
+ cd codeintel
56
+ pip install -e .
57
+ ```
58
+
59
+ Register with your AI agent(s):
60
+
61
+ ```bash
62
+ codeintel install # registers with Claude, Codex, Gemini, Zed
63
+ ```
64
+
65
+ Index a project, check what's ready, and run your first query:
66
+
67
+ ```bash
68
+ codeintel index /path/to/your/project
69
+ codeintel doctor /path/to/your/project # which engines are ready + how to fix the rest
70
+ codeintel query --op search --target "authentication middleware"
71
+ ```
72
+
73
+ ## How it works
74
+
75
+ A `Gateway` receives every query and dispatches it to one of three providers — graph (structural relationships), LSP (precise symbol resolution), or semantic (embedding-based search) — based on the operation type. Each provider is fully isolated: if it is unavailable or raises an exception, the gateway catches it and returns a safe-null envelope. The caller always gets a well-formed response with no exception to catch.
76
+
77
+ ```mermaid
78
+ flowchart LR
79
+ A["AI agent · MCP"] --> GW
80
+ H["Harness · HTTP"] --> GW
81
+ C["Developer · CLI"] --> GW
82
+ GW["Gateway<br/>route · cache · safe-null"] -->|"auto: search"| SP[SemanticProvider]
83
+ GW -->|"auto: impact / callers / …"| GP[GraphProvider]
84
+ GW -->|"auto: symbol"| LP[LspProvider]
85
+ GP --> GB[("codebase-memory-mcp")]
86
+ LP --> LB[("language server")]
87
+ SP --> SB[("fastembed + sqlite-vec")]
88
+ ```
89
+
90
+ > Full walkthrough: **[docs/architecture.md](docs/architecture.md)** · **[docs/query-flow.md](docs/query-flow.md)**.
91
+
92
+ ## Safe-null contract
93
+
94
+ Every `Gateway.query()` call returns a dict with exactly these keys:
95
+
96
+ ```json
97
+ {"ok": true, "op": "search", "target": "auth", "result": null, "engine": "semantic", "cached": false}
98
+ ```
99
+
100
+ `ok` is always `true`. `result` is `null` when no provider has an answer — never an exception, never a 500. An optional `reason` key explains null results (e.g. `"engine-unavailable"`, `"no-result"`). Callers must check `result is not None` before using the value.
101
+
102
+ ## Engines
103
+
104
+ | Engine | Key ops | Install prereq |
105
+ |---|---|---|
106
+ | `graph` | `impact`, `callers`, `callees`, `chain`, `pattern`, `overview`, `context` | `codebase-memory-mcp` CLI on PATH — see [docs/graph.md](docs/graph.md) |
107
+ | `lsp` | `symbol`, `overview`, `context` | `uvx` on PATH — serena is fetched from GitHub on first use; see [docs/lsp.md](docs/lsp.md) |
108
+ | `semantic` | `search`, `context` | `fastembed` + `sqlite-vec` (installed with the package) — see [docs/semantic.md](docs/semantic.md) |
109
+
110
+ Run `codeintel doctor` at any time to see which engines are actually ready for a repo and how to fix the ones that aren't.
111
+
112
+ Pass `--engine auto` (the default) and codeintel chooses the best engine per operation. Pass `--engine both` or `--engine all` to fan out to multiple engines and merge results.
113
+
114
+ ## Documentation
115
+
116
+ Full system docs live in [`docs/`](docs/) — start with the index:
117
+
118
+ - **[Architecture](docs/architecture.md)** — layers, the `CodeProvider` protocol, the safe-null contract, caching, freshness (ASCII + Mermaid).
119
+ - **[Query flow](docs/query-flow.md)** — request lifecycle, engine selection, fan-out & merge, and why it never throws.
120
+ - **[Map file](docs/map-file.md)** — the static `CODE_INTEL.md` orientation layer for hosts with no MCP support.
121
+ - Engine references: **[graph](docs/graph.md)** · **[lsp](docs/lsp.md)** · **[semantic](docs/semantic.md)**.
122
+
123
+ ## CLI reference
124
+
125
+ | Command | Purpose |
126
+ |---|---|
127
+ | `codeintel install [--agent claude\|codex\|gemini\|zed\|all]` | Register codeintel with AI agent(s) |
128
+ | `codeintel setup [project_root] [--index] [--warm] [--install-uv]` | Check backends + optionally index this repo; ends with a health report |
129
+ | `codeintel index [project_root]` | Index a project for semantic search |
130
+ | `codeintel serve` | Start the MCP server (stdio transport) |
131
+ | `codeintel serve-http [--host HOST] [--port 8766] [--allow-remote]` | Start the HTTP transport (loopback-only unless `--allow-remote`) |
132
+ | `codeintel query --op OP --target TARGET [--engine auto]` | Run a single query and print the result |
133
+ | `codeintel status [project_root]` | Show engine availability and index age |
134
+ | `codeintel doctor [project_root] [--deep] [--json]` | Diagnose per-engine health + repo index status, with a fix for each gap |
135
+ | `codeintel map [project_root]` | Generate the `CODE_INTEL.md` orientation file |
136
+ | `codeintel reset [project_root] [--all] [--yes]` | Clear the semantic index (this repo, or `--all`) to recover from a corrupt/stale DB |
137
+
138
+ Human-facing commands (`doctor`, `status`, `query`, `setup`, `reset`) honor `--no-color` / `NO_COLOR` and `--ascii`, and auto-degrade to plain text when piped.
139
+
140
+ ## Config
141
+
142
+ Create `.codeintel.toml` at your project root to override defaults:
143
+
144
+ ```toml
145
+ backend = "auto" # auto | graph | lsp | semantic
146
+ semantic = "on" # on | off
147
+ reindex = "on-demand" # on-demand | never
148
+ cosine_floor = 0.25 # minimum similarity score for semantic hits
149
+ max_chunks = 500 # max chunks to embed per project
150
+ model = "BAAI/bge-small-en-v1.5" # fastembed embedding model
151
+ ```
152
+
153
+ ## Privacy & dependencies
154
+
155
+ **codeintel is local-first** — one local process, no cloud service, no API keys, no telemetry, and no per-query network. Its own code makes zero outbound HTTP calls, and the HTTP transport binds to `127.0.0.1` only.
156
+
157
+ **Bundled (installed with the package, run locally):** `mcp` (the tool interface) · `sqlite-vec` (the semantic index, a local DB file) · `fastembed` (the local embedding model).
158
+
159
+ **Optional external backends** — auto-detected on `PATH`; if one is absent, that engine returns a safe-null and the agent simply degrades to grep:
160
+
161
+ | Engine | Needs on `PATH` | Third-party? |
162
+ |---|---|---|
163
+ | `graph` | `codebase-memory-mcp` | yes — external CLI |
164
+ | `lsp` | `uvx` (fetches & runs serena from GitHub on first use) | yes — [oraios/serena](https://github.com/oraios/serena) |
165
+ | `semantic` | *nothing external* | no — fully in-house |
166
+
167
+ Not sure what's installed? `codeintel doctor` reports exactly which backends are present, whether this repo is indexed, and the command to fix each gap.
168
+
169
+ **The only network touch is first-run setup:** `fastembed` downloads the `BAAI/bge-small-en-v1.5` weights once (cached under `~/.cache`, fully offline thereafter); the optional backends also install on first use *if you opt in*. After that, **no code or data leaves your machine** — which is what makes `--engine all` safe to run on a private repo.
170
+
171
+ ## For agents
172
+
173
+ Start the HTTP server, then POST queries to `/code/query`:
174
+
175
+ ```bash
176
+ codeintel serve-http & # listens on 127.0.0.1:8766 by default
177
+ ```
178
+
179
+ ```python
180
+ import urllib.request, json
181
+
182
+ def code_query(op: str, target: str, engine: str = "auto") -> dict:
183
+ body = json.dumps({"op": op, "target": target, "engine": engine}).encode()
184
+ req = urllib.request.Request(
185
+ "http://127.0.0.1:8766/code/query",
186
+ data=body,
187
+ headers={"Content-Type": "application/json"},
188
+ )
189
+ with urllib.request.urlopen(req) as resp:
190
+ return json.loads(resp.read())
191
+
192
+ result = code_query("search", "authentication middleware")
193
+ if result["result"] is not None:
194
+ print(result["result"]) # ranked semantic matches
195
+ ```
196
+
197
+ The response is always JSON-safe. Check `result["result"] is not None` before use. Never catch an exception from the gateway — it never raises.
198
+
199
+ ## Development
200
+
201
+ ```bash
202
+ git clone https://github.com/hamilton-sky/codeintel.git
203
+ cd codeintel
204
+ pip install -e .[dev]
205
+ pytest tests/ -q # full suite, ~1s
206
+ ```
@@ -0,0 +1,31 @@
1
+ codecortex-0.2.0.dist-info/licenses/LICENSE,sha256=DIRvhlH8EulEUOYQxKDFFiimJrlVT5-kCKxQIgi2m7c,1073
2
+ codeintel/__init__.py,sha256=Zn1KFblwuFHiDRdRAiRnDBRkbPttWh44jKa5zG2ov0E,22
3
+ codeintel/__main__.py,sha256=3MpEhdpAfHiO0rwIB7hxXM_mDN3RovPQBLXNH36aLUg,15189
4
+ codeintel/cache.py,sha256=MvZjxK899I-qUH1emLySII03cgT-d6ucxIOCOfjlNVM,2107
5
+ codeintel/config.py,sha256=Q7Mgq3UleJO6cpBKr1S7I1SeRbyL_rDekmCi8iasy9s,1133
6
+ codeintel/doctor.py,sha256=jiPuZzS801xfWwTyeVO9UHUxQqAJ-YqTlX-_epZfoyI,6659
7
+ codeintel/gateway.py,sha256=1c0rtjGGcgaa63Klk79LtdeOTl2ld8cvcnFlRSpKNsg,8929
8
+ codeintel/http_server.py,sha256=NH07e3uq7hp8H3e6ghmjeUx6TyWJ1a1A8CNZf3XRuLw,3543
9
+ codeintel/indexer.py,sha256=QKQtVu79DsEf1qSj9pLSdrspy5ajgzuNmupaZFL128s,9578
10
+ codeintel/injector.py,sha256=UGj5luzPJ-oX50Ks6BbZ2O2ChdDq4cOFway6OB2d4co,2521
11
+ codeintel/installer.py,sha256=RhVTB-J-YM4f0SpYiTVd5Ztb9E2SKxrJgpUBuorzJ6A,3014
12
+ codeintel/mapper.py,sha256=JN-lMO1CXIyDKSwHfzp0JuK396FR41XhFgtAip77G0E,7493
13
+ codeintel/onboarding.py,sha256=TOpRKkFvN2UesA7lW233s5PUJTFj98i7sby953EGWIQ,8846
14
+ codeintel/policy.py,sha256=mUAlk9oYoszQLEcvoroFLRd9DSTzho7YcbM7xb6RS-k,935
15
+ codeintel/provider.py,sha256=SlIModATCANJA4VyJs_Rz24bpQfzHG8ljHfpS1LWa8c,1214
16
+ codeintel/reindexer.py,sha256=zFNeLgPRZPfeczc5Cx3aPYkJpyrjd6AHyvuqsrLGf8E,4443
17
+ codeintel/reset.py,sha256=8Mpak4Tj5e-yTcM3_EnZJzbJPwPV2o8xpxGjv-wk9EE,3675
18
+ codeintel/searcher.py,sha256=R9PV7jSx-GO--qmh65FXlYxpbKbjCnxV-EF9hGe8hZU,4588
19
+ codeintel/semantic_db.py,sha256=m6s8aN7_KJOUSuu_1kNNHu6ZocqzEWTVAa8Y6xBO140,2661
20
+ codeintel/server.py,sha256=9eo531Hj1XZYz3I6Zx2DQZ-fsYPYtmIRJH2TTe5yRjo,7031
21
+ codeintel/term.py,sha256=csbSYD8euC13FnnpThy8kylDgZeejQ6j6Ol6Gu21Clo,6819
22
+ codeintel/providers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
+ codeintel/providers/graph.py,sha256=0nj7P2ncTkm68ITxMkti3ZOPfvoIpEPyTLdyGQ7efnI,18916
24
+ codeintel/providers/lsp.py,sha256=3jn2bMfo5jiaL4_X3kIjzzxLsL475xIZsT-okWy-vgg,16759
25
+ codeintel/providers/none.py,sha256=VI84XQkT7opQm6rBAEn0CdM8CNVlKGWYmjAhyeWbHeg,783
26
+ codeintel/providers/semantic.py,sha256=Yka9Puyd8LqY2jfvjoJzAsSMZvb3S-cViQC6fuesdUk,5397
27
+ codecortex-0.2.0.dist-info/METADATA,sha256=WfiL2GZ0JN1VhuKX2GTkIi70q4uA6kB2NrRVL9Po2XI,9853
28
+ codecortex-0.2.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
29
+ codecortex-0.2.0.dist-info/entry_points.txt,sha256=zrYfo95-8JkK0G2e5f9d94sYqqim7ZrdCW8knIfSy2U,54
30
+ codecortex-0.2.0.dist-info/top_level.txt,sha256=DF3TH1hHLWrrnX-7HhfMX0OnS2EXgqPRhPbiL_oSekw,10
31
+ codecortex-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ codeintel = codeintel.__main__:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Shammai Hamilton
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ codeintel
codeintel/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.2.0"
codeintel/__main__.py ADDED
@@ -0,0 +1,361 @@
1
+ import argparse
2
+ import os
3
+ import sys
4
+
5
+ from codeintel import __version__
6
+
7
+
8
+ def main() -> None:
9
+ parser = argparse.ArgumentParser(prog="codeintel")
10
+ parser.add_argument("--version", action="version", version=f"codeintel {__version__}")
11
+ subparsers = parser.add_subparsers(dest="command")
12
+
13
+ # Shared flags for the human-facing (styled) commands.
14
+ color_parent = argparse.ArgumentParser(add_help=False)
15
+ color_parent.add_argument("--no-color", action="store_true", help="Disable ANSI color output")
16
+ color_parent.add_argument("--ascii", action="store_true", help="Use ASCII-only glyphs")
17
+
18
+ subparsers.add_parser("serve", help="Start the MCP server")
19
+
20
+ # index subcommand
21
+ index_parser = subparsers.add_parser("index", help="Index a project for semantic search")
22
+ index_parser.add_argument(
23
+ "project_root",
24
+ nargs="?",
25
+ default=None,
26
+ help="Project root directory (default: cwd)",
27
+ )
28
+
29
+ # query subcommand
30
+ query_parser = subparsers.add_parser("query", help="Query the code intelligence engine")
31
+ query_parser.add_argument("--op", required=True, help="Query operation (e.g. search, symbol)")
32
+ query_parser.add_argument("--target", required=True, help="Query target")
33
+ query_parser.add_argument("--engine", default="auto", help="Engine to use (default: auto)")
34
+ query_parser.add_argument(
35
+ "--project-root",
36
+ default=None,
37
+ help="Project root directory (default: cwd)",
38
+ )
39
+
40
+ # status subcommand
41
+ status_parser = subparsers.add_parser("status", help="Show code intelligence engine status")
42
+ status_parser.add_argument(
43
+ "project_root",
44
+ nargs="?",
45
+ default=None,
46
+ help="Project root directory (default: cwd)",
47
+ )
48
+
49
+ # serve-http subcommand
50
+ http_parser = subparsers.add_parser("serve-http", help="Start the HTTP transport server")
51
+ http_parser.add_argument("--port", type=int, default=8766, help="Port to listen on (default: 8766)")
52
+ http_parser.add_argument("--host", default="127.0.0.1", help="Host to bind to (default: 127.0.0.1)")
53
+ http_parser.add_argument("--allow-remote", action="store_true", help="Permit binding a non-loopback host (exposes an UNAUTHENTICATED endpoint)")
54
+
55
+ # install subcommand
56
+ install_parser = subparsers.add_parser("install", help="Register codeintel with AI agents")
57
+ install_parser.add_argument(
58
+ "--agent",
59
+ choices=["claude", "codex", "gemini", "zed", "all"],
60
+ default="all",
61
+ help="Agent to register with (default: all)",
62
+ )
63
+
64
+ # map subcommand
65
+ map_parser = subparsers.add_parser("map", help="Generate CODE_INTEL.md orientation file")
66
+ map_parser.add_argument("project_root", nargs="?", default=None)
67
+ map_parser.add_argument("--inject", action="store_true", help="Inject reference block into CLAUDE.md/AGENTS.md")
68
+ map_parser.add_argument("--budget", type=int, default=32768, help="Byte budget for CODE_INTEL.md (default: 32768)")
69
+
70
+ # doctor subcommand
71
+ doctor_parser = subparsers.add_parser("doctor", parents=[color_parent], help="Diagnose engine health + index status for a repo")
72
+ doctor_parser.add_argument("project_root", nargs="?", default=None, help="Project root (default: cwd)")
73
+ doctor_parser.add_argument("--deep", action="store_true", help="Also boot-check serena (slower; first boot pulls it via uvx)")
74
+ doctor_parser.add_argument("--json", action="store_true", help="Emit the structured JSON report instead of the table")
75
+
76
+ # setup subcommand
77
+ setup_parser = subparsers.add_parser("setup", parents=[color_parent], help="Prepare backends and optionally index this repo")
78
+ setup_parser.add_argument("project_root", nargs="?", default=None, help="Project root (default: cwd)")
79
+ setup_parser.add_argument("--install-uv", action="store_true", help="Run `pip install uv` (provides uvx for the LSP engine)")
80
+ setup_parser.add_argument("--install-deps", action="store_true", help="Run `pip install -e .` (semantic engine deps)")
81
+ setup_parser.add_argument("--index", action="store_true", help="Index this repo now (first run downloads the ~50MB model)")
82
+ setup_parser.add_argument("--warm", action="store_true", help="Boot serena now (first run pulls it via uvx; slow)")
83
+ setup_parser.add_argument("--json", action="store_true", help="Emit the structured JSON report")
84
+
85
+ # reset subcommand
86
+ reset_parser = subparsers.add_parser("reset", parents=[color_parent], help="Clear the semantic index (recover from a corrupt/stale DB)")
87
+ reset_parser.add_argument("project_root", nargs="?", default=None, help="Project root (default: cwd)")
88
+ reset_parser.add_argument("--all", action="store_true", help="Clear the ENTIRE index (all projects), not just this repo")
89
+ reset_parser.add_argument("--yes", "-y", action="store_true", help="Skip the confirmation prompt")
90
+ reset_parser.add_argument("--json", action="store_true", help="Emit the structured JSON report")
91
+
92
+ args = parser.parse_args()
93
+
94
+ from codeintel import term
95
+ term.configure(
96
+ no_color=getattr(args, "no_color", False),
97
+ ascii_mode=(True if getattr(args, "ascii", False) else None),
98
+ )
99
+
100
+ if args.command == "serve":
101
+ from codeintel.server import run
102
+ run()
103
+
104
+ elif args.command == "index":
105
+ from codeintel.config import load_config
106
+ from codeintel.indexer import Indexer
107
+ from codeintel.semantic_db import SemanticDb, default_db_path
108
+
109
+ project_root = args.project_root or os.getcwd()
110
+ cfg = load_config(project_root)
111
+
112
+ db_path = default_db_path()
113
+ os.makedirs(os.path.dirname(db_path), exist_ok=True)
114
+ db = SemanticDb(db_path)
115
+ try:
116
+ db.init()
117
+ count = Indexer(
118
+ db,
119
+ model_name=str(cfg.get("model") or "BAAI/bge-small-en-v1.5"),
120
+ window=int(cfg.get("window", 20)),
121
+ stride=int(cfg.get("stride", 10)),
122
+ max_chunks=int(cfg.get("max_chunks", 500)),
123
+ ).index(project_root)
124
+ if count > 0:
125
+ print(f"Indexed {count} chunks")
126
+ else:
127
+ print("Nothing new to index")
128
+ finally:
129
+ db.close()
130
+
131
+ # best-effort graph reindex
132
+ import shutil
133
+ if shutil.which("codebase-memory-mcp"):
134
+ try:
135
+ from codeintel.reindexer import Reindexer
136
+ Reindexer()._graph_reindex(project_root)
137
+ except Exception:
138
+ pass
139
+
140
+ # best-effort map refresh after index
141
+ try:
142
+ from codeintel.providers.graph import GraphProvider
143
+ from codeintel.mapper import MapGenerator
144
+ _provider = GraphProvider()
145
+ _gen = MapGenerator(_provider)
146
+ _content = _gen.generate(project_root)
147
+ _gen.write(project_root, _content)
148
+ except Exception:
149
+ pass
150
+
151
+ elif args.command == "map":
152
+ from codeintel.providers.graph import GraphProvider
153
+ from codeintel.mapper import MapGenerator
154
+ from codeintel.injector import Injector
155
+
156
+ project_root = args.project_root or os.getcwd()
157
+ try:
158
+ provider = GraphProvider()
159
+ except Exception:
160
+ provider = None
161
+ gen = MapGenerator(provider)
162
+ try:
163
+ content = gen.generate(project_root, budget_bytes=args.budget)
164
+ path = gen.write(project_root, content)
165
+ print(f"Wrote {path} ({len(content.encode())} bytes)")
166
+ if args.inject:
167
+ inj_path, inj_action = Injector().inject(project_root)
168
+ if inj_path:
169
+ print(f"Inject: {inj_action} block in {inj_path}")
170
+ else:
171
+ print(f"Inject: {inj_action}")
172
+ except Exception as exc:
173
+ # Never-raise parity with the MCP code.map handler — degrade, don't crash.
174
+ print(f"map failed: {exc}")
175
+ sys.exit(0)
176
+
177
+ elif args.command == "query":
178
+ try:
179
+ import time
180
+
181
+ from codeintel import server
182
+ project_root = args.project_root or os.getcwd()
183
+ engine = args.engine if args.engine != "auto" else None
184
+ gw = server._build_gateway()
185
+
186
+ def _run_query():
187
+ return gw.query(
188
+ op=args.op,
189
+ target=args.target,
190
+ engine=engine,
191
+ role="",
192
+ project_root=project_root,
193
+ )
194
+
195
+ result = _run_query()
196
+
197
+ # The LSP engine warms a serena session in a background thread and returns
198
+ # reason:warming on the first call. A one-shot CLI process would otherwise always
199
+ # exit on 'warming' (the subprocess dies with it). Wait — bounded, never-raise —
200
+ # for the session to boot, re-querying the same gateway (session is cached per root).
201
+ if result.get("result") is None and result.get("reason") == "warming":
202
+ print("(lsp warming up — waiting for the language server...)", file=sys.stderr)
203
+ deadline = time.monotonic() + 45.0
204
+ while time.monotonic() < deadline:
205
+ time.sleep(0.5)
206
+ result = _run_query()
207
+ if result.get("result") is not None or result.get("reason") != "warming":
208
+ break
209
+
210
+ value = result.get("result")
211
+ if value is not None:
212
+ print(value)
213
+ else:
214
+ reason = result.get("reason", "unknown")
215
+ hint = result.get("hint")
216
+ print(f"No result (reason: {reason})")
217
+ if hint:
218
+ print(f" hint: {hint}", file=sys.stderr)
219
+ except Exception as exc:
220
+ print(f"No result (reason: {exc})")
221
+ sys.exit(0)
222
+
223
+ elif args.command == "status":
224
+ try:
225
+ from codeintel import server
226
+
227
+ project_root = args.project_root or os.getcwd()
228
+ status = server.code_status_handler({})
229
+
230
+ print("Engine status:")
231
+ for engine in ["graph", "lsp", "semantic"]:
232
+ available = status.get(engine, False)
233
+ state = "available" if available else "unavailable"
234
+ print(f" {engine:<10} {state}")
235
+
236
+ from codeintel.semantic_db import default_db_path
237
+ db_path = default_db_path()
238
+ if os.path.exists(db_path):
239
+ import datetime
240
+ mtime = os.path.getmtime(db_path)
241
+ age = datetime.datetime.now() - datetime.datetime.fromtimestamp(mtime)
242
+ hours = int(age.total_seconds() // 3600)
243
+ minutes = int((age.total_seconds() % 3600) // 60)
244
+ print(f"\nIndex age: {hours}h {minutes}m ({db_path})")
245
+ else:
246
+ print(f"\nIndex: not found ({db_path})")
247
+ except Exception as exc:
248
+ print(f"Status unavailable: {exc}")
249
+ sys.exit(0)
250
+
251
+ elif args.command == "doctor":
252
+ try:
253
+ from codeintel import doctor as _doctor
254
+
255
+ project_root = args.project_root or os.getcwd()
256
+ report = _doctor.run_doctor(project_root, deep=args.deep)
257
+ if args.json:
258
+ import json as _json
259
+ print(_json.dumps(report, indent=2))
260
+ else:
261
+ print(_doctor.render_doctor_text(report))
262
+ # Exit non-zero when a repo-critical engine is unhealthy, so scripts/CI can gate on it.
263
+ sys.exit(0 if report.get("summary", {}).get("healthy") else 1)
264
+ except Exception as exc:
265
+ print(f"doctor unavailable: {exc}")
266
+ sys.exit(0)
267
+
268
+ elif args.command == "setup":
269
+ try:
270
+ from codeintel import onboarding
271
+
272
+ project_root = args.project_root or os.getcwd()
273
+ report = onboarding.run_setup(
274
+ project_root,
275
+ install_uv=args.install_uv,
276
+ install_deps=args.install_deps,
277
+ do_index=args.index,
278
+ warm_lsp=args.warm,
279
+ )
280
+ if args.json:
281
+ import json as _json
282
+ print(_json.dumps(report, indent=2))
283
+ else:
284
+ print(onboarding.render_setup_text(report))
285
+ healthy = report.get("doctor", {}).get("summary", {}).get("healthy")
286
+ sys.exit(0 if healthy else 1)
287
+ except Exception as exc:
288
+ print(f"setup unavailable: {exc}")
289
+ sys.exit(0)
290
+
291
+ elif args.command == "reset":
292
+ try:
293
+ from codeintel import reset as _reset
294
+
295
+ project_root = args.project_root or os.getcwd()
296
+ preview = _reset.run_reset(project_root, all_projects=args.all, apply=False)
297
+ if not args.yes:
298
+ if not sys.stdin.isatty():
299
+ print("refusing to reset without --yes in a non-interactive shell")
300
+ sys.exit(1)
301
+ target = "ALL projects" if args.all else project_root
302
+ count = preview.get("count", 0)
303
+ ans = input(f"Reset semantic index for {target} ({count} entries)? [y/N] ").strip().lower()
304
+ if ans not in ("y", "yes"):
305
+ print("aborted")
306
+ sys.exit(0)
307
+ report = _reset.run_reset(project_root, all_projects=args.all, apply=True)
308
+ if args.json:
309
+ import json as _json
310
+ print(_json.dumps(report, indent=2))
311
+ else:
312
+ print(report.get("detail", "reset complete"))
313
+ sys.exit(0)
314
+ except Exception as exc:
315
+ print(f"reset unavailable: {exc}")
316
+ sys.exit(0)
317
+
318
+ elif args.command == "serve-http":
319
+ try:
320
+ from codeintel.http_server import run
321
+ run(host=args.host, port=args.port, allow_remote=args.allow_remote)
322
+ except KeyboardInterrupt:
323
+ pass
324
+ except Exception as exc:
325
+ # A startup failure (e.g. port in use) should print a friendly line, not a traceback.
326
+ # SystemExit from the non-loopback guard is BaseException — it passes through here.
327
+ print(f"serve-http failed: {exc}", file=sys.stderr)
328
+ sys.exit(1)
329
+
330
+ elif args.command == "install":
331
+ from codeintel.installer import Installer
332
+
333
+ installer = Installer()
334
+ if args.agent == "all":
335
+ results = installer.register_all()
336
+ else:
337
+ results = [installer.register(args.agent)]
338
+
339
+ any_ok = False
340
+ for r in results:
341
+ agent = r["agent"]
342
+ path = r["path"]
343
+ action = r["action"]
344
+ if action == "registered":
345
+ print(f"v {agent}: registered at {path}")
346
+ any_ok = True
347
+ elif action == "already":
348
+ print(f"~ {agent}: already registered at {path}")
349
+ any_ok = True
350
+ else:
351
+ print(f"x {agent}: failed — {r['reason']}")
352
+
353
+ sys.exit(0 if any_ok else 1)
354
+
355
+ else:
356
+ parser.print_help()
357
+ sys.exit(0)
358
+
359
+
360
+ if __name__ == "__main__":
361
+ main()