codeframe-ai 0.9.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 (197) hide show
  1. codeframe/__init__.py +11 -0
  2. codeframe/__main__.py +20 -0
  3. codeframe/adapters/__init__.py +5 -0
  4. codeframe/adapters/e2b/__init__.py +13 -0
  5. codeframe/adapters/e2b/adapter.py +342 -0
  6. codeframe/adapters/e2b/budget.py +71 -0
  7. codeframe/adapters/e2b/credential_scanner.py +134 -0
  8. codeframe/adapters/llm/__init__.py +92 -0
  9. codeframe/adapters/llm/anthropic.py +414 -0
  10. codeframe/adapters/llm/base.py +444 -0
  11. codeframe/adapters/llm/mock.py +281 -0
  12. codeframe/adapters/llm/openai.py +483 -0
  13. codeframe/agents/__init__.py +8 -0
  14. codeframe/agents/dependency_resolver.py +714 -0
  15. codeframe/auth/__init__.py +16 -0
  16. codeframe/auth/api_key_router.py +238 -0
  17. codeframe/auth/api_keys.py +156 -0
  18. codeframe/auth/dependencies.py +358 -0
  19. codeframe/auth/manager.py +178 -0
  20. codeframe/auth/models.py +30 -0
  21. codeframe/auth/router.py +93 -0
  22. codeframe/auth/schemas.py +15 -0
  23. codeframe/auth/scopes.py +53 -0
  24. codeframe/cli/__init__.py +12 -0
  25. codeframe/cli/__main__.py +20 -0
  26. codeframe/cli/api_client.py +275 -0
  27. codeframe/cli/app.py +5688 -0
  28. codeframe/cli/auth.py +122 -0
  29. codeframe/cli/auth_commands.py +958 -0
  30. codeframe/cli/commands/__init__.py +5 -0
  31. codeframe/cli/config_commands.py +79 -0
  32. codeframe/cli/dashboard_commands.py +67 -0
  33. codeframe/cli/engines_commands.py +205 -0
  34. codeframe/cli/env_commands.py +409 -0
  35. codeframe/cli/helpers.py +56 -0
  36. codeframe/cli/hooks_commands.py +208 -0
  37. codeframe/cli/import_commands.py +129 -0
  38. codeframe/cli/pr_commands.py +549 -0
  39. codeframe/cli/proof_commands.py +415 -0
  40. codeframe/cli/stats_commands.py +311 -0
  41. codeframe/cli/telemetry_runtime.py +153 -0
  42. codeframe/cli/validators.py +123 -0
  43. codeframe/config/rate_limits.py +165 -0
  44. codeframe/core/__init__.py +15 -0
  45. codeframe/core/adapters/__init__.py +43 -0
  46. codeframe/core/adapters/agent_adapter.py +114 -0
  47. codeframe/core/adapters/builtin.py +326 -0
  48. codeframe/core/adapters/claude_code.py +62 -0
  49. codeframe/core/adapters/codex.py +393 -0
  50. codeframe/core/adapters/git_utils.py +40 -0
  51. codeframe/core/adapters/kilocode.py +126 -0
  52. codeframe/core/adapters/opencode.py +48 -0
  53. codeframe/core/adapters/streaming_chat.py +483 -0
  54. codeframe/core/adapters/subprocess_adapter.py +213 -0
  55. codeframe/core/adapters/verification_wrapper.py +269 -0
  56. codeframe/core/agent.py +2183 -0
  57. codeframe/core/agents_config.py +569 -0
  58. codeframe/core/api_key_service.py +211 -0
  59. codeframe/core/artifacts.py +428 -0
  60. codeframe/core/blocker_detection.py +218 -0
  61. codeframe/core/blockers.py +433 -0
  62. codeframe/core/checkpoints.py +481 -0
  63. codeframe/core/conductor.py +2255 -0
  64. codeframe/core/config.py +827 -0
  65. codeframe/core/config_watcher.py +268 -0
  66. codeframe/core/context.py +542 -0
  67. codeframe/core/context_packager.py +234 -0
  68. codeframe/core/credentials.py +735 -0
  69. codeframe/core/dependency_analyzer.py +229 -0
  70. codeframe/core/dependency_graph.py +290 -0
  71. codeframe/core/diagnostic_agent.py +712 -0
  72. codeframe/core/diagnostics.py +616 -0
  73. codeframe/core/editor.py +556 -0
  74. codeframe/core/engine_registry.py +256 -0
  75. codeframe/core/engine_stats.py +231 -0
  76. codeframe/core/environment.py +697 -0
  77. codeframe/core/events.py +375 -0
  78. codeframe/core/executor.py +1005 -0
  79. codeframe/core/fix_tracker.py +480 -0
  80. codeframe/core/gates.py +1322 -0
  81. codeframe/core/git.py +477 -0
  82. codeframe/core/github_connect_service.py +178 -0
  83. codeframe/core/github_integration_config.py +118 -0
  84. codeframe/core/github_issues_service.py +449 -0
  85. codeframe/core/hooks.py +184 -0
  86. codeframe/core/importers/__init__.py +1 -0
  87. codeframe/core/importers/ralph.py +540 -0
  88. codeframe/core/installer.py +650 -0
  89. codeframe/core/models.py +1026 -0
  90. codeframe/core/notifications_config.py +183 -0
  91. codeframe/core/planner.py +437 -0
  92. codeframe/core/prd.py +670 -0
  93. codeframe/core/prd_discovery.py +1118 -0
  94. codeframe/core/prd_stress_test.py +499 -0
  95. codeframe/core/progress.py +126 -0
  96. codeframe/core/proof/__init__.py +34 -0
  97. codeframe/core/proof/capture.py +79 -0
  98. codeframe/core/proof/evidence.py +56 -0
  99. codeframe/core/proof/ledger.py +574 -0
  100. codeframe/core/proof/models.py +162 -0
  101. codeframe/core/proof/obligations.py +103 -0
  102. codeframe/core/proof/runner.py +233 -0
  103. codeframe/core/proof/scope.py +81 -0
  104. codeframe/core/proof/stubs.py +156 -0
  105. codeframe/core/quick_fixes.py +558 -0
  106. codeframe/core/react_agent.py +1650 -0
  107. codeframe/core/reconciliation.py +183 -0
  108. codeframe/core/replay.py +788 -0
  109. codeframe/core/review.py +285 -0
  110. codeframe/core/runtime.py +1134 -0
  111. codeframe/core/sandbox/__init__.py +27 -0
  112. codeframe/core/sandbox/context.py +98 -0
  113. codeframe/core/sandbox/worktree.py +20 -0
  114. codeframe/core/schedule.py +396 -0
  115. codeframe/core/stall_detector.py +71 -0
  116. codeframe/core/stall_monitor.py +134 -0
  117. codeframe/core/state_machine.py +121 -0
  118. codeframe/core/streaming.py +502 -0
  119. codeframe/core/task_tree.py +400 -0
  120. codeframe/core/tasks.py +1022 -0
  121. codeframe/core/telemetry.py +232 -0
  122. codeframe/core/templates.py +221 -0
  123. codeframe/core/tools.py +942 -0
  124. codeframe/core/workspace.py +887 -0
  125. codeframe/core/worktrees.py +276 -0
  126. codeframe/git/__init__.py +5 -0
  127. codeframe/git/github_integration.py +505 -0
  128. codeframe/lib/__init__.py +0 -0
  129. codeframe/lib/audit_logger.py +248 -0
  130. codeframe/lib/metrics_tracker.py +800 -0
  131. codeframe/lib/quality/__init__.py +7 -0
  132. codeframe/lib/quality/complexity_analyzer.py +316 -0
  133. codeframe/lib/quality/owasp_patterns.py +284 -0
  134. codeframe/lib/quality/security_scanner.py +250 -0
  135. codeframe/lib/rate_limiter.py +312 -0
  136. codeframe/notifications/__init__.py +0 -0
  137. codeframe/notifications/webhook.py +380 -0
  138. codeframe/planning/__init__.py +30 -0
  139. codeframe/planning/issue_generator.py +219 -0
  140. codeframe/planning/prd_template_functions.py +137 -0
  141. codeframe/planning/prd_templates.py +975 -0
  142. codeframe/planning/task_scheduler.py +511 -0
  143. codeframe/planning/task_templates.py +533 -0
  144. codeframe/platform_store/__init__.py +5 -0
  145. codeframe/platform_store/database.py +277 -0
  146. codeframe/platform_store/repositories/__init__.py +24 -0
  147. codeframe/platform_store/repositories/api_key_repository.py +245 -0
  148. codeframe/platform_store/repositories/audit_repository.py +67 -0
  149. codeframe/platform_store/repositories/base.py +295 -0
  150. codeframe/platform_store/repositories/interactive_sessions.py +165 -0
  151. codeframe/platform_store/repositories/token_repository.py +598 -0
  152. codeframe/platform_store/repositories/workspace_registry_repository.py +175 -0
  153. codeframe/platform_store/schema_manager.py +321 -0
  154. codeframe/templates/AGENTS.md.default +94 -0
  155. codeframe/tui/__init__.py +5 -0
  156. codeframe/tui/app.py +256 -0
  157. codeframe/tui/data_service.py +103 -0
  158. codeframe/ui/__init__.py +0 -0
  159. codeframe/ui/dependencies.py +103 -0
  160. codeframe/ui/models.py +999 -0
  161. codeframe/ui/response_models.py +201 -0
  162. codeframe/ui/routers/__init__.py +5 -0
  163. codeframe/ui/routers/_helpers.py +29 -0
  164. codeframe/ui/routers/batches_v2.py +315 -0
  165. codeframe/ui/routers/blockers_v2.py +320 -0
  166. codeframe/ui/routers/checkpoints_v2.py +310 -0
  167. codeframe/ui/routers/costs_v2.py +322 -0
  168. codeframe/ui/routers/diagnose_v2.py +225 -0
  169. codeframe/ui/routers/discovery_v2.py +417 -0
  170. codeframe/ui/routers/environment_v2.py +284 -0
  171. codeframe/ui/routers/events_v2.py +75 -0
  172. codeframe/ui/routers/gates_v2.py +166 -0
  173. codeframe/ui/routers/git_v2.py +284 -0
  174. codeframe/ui/routers/github_integrations_v2.py +532 -0
  175. codeframe/ui/routers/interactive_sessions_v2.py +238 -0
  176. codeframe/ui/routers/pr_v2.py +709 -0
  177. codeframe/ui/routers/prd_v2.py +695 -0
  178. codeframe/ui/routers/proof_v2.py +755 -0
  179. codeframe/ui/routers/review_v2.py +360 -0
  180. codeframe/ui/routers/schedule_v2.py +214 -0
  181. codeframe/ui/routers/session_chat_ws.py +354 -0
  182. codeframe/ui/routers/settings_v2.py +562 -0
  183. codeframe/ui/routers/streaming_v2.py +155 -0
  184. codeframe/ui/routers/tasks_v2.py +1098 -0
  185. codeframe/ui/routers/templates_v2.py +232 -0
  186. codeframe/ui/routers/terminal_ws.py +267 -0
  187. codeframe/ui/routers/workspace_v2.py +527 -0
  188. codeframe/ui/server.py +568 -0
  189. codeframe/ui/shared.py +241 -0
  190. codeframe/workspace/__init__.py +5 -0
  191. codeframe/workspace/manager.py +249 -0
  192. codeframe_ai-0.9.0.dist-info/METADATA +517 -0
  193. codeframe_ai-0.9.0.dist-info/RECORD +197 -0
  194. codeframe_ai-0.9.0.dist-info/WHEEL +5 -0
  195. codeframe_ai-0.9.0.dist-info/entry_points.txt +3 -0
  196. codeframe_ai-0.9.0.dist-info/licenses/LICENSE +661 -0
  197. codeframe_ai-0.9.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,958 @@
