codexa 0.4.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.
Files changed (189) hide show
  1. codexa-0.4.0.dist-info/METADATA +650 -0
  2. codexa-0.4.0.dist-info/RECORD +189 -0
  3. codexa-0.4.0.dist-info/WHEEL +5 -0
  4. codexa-0.4.0.dist-info/entry_points.txt +2 -0
  5. codexa-0.4.0.dist-info/licenses/LICENSE +21 -0
  6. codexa-0.4.0.dist-info/top_level.txt +1 -0
  7. semantic_code_intelligence/__init__.py +5 -0
  8. semantic_code_intelligence/analysis/__init__.py +21 -0
  9. semantic_code_intelligence/analysis/ai_features.py +351 -0
  10. semantic_code_intelligence/bridge/__init__.py +28 -0
  11. semantic_code_intelligence/bridge/context_provider.py +245 -0
  12. semantic_code_intelligence/bridge/protocol.py +167 -0
  13. semantic_code_intelligence/bridge/server.py +348 -0
  14. semantic_code_intelligence/bridge/vscode.py +271 -0
  15. semantic_code_intelligence/ci/__init__.py +13 -0
  16. semantic_code_intelligence/ci/hooks.py +98 -0
  17. semantic_code_intelligence/ci/hotspots.py +272 -0
  18. semantic_code_intelligence/ci/impact.py +246 -0
  19. semantic_code_intelligence/ci/metrics.py +591 -0
  20. semantic_code_intelligence/ci/pr.py +412 -0
  21. semantic_code_intelligence/ci/quality.py +557 -0
  22. semantic_code_intelligence/ci/templates.py +164 -0
  23. semantic_code_intelligence/ci/trace.py +224 -0
  24. semantic_code_intelligence/cli/__init__.py +0 -0
  25. semantic_code_intelligence/cli/commands/__init__.py +0 -0
  26. semantic_code_intelligence/cli/commands/ask_cmd.py +153 -0
  27. semantic_code_intelligence/cli/commands/benchmark_cmd.py +303 -0
  28. semantic_code_intelligence/cli/commands/chat_cmd.py +252 -0
  29. semantic_code_intelligence/cli/commands/ci_gen_cmd.py +74 -0
  30. semantic_code_intelligence/cli/commands/context_cmd.py +120 -0
  31. semantic_code_intelligence/cli/commands/cross_refactor_cmd.py +113 -0
  32. semantic_code_intelligence/cli/commands/deps_cmd.py +91 -0
  33. semantic_code_intelligence/cli/commands/docs_cmd.py +101 -0
  34. semantic_code_intelligence/cli/commands/doctor_cmd.py +147 -0
  35. semantic_code_intelligence/cli/commands/evolve_cmd.py +171 -0
  36. semantic_code_intelligence/cli/commands/explain_cmd.py +112 -0
  37. semantic_code_intelligence/cli/commands/gate_cmd.py +135 -0
  38. semantic_code_intelligence/cli/commands/grep_cmd.py +234 -0
  39. semantic_code_intelligence/cli/commands/hotspots_cmd.py +119 -0
  40. semantic_code_intelligence/cli/commands/impact_cmd.py +131 -0
  41. semantic_code_intelligence/cli/commands/index_cmd.py +138 -0
  42. semantic_code_intelligence/cli/commands/init_cmd.py +152 -0
  43. semantic_code_intelligence/cli/commands/investigate_cmd.py +163 -0
  44. semantic_code_intelligence/cli/commands/languages_cmd.py +101 -0
  45. semantic_code_intelligence/cli/commands/lsp_cmd.py +49 -0
  46. semantic_code_intelligence/cli/commands/mcp_cmd.py +50 -0
  47. semantic_code_intelligence/cli/commands/metrics_cmd.py +264 -0
  48. semantic_code_intelligence/cli/commands/models_cmd.py +157 -0
  49. semantic_code_intelligence/cli/commands/plugin_cmd.py +275 -0
  50. semantic_code_intelligence/cli/commands/pr_summary_cmd.py +178 -0
  51. semantic_code_intelligence/cli/commands/quality_cmd.py +208 -0
  52. semantic_code_intelligence/cli/commands/refactor_cmd.py +103 -0
  53. semantic_code_intelligence/cli/commands/review_cmd.py +88 -0
  54. semantic_code_intelligence/cli/commands/search_cmd.py +236 -0
  55. semantic_code_intelligence/cli/commands/serve_cmd.py +117 -0
  56. semantic_code_intelligence/cli/commands/suggest_cmd.py +100 -0
  57. semantic_code_intelligence/cli/commands/summary_cmd.py +78 -0
  58. semantic_code_intelligence/cli/commands/tool_cmd.py +282 -0
  59. semantic_code_intelligence/cli/commands/trace_cmd.py +123 -0
  60. semantic_code_intelligence/cli/commands/tui_cmd.py +58 -0
  61. semantic_code_intelligence/cli/commands/viz_cmd.py +127 -0
  62. semantic_code_intelligence/cli/commands/watch_cmd.py +72 -0
  63. semantic_code_intelligence/cli/commands/web_cmd.py +61 -0
  64. semantic_code_intelligence/cli/commands/workspace_cmd.py +250 -0
  65. semantic_code_intelligence/cli/main.py +65 -0
  66. semantic_code_intelligence/cli/router.py +92 -0
  67. semantic_code_intelligence/config/__init__.py +0 -0
  68. semantic_code_intelligence/config/settings.py +260 -0
  69. semantic_code_intelligence/context/__init__.py +19 -0
  70. semantic_code_intelligence/context/engine.py +429 -0
  71. semantic_code_intelligence/context/memory.py +253 -0
  72. semantic_code_intelligence/daemon/__init__.py +1 -0
  73. semantic_code_intelligence/daemon/watcher.py +515 -0
  74. semantic_code_intelligence/docs/__init__.py +1080 -0
  75. semantic_code_intelligence/embeddings/__init__.py +0 -0
  76. semantic_code_intelligence/embeddings/enhanced.py +131 -0
  77. semantic_code_intelligence/embeddings/generator.py +149 -0
  78. semantic_code_intelligence/embeddings/model_registry.py +100 -0
  79. semantic_code_intelligence/evolution/__init__.py +1 -0
  80. semantic_code_intelligence/evolution/budget_guard.py +111 -0
  81. semantic_code_intelligence/evolution/commit_manager.py +88 -0
  82. semantic_code_intelligence/evolution/context_builder.py +131 -0
  83. semantic_code_intelligence/evolution/engine.py +249 -0
  84. semantic_code_intelligence/evolution/patch_generator.py +229 -0
  85. semantic_code_intelligence/evolution/task_selector.py +214 -0
  86. semantic_code_intelligence/evolution/test_runner.py +111 -0
  87. semantic_code_intelligence/indexing/__init__.py +0 -0
  88. semantic_code_intelligence/indexing/chunker.py +174 -0
  89. semantic_code_intelligence/indexing/parallel.py +86 -0
  90. semantic_code_intelligence/indexing/scanner.py +146 -0
  91. semantic_code_intelligence/indexing/semantic_chunker.py +337 -0
  92. semantic_code_intelligence/llm/__init__.py +62 -0
  93. semantic_code_intelligence/llm/cache.py +219 -0
  94. semantic_code_intelligence/llm/cached_provider.py +145 -0
  95. semantic_code_intelligence/llm/conversation.py +190 -0
  96. semantic_code_intelligence/llm/cross_refactor.py +272 -0
  97. semantic_code_intelligence/llm/investigation.py +274 -0
  98. semantic_code_intelligence/llm/mock_provider.py +77 -0
  99. semantic_code_intelligence/llm/ollama_provider.py +122 -0
  100. semantic_code_intelligence/llm/openai_provider.py +100 -0
  101. semantic_code_intelligence/llm/provider.py +92 -0
  102. semantic_code_intelligence/llm/rate_limiter.py +164 -0
  103. semantic_code_intelligence/llm/reasoning.py +438 -0
  104. semantic_code_intelligence/llm/safety.py +110 -0
  105. semantic_code_intelligence/llm/streaming.py +251 -0
  106. semantic_code_intelligence/lsp/__init__.py +609 -0
  107. semantic_code_intelligence/mcp/__init__.py +393 -0
  108. semantic_code_intelligence/parsing/__init__.py +19 -0
  109. semantic_code_intelligence/parsing/parser.py +375 -0
  110. semantic_code_intelligence/plugins/__init__.py +255 -0
  111. semantic_code_intelligence/plugins/examples/__init__.py +1 -0
  112. semantic_code_intelligence/plugins/examples/code_quality.py +73 -0
  113. semantic_code_intelligence/plugins/examples/search_annotator.py +56 -0
  114. semantic_code_intelligence/scalability/__init__.py +205 -0
  115. semantic_code_intelligence/search/__init__.py +0 -0
  116. semantic_code_intelligence/search/formatter.py +123 -0
  117. semantic_code_intelligence/search/grep.py +361 -0
  118. semantic_code_intelligence/search/hybrid_search.py +170 -0
  119. semantic_code_intelligence/search/keyword_search.py +311 -0
  120. semantic_code_intelligence/search/section_expander.py +103 -0
  121. semantic_code_intelligence/services/__init__.py +0 -0
  122. semantic_code_intelligence/services/indexing_service.py +630 -0
  123. semantic_code_intelligence/services/search_service.py +269 -0
  124. semantic_code_intelligence/storage/__init__.py +0 -0
  125. semantic_code_intelligence/storage/chunk_hash_store.py +86 -0
  126. semantic_code_intelligence/storage/hash_store.py +66 -0
  127. semantic_code_intelligence/storage/index_manifest.py +85 -0
  128. semantic_code_intelligence/storage/index_stats.py +138 -0
  129. semantic_code_intelligence/storage/query_history.py +160 -0
  130. semantic_code_intelligence/storage/symbol_registry.py +209 -0
  131. semantic_code_intelligence/storage/vector_store.py +297 -0
  132. semantic_code_intelligence/tests/__init__.py +0 -0
  133. semantic_code_intelligence/tests/test_ai_features.py +351 -0
  134. semantic_code_intelligence/tests/test_chunker.py +119 -0
  135. semantic_code_intelligence/tests/test_cli.py +188 -0
  136. semantic_code_intelligence/tests/test_config.py +154 -0
  137. semantic_code_intelligence/tests/test_context.py +381 -0
  138. semantic_code_intelligence/tests/test_embeddings.py +73 -0
  139. semantic_code_intelligence/tests/test_endtoend.py +1142 -0
  140. semantic_code_intelligence/tests/test_enhanced_embeddings.py +92 -0
  141. semantic_code_intelligence/tests/test_hash_store.py +79 -0
  142. semantic_code_intelligence/tests/test_logging.py +55 -0
  143. semantic_code_intelligence/tests/test_new_cli.py +138 -0
  144. semantic_code_intelligence/tests/test_parser.py +495 -0
  145. semantic_code_intelligence/tests/test_phase10.py +355 -0
  146. semantic_code_intelligence/tests/test_phase11.py +593 -0
  147. semantic_code_intelligence/tests/test_phase12.py +375 -0
  148. semantic_code_intelligence/tests/test_phase13.py +663 -0
  149. semantic_code_intelligence/tests/test_phase14.py +568 -0
  150. semantic_code_intelligence/tests/test_phase15.py +814 -0
  151. semantic_code_intelligence/tests/test_phase16.py +792 -0
  152. semantic_code_intelligence/tests/test_phase17.py +815 -0
  153. semantic_code_intelligence/tests/test_phase18.py +934 -0
  154. semantic_code_intelligence/tests/test_phase19.py +986 -0
  155. semantic_code_intelligence/tests/test_phase20.py +2753 -0
  156. semantic_code_intelligence/tests/test_phase20b.py +2058 -0
  157. semantic_code_intelligence/tests/test_phase20c.py +962 -0
  158. semantic_code_intelligence/tests/test_phase21.py +428 -0
  159. semantic_code_intelligence/tests/test_phase22.py +799 -0
  160. semantic_code_intelligence/tests/test_phase23.py +783 -0
  161. semantic_code_intelligence/tests/test_phase24.py +715 -0
  162. semantic_code_intelligence/tests/test_phase25.py +496 -0
  163. semantic_code_intelligence/tests/test_phase26.py +251 -0
  164. semantic_code_intelligence/tests/test_phase27.py +531 -0
  165. semantic_code_intelligence/tests/test_phase8.py +592 -0
  166. semantic_code_intelligence/tests/test_phase9.py +643 -0
  167. semantic_code_intelligence/tests/test_plugins.py +293 -0
  168. semantic_code_intelligence/tests/test_priority_features.py +727 -0
  169. semantic_code_intelligence/tests/test_router.py +41 -0
  170. semantic_code_intelligence/tests/test_scalability.py +138 -0
  171. semantic_code_intelligence/tests/test_scanner.py +125 -0
  172. semantic_code_intelligence/tests/test_search.py +160 -0
  173. semantic_code_intelligence/tests/test_semantic_chunker.py +255 -0
  174. semantic_code_intelligence/tests/test_tools.py +182 -0
  175. semantic_code_intelligence/tests/test_vector_store.py +151 -0
  176. semantic_code_intelligence/tests/test_watcher.py +211 -0
  177. semantic_code_intelligence/tools/__init__.py +442 -0
  178. semantic_code_intelligence/tools/executor.py +232 -0
  179. semantic_code_intelligence/tools/protocol.py +200 -0
  180. semantic_code_intelligence/tui/__init__.py +454 -0
  181. semantic_code_intelligence/utils/__init__.py +0 -0
  182. semantic_code_intelligence/utils/logging.py +112 -0
  183. semantic_code_intelligence/version.py +3 -0
  184. semantic_code_intelligence/web/__init__.py +11 -0
  185. semantic_code_intelligence/web/api.py +289 -0
  186. semantic_code_intelligence/web/server.py +397 -0
  187. semantic_code_intelligence/web/ui.py +659 -0
  188. semantic_code_intelligence/web/visualize.py +226 -0
  189. semantic_code_intelligence/workspace/__init__.py +427 -0
