codedd-cli 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. codedd_cli/__init__.py +3 -0
  2. codedd_cli/__main__.py +19 -0
  3. codedd_cli/api/__init__.py +4 -0
  4. codedd_cli/api/client.py +118 -0
  5. codedd_cli/api/endpoints.py +44 -0
  6. codedd_cli/api/exceptions.py +25 -0
  7. codedd_cli/auditor/__init__.py +6 -0
  8. codedd_cli/auditor/architecture_analyzer.py +1241 -0
  9. codedd_cli/auditor/architecture_prompts.py +171 -0
  10. codedd_cli/auditor/complexity_analyzer.py +942 -0
  11. codedd_cli/auditor/dependency_scanner.py +2478 -0
  12. codedd_cli/auditor/file_auditor.py +572 -0
  13. codedd_cli/auditor/git_stats_collector.py +332 -0
  14. codedd_cli/auditor/response_parser.py +487 -0
  15. codedd_cli/auditor/vulnerability_validator.py +324 -0
  16. codedd_cli/auth/__init__.py +4 -0
  17. codedd_cli/auth/session.py +41 -0
  18. codedd_cli/auth/token_manager.py +87 -0
  19. codedd_cli/cli.py +69 -0
  20. codedd_cli/commands/__init__.py +1 -0
  21. codedd_cli/commands/audit_cmd.py +1877 -0
  22. codedd_cli/commands/audits_cmd.py +273 -0
  23. codedd_cli/commands/auth_cmd.py +230 -0
  24. codedd_cli/commands/config_cmd.py +454 -0
  25. codedd_cli/commands/scope_cmd.py +967 -0
  26. codedd_cli/config/__init__.py +4 -0
  27. codedd_cli/config/constants.py +22 -0
  28. codedd_cli/config/settings.py +369 -0
  29. codedd_cli/llm/__init__.py +1 -0
  30. codedd_cli/llm/key_manager.py +271 -0
  31. codedd_cli/models/__init__.py +5 -0
  32. codedd_cli/models/account.py +13 -0
  33. codedd_cli/models/audit.py +32 -0
  34. codedd_cli/models/local_directory.py +26 -0
  35. codedd_cli/scanner/__init__.py +18 -0
  36. codedd_cli/scanner/file_classifier.py +247 -0
  37. codedd_cli/scanner/file_walker.py +207 -0
  38. codedd_cli/scanner/line_counter.py +75 -0
  39. codedd_cli/utils/__init__.py +1 -0
  40. codedd_cli/utils/directory_validator.py +179 -0
  41. codedd_cli/utils/display.py +493 -0
  42. codedd_cli/utils/payload_inspector.py +180 -0
  43. codedd_cli/utils/security.py +14 -0
  44. codedd_cli/utils/validators.py +37 -0
  45. codedd_cli-0.1.0.dist-info/METADATA +276 -0
  46. codedd_cli-0.1.0.dist-info/RECORD +49 -0
  47. codedd_cli-0.1.0.dist-info/WHEEL +4 -0
  48. codedd_cli-0.1.0.dist-info/entry_points.txt +3 -0
  49. codedd_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,454 @@
