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,289 @@
1
+ """REST API server — developer-friendly HTTP endpoints wrapping CodexA services.
2
+
3
+ Uses only the Python standard library (``http.server``). All endpoints
4
+ return JSON and can be selectively enabled/disabled.
5
+
6
+ Endpoints
7
+ ---------
8
+ GET /health → system & index status
9
+ GET /api/search → semantic search (``?q=...&top_k=5``)
10
+ POST /api/ask → natural-language code Q\u0026A
11
+ POST /api/analyze → code explanation / validation
12
+ GET /api/symbols → list indexed symbols (``?file=...&kind=...``)
13
+ GET /api/deps → dependency map (``?file=...``)
14
+ GET /api/callgraph → call graph (``?symbol=...``)
15
+ GET /api/summary → repository summary
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import json
21
+ import time
22
+ import urllib.parse
23
+ from http.server import BaseHTTPRequestHandler
24
+ from pathlib import Path
25
+ from typing import Any
26
+
27
+ from semantic_code_intelligence.bridge.context_provider import ContextProvider
28
+ from semantic_code_intelligence.utils.logging import get_logger
29
+
30
+ logger = get_logger("web.api")
31
+
32
+
33
+ class APIHandler(BaseHTTPRequestHandler):
34
+ """JSON REST API handler for CodexA developer endpoints.
35
+
36
+ Class-level attributes are injected before the server starts:
37
+ project_root — the root path of the project being served
38
+ provider — a ``ContextProvider`` instance
39
+ """
40
+
41
+ project_root: Path
42
+ provider: ContextProvider
43
+
44
+ # Use our own logger instead of stderr.
45
+ def log_message(self, fmt: str, *args: Any) -> None: # noqa: D102
46
+ logger.debug(fmt, *args)
47
+
48
+ # ------------------------------------------------------------------
49
+ # Routing
50
+ # ------------------------------------------------------------------
51
+
52
+ def do_GET(self) -> None: # noqa: N802
53
+ parsed = urllib.parse.urlparse(self.path)
54
+ path = parsed.path.rstrip("/")
55
+ qs = urllib.parse.parse_qs(parsed.query)
56
+
57
+ routes: dict[str, Any] = {
58
+ "/health": self._handle_health,
59
+ "/api/search": self._handle_search,
60
+ "/api/symbols": self._handle_symbols,
61
+ "/api/deps": self._handle_deps,
62
+ "/api/callgraph": self._handle_callgraph,
63
+ "/api/summary": self._handle_summary,
64
+ }
65
+
66
+ handler = routes.get(path)
67
+ if handler:
68
+ handler(qs)
69
+ else:
70
+ self._json(404, {"error": "Not found", "path": self.path})
71
+
72
+ def do_POST(self) -> None: # noqa: N802
73
+ parsed = urllib.parse.urlparse(self.path)
74
+ path = parsed.path.rstrip("/")
75
+
76
+ body = self._read_body()
77
+ if body is None:
78
+ return # error already sent
79
+
80
+ routes: dict[str, Any] = {
81
+ "/api/ask": self._handle_ask,
82
+ "/api/analyze": self._handle_analyze,
83
+ }
84
+
85
+ handler = routes.get(path)
86
+ if handler:
87
+ handler(body)
88
+ else:
89
+ self._json(404, {"error": "Not found", "path": self.path})
90
+
91
+ def do_OPTIONS(self) -> None: # noqa: N802
92
+ self.send_response(204)
93
+ self._cors_headers()
94
+ self.end_headers()
95
+
96
+ # ------------------------------------------------------------------
97
+ # GET handlers
98
+ # ------------------------------------------------------------------
99
+
100
+ def _handle_health(self, qs: dict[str, list[str]]) -> None:
101
+ """GET /health — project status and basic info."""
102
+ codex_dir = self.project_root / ".codexa"
103
+ self._json(200, {
104
+ "status": "ok",
105
+ "project_root": str(self.project_root),
106
+ "indexed": (codex_dir / "index").exists() if codex_dir.exists() else False,
107
+ "config_found": (codex_dir / "config.json").exists() if codex_dir.exists() else False,
108
+ })
109
+
110
+ def _handle_search(self, qs: dict[str, list[str]]) -> None:
111
+ """GET /api/search?q=...&top_k=5&threshold=0.2"""
112
+ query = _qs_first(qs, "q", "")
113
+ if not query:
114
+ self._json(400, {"error": "Missing required parameter: q"})
115
+ return
116
+
117
+ top_k = int(_qs_first(qs, "top_k", "5"))
118
+ threshold = float(_qs_first(qs, "threshold", "0.2"))
119
+
120
+ start = time.monotonic()
121
+ data = self.provider.context_for_query(
122
+ query, top_k=top_k, threshold=threshold,
123
+ )
124
+ elapsed = (time.monotonic() - start) * 1000
125
+ data["elapsed_ms"] = round(elapsed, 2)
126
+ self._json(200, data)
127
+
128
+ def _handle_symbols(self, qs: dict[str, list[str]]) -> None:
129
+ """GET /api/symbols?file=...&kind=..."""
130
+ builder = self.provider._ensure_indexed()
131
+ file_filter = _qs_first(qs, "file", "")
132
+ kind_filter = _qs_first(qs, "kind", "")
133
+
134
+ symbols = builder.get_all_symbols()
135
+ if file_filter:
136
+ symbols = [s for s in symbols if file_filter in s.file_path]
137
+ if kind_filter:
138
+ symbols = [s for s in symbols if s.kind == kind_filter]
139
+
140
+ self._json(200, {
141
+ "count": len(symbols),
142
+ "symbols": [s.to_dict() for s in symbols[:200]], # cap at 200
143
+ })
144
+
145
+ def _handle_deps(self, qs: dict[str, list[str]]) -> None:
146
+ """GET /api/deps?file=..."""
147
+ file_path = _qs_first(qs, "file", "")
148
+ data = self.provider.get_dependencies(file_path=file_path)
149
+ self._json(200, data)
150
+
151
+ def _handle_callgraph(self, qs: dict[str, list[str]]) -> None:
152
+ """GET /api/callgraph?symbol=..."""
153
+ symbol = _qs_first(qs, "symbol", "")
154
+ data = self.provider.get_call_graph(symbol_name=symbol)
155
+ self._json(200, data)
156
+
157
+ def _handle_summary(self, qs: dict[str, list[str]]) -> None:
158
+ """GET /api/summary"""
159
+ data = self.provider.context_for_repo()
160
+ self._json(200, data)
161
+
162
+ # ------------------------------------------------------------------
163
+ # POST handlers
164
+ # ------------------------------------------------------------------
165
+
166
+ def _handle_ask(self, body: dict[str, Any]) -> None:
167
+ """POST /api/ask — natural-language question about the codebase.
168
+
169
+ Request body: ``{"question": "...", "top_k": 5}``
170
+ """
171
+ question = body.get("question", "")
172
+ if not question:
173
+ self._json(400, {"error": "Missing required field: question"})
174
+ return
175
+
176
+ top_k = body.get("top_k", 5)
177
+ start = time.monotonic()
178
+ # Try LLM-powered answer via ReasoningEngine
179
+ try:
180
+ from semantic_code_intelligence.config.settings import load_config, LLMConfig
181
+ from semantic_code_intelligence.llm.reasoning import ReasoningEngine
182
+
183
+ config = load_config(self.provider._root)
184
+ llm_cfg: LLMConfig = config.llm
185
+ provider = None
186
+ if llm_cfg.provider == "openai":
187
+ from semantic_code_intelligence.llm.openai_provider import OpenAIProvider
188
+ provider = OpenAIProvider(
189
+ api_key=llm_cfg.api_key, model=llm_cfg.model,
190
+ base_url=llm_cfg.base_url or None,
191
+ temperature=llm_cfg.temperature, max_tokens=llm_cfg.max_tokens,
192
+ )
193
+ elif llm_cfg.provider == "ollama":
194
+ from semantic_code_intelligence.llm.ollama_provider import OllamaProvider
195
+ provider = OllamaProvider(
196
+ model=llm_cfg.model,
197
+ base_url=llm_cfg.base_url or "http://localhost:11434",
198
+ temperature=llm_cfg.temperature, max_tokens=llm_cfg.max_tokens,
199
+ )
200
+ if provider is not None:
201
+ engine = ReasoningEngine(provider, self.provider._root)
202
+ result = engine.ask(question, top_k=top_k)
203
+ elapsed = (time.monotonic() - start) * 1000
204
+ self._json(200, {
205
+ "question": result.question,
206
+ "answer": result.answer,
207
+ "snippets": result.context_snippets,
208
+ "elapsed_ms": round(elapsed, 2),
209
+ })
210
+ return
211
+ except Exception:
212
+ logger.debug("LLM ask failed, falling back to context search", exc_info=True)
213
+
214
+ # Fallback: return snippets with a synthesized answer
215
+ data = self.provider.context_for_query(question, top_k=top_k)
216
+ snippets = data.get("snippets", [])
217
+ summary_parts = []
218
+ for s in snippets[:3]:
219
+ fp = s.get("file_path", "")
220
+ sl = s.get("start_line", "")
221
+ summary_parts.append(f"- {fp} (line {sl})")
222
+ answer = f"Based on {len(snippets)} relevant code snippets:\n" + "\n".join(summary_parts)
223
+ if not snippets:
224
+ answer = "No relevant code found for this question. Try rephrasing or indexing more files."
225
+ data["answer"] = answer
226
+ elapsed = (time.monotonic() - start) * 1000
227
+ data["elapsed_ms"] = round(elapsed, 2)
228
+ self._json(200, data)
229
+
230
+ def _handle_analyze(self, body: dict[str, Any]) -> None:
231
+ """POST /api/analyze — code validation / explanation.
232
+
233
+ Request body: ``{"code": "...", "mode": "validate|explain"}``
234
+ """
235
+ code = body.get("code", "")
236
+ mode = body.get("mode", "validate")
237
+
238
+ if not code:
239
+ self._json(400, {"error": "Missing required field: code"})
240
+ return
241
+
242
+ if mode == "validate":
243
+ data = self.provider.validate_code(code=code)
244
+ elif mode == "explain":
245
+ # File-level explanation for the provided code snippet
246
+ file_path = body.get("file_path", "snippet.py")
247
+ data = self.provider.context_for_file(file_path=file_path)
248
+ else:
249
+ self._json(400, {"error": f"Unknown mode: {mode}. Use 'validate' or 'explain'."})
250
+ return
251
+
252
+ self._json(200, data)
253
+
254
+ # ------------------------------------------------------------------
255
+ # Helpers
256
+ # ------------------------------------------------------------------
257
+
258
+ def _read_body(self) -> dict[str, Any] | None:
259
+ content_length = int(self.headers.get("Content-Length", 0))
260
+ if content_length == 0:
261
+ self._json(400, {"error": "Empty request body"})
262
+ return None
263
+ raw = self.rfile.read(content_length)
264
+ try:
265
+ parsed: dict[str, Any] | None = json.loads(raw.decode("utf-8"))
266
+ return parsed
267
+ except (json.JSONDecodeError, UnicodeDecodeError) as exc:
268
+ self._json(400, {"error": f"Invalid JSON: {exc}"})
269
+ return None
270
+
271
+ def _json(self, status: int, body: dict[str, Any]) -> None:
272
+ payload = json.dumps(body, indent=2, default=str).encode("utf-8")
273
+ self.send_response(status)
274
+ self.send_header("Content-Type", "application/json; charset=utf-8")
275
+ self.send_header("Content-Length", str(len(payload)))
276
+ self._cors_headers()
277
+ self.end_headers()
278
+ self.wfile.write(payload)
279
+
280
+ def _cors_headers(self) -> None:
281
+ self.send_header("Access-Control-Allow-Origin", "*")
282
+ self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
283
+ self.send_header("Access-Control-Allow-Headers", "Content-Type")
284
+
285
+
286
+ def _qs_first(qs: dict[str, list[str]], key: str, default: str = "") -> str:
287
+ """Return the first value for a query-string key, or *default*."""
288
+ vals = qs.get(key, [])
289
+ return vals[0] if vals else default
@@ -0,0 +1,397 @@
1
+ """Combined web server — serves both the REST API and web UI.
2
+
3
+ Merges API and UI routing into a single HTTP handler so that
4
+ a single ``codexa web`` command provides both the browser interface and the
5
+ developer API on the same port.
6
+
7
+ Uses only the Python standard library.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import threading
14
+ import urllib.parse
15
+ from http.server import HTTPServer, BaseHTTPRequestHandler
16
+ from pathlib import Path
17
+ from typing import Any
18
+
19
+ from semantic_code_intelligence.bridge.context_provider import ContextProvider
20
+ from semantic_code_intelligence.utils.logging import get_logger
21
+ from semantic_code_intelligence.web.api import APIHandler, _qs_first
22
+ from semantic_code_intelligence.web.ui import (
23
+ UIHandler,
24
+ page_search,
25
+ page_symbols,
26
+ page_tools,
27
+ page_quality,
28
+ page_ask,
29
+ page_workspace,
30
+ page_viz,
31
+ _page,
32
+ )
33
+ from semantic_code_intelligence.web.visualize import (
34
+ render_call_graph,
35
+ render_dependency_graph,
36
+ )
37
+
38
+ logger = get_logger("web.server")
39
+
40
+
41
+ class _CombinedHandler(BaseHTTPRequestHandler):
42
+ """Routes API requests to ``APIHandler`` logic and UI requests to page builders.
43
+
44
+ Requests starting with ``/api/`` or ``/health`` are handled by API
45
+ logic; everything else returns server-rendered HTML pages.
46
+ """
47
+
48
+ project_root: Path
49
+ provider: ContextProvider
50
+
51
+ def log_message(self, fmt: str, *args: Any) -> None:
52
+ logger.debug(fmt, *args)
53
+
54
+ # ------------------------------------------------------------------
55
+ # Routing
56
+ # ------------------------------------------------------------------
57
+
58
+ def do_GET(self) -> None: # noqa: N802
59
+ parsed = urllib.parse.urlparse(self.path)
60
+ path = parsed.path.rstrip("/") or "/"
61
+ qs = urllib.parse.parse_qs(parsed.query)
62
+
63
+ # API routes — delegate to APIHandler methods directly
64
+ if path.startswith("/api/") or path == "/health":
65
+ self._dispatch_api_get(path, qs)
66
+ else:
67
+ self._dispatch_ui(path, qs)
68
+
69
+ def do_POST(self) -> None: # noqa: N802
70
+ parsed = urllib.parse.urlparse(self.path)
71
+ path = parsed.path.rstrip("/")
72
+ self._dispatch_api_post(path)
73
+
74
+ def do_OPTIONS(self) -> None: # noqa: N802
75
+ self.send_response(204)
76
+ self._cors_headers()
77
+ self.end_headers()
78
+
79
+ # ------------------------------------------------------------------
80
+ # API dispatch (inline — avoids creating new handler instances)
81
+ # ------------------------------------------------------------------
82
+
83
+ def _dispatch_api_get(self, path: str, qs: dict[str, list[str]]) -> None:
84
+ import time
85
+
86
+ if path == "/health":
87
+ codex_dir = self.project_root / ".codexa"
88
+ self._json(200, {
89
+ "status": "ok",
90
+ "project_root": str(self.project_root),
91
+ "indexed": (codex_dir / "index").exists() if codex_dir.exists() else False,
92
+ "config_found": (codex_dir / "config.json").exists() if codex_dir.exists() else False,
93
+ })
94
+ elif path == "/api/search":
95
+ query = _qs_first(qs, "q", "")
96
+ if not query:
97
+ self._json(400, {"error": "Missing required parameter: q"})
98
+ return
99
+ top_k = int(_qs_first(qs, "top_k", "5"))
100
+ threshold = float(_qs_first(qs, "threshold", "0.2"))
101
+ start = time.monotonic()
102
+ data = self.provider.context_for_query(query, top_k=top_k, threshold=threshold)
103
+ data["elapsed_ms"] = round((time.monotonic() - start) * 1000, 2)
104
+ self._json(200, data)
105
+ elif path == "/api/symbols":
106
+ builder = self.provider._ensure_indexed()
107
+ file_filter = _qs_first(qs, "file", "")
108
+ kind_filter = _qs_first(qs, "kind", "")
109
+ symbols = builder.get_all_symbols()
110
+ if file_filter:
111
+ symbols = [s for s in symbols if file_filter in s.file_path]
112
+ if kind_filter:
113
+ symbols = [s for s in symbols if s.kind == kind_filter]
114
+ self._json(200, {
115
+ "count": len(symbols),
116
+ "symbols": [s.to_dict() for s in symbols[:200]],
117
+ })
118
+ elif path == "/api/deps":
119
+ file_path = _qs_first(qs, "file", "")
120
+ data = self.provider.get_dependencies(file_path=file_path)
121
+ self._json(200, data)
122
+ elif path == "/api/callgraph":
123
+ symbol = _qs_first(qs, "symbol", "")
124
+ data = self.provider.get_call_graph(symbol_name=symbol)
125
+ self._json(200, data)
126
+ elif path == "/api/summary":
127
+ data = self.provider.context_for_repo()
128
+ self._json(200, data)
129
+ elif path == "/api/quality":
130
+ try:
131
+ from semantic_code_intelligence.ci.quality import analyze_project
132
+ report = analyze_project(self.project_root)
133
+ self._json(200, report.to_dict())
134
+ except Exception as exc:
135
+ self._json(500, {"error": str(exc)})
136
+ elif path == "/api/metrics":
137
+ try:
138
+ from semantic_code_intelligence.ci.metrics import compute_project_metrics
139
+ pm = compute_project_metrics(self.project_root)
140
+ self._json(200, pm.to_dict())
141
+ except Exception as exc:
142
+ self._json(500, {"error": str(exc)})
143
+ elif path == "/api/hotspots":
144
+ try:
145
+ from semantic_code_intelligence.ci.hotspots import analyze_hotspots
146
+ from semantic_code_intelligence.context.engine import CallGraph, ContextBuilder, DependencyMap
147
+ builder = ContextBuilder()
148
+ dep_map = DependencyMap()
149
+ root = self.project_root
150
+ py_files = sorted(root.rglob("*.py"))
151
+ py_files = [f for f in py_files if ".venv" not in f.parts and "__pycache__" not in f.parts]
152
+ for fp in py_files:
153
+ try:
154
+ content = fp.read_text(encoding="utf-8", errors="replace")
155
+ builder.index_file(str(fp), content)
156
+ dep_map.add_file(str(fp), content)
157
+ except Exception:
158
+ continue
159
+ symbols = builder.get_all_symbols()
160
+ call_graph = CallGraph()
161
+ call_graph.build(symbols)
162
+ report = analyze_hotspots(symbols, call_graph, dep_map, root)
163
+ self._json(200, report.to_dict())
164
+ except Exception as exc:
165
+ self._json(500, {"error": str(exc)})
166
+ elif path.startswith("/api/viz/"):
167
+ kind = path.replace("/api/viz/", "").strip("/")
168
+ target = _qs_first(qs, "target", "")
169
+ try:
170
+ if kind == "callgraph":
171
+ data = self.provider.get_call_graph(symbol_name=target)
172
+ edges = data.get("edges", [])
173
+ mermaid = render_call_graph(edges)
174
+ elif kind == "deps":
175
+ data = self.provider.get_dependencies(file_path=target)
176
+ mermaid = render_dependency_graph(data)
177
+ else:
178
+ self._json(400, {"error": f"Unknown viz kind: {kind}"})
179
+ return
180
+ self._json(200, {"kind": kind, "mermaid": mermaid})
181
+ except Exception as exc:
182
+ self._json(500, {"error": str(exc)})
183
+ else:
184
+ self._json(404, {"error": "Not found", "path": self.path})
185
+
186
+ def _dispatch_api_post(self, path: str) -> None:
187
+ import time
188
+
189
+ content_length = int(self.headers.get("Content-Length", 0))
190
+ if content_length == 0:
191
+ self._json(400, {"error": "Empty request body"})
192
+ return
193
+ raw = self.rfile.read(content_length)
194
+ try:
195
+ body = json.loads(raw.decode("utf-8"))
196
+ except (json.JSONDecodeError, UnicodeDecodeError) as exc:
197
+ self._json(400, {"error": f"Invalid JSON: {exc}"})
198
+ return
199
+
200
+ if path == "/api/ask":
201
+ question = body.get("question", "")
202
+ if not question:
203
+ self._json(400, {"error": "Missing required field: question"})
204
+ return
205
+ top_k = body.get("top_k", 5)
206
+ start = time.monotonic()
207
+ try:
208
+ from semantic_code_intelligence.config.settings import load_config, LLMConfig
209
+ from semantic_code_intelligence.llm.reasoning import ReasoningEngine
210
+ config = load_config(self.project_root)
211
+ llm_cfg: LLMConfig = config.llm
212
+ if llm_cfg.provider == "openai":
213
+ from semantic_code_intelligence.llm.openai_provider import OpenAIProvider
214
+ provider = OpenAIProvider(
215
+ api_key=llm_cfg.api_key, model=llm_cfg.model,
216
+ base_url=llm_cfg.base_url or None,
217
+ temperature=llm_cfg.temperature, max_tokens=llm_cfg.max_tokens,
218
+ )
219
+ elif llm_cfg.provider == "ollama":
220
+ from semantic_code_intelligence.llm.ollama_provider import OllamaProvider
221
+ provider = OllamaProvider(
222
+ model=llm_cfg.model,
223
+ base_url=llm_cfg.base_url or "http://localhost:11434",
224
+ temperature=llm_cfg.temperature, max_tokens=llm_cfg.max_tokens,
225
+ )
226
+ else:
227
+ provider = None # type: ignore[assignment]
228
+ if provider is not None:
229
+ engine = ReasoningEngine(provider, self.project_root)
230
+ result = engine.ask(question, top_k=top_k)
231
+ elapsed = (time.monotonic() - start) * 1000
232
+ self._json(200, {
233
+ "question": result.question,
234
+ "answer": result.answer,
235
+ "snippets": result.context_snippets,
236
+ "elapsed_ms": round(elapsed, 2),
237
+ })
238
+ return
239
+ except Exception:
240
+ logger.debug("LLM ask failed, falling back to context search", exc_info=True)
241
+ # Fallback: return snippets with a synthesized answer
242
+ data = self.provider.context_for_query(question, top_k=top_k)
243
+ snippets = data.get("snippets", [])
244
+ summary_parts = []
245
+ for s in snippets[:3]:
246
+ fp = s.get("file_path", "")
247
+ sl = s.get("start_line", "")
248
+ summary_parts.append(f"- {fp} (line {sl})")
249
+ answer = f"Based on {len(snippets)} relevant code snippets:\n" + "\n".join(summary_parts)
250
+ if not snippets:
251
+ answer = "No relevant code found for this question. Try rephrasing or indexing more files."
252
+ data["answer"] = answer
253
+ data["elapsed_ms"] = round((time.monotonic() - start) * 1000, 2)
254
+ self._json(200, data)
255
+ elif path == "/api/analyze":
256
+ code = body.get("code", "")
257
+ mode = body.get("mode", "validate")
258
+ if not code:
259
+ self._json(400, {"error": "Missing required field: code"})
260
+ return
261
+ if mode == "validate":
262
+ data = self.provider.validate_code(code=code)
263
+ elif mode == "explain":
264
+ file_path = body.get("file_path", "snippet.py")
265
+ data = self.provider.context_for_file(file_path=file_path)
266
+ else:
267
+ self._json(400, {"error": f"Unknown mode: {mode}"})
268
+ return
269
+ self._json(200, data)
270
+ elif path == "/api/tools/run":
271
+ tool_name = body.get("tool_name", "")
272
+ arguments = body.get("arguments", {})
273
+ if not tool_name:
274
+ self._json(400, {"error": "Missing required field: tool_name"})
275
+ return
276
+ try:
277
+ from semantic_code_intelligence.tools.executor import ToolExecutor
278
+ from semantic_code_intelligence.tools.protocol import ToolInvocation
279
+ executor = ToolExecutor(self.project_root)
280
+ invocation = ToolInvocation(tool_name=tool_name, arguments=arguments)
281
+ result = executor.execute(invocation)
282
+ self._json(200, result.to_dict())
283
+ except Exception as exc:
284
+ self._json(500, {"error": str(exc)})
285
+ else:
286
+ self._json(404, {"error": "Not found"})
287
+
288
+ # ------------------------------------------------------------------
289
+ # UI dispatch
290
+ # ------------------------------------------------------------------
291
+
292
+ def _dispatch_ui(self, path: str, qs: dict[str, list[str]]) -> None:
293
+ pages: dict[str, str] = {
294
+ "/": page_search(),
295
+ "/symbols": page_symbols(),
296
+ "/tools": page_tools(),
297
+ "/quality": page_quality(),
298
+ "/ask": page_ask(),
299
+ "/workspace": page_workspace(),
300
+ "/viz": page_viz(),
301
+ }
302
+ content = pages.get(path)
303
+ if content:
304
+ self._html(200, content)
305
+ else:
306
+ self._html(404, _page("Not Found", '<div class="empty">Page not found.</div>'))
307
+
308
+ # ------------------------------------------------------------------
309
+ # Response helpers
310
+ # ------------------------------------------------------------------
311
+
312
+ def _json(self, status: int, body: dict[str, Any]) -> None:
313
+ payload = json.dumps(body, indent=2, default=str).encode("utf-8")
314
+ self.send_response(status)
315
+ self.send_header("Content-Type", "application/json; charset=utf-8")
316
+ self.send_header("Content-Length", str(len(payload)))
317
+ self._cors_headers()
318
+ self.end_headers()
319
+ self.wfile.write(payload)
320
+
321
+ def _html(self, status: int, content: str) -> None:
322
+ payload = content.encode("utf-8")
323
+ self.send_response(status)
324
+ self.send_header("Content-Type", "text/html; charset=utf-8")
325
+ self.send_header("Content-Length", str(len(payload)))
326
+ self.end_headers()
327
+ self.wfile.write(payload)
328
+
329
+ def _cors_headers(self) -> None:
330
+ self.send_header("Access-Control-Allow-Origin", "*")
331
+ self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
332
+ self.send_header("Access-Control-Allow-Headers", "Content-Type")
333
+
334
+
335
+ class WebServer:
336
+ """Combined web server serving API and UI on a single port.
337
+
338
+ Usage::
339
+
340
+ server = WebServer(Path("."), host="127.0.0.1", port=8080)
341
+ server.start() # blocks
342
+ # or
343
+ server.start_background()
344
+ server.stop()
345
+ """
346
+
347
+ DEFAULT_PORT = 8080
348
+
349
+ def __init__(
350
+ self,
351
+ project_root: Path,
352
+ host: str = "127.0.0.1",
353
+ port: int = DEFAULT_PORT,
354
+ ) -> None:
355
+ self._root = project_root.resolve()
356
+ self._host = host
357
+ self._port = port
358
+ self._httpd: HTTPServer | None = None
359
+ self._thread: threading.Thread | None = None
360
+ self._provider = ContextProvider(self._root)
361
+
362
+ @property
363
+ def url(self) -> str:
364
+ return f"http://{self._host}:{self._port}"
365
+
366
+ def _make_server(self) -> HTTPServer:
367
+ _CombinedHandler.project_root = self._root
368
+ _CombinedHandler.provider = self._provider
369
+ return HTTPServer((self._host, self._port), _CombinedHandler)
370
+
371
+ def start(self) -> None:
372
+ """Start the server (blocking)."""
373
+ self._httpd = self._make_server()
374
+ logger.info("CodexA web server listening on %s", self.url)
375
+ try:
376
+ self._httpd.serve_forever()
377
+ except KeyboardInterrupt:
378
+ pass
379
+ finally:
380
+ self._httpd.server_close()
381
+
382
+ def start_background(self) -> None:
383
+ """Start in a background daemon thread."""
384
+ self._httpd = self._make_server()
385
+ self._thread = threading.Thread(target=self._httpd.serve_forever, daemon=True)
386
+ self._thread.start()
387
+ logger.info("CodexA web server started in background on %s", self.url)
388
+
389
+ def stop(self) -> None:
390
+ """Shut down the server."""
391
+ if self._httpd:
392
+ self._httpd.shutdown()
393
+ self._httpd.server_close()
394
+ self._httpd = None
395
+ if self._thread:
396
+ self._thread.join(timeout=5)
397
+ self._thread = None