codedd-cli 0.1.4__tar.gz → 0.1.5__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 (57) hide show
  1. {codedd_cli-0.1.4 → codedd_cli-0.1.5}/PKG-INFO +1 -1
  2. {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/__init__.py +1 -1
  3. {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/api/client.py +2 -2
  4. codedd_cli-0.1.5/codedd_cli/auditor/audit_checkpoint.py +179 -0
  5. {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/auditor/file_auditor.py +29 -5
  6. {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/auditor/sub_audit_runner.py +267 -122
  7. {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/auditor/vulnerability_validator.py +40 -6
  8. {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/commands/audit_cmd.py +687 -7
  9. {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/commands/audits_cmd.py +12 -1
  10. {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/commands/auth_cmd.py +9 -12
  11. {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/commands/config_cmd.py +21 -20
  12. {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/commands/fix_cmd.py +466 -9
  13. {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/commands/scope_cmd.py +18 -4
  14. {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/config/constants.py +11 -0
  15. {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/config/settings.py +23 -2
  16. {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/llm/key_manager.py +4 -1
  17. {codedd_cli-0.1.4 → codedd_cli-0.1.5}/pyproject.toml +1 -1
  18. {codedd_cli-0.1.4 → codedd_cli-0.1.5}/LICENSE +0 -0
  19. {codedd_cli-0.1.4 → codedd_cli-0.1.5}/README.md +0 -0
  20. {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/__main__.py +0 -0
  21. {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/api/__init__.py +0 -0
  22. {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/api/endpoints.py +0 -0
  23. {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/api/exceptions.py +0 -0
  24. {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/auditor/__init__.py +0 -0
  25. {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/auditor/archetype_classifier.py +0 -0
  26. {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/auditor/architecture_analyzer.py +0 -0
  27. {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/auditor/architecture_prompts.py +0 -0
  28. {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/auditor/commit_classifier.py +0 -0
  29. {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/auditor/complexity_analyzer.py +0 -0
  30. {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/auditor/dependency_scanner.py +0 -0
  31. {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/auditor/enhanced_architecture_detectors.py +0 -0
  32. {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/auditor/git_stats_collector.py +0 -0
  33. {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/auditor/response_parser.py +0 -0
  34. {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/auditor/tier_definitions.py +0 -0
  35. {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/auth/__init__.py +0 -0
  36. {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/auth/session.py +0 -0
  37. {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/auth/token_manager.py +0 -0
  38. {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/cli.py +0 -0
  39. {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/commands/__init__.py +0 -0
  40. {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/commands/ai_docs_cmd.py +0 -0
  41. {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/config/__init__.py +0 -0
  42. {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/config/fix_session.py +0 -0
  43. {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/llm/__init__.py +0 -0
  44. {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/models/__init__.py +0 -0
  45. {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/models/account.py +0 -0
  46. {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/models/audit.py +0 -0
  47. {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/models/local_directory.py +0 -0
  48. {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/scanner/__init__.py +0 -0
  49. {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/scanner/file_classifier.py +0 -0
  50. {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/scanner/file_walker.py +0 -0
  51. {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/scanner/line_counter.py +0 -0
  52. {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/utils/__init__.py +0 -0
  53. {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/utils/directory_validator.py +0 -0
  54. {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/utils/display.py +0 -0
  55. {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/utils/payload_inspector.py +0 -0
  56. {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/utils/security.py +0 -0
  57. {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/utils/validators.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codedd-cli
3
- Version: 0.1.4
3
+ Version: 0.1.5
4
4
  Summary: CLI tool for CodeDD — run code audits from your terminal
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -1,3 +1,3 @@
1
1
  """CodeDD CLI — run code audits from your terminal."""
2
2
 
3
- __version__ = "0.1.4"
3
+ __version__ = "0.1.5"
@@ -37,10 +37,10 @@ class CodeDDClient:
37
37
  data = resp.json()
38
38
  """
39
39
 
40
- def __init__(self, config: ConfigManager | None = None) -> None:
40
+ def __init__(self, config: ConfigManager | None = None, token: str | None = None) -> None:
41
41
  self._config = config or ConfigManager()
42
42
  self._base_url = self._config.api_url.rstrip("/")
43
- self._token = TokenManager.retrieve()
43
+ self._token = token if token is not None else TokenManager.retrieve()
44
44
 
45
45
  # For localhost HTTP, disable TLS verification (not applicable anyway)
46
46
  # For HTTPS, always verify certificates
@@ -0,0 +1,179 @@
1
+ """
2
+ Persistent checkpoint for resumable local audit sessions.
3
+
4
+ State is written to ``~/.codedd/audit_progress_{audit_uuid}.json`` after
5
+ every batch submission so an interrupted audit can resume where it left off.
6
+ Writes are atomic (write-then-rename) so a crash mid-save never corrupts the
7
+ file.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import logging
14
+ import os
15
+ from datetime import datetime, timezone
16
+ from pathlib import Path
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ _FORMAT_VERSION = 1
21
+ _FILENAME_PREFIX = "audit_progress_"
22
+
23
+
24
+ class AuditCheckpoint:
25
+ """Tracks per-sub-audit submission progress across CLI sessions."""
26
+
27
+ def __init__(self, audit_uuid: str, config_dir: Path) -> None:
28
+ self._audit_uuid = audit_uuid
29
+ self._path = config_dir / f"{_FILENAME_PREFIX}{audit_uuid}.json"
30
+ self._data: dict = self._load()
31
+
32
+ # ---- Internal persistence -----------------------------------------------
33
+
34
+ def _empty(self) -> dict:
35
+ now = datetime.now(timezone.utc).isoformat()
36
+ return {
37
+ "version": _FORMAT_VERSION,
38
+ "audit_uuid": self._audit_uuid,
39
+ "created_at": now,
40
+ "updated_at": now,
41
+ "sub_audits": {},
42
+ }
43
+
44
+ def _load(self) -> dict:
45
+ if not self._path.exists():
46
+ return self._empty()
47
+ try:
48
+ with open(self._path, encoding="utf-8") as fh:
49
+ data = json.load(fh)
50
+ if not isinstance(data, dict) or data.get("audit_uuid") != self._audit_uuid:
51
+ logger.warning("Checkpoint mismatch or corrupt — starting fresh.")
52
+ return self._empty()
53
+ return data
54
+ except Exception as exc:
55
+ logger.warning("Cannot read checkpoint (%s) — starting fresh.", exc)
56
+ return self._empty()
57
+
58
+ def _save(self) -> None:
59
+ self._data["updated_at"] = datetime.now(timezone.utc).isoformat()
60
+ tmp = self._path.with_suffix(".json.tmp")
61
+ try:
62
+ with open(tmp, "w", encoding="utf-8") as fh:
63
+ json.dump(self._data, fh, indent=2)
64
+ os.replace(tmp, self._path)
65
+ except Exception as exc:
66
+ logger.warning("Checkpoint save failed: %s", exc)
67
+ try:
68
+ tmp.unlink(missing_ok=True)
69
+ except Exception:
70
+ pass
71
+
72
+ def _sub(self, sub_uuid: str) -> dict:
73
+ subs = self._data.setdefault("sub_audits", {})
74
+ if sub_uuid not in subs:
75
+ subs[sub_uuid] = {"submitted_file_paths": [], "vuln_submitted_count": 0}
76
+ return subs[sub_uuid]
77
+
78
+ # ---- File audit phase ---------------------------------------------------
79
+
80
+ def get_submitted_files(self, sub_uuid: str) -> set[str]:
81
+ """Return the set of file_path strings already submitted for this sub-audit."""
82
+ return set(self._sub(sub_uuid).get("submitted_file_paths", []))
83
+
84
+ def mark_files_submitted(self, sub_uuid: str, file_paths: list[str]) -> None:
85
+ """Append newly submitted file paths and persist immediately."""
86
+ sub = self._sub(sub_uuid)
87
+ existing: set[str] = set(sub.get("submitted_file_paths", []))
88
+ added = False
89
+ for fp in file_paths:
90
+ if fp not in existing:
91
+ sub.setdefault("submitted_file_paths", []).append(fp)
92
+ existing.add(fp)
93
+ added = True
94
+ if added:
95
+ self._save()
96
+
97
+ # ---- Vulnerability validation phase ------------------------------------
98
+
99
+ def get_vuln_submitted_count(self, sub_uuid: str) -> int:
100
+ """Number of vuln-validation outcomes already submitted for this sub-audit."""
101
+ # Path-based tracking is preferred; fall back to legacy count field.
102
+ paths = self._sub(sub_uuid).get("vuln_validated_paths")
103
+ if paths is not None:
104
+ return len(paths)
105
+ return int(self._sub(sub_uuid).get("vuln_submitted_count", 0))
106
+
107
+ def mark_vulns_submitted(self, sub_uuid: str, total_count: int) -> None:
108
+ """Update the total count of submitted vuln validations and persist."""
109
+ self._sub(sub_uuid)["vuln_submitted_count"] = total_count
110
+ self._save()
111
+
112
+ def get_vuln_validated_paths(self, sub_uuid: str) -> set[str]:
113
+ """Return file_paths whose validation outcomes were already submitted."""
114
+ return set(self._sub(sub_uuid).get("vuln_validated_paths", []))
115
+
116
+ def mark_vuln_paths_submitted(self, sub_uuid: str, file_paths: list[str]) -> None:
117
+ """Append newly submitted vuln file_paths and persist immediately."""
118
+ sub = self._sub(sub_uuid)
119
+ existing: list[str] = sub.setdefault("vuln_validated_paths", [])
120
+ existing_set = set(existing)
121
+ added = False
122
+ for fp in file_paths:
123
+ if fp not in existing_set:
124
+ existing.append(fp)
125
+ existing_set.add(fp)
126
+ added = True
127
+ if added:
128
+ self._save()
129
+
130
+ def set_vuln_candidates(self, sub_uuid: str, candidates: list[dict]) -> None:
131
+ """Persist serialised validation candidates for recovery on resume."""
132
+ self._sub(sub_uuid)["vuln_candidates"] = candidates
133
+ self._save()
134
+
135
+ def get_vuln_candidates(self, sub_uuid: str) -> list[dict]:
136
+ """Return previously persisted validation candidates (empty if none)."""
137
+ return list(self._sub(sub_uuid).get("vuln_candidates", []))
138
+
139
+ # ---- Audit plan metadata ------------------------------------------------
140
+
141
+ def set_plan_file_count(self, count: int) -> None:
142
+ """Persist the total number of files in the audit plan."""
143
+ if count > 0:
144
+ self._data["plan_file_count"] = count
145
+ self._save()
146
+
147
+ def get_plan_file_count(self) -> int:
148
+ """Return the stored audit plan file count (0 if not yet set)."""
149
+ return int(self._data.get("plan_file_count", 0))
150
+
151
+ # ---- Introspection ------------------------------------------------------
152
+
153
+ def has_progress(self) -> bool:
154
+ """True if the checkpoint file exists and has at least one submitted file."""
155
+ if not self._path.exists():
156
+ return False
157
+ return self.total_submitted_files() > 0
158
+
159
+ def total_submitted_files(self) -> int:
160
+ return sum(
161
+ len(sub.get("submitted_file_paths", []))
162
+ for sub in self._data.get("sub_audits", {}).values()
163
+ )
164
+
165
+ def created_at(self) -> str | None:
166
+ return self._data.get("created_at")
167
+
168
+ @property
169
+ def path(self) -> Path:
170
+ return self._path
171
+
172
+ # ---- Lifecycle ----------------------------------------------------------
173
+
174
+ def clear(self) -> None:
175
+ """Delete checkpoint file after a successful, complete audit."""
176
+ try:
177
+ self._path.unlink(missing_ok=True)
178
+ except Exception as exc:
179
+ logger.warning("Could not delete checkpoint file: %s", exc)
@@ -21,6 +21,7 @@ from __future__ import annotations
21
21
 
22
22
  import logging
23
23
  import random
24
+ import threading
24
25
  import time
25
26
  from collections.abc import Callable
26
27
  from concurrent.futures import ThreadPoolExecutor, as_completed
@@ -34,7 +35,7 @@ from codedd_cli.auditor.response_parser import (
34
35
  is_response_complete,
35
36
  parse_audit_response,
36
37
  )
37
- from codedd_cli.llm.key_manager import PROVIDER_MODELS
38
+ from codedd_cli.llm.key_manager import KEY_PROVIDERS, PROVIDER_MODELS
38
39
 
39
40
  # Callback for debug dump: (full_prompt, response_text, audit_data, none_count)
40
41
  OnDumpLLMCallback = Callable[[str, str, dict | None, int], None]
@@ -230,13 +231,11 @@ class LocalFileAuditor:
230
231
  if not available:
231
232
  return None, None
232
233
 
233
- if self._preference in ("anthropic", "openai", "gemini", "grok"):
234
+ if self._preference in KEY_PROVIDERS:
234
235
  ordered = [self._preference] + [p for p in available if p != self._preference]
235
236
  ordered = [p for p in ordered if p in available]
236
237
  else:
237
- # Keep historical defaults first, then expanded providers.
238
- default_order = ("anthropic", "openai", "gemini", "grok")
239
- ordered = [p for p in default_order if p in available]
238
+ ordered = [p for p in KEY_PROVIDERS if p in available]
240
239
 
241
240
  primary = ordered[0] if ordered else None
242
241
  fallback = ordered[1] if len(ordered) > 1 else None
@@ -453,6 +452,8 @@ class LocalFileAuditor:
453
452
  files: list[dict],
454
453
  scope_dirs: dict[str, str],
455
454
  on_progress: Callable[[AuditFileResult], None] | None = None,
455
+ on_batch: Callable[[list[AuditFileResult]], None] | None = None,
456
+ batch_size: int = 25,
456
457
  ) -> list[AuditFileResult]:
457
458
  """
458
459
  Audit a list of files concurrently using a thread pool.
@@ -463,11 +464,17 @@ class LocalFileAuditor:
463
464
  sub-audit's ``root_path``.
464
465
  scope_dirs: Mapping of ``repo_name`` → local directory path.
465
466
  on_progress: Optional callback invoked after each file completes.
467
+ on_batch: Optional callback invoked with a list of results every
468
+ ``batch_size`` completions (and once for the final partial
469
+ batch). Called outside the internal lock so I/O is safe.
470
+ batch_size: How many results to accumulate before firing ``on_batch``.
466
471
 
467
472
  Returns:
468
473
  List of ``AuditFileResult`` objects (one per file).
469
474
  """
470
475
  results: list[AuditFileResult] = []
476
+ _pending: list[AuditFileResult] = []
477
+ _lock = threading.Lock()
471
478
 
472
479
  with ThreadPoolExecutor(max_workers=self._max_concurrent) as pool:
473
480
  future_map = {}
@@ -498,10 +505,27 @@ class LocalFileAuditor:
498
505
  relative_path=info["relative_path"],
499
506
  error=str(exc),
500
507
  )
508
+
501
509
  results.append(result)
510
+
511
+ batch_to_emit: list[AuditFileResult] | None = None
512
+ if on_batch:
513
+ with _lock:
514
+ _pending.append(result)
515
+ if len(_pending) >= batch_size:
516
+ batch_to_emit = _pending[:batch_size]
517
+ del _pending[:batch_size]
518
+
519
+ if batch_to_emit is not None:
520
+ on_batch(batch_to_emit)
521
+
502
522
  if on_progress:
503
523
  on_progress(result)
504
524
 
525
+ # Emit final partial batch
526
+ if on_batch and _pending:
527
+ on_batch(_pending)
528
+
505
529
  return results
506
530
 
507
531
  # ------------------------------------------------------------------