1
+ """
2
+ ``codedd config`` sub-commands.
3
+
4
+ General configuration:
5
+ show – display the current CLI configuration
6
+ set – update a configuration value (e.g. ``api_url``)
7
+
8
+ LLM key management:
9
+ set-key – store an Anthropic or OpenAI API key in the OS keychain
10
+ show-keys – show which LLM providers have keys configured
11
+ remove-key – remove a stored LLM API key
12
+ provider – set the preferred LLM provider (anthropic / openai / both)
13
+ """
14
+
15
+ import getpass
16
+ from typing import Optional
17
+
18
+ import typer
19
+ from rich.console import Console
20
+ from rich.panel import Panel
21
+ from rich.prompt import Confirm, IntPrompt
22
+ from rich.table import Table
23
+
24
+ from codedd_cli.config.settings import ConfigManager
25
+ from codedd_cli.llm.key_manager import (
26
+ PROVIDER_MODELS,
27
+ VALID_PROVIDERS,
28
+ LLMKeyManager,
29
+ )
30
+ from codedd_cli.utils.display import SYMBOL_FAIL, SYMBOL_INFO, SYMBOL_OK, SYMBOL_WARN
31
+
32
+ console = Console()
33
+ config_app = typer.Typer(no_args_is_help=True)
34
+
35
+ # Keys that users are allowed to modify
36
+ _WRITABLE_KEYS = {
37
+ "api_url": ("server", "api_url"),
38
+ }
39
+
40
+
41
+ @config_app.command("show")
42
+ def show() -> None:
43
+ """
44
+ Display the current CLI configuration.
45
+ """
46
+ cfg = ConfigManager()
47
+
48
+ lines = [
49
+ f"[bold]Config file:[/bold] {cfg.config_path}",
50
+ "",
51
+ "[bold underline]Server[/bold underline]",
52
+ f" api_url = {cfg.api_url}",
53
+ "",
54
+ "[bold underline]Session[/bold underline]",
55
+ f" authenticated = {'[green]yes[/green]' if cfg.is_authenticated else '[red]no[/red]'}",
56
+ f" account_name = {cfg.account_name or '—'}",
57
+ f" account_uuid = {cfg.account_uuid[:8] + '…' if cfg.account_uuid else '—'}",
58
+ f" token_hint = {cfg.token_hint or '—'}",
59
+ "",
60
+ "[bold underline]Active Audit[/bold underline]",
61
+ f" audit_uuid = {cfg.active_audit_uuid[:8] + '…' if cfg.active_audit_uuid else '—'}",
62
+ f" audit_type = {cfg.active_audit_type or '—'}",
63
+ f" audit_name = {cfg.active_audit_name or '—'}",
64
+ "",
65
+ "[bold underline]LLM[/bold underline]",
66
+ f" provider = {cfg.llm_provider}",
67
+ f" concurrency = {cfg.llm_concurrency}",
68
+ ]
69
+
70
+ console.print(Panel("\n".join(lines), title="CodeDD CLI Configuration", expand=False))
71
+
72
+
73
+ @config_app.command("set")
74
+ def set_value(
75
+ key: str = typer.Argument(help="Configuration key to set (e.g. api_url)."),
76
+ value: str = typer.Argument(help="Value to assign to the key."),
77
+ ) -> None:
78
+ """
79
+ Update a configuration value.
80
+
81
+ Currently supported keys: ``api_url``
82
+
83
+ Examples:
84
+ codedd config set api_url http://localhost:8000/django_codedd
85
+ codedd config set api_url https://api.codedd.ai/django_codedd
86
+ """
87
+ if key not in _WRITABLE_KEYS:
88
+ console.print(
89
+ f"[red]Unknown key:[/red] {key}\n"
90
+ f"Supported keys: {', '.join(_WRITABLE_KEYS.keys())}"
91
+ )
92
+ raise typer.Exit(code=1)
93
+
94
+ # Validate api_url format
95
+ if key == "api_url":
96
+ value = value.rstrip("/")
97
+ if not (value.startswith("http://") or value.startswith("https://")):
98
+ console.print(
99
+ "[red]Invalid URL format.[/red] "
100
+ "URL must start with http:// or https://\n"
101
+ "Example: [bold]http://localhost:8000/django_codedd[/bold]"
102
+ )
103
+ raise typer.Exit(code=1)
104
+
105
+ # Warn if /django_codedd path is missing
106
+ if "/django_codedd" not in value:
107
+ console.print(
108
+ "[yellow]Warning:[/yellow] URL doesn't include '/django_codedd' path.\n"
109
+ "Did you mean: [bold]" + value + "/django_codedd[/bold] ?\n"
110
+ "[dim]Continuing with the provided URL...[/dim]"
111
+ )
112
+
113
+ section, config_key = _WRITABLE_KEYS[key]
114
+ cfg = ConfigManager()
115
+ cfg.set(section, config_key, value)
116
+
117
+ console.print(f"[green]{SYMBOL_OK}[/green] Set [bold]{key}[/bold] = {value}")
118
+
119
+ if key == "api_url":
120
+ console.print(
121
+ "[dim]Tip:[/dim] Use [bold]codedd config show[/bold] to verify your configuration."
122
+ )
123
+
124
+
125
+ # ---------------------------------------------------------------------------
126
+ # codedd config set-key
127
+ # ---------------------------------------------------------------------------
128
+
129
+ @config_app.command("set-key")
130
+ def set_key(
131
+ provider: Optional[str] = typer.Argument(
132
+ None,
133
+ help="LLM provider: 'anthropic' or 'openai'. Prompted if omitted.",
134
+ ),
135
+ skip_validation: bool = typer.Option(
136
+ False,
137
+ "--skip-validation",
138
+ help="Store the key without making a validation API call.",
139
+ ),
140
+ ) -> None:
141
+ """
142
+ Store an LLM API key securely in the OS keychain.
143
+
144
+ The key is validated with a lightweight API call before storage
145
+ (unless ``--skip-validation`` is passed). If validation fails the
146
+ user is asked whether to store the key anyway.
147
+
148
+ Examples::
149
+
150
+ codedd config set-key anthropic
151
+ codedd config set-key openai
152
+ codedd config set-key # interactive provider prompt
153
+ """
154
+ # -- Provider selection --
155
+ if provider is None:
156
+ provider = _prompt_provider_choice()
157
+
158
+ provider = provider.lower().strip()
159
+ if provider not in ("anthropic", "openai"):
160
+ console.print(
161
+ f"[red]{SYMBOL_FAIL}[/red] Unknown provider [bold]{provider}[/bold]. "
162
+ "Use [bold]anthropic[/bold] or [bold]openai[/bold]."
163
+ )
164
+ raise typer.Exit(code=1)
165
+
166
+ model = PROVIDER_MODELS.get(provider, "")
167
+ console.print(
168
+ f"\n[bold]Provider:[/bold] {provider} "
169
+ f"[dim](model: {model})[/dim]\n"
170
+ )
171
+
172
+ # -- If key already exists, show preview and prompt Keep / Update / Delete --
173
+ existing_key = LLMKeyManager.retrieve_key(provider)
174
+ if existing_key:
175
+ preview = LLMKeyManager.mask_key_preview(existing_key)
176
+ console.print(
177
+ f" [dim]A key for [bold]{provider}[/bold] is already stored:[/dim] "
178
+ f"[bold]{preview}[/bold]\n"
179
+ )
180
+ choice = _prompt_keep_update_delete()
181
+ if choice == 1:
182
+ console.print(f" [green]{SYMBOL_OK}[/green] Keeping existing {provider} key.")
183
+ return
184
+ if choice == 3:
185
+ LLMKeyManager.remove_key(provider)
186
+ console.print(f" [green]{SYMBOL_OK}[/green] {provider} API key removed.")
187
+ return
188
+ # choice == 2: Update — fall through to paste/validate/store
189
+
190
+ # -- Key input (masked) --
191
+ try:
192
+ api_key = getpass.getpass(f" Paste your {provider} API key: ").strip()
193
+ except (EOFError, KeyboardInterrupt):
194
+ console.print("\n[dim]Cancelled.[/dim]")
195
+ raise typer.Exit()
196
+
197
+ if not api_key:
198
+ console.print(f"[red]{SYMBOL_FAIL}[/red] No key entered.")
199
+ raise typer.Exit(code=1)
200
+
201
+ # -- Validation --
202
+ if not skip_validation:
203
+ console.print(f"\n[dim]Validating key with {provider} API...[/dim]")
204
+ is_valid, message = LLMKeyManager.validate_key(provider, api_key)
205
+
206
+ if is_valid:
207
+ console.print(f" [green]{SYMBOL_OK}[/green] {message}")
208
+ else:
209
+ console.print(f" [yellow]{SYMBOL_WARN}[/yellow] {message}")
210
+ if not Confirm.ask(" Store the key anyway?", default=False):
211
+ console.print("[dim]Key not stored.[/dim]")
212
+ raise typer.Exit()
213
+
214
+ # -- Store --
215
+ try:
216
+ LLMKeyManager.store_key(provider, api_key)
217
+ except Exception as exc:
218
+ console.print(
219
+ f"[red]{SYMBOL_FAIL}[/red] Failed to store key in OS keychain: {exc}"
220
+ )
221
+ raise typer.Exit(code=1)
222
+
223
+ masked = LLMKeyManager.mask_key(api_key)
224
+ console.print(
225
+ f"\n[green]{SYMBOL_OK}[/green] {provider} API key stored "
226
+ f"[dim]({masked})[/dim]"
227
+ )
228
+
229
+ # -- Auto-set provider preference if this is the first key --
230
+ cfg = ConfigManager()
231
+ configured = LLMKeyManager.get_configured_providers()
232
+ if len(configured) == 1:
233
+ cfg.llm_provider = configured[0]
234
+ cfg.save()
235
+ console.print(
236
+ f"[dim]{SYMBOL_INFO}[/dim] LLM provider set to "
237
+ f"[bold]{configured[0]}[/bold]."
238
+ )
239
+ elif len(configured) == 2 and cfg.llm_provider not in VALID_PROVIDERS:
240
+ cfg.llm_provider = "both"
241
+ cfg.save()
242
+ console.print(
243
+ f"[dim]{SYMBOL_INFO}[/dim] Both providers configured — "
244
+ "using Anthropic (primary) + OpenAI (fallback)."
245
+ )
246
+
247
+
248
+ # ---------------------------------------------------------------------------
249
+ # codedd config show-keys
250
+ # ---------------------------------------------------------------------------
251
+
252
+ @config_app.command("show-keys")
253
+ def show_keys() -> None:
254
+ """
255
+ Show which LLM providers have API keys stored in the OS keychain.
256
+
257
+ Keys are displayed in masked form (first 8 characters only).
258
+ """
259
+ cfg = ConfigManager()
260
+ preference = cfg.llm_provider
261
+
262
+ table = Table(
263
+ title="LLM API Keys",
264
+ show_header=True,
265
+ padding=(0, 1),
266
+ )
267
+ table.add_column("Provider", style="bold", width=12)
268
+ table.add_column("Key", width=20)
269
+ table.add_column("Model", width=24)
270
+ table.add_column("Status", width=14)
271
+
272
+ for provider in ("anthropic", "openai"):
273
+ key = LLMKeyManager.retrieve_key(provider)
274
+ if key:
275
+ masked = LLMKeyManager.mask_key(key)
276
+ model = PROVIDER_MODELS.get(provider, "")
277
+ # Determine if this provider is active based on preference
278
+ if preference == "both" or preference == provider:
279
+ status = "[green]active[/green]"
280
+ else:
281
+ status = "[dim]stored[/dim]"
282
+ table.add_row(provider, masked, model, status)
283
+ else:
284
+ table.add_row(provider, "[dim]not set[/dim]", "", "[dim]—[/dim]")
285
+
286
+ console.print(table)
287
+ console.print(
288
+ f"\n[dim]Provider preference:[/dim] [bold]{preference}[/bold]"
289
+ )
290
+ console.print(
291
+ "[dim]Use [bold]codedd config set-key[/bold] to add a key, "
292
+ "[bold]codedd config provider[/bold] to change preference.[/dim]"
293
+ )
294
+
295
+
296
+ # ---------------------------------------------------------------------------
297
+ # codedd config remove-key
298
+ # ---------------------------------------------------------------------------
299
+
300
+ @config_app.command("remove-key")
301
+ def remove_key(
302
+ provider: str = typer.Argument(
303
+ help="LLM provider whose key to remove: 'anthropic' or 'openai'.",
304
+ ),
305
+ ) -> None:
306
+ """
307
+ Remove an LLM API key from the OS keychain.
308
+ """
309
+ provider = provider.lower().strip()
310
+ if provider not in ("anthropic", "openai"):
311
+ console.print(
312
+ f"[red]{SYMBOL_FAIL}[/red] Unknown provider [bold]{provider}[/bold]. "
313
+ "Use [bold]anthropic[/bold] or [bold]openai[/bold]."
314
+ )
315
+ raise typer.Exit(code=1)
316
+
317
+ if not LLMKeyManager.has_key(provider):
318
+ console.print(
319
+ f"[dim]{SYMBOL_INFO}[/dim] No {provider} key is currently stored."
320
+ )
321
+ raise typer.Exit()
322
+
323
+ if not Confirm.ask(
324
+ f"Remove the stored [bold]{provider}[/bold] API key?", default=False
325
+ ):
326
+ console.print("[dim]Cancelled.[/dim]")
327
+ raise typer.Exit()
328
+
329
+ removed = LLMKeyManager.remove_key(provider)
330
+ if removed:
331
+ console.print(
332
+ f"[green]{SYMBOL_OK}[/green] {provider} API key removed."
333
+ )
334
+ else:
335
+ console.print(
336
+ f"[red]{SYMBOL_FAIL}[/red] Failed to remove {provider} key."
337
+ )
338
+
339
+
340
+ # ---------------------------------------------------------------------------
341
+ # codedd config provider
342
+ # ---------------------------------------------------------------------------
343
+
344
+ @config_app.command("provider")
345
+ def set_provider(
346
+ value: str = typer.Argument(
347
+ help="Preferred LLM provider: 'anthropic', 'openai', or 'both'.",
348
+ ),
349
+ ) -> None:
350
+ """
351
+ Set the preferred LLM provider for audits.
352
+
353
+ - ``anthropic`` — use Anthropic only (claude-sonnet-4-6)
354
+ - ``openai`` — use OpenAI only (gpt-5.2)
355
+ - ``both`` — Anthropic primary, OpenAI fallback (recommended)
356
+ """
357
+ value = value.lower().strip()
358
+ if value not in VALID_PROVIDERS:
359
+ console.print(
360
+ f"[red]{SYMBOL_FAIL}[/red] Invalid provider [bold]{value}[/bold]. "
361
+ f"Choose one of: {', '.join(VALID_PROVIDERS)}"
362
+ )
363
+ raise typer.Exit(code=1)
364
+
365
+ # Warn if the selected provider has no key stored
366
+ if value in ("anthropic", "openai") and not LLMKeyManager.has_key(value):
367
+ console.print(
368
+ f"[yellow]{SYMBOL_WARN}[/yellow] No {value} API key stored. "
369
+ f"Run [bold cyan]codedd config set-key {value}[/bold cyan] first."
370
+ )
371
+ elif value == "both":
372
+ missing = [p for p in ("anthropic", "openai") if not LLMKeyManager.has_key(p)]
373
+ if missing:
374
+ console.print(
375
+ f"[yellow]{SYMBOL_WARN}[/yellow] Missing key(s) for: "
376
+ f"{', '.join(missing)}. "
377
+ "Add them with [bold cyan]codedd config set-key[/bold cyan]."
378
+ )
379
+
380
+ cfg = ConfigManager()
381
+ cfg.llm_provider = value
382
+ cfg.save()
383
+ console.print(
384
+ f"[green]{SYMBOL_OK}[/green] LLM provider preference set to "
385
+ f"[bold]{value}[/bold]."
386
+ )
387
+
388
+
389
+ # ---------------------------------------------------------------------------
390
+ # codedd config concurrency
391
+ # ---------------------------------------------------------------------------
392
+
393
+ @config_app.command("concurrency")
394
+ def set_concurrency(
395
+ value: int = typer.Argument(
396
+ help="Max parallel LLM calls during file auditing (1–32).",
397
+ ),
398
+ ) -> None:
399
+ """
400
+ Set the maximum number of concurrent LLM API calls during audits.
401
+
402
+ Higher values speed up audits but may trigger rate-limit errors
403
+ depending on your API tier:
404
+
405
+ \b
406
+ 1–2 Free / starter tier (conservative)
407
+ 4 Default — works well for most paid tiers
408
+ 8–16 High-throughput tiers (Anthropic Scale, OpenAI Tier 4+)
409
+ 32 Maximum — only if your provider contract allows it
410
+ """
411
+ if value < 1 or value > 32:
412
+ console.print(
413
+ f"[red]{SYMBOL_FAIL}[/red] Value must be between 1 and 32 (got {value})."
414
+ )
415
+ raise typer.Exit(code=1)
416
+
417
+ cfg = ConfigManager()
418
+ cfg.llm_concurrency = value
419
+ cfg.save()
420
+ console.print(
421
+ f"[green]{SYMBOL_OK}[/green] LLM concurrency set to "
422
+ f"[bold]{value}[/bold] parallel calls."
423
+ )
424
+
425
+
426
+ # ---------------------------------------------------------------------------
427
+ # Internal helpers
428
+ # ---------------------------------------------------------------------------
429
+
430
+ def _prompt_keep_update_delete() -> int:
431
+ """Prompt user to Keep (1), Update (2), or Delete (3) the existing key."""
432
+ console.print(
433
+ " [bold cyan]1[/bold cyan] Keep existing key\n"
434
+ " [bold cyan]2[/bold cyan] Update with a new key\n"
435
+ " [bold cyan]3[/bold cyan] Delete stored key\n"
436
+ )
437
+ return IntPrompt.ask(" Enter 1, 2, or 3", choices=["1", "2", "3"], default=1)
438
+
439
+
440
+ def _prompt_provider_choice() -> str:
441
+ """Interactive prompt to choose a provider when none was supplied."""
442
+ console.print(
443
+ "\n[bold]Select LLM provider:[/bold]\n"
444
+ "[dim]CodeDD uses LLMs to analyze your source code during the audit. "
445
+ "Your API key is stored in your system keychain (or equivalent) and is "
446
+ "never written to config files.[/dim]\n"
447
+ )
448
+ console.print(" [bold cyan]1[/bold cyan] Anthropic [dim](claude-sonnet-4-6)[/dim]")
449
+ console.print(" [bold cyan]2[/bold cyan] OpenAI [dim](gpt-5.2)[/dim]")
450
+ console.print()
451
+ choice = typer.prompt(" Enter 1 or 2", type=int, default=1)
452
+ if choice == 2:
453
+ return "openai"
454
+ return "anthropic"