codedocent 0.3.0__tar.gz → 0.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- codedocent-0.5.0/PKG-INFO +76 -0
- codedocent-0.5.0/README.md +66 -0
- {codedocent-0.3.0 → codedocent-0.5.0}/codedocent/analyzer.py +82 -15
- {codedocent-0.3.0 → codedocent-0.5.0}/codedocent/cli.py +177 -17
- codedocent-0.5.0/codedocent/cloud_ai.py +208 -0
- codedocent-0.5.0/codedocent/gui.py +294 -0
- {codedocent-0.3.0 → codedocent-0.5.0}/codedocent/quality.py +6 -54
- {codedocent-0.3.0 → codedocent-0.5.0}/codedocent/server.py +40 -5
- {codedocent-0.3.0 → codedocent-0.5.0}/codedocent/templates/interactive.html +9 -11
- codedocent-0.5.0/codedocent.egg-info/PKG-INFO +76 -0
- {codedocent-0.3.0 → codedocent-0.5.0}/codedocent.egg-info/SOURCES.txt +2 -0
- {codedocent-0.3.0 → codedocent-0.5.0}/pyproject.toml +2 -2
- {codedocent-0.3.0 → codedocent-0.5.0}/tests/test_analyzer.py +187 -141
- {codedocent-0.3.0 → codedocent-0.5.0}/tests/test_cli.py +170 -9
- codedocent-0.5.0/tests/test_cloud_ai.py +266 -0
- {codedocent-0.3.0 → codedocent-0.5.0}/tests/test_gui.py +35 -0
- codedocent-0.3.0/PKG-INFO +0 -58
- codedocent-0.3.0/README.md +0 -40
- codedocent-0.3.0/codedocent/gui.py +0 -158
- codedocent-0.3.0/codedocent.egg-info/PKG-INFO +0 -58
- {codedocent-0.3.0 → codedocent-0.5.0}/LICENSE +0 -0
- {codedocent-0.3.0 → codedocent-0.5.0}/codedocent/__init__.py +0 -0
- {codedocent-0.3.0 → codedocent-0.5.0}/codedocent/__main__.py +0 -0
- {codedocent-0.3.0 → codedocent-0.5.0}/codedocent/editor.py +0 -0
- {codedocent-0.3.0 → codedocent-0.5.0}/codedocent/ollama_utils.py +0 -0
- {codedocent-0.3.0 → codedocent-0.5.0}/codedocent/parser.py +0 -0
- {codedocent-0.3.0 → codedocent-0.5.0}/codedocent/renderer.py +0 -0
- {codedocent-0.3.0 → codedocent-0.5.0}/codedocent/scanner.py +0 -0
- {codedocent-0.3.0 → codedocent-0.5.0}/codedocent/templates/base.html +0 -0
- {codedocent-0.3.0 → codedocent-0.5.0}/codedocent.egg-info/dependency_links.txt +0 -0
- {codedocent-0.3.0 → codedocent-0.5.0}/codedocent.egg-info/entry_points.txt +0 -0
- {codedocent-0.3.0 → codedocent-0.5.0}/codedocent.egg-info/requires.txt +4 -4
- {codedocent-0.3.0 → codedocent-0.5.0}/codedocent.egg-info/top_level.txt +0 -0
- {codedocent-0.3.0 → codedocent-0.5.0}/setup.cfg +0 -0
- {codedocent-0.3.0 → codedocent-0.5.0}/tests/test_editor.py +0 -0
- {codedocent-0.3.0 → codedocent-0.5.0}/tests/test_parser.py +0 -0
- {codedocent-0.3.0 → codedocent-0.5.0}/tests/test_renderer.py +0 -0
- {codedocent-0.3.0 → codedocent-0.5.0}/tests/test_scanner.py +0 -0
- {codedocent-0.3.0 → codedocent-0.5.0}/tests/test_server.py +0 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: codedocent
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: Code visualization for non-programmers
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Provides-Extra: dev
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
|
|
11
|
+
# codedocent
|
|
12
|
+
|
|
13
|
+
<img width="1658" height="2158" alt="Screenshot_2026-02-09_13-17-06" src="https://github.com/user-attachments/assets/ff097ead-69ec-4618-b7b7-2b99c60ac57e" />
|
|
14
|
+
|
|
15
|
+
**Code visualization for non-programmers.**
|
|
16
|
+
|
|
17
|
+
A docent is a guide who explains things to people who aren't experts. Codedocent does that for code.
|
|
18
|
+
|
|
19
|
+
## The problem
|
|
20
|
+
|
|
21
|
+
You're staring at a codebase you didn't write — maybe thousands of files across dozens of directories — and you need to understand what it does. Reading every file isn't realistic. You need a way to visualize the code structure, get a high-level map of what's where, and drill into the parts that matter without losing context.
|
|
22
|
+
|
|
23
|
+
Codedocent parses the codebase into a navigable, visual block structure and explains each piece in plain English. It's an AI code analysis tool — use a cloud provider for speed or run locally through Ollama for full privacy. Point it at any codebase and get a structural overview you can explore interactively, understand quickly, and share as a static HTML file.
|
|
24
|
+
|
|
25
|
+
## Who this is for
|
|
26
|
+
|
|
27
|
+
- **Developers onboarding onto an unfamiliar codebase** — get oriented in minutes instead of days
|
|
28
|
+
- **Non-programmers** (managers, designers, PMs) who need to understand what code does without reading it
|
|
29
|
+
- **Solo developers inheriting legacy code** — map out the structure before making changes
|
|
30
|
+
- **Code reviewers** who want a high-level overview before diving into details
|
|
31
|
+
- **Security reviewers** who need a structural map of an application
|
|
32
|
+
- **Students** learning to read and navigate real-world codebases
|
|
33
|
+
|
|
34
|
+
## What you see
|
|
35
|
+
|
|
36
|
+
Nested, color-coded blocks representing directories, files, classes, and functions — the entire structure of a codebase laid out visually. Each block shows a plain English summary, a pseudocode translation, and quality warnings (green/yellow/red). Click any block to drill down; breadcrumbs navigate you back up. You can export code from any block or paste replacement code back into the source file. AI explanations come from your choice of cloud provider or local Ollama.
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install codedocent
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Requires Python 3.10+. Cloud AI needs an API key set in an env var (e.g. `OPENAI_API_KEY`). Local AI needs [Ollama](https://ollama.com) running. `--no-ai` skips AI entirely.
|
|
45
|
+
|
|
46
|
+
## Quick start
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
codedocent # setup wizard — walks you through everything
|
|
50
|
+
codedocent /path/to/code # interactive mode (recommended)
|
|
51
|
+
codedocent /path/to/code --full # full analysis, static HTML output
|
|
52
|
+
codedocent --gui # graphical launcher
|
|
53
|
+
codedocent /path/to/code --cloud openai # use OpenAI
|
|
54
|
+
codedocent /path/to/code --cloud groq # use Groq
|
|
55
|
+
codedocent /path/to/code --cloud custom --endpoint https://my-llm/v1/chat/completions
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## How it works
|
|
59
|
+
|
|
60
|
+
Parses code structure with tree-sitter, scores quality with static analysis, and sends individual blocks to a cloud AI provider or local Ollama model for plain English summaries and pseudocode. Interactive mode analyzes on click — typically 1-2 seconds per block. Full mode analyzes everything upfront into a self-contained HTML file you can share.
|
|
61
|
+
|
|
62
|
+
## AI options
|
|
63
|
+
|
|
64
|
+
- **Cloud AI** — send code to OpenAI, OpenRouter, Groq, or any OpenAI-compatible endpoint. Fast, no local setup. Your code is sent to that service. API keys are read from env vars (`OPENAI_API_KEY`, `OPENROUTER_API_KEY`, `GROQ_API_KEY`, `CODEDOCENT_API_KEY` for custom endpoints).
|
|
65
|
+
- **Local AI** — [Ollama](https://ollama.com) on your machine. Code never leaves your laptop. No API keys, no accounts.
|
|
66
|
+
- **No AI** (`--no-ai`) — structure and quality scores only.
|
|
67
|
+
|
|
68
|
+
The setup wizard (`codedocent` with no args) walks you through choosing.
|
|
69
|
+
|
|
70
|
+
## Supported languages
|
|
71
|
+
|
|
72
|
+
Full AST parsing for Python and JavaScript/TypeScript (functions, classes, methods, imports). File-level detection for 23 extensions including C, C++, Rust, Go, Java, Ruby, PHP, Swift, Kotlin, Scala, HTML, CSS, and config formats.
|
|
73
|
+
|
|
74
|
+
## License
|
|
75
|
+
|
|
76
|
+
MIT
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# codedocent
|
|
2
|
+
|
|
3
|
+
<img width="1658" height="2158" alt="Screenshot_2026-02-09_13-17-06" src="https://github.com/user-attachments/assets/ff097ead-69ec-4618-b7b7-2b99c60ac57e" />
|
|
4
|
+
|
|
5
|
+
**Code visualization for non-programmers.**
|
|
6
|
+
|
|
7
|
+
A docent is a guide who explains things to people who aren't experts. Codedocent does that for code.
|
|
8
|
+
|
|
9
|
+
## The problem
|
|
10
|
+
|
|
11
|
+
You're staring at a codebase you didn't write — maybe thousands of files across dozens of directories — and you need to understand what it does. Reading every file isn't realistic. You need a way to visualize the code structure, get a high-level map of what's where, and drill into the parts that matter without losing context.
|
|
12
|
+
|
|
13
|
+
Codedocent parses the codebase into a navigable, visual block structure and explains each piece in plain English. It's an AI code analysis tool — use a cloud provider for speed or run locally through Ollama for full privacy. Point it at any codebase and get a structural overview you can explore interactively, understand quickly, and share as a static HTML file.
|
|
14
|
+
|
|
15
|
+
## Who this is for
|
|
16
|
+
|
|
17
|
+
- **Developers onboarding onto an unfamiliar codebase** — get oriented in minutes instead of days
|
|
18
|
+
- **Non-programmers** (managers, designers, PMs) who need to understand what code does without reading it
|
|
19
|
+
- **Solo developers inheriting legacy code** — map out the structure before making changes
|
|
20
|
+
- **Code reviewers** who want a high-level overview before diving into details
|
|
21
|
+
- **Security reviewers** who need a structural map of an application
|
|
22
|
+
- **Students** learning to read and navigate real-world codebases
|
|
23
|
+
|
|
24
|
+
## What you see
|
|
25
|
+
|
|
26
|
+
Nested, color-coded blocks representing directories, files, classes, and functions — the entire structure of a codebase laid out visually. Each block shows a plain English summary, a pseudocode translation, and quality warnings (green/yellow/red). Click any block to drill down; breadcrumbs navigate you back up. You can export code from any block or paste replacement code back into the source file. AI explanations come from your choice of cloud provider or local Ollama.
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install codedocent
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Requires Python 3.10+. Cloud AI needs an API key set in an env var (e.g. `OPENAI_API_KEY`). Local AI needs [Ollama](https://ollama.com) running. `--no-ai` skips AI entirely.
|
|
35
|
+
|
|
36
|
+
## Quick start
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
codedocent # setup wizard — walks you through everything
|
|
40
|
+
codedocent /path/to/code # interactive mode (recommended)
|
|
41
|
+
codedocent /path/to/code --full # full analysis, static HTML output
|
|
42
|
+
codedocent --gui # graphical launcher
|
|
43
|
+
codedocent /path/to/code --cloud openai # use OpenAI
|
|
44
|
+
codedocent /path/to/code --cloud groq # use Groq
|
|
45
|
+
codedocent /path/to/code --cloud custom --endpoint https://my-llm/v1/chat/completions
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## How it works
|
|
49
|
+
|
|
50
|
+
Parses code structure with tree-sitter, scores quality with static analysis, and sends individual blocks to a cloud AI provider or local Ollama model for plain English summaries and pseudocode. Interactive mode analyzes on click — typically 1-2 seconds per block. Full mode analyzes everything upfront into a self-contained HTML file you can share.
|
|
51
|
+
|
|
52
|
+
## AI options
|
|
53
|
+
|
|
54
|
+
- **Cloud AI** — send code to OpenAI, OpenRouter, Groq, or any OpenAI-compatible endpoint. Fast, no local setup. Your code is sent to that service. API keys are read from env vars (`OPENAI_API_KEY`, `OPENROUTER_API_KEY`, `GROQ_API_KEY`, `CODEDOCENT_API_KEY` for custom endpoints).
|
|
55
|
+
- **Local AI** — [Ollama](https://ollama.com) on your machine. Code never leaves your laptop. No API keys, no accounts.
|
|
56
|
+
- **No AI** (`--no-ai`) — structure and quality scores only.
|
|
57
|
+
|
|
58
|
+
The setup wizard (`codedocent` with no args) walks you through choosing.
|
|
59
|
+
|
|
60
|
+
## Supported languages
|
|
61
|
+
|
|
62
|
+
Full AST parsing for Python and JavaScript/TypeScript (functions, classes, methods, imports). File-level detection for 23 extensions including C, C++, Rust, Go, Java, Ruby, PHP, Swift, Kotlin, Scala, HTML, CSS, and config formats.
|
|
63
|
+
|
|
64
|
+
## License
|
|
65
|
+
|
|
66
|
+
MIT
|
|
@@ -115,13 +115,49 @@ def _parse_ai_response(text: str) -> tuple[str, str]:
|
|
|
115
115
|
_AI_TIMEOUT = 120
|
|
116
116
|
|
|
117
117
|
|
|
118
|
+
def _summarize_with_cloud(
|
|
119
|
+
node: CodeNode, ai_config: dict,
|
|
120
|
+
) -> tuple[str, str] | None:
|
|
121
|
+
"""Call a cloud AI endpoint for summary and pseudocode.
|
|
122
|
+
|
|
123
|
+
Returns ``None`` if the call times out.
|
|
124
|
+
Raises ``RuntimeError`` on API errors.
|
|
125
|
+
"""
|
|
126
|
+
from codedocent.cloud_ai import cloud_chat # pylint: disable=import-outside-toplevel # noqa: E501
|
|
127
|
+
|
|
128
|
+
prompt = _build_prompt(node, ai_config["model"])
|
|
129
|
+
pool = ThreadPoolExecutor(max_workers=1)
|
|
130
|
+
future = pool.submit(
|
|
131
|
+
cloud_chat,
|
|
132
|
+
prompt, ai_config["endpoint"],
|
|
133
|
+
ai_config["api_key"], ai_config["model"],
|
|
134
|
+
)
|
|
135
|
+
try:
|
|
136
|
+
raw = future.result(timeout=_AI_TIMEOUT)
|
|
137
|
+
except TimeoutError:
|
|
138
|
+
future.cancel()
|
|
139
|
+
pool.shutdown(wait=False, cancel_futures=True)
|
|
140
|
+
return None
|
|
141
|
+
pool.shutdown(wait=False)
|
|
142
|
+
raw = _strip_think_tags(raw)
|
|
143
|
+
if not raw or len(raw) < 10:
|
|
144
|
+
return ("Could not generate summary", "")
|
|
145
|
+
summary, pseudocode = _parse_ai_response(raw)
|
|
146
|
+
if not summary or len(summary) < 5:
|
|
147
|
+
summary = "Could not generate summary"
|
|
148
|
+
return summary, pseudocode
|
|
149
|
+
|
|
150
|
+
|
|
118
151
|
def _summarize_with_ai(
|
|
119
|
-
node: CodeNode, model: str
|
|
152
|
+
node: CodeNode, model: str, ai_config: dict | None = None,
|
|
120
153
|
) -> tuple[str, str] | None:
|
|
121
|
-
"""Call ollama to get summary and pseudocode for a node.
|
|
154
|
+
"""Call ollama (or cloud) to get summary and pseudocode for a node.
|
|
122
155
|
|
|
123
156
|
Returns ``None`` if the AI call times out.
|
|
124
157
|
"""
|
|
158
|
+
if ai_config and ai_config.get("backend") == "cloud":
|
|
159
|
+
return _summarize_with_cloud(node, ai_config)
|
|
160
|
+
|
|
125
161
|
prompt = _build_prompt(node, model)
|
|
126
162
|
pool = ThreadPoolExecutor(max_workers=1)
|
|
127
163
|
future = pool.submit(
|
|
@@ -151,6 +187,13 @@ def _summarize_with_ai(
|
|
|
151
187
|
return summary, pseudocode
|
|
152
188
|
|
|
153
189
|
|
|
190
|
+
def _cache_model_id(model: str, ai_config: dict | None = None) -> str:
|
|
191
|
+
"""Return a cache-key model identifier."""
|
|
192
|
+
if ai_config and ai_config.get("backend") == "cloud":
|
|
193
|
+
return f"cloud:{ai_config['provider']}:{ai_config['model']}"
|
|
194
|
+
return model
|
|
195
|
+
|
|
196
|
+
|
|
154
197
|
# ---------------------------------------------------------------------------
|
|
155
198
|
# Cache
|
|
156
199
|
# ---------------------------------------------------------------------------
|
|
@@ -235,12 +278,16 @@ def assign_node_ids(root: CodeNode) -> dict[str, CodeNode]:
|
|
|
235
278
|
# ---------------------------------------------------------------------------
|
|
236
279
|
|
|
237
280
|
|
|
238
|
-
def analyze_single_node(
|
|
281
|
+
def analyze_single_node( # pylint: disable=too-many-locals
|
|
282
|
+
node: CodeNode, model: str, cache_dir: str,
|
|
283
|
+
*, ai_config: dict | None = None,
|
|
284
|
+
) -> None:
|
|
239
285
|
"""Run quality scoring + AI analysis on a single node.
|
|
240
286
|
|
|
241
287
|
Reads/writes the cache. Applies min-lines guard and garbage fallback.
|
|
242
288
|
"""
|
|
243
|
-
|
|
289
|
+
is_cloud = ai_config and ai_config.get("backend") == "cloud"
|
|
290
|
+
if not is_cloud and ollama is None:
|
|
244
291
|
node.summary = "AI unavailable (ollama not installed)"
|
|
245
292
|
return
|
|
246
293
|
|
|
@@ -263,8 +310,9 @@ def analyze_single_node(node: CodeNode, model: str, cache_dir: str) -> None:
|
|
|
263
310
|
cache_path = os.path.join(cache_dir, CACHE_FILENAME)
|
|
264
311
|
cache = _load_cache(cache_path)
|
|
265
312
|
|
|
266
|
-
|
|
267
|
-
|
|
313
|
+
model_id = _cache_model_id(model, ai_config)
|
|
314
|
+
if cache.get("model") != model_id:
|
|
315
|
+
cache = {"version": 1, "model": model_id, "entries": {}}
|
|
268
316
|
|
|
269
317
|
key = _cache_key(node)
|
|
270
318
|
if key in cache["entries"]:
|
|
@@ -274,7 +322,7 @@ def analyze_single_node(node: CodeNode, model: str, cache_dir: str) -> None:
|
|
|
274
322
|
return
|
|
275
323
|
|
|
276
324
|
try:
|
|
277
|
-
result = _summarize_with_ai(node, model)
|
|
325
|
+
result = _summarize_with_ai(node, model, ai_config=ai_config)
|
|
278
326
|
if result is None:
|
|
279
327
|
node.summary = "Summary timed out"
|
|
280
328
|
return
|
|
@@ -287,7 +335,8 @@ def analyze_single_node(node: CodeNode, model: str, cache_dir: str) -> None:
|
|
|
287
335
|
ConnectionError, RuntimeError, ValueError,
|
|
288
336
|
OSError, AttributeError, TypeError,
|
|
289
337
|
) as e:
|
|
290
|
-
|
|
338
|
+
print(f" AI error for {node.name}: {e}", file=sys.stderr)
|
|
339
|
+
node.summary = "Summary generation failed"
|
|
291
340
|
|
|
292
341
|
|
|
293
342
|
# ---------------------------------------------------------------------------
|
|
@@ -359,6 +408,7 @@ def _run_ai_batch(
|
|
|
359
408
|
model: str,
|
|
360
409
|
cache: dict,
|
|
361
410
|
workers: int,
|
|
411
|
+
ai_config: dict | None = None,
|
|
362
412
|
) -> int:
|
|
363
413
|
"""Phases 2 & 3: AI-analyze files then code nodes."""
|
|
364
414
|
total, counter = len(all_nodes), [0]
|
|
@@ -384,7 +434,7 @@ def _run_ai_batch(
|
|
|
384
434
|
return
|
|
385
435
|
_progress(f"Analyzing {node.name}")
|
|
386
436
|
try:
|
|
387
|
-
result = _summarize_with_ai(node, model)
|
|
437
|
+
result = _summarize_with_ai(node, model, ai_config=ai_config)
|
|
388
438
|
if result is None:
|
|
389
439
|
node.summary = "Summary timed out"
|
|
390
440
|
return
|
|
@@ -425,13 +475,16 @@ def _require_ollama() -> None:
|
|
|
425
475
|
sys.exit(1)
|
|
426
476
|
|
|
427
477
|
|
|
428
|
-
def _init_cache(
|
|
478
|
+
def _init_cache(
|
|
479
|
+
root: CodeNode, model: str, ai_config: dict | None = None,
|
|
480
|
+
) -> tuple[str, dict]:
|
|
429
481
|
"""Load (or reset) the analysis cache for *model*."""
|
|
430
482
|
cache_dir = root.filepath or "."
|
|
431
483
|
cache_path = os.path.join(cache_dir, CACHE_FILENAME)
|
|
432
484
|
cache = _load_cache(cache_path)
|
|
433
|
-
|
|
434
|
-
|
|
485
|
+
model_id = _cache_model_id(model, ai_config)
|
|
486
|
+
if cache.get("model") != model_id:
|
|
487
|
+
cache = {"version": 1, "model": model_id, "entries": {}}
|
|
435
488
|
return cache_path, cache
|
|
436
489
|
|
|
437
490
|
|
|
@@ -439,11 +492,15 @@ def analyze(
|
|
|
439
492
|
root: CodeNode,
|
|
440
493
|
model: str = "qwen3:14b",
|
|
441
494
|
workers: int = 1,
|
|
495
|
+
*,
|
|
496
|
+
ai_config: dict | None = None,
|
|
442
497
|
) -> CodeNode:
|
|
443
498
|
"""Analyze the full tree with AI summaries and quality scoring."""
|
|
444
|
-
|
|
499
|
+
is_cloud = ai_config and ai_config.get("backend") == "cloud"
|
|
500
|
+
if not is_cloud:
|
|
501
|
+
_require_ollama()
|
|
445
502
|
|
|
446
|
-
cache_path, cache = _init_cache(root, model)
|
|
503
|
+
cache_path, cache = _init_cache(root, model, ai_config=ai_config)
|
|
447
504
|
all_nodes = _collect_nodes(root)
|
|
448
505
|
start_time = time.monotonic()
|
|
449
506
|
|
|
@@ -451,7 +508,9 @@ def analyze(
|
|
|
451
508
|
_rollup_file_quality(all_nodes)
|
|
452
509
|
|
|
453
510
|
try:
|
|
454
|
-
ai_count = _run_ai_batch(
|
|
511
|
+
ai_count = _run_ai_batch(
|
|
512
|
+
all_nodes, model, cache, workers, ai_config=ai_config,
|
|
513
|
+
)
|
|
455
514
|
except ConnectionError as e:
|
|
456
515
|
print(
|
|
457
516
|
f"\nError: Could not connect to ollama: {e}\n"
|
|
@@ -460,6 +519,14 @@ def analyze(
|
|
|
460
519
|
file=sys.stderr,
|
|
461
520
|
)
|
|
462
521
|
sys.exit(1)
|
|
522
|
+
except RuntimeError as e:
|
|
523
|
+
print(
|
|
524
|
+
f"\nError: Cloud AI request failed: {e}\n"
|
|
525
|
+
"Check your API key and endpoint, or use --no-ai"
|
|
526
|
+
" to skip AI analysis.",
|
|
527
|
+
file=sys.stderr,
|
|
528
|
+
)
|
|
529
|
+
sys.exit(1)
|
|
463
530
|
|
|
464
531
|
_summarize_directories(all_nodes)
|
|
465
532
|
_save_cache(cache_path, cache)
|
|
@@ -6,6 +6,9 @@ import argparse
|
|
|
6
6
|
import os
|
|
7
7
|
import sys
|
|
8
8
|
|
|
9
|
+
from codedocent.cloud_ai import (
|
|
10
|
+
CLOUD_PROVIDERS, validate_cloud_config, _MaskedSecret,
|
|
11
|
+
)
|
|
9
12
|
from codedocent.ollama_utils import check_ollama, fetch_ollama_models
|
|
10
13
|
from codedocent.parser import CodeNode, parse_directory
|
|
11
14
|
from codedocent.scanner import scan_directory
|
|
@@ -88,28 +91,111 @@ def _pick_model(models: list[str]) -> str:
|
|
|
88
91
|
return models[0]
|
|
89
92
|
|
|
90
93
|
|
|
94
|
+
def _wizard_cloud_setup() -> dict | None:
|
|
95
|
+
"""Handle the cloud AI setup sub-flow of the wizard.
|
|
96
|
+
|
|
97
|
+
Returns an ai_config dict on success, or None if the user
|
|
98
|
+
cannot proceed (exits with instructions).
|
|
99
|
+
"""
|
|
100
|
+
print("\nWhich cloud provider?")
|
|
101
|
+
providers = ["openai", "openrouter", "groq", "custom"]
|
|
102
|
+
for i, p in enumerate(providers, 1):
|
|
103
|
+
print(f" {i}. {CLOUD_PROVIDERS[p]['name']}")
|
|
104
|
+
choice = _safe_input("Provider [1]: ").strip()
|
|
105
|
+
try:
|
|
106
|
+
idx = int(choice) - 1 if choice else 0
|
|
107
|
+
if not 0 <= idx < len(providers):
|
|
108
|
+
idx = 0
|
|
109
|
+
except ValueError:
|
|
110
|
+
idx = 0
|
|
111
|
+
provider = providers[idx]
|
|
112
|
+
|
|
113
|
+
info = CLOUD_PROVIDERS[provider]
|
|
114
|
+
endpoint = info["endpoint"]
|
|
115
|
+
|
|
116
|
+
if provider == "custom":
|
|
117
|
+
endpoint = _safe_input("Endpoint URL: ").strip()
|
|
118
|
+
if not endpoint:
|
|
119
|
+
print("Error: custom provider requires an endpoint URL.")
|
|
120
|
+
sys.exit(1)
|
|
121
|
+
|
|
122
|
+
env_var = info["env_var"]
|
|
123
|
+
api_key = os.environ.get(env_var, "")
|
|
124
|
+
if api_key:
|
|
125
|
+
print(f"API key found in ${env_var}")
|
|
126
|
+
else:
|
|
127
|
+
print(
|
|
128
|
+
f"\nAPI key not found. Set it with:\n"
|
|
129
|
+
f" export {env_var}=your-key-here\n"
|
|
130
|
+
f"Then run codedocent again."
|
|
131
|
+
)
|
|
132
|
+
sys.exit(1)
|
|
133
|
+
|
|
134
|
+
# Model picker
|
|
135
|
+
models = info["models"]
|
|
136
|
+
if models:
|
|
137
|
+
model = _pick_model(models)
|
|
138
|
+
else:
|
|
139
|
+
model = _safe_input("Model name: ").strip()
|
|
140
|
+
if not model:
|
|
141
|
+
print("Error: model name is required.")
|
|
142
|
+
sys.exit(1)
|
|
143
|
+
|
|
144
|
+
# Validate
|
|
145
|
+
print("Testing connection...", end=" ", flush=True)
|
|
146
|
+
ok, err = validate_cloud_config(provider, endpoint, api_key, model)
|
|
147
|
+
if ok:
|
|
148
|
+
print("success!")
|
|
149
|
+
else:
|
|
150
|
+
print(f"failed: {err}")
|
|
151
|
+
sys.exit(1)
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
"backend": "cloud",
|
|
155
|
+
"provider": provider,
|
|
156
|
+
"endpoint": endpoint,
|
|
157
|
+
"api_key": _MaskedSecret(api_key),
|
|
158
|
+
"model": model,
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
|
|
91
162
|
def _run_wizard() -> argparse.Namespace:
|
|
92
163
|
"""Interactive setup wizard for codedocent."""
|
|
93
164
|
print("\ncodedocent \u2014 code visualization for humans\n")
|
|
94
165
|
|
|
95
166
|
path = _ask_folder()
|
|
96
167
|
|
|
97
|
-
# ---
|
|
168
|
+
# --- Backend choice ---
|
|
98
169
|
model = "qwen3:14b"
|
|
99
170
|
no_ai = False
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
171
|
+
ai_config = None
|
|
172
|
+
|
|
173
|
+
print("How would you like Codedocent to understand your code?")
|
|
174
|
+
print(" [1] Cloud AI \u2014 Uses a service like OpenAI or OpenRouter.")
|
|
175
|
+
print(" [2] Local AI \u2014 Uses Ollama running on your machine.")
|
|
176
|
+
print(" [3] No AI \u2014 Just show code structure and quality scores.")
|
|
177
|
+
backend_choice = _safe_input("Choice [2]: ").strip()
|
|
178
|
+
|
|
179
|
+
if backend_choice == "1":
|
|
180
|
+
ai_config = _wizard_cloud_setup()
|
|
181
|
+
if ai_config:
|
|
182
|
+
model = ai_config["model"]
|
|
183
|
+
elif backend_choice == "3":
|
|
184
|
+
no_ai = True
|
|
185
|
+
else:
|
|
186
|
+
# Default: Local AI (Ollama)
|
|
187
|
+
print("Checking for Ollama...", end=" ", flush=True)
|
|
188
|
+
if _check_ollama():
|
|
189
|
+
print("found!")
|
|
190
|
+
models = _fetch_ollama_models()
|
|
191
|
+
if models:
|
|
192
|
+
model = _pick_model(models)
|
|
193
|
+
else:
|
|
194
|
+
print("No models found.")
|
|
195
|
+
no_ai = _ask_no_ai_fallback()
|
|
107
196
|
else:
|
|
108
|
-
print("
|
|
197
|
+
print("not found.")
|
|
109
198
|
no_ai = _ask_no_ai_fallback()
|
|
110
|
-
else:
|
|
111
|
-
print("not found.")
|
|
112
|
-
no_ai = _ask_no_ai_fallback()
|
|
113
199
|
|
|
114
200
|
# --- Mode ---
|
|
115
201
|
print("\nHow do you want to view it?")
|
|
@@ -133,6 +219,10 @@ def _run_wizard() -> argparse.Namespace:
|
|
|
133
219
|
port=None,
|
|
134
220
|
workers=1,
|
|
135
221
|
gui=False,
|
|
222
|
+
cloud=ai_config["provider"] if ai_config else None,
|
|
223
|
+
endpoint=ai_config["endpoint"] if ai_config else None,
|
|
224
|
+
api_key_env=None,
|
|
225
|
+
ai_config=ai_config,
|
|
136
226
|
)
|
|
137
227
|
|
|
138
228
|
|
|
@@ -156,7 +246,7 @@ def _build_arg_parser() -> argparse.ArgumentParser:
|
|
|
156
246
|
)
|
|
157
247
|
parser.add_argument(
|
|
158
248
|
"--model", default="qwen3:14b",
|
|
159
|
-
help="
|
|
249
|
+
help="AI model for summaries (default: qwen3:14b)",
|
|
160
250
|
)
|
|
161
251
|
parser.add_argument(
|
|
162
252
|
"--no-ai", action="store_true",
|
|
@@ -178,9 +268,71 @@ def _build_arg_parser() -> argparse.ArgumentParser:
|
|
|
178
268
|
"--gui", action="store_true",
|
|
179
269
|
help="Open GUI launcher",
|
|
180
270
|
)
|
|
271
|
+
parser.add_argument(
|
|
272
|
+
"--cloud",
|
|
273
|
+
choices=["openai", "openrouter", "groq", "custom"],
|
|
274
|
+
default=None,
|
|
275
|
+
help="Use cloud AI provider instead of Ollama",
|
|
276
|
+
)
|
|
277
|
+
parser.add_argument(
|
|
278
|
+
"--endpoint", default=None,
|
|
279
|
+
help="Custom endpoint URL (required with --cloud custom)",
|
|
280
|
+
)
|
|
281
|
+
parser.add_argument(
|
|
282
|
+
"--api-key-env", default=None,
|
|
283
|
+
help="Override environment variable name for the API key",
|
|
284
|
+
)
|
|
181
285
|
return parser
|
|
182
286
|
|
|
183
287
|
|
|
288
|
+
def _build_ai_config(args: argparse.Namespace) -> dict | None:
|
|
289
|
+
"""Build an ai_config dict from parsed CLI args, or None for ollama."""
|
|
290
|
+
if args.cloud is None:
|
|
291
|
+
return None
|
|
292
|
+
|
|
293
|
+
provider = args.cloud
|
|
294
|
+
info = CLOUD_PROVIDERS[provider]
|
|
295
|
+
|
|
296
|
+
# Endpoint
|
|
297
|
+
if provider == "custom":
|
|
298
|
+
if not args.endpoint:
|
|
299
|
+
print(
|
|
300
|
+
"Error: --endpoint is required with --cloud custom",
|
|
301
|
+
file=sys.stderr,
|
|
302
|
+
)
|
|
303
|
+
sys.exit(1)
|
|
304
|
+
endpoint = args.endpoint
|
|
305
|
+
else:
|
|
306
|
+
endpoint = args.endpoint or info["endpoint"]
|
|
307
|
+
|
|
308
|
+
# Validate endpoint
|
|
309
|
+
from codedocent.cloud_ai import _validate_endpoint # pylint: disable=import-outside-toplevel # noqa: E501
|
|
310
|
+
try:
|
|
311
|
+
_validate_endpoint(endpoint)
|
|
312
|
+
except ValueError:
|
|
313
|
+
print("Error: Invalid endpoint URL", file=sys.stderr)
|
|
314
|
+
sys.exit(1)
|
|
315
|
+
|
|
316
|
+
# API key
|
|
317
|
+
env_var = args.api_key_env or info["env_var"]
|
|
318
|
+
api_key = os.environ.get(env_var, "")
|
|
319
|
+
if not api_key:
|
|
320
|
+
print(
|
|
321
|
+
f"Error: API key not found. Set it with:\n"
|
|
322
|
+
f" export {env_var}=your-key-here",
|
|
323
|
+
file=sys.stderr,
|
|
324
|
+
)
|
|
325
|
+
sys.exit(1)
|
|
326
|
+
|
|
327
|
+
return {
|
|
328
|
+
"backend": "cloud",
|
|
329
|
+
"provider": provider,
|
|
330
|
+
"endpoint": endpoint,
|
|
331
|
+
"api_key": _MaskedSecret(api_key),
|
|
332
|
+
"model": args.model,
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
|
|
184
336
|
def _run_text_mode(tree: CodeNode) -> None:
|
|
185
337
|
"""Text mode: quality score only, print tree."""
|
|
186
338
|
from codedocent.analyzer import analyze_no_ai # pylint: disable=import-outside-toplevel # noqa: E501
|
|
@@ -201,18 +353,20 @@ def _run_no_ai_mode(tree: CodeNode, output: str) -> None:
|
|
|
201
353
|
|
|
202
354
|
def _run_full_mode(
|
|
203
355
|
tree: CodeNode, model: str, workers: int, output: str,
|
|
356
|
+
ai_config: dict | None = None,
|
|
204
357
|
) -> None:
|
|
205
358
|
"""Full mode: upfront AI analysis, static HTML."""
|
|
206
359
|
from codedocent.analyzer import analyze # pylint: disable=import-outside-toplevel # noqa: E501
|
|
207
360
|
from codedocent.renderer import render # pylint: disable=import-outside-toplevel # noqa: E501
|
|
208
361
|
|
|
209
|
-
analyze(tree, model=model, workers=workers)
|
|
362
|
+
analyze(tree, model=model, workers=workers, ai_config=ai_config)
|
|
210
363
|
render(tree, output)
|
|
211
364
|
print(f"HTML output written to {output}")
|
|
212
365
|
|
|
213
366
|
|
|
214
367
|
def _run_interactive_mode(
|
|
215
368
|
tree: CodeNode, model: str, port: int | None,
|
|
369
|
+
ai_config: dict | None = None,
|
|
216
370
|
) -> None:
|
|
217
371
|
"""Default lazy mode: interactive server."""
|
|
218
372
|
from codedocent.analyzer import analyze_no_ai, assign_node_ids # pylint: disable=import-outside-toplevel # noqa: E501
|
|
@@ -220,7 +374,8 @@ def _run_interactive_mode(
|
|
|
220
374
|
|
|
221
375
|
analyze_no_ai(tree)
|
|
222
376
|
node_lookup = assign_node_ids(tree)
|
|
223
|
-
start_server(tree, node_lookup, model=model, port=port
|
|
377
|
+
start_server(tree, node_lookup, model=model, port=port,
|
|
378
|
+
ai_config=ai_config)
|
|
224
379
|
|
|
225
380
|
|
|
226
381
|
def main() -> None:
|
|
@@ -236,6 +391,9 @@ def main() -> None:
|
|
|
236
391
|
|
|
237
392
|
if args.path is None:
|
|
238
393
|
args = _run_wizard()
|
|
394
|
+
ai_config = getattr(args, "ai_config", None)
|
|
395
|
+
else:
|
|
396
|
+
ai_config = _build_ai_config(args)
|
|
239
397
|
|
|
240
398
|
scanned = scan_directory(args.path)
|
|
241
399
|
tree = parse_directory(scanned, root=args.path)
|
|
@@ -245,9 +403,11 @@ def main() -> None:
|
|
|
245
403
|
elif args.no_ai:
|
|
246
404
|
_run_no_ai_mode(tree, args.output)
|
|
247
405
|
elif args.full:
|
|
248
|
-
_run_full_mode(tree, args.model, args.workers, args.output
|
|
406
|
+
_run_full_mode(tree, args.model, args.workers, args.output,
|
|
407
|
+
ai_config=ai_config)
|
|
249
408
|
else:
|
|
250
|
-
_run_interactive_mode(tree, args.model, args.port
|
|
409
|
+
_run_interactive_mode(tree, args.model, args.port,
|
|
410
|
+
ai_config=ai_config)
|
|
251
411
|
|
|
252
412
|
|
|
253
413
|
if __name__ == "__main__":
|