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.
Files changed (39) hide show
  1. codedocent-0.5.0/PKG-INFO +76 -0
  2. codedocent-0.5.0/README.md +66 -0
  3. {codedocent-0.3.0 → codedocent-0.5.0}/codedocent/analyzer.py +82 -15
  4. {codedocent-0.3.0 → codedocent-0.5.0}/codedocent/cli.py +177 -17
  5. codedocent-0.5.0/codedocent/cloud_ai.py +208 -0
  6. codedocent-0.5.0/codedocent/gui.py +294 -0
  7. {codedocent-0.3.0 → codedocent-0.5.0}/codedocent/quality.py +6 -54
  8. {codedocent-0.3.0 → codedocent-0.5.0}/codedocent/server.py +40 -5
  9. {codedocent-0.3.0 → codedocent-0.5.0}/codedocent/templates/interactive.html +9 -11
  10. codedocent-0.5.0/codedocent.egg-info/PKG-INFO +76 -0
  11. {codedocent-0.3.0 → codedocent-0.5.0}/codedocent.egg-info/SOURCES.txt +2 -0
  12. {codedocent-0.3.0 → codedocent-0.5.0}/pyproject.toml +2 -2
  13. {codedocent-0.3.0 → codedocent-0.5.0}/tests/test_analyzer.py +187 -141
  14. {codedocent-0.3.0 → codedocent-0.5.0}/tests/test_cli.py +170 -9
  15. codedocent-0.5.0/tests/test_cloud_ai.py +266 -0
  16. {codedocent-0.3.0 → codedocent-0.5.0}/tests/test_gui.py +35 -0
  17. codedocent-0.3.0/PKG-INFO +0 -58
  18. codedocent-0.3.0/README.md +0 -40
  19. codedocent-0.3.0/codedocent/gui.py +0 -158
  20. codedocent-0.3.0/codedocent.egg-info/PKG-INFO +0 -58
  21. {codedocent-0.3.0 → codedocent-0.5.0}/LICENSE +0 -0
  22. {codedocent-0.3.0 → codedocent-0.5.0}/codedocent/__init__.py +0 -0
  23. {codedocent-0.3.0 → codedocent-0.5.0}/codedocent/__main__.py +0 -0
  24. {codedocent-0.3.0 → codedocent-0.5.0}/codedocent/editor.py +0 -0
  25. {codedocent-0.3.0 → codedocent-0.5.0}/codedocent/ollama_utils.py +0 -0
  26. {codedocent-0.3.0 → codedocent-0.5.0}/codedocent/parser.py +0 -0
  27. {codedocent-0.3.0 → codedocent-0.5.0}/codedocent/renderer.py +0 -0
  28. {codedocent-0.3.0 → codedocent-0.5.0}/codedocent/scanner.py +0 -0
  29. {codedocent-0.3.0 → codedocent-0.5.0}/codedocent/templates/base.html +0 -0
  30. {codedocent-0.3.0 → codedocent-0.5.0}/codedocent.egg-info/dependency_links.txt +0 -0
  31. {codedocent-0.3.0 → codedocent-0.5.0}/codedocent.egg-info/entry_points.txt +0 -0
  32. {codedocent-0.3.0 → codedocent-0.5.0}/codedocent.egg-info/requires.txt +4 -4
  33. {codedocent-0.3.0 → codedocent-0.5.0}/codedocent.egg-info/top_level.txt +0 -0
  34. {codedocent-0.3.0 → codedocent-0.5.0}/setup.cfg +0 -0
  35. {codedocent-0.3.0 → codedocent-0.5.0}/tests/test_editor.py +0 -0
  36. {codedocent-0.3.0 → codedocent-0.5.0}/tests/test_parser.py +0 -0
  37. {codedocent-0.3.0 → codedocent-0.5.0}/tests/test_renderer.py +0 -0
  38. {codedocent-0.3.0 → codedocent-0.5.0}/tests/test_scanner.py +0 -0
  39. {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(node: CodeNode, model: str, cache_dir: str) -> None:
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
- if ollama is None:
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
- if cache.get("model") != model:
267
- cache = {"version": 1, "model": model, "entries": {}}
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
- node.summary = f"Summary generation failed: {e}"
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(root: CodeNode, model: str) -> tuple[str, dict]:
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
- if cache.get("model") != model:
434
- cache = {"version": 1, "model": model, "entries": {}}
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
- _require_ollama()
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(all_nodes, model, cache, workers)
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
- # --- Ollama check ---
168
+ # --- Backend choice ---
98
169
  model = "qwen3:14b"
99
170
  no_ai = False
100
-
101
- print("Checking for Ollama...", end=" ", flush=True)
102
- if _check_ollama():
103
- print("found!")
104
- models = _fetch_ollama_models()
105
- if models:
106
- model = _pick_model(models)
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("No models found.")
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="Ollama model for AI summaries (default: qwen3:14b)",
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__":