1
+ """CLI authentication commands (login, logout, register, whoami, credentials, API keys).
2
+
3
+ This module provides commands for:
4
+ - Logging in with email/password
5
+ - Logging out (clearing credentials)
6
+ - Registering a new account
7
+ - Viewing current user info
8
+ - Managing API credentials (setup, list, validate, rotate, remove)
9
+ - Managing API keys (create, list, revoke, rotate)
10
+
11
+ Usage:
12
+ codeframe auth login --email user@example.com --password secret
13
+ codeframe auth logout
14
+ codeframe auth register --email new@example.com --password secret
15
+ codeframe auth whoami
16
+ codeframe auth setup --provider anthropic --value sk-ant-...
17
+ codeframe auth list
18
+ codeframe auth validate anthropic
19
+ codeframe auth rotate anthropic --value sk-ant-new-...
20
+ codeframe auth remove anthropic --yes
21
+ codeframe auth api-key-create --name "My Key" --user-id 1
22
+ codeframe auth api-key-list --user-id 1
23
+ codeframe auth api-key-revoke <key-id> --user-id 1 --yes
24
+ codeframe auth api-key-rotate <key-id> --user-id 1
25
+ """
26
+
27
+ import logging
28
+ import os
29
+ from typing import Optional, Tuple
30
+
31
+ import requests
32
+ import typer
33
+ from rich.table import Table
34
+
35
+ from codeframe.cli.auth import store_token, clear_token, is_authenticated
36
+ from codeframe.cli.api_client import APIClient, AuthenticationError, get_api_base_url
37
+ from codeframe.cli.helpers import console
38
+ from codeframe.core.credentials import (
39
+ CredentialManager,
40
+ CredentialProvider,
41
+ CredentialSource,
42
+ )
43
+ from codeframe.core.api_key_service import ApiKeyService
44
+ from codeframe.platform_store.database import Database
45
+
46
+ logger = logging.getLogger(__name__)
47
+
48
+
49
+ # =============================================================================
50
+ # Database Helper for CLI
51
+ # =============================================================================
52
+
53
+
54
+ def get_db_for_cli() -> Database:
55
+ """Get database instance for CLI commands.
56
+
57
+ Uses DATABASE_PATH environment variable if set, otherwise defaults
58
+ to .codeframe/state.db in the current directory.
59
+
60
+ Returns:
61
+ Initialized Database instance
62
+ """
63
+ db_path = os.getenv(
64
+ "DATABASE_PATH",
65
+ os.path.join(os.getcwd(), ".codeframe", "state.db")
66
+ )
67
+ db = Database(db_path)
68
+ db.initialize()
69
+ return db
70
+
71
+
72
+ # Provider name aliases for CLI convenience
73
+ PROVIDER_ALIASES = {
74
+ # Anthropic
75
+ "anthropic": CredentialProvider.LLM_ANTHROPIC,
76
+ "claude": CredentialProvider.LLM_ANTHROPIC,
77
+ "llm_anthropic": CredentialProvider.LLM_ANTHROPIC,
78
+ # OpenAI
79
+ "openai": CredentialProvider.LLM_OPENAI,
80
+ "gpt": CredentialProvider.LLM_OPENAI,
81
+ "gpt4": CredentialProvider.LLM_OPENAI,
82
+ "llm_openai": CredentialProvider.LLM_OPENAI,
83
+ # GitHub
84
+ "github": CredentialProvider.GIT_GITHUB,
85
+ "gh": CredentialProvider.GIT_GITHUB,
86
+ "git_github": CredentialProvider.GIT_GITHUB,
87
+ # GitLab
88
+ "gitlab": CredentialProvider.GIT_GITLAB,
89
+ "gl": CredentialProvider.GIT_GITLAB,
90
+ "git_gitlab": CredentialProvider.GIT_GITLAB,
91
+ # CI/CD
92
+ "cicd": CredentialProvider.CICD_GENERIC,
93
+ "ci": CredentialProvider.CICD_GENERIC,
94
+ "cicd_generic": CredentialProvider.CICD_GENERIC,
95
+ # Database
96
+ "database": CredentialProvider.DATABASE,
97
+ "db": CredentialProvider.DATABASE,
98
+ }
99
+
100
+
101
+ def resolve_provider_name(name: str) -> CredentialProvider:
102
+ """Resolve a provider name/alias to CredentialProvider enum.
103
+
104
+ Args:
105
+ name: Provider name or alias (case-insensitive)
106
+
107
+ Returns:
108
+ CredentialProvider enum value
109
+
110
+ Raises:
111
+ ValueError: If name doesn't match any known provider
112
+ """
113
+ normalized = name.lower().strip()
114
+
115
+ if normalized in PROVIDER_ALIASES:
116
+ return PROVIDER_ALIASES[normalized]
117
+
118
+ # Try direct enum lookup
119
+ try:
120
+ return CredentialProvider[name.upper()]
121
+ except KeyError:
122
+ pass
123
+
124
+ valid_names = sorted(set(PROVIDER_ALIASES.keys()))
125
+ raise ValueError(
126
+ f"Unknown provider: '{name}'. Valid providers: {', '.join(valid_names)}"
127
+ )
128
+
129
+
130
+ def validate_anthropic_credential(api_key: str) -> Tuple[bool, str]:
131
+ """Validate Anthropic API key by making a test request.
132
+
133
+ Args:
134
+ api_key: The API key to validate
135
+
136
+ Returns:
137
+ Tuple of (is_valid, message)
138
+ """
139
+ try:
140
+ from anthropic import Anthropic, AuthenticationError as AnthropicAuthError
141
+ from anthropic import APIConnectionError, RateLimitError, APIStatusError
142
+
143
+ client = Anthropic(api_key=api_key)
144
+ # Make a minimal request to validate
145
+ client.messages.create(
146
+ model="claude-3-haiku-20240307",
147
+ max_tokens=1,
148
+ messages=[{"role": "user", "content": "hi"}],
149
+ )
150
+ return True, "API key is valid"
151
+ except AnthropicAuthError:
152
+ return False, "Invalid API key - authentication failed"
153
+ except APIConnectionError as e:
154
+ return False, f"Unable to validate - network error: {e}"
155
+ except RateLimitError:
156
+ # Rate limited but key is valid (they wouldn't rate limit invalid keys)
157
+ return True, "API key appears valid (rate limited)"
158
+ except APIStatusError as e:
159
+ return False, f"Unable to validate - API error (HTTP {e.status_code})"
160
+ except ImportError:
161
+ return False, "Unable to validate - anthropic package not installed"
162
+ except Exception as e:
163
+ return False, f"Unable to validate - unexpected error: {type(e).__name__}: {e}"
164
+
165
+
166
+ def validate_openai_credential(api_key: str) -> Tuple[bool, str]:
167
+ """Validate OpenAI API key by making a test request.
168
+
169
+ Args:
170
+ api_key: The API key to validate
171
+
172
+ Returns:
173
+ Tuple of (is_valid, message)
174
+ """
175
+ try:
176
+ from openai import OpenAI, AuthenticationError as OpenAIAuthError
177
+ from openai import APIConnectionError, RateLimitError, APIStatusError
178
+
179
+ client = OpenAI(api_key=api_key)
180
+ # List models is a cheap validation
181
+ client.models.list()
182
+ return True, "API key is valid"
183
+ except OpenAIAuthError:
184
+ return False, "Invalid API key - authentication failed"
185
+ except APIConnectionError as e:
186
+ return False, f"Unable to validate - network error: {e}"
187
+ except RateLimitError:
188
+ # Rate limited but key is valid
189
+ return True, "API key appears valid (rate limited)"
190
+ except APIStatusError as e:
191
+ return False, f"Unable to validate - API error (HTTP {e.status_code})"
192
+ except ImportError:
193
+ return False, "Unable to validate - openai package not installed"
194
+ except Exception as e:
195
+ return False, f"Unable to validate - unexpected error: {type(e).__name__}: {e}"
196
+
197
+
198
+ def validate_github_credential(token: str) -> Tuple[bool, str]:
199
+ """Validate GitHub token by making a test request.
200
+
201
+ Args:
202
+ token: The GitHub token to validate
203
+
204
+ Returns:
205
+ Tuple of (is_valid, message)
206
+ """
207
+ try:
208
+ response = requests.get(
209
+ "https://api.github.com/user",
210
+ headers={
211
+ "Authorization": f"Bearer {token}",
212
+ "Accept": "application/vnd.github.v3+json",
213
+ },
214
+ timeout=10,
215
+ )
216
+ if response.status_code == 200:
217
+ user = response.json()
218
+ return True, f"Token valid for user: {user.get('login', 'unknown')}"
219
+ elif response.status_code == 401:
220
+ return False, "Invalid token - authentication failed"
221
+ elif response.status_code == 403:
222
+ # Forbidden - token may be valid but rate limited or lacking permissions
223
+ return False, "Token rejected - rate limited or insufficient permissions"
224
+ elif response.status_code == 429:
225
+ # Rate limited but token format was accepted
226
+ return True, "Token appears valid (rate limited)"
227
+ else:
228
+ return False, f"Unable to validate - GitHub API returned HTTP {response.status_code}"
229
+ except requests.ConnectionError:
230
+ return False, "Unable to validate - network error (cannot reach GitHub)"
231
+ except requests.Timeout:
232
+ return False, "Unable to validate - request timed out"
233
+ except requests.RequestException as e:
234
+ return False, f"Unable to validate - request error: {e}"
235
+ except Exception as e:
236
+ return False, f"Unable to validate - unexpected error: {type(e).__name__}: {e}"
237
+
238
+ auth_app = typer.Typer(
239
+ name="auth",
240
+ help="Authentication commands",
241
+ no_args_is_help=True,
242
+ )
243
+
244
+
245
+ @auth_app.command()
246
+ def login(
247
+ email: Optional[str] = typer.Option(None, "--email", "-e", help="Your email address"),
248
+ password: Optional[str] = typer.Option(None, "--password", "-p", help="Your password", hide_input=True),
249
+ ):
250
+ """Log in to CodeFRAME.
251
+
252
+ Authenticates with the server and stores your JWT token locally.
253
+ You will be prompted for email and password if not provided.
254
+
255
+ Examples:
256
+
257
+ codeframe auth login
258
+
259
+ codeframe auth login --email user@example.com
260
+
261
+ codeframe auth login -e user@example.com -p secret
262
+ """
263
+ # Prompt for email if not provided
264
+ if not email:
265
+ email = typer.prompt("Email")
266
+
267
+ # Prompt for password if not provided
268
+ if not password:
269
+ password = typer.prompt("Password", hide_input=True)
270
+
271
+ # Call login API
272
+ base_url = get_api_base_url()
273
+ login_url = f"{base_url}/auth/jwt/login"
274
+
275
+ try:
276
+ # FastAPI Users expects form data, not JSON
277
+ response = requests.post(
278
+ login_url,
279
+ data={
280
+ "username": email, # FastAPI Users uses 'username' field
281
+ "password": password,
282
+ },
283
+ headers={"Content-Type": "application/x-www-form-urlencoded"},
284
+ timeout=30,
285
+ )
286
+
287
+ if response.status_code == 200:
288
+ data = response.json()
289
+ token = data.get("access_token")
290
+
291
+ if token:
292
+ store_token(token)
293
+ console.print(f"[green]✓ Successfully logged in as {email}[/green]")
294
+ else:
295
+ console.print("[red]Error:[/red] No token received from server")
296
+ raise typer.Exit(1)
297
+
298
+ elif response.status_code == 400:
299
+ # Bad credentials
300
+ console.print("[red]Error:[/red] Invalid email or password")
301
+ raise typer.Exit(1)
302
+
303
+ elif response.status_code == 422:
304
+ # Validation error
305
+ console.print("[red]Error:[/red] Invalid request format")
306
+ raise typer.Exit(1)
307
+
308
+ else:
309
+ console.print(f"[red]Error:[/red] Login failed (HTTP {response.status_code})")
310
+ raise typer.Exit(1)
311
+
312
+ except requests.ConnectionError:
313
+ console.print(f"[red]Error:[/red] Cannot connect to server at {base_url}")
314
+ console.print("Make sure the CodeFRAME server is running: codeframe serve")
315
+ raise typer.Exit(1)
316
+
317
+ except requests.Timeout:
318
+ console.print("[red]Error:[/red] Request timed out")
319
+ raise typer.Exit(1)
320
+
321
+
322
+ @auth_app.command()
323
+ def logout():
324
+ """Log out of CodeFRAME.
325
+
326
+ Removes your stored credentials from the local machine.
327
+
328
+ Example:
329
+
330
+ codeframe auth logout
331
+ """
332
+ clear_token()
333
+ console.print("[green]✓ Logged out successfully[/green]")
334
+
335
+
336
+ @auth_app.command()
337
+ def register(
338
+ email: Optional[str] = typer.Option(None, "--email", "-e", help="Your email address"),
339
+ password: Optional[str] = typer.Option(None, "--password", "-p", help="Your password", hide_input=True),
340
+ ):
341
+ """Register a new CodeFRAME account.
342
+
343
+ Creates a new account and automatically logs you in.
344
+
345
+ Examples:
346
+
347
+ codeframe auth register
348
+
349
+ codeframe auth register --email new@example.com --password secret
350
+ """
351
+ # Prompt for email if not provided
352
+ if not email:
353
+ email = typer.prompt("Email")
354
+
355
+ # Prompt for password if not provided
356
+ if not password:
357
+ password = typer.prompt("Password", hide_input=True)
358
+ # Confirm password
359
+ password_confirm = typer.prompt("Confirm password", hide_input=True)
360
+ if password != password_confirm:
361
+ console.print("[red]Error:[/red] Passwords don't match")
362
+ raise typer.Exit(1)
363
+
364
+ # Call register API
365
+ base_url = get_api_base_url()
366
+ register_url = f"{base_url}/auth/register"
367
+
368
+ try:
369
+ response = requests.post(
370
+ register_url,
371
+ json={
372
+ "email": email,
373
+ "password": password,
374
+ },
375
+ headers={"Content-Type": "application/json"},
376
+ timeout=30,
377
+ )
378
+
379
+ if response.status_code == 201:
380
+ console.print(f"[green]✓ Account registered successfully for {email}[/green]")
381
+
382
+ # Auto-login after registration
383
+ console.print("Logging in...")
384
+ login_url = f"{base_url}/auth/jwt/login"
385
+ login_response = requests.post(
386
+ login_url,
387
+ data={"username": email, "password": password},
388
+ headers={"Content-Type": "application/x-www-form-urlencoded"},
389
+ timeout=30,
390
+ )
391
+
392
+ if login_response.status_code == 200:
393
+ token = login_response.json().get("access_token")
394
+ if token:
395
+ store_token(token)
396
+ console.print("[green]✓ Logged in automatically[/green]")
397
+ else:
398
+ console.print("[yellow]Note:[/yellow] Please log in manually: codeframe auth login")
399
+
400
+ elif response.status_code == 400:
401
+ error_detail = response.json().get("detail", "")
402
+ if "ALREADY_EXISTS" in str(error_detail).upper():
403
+ console.print("[red]Error:[/red] An account with this email already exists")
404
+ else:
405
+ console.print(f"[red]Error:[/red] Registration failed: {error_detail}")
406
+ raise typer.Exit(1)
407
+
408
+ elif response.status_code == 422:
409
+ # Validation error
410
+ error_data = response.json()
411
+ console.print("[red]Error:[/red] Invalid registration data")
412
+ if "detail" in error_data:
413
+ for error in error_data["detail"]:
414
+ field = error.get("loc", ["", ""])[-1]
415
+ msg = error.get("msg", "")
416
+ console.print(f" - {field}: {msg}")
417
+ raise typer.Exit(1)
418
+
419
+ else:
420
+ console.print(f"[red]Error:[/red] Registration failed (HTTP {response.status_code})")
421
+ raise typer.Exit(1)
422
+
423
+ except requests.ConnectionError:
424
+ console.print(f"[red]Error:[/red] Cannot connect to server at {base_url}")
425
+ console.print("Make sure the CodeFRAME server is running: codeframe serve")
426
+ raise typer.Exit(1)
427
+
428
+ except requests.Timeout:
429
+ console.print("[red]Error:[/red] Request timed out")
430
+ raise typer.Exit(1)
431
+
432
+
433
+ @auth_app.command()
434
+ def whoami():
435
+ """Show current logged-in user.
436
+
437
+ Displays your email and account information.
438
+
439
+ Example:
440
+
441
+ codeframe auth whoami
442
+ """
443
+ # Check if authenticated
444
+ if not is_authenticated():
445
+ console.print("[yellow]Not logged in.[/yellow]")
446
+ console.print("Please log in: codeframe auth login")
447
+ raise typer.Exit(1)
448
+
449
+ # Get user info
450
+ try:
451
+ client = APIClient()
452
+ user = client.get("/users/me")
453
+
454
+ console.print(f"\n[bold]Logged in as:[/bold] {user.get('email', 'Unknown')}")
455
+ if user.get("id"):
456
+ console.print(f"[bold]User ID:[/bold] {user['id']}")
457
+ console.print()
458
+
459
+ except AuthenticationError:
460
+ console.print("[yellow]Session expired.[/yellow]")
461
+ console.print("Please log in again: codeframe auth login")
462
+ raise typer.Exit(1)
463
+
464
+ except Exception as e:
465
+ console.print(f"[red]Error:[/red] {e}")
466
+ raise typer.Exit(1)
467
+
468
+
469
+ # =============================================================================
470
+ # Credential Management Commands
471
+ # =============================================================================
472
+
473
+
474
+ @auth_app.command("setup")
475
+ def setup_credential(
476
+ provider: Optional[str] = typer.Option(
477
+ None, "--provider", "-p", help="Provider name (e.g., anthropic, github, openai)"
478
+ ),
479
+ value: Optional[str] = typer.Option(
480
+ None, "--value", "-v", help="Credential value (API key or token)", hide_input=True
481
+ ),
482
+ ):
483
+ """Configure a credential for a provider.
484
+
485
+ Securely stores API keys and tokens for LLM providers, Git services,
486
+ and other integrations. Credentials are stored in the system keyring
487
+ or an encrypted file.
488
+
489
+ Examples:
490
+
491
+ codeframe auth setup # Interactive mode
492
+
493
+ codeframe auth setup --provider anthropic --value sk-ant-...
494
+
495
+ codeframe auth setup -p github -v ghp_...
496
+ """
497
+ manager = CredentialManager()
498
+
499
+ # Interactive provider selection if not provided
500
+ if not provider:
501
+ console.print("\n[bold]Select a provider to configure:[/bold]\n")
502
+ providers = [
503
+ ("1", "anthropic", "Anthropic (Claude) - LLM API"),
504
+ ("2", "openai", "OpenAI (GPT) - LLM API"),
505
+ ("3", "github", "GitHub - Git integration"),
506
+ ("4", "gitlab", "GitLab - Git integration"),
507
+ ]
508
+ for num, _, desc in providers:
509
+ console.print(f" {num}. {desc}")
510
+
511
+ choice = typer.prompt("\nEnter number or provider name", default="1")
512
+
513
+ # Map number to provider
514
+ choice_map = {p[0]: p[1] for p in providers}
515
+ provider = choice_map.get(choice, choice)
516
+
517
+ # Resolve provider name
518
+ try:
519
+ provider_enum = resolve_provider_name(provider)
520
+ except ValueError as e:
521
+ console.print(f"[red]Error:[/red] {e}")
522
+ raise typer.Exit(1)
523
+
524
+ # Prompt for value if not provided
525
+ if not value:
526
+ console.print(f"\nConfiguring [bold]{provider_enum.display_name}[/bold]")
527
+
528
+ # Show environment variable hint
529
+ env_hint = f"(or set {provider_enum.env_var} environment variable)"
530
+ console.print(f"[dim]{env_hint}[/dim]\n")
531
+
532
+ value = typer.prompt(
533
+ "Enter credential value",
534
+ hide_input=True,
535
+ )
536
+
537
+ # Reject empty or whitespace-only values
538
+ if not value or not value.strip():
539
+ console.print("[red]Error:[/red] Credential value cannot be empty")
540
+ raise typer.Exit(1)
541
+
542
+ # Validate format
543
+ if not manager.validate_credential_format(provider_enum, value):
544
+ console.print("[red]Error:[/red] Invalid credential format")
545
+ console.print("Please check the value and try again.")
546
+ raise typer.Exit(1)
547
+
548
+ # Store credential
549
+ manager.set_credential(provider_enum, value)
550
+ console.print(f"[green]Successfully stored credential for {provider_enum.display_name}[/green]")
551
+
552
+
553
+ @auth_app.command("list")
554
+ def list_credentials():
555
+ """List all configured credentials.
556
+
557
+ Shows credentials from both environment variables and secure storage.
558
+ Values are masked for security.
559
+
560
+ Example:
561
+
562
+ codeframe auth list
563
+ """
564
+ manager = CredentialManager()
565
+ credentials = manager.list_credentials()
566
+
567
+ if not credentials:
568
+ console.print("\n[yellow]No credentials configured.[/yellow]")
569
+ console.print("Use 'codeframe auth setup' to configure credentials.\n")
570
+ return
571
+
572
+ # Create table
573
+ table = Table(title="Configured Credentials")
574
+ table.add_column("Provider", style="cyan")
575
+ table.add_column("Source", style="green")
576
+ table.add_column("Value (masked)", style="dim")
577
+ table.add_column("Status")
578
+
579
+ for cred in credentials:
580
+ source_str = "env" if cred.source == CredentialSource.ENVIRONMENT else "stored"
581
+
582
+ if cred.is_expired:
583
+ status = "[red]expired[/red]"
584
+ else:
585
+ status = "[green]valid[/green]"
586
+
587
+ table.add_row(
588
+ cred.provider.display_name,
589
+ source_str,
590
+ cred.masked_value or "***",
591
+ status,
592
+ )
593
+
594
+ console.print()
595
+ console.print(table)
596
+ console.print()
597
+
598
+
599
+ @auth_app.command("validate")
600
+ def validate_credential(
601
+ provider: str = typer.Argument(..., help="Provider to validate (e.g., anthropic, github)"),
602
+ ):
603
+ """Validate a credential by testing it with the provider.
604
+
605
+ Makes a minimal API call to verify the credential is working.
606
+
607
+ Examples:
608
+
609
+ codeframe auth validate anthropic
610
+
611
+ codeframe auth validate github
612
+ """
613
+ manager = CredentialManager()
614
+
615
+ # Resolve provider
616
+ try:
617
+ provider_enum = resolve_provider_name(provider)
618
+ except ValueError as e:
619
+ console.print(f"[red]Error:[/red] {e}")
620
+ raise typer.Exit(1)
621
+
622
+ # Get credential
623
+ value = manager.get_credential(provider_enum)
624
+ if not value:
625
+ console.print(f"[red]Error:[/red] No credential configured for {provider_enum.display_name}")
626
+ console.print("Use 'codeframe auth setup' to configure it, or set the environment variable.")
627
+ raise typer.Exit(1)
628
+
629
+ console.print(f"Validating {provider_enum.display_name} credential...")
630
+
631
+ # Validate based on provider type
632
+ if provider_enum == CredentialProvider.LLM_ANTHROPIC:
633
+ valid, message = validate_anthropic_credential(value)
634
+ elif provider_enum == CredentialProvider.LLM_OPENAI:
635
+ valid, message = validate_openai_credential(value)
636
+ elif provider_enum == CredentialProvider.GIT_GITHUB:
637
+ valid, message = validate_github_credential(value)
638
+ else:
639
+ # Generic format validation for unsupported providers
640
+ valid = manager.validate_credential_format(provider_enum, value)
641
+ message = "Format appears valid" if valid else "Invalid format"
642
+
643
+ if valid:
644
+ console.print(f"[green]{message}[/green]")
645
+ else:
646
+ console.print(f"[red]Error:[/red] {message}")
647
+ raise typer.Exit(1)
648
+
649
+
650
+ @auth_app.command("rotate")
651
+ def rotate_credential(
652
+ provider: str = typer.Argument(..., help="Provider to rotate credential for"),
653
+ value: Optional[str] = typer.Option(
654
+ None, "--value", "-v", help="New credential value", hide_input=True
655
+ ),
656
+ force: bool = typer.Option(
657
+ False, "--force", "-f", help="Skip API validation of new credential"
658
+ ),
659
+ ):
660
+ """Rotate a credential with a new value.
661
+
662
+ Validates the new credential before storing (unless --force is used).
663
+ The old credential is only replaced after successful validation.
664
+
665
+ Examples:
666
+
667
+ codeframe auth rotate anthropic --value sk-ant-new-...
668
+
669
+ codeframe auth rotate github -v ghp_new_token --force
670
+ """
671
+ manager = CredentialManager()
672
+
673
+ # Resolve provider
674
+ try:
675
+ provider_enum = resolve_provider_name(provider)
676
+ except ValueError as e:
677
+ console.print(f"[red]Error:[/red] {e}")
678
+ raise typer.Exit(1)
679
+
680
+ # Check if credential exists
681
+ source = manager.get_credential_source(provider_enum)
682
+ if source == CredentialSource.NOT_FOUND:
683
+ console.print(f"[yellow]Note:[/yellow] No existing credential for {provider_enum.display_name}")
684
+ console.print("Use 'codeframe auth setup' to create a new credential.")
685
+ raise typer.Exit(1)
686
+
687
+ # Prompt for new value if not provided
688
+ if not value:
689
+ value = typer.prompt("Enter new credential value", hide_input=True)
690
+ value = value.strip()
691
+ # Validate format
692
+ if not manager.validate_credential_format(provider_enum, value):
693
+ console.print("[red]Error:[/red] Invalid credential format")
694
+ raise typer.Exit(1)
695
+
696
+ # Validate with API unless --force
697
+ if not force:
698
+ console.print("Validating new credential...")
699
+
700
+ if provider_enum == CredentialProvider.LLM_ANTHROPIC:
701
+ valid, message = validate_anthropic_credential(value)
702
+ elif provider_enum == CredentialProvider.LLM_OPENAI:
703
+ valid, message = validate_openai_credential(value)
704
+ elif provider_enum == CredentialProvider.GIT_GITHUB:
705
+ valid, message = validate_github_credential(value)
706
+ else:
707
+ valid = True
708
+ message = "Skipping API validation for this provider type"
709
+
710
+ if not valid:
711
+ console.print(f"[red]Error:[/red] {message}")
712
+ console.print("Use --force to skip validation.")
713
+ raise typer.Exit(1)
714
+
715
+ # Rotate credential
716
+ manager.rotate_credential(provider_enum, value)
717
+ console.print(f"[green]Successfully rotated credential for {provider_enum.display_name}[/green]")
718
+
719
+
720
+ @auth_app.command("remove")
721
+ def remove_credential(
722
+ provider: str = typer.Argument(..., help="Provider to remove credential for"),
723
+ yes: bool = typer.Option(
724
+ False, "--yes", "-y", help="Skip confirmation prompt"
725
+ ),
726
+ ):
727
+ """Remove a stored credential.
728
+
729
+ Note: This only removes credentials from secure storage.
730
+ Environment variables are not affected.
731
+
732
+ Examples:
733
+
734
+ codeframe auth remove anthropic
735
+
736
+ codeframe auth remove github --yes
737
+ """
738
+ manager = CredentialManager()
739
+
740
+ # Resolve provider
741
+ try:
742
+ provider_enum = resolve_provider_name(provider)
743
+ except ValueError as e:
744
+ console.print(f"[red]Error:[/red] {e}")
745
+ raise typer.Exit(1)
746
+
747
+ # Check if credential exists in storage (not just environment)
748
+ source = manager.get_credential_source(provider_enum)
749
+ if source != CredentialSource.STORED:
750
+ if source == CredentialSource.ENVIRONMENT:
751
+ console.print(
752
+ f"[yellow]Credential for {provider_enum.display_name} is set via environment "
753
+ f"variable ({provider_enum.env_var}) and cannot be removed by this command[/yellow]"
754
+ )
755
+ else:
756
+ console.print(f"[yellow]No stored credential found for {provider_enum.display_name}[/yellow]")
757
+ return
758
+
759
+ # Confirm deletion
760
+ if not yes:
761
+ confirm = typer.confirm(
762
+ f"Remove stored credential for {provider_enum.display_name}?"
763
+ )
764
+ if not confirm:
765
+ console.print("Cancelled.")
766
+ return
767
+
768
+ # Delete credential
769
+ try:
770
+ manager.delete_credential(provider_enum)
771
+ console.print(f"[green]Removed stored credential for {provider_enum.display_name}[/green]")
772
+ except Exception as e:
773
+ logger.debug(f"Credential deletion error: {e}")
774
+ console.print("[red]Error:[/red] Failed to remove stored credential")
775
+ raise typer.Exit(1)
776
+
777
+
778
+ # =============================================================================
779
+ # API Key Management Commands
780
+ # =============================================================================
781
+
782
+
783
+ @auth_app.command("api-key-create")
784
+ def api_key_create(
785
+ name: str = typer.Option(..., "--name", "-n", help="Human-readable name for the key"),
786
+ user_id: int = typer.Option(..., "--user-id", "-u", help="User ID to create key for"),
787
+ scopes: Optional[str] = typer.Option(
788
+ None, "--scopes", "-s", help="Comma-separated scopes (default: read,write)"
789
+ ),
790
+ ):
791
+ """Create a new API key.
792
+
793
+ Creates a new API key for programmatic access to the CodeFRAME API.
794
+ The full key is displayed only once - store it securely.
795
+
796
+ Examples:
797
+
798
+ codeframe auth api-key-create --name "CI Key" --user-id 1
799
+
800
+ codeframe auth api-key-create -n "Read Only" -u 1 -s read
801
+ """
802
+ from codeframe.auth.api_keys import SCOPE_READ, SCOPE_WRITE, validate_scopes
803
+
804
+ # Parse scopes
805
+ if scopes:
806
+ scope_list = [s.strip() for s in scopes.split(",")]
807
+ if not validate_scopes(scope_list):
808
+ console.print("[red]Error:[/red] Invalid scopes. Valid scopes: read, write, admin")
809
+ raise typer.Exit(1)
810
+ else:
811
+ scope_list = [SCOPE_READ, SCOPE_WRITE]
812
+
813
+ # Get database and service
814
+ db = get_db_for_cli()
815
+ service = ApiKeyService(db)
816
+
817
+ try:
818
+ result = service.create_api_key(
819
+ user_id=user_id,
820
+ name=name,
821
+ scopes=scope_list,
822
+ )
823
+
824
+ console.print("\n[green]API key created successfully![/green]\n")
825
+ console.print(f"[bold]Key:[/bold] {result.key}")
826
+ console.print(f"[bold]ID:[/bold] {result.id}")
827
+ console.print(f"[bold]Prefix:[/bold] {result.prefix}")
828
+ console.print()
829
+ console.print("[yellow]Save this key securely - it will not be shown again.[/yellow]\n")
830
+
831
+ except ValueError as e:
832
+ console.print(f"[red]Error:[/red] {e}")
833
+ raise typer.Exit(1)
834
+ except Exception as e:
835
+ logger.debug(f"API key creation error: {e}")
836
+ console.print("[red]Error:[/red] Failed to create API key")
837
+ raise typer.Exit(1)
838
+
839
+
840
+ @auth_app.command("api-key-list")
841
+ def api_key_list(
842
+ user_id: int = typer.Option(..., "--user-id", "-u", help="User ID to list keys for"),
843
+ ):
844
+ """List all API keys for a user.
845
+
846
+ Shows API key metadata without exposing the full key or hash.
847
+
848
+ Example:
849
+
850
+ codeframe auth api-key-list --user-id 1
851
+ """
852
+ # Get database and service
853
+ db = get_db_for_cli()
854
+ service = ApiKeyService(db)
855
+
856
+ keys = service.list_api_keys(user_id=user_id)
857
+
858
+ if not keys:
859
+ console.print("\n[yellow]No API keys found.[/yellow]\n")
860
+ return
861
+
862
+ # Create table
863
+ table = Table(title="API Keys")
864
+ table.add_column("ID", style="cyan")
865
+ table.add_column("Name", style="green")
866
+ table.add_column("Prefix", style="dim")
867
+ table.add_column("Scopes")
868
+ table.add_column("Created")
869
+ table.add_column("Last Used")
870
+ table.add_column("Status")
871
+
872
+ for key in keys:
873
+ status = "[green]active[/green]" if key.is_active else "[red]revoked[/red]"
874
+ last_used = key.last_used_at or "never"
875
+ scopes_str = ", ".join(key.scopes)
876
+
877
+ table.add_row(
878
+ key.id,
879
+ key.name,
880
+ key.prefix,
881
+ scopes_str,
882
+ key.created_at[:10] if key.created_at else "",
883
+ last_used[:10] if last_used != "never" else last_used,
884
+ status,
885
+ )
886
+
887
+ console.print()
888
+ console.print(table)
889
+ console.print()
890
+
891
+
892
+ @auth_app.command("api-key-revoke")
893
+ def api_key_revoke(
894
+ key_id: str = typer.Argument(..., help="API key ID to revoke"),
895
+ user_id: int = typer.Option(..., "--user-id", "-u", help="User ID (must own the key)"),
896
+ yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
897
+ ):
898
+ """Revoke an API key.
899
+
900
+ Revoked keys can no longer be used for authentication.
901
+
902
+ Examples:
903
+
904
+ codeframe auth api-key-revoke abc123 --user-id 1 --yes
905
+
906
+ codeframe auth api-key-revoke abc123 -u 1
907
+ """
908
+ # Confirm revocation
909
+ if not yes:
910
+ confirm = typer.confirm(f"Revoke API key {key_id}?")
911
+ if not confirm:
912
+ console.print("Cancelled.")
913
+ return
914
+
915
+ # Get database and service
916
+ db = get_db_for_cli()
917
+ service = ApiKeyService(db)
918
+
919
+ success = service.revoke_api_key(key_id, user_id=user_id)
920
+
921
+ if success:
922
+ console.print(f"[green]API key {key_id} revoked successfully.[/green]")
923
+ else:
924
+ console.print("[red]Error:[/red] API key not found or not owned by user")
925
+ raise typer.Exit(1)
926
+
927
+
928
+ @auth_app.command("api-key-rotate")
929
+ def api_key_rotate(
930
+ key_id: str = typer.Argument(..., help="API key ID to rotate"),
931
+ user_id: int = typer.Option(..., "--user-id", "-u", help="User ID (must own the key)"),
932
+ ):
933
+ """Rotate an API key.
934
+
935
+ Creates a new key with the same name and scopes, then revokes the old key.
936
+ Use this when a key may have been compromised.
937
+
938
+ Example:
939
+
940
+ codeframe auth api-key-rotate abc123 --user-id 1
941
+ """
942
+ # Get database and service
943
+ db = get_db_for_cli()
944
+ service = ApiKeyService(db)
945
+
946
+ result = service.rotate_api_key(key_id, user_id=user_id)
947
+
948
+ if result:
949
+ console.print("\n[green]API key rotated successfully![/green]\n")
950
+ console.print(f"[bold]New Key:[/bold] {result.key}")
951
+ console.print(f"[bold]New ID:[/bold] {result.id}")
952
+ console.print(f"[bold]Prefix:[/bold] {result.prefix}")
953
+ console.print()
954
+ console.print("[yellow]Save this key securely - it will not be shown again.[/yellow]")
955
+ console.print("[dim]The old key has been revoked.[/dim]\n")
956
+ else:
957
+ console.print("[red]Error:[/red] API key not found or not owned by user")
958
+ raise typer.Exit(1)