codecortex 0.12.0__tar.gz → 0.12.1__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.12.0/src/codecortex.egg-info → codecortex-0.12.1}/PKG-INFO +7 -2
- {codecortex-0.12.0 → codecortex-0.12.1}/README.md +6 -1
- {codecortex-0.12.0 → codecortex-0.12.1/src/codecortex.egg-info}/PKG-INFO +7 -2
- {codecortex-0.12.0 → codecortex-0.12.1}/src/codecortex.egg-info/SOURCES.txt +1 -0
- codecortex-0.12.1/src/codeintel/__init__.py +1 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/src/codeintel/__main__.py +106 -1
- {codecortex-0.12.0 → codecortex-0.12.1}/src/codeintel/http_server.py +7 -0
- codecortex-0.12.1/tests/test_cli_help.py +136 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_rbac.py +10 -0
- codecortex-0.12.0/src/codeintel/__init__.py +0 -1
- {codecortex-0.12.0 → codecortex-0.12.1}/LICENSE +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/pyproject.toml +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/setup.cfg +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/src/codecortex.egg-info/dependency_links.txt +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/src/codecortex.egg-info/entry_points.txt +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/src/codecortex.egg-info/requires.txt +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/src/codecortex.egg-info/top_level.txt +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/src/codeintel/auth.py +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/src/codeintel/cache.py +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/src/codeintel/config.py +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/src/codeintel/doctor.py +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/src/codeintel/gateway.py +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/src/codeintel/grapher.py +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/src/codeintel/indexer.py +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/src/codeintel/injector.py +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/src/codeintel/installer.py +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/src/codeintel/logconfig.py +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/src/codeintel/mapper.py +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/src/codeintel/metrics.py +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/src/codeintel/onboarding.py +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/src/codeintel/policy.py +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/src/codeintel/provider.py +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/src/codeintel/providers/__init__.py +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/src/codeintel/providers/graph.py +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/src/codeintel/providers/lsp.py +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/src/codeintel/providers/none.py +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/src/codeintel/providers/semantic.py +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/src/codeintel/reindexer.py +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/src/codeintel/reset.py +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/src/codeintel/searcher.py +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/src/codeintel/semantic_db.py +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/src/codeintel/server.py +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/src/codeintel/term.py +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/src/codeintel/verify.py +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/src/codeintel/viewer/__init__.py +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/src/codeintel/viewer/graph_template.html +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_cache.py +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_chunking.py +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_config.py +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_doctor.py +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_e2e.py +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_engine_adoption.py +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_enterprise.py +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_gateway.py +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_graph_provider.py +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_graph_real.py +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_graph_stdin.py +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_grapher.py +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_hardening.py +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_http_auth.py +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_http_server.py +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_installer.py +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_integration.py +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_lsp_provider.py +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_lsp_real.py +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_mapper.py +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_mcp_handshake.py +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_mcp_server.py +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_model_dimension.py +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_never_raise.py +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_onboarding.py +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_reindexer.py +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_rerank.py +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_reset.py +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_semantic_provider.py +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_term.py +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_treesitter.py +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_verify.py +0 -0
- {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_verify_call.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codecortex
|
|
3
|
-
Version: 0.12.
|
|
3
|
+
Version: 0.12.1
|
|
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
|
|
@@ -229,6 +229,9 @@ against the built wheel in a clean environment: it registers Codex and Claude Co
|
|
|
229
229
|
`code.query` over a fixture repo. A release that writes a config no host reads, or that returns
|
|
230
230
|
`ok: true` with nothing in it, fails there instead of on your machine.
|
|
231
231
|
|
|
232
|
+
> Full reference — what each host reads, the absolute-path rationale, and troubleshooting:
|
|
233
|
+
> **[docs/install.md](docs/install.md)**.
|
|
234
|
+
|
|
232
235
|
## How it works
|
|
233
236
|
|
|
234
237
|
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.
|
|
@@ -275,6 +278,7 @@ Pass `--engine auto` (the default) and codeintel chooses the best engine per ope
|
|
|
275
278
|
Full system docs live in [`docs/`](docs/) — start with the index:
|
|
276
279
|
|
|
277
280
|
- **[Architecture](docs/architecture.md)** — layers, the `CodeProvider` protocol, the safe-null contract, caching, freshness (ASCII + Mermaid).
|
|
281
|
+
- **[Install & registration](docs/install.md)** — what each agent host actually reads, why the registered command is an absolute path, and the three levels of proof that registration worked.
|
|
278
282
|
- **[Query flow](docs/query-flow.md)** — request lifecycle, engine selection, fan-out & merge, and why it never throws.
|
|
279
283
|
- **[Map file](docs/map-file.md)** — the static `CODE_INTEL.md` orientation layer for hosts with no MCP support.
|
|
280
284
|
- **[Benchmarks](docs/benchmarks.md)** — real numbers at scale: 25 k chunks indexed in ~8 min, ~235 ms warm queries, 60 MB index.
|
|
@@ -284,6 +288,7 @@ Full system docs live in [`docs/`](docs/) — start with the index:
|
|
|
284
288
|
|
|
285
289
|
| Command | Purpose |
|
|
286
290
|
|---|---|
|
|
291
|
+
| `codeintel help` | Every command grouped by task, with descriptions and examples (also the bare `codeintel`). A mistyped command suggests what you meant. |
|
|
287
292
|
| `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 |
|
|
288
293
|
| `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 |
|
|
289
294
|
| `codeintel index [project_root]` | Index a project for semantic search |
|
|
@@ -409,7 +414,7 @@ docker build -t codeintel . && docker run -p 127.0.0.1:8766:8766 \
|
|
|
409
414
|
git clone https://github.com/hamilton-sky/codeintel.git
|
|
410
415
|
cd codeintel
|
|
411
416
|
pip install -e .[dev]
|
|
412
|
-
pytest tests/ -q # ~
|
|
417
|
+
pytest tests/ -q # ~410 tests, ~35s (live graph/LSP backend tests skip when absent)
|
|
413
418
|
```
|
|
414
419
|
|
|
415
420
|
**Release gate.** The unit suite runs against the source tree, so it cannot see a packaging break, a
|
|
@@ -195,6 +195,9 @@ against the built wheel in a clean environment: it registers Codex and Claude Co
|
|
|
195
195
|
`code.query` over a fixture repo. A release that writes a config no host reads, or that returns
|
|
196
196
|
`ok: true` with nothing in it, fails there instead of on your machine.
|
|
197
197
|
|
|
198
|
+
> Full reference — what each host reads, the absolute-path rationale, and troubleshooting:
|
|
199
|
+
> **[docs/install.md](docs/install.md)**.
|
|
200
|
+
|
|
198
201
|
## How it works
|
|
199
202
|
|
|
200
203
|
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.
|
|
@@ -241,6 +244,7 @@ Pass `--engine auto` (the default) and codeintel chooses the best engine per ope
|
|
|
241
244
|
Full system docs live in [`docs/`](docs/) — start with the index:
|
|
242
245
|
|
|
243
246
|
- **[Architecture](docs/architecture.md)** — layers, the `CodeProvider` protocol, the safe-null contract, caching, freshness (ASCII + Mermaid).
|
|
247
|
+
- **[Install & registration](docs/install.md)** — what each agent host actually reads, why the registered command is an absolute path, and the three levels of proof that registration worked.
|
|
244
248
|
- **[Query flow](docs/query-flow.md)** — request lifecycle, engine selection, fan-out & merge, and why it never throws.
|
|
245
249
|
- **[Map file](docs/map-file.md)** — the static `CODE_INTEL.md` orientation layer for hosts with no MCP support.
|
|
246
250
|
- **[Benchmarks](docs/benchmarks.md)** — real numbers at scale: 25 k chunks indexed in ~8 min, ~235 ms warm queries, 60 MB index.
|
|
@@ -250,6 +254,7 @@ Full system docs live in [`docs/`](docs/) — start with the index:
|
|
|
250
254
|
|
|
251
255
|
| Command | Purpose |
|
|
252
256
|
|---|---|
|
|
257
|
+
| `codeintel help` | Every command grouped by task, with descriptions and examples (also the bare `codeintel`). A mistyped command suggests what you meant. |
|
|
253
258
|
| `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 |
|
|
254
259
|
| `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 |
|
|
255
260
|
| `codeintel index [project_root]` | Index a project for semantic search |
|
|
@@ -375,7 +380,7 @@ docker build -t codeintel . && docker run -p 127.0.0.1:8766:8766 \
|
|
|
375
380
|
git clone https://github.com/hamilton-sky/codeintel.git
|
|
376
381
|
cd codeintel
|
|
377
382
|
pip install -e .[dev]
|
|
378
|
-
pytest tests/ -q # ~
|
|
383
|
+
pytest tests/ -q # ~410 tests, ~35s (live graph/LSP backend tests skip when absent)
|
|
379
384
|
```
|
|
380
385
|
|
|
381
386
|
**Release gate.** The unit suite runs against the source tree, so it cannot see a packaging break, a
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codecortex
|
|
3
|
-
Version: 0.12.
|
|
3
|
+
Version: 0.12.1
|
|
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
|
|
@@ -229,6 +229,9 @@ against the built wheel in a clean environment: it registers Codex and Claude Co
|
|
|
229
229
|
`code.query` over a fixture repo. A release that writes a config no host reads, or that returns
|
|
230
230
|
`ok: true` with nothing in it, fails there instead of on your machine.
|
|
231
231
|
|
|
232
|
+
> Full reference — what each host reads, the absolute-path rationale, and troubleshooting:
|
|
233
|
+
> **[docs/install.md](docs/install.md)**.
|
|
234
|
+
|
|
232
235
|
## How it works
|
|
233
236
|
|
|
234
237
|
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.
|
|
@@ -275,6 +278,7 @@ Pass `--engine auto` (the default) and codeintel chooses the best engine per ope
|
|
|
275
278
|
Full system docs live in [`docs/`](docs/) — start with the index:
|
|
276
279
|
|
|
277
280
|
- **[Architecture](docs/architecture.md)** — layers, the `CodeProvider` protocol, the safe-null contract, caching, freshness (ASCII + Mermaid).
|
|
281
|
+
- **[Install & registration](docs/install.md)** — what each agent host actually reads, why the registered command is an absolute path, and the three levels of proof that registration worked.
|
|
278
282
|
- **[Query flow](docs/query-flow.md)** — request lifecycle, engine selection, fan-out & merge, and why it never throws.
|
|
279
283
|
- **[Map file](docs/map-file.md)** — the static `CODE_INTEL.md` orientation layer for hosts with no MCP support.
|
|
280
284
|
- **[Benchmarks](docs/benchmarks.md)** — real numbers at scale: 25 k chunks indexed in ~8 min, ~235 ms warm queries, 60 MB index.
|
|
@@ -284,6 +288,7 @@ Full system docs live in [`docs/`](docs/) — start with the index:
|
|
|
284
288
|
|
|
285
289
|
| Command | Purpose |
|
|
286
290
|
|---|---|
|
|
291
|
+
| `codeintel help` | Every command grouped by task, with descriptions and examples (also the bare `codeintel`). A mistyped command suggests what you meant. |
|
|
287
292
|
| `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 |
|
|
288
293
|
| `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 |
|
|
289
294
|
| `codeintel index [project_root]` | Index a project for semantic search |
|
|
@@ -409,7 +414,7 @@ docker build -t codeintel . && docker run -p 127.0.0.1:8766:8766 \
|
|
|
409
414
|
git clone https://github.com/hamilton-sky/codeintel.git
|
|
410
415
|
cd codeintel
|
|
411
416
|
pip install -e .[dev]
|
|
412
|
-
pytest tests/ -q # ~
|
|
417
|
+
pytest tests/ -q # ~410 tests, ~35s (live graph/LSP backend tests skip when absent)
|
|
413
418
|
```
|
|
414
419
|
|
|
415
420
|
**Release gate.** The unit suite runs against the source tree, so it cannot see a packaging break, a
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.12.1"
|
|
@@ -1,9 +1,101 @@
|
|
|
1
1
|
import argparse
|
|
2
|
+
import difflib
|
|
2
3
|
import os
|
|
3
4
|
import sys
|
|
4
5
|
|
|
5
6
|
from codeintel import __version__
|
|
6
7
|
|
|
8
|
+
# Commands grouped by what you are trying to DO. argparse lists them in declaration order with no
|
|
9
|
+
# grouping, which turns "what can this thing do?" into reading twelve lines to find the one verb you
|
|
10
|
+
# wanted. Each entry is (command, one-line description).
|
|
11
|
+
_COMMAND_GROUPS: list[tuple[str, list[tuple[str, str]]]] = [
|
|
12
|
+
("Understand your code", [
|
|
13
|
+
("query", "Ask one question — search, callers, callees, impact, chain, symbol, hotspots"),
|
|
14
|
+
("map", "Write CODE_INTEL.md — a committable architecture overview"),
|
|
15
|
+
("graph", "Interactive call-graph viewer (--html), or the graph as JSON"),
|
|
16
|
+
]),
|
|
17
|
+
("Set up", [
|
|
18
|
+
("setup", "Prepare backends + index this repo (--all does everything automatable)"),
|
|
19
|
+
("index", "Index a project for semantic search"),
|
|
20
|
+
("install", "Register codeintel with the AI agents installed on this machine"),
|
|
21
|
+
]),
|
|
22
|
+
("Check health", [
|
|
23
|
+
("doctor", "Per-engine health + index status, with the fix for each gap"),
|
|
24
|
+
("status", "Engine readiness and index age at a glance"),
|
|
25
|
+
("reset", "Clear the semantic index (recover from a corrupt or stale DB)"),
|
|
26
|
+
]),
|
|
27
|
+
("Run as a server", [
|
|
28
|
+
("serve", "Start the MCP server over stdio — what an agent host launches"),
|
|
29
|
+
("serve-http", "Start the HTTP transport (loopback only unless --allow-remote)"),
|
|
30
|
+
("gen-token", "Print a secure random bearer token for serve-http / RBAC"),
|
|
31
|
+
]),
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
_COMMANDS = [name for _group, items in _COMMAND_GROUPS for name, _desc in items]
|
|
35
|
+
|
|
36
|
+
_EXAMPLES = [
|
|
37
|
+
("codeintel setup --all .", "prepare backends and index this repo"),
|
|
38
|
+
("codeintel install", "register with the agents you have"),
|
|
39
|
+
("codeintel query --op callers --target my_function", "who calls it?"),
|
|
40
|
+
("codeintel doctor", "why is a query coming back empty?"),
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def render_help() -> str:
|
|
45
|
+
"""The `codeintel` / `codeintel help` screen: grouped, colored, with real examples.
|
|
46
|
+
|
|
47
|
+
Color comes from codeintel.term, so it auto-degrades on a pipe, under NO_COLOR, and on a dumb
|
|
48
|
+
terminal — same as every other human-facing command."""
|
|
49
|
+
from codeintel.term import c
|
|
50
|
+
|
|
51
|
+
width = max(len(name) for name in _COMMANDS)
|
|
52
|
+
out = [
|
|
53
|
+
c.bold("codeintel") + c.dim(f" {__version__}")
|
|
54
|
+
+ c.dim(" — code intelligence for AI agents: graph + LSP + semantic search"),
|
|
55
|
+
"",
|
|
56
|
+
c.dim("usage: ") + "codeintel <command> [options]",
|
|
57
|
+
]
|
|
58
|
+
for group, items in _COMMAND_GROUPS:
|
|
59
|
+
out.append("")
|
|
60
|
+
out.append(" " + c.bold(group))
|
|
61
|
+
for name, desc in items:
|
|
62
|
+
out.append(" " + c.cyan(name.ljust(width)) + " " + desc)
|
|
63
|
+
|
|
64
|
+
out.append("")
|
|
65
|
+
out.append(" " + c.bold("Examples"))
|
|
66
|
+
# Width from the content, not a guess — a hardcoded column silently loses its gutter the moment
|
|
67
|
+
# one example grows past it, and ljust() will not pad below the string's own length.
|
|
68
|
+
cmd_width = max(len(cmd) for cmd, _why in _EXAMPLES) + 2
|
|
69
|
+
for cmd, why in _EXAMPLES:
|
|
70
|
+
out.append(" " + cmd.ljust(cmd_width) + c.dim("# " + why))
|
|
71
|
+
|
|
72
|
+
out.append("")
|
|
73
|
+
out.append(" " + c.dim("codeintel <command> --help") + " full options for one command")
|
|
74
|
+
out.append(" " + c.dim("docs: https://github.com/hamilton-sky/codeintel"))
|
|
75
|
+
return "\n".join(out)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _suggest(unknown: str) -> list[str]:
|
|
79
|
+
"""Commands a typo probably meant. Close matches first, then prefix matches — `gragh` should
|
|
80
|
+
land on `graph`, and a bare `serv` on both `serve` and `serve-http`."""
|
|
81
|
+
close = difflib.get_close_matches(unknown, _COMMANDS, n=3, cutoff=0.5)
|
|
82
|
+
prefix = [cmd for cmd in _COMMANDS if cmd.startswith(unknown) and cmd not in close]
|
|
83
|
+
return (close + prefix)[:3]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _unknown_command(name: str) -> int:
|
|
87
|
+
"""Report an unrecognized command with a way forward. argparse's own error dumps the full list
|
|
88
|
+
of choices and stops there, which is a dead end for a one-character typo."""
|
|
89
|
+
from codeintel.term import c_err as e
|
|
90
|
+
|
|
91
|
+
print(e.red(f"unknown command: {name!r}"), file=sys.stderr)
|
|
92
|
+
matches = _suggest(name)
|
|
93
|
+
if matches:
|
|
94
|
+
joined = " or ".join(e.cyan(m) for m in matches)
|
|
95
|
+
print(f"\n did you mean {joined}?", file=sys.stderr)
|
|
96
|
+
print("\n " + e.dim("run `codeintel help` to see every command"), file=sys.stderr)
|
|
97
|
+
return 2
|
|
98
|
+
|
|
7
99
|
|
|
8
100
|
def main() -> None:
|
|
9
101
|
parser = argparse.ArgumentParser(prog="codeintel")
|
|
@@ -113,6 +205,19 @@ def main() -> None:
|
|
|
113
205
|
reset_parser.add_argument("--json", action="store_true", help="Emit the structured JSON report")
|
|
114
206
|
|
|
115
207
|
subparsers.add_parser("gen-token", help="Print a secure random bearer token (for serve-http / RBAC auth.toml)")
|
|
208
|
+
subparsers.add_parser("help", help="Show every command, grouped, with examples")
|
|
209
|
+
|
|
210
|
+
# Intercept an unrecognized command BEFORE argparse, whose error prints the full choice list and
|
|
211
|
+
# stops — a dead end for a one-character typo. Only a bare word is claimed here; anything
|
|
212
|
+
# starting with `-` (--version, --help) still goes to argparse.
|
|
213
|
+
argv = sys.argv[1:]
|
|
214
|
+
if argv and not argv[0].startswith("-") and argv[0] not in _COMMANDS + ["help"]:
|
|
215
|
+
sys.exit(_unknown_command(argv[0]))
|
|
216
|
+
if not argv or argv[0] == "help":
|
|
217
|
+
from codeintel import term
|
|
218
|
+
term.configure(no_color=False, ascii_mode=None)
|
|
219
|
+
print(render_help())
|
|
220
|
+
sys.exit(0)
|
|
116
221
|
|
|
117
222
|
args = parser.parse_args()
|
|
118
223
|
|
|
@@ -464,7 +569,7 @@ def main() -> None:
|
|
|
464
569
|
sys.exit(0)
|
|
465
570
|
|
|
466
571
|
else:
|
|
467
|
-
|
|
572
|
+
print(render_help())
|
|
468
573
|
sys.exit(0)
|
|
469
574
|
|
|
470
575
|
|
|
@@ -131,6 +131,13 @@ class _Handler(BaseHTTPRequestHandler):
|
|
|
131
131
|
parsed["role"] = role # server-authoritative — overrides any client-supplied role (no escalation)
|
|
132
132
|
if self.path == "/code/doctor":
|
|
133
133
|
result = code_doctor_handler(parsed)
|
|
134
|
+
# `registrations` (which agent hosts this machine registered codeintel with, and where)
|
|
135
|
+
# is a LOCAL diagnostic: on a shared deployment the server is not an agent host, so the
|
|
136
|
+
# field says nothing useful and only hands a client the server user's home layout and
|
|
137
|
+
# which agent tools are installed there. Dropped on the network transport; the CLI and
|
|
138
|
+
# the stdio MCP tool — both running as the user, on their own machine — still get it.
|
|
139
|
+
if isinstance(result, dict):
|
|
140
|
+
result.pop("registrations", None)
|
|
134
141
|
else:
|
|
135
142
|
result = code_query_handler(parsed)
|
|
136
143
|
# An RBAC denial (query OR doctor) comes back as a safe-null with this reason — 403 it.
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""The CLI's front door: `codeintel help` and what happens when you mistype a command.
|
|
2
|
+
|
|
3
|
+
argparse's default behavior on an unknown subcommand is to print the full list of choices and stop
|
|
4
|
+
— a dead end for a one-character typo (`gragh`, `dector`), which is exactly how it gets hit. And its
|
|
5
|
+
help lists twelve commands in declaration order with no grouping, so "what can this thing do?" means
|
|
6
|
+
reading every line to find the one verb you wanted.
|
|
7
|
+
|
|
8
|
+
The load-bearing invariant here is that the help screen stays HONEST: every command it advertises
|
|
9
|
+
must actually be a registered subcommand, and every registered subcommand must be advertised.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
|
|
16
|
+
import pytest
|
|
17
|
+
|
|
18
|
+
from codeintel.__main__ import _COMMAND_GROUPS, _COMMANDS, _suggest, render_help
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _run(*args, env_extra=None):
|
|
22
|
+
import os
|
|
23
|
+
env = {**os.environ, "NO_COLOR": "1", **(env_extra or {})}
|
|
24
|
+
return subprocess.run([sys.executable, "-m", "codeintel", *args],
|
|
25
|
+
capture_output=True, text=True, timeout=120, env=env)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# --------------------------------------------------------------------------- honesty
|
|
29
|
+
|
|
30
|
+
def test_help_advertises_exactly_the_registered_subcommands():
|
|
31
|
+
"""The drift that makes help worse than useless: a command renamed in argparse but not in the
|
|
32
|
+
help table, or advertised here and never wired up."""
|
|
33
|
+
listed = _run("--help").stdout
|
|
34
|
+
# argparse's own listing is the source of truth for what is actually registered.
|
|
35
|
+
registered = {line.split()[0] for line in listed.splitlines()
|
|
36
|
+
if line.startswith(" ") and line.strip() and not line.startswith(" ")}
|
|
37
|
+
registered = {r for r in registered if not r.startswith("-")}
|
|
38
|
+
assert set(_COMMANDS) | {"help"} == registered
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_every_advertised_command_has_a_description():
|
|
42
|
+
for _group, items in _COMMAND_GROUPS:
|
|
43
|
+
for name, desc in items:
|
|
44
|
+
assert desc.strip(), name
|
|
45
|
+
assert len(desc) > 20, f"{name}: description is too thin to help anyone"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_no_command_is_listed_in_two_groups():
|
|
49
|
+
assert len(_COMMANDS) == len(set(_COMMANDS))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# --------------------------------------------------------------------------- help screen
|
|
53
|
+
|
|
54
|
+
def test_bare_invocation_shows_the_grouped_help_and_exits_zero():
|
|
55
|
+
res = _run()
|
|
56
|
+
assert res.returncode == 0
|
|
57
|
+
for group, _items in _COMMAND_GROUPS:
|
|
58
|
+
assert group in res.stdout
|
|
59
|
+
assert "Examples" in res.stdout
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_help_subcommand_matches_the_bare_invocation():
|
|
63
|
+
assert _run("help").stdout == _run().stdout
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_help_lists_every_command_with_its_description():
|
|
67
|
+
out = _run("help").stdout
|
|
68
|
+
for name, desc in [(n, d) for _g, items in _COMMAND_GROUPS for n, d in items]:
|
|
69
|
+
assert name in out and desc in out
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_examples_stay_aligned_as_they_grow():
|
|
73
|
+
"""A hardcoded comment column silently loses its gutter the moment an example outgrows it —
|
|
74
|
+
ljust() will not pad below the string's own length."""
|
|
75
|
+
lines = [ln for ln in _run("help").stdout.splitlines() if " # " in ln]
|
|
76
|
+
assert len(lines) >= 3
|
|
77
|
+
assert len({ln.index("#") for ln in lines}) == 1 # one shared comment column
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_examples_only_reference_real_commands():
|
|
81
|
+
from codeintel.__main__ import _EXAMPLES
|
|
82
|
+
for cmd, _why in _EXAMPLES:
|
|
83
|
+
assert cmd.split()[0] == "codeintel"
|
|
84
|
+
assert cmd.split()[1] in _COMMANDS
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# --------------------------------------------------------------------------- typos
|
|
88
|
+
|
|
89
|
+
@pytest.mark.parametrize("typo,expected", [
|
|
90
|
+
("gragh", "graph"), # observed in real use
|
|
91
|
+
("dector", "doctor"), # observed in real use
|
|
92
|
+
("doctro", "doctor"),
|
|
93
|
+
("instal", "install"),
|
|
94
|
+
("quer", "query"),
|
|
95
|
+
("statuss", "status"),
|
|
96
|
+
])
|
|
97
|
+
def test_a_typo_suggests_the_command_that_was_meant(typo, expected):
|
|
98
|
+
assert expected in _suggest(typo)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_a_prefix_suggests_every_command_it_could_be():
|
|
102
|
+
assert set(_suggest("serve")) >= {"serve", "serve-http"}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_unknown_command_exits_two_with_a_suggestion():
|
|
106
|
+
res = _run("gragh", ".", "--html")
|
|
107
|
+
assert res.returncode == 2
|
|
108
|
+
assert "unknown command: 'gragh'" in res.stderr
|
|
109
|
+
assert "did you mean" in res.stderr and "graph" in res.stderr
|
|
110
|
+
assert "codeintel help" in res.stderr
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_unrecognizable_input_still_points_at_help():
|
|
114
|
+
res = _run("zzzzzzzz")
|
|
115
|
+
assert res.returncode == 2
|
|
116
|
+
assert "codeintel help" in res.stderr # no guess to offer, but never a dead end
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def test_flags_are_left_to_argparse():
|
|
120
|
+
"""Only a bare word is claimed as a command — `--version` and `--help` must still work."""
|
|
121
|
+
assert _run("--version").returncode == 0
|
|
122
|
+
assert _run("--help").returncode == 0
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# --------------------------------------------------------------------------- color
|
|
126
|
+
|
|
127
|
+
def test_color_is_stripped_when_not_a_tty():
|
|
128
|
+
assert "\x1b[" not in _run("help").stdout
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def test_no_color_beats_force_color():
|
|
132
|
+
assert "\x1b[" not in _run("help", env_extra={"FORCE_COLOR": "1"}).stdout
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def test_render_help_never_raises_without_a_terminal():
|
|
136
|
+
assert isinstance(render_help(), str) and render_help()
|
|
@@ -141,6 +141,16 @@ def test_rbac_reader_denied_doctor_is_403(rbac_server):
|
|
|
141
141
|
assert status == 403 and body["reason"] == "op-not-allowed-for-role"
|
|
142
142
|
|
|
143
143
|
|
|
144
|
+
def test_doctor_over_http_omits_local_registrations(rbac_server):
|
|
145
|
+
"""`registrations` names the agent config files on THIS machine. A shared HTTP deployment is
|
|
146
|
+
not an agent host, so the field says nothing useful there and only leaks the server user's home
|
|
147
|
+
layout and which agent tools are installed. It stays on the local CLI / stdio MCP surfaces."""
|
|
148
|
+
status, body = _doctor(rbac_server, "admintok")
|
|
149
|
+
assert status == 200
|
|
150
|
+
assert "registrations" not in body
|
|
151
|
+
assert "engines" in body # the rest of the report is untouched
|
|
152
|
+
|
|
153
|
+
|
|
144
154
|
def test_rbac_admin_allowed_doctor(rbac_server):
|
|
145
155
|
status, body = _doctor(rbac_server, "admintok") # admin is ["*"]
|
|
146
156
|
assert status == 200 and body.get("reason") != "op-not-allowed-for-role"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.12.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|