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