codegraph-cli 2.1.1__py3-none-any.whl → 2.1.2__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 (42) hide show
  1. codegraph_cli/__init__.py +1 -1
  2. codegraph_cli/agents.py +59 -3
  3. codegraph_cli/chat_agent.py +58 -11
  4. codegraph_cli/cli.py +569 -54
  5. codegraph_cli/cli_chat.py +200 -95
  6. codegraph_cli/cli_diagnose.py +13 -2
  7. codegraph_cli/cli_docs.py +207 -0
  8. codegraph_cli/cli_explore.py +1053 -0
  9. codegraph_cli/cli_export.py +941 -0
  10. codegraph_cli/cli_groups.py +33 -0
  11. codegraph_cli/cli_health.py +316 -0
  12. codegraph_cli/cli_history.py +213 -0
  13. codegraph_cli/cli_onboard.py +380 -0
  14. codegraph_cli/cli_quickstart.py +256 -0
  15. codegraph_cli/cli_refactor.py +17 -3
  16. codegraph_cli/cli_setup.py +12 -12
  17. codegraph_cli/cli_suggestions.py +90 -0
  18. codegraph_cli/cli_test.py +17 -3
  19. codegraph_cli/cli_tui.py +210 -0
  20. codegraph_cli/cli_v2.py +24 -4
  21. codegraph_cli/cli_watch.py +158 -0
  22. codegraph_cli/cli_workflows.py +255 -0
  23. codegraph_cli/codegen_agent.py +15 -1
  24. codegraph_cli/config.py +18 -5
  25. codegraph_cli/context_manager.py +117 -15
  26. codegraph_cli/crew_agents.py +26 -7
  27. codegraph_cli/crew_chat.py +141 -12
  28. codegraph_cli/crew_tools.py +21 -1
  29. codegraph_cli/embeddings.py +95 -5
  30. codegraph_cli/llm.py +42 -55
  31. codegraph_cli/project_context.py +64 -1
  32. codegraph_cli/rag.py +282 -19
  33. codegraph_cli/storage.py +310 -14
  34. codegraph_cli/vector_store.py +110 -8
  35. {codegraph_cli-2.1.1.dist-info → codegraph_cli-2.1.2.dist-info}/METADATA +35 -24
  36. codegraph_cli-2.1.2.dist-info/RECORD +55 -0
  37. codegraph_cli-2.1.2.dist-info/entry_points.txt +2 -0
  38. codegraph_cli-2.1.1.dist-info/RECORD +0 -43
  39. codegraph_cli-2.1.1.dist-info/entry_points.txt +0 -2
  40. {codegraph_cli-2.1.1.dist-info → codegraph_cli-2.1.2.dist-info}/WHEEL +0 -0
  41. {codegraph_cli-2.1.1.dist-info → codegraph_cli-2.1.2.dist-info}/licenses/LICENSE +0 -0
  42. {codegraph_cli-2.1.1.dist-info → codegraph_cli-2.1.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1053 @@
1
+ """Visual Code Explorer — browser-based UI for navigating indexed projects.
2
+
3
+ Launches a local web server serving a self-contained HTML page with:
4
+ - Directory tree sidebar with expandable folders
5
+ - File analysis: AI explanations, dependencies, syntax-highlighted code
6
+ - Mermaid diagrams (file-level and system-level)
7
+ - Export features (Mermaid, Excalidraw link, HTML)
8
+
9
+ Uses Starlette + Uvicorn (already installed) — zero extra dependencies.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import logging
16
+ import urllib.parse
17
+ from pathlib import Path
18
+ from typing import Any, Dict, List, Optional
19
+
20
+ import typer
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+ explore_app = typer.Typer(
25
+ help="🌐 Visual code explorer in your browser.",
26
+ no_args_is_help=True,
27
+ rich_markup_mode="rich",
28
+ add_completion=False,
29
+ )
30
+
31
+
32
+ # ===================================================================
33
+ # Backend helpers — build data from GraphStore
34
+ # ===================================================================
35
+
36
+
37
+ def _build_api_tree(store: Any) -> Dict:
38
+ """Build JSON directory tree from indexed nodes."""
39
+ nodes_by_file = store.all_by_file()
40
+ root: Dict[str, Any] = {"name": "root", "type": "dir", "path": "", "children": []}
41
+ dir_cache: Dict[str, Dict] = {"": root}
42
+
43
+ all_paths: set[str] = set()
44
+ for fp in nodes_by_file:
45
+ parts = Path(fp).parts
46
+ for i in range(len(parts) - 1):
47
+ all_paths.add(str(Path(*parts[: i + 1])))
48
+ all_paths.add(fp)
49
+
50
+ # Create directory entries
51
+ for p in sorted(all_paths):
52
+ pp = Path(p)
53
+ parent_key = str(pp.parent) if str(pp.parent) != "." else ""
54
+ if p in nodes_by_file:
55
+ # It's a file
56
+ entry = {"name": pp.name, "type": "file", "path": p, "children": []}
57
+ else:
58
+ entry = {"name": pp.name, "type": "dir", "path": p, "children": []}
59
+ dir_cache[p] = entry
60
+
61
+ parent = dir_cache.get(parent_key, root)
62
+ parent["children"].append(entry)
63
+
64
+ return root
65
+
66
+
67
+ def _analyze_file(store: Any, file_path: str) -> Dict:
68
+ """Deep analysis of a single file: code, deps, dependents, symbols."""
69
+ nodes_by_file = store.all_by_file()
70
+ file_nodes = nodes_by_file.get(file_path, [])
71
+
72
+ # Read source code from the first node or reconstruct
73
+ content_lines: list[str] = []
74
+ functions: list[dict] = []
75
+ classes: list[dict] = []
76
+
77
+ for node in sorted(file_nodes, key=lambda n: n.get("start_line", 0)):
78
+ ntype = node.get("node_type", "")
79
+ name = node.get("name", "")
80
+ qualname = node.get("qualname", name)
81
+ start = node.get("start_line", 0)
82
+ end = node.get("end_line", 0)
83
+ code = node.get("code", "")
84
+
85
+ if ntype == "function":
86
+ functions.append({"name": name, "qualname": qualname, "line": start, "end_line": end})
87
+ elif ntype == "class":
88
+ classes.append({"name": name, "qualname": qualname, "line": start, "end_line": end})
89
+
90
+ if code:
91
+ content_lines.append(code)
92
+
93
+ # Build full content — try reading the actual file first
94
+ content = ""
95
+ metadata = store.get_metadata()
96
+ source_root = metadata.get("source_path", "")
97
+ if source_root:
98
+ actual_file = Path(source_root) / file_path
99
+ if actual_file.exists():
100
+ try:
101
+ content = actual_file.read_text(encoding="utf-8", errors="replace")
102
+ except Exception:
103
+ content = "\n\n".join(content_lines)
104
+ if not content:
105
+ content = "\n\n".join(content_lines)
106
+
107
+ # Dependencies — symbols this file calls (outgoing edges)
108
+ deps: list[str] = []
109
+ dependents: list[str] = []
110
+ node_ids = {n.get("node_id") for n in file_nodes}
111
+ for nid in node_ids:
112
+ for edge in store.neighbors(nid):
113
+ dst = edge["dst"] if isinstance(edge, dict) else edge[1]
114
+ dst_node = store.get_node(dst)
115
+ if dst_node:
116
+ deps.append(dst_node["qualname"])
117
+ for edge in store.reverse_neighbors(nid):
118
+ src = edge["src"] if isinstance(edge, dict) else edge[0]
119
+ src_node = store.get_node(src)
120
+ if src_node:
121
+ dependents.append(src_node["qualname"])
122
+
123
+ deps = sorted(set(deps))
124
+ dependents = sorted(set(dependents))
125
+
126
+ # Generate Mermaid diagram for this file
127
+ mermaid = _generate_file_mermaid(store, file_path, file_nodes)
128
+
129
+ return {
130
+ "name": Path(file_path).name,
131
+ "path": file_path,
132
+ "content": content,
133
+ "explanation": None, # filled by /api/explain
134
+ "deps": deps,
135
+ "dependents": dependents,
136
+ "mermaid": mermaid,
137
+ "functions": functions,
138
+ "classes": classes,
139
+ }
140
+
141
+
142
+ def _generate_file_mermaid(store: Any, file_path: str, file_nodes: list) -> Optional[str]:
143
+ """Generate a Mermaid diagram for a single file's internal structure."""
144
+ if not file_nodes:
145
+ return None
146
+
147
+ lines = ["graph TD"]
148
+ node_ids_in_file = set()
149
+ id_to_label: dict[str, str] = {}
150
+
151
+ for node in file_nodes:
152
+ nid = node.get("node_id", "")
153
+ name = node.get("name", "unknown")
154
+ ntype = node.get("node_type", "")
155
+ safe_id = nid.replace(".", "_").replace("/", "_").replace("-", "_").replace(":", "_")
156
+ node_ids_in_file.add(nid)
157
+ id_to_label[nid] = safe_id
158
+
159
+ if ntype == "class":
160
+ lines.append(f' {safe_id}["{name} (class)"]')
161
+ elif ntype == "function":
162
+ lines.append(f' {safe_id}("{name}()")')
163
+ else:
164
+ lines.append(f' {safe_id}["{name}"]')
165
+
166
+ # Add edges between nodes in this file
167
+ edge_count = 0
168
+ for nid in node_ids_in_file:
169
+ for edge in store.neighbors(nid):
170
+ dst = edge["dst"] if isinstance(edge, dict) else edge[1]
171
+ if dst in node_ids_in_file and edge_count < 50:
172
+ src_safe = id_to_label.get(nid, "")
173
+ dst_safe = id_to_label.get(dst, "")
174
+ if src_safe and dst_safe:
175
+ lines.append(f" {src_safe} --> {dst_safe}")
176
+ edge_count += 1
177
+
178
+ if len(lines) <= 1:
179
+ return None
180
+ return "\n".join(lines)
181
+
182
+
183
+ def _generate_system_mermaid(store: Any) -> str:
184
+ """Generate a system-level architecture Mermaid diagram (file-to-file deps)."""
185
+ nodes_by_file = store.all_by_file()
186
+ edges = store.get_edges()
187
+
188
+ # Map node_id -> file_path
189
+ nid_to_file: dict[str, str] = {}
190
+ for fp, nodes in nodes_by_file.items():
191
+ for n in nodes:
192
+ nid_to_file[n.get("node_id", "")] = fp
193
+
194
+ # Build file-level edges
195
+ file_edges: set[tuple[str, str]] = set()
196
+ for edge in edges:
197
+ src = edge["src"] if isinstance(edge, dict) else edge[0]
198
+ dst = edge["dst"] if isinstance(edge, dict) else edge[1]
199
+ src_file = nid_to_file.get(src, "")
200
+ dst_file = nid_to_file.get(dst, "")
201
+ if src_file and dst_file and src_file != dst_file:
202
+ file_edges.add((src_file, dst_file))
203
+
204
+ lines = ["graph LR"]
205
+ file_ids: dict[str, str] = {}
206
+ for fp in sorted(nodes_by_file.keys()):
207
+ safe = fp.replace("/", "_").replace(".", "_").replace("-", "_").replace(" ", "_")
208
+ file_ids[fp] = safe
209
+ short = Path(fp).name
210
+ lines.append(f' {safe}["{short}"]')
211
+
212
+ for src_f, dst_f in sorted(file_edges):
213
+ src_id = file_ids.get(src_f)
214
+ dst_id = file_ids.get(dst_f)
215
+ if src_id and dst_id:
216
+ lines.append(f" {src_id} --> {dst_id}")
217
+
218
+ return "\n".join(lines)
219
+
220
+
221
+ def _explain_file(store: Any, file_path: str, llm_provider: str, llm_model: str, llm_api_key: str) -> Optional[str]:
222
+ """Use LLM to generate a plain-English explanation of a file."""
223
+ try:
224
+ from .llm import LocalLLM
225
+ llm = LocalLLM(model=llm_model, provider=llm_provider, api_key=llm_api_key)
226
+
227
+ # Get file content
228
+ metadata = store.get_metadata()
229
+ source_root = metadata.get("source_path", "")
230
+ content = ""
231
+ if source_root:
232
+ actual = Path(source_root) / file_path
233
+ if actual.exists():
234
+ content = actual.read_text(encoding="utf-8", errors="replace")[:4000]
235
+
236
+ if not content:
237
+ nodes = store.all_by_file().get(file_path, [])
238
+ content = "\n".join(n.get("code", "")[:500] for n in nodes[:5])
239
+
240
+ if not content:
241
+ return None
242
+
243
+ prompt = (
244
+ f"Explain what this file does in 2-3 concise sentences. "
245
+ f"Focus on its purpose and key functionality.\n\n"
246
+ f"File: {file_path}\n```\n{content[:3000]}\n```"
247
+ )
248
+ return llm.explain(prompt)
249
+ except Exception as e:
250
+ logger.warning("LLM explanation failed: %s", e)
251
+ return None
252
+
253
+
254
+ # ===================================================================
255
+ # HTML Template — complete self-contained SPA
256
+ # ===================================================================
257
+
258
+ HTML_TEMPLATE = r"""<!DOCTYPE html>
259
+ <html lang="en" class="dark">
260
+ <head>
261
+ <meta charset="UTF-8">
262
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
263
+ <title>CodeGraph Explorer</title>
264
+ <script src="https://cdn.tailwindcss.com"></script>
265
+ <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
266
+ <script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
267
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
268
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
269
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/python.min.js"></script>
270
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/javascript.min.js"></script>
271
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/typescript.min.js"></script>
272
+ <script src="https://cdn.jsdelivr.net/npm/pako@2.1.0/dist/pako.min.js"></script>
273
+ <script>
274
+ tailwind.config = {
275
+ darkMode: 'class',
276
+ theme: { extend: { colors: { 'cg-bg': '#0d1117', 'cg-sidebar': '#161b22', 'cg-card': '#1c2128', 'cg-border': '#30363d', 'cg-accent': '#58a6ff', 'cg-green': '#3fb950', 'cg-purple': '#bc8cff', 'cg-orange': '#d29922' } } }
277
+ }
278
+ </script>
279
+ <style>
280
+ html, body { margin:0; padding:0; height:100%; overflow:hidden; background:#0d1117; color:#c9d1d9; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif; }
281
+ ::-webkit-scrollbar { width: 8px; height: 8px; }
282
+ ::-webkit-scrollbar-track { background: #161b22; }
283
+ ::-webkit-scrollbar-thumb { background: #30363d; border-radius: 4px; }
284
+ ::-webkit-scrollbar-thumb:hover { background: #484f58; }
285
+ .tree-item { transition: background 0.1s; }
286
+ .tree-item:hover { background: #1c2128; }
287
+ .tree-item.active { background: #1c2128; border-right: 2px solid #58a6ff; }
288
+ .fade-in { animation: fadeIn 0.2s ease-in; }
289
+ @keyframes fadeIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }
290
+ .slide-down { overflow: hidden; transition: max-height 0.25s ease-out; }
291
+ .toast { animation: toastIn 0.3s ease-out, toastOut 0.3s ease-in 1.7s; }
292
+ @keyframes toastIn { from { opacity:0; transform:translateY(10px); } to { opacity:1; transform:translateY(0); } }
293
+ @keyframes toastOut { from { opacity:1; } to { opacity:0; } }
294
+ pre code.hljs { background: transparent !important; padding: 0 !important; }
295
+ .mermaid svg { max-width: 100%; }
296
+ </style>
297
+ </head>
298
+ <body x-data="explorer()" x-init="init()" @keydown.escape.window="selectedFile = null">
299
+
300
+ <!-- Toast -->
301
+ <div x-show="toast" x-transition x-text="toast" class="fixed bottom-6 right-6 z-50 bg-cg-green text-black px-4 py-2 rounded-lg font-medium shadow-lg toast"></div>
302
+
303
+ <!-- Export Modal -->
304
+ <div x-show="showExportModal" x-transition class="fixed inset-0 z-40 flex items-center justify-center bg-black/60" @click.self="showExportModal = false" @keydown.escape.window="showExportModal = false">
305
+ <div class="bg-cg-sidebar border border-cg-border rounded-xl shadow-2xl w-[480px] max-w-[95vw] p-6 fade-in">
306
+ <div class="flex items-center justify-between mb-5">
307
+ <h2 class="text-lg font-bold text-white">📄 Export to DOCX</h2>
308
+ <button @click="showExportModal = false" class="text-gray-500 hover:text-white text-xl">&times;</button>
309
+ </div>
310
+
311
+ <div class="space-y-4">
312
+ <!-- Include Code -->
313
+ <label class="flex items-center gap-3 cursor-pointer">
314
+ <input type="checkbox" x-model="exportOpts.includeCode" class="rounded border-cg-border bg-cg-bg text-cg-accent focus:ring-cg-accent">
315
+ <div>
316
+ <div class="text-sm text-gray-200">Include source code</div>
317
+ <div class="text-xs text-gray-500">Adds full file content (larger file)</div>
318
+ </div>
319
+ </label>
320
+
321
+ <!-- Include Diagram -->
322
+ <label class="flex items-center gap-3 cursor-pointer">
323
+ <input type="checkbox" x-model="exportOpts.includeDiagram" class="rounded border-cg-border bg-cg-bg text-cg-accent focus:ring-cg-accent">
324
+ <div>
325
+ <div class="text-sm text-gray-200">Include architecture diagram</div>
326
+ <div class="text-xs text-gray-500">Embeds system diagram as image</div>
327
+ </div>
328
+ </label>
329
+
330
+ <!-- Enhanced (LLM) -->
331
+ <label class="flex items-center gap-3 cursor-pointer" :class="!exportStatus.llm_available && 'opacity-50'">
332
+ <input type="checkbox" x-model="exportOpts.enhanced" :disabled="!exportStatus.llm_available" class="rounded border-cg-border bg-cg-bg text-cg-purple focus:ring-cg-purple">
333
+ <div>
334
+ <div class="text-sm text-gray-200">AI-enhanced explanations</div>
335
+ <div class="text-xs text-gray-500" x-text="exportStatus.llm_available ? 'Using ' + (exportStatus.provider || '') + '/' + (exportStatus.model || '') : 'No LLM configured — run cg config setup'"></div>
336
+ </div>
337
+ </label>
338
+
339
+ <!-- Depth (only if enhanced) -->
340
+ <div x-show="exportOpts.enhanced && exportStatus.llm_available" x-transition class="pl-8">
341
+ <div class="text-sm text-gray-300 mb-2">Explanation depth:</div>
342
+ <div class="flex gap-2">
343
+ <template x-for="d in ['overview', 'modules', 'files']" :key="d">
344
+ <button @click="exportOpts.depth = d"
345
+ :class="exportOpts.depth === d ? 'bg-cg-accent text-black' : 'bg-cg-bg text-gray-400 border border-cg-border'"
346
+ class="px-3 py-1.5 text-xs rounded transition capitalize" x-text="d"></button>
347
+ </template>
348
+ </div>
349
+ <div class="text-xs text-gray-600 mt-1" x-text="exportOpts.depth === 'overview' ? 'Project-level summary only' : exportOpts.depth === 'modules' ? 'Per-module summaries' : 'Per-file explanations (slowest)'"></div>
350
+ </div>
351
+ </div>
352
+
353
+ <!-- Actions -->
354
+ <div class="mt-6 flex items-center justify-between">
355
+ <button @click="showExportModal = false" class="px-4 py-2 text-sm text-gray-400 hover:text-white transition">Cancel</button>
356
+ <button @click="startExport()" :disabled="exporting" class="px-5 py-2 text-sm bg-cg-accent text-black font-medium rounded-lg hover:bg-blue-400 transition disabled:opacity-50 flex items-center gap-2">
357
+ <span x-show="exporting" class="animate-spin">⏳</span>
358
+ <span x-text="exporting ? 'Exporting...' : '📄 Export DOCX'"></span>
359
+ </button>
360
+ </div>
361
+ </div>
362
+ </div>
363
+
364
+ <div class="flex h-screen">
365
+
366
+ <!-- Sidebar -->
367
+ <div class="w-[300px] min-w-[300px] bg-cg-sidebar border-r border-cg-border flex flex-col">
368
+ <!-- Header -->
369
+ <div class="p-4 border-b border-cg-border">
370
+ <div class="flex items-center gap-2 mb-2">
371
+ <span class="text-xl">🧠</span>
372
+ <h1 class="text-sm font-bold text-white">CodeGraph Explorer</h1>
373
+ </div>
374
+ <div class="text-xs text-gray-500" x-text="projectName"></div>
375
+ <div class="text-xs text-gray-600" x-text="projectStats"></div>
376
+ </div>
377
+
378
+ <!-- Search -->
379
+ <div class="p-3 border-b border-cg-border">
380
+ <input type="text" x-model="treeFilter" placeholder="Filter files..."
381
+ class="w-full bg-cg-bg border border-cg-border rounded px-3 py-1.5 text-sm text-gray-300 placeholder-gray-600 focus:border-cg-accent focus:outline-none">
382
+ </div>
383
+
384
+ <!-- File Tree -->
385
+ <div class="flex-1 overflow-y-auto py-2">
386
+ <template x-if="tree && tree.children">
387
+ <div>
388
+ <template x-for="child in filteredTree(tree.children)" :key="child.path">
389
+ <div x-data="{ open: false }">
390
+ <div @click="child.type === 'dir' ? open = !open : loadFile(child.path)"
391
+ :class="{'active': selectedFile === child.path}"
392
+ class="tree-item flex items-center gap-1.5 px-3 py-1 cursor-pointer text-sm select-none">
393
+ <span class="w-4 text-center text-xs text-gray-500" x-show="child.type === 'dir'" x-text="open ? '▾' : '▸'"></span>
394
+ <span class="w-4 text-center" x-show="child.type !== 'dir'"></span>
395
+ <span x-text="child.type === 'dir' ? '📁' : fileIcon(child.name)" class="text-sm"></span>
396
+ <span class="truncate" :class="child.type === 'dir' ? 'text-gray-300 font-medium' : 'text-gray-400'" x-text="child.name"></span>
397
+ </div>
398
+ <!-- Children -->
399
+ <div x-show="open && child.type === 'dir'" x-transition class="pl-4">
400
+ <template x-for="sub in filteredTree(child.children || [])" :key="sub.path">
401
+ <div x-data="{ subOpen: false }">
402
+ <div @click="sub.type === 'dir' ? subOpen = !subOpen : loadFile(sub.path)"
403
+ :class="{'active': selectedFile === sub.path}"
404
+ class="tree-item flex items-center gap-1.5 px-3 py-1 cursor-pointer text-sm select-none">
405
+ <span class="w-4 text-center text-xs text-gray-500" x-show="sub.type === 'dir'" x-text="subOpen ? '▾' : '▸'"></span>
406
+ <span class="w-4 text-center" x-show="sub.type !== 'dir'"></span>
407
+ <span x-text="sub.type === 'dir' ? '📁' : fileIcon(sub.name)" class="text-sm"></span>
408
+ <span class="truncate" :class="sub.type === 'dir' ? 'text-gray-300 font-medium' : 'text-gray-400'" x-text="sub.name"></span>
409
+ </div>
410
+ <div x-show="subOpen && sub.type === 'dir'" x-transition class="pl-4">
411
+ <template x-for="deep in filteredTree(sub.children || [])" :key="deep.path">
412
+ <div @click="deep.type === 'dir' ? null : loadFile(deep.path)"
413
+ :class="{'active': selectedFile === deep.path}"
414
+ class="tree-item flex items-center gap-1.5 px-3 py-1 cursor-pointer text-sm select-none">
415
+ <span class="w-4 text-center" x-show="deep.type === 'dir'">📁</span>
416
+ <span class="w-4 text-center" x-show="deep.type !== 'dir'"></span>
417
+ <span x-text="deep.type === 'dir' ? '📁' : fileIcon(deep.name)" class="text-sm"></span>
418
+ <span class="truncate text-gray-400" x-text="deep.name"></span>
419
+ </div>
420
+ </template>
421
+ </div>
422
+ </div>
423
+ </template>
424
+ </div>
425
+ </div>
426
+ </template>
427
+ </div>
428
+ </template>
429
+ <div x-show="loading" class="p-4 text-center text-gray-500 text-sm">Loading tree...</div>
430
+ </div>
431
+
432
+ <!-- System Diagram Button -->
433
+ <div class="p-3 border-t border-cg-border space-y-2">
434
+ <button @click="loadSystemDiagram()" class="w-full bg-cg-card hover:bg-gray-700 border border-cg-border text-sm text-gray-300 rounded py-2 transition">
435
+ 🗺️ System Architecture
436
+ </button>
437
+ <button @click="showExportModal = true; loadExportStatus()" class="w-full bg-blue-900/30 hover:bg-blue-900/50 border border-blue-800/40 text-sm text-cg-accent rounded py-2 transition">
438
+ 📄 Export to DOCX
439
+ </button>
440
+ </div>
441
+ </div>
442
+
443
+ <!-- Main Panel -->
444
+ <div class="flex-1 flex flex-col overflow-hidden">
445
+
446
+ <!-- Top Bar -->
447
+ <div class="flex items-center justify-between px-5 py-3 border-b border-cg-border bg-cg-sidebar">
448
+ <div class="flex items-center gap-3">
449
+ <span class="text-sm text-gray-400" x-text="selectedFile || 'Select a file to explore'"></span>
450
+ <span x-show="fileLoading" class="text-xs text-gray-500">⏳ Loading...</span>
451
+ </div>
452
+ <div class="flex items-center gap-2" x-show="fileData">
453
+ <button @click="triggerExplain()" class="px-3 py-1 text-xs bg-purple-900/40 text-cg-purple border border-purple-800/50 rounded hover:bg-purple-900/60 transition" title="AI Explain">
454
+ 🤖 Explain
455
+ </button>
456
+ <button @click="exportMermaid()" x-show="fileData && fileData.mermaid" class="px-3 py-1 text-xs bg-cg-card text-gray-300 border border-cg-border rounded hover:bg-gray-700 transition">
457
+ 📋 Copy Mermaid
458
+ </button>
459
+ <button @click="openMermaidLive(fileData?.mermaid)" x-show="fileData && fileData.mermaid" class="px-3 py-1 text-xs bg-cg-card text-gray-300 border border-cg-border rounded hover:bg-gray-700 transition" title="Open in Mermaid Live Editor">
460
+ 🔗 Mermaid Live
461
+ </button>
462
+ <button @click="exportDiagramSVG('fileMermaid', fileData?.name || 'diagram')" x-show="fileData && fileData.mermaid" class="px-3 py-1 text-xs bg-cg-card text-gray-300 border border-cg-border rounded hover:bg-gray-700 transition" title="Download as SVG">
463
+ 🖼️ SVG
464
+ </button>
465
+ </div>
466
+ </div>
467
+
468
+ <!-- Content -->
469
+ <div class="flex-1 overflow-y-auto p-6 space-y-6">
470
+
471
+ <!-- Welcome -->
472
+ <div x-show="!selectedFile && !systemDiagram" class="flex flex-col items-center justify-center h-full text-center fade-in">
473
+ <div class="text-6xl mb-4">🧠</div>
474
+ <h2 class="text-2xl font-bold text-white mb-2">CodeGraph Explorer</h2>
475
+ <p class="text-gray-500 max-w-md">Select a file from the sidebar to explore its structure, dependencies, and AI-powered explanations.</p>
476
+ <div class="mt-6 flex gap-3">
477
+ <div class="px-4 py-2 bg-cg-card rounded border border-cg-border text-sm text-gray-400">📁 Browse files in sidebar</div>
478
+ <div class="px-4 py-2 bg-cg-card rounded border border-cg-border text-sm text-gray-400">🗺️ View system architecture</div>
479
+ </div>
480
+ <div class="mt-8 text-xs text-gray-600">
481
+ <span class="text-gray-500">Shortcuts:</span> Esc to deselect
482
+ </div>
483
+ </div>
484
+
485
+ <!-- System Diagram View -->
486
+ <div x-show="systemDiagram && !selectedFile" class="fade-in">
487
+ <div class="bg-cg-card rounded-lg border border-cg-border p-6">
488
+ <div class="flex items-center justify-between mb-4">
489
+ <h3 class="text-lg font-bold text-white">🗺️ System Architecture</h3>
490
+ <div class="flex items-center gap-2">
491
+ <button @click="copyToClipboard(systemDiagram); showToast('Mermaid copied!')" class="px-3 py-1 text-xs bg-cg-bg text-gray-300 border border-cg-border rounded hover:bg-gray-700 transition" title="Copy Mermaid code">📋 Mermaid</button>
492
+ <button @click="exportDiagramSVG('systemMermaid', 'system-architecture')" class="px-3 py-1 text-xs bg-cg-bg text-gray-300 border border-cg-border rounded hover:bg-gray-700 transition" title="Download as SVG">🖼️ SVG</button>
493
+ <button @click="openMermaidLive(systemDiagram)" class="px-3 py-1 text-xs bg-cg-bg text-gray-300 border border-cg-border rounded hover:bg-gray-700 transition" title="Open in Mermaid Live Editor">🔗 Mermaid Live</button>
494
+ </div>
495
+ </div>
496
+ <div class="bg-cg-bg rounded p-4 overflow-x-auto">
497
+ <div class="mermaid" x-ref="systemMermaid"></div>
498
+ </div>
499
+ </div>
500
+ </div>
501
+
502
+ <!-- File View -->
503
+ <div x-show="selectedFile && fileData" class="space-y-6 fade-in">
504
+
505
+ <!-- File Header -->
506
+ <div class="flex items-center gap-3">
507
+ <span class="text-2xl" x-text="fileIcon(fileData?.name || '')"></span>
508
+ <div>
509
+ <h2 class="text-xl font-bold text-white" x-text="fileData?.name"></h2>
510
+ <span class="text-xs text-gray-500" x-text="fileData?.path"></span>
511
+ </div>
512
+ </div>
513
+
514
+ <!-- AI Explanation -->
515
+ <div x-show="fileData?.explanation" class="bg-purple-900/20 border border-purple-800/40 rounded-lg p-4 fade-in">
516
+ <div class="flex items-center gap-2 mb-2">
517
+ <span class="text-sm">🤖</span>
518
+ <span class="text-sm font-medium text-cg-purple">AI Explanation</span>
519
+ </div>
520
+ <p class="text-sm text-gray-300 leading-relaxed" x-text="fileData?.explanation"></p>
521
+ </div>
522
+
523
+ <!-- Symbols -->
524
+ <div x-show="(fileData?.functions?.length || 0) + (fileData?.classes?.length || 0) > 0" class="bg-cg-card rounded-lg border border-cg-border p-4">
525
+ <h3 class="text-sm font-semibold text-white mb-3">📌 Symbols</h3>
526
+ <div class="flex flex-wrap gap-2">
527
+ <template x-for="fn in (fileData?.functions || [])" :key="fn.name">
528
+ <span class="inline-flex items-center gap-1 px-2 py-1 bg-blue-900/30 text-blue-400 text-xs rounded border border-blue-800/40">
529
+ <span>ƒ</span> <span x-text="fn.name"></span>
530
+ <span class="text-blue-600" x-text="'L' + fn.line"></span>
531
+ </span>
532
+ </template>
533
+ <template x-for="cls in (fileData?.classes || [])" :key="cls.name">
534
+ <span class="inline-flex items-center gap-1 px-2 py-1 bg-orange-900/30 text-cg-orange text-xs rounded border border-orange-800/40">
535
+ <span>◆</span> <span x-text="cls.name"></span>
536
+ <span class="text-orange-600" x-text="'L' + cls.line"></span>
537
+ </span>
538
+ </template>
539
+ </div>
540
+ </div>
541
+
542
+ <!-- Dependencies Grid -->
543
+ <div class="grid grid-cols-2 gap-4" x-show="(fileData?.deps?.length || 0) + (fileData?.dependents?.length || 0) > 0">
544
+ <div class="bg-cg-card rounded-lg border border-cg-border p-4">
545
+ <h3 class="text-sm font-semibold text-cg-green mb-3">⬆️ Dependencies <span class="text-gray-600 font-normal" x-text="'(' + (fileData?.deps?.length || 0) + ')'"></span></h3>
546
+ <div class="space-y-1 max-h-40 overflow-y-auto">
547
+ <template x-for="dep in (fileData?.deps || [])" :key="dep">
548
+ <div class="text-xs text-gray-400 font-mono truncate" x-text="dep"></div>
549
+ </template>
550
+ <div x-show="!(fileData?.deps?.length)" class="text-xs text-gray-600 italic">None</div>
551
+ </div>
552
+ </div>
553
+ <div class="bg-cg-card rounded-lg border border-cg-border p-4">
554
+ <h3 class="text-sm font-semibold text-cg-accent mb-3">⬇️ Dependents <span class="text-gray-600 font-normal" x-text="'(' + (fileData?.dependents?.length || 0) + ')'"></span></h3>
555
+ <div class="space-y-1 max-h-40 overflow-y-auto">
556
+ <template x-for="d in (fileData?.dependents || [])" :key="d">
557
+ <div class="text-xs text-gray-400 font-mono truncate" x-text="d"></div>
558
+ </template>
559
+ <div x-show="!(fileData?.dependents?.length)" class="text-xs text-gray-600 italic">None</div>
560
+ </div>
561
+ </div>
562
+ </div>
563
+
564
+ <!-- Source Code (collapsible) -->
565
+ <div class="bg-cg-card rounded-lg border border-cg-border overflow-hidden">
566
+ <div class="flex items-center justify-between px-4 py-2 border-b border-cg-border cursor-pointer select-none" @click="codeOpen = !codeOpen">
567
+ <div class="flex items-center gap-2">
568
+ <span class="text-xs text-gray-500 transition-transform duration-200" :class="codeOpen && 'rotate-90'" style="display:inline-block">▶</span>
569
+ <h3 class="text-sm font-semibold text-white">📝 Source Code</h3>
570
+ </div>
571
+ <div class="flex items-center gap-1" @click.stop>
572
+ <button @click="copyToClipboard(fileData?.content || ''); showToast('Code copied!')" class="px-2 py-1 text-xs text-gray-400 hover:text-white transition" title="Copy source code">📋 Copy</button>
573
+ </div>
574
+ </div>
575
+ <div x-show="codeOpen" x-transition:enter="transition ease-out duration-200" x-transition:leave="transition ease-in duration-150" class="overflow-x-auto">
576
+ <pre class="p-4 text-sm leading-relaxed" style="max-height:600px; overflow-y:auto"><code x-ref="codeBlock" class="hljs"></code></pre>
577
+ </div>
578
+ </div>
579
+
580
+ <!-- Mermaid Diagram -->
581
+ <div x-show="fileData?.mermaid" class="bg-cg-card rounded-lg border border-cg-border p-4">
582
+ <div class="flex items-center justify-between mb-3">
583
+ <h3 class="text-sm font-semibold text-white">📊 File Diagram</h3>
584
+ <div class="flex items-center gap-2">
585
+ <button @click="exportMermaid()" class="px-2 py-1 text-xs bg-cg-bg text-gray-400 border border-cg-border rounded hover:text-white hover:bg-gray-700 transition" title="Copy Mermaid code">📋 Mermaid</button>
586
+ <button @click="exportDiagramSVG('fileMermaid', fileData?.name || 'diagram')" class="px-2 py-1 text-xs bg-cg-bg text-gray-400 border border-cg-border rounded hover:text-white hover:bg-gray-700 transition" title="Download as SVG">🖼️ SVG</button>
587
+ <button @click="openMermaidLive(fileData?.mermaid)" class="px-2 py-1 text-xs bg-cg-bg text-gray-400 border border-cg-border rounded hover:text-white hover:bg-gray-700 transition" title="Open in Mermaid Live Editor">🔗 Mermaid Live</button>
588
+ </div>
589
+ </div>
590
+ <div class="bg-cg-bg rounded p-4 overflow-x-auto">
591
+ <div class="mermaid" x-ref="fileMermaid"></div>
592
+ </div>
593
+ </div>
594
+
595
+ </div>
596
+ </div>
597
+ </div>
598
+ </div>
599
+
600
+ <script>
601
+ mermaid.initialize({ startOnLoad: false, theme: 'dark', securityLevel: 'loose' });
602
+
603
+ function explorer() {
604
+ return {
605
+ tree: null,
606
+ selectedFile: null,
607
+ fileData: null,
608
+ fileLoading: false,
609
+ loading: true,
610
+ toast: null,
611
+ treeFilter: '',
612
+ systemDiagram: null,
613
+ projectName: '',
614
+ projectStats: '',
615
+ codeOpen: true,
616
+ showExportModal: false,
617
+ exporting: false,
618
+ exportStatus: { llm_available: false, provider: null, model: null },
619
+ exportOpts: { includeCode: false, includeDiagram: true, enhanced: false, depth: 'modules' },
620
+
621
+ async init() {
622
+ try {
623
+ const res = await fetch('/api/tree');
624
+ const data = await res.json();
625
+ this.tree = data.tree;
626
+ this.projectName = data.project || '';
627
+ const stats = data.stats || {};
628
+ this.projectStats = `${stats.files || 0} files · ${stats.functions || 0} functions · ${stats.classes || 0} classes`;
629
+ } catch(e) {
630
+ console.error('Failed to load tree:', e);
631
+ }
632
+ this.loading = false;
633
+ },
634
+
635
+ filteredTree(children) {
636
+ if (!this.treeFilter) return children || [];
637
+ const q = this.treeFilter.toLowerCase();
638
+ return (children || []).filter(c => {
639
+ if (c.name.toLowerCase().includes(q)) return true;
640
+ if (c.type === 'dir' && c.children) return this.filteredTree(c.children).length > 0;
641
+ return false;
642
+ });
643
+ },
644
+
645
+ async loadFile(path) {
646
+ this.selectedFile = path;
647
+ this.systemDiagram = null;
648
+ this.fileLoading = true;
649
+ this.fileData = null;
650
+ this.codeOpen = true;
651
+ try {
652
+ const res = await fetch('/api/file/' + encodeURIComponent(path));
653
+ this.fileData = await res.json();
654
+ this.$nextTick(() => {
655
+ this.highlightCode();
656
+ this.renderFileMermaid();
657
+ });
658
+ } catch(e) {
659
+ console.error('Failed to load file:', e);
660
+ this.fileData = { name: path.split('/').pop(), path: path, content: 'Error loading file', deps: [], dependents: [], functions: [], classes: [] };
661
+ }
662
+ this.fileLoading = false;
663
+ },
664
+
665
+ async loadSystemDiagram() {
666
+ this.selectedFile = null;
667
+ this.fileData = null;
668
+ try {
669
+ const res = await fetch('/api/diagram');
670
+ const data = await res.json();
671
+ this.systemDiagram = data.mermaid;
672
+ this.$nextTick(() => this.renderSystemMermaid());
673
+ } catch(e) {
674
+ console.error('Failed to load diagram:', e);
675
+ }
676
+ },
677
+
678
+ async triggerExplain() {
679
+ if (!this.selectedFile) return;
680
+ try {
681
+ const res = await fetch('/api/explain/' + encodeURIComponent(this.selectedFile));
682
+ const data = await res.json();
683
+ if (data.explanation && this.fileData) {
684
+ this.fileData.explanation = data.explanation;
685
+ } else if (data.error) {
686
+ this.showToast('LLM not available');
687
+ }
688
+ } catch(e) { this.showToast('Explain failed'); }
689
+ },
690
+
691
+ highlightCode() {
692
+ if (!this.fileData?.content || !this.$refs.codeBlock) return;
693
+ const ext = (this.fileData.name || '').split('.').pop();
694
+ const langMap = { py: 'python', js: 'javascript', ts: 'typescript', jsx: 'javascript', tsx: 'typescript' };
695
+ const lang = langMap[ext] || ext || 'plaintext';
696
+ this.$refs.codeBlock.className = 'hljs language-' + lang;
697
+ this.$refs.codeBlock.textContent = this.fileData.content;
698
+ hljs.highlightElement(this.$refs.codeBlock);
699
+ },
700
+
701
+ async renderFileMermaid() {
702
+ if (!this.fileData?.mermaid || !this.$refs.fileMermaid) return;
703
+ try {
704
+ const id = 'fm-' + Date.now();
705
+ const { svg } = await mermaid.render(id, this.fileData.mermaid);
706
+ this.$refs.fileMermaid.innerHTML = svg;
707
+ } catch(e) { this.$refs.fileMermaid.innerHTML = '<span class="text-red-400 text-xs">Diagram render error</span>'; }
708
+ },
709
+
710
+ async renderSystemMermaid() {
711
+ if (!this.systemDiagram || !this.$refs.systemMermaid) return;
712
+ try {
713
+ const id = 'sm-' + Date.now();
714
+ const { svg } = await mermaid.render(id, this.systemDiagram);
715
+ this.$refs.systemMermaid.innerHTML = svg;
716
+ } catch(e) { this.$refs.systemMermaid.innerHTML = '<span class="text-red-400 text-xs">Diagram render error</span>'; }
717
+ },
718
+
719
+ fileIcon(name) {
720
+ const ext = (name || '').split('.').pop();
721
+ const icons = { py: '🐍', js: '📜', ts: '🔷', jsx: '⚛️', tsx: '⚛️', json: '📋', md: '📝', yml: '⚙️', yaml: '⚙️', toml: '⚙️', html: '🌐', css: '🎨', go: '🔹', rs: '🦀', java: '☕', rb: '💎' };
722
+ return icons[ext] || '📄';
723
+ },
724
+
725
+ exportMermaid() {
726
+ if (this.fileData?.mermaid) {
727
+ this.copyToClipboard(this.fileData.mermaid);
728
+ this.showToast('Mermaid copied!');
729
+ }
730
+ },
731
+
732
+ exportDiagramSVG(refName, filename) {
733
+ const el = this.$refs[refName];
734
+ if (!el) return;
735
+ const svg = el.querySelector('svg');
736
+ if (!svg) { this.showToast('No diagram rendered'); return; }
737
+ const svgData = new XMLSerializer().serializeToString(svg);
738
+ const blob = new Blob([svgData], { type: 'image/svg+xml' });
739
+ const a = document.createElement('a');
740
+ a.href = URL.createObjectURL(blob);
741
+ a.download = (filename || 'diagram') + '.svg';
742
+ a.click();
743
+ URL.revokeObjectURL(a.href);
744
+ this.showToast('SVG downloaded!');
745
+ },
746
+
747
+ openMermaidLive(mermaidCode) {
748
+ if (!mermaidCode) return;
749
+ try {
750
+ const state = JSON.stringify({
751
+ code: mermaidCode,
752
+ mermaid: JSON.stringify({ theme: 'dark' }),
753
+ autoSync: true,
754
+ updateDiagram: true
755
+ });
756
+ const data = new TextEncoder().encode(state);
757
+ const compressed = pako.deflate(data, { level: 9 });
758
+ let b64 = '';
759
+ for (let i = 0; i < compressed.length; i++) {
760
+ b64 += String.fromCharCode(compressed[i]);
761
+ }
762
+ const encoded = btoa(b64).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
763
+ window.open('https://mermaid.live/edit#pako:' + encoded, '_blank');
764
+ } catch(e) {
765
+ console.error('Mermaid Live encoding failed:', e);
766
+ this.copyToClipboard(mermaidCode);
767
+ this.showToast('Copied — paste at mermaid.live/edit');
768
+ }
769
+ },
770
+
771
+ async loadExportStatus() {
772
+ try {
773
+ const res = await fetch('/api/export/status');
774
+ this.exportStatus = await res.json();
775
+ } catch(e) { console.error('Export status failed:', e); }
776
+ },
777
+
778
+ async startExport() {
779
+ this.exporting = true;
780
+ try {
781
+ const params = new URLSearchParams({
782
+ code: this.exportOpts.includeCode,
783
+ enhanced: this.exportOpts.enhanced,
784
+ depth: this.exportOpts.depth,
785
+ no_diagram: !this.exportOpts.includeDiagram,
786
+ });
787
+ const res = await fetch('/api/export?' + params.toString());
788
+ if (!res.ok) {
789
+ const err = await res.json();
790
+ this.showToast('Export failed: ' + (err.error || 'unknown'));
791
+ this.exporting = false;
792
+ return;
793
+ }
794
+ const blob = await res.blob();
795
+ const url = URL.createObjectURL(blob);
796
+ const a = document.createElement('a');
797
+ a.href = url;
798
+ const cd = res.headers.get('Content-Disposition') || '';
799
+ const filenameMatch = cd.match(/filename="(.+?)"/);
800
+ a.download = filenameMatch ? filenameMatch[1] : 'project-docs.docx';
801
+ a.click();
802
+ URL.revokeObjectURL(url);
803
+ this.showToast('DOCX downloaded!');
804
+ this.showExportModal = false;
805
+ } catch(e) {
806
+ console.error('Export failed:', e);
807
+ this.showToast('Export failed');
808
+ }
809
+ this.exporting = false;
810
+ },
811
+
812
+ copyToClipboard(text) { navigator.clipboard.writeText(text).catch(() => {}); },
813
+
814
+ showToast(msg) {
815
+ this.toast = msg;
816
+ setTimeout(() => this.toast = null, 2000);
817
+ }
818
+ };
819
+ }
820
+ </script>
821
+ </body>
822
+ </html>"""
823
+
824
+
825
+ # ===================================================================
826
+ # Starlette app + Uvicorn server
827
+ # ===================================================================
828
+
829
+
830
+ def _create_server(store: Any, llm_provider: str, llm_model: str, llm_api_key: str):
831
+ """Create the Starlette ASGI application."""
832
+ try:
833
+ from starlette.applications import Starlette
834
+ from starlette.responses import HTMLResponse, JSONResponse
835
+ from starlette.routing import Route
836
+ except ImportError:
837
+ raise ImportError(
838
+ "The 'explore' feature requires starlette and uvicorn.\n"
839
+ "Install with: pip install codegraph-cli[explore]"
840
+ )
841
+
842
+ async def homepage(request):
843
+ return HTMLResponse(HTML_TEMPLATE)
844
+
845
+ async def api_tree(request):
846
+ try:
847
+ tree = _build_api_tree(store)
848
+ nodes = store.get_nodes()
849
+ files = set(n["file_path"] for n in nodes)
850
+ functions = sum(1 for n in nodes if n["node_type"] == "function")
851
+ classes = sum(1 for n in nodes if n["node_type"] == "class")
852
+ meta = store.get_metadata()
853
+ return JSONResponse({
854
+ "tree": tree,
855
+ "project": meta.get("project_name", ""),
856
+ "stats": {"files": len(files), "functions": functions, "classes": classes, "nodes": len(nodes)},
857
+ })
858
+ except Exception as e:
859
+ return JSONResponse({"error": str(e)}, status_code=500)
860
+
861
+ async def api_file(request):
862
+ file_path = urllib.parse.unquote(request.path_params["path"])
863
+ try:
864
+ data = _analyze_file(store, file_path)
865
+ return JSONResponse(data)
866
+ except Exception as e:
867
+ return JSONResponse({"error": str(e)}, status_code=500)
868
+
869
+ async def api_diagram(request):
870
+ try:
871
+ mermaid = _generate_system_mermaid(store)
872
+ return JSONResponse({"mermaid": mermaid})
873
+ except Exception as e:
874
+ return JSONResponse({"error": str(e)}, status_code=500)
875
+
876
+ async def api_explain(request):
877
+ file_path = urllib.parse.unquote(request.path_params["path"])
878
+ try:
879
+ explanation = _explain_file(store, file_path, llm_provider, llm_model, llm_api_key)
880
+ if explanation:
881
+ return JSONResponse({"explanation": explanation})
882
+ return JSONResponse({"error": "LLM not configured or unavailable"}, status_code=200)
883
+ except Exception as e:
884
+ return JSONResponse({"error": str(e)}, status_code=500)
885
+
886
+ async def api_export(request):
887
+ """Generate and return DOCX as a file download."""
888
+ try:
889
+ import tempfile
890
+ from .cli_export import generate_basic_docx, generate_enhanced_docx
891
+ from starlette.responses import Response
892
+
893
+ params = request.query_params
894
+ include_code = params.get("code", "false").lower() == "true"
895
+ enhanced = params.get("enhanced", "false").lower() == "true"
896
+ depth = params.get("depth", "modules")
897
+ no_diagram = params.get("no_diagram", "false").lower() == "true"
898
+
899
+ with tempfile.NamedTemporaryFile(suffix=".docx", delete=False) as tmp:
900
+ tmp_path = Path(tmp.name)
901
+
902
+ if enhanced and llm_provider:
903
+ generate_enhanced_docx(
904
+ store=store,
905
+ output_path=tmp_path,
906
+ include_code=include_code,
907
+ include_diagram=not no_diagram,
908
+ explanation_depth=depth,
909
+ llm_provider=llm_provider,
910
+ llm_model=llm_model,
911
+ llm_api_key=llm_api_key,
912
+ )
913
+ else:
914
+ generate_basic_docx(
915
+ store=store,
916
+ output_path=tmp_path,
917
+ include_code=include_code,
918
+ include_diagram=not no_diagram,
919
+ )
920
+
921
+ data = tmp_path.read_bytes()
922
+ tmp_path.unlink(missing_ok=True)
923
+
924
+ meta = store.get_metadata()
925
+ filename = f"{meta.get('project_name', 'project')}-docs.docx"
926
+
927
+ return Response(
928
+ content=data,
929
+ media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
930
+ headers={"Content-Disposition": f'attachment; filename="{filename}"'},
931
+ )
932
+ except Exception as e:
933
+ logger.exception("Export failed")
934
+ return JSONResponse({"error": str(e)}, status_code=500)
935
+
936
+ async def api_export_status(request):
937
+ """Return export capabilities (whether LLM is available etc)."""
938
+ return JSONResponse({
939
+ "llm_available": bool(llm_provider and llm_model),
940
+ "provider": llm_provider or None,
941
+ "model": llm_model or None,
942
+ })
943
+
944
+ app = Starlette(routes=[
945
+ Route("/", homepage),
946
+ Route("/api/tree", api_tree),
947
+ Route("/api/file/{path:path}", api_file),
948
+ Route("/api/diagram", api_diagram),
949
+ Route("/api/explain/{path:path}", api_explain),
950
+ Route("/api/export", api_export),
951
+ Route("/api/export/status", api_export_status),
952
+ ])
953
+ return app
954
+
955
+
956
+ # ===================================================================
957
+ # Typer command
958
+ # ===================================================================
959
+
960
+
961
+ @explore_app.command("open")
962
+ def explore_open(
963
+ port: int = typer.Option(8421, "--port", "-p", help="Port for the local web server."),
964
+ ):
965
+ """🌐 Open the visual code explorer in your browser.
966
+
967
+ Launches a local web server and opens a modern UI for navigating
968
+ your indexed codebase with syntax highlighting, dependency graphs,
969
+ AI explanations, and Mermaid diagrams.
970
+
971
+ Example:
972
+ cg explore open
973
+ cg explore open --port 9000
974
+ """
975
+ import socket
976
+ import threading
977
+ import time
978
+ import webbrowser
979
+
980
+ from rich.console import Console
981
+
982
+ from .storage import GraphStore, ProjectManager
983
+ from . import config as cfg
984
+
985
+ console = Console()
986
+
987
+ pm = ProjectManager()
988
+ project = pm.get_current_project()
989
+ if not project:
990
+ console.print("[red]No project loaded.[/red] Run [cyan]cg project index <path>[/cyan] first.")
991
+ raise typer.Exit(code=1)
992
+
993
+ project_dir = pm.project_dir(project)
994
+ if not project_dir.exists():
995
+ console.print(f"[red]Project '{project}' not found.[/red]")
996
+ raise typer.Exit(code=1)
997
+
998
+ store = GraphStore(project_dir)
999
+
1000
+ # Verify the project has nodes
1001
+ nodes = store.get_nodes()
1002
+ if not nodes:
1003
+ console.print("[red]Project is empty — no indexed nodes found.[/red] Re-run [cyan]cg project index[/cyan].")
1004
+ store.close()
1005
+ raise typer.Exit(code=1)
1006
+
1007
+ # Check if port is available
1008
+ actual_port = port
1009
+ for attempt in range(5):
1010
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
1011
+ if s.connect_ex(("127.0.0.1", actual_port)) != 0:
1012
+ break
1013
+ actual_port += 1
1014
+ else:
1015
+ console.print(f"[red]Ports {port}-{port+4} are all in use.[/red]")
1016
+ store.close()
1017
+ raise typer.Exit(code=1)
1018
+
1019
+ server_app = _create_server(
1020
+ store,
1021
+ llm_provider=cfg.LLM_PROVIDER,
1022
+ llm_model=cfg.LLM_MODEL,
1023
+ llm_api_key=cfg.LLM_API_KEY,
1024
+ )
1025
+
1026
+ url = f"http://127.0.0.1:{actual_port}"
1027
+ console.print(f"\n[bold green]🌐 CodeGraph Explorer[/bold green]")
1028
+ console.print(f" Project: [cyan]{project}[/cyan]")
1029
+ console.print(f" URL: [link={url}]{url}[/link]")
1030
+ console.print(f" Nodes: {len(nodes)}")
1031
+ console.print(f"\n [dim]Press Ctrl+C to stop the server[/dim]\n")
1032
+
1033
+ # Open browser after a short delay
1034
+ def _open_browser():
1035
+ time.sleep(1.0)
1036
+ webbrowser.open(url)
1037
+
1038
+ threading.Thread(target=_open_browser, daemon=True).start()
1039
+
1040
+ try:
1041
+ import uvicorn
1042
+ except ImportError:
1043
+ raise ImportError(
1044
+ "The 'explore' feature requires uvicorn.\n"
1045
+ "Install with: pip install codegraph-cli[explore]"
1046
+ )
1047
+ try:
1048
+ uvicorn.run(server_app, host="127.0.0.1", port=actual_port, log_level="warning")
1049
+ except KeyboardInterrupt:
1050
+ pass
1051
+ finally:
1052
+ store.close()
1053
+ console.print("\n[dim]Server stopped.[/dim]")