@@ -0,0 +1,119 @@
1
+ """CLI command: hotspots — identify high-risk code hotspots."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json as json_mod
6
+ from pathlib import Path
7
+
8
+ import click
9
+
10
+ from semantic_code_intelligence.utils.logging import (
11
+ console,
12
+ get_logger,
13
+ print_error,
14
+ print_success,
15
+ )
16
+
17
+ logger = get_logger("cli.hotspots")
18
+
19
+
20
+ @click.command("hotspots")
21
+ @click.option(
22
+ "--path", "-p",
23
+ default=".",
24
+ type=click.Path(exists=True, file_okay=False, resolve_path=True),
25
+ help="Project root path.",
26
+ )
27
+ @click.option(
28
+ "--json-output", "--json", "json_mode",
29
+ is_flag=True, default=False,
30
+ help="Output in JSON format.",
31
+ )
32
+ @click.option(
33
+ "--pipe",
34
+ is_flag=True, default=False,
35
+ help="Plain text output for piping / CI.",
36
+ )
37
+ @click.option(
38
+ "--top-n", "-n",
39
+ type=int, default=20,
40
+ help="Number of hotspots to report (default: 20).",
41
+ )
42
+ @click.option(
43
+ "--include-git/--no-git",
44
+ default=True,
45
+ help="Include git churn data (default: enabled).",
46
+ )
47
+ @click.pass_context
48
+ def hotspots_cmd(
49
+ ctx: click.Context,
50
+ path: str,
51
+ json_mode: bool,
52
+ pipe: bool,
53
+ top_n: int,
54
+ include_git: bool,
55
+ ) -> None:
56
+ """Identify high-risk code hotspots via multi-factor analysis.
57
+
58
+ Combines complexity, duplication, fan-in/out, and git churn to
59
+ score symbols by maintenance risk.
60
+
61
+ Examples:
62
+
63
+ codexa hotspots
64
+
65
+ codexa hotspots --top-n 10 --json
66
+
67
+ codexa hotspots --no-git --pipe
68
+ """
69
+ from semantic_code_intelligence.ci.hotspots import analyze_hotspots
70
+ from semantic_code_intelligence.context.engine import CallGraph, ContextBuilder, DependencyMap
71
+
72
+ root = Path(path).resolve()
73
+ builder = ContextBuilder()
74
+ dep_map = DependencyMap()
75
+
76
+ py_files = sorted(root.rglob("*.py"))
77
+ py_files = [f for f in py_files if ".venv" not in f.parts and "__pycache__" not in f.parts]
78
+
79
+ for fp in py_files:
80
+ try:
81
+ content = fp.read_text(encoding="utf-8", errors="replace")
82
+ builder.index_file(str(fp), content)
83
+ dep_map.add_file(str(fp), content)
84
+ except Exception:
85
+ logger.debug("Failed to index %s", fp)
86
+ continue
87
+
88
+ symbols = builder.get_all_symbols()
89
+ call_graph = CallGraph()
90
+ call_graph.build(symbols)
91
+
92
+ try:
93
+ report = analyze_hotspots(
94
+ symbols, call_graph, dep_map, root,
95
+ top_n=top_n, include_git=include_git,
96
+ )
97
+ except Exception as exc:
98
+ logger.debug("Hotspot analysis failed", exc_info=True)
99
+ print_error(f"Hotspot analysis failed: {exc}")
100
+ ctx.exit(1)
101
+ return
102
+
103
+ if json_mode:
104
+ click.echo(json_mod.dumps(report.to_dict(), indent=2))
105
+ elif pipe:
106
+ click.echo(f"files={report.files_analyzed} symbols={report.symbols_analyzed} hotspots={len(report.hotspots)}")
107
+ for h in report.hotspots:
108
+ click.echo(f" {h.risk_score:.3f} {h.kind:<10} {h.file_path}:{h.name}")
109
+ else:
110
+ console.print(f"\n[bold]Hotspot Analysis[/bold] — {report.files_analyzed} files, {report.symbols_analyzed} symbols\n")
111
+ if not report.hotspots:
112
+ print_success("No significant hotspots detected.")
113
+ return
114
+ for i, h in enumerate(report.hotspots, 1):
115
+ colour = "red" if h.risk_score >= 0.7 else "yellow" if h.risk_score >= 0.4 else "green"
116
+ console.print(f" [{colour}]{i:>3}. {h.risk_score:.3f}[/{colour}] {h.kind:<10} [cyan]{h.file_path}[/cyan]:[bold]{h.name}[/bold]")
117
+ for f in h.factors:
118
+ console.print(f" {f.name}: {f.raw_value:.2f} (norm={f.normalized:.2f}, w={f.weight:.2f})")
119
+ console.print()
@@ -0,0 +1,131 @@
1
+ """CLI command: impact — analyse blast radius of code changes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json as json_mod
6
+ from pathlib import Path
7
+
8
+ import click
9
+
10
+ from semantic_code_intelligence.utils.logging import (
11
+ console,
12
+ get_logger,
13
+ print_error,
14
+ print_success,
15
+ )
16
+
17
+ logger = get_logger("cli.impact")
18
+
19
+
20
+ @click.command("impact")
21
+ @click.argument("target")
22
+ @click.option(
23
+ "--path", "-p",
24
+ default=".",
25
+ type=click.Path(exists=True, file_okay=False, resolve_path=True),
26
+ help="Project root path.",
27
+ )
28
+ @click.option(
29
+ "--json-output", "--json", "json_mode",
30
+ is_flag=True, default=False,
31
+ help="Output in JSON format.",
32
+ )
33
+ @click.option(
34
+ "--pipe",
35
+ is_flag=True, default=False,
36
+ help="Plain text output for piping / CI.",
37
+ )
38
+ @click.option(
39
+ "--max-depth", "-d",
40
+ type=int, default=5,
41
+ help="Maximum traversal depth (default: 5).",
42
+ )
43
+ @click.pass_context
44
+ def impact_cmd(
45
+ ctx: click.Context,
46
+ target: str,
47
+ path: str,
48
+ json_mode: bool,
49
+ pipe: bool,
50
+ max_depth: int,
51
+ ) -> None:
52
+ """Analyse the blast radius of a change to TARGET.
53
+
54
+ TARGET can be a symbol name (function/class) or a file path relative
55
+ to the project root.
56
+
57
+ Examples:
58
+
59
+ codexa impact parse_file
60
+
61
+ codexa impact src/parser.py --json
62
+
63
+ codexa impact MyClass --max-depth 3 --pipe
64
+ """
65
+ from semantic_code_intelligence.ci.impact import analyze_impact
66
+ from semantic_code_intelligence.context.engine import CallGraph, ContextBuilder, DependencyMap
67
+
68
+ root = Path(path).resolve()
69
+ builder = ContextBuilder()
70
+ dep_map = DependencyMap()
71
+
72
+ py_files = sorted(root.rglob("*.py"))
73
+ py_files = [f for f in py_files if ".venv" not in f.parts and "__pycache__" not in f.parts]
74
+
75
+ for fp in py_files:
76
+ try:
77
+ content = fp.read_text(encoding="utf-8", errors="replace")
78
+ builder.index_file(str(fp), content)
79
+ dep_map.add_file(str(fp), content)
80
+ except Exception:
81
+ logger.debug("Failed to index %s", fp)
82
+ continue
83
+
84
+ symbols = builder.get_all_symbols()
85
+ call_graph = CallGraph()
86
+ call_graph.build(symbols)
87
+
88
+ try:
89
+ report = analyze_impact(
90
+ target, symbols, call_graph, dep_map, root,
91
+ max_depth=max_depth,
92
+ )
93
+ except Exception as exc:
94
+ logger.debug("Impact analysis failed", exc_info=True)
95
+ print_error(f"Impact analysis failed: {exc}")
96
+ ctx.exit(1)
97
+ return
98
+
99
+ if json_mode:
100
+ click.echo(json_mod.dumps(report.to_dict(), indent=2))
101
+ elif pipe:
102
+ click.echo(f"target={report.target} kind={report.target_kind} affected={report.total_affected}")
103
+ for s in report.direct_symbols:
104
+ click.echo(f" DIRECT {s.relationship:<20} {s.file_path}:{s.name}")
105
+ for s in report.transitive_symbols:
106
+ click.echo(f" TRANS {s.relationship:<20} {s.file_path}:{s.name}")
107
+ for m in report.affected_modules:
108
+ click.echo(f" MODULE {m.relationship:<20} {m.file_path}")
109
+ else:
110
+ console.print(f"\n[bold]Impact Analysis[/bold] — target: [cyan]{report.target}[/cyan] ({report.target_kind})\n")
111
+ if report.total_affected == 0:
112
+ print_success("No downstream impact detected.")
113
+ return
114
+
115
+ if report.direct_symbols:
116
+ console.print("[bold]Direct callers:[/bold]")
117
+ for s in report.direct_symbols:
118
+ console.print(f" [yellow]{s.name}[/yellow] ({s.kind}) [dim]{s.file_path}[/dim]")
119
+
120
+ if report.transitive_symbols:
121
+ console.print("\n[bold]Transitive callers:[/bold]")
122
+ for s in report.transitive_symbols:
123
+ console.print(f" [yellow]{s.name}[/yellow] depth={s.depth} [dim]{s.file_path}[/dim]")
124
+
125
+ if report.affected_modules:
126
+ console.print("\n[bold]Affected modules:[/bold]")
127
+ for m in report.affected_modules:
128
+ console.print(f" [cyan]{m.file_path}[/cyan] ({m.relationship}, depth={m.depth})")
129
+
130
+ console.print(f"\n[bold]Total affected:[/bold] {report.total_affected}")
131
+ console.print()
@@ -0,0 +1,138 @@
1
+ """CLI command: index - Index a codebase for semantic search."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from pathlib import Path
7
+
8
+ import click
9
+
10
+ from semantic_code_intelligence.config.settings import AppConfig
11
+ from semantic_code_intelligence.services.indexing_service import run_indexing
12
+ from semantic_code_intelligence.utils.logging import (
13
+ get_logger,
14
+ print_error,
15
+ print_info,
16
+ print_success,
17
+ print_warning,
18
+ )
19
+
20
+ logger = get_logger("cli.index")
21
+
22
+
23
+ def _run_watch_mode(root: Path, force: bool) -> None:
24
+ """Run continuous watch-mode indexing with live incremental updates."""
25
+ from semantic_code_intelligence.daemon.watcher import NativeFileWatcher
26
+ from semantic_code_intelligence.services.indexing_service import run_incremental_indexing
27
+
28
+ # Initial index
29
+ print_info("Watch mode: performing initial index...")
30
+ result = run_indexing(project_root=root, force=force)
31
+ print_success(
32
+ f"Initial index: {result.files_indexed} files, "
33
+ f"{result.chunks_created} chunks, {result.total_vectors} vectors."
34
+ )
35
+ print_info("Watching for changes... (press Ctrl+C to stop)")
36
+
37
+ update_count = 0
38
+
39
+ def _on_changes(events: list) -> None:
40
+ nonlocal update_count
41
+ changed = [str(e.path) for e in events if e.change_type in ("created", "modified")]
42
+ deleted = [str(e.path) for e in events if e.change_type == "deleted"]
43
+ if not changed and not deleted:
44
+ return
45
+ try:
46
+ inc = run_incremental_indexing(root, changed_files=changed, deleted_files=deleted)
47
+ update_count += 1
48
+ print_success(
49
+ f"[update #{update_count}] Re-indexed {inc.files_indexed} files "
50
+ f"({inc.chunks_created} chunks). {len(deleted)} deleted."
51
+ )
52
+ except Exception as exc:
53
+ logger.debug("Incremental indexing error", exc_info=True)
54
+ print_error(f"Incremental indexing failed: {exc}")
55
+
56
+ watcher = NativeFileWatcher(root)
57
+ watcher.on_change(_on_changes)
58
+ watcher.start()
59
+
60
+ try:
61
+ while True:
62
+ time.sleep(1)
63
+ except KeyboardInterrupt:
64
+ pass
65
+ finally:
66
+ watcher.stop()
67
+ print_success(f"Watch mode stopped. {update_count} incremental updates applied.")
68
+
69
+
70
+ @click.command("index")
71
+ @click.argument(
72
+ "path",
73
+ default=".",
74
+ type=click.Path(exists=True, file_okay=False, resolve_path=True),
75
+ )
76
+ @click.option(
77
+ "--force",
78
+ is_flag=True,
79
+ default=False,
80
+ help="Force full re-index, ignoring cache.",
81
+ )
82
+ @click.option(
83
+ "--watch",
84
+ "-w",
85
+ is_flag=True,
86
+ default=False,
87
+ help="Watch for file changes and re-index incrementally.",
88
+ )
89
+ @click.pass_context
90
+ def index_cmd(ctx: click.Context, path: str, force: bool, watch: bool) -> None:
91
+ """Index a codebase for semantic search.
92
+
93
+ Scans the target directory, extracts code chunks, generates embeddings,
94
+ and stores them in the vector index.
95
+
96
+ Use --watch to enable live incremental re-indexing on file changes.
97
+
98
+ \b
99
+ Examples:
100
+ codexa index
101
+ codexa index --force
102
+ codexa index --watch
103
+ """
104
+ root = Path(path).resolve()
105
+ config_dir = AppConfig.config_dir(root)
106
+
107
+ if not config_dir.exists():
108
+ print_error(
109
+ f"Project not initialized at {root}. Run 'codexa init' first."
110
+ )
111
+ ctx.exit(1)
112
+ return
113
+
114
+ if watch:
115
+ _run_watch_mode(root, force)
116
+ return
117
+
118
+ print_info(f"Indexing codebase at: {root}")
119
+
120
+ if force:
121
+ print_info("Force mode: full re-index will be performed.")
122
+
123
+ try:
124
+ result = run_indexing(project_root=root, force=force)
125
+ except Exception as e:
126
+ print_error(f"Indexing failed: {e}")
127
+ logger.debug("Indexing error details:", exc_info=True)
128
+ ctx.exit(1)
129
+ return
130
+
131
+ if result.files_scanned == 0:
132
+ print_warning("No indexable files found.")
133
+ else:
134
+ print_success(
135
+ f"Indexed {result.files_indexed} files "
136
+ f"({result.chunks_created} chunks, {result.total_vectors} vectors). "
137
+ f"Skipped {result.files_skipped} unchanged files."
138
+ )
@@ -0,0 +1,152 @@
1
+ """CLI command: init - Initialize a new project for semantic code intelligence."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+
8
+ import click
9
+
10
+ from semantic_code_intelligence.config.settings import (
11
+ AppConfig,
12
+ init_project,
13
+ load_config,
14
+ )
15
+ from semantic_code_intelligence.utils.logging import (
16
+ get_logger,
17
+ print_error,
18
+ print_info,
19
+ print_success,
20
+ print_warning,
21
+ )
22
+
23
+ logger = get_logger("cli.init")
24
+
25
+
26
+ def _generate_vscode_mcp_config(root: Path) -> bool:
27
+ """Create .vscode/settings.json with MCP server config if not present."""
28
+ vscode_dir = root / ".vscode"
29
+ settings_path = vscode_dir / "settings.json"
30
+
31
+ mcp_block = {
32
+ "mcp": {
33
+ "servers": {
34
+ "codexa": {
35
+ "command": "codexa",
36
+ "args": ["mcp", "--path", str(root)],
37
+ }
38
+ }
39
+ }
40
+ }
41
+
42
+ if settings_path.exists():
43
+ try:
44
+ existing = json.loads(settings_path.read_text(encoding="utf-8"))
45
+ except (json.JSONDecodeError, OSError):
46
+ existing = {}
47
+
48
+ if "mcp" in existing:
49
+ return False # already configured
50
+
51
+ existing.update(mcp_block)
52
+ settings_path.write_text(
53
+ json.dumps(existing, indent=4) + "\n", encoding="utf-8"
54
+ )
55
+ return True
56
+
57
+ vscode_dir.mkdir(exist_ok=True)
58
+ settings_path.write_text(
59
+ json.dumps(mcp_block, indent=4) + "\n", encoding="utf-8"
60
+ )
61
+ return True
62
+
63
+
64
+ @click.command("init")
65
+ @click.argument(
66
+ "path",
67
+ default=".",
68
+ type=click.Path(exists=True, file_okay=False, resolve_path=True),
69
+ )
70
+ @click.option(
71
+ "--index",
72
+ "auto_index",
73
+ is_flag=True,
74
+ default=False,
75
+ help="Automatically index the project after initialization.",
76
+ )
77
+ @click.option(
78
+ "--vscode",
79
+ "setup_vscode",
80
+ is_flag=True,
81
+ default=False,
82
+ help="Generate .vscode/settings.json with MCP server config.",
83
+ )
84
+ @click.pass_context
85
+ def init_cmd(ctx: click.Context, path: str, auto_index: bool, setup_vscode: bool) -> None:
86
+ """Initialize a project for semantic code indexing.
87
+
88
+ Creates a .codexa/ directory with default configuration and an empty index.
89
+
90
+ \b
91
+ Quick start:
92
+ codexa init # basic setup
93
+ codexa init --index # setup + build index immediately
94
+ codexa init --vscode # setup + configure VS Code MCP
95
+ codexa init --index --vscode # full setup in one command
96
+ """
97
+ root = Path(path).resolve()
98
+
99
+ # Check if already initialized
100
+ config_dir = AppConfig.config_dir(root)
101
+ if config_dir.exists():
102
+ print_info(f"Project already initialized at {root}")
103
+ print_info(f"Config directory: {config_dir}")
104
+ # Still allow --vscode and --index on existing projects
105
+ if setup_vscode:
106
+ if _generate_vscode_mcp_config(root):
107
+ print_success("VS Code MCP config written to .vscode/settings.json")
108
+ else:
109
+ print_info("VS Code MCP config already exists")
110
+ if auto_index:
111
+ _run_index(root)
112
+ return
113
+
114
+ try:
115
+ config, config_path = init_project(root)
116
+ print_success(f"Initialized project at {root}")
117
+ print_info(f"Config file: {config_path}")
118
+ print_info(f"Index directory: {AppConfig.index_dir(root)}")
119
+ logger.debug("Default config: %s", config.model_dump())
120
+ except OSError as e:
121
+ print_error(f"Failed to initialize project: {e}")
122
+ ctx.exit(1)
123
+ return
124
+
125
+ if setup_vscode:
126
+ if _generate_vscode_mcp_config(root):
127
+ print_success("VS Code MCP config written to .vscode/settings.json")
128
+
129
+ if auto_index:
130
+ _run_index(root)
131
+ else:
132
+ print_info("")
133
+ print_info("Next steps:")
134
+ print_info(" codexa index — Build the search index")
135
+ print_info(" codexa search — Search your code")
136
+ print_info(" codexa grep — Raw file search (no index needed)")
137
+
138
+
139
+ def _run_index(root: Path) -> None:
140
+ """Run indexing as part of init."""
141
+ from semantic_code_intelligence.services.indexing_service import index_project
142
+
143
+ print_info("Building search index...")
144
+ try:
145
+ result = index_project(root)
146
+ print_success(
147
+ f"Indexed {result.chunks_stored} chunks from "
148
+ f"{result.files_scanned} files"
149
+ )
150
+ except Exception as e:
151
+ print_warning(f"Indexing failed: {e}")
152
+ print_info("Run 'codexa index' manually to build the index.")
@@ -0,0 +1,163 @@
1
+ """CLI command: investigate — autonomous multi-step code investigation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json as json_mod
6
+ from pathlib import Path
7
+ from typing import TYPE_CHECKING, Any
8
+
9
+ import click
10
+
11
+ from semantic_code_intelligence.utils.logging import (
12
+ console,
13
+ get_logger,
14
+ print_error,
15
+ print_info,
16
+ )
17
+
18
+ if TYPE_CHECKING:
19
+ from semantic_code_intelligence.llm.provider import LLMProvider
20
+
21
+ logger = get_logger("cli.investigate")
22
+
23
+
24
+ def _wrap_provider(provider: LLMProvider, llm: Any, config: Any) -> LLMProvider:
25
+ """Wrap a provider with caching and rate limiting based on config."""
26
+ from semantic_code_intelligence.llm.cache import LLMCache
27
+ from semantic_code_intelligence.llm.cached_provider import CachedProvider
28
+ from semantic_code_intelligence.llm.rate_limiter import RateLimiter
29
+
30
+ cache = None
31
+ if getattr(llm, "cache_enabled", False):
32
+ cache_dir = str(config.config_dir(config.project_root)) if hasattr(config, "config_dir") else None
33
+ cache = LLMCache(
34
+ cache_dir=cache_dir,
35
+ ttl_hours=getattr(llm, "cache_ttl_hours", 24),
36
+ max_entries=getattr(llm, "cache_max_entries", 1000),
37
+ )
38
+
39
+ rate_limiter = None
40
+ rpm = getattr(llm, "rate_limit_rpm", 0)
41
+ tpm = getattr(llm, "rate_limit_tpm", 0)
42
+ if rpm > 0 or tpm > 0:
43
+ rate_limiter = RateLimiter(rpm=rpm, tpm=tpm)
44
+
45
+ if cache is not None or rate_limiter is not None:
46
+ return CachedProvider(provider, cache=cache, rate_limiter=rate_limiter)
47
+ return provider
48
+
49
+
50
+ def _get_provider(config: Any) -> LLMProvider:
51
+ """Build an LLM provider from the app configuration."""
52
+ from semantic_code_intelligence.config.settings import LLMConfig
53
+
54
+ llm: LLMConfig = config.llm
55
+ if llm.provider == "openai":
56
+ from semantic_code_intelligence.llm.openai_provider import OpenAIProvider
57
+
58
+ provider: LLMProvider = OpenAIProvider(
59
+ api_key=llm.api_key,
60
+ model=llm.model,
61
+ base_url=llm.base_url or None,
62
+ temperature=llm.temperature,
63
+ max_tokens=llm.max_tokens,
64
+ )
65
+ elif llm.provider == "ollama":
66
+ from semantic_code_intelligence.llm.ollama_provider import OllamaProvider
67
+
68
+ provider = OllamaProvider(
69
+ model=llm.model,
70
+ base_url=llm.base_url or "http://localhost:11434",
71
+ temperature=llm.temperature,
72
+ max_tokens=llm.max_tokens,
73
+ )
74
+ else:
75
+ from semantic_code_intelligence.llm.mock_provider import MockProvider
76
+
77
+ provider = MockProvider()
78
+
79
+ return _wrap_provider(provider, llm, config)
80
+
81
+
82
+ @click.command("investigate")
83
+ @click.argument("question", type=str)
84
+ @click.option(
85
+ "--max-steps", "-n",
86
+ default=6,
87
+ type=int,
88
+ help="Maximum investigation steps before forcing a conclusion.",
89
+ )
90
+ @click.option(
91
+ "--json-output", "--json", "json_mode",
92
+ is_flag=True,
93
+ default=False,
94
+ help="Output in JSON format.",
95
+ )
96
+ @click.option(
97
+ "--path", "-p",
98
+ default=".",
99
+ type=click.Path(exists=True, file_okay=False, resolve_path=True),
100
+ help="Project root path.",
101
+ )
102
+ @click.option(
103
+ "--stream",
104
+ is_flag=True,
105
+ default=False,
106
+ help="Stream the conclusion tokens incrementally.",
107
+ )
108
+ @click.option("--pipe", is_flag=True, default=False, hidden=True)
109
+ @click.pass_context
110
+ def investigate_cmd(
111
+ ctx: click.Context,
112
+ question: str,
113
+ max_steps: int,
114
+ json_mode: bool,
115
+ path: str,
116
+ stream: bool,
117
+ pipe: bool,
118
+ ) -> None:
119
+ """Run an autonomous multi-step investigation to answer a question.
120
+
121
+ CodexA iteratively searches, analyses symbols, and examines dependencies
122
+ until it can confidently answer your question. Each step is visible
123
+ so you can follow the reasoning chain.
124
+ """
125
+ from semantic_code_intelligence.config.settings import load_config
126
+ from semantic_code_intelligence.llm.investigation import InvestigationChain
127
+
128
+ root = Path(path).resolve()
129
+ pipe = pipe or ctx.obj.get("pipe", False)
130
+
131
+ config = load_config(root)
132
+ provider = _get_provider(config)
133
+
134
+ chain = InvestigationChain(provider, root, max_steps=max_steps)
135
+ result = chain.investigate(question, stream_conclusion=stream and not json_mode)
136
+
137
+ if json_mode:
138
+ click.echo(json_mod.dumps(result.to_dict(), indent=2))
139
+ elif pipe:
140
+ for step in result.steps:
141
+ click.echo(f"[{step['step']}] {step['action']}: {step.get('action_input', '')}")
142
+ click.echo(f"\nConclusion: {result.conclusion}")
143
+ else:
144
+ from rich.panel import Panel
145
+ from rich.markdown import Markdown
146
+
147
+ for step in result.steps:
148
+ action = step["action"]
149
+ thought = step.get("thought", "")
150
+ output = step.get("output", "")[:300]
151
+ console.print(
152
+ f" [bold cyan]Step {step['step']}[/] [{action}] "
153
+ f"[dim]{thought}[/]"
154
+ )
155
+ if output and action != "conclude":
156
+ console.print(f" [dim]{output}[/dim]")
157
+
158
+ console.print()
159
+ console.print(Panel(
160
+ Markdown(result.conclusion),
161
+ title=f"Investigation ({result.total_steps} steps)",
162
+ border_style="green",
163
+ ))