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.
Files changed (79) hide show
  1. {codecortex-0.12.0/src/codecortex.egg-info → codecortex-0.12.1}/PKG-INFO +7 -2
  2. {codecortex-0.12.0 → codecortex-0.12.1}/README.md +6 -1
  3. {codecortex-0.12.0 → codecortex-0.12.1/src/codecortex.egg-info}/PKG-INFO +7 -2
  4. {codecortex-0.12.0 → codecortex-0.12.1}/src/codecortex.egg-info/SOURCES.txt +1 -0
  5. codecortex-0.12.1/src/codeintel/__init__.py +1 -0
  6. {codecortex-0.12.0 → codecortex-0.12.1}/src/codeintel/__main__.py +106 -1
  7. {codecortex-0.12.0 → codecortex-0.12.1}/src/codeintel/http_server.py +7 -0
  8. codecortex-0.12.1/tests/test_cli_help.py +136 -0
  9. {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_rbac.py +10 -0
  10. codecortex-0.12.0/src/codeintel/__init__.py +0 -1
  11. {codecortex-0.12.0 → codecortex-0.12.1}/LICENSE +0 -0
  12. {codecortex-0.12.0 → codecortex-0.12.1}/pyproject.toml +0 -0
  13. {codecortex-0.12.0 → codecortex-0.12.1}/setup.cfg +0 -0
  14. {codecortex-0.12.0 → codecortex-0.12.1}/src/codecortex.egg-info/dependency_links.txt +0 -0
  15. {codecortex-0.12.0 → codecortex-0.12.1}/src/codecortex.egg-info/entry_points.txt +0 -0
  16. {codecortex-0.12.0 → codecortex-0.12.1}/src/codecortex.egg-info/requires.txt +0 -0
  17. {codecortex-0.12.0 → codecortex-0.12.1}/src/codecortex.egg-info/top_level.txt +0 -0
  18. {codecortex-0.12.0 → codecortex-0.12.1}/src/codeintel/auth.py +0 -0
  19. {codecortex-0.12.0 → codecortex-0.12.1}/src/codeintel/cache.py +0 -0
  20. {codecortex-0.12.0 → codecortex-0.12.1}/src/codeintel/config.py +0 -0
  21. {codecortex-0.12.0 → codecortex-0.12.1}/src/codeintel/doctor.py +0 -0
  22. {codecortex-0.12.0 → codecortex-0.12.1}/src/codeintel/gateway.py +0 -0
  23. {codecortex-0.12.0 → codecortex-0.12.1}/src/codeintel/grapher.py +0 -0
  24. {codecortex-0.12.0 → codecortex-0.12.1}/src/codeintel/indexer.py +0 -0
  25. {codecortex-0.12.0 → codecortex-0.12.1}/src/codeintel/injector.py +0 -0
  26. {codecortex-0.12.0 → codecortex-0.12.1}/src/codeintel/installer.py +0 -0
  27. {codecortex-0.12.0 → codecortex-0.12.1}/src/codeintel/logconfig.py +0 -0
  28. {codecortex-0.12.0 → codecortex-0.12.1}/src/codeintel/mapper.py +0 -0
  29. {codecortex-0.12.0 → codecortex-0.12.1}/src/codeintel/metrics.py +0 -0
  30. {codecortex-0.12.0 → codecortex-0.12.1}/src/codeintel/onboarding.py +0 -0
  31. {codecortex-0.12.0 → codecortex-0.12.1}/src/codeintel/policy.py +0 -0
  32. {codecortex-0.12.0 → codecortex-0.12.1}/src/codeintel/provider.py +0 -0
  33. {codecortex-0.12.0 → codecortex-0.12.1}/src/codeintel/providers/__init__.py +0 -0
  34. {codecortex-0.12.0 → codecortex-0.12.1}/src/codeintel/providers/graph.py +0 -0
  35. {codecortex-0.12.0 → codecortex-0.12.1}/src/codeintel/providers/lsp.py +0 -0
  36. {codecortex-0.12.0 → codecortex-0.12.1}/src/codeintel/providers/none.py +0 -0
  37. {codecortex-0.12.0 → codecortex-0.12.1}/src/codeintel/providers/semantic.py +0 -0
  38. {codecortex-0.12.0 → codecortex-0.12.1}/src/codeintel/reindexer.py +0 -0
  39. {codecortex-0.12.0 → codecortex-0.12.1}/src/codeintel/reset.py +0 -0
  40. {codecortex-0.12.0 → codecortex-0.12.1}/src/codeintel/searcher.py +0 -0
  41. {codecortex-0.12.0 → codecortex-0.12.1}/src/codeintel/semantic_db.py +0 -0
  42. {codecortex-0.12.0 → codecortex-0.12.1}/src/codeintel/server.py +0 -0
  43. {codecortex-0.12.0 → codecortex-0.12.1}/src/codeintel/term.py +0 -0
  44. {codecortex-0.12.0 → codecortex-0.12.1}/src/codeintel/verify.py +0 -0
  45. {codecortex-0.12.0 → codecortex-0.12.1}/src/codeintel/viewer/__init__.py +0 -0
  46. {codecortex-0.12.0 → codecortex-0.12.1}/src/codeintel/viewer/graph_template.html +0 -0
  47. {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_cache.py +0 -0
  48. {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_chunking.py +0 -0
  49. {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_config.py +0 -0
  50. {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_doctor.py +0 -0
  51. {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_e2e.py +0 -0
  52. {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_engine_adoption.py +0 -0
  53. {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_enterprise.py +0 -0
  54. {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_gateway.py +0 -0
  55. {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_graph_provider.py +0 -0
  56. {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_graph_real.py +0 -0
  57. {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_graph_stdin.py +0 -0
  58. {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_grapher.py +0 -0
  59. {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_hardening.py +0 -0
  60. {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_http_auth.py +0 -0
  61. {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_http_server.py +0 -0
  62. {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_installer.py +0 -0
  63. {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_integration.py +0 -0
  64. {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_lsp_provider.py +0 -0
  65. {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_lsp_real.py +0 -0
  66. {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_mapper.py +0 -0
  67. {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_mcp_handshake.py +0 -0
  68. {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_mcp_server.py +0 -0
  69. {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_model_dimension.py +0 -0
  70. {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_never_raise.py +0 -0
  71. {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_onboarding.py +0 -0
  72. {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_reindexer.py +0 -0
  73. {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_rerank.py +0 -0
  74. {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_reset.py +0 -0
  75. {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_semantic_provider.py +0 -0
  76. {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_term.py +0 -0
  77. {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_treesitter.py +0 -0
  78. {codecortex-0.12.0 → codecortex-0.12.1}/tests/test_verify.py +0 -0
  79. {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.0
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 # ~390 tests, ~35s (live graph/LSP backend tests skip when absent)
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 # ~390 tests, ~35s (live graph/LSP backend tests skip when absent)
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.0
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 # ~390 tests, ~35s (live graph/LSP backend tests skip when absent)
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
@@ -41,6 +41,7 @@ src/codeintel/viewer/__init__.py
41
41
  src/codeintel/viewer/graph_template.html
42
42
  tests/test_cache.py
43
43
  tests/test_chunking.py
44
+ tests/test_cli_help.py
44
45
  tests/test_config.py
45
46
  tests/test_doctor.py
46
47
  tests/test_e2e.py
@@ -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
- parser.print_help()
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