cade-cli 0.3.3__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 (44) hide show
  1. cade_cli-0.3.3.dist-info/METADATA +151 -0
  2. cade_cli-0.3.3.dist-info/RECORD +44 -0
  3. cade_cli-0.3.3.dist-info/WHEEL +4 -0
  4. cade_cli-0.3.3.dist-info/entry_points.txt +2 -0
  5. cadecoder/__init__.py +1 -0
  6. cadecoder/ai/__init__.py +6 -0
  7. cadecoder/ai/prompts.py +572 -0
  8. cadecoder/cli/__init__.py +0 -0
  9. cadecoder/cli/app.py +147 -0
  10. cadecoder/cli/auth.py +483 -0
  11. cadecoder/cli/commands/__init__.py +5 -0
  12. cadecoder/cli/commands/auth.py +143 -0
  13. cadecoder/cli/commands/chat.py +264 -0
  14. cadecoder/cli/commands/mcp.py +477 -0
  15. cadecoder/cli/commands/tools.py +226 -0
  16. cadecoder/core/__init__.py +12 -0
  17. cadecoder/core/config.py +380 -0
  18. cadecoder/core/constants.py +281 -0
  19. cadecoder/core/errors.py +145 -0
  20. cadecoder/core/logging.py +148 -0
  21. cadecoder/core/types.py +235 -0
  22. cadecoder/core/utils.py +279 -0
  23. cadecoder/execution/__init__.py +46 -0
  24. cadecoder/execution/context_window.py +521 -0
  25. cadecoder/execution/orchestrator.py +562 -0
  26. cadecoder/execution/parallel.py +287 -0
  27. cadecoder/providers/__init__.py +60 -0
  28. cadecoder/providers/base.py +294 -0
  29. cadecoder/providers/openai.py +251 -0
  30. cadecoder/storage/__init__.py +0 -0
  31. cadecoder/storage/threads.py +489 -0
  32. cadecoder/templates/login_failed.html +21 -0
  33. cadecoder/templates/login_success.html +21 -0
  34. cadecoder/templates/styles.css +87 -0
  35. cadecoder/tools/__init__.py +19 -0
  36. cadecoder/tools/builtin.py +644 -0
  37. cadecoder/tools/filesystem.py +315 -0
  38. cadecoder/tools/git.py +221 -0
  39. cadecoder/tools/manager.py +1635 -0
  40. cadecoder/ui/__init__.py +7 -0
  41. cadecoder/ui/display.py +338 -0
  42. cadecoder/ui/input.py +145 -0
  43. cadecoder/ui/session.py +455 -0
  44. cadecoder/ui/state.py +20 -0
@@ -0,0 +1,143 @@
1
+ """Authentication commands for CadeCoder CLI.
2
+
3
+ Uses arcade-core for OAuth 2.0 authentication with PKCE.
4
+ Credentials are shared with arcade-cli at ~/.arcade/credentials.yaml.
5
+ """
6
+
7
+ from typing import Annotated
8
+
9
+ import typer
10
+ from arcade_core.config_model import Config
11
+ from arcade_core.constants import PROD_COORDINATOR_HOST
12
+ from rich.console import Console
13
+ from rich.markup import escape
14
+
15
+ from cadecoder.cli.auth import (
16
+ OAuthLoginError,
17
+ build_coordinator_url,
18
+ check_existing_login,
19
+ perform_oauth_login,
20
+ save_credentials_from_whoami,
21
+ )
22
+ from cadecoder.core.logging import log
23
+
24
+ console = Console(stderr=True)
25
+
26
+
27
+ def login(
28
+ host: Annotated[
29
+ str,
30
+ typer.Option(
31
+ "--host",
32
+ help="The Arcade Coordinator host (defaults to production).",
33
+ envvar="ARCADE_CLOUD_HOST",
34
+ ),
35
+ ] = PROD_COORDINATOR_HOST,
36
+ port: Annotated[
37
+ int | None,
38
+ typer.Option(
39
+ "--port",
40
+ help="Coordinator port (for local development).",
41
+ ),
42
+ ] = None,
43
+ force: Annotated[
44
+ bool,
45
+ typer.Option(
46
+ "--force",
47
+ "-f",
48
+ help="Force login even if already logged in.",
49
+ ),
50
+ ] = False,
51
+ ) -> None:
52
+ """Log in to Arcade Cloud using OAuth.
53
+
54
+ Opens a browser for authentication and stores credentials
55
+ at ~/.arcade/credentials.yaml (shared with arcade-cli).
56
+ """
57
+ try:
58
+ # Check if already logged in
59
+ if not force and check_existing_login(suppress_message=False):
60
+ console.print("\n[dim]Use --force to re-authenticate.[/dim]")
61
+ return
62
+
63
+ # Build coordinator URL
64
+ coordinator_url = build_coordinator_url(host, port)
65
+
66
+ # Perform OAuth login
67
+ def on_status(msg: str) -> None:
68
+ console.print(f"[dim]{msg}[/dim]")
69
+
70
+ tokens, whoami = perform_oauth_login(coordinator_url, on_status=on_status)
71
+
72
+ # Save credentials
73
+ save_credentials_from_whoami(tokens, whoami, coordinator_url)
74
+
75
+ # Success message
76
+ console.print("\n[green]✓ Login successful![/green]")
77
+ console.print(f" Email: {whoami.email}")
78
+
79
+ org = whoami.get_selected_org()
80
+ project = whoami.get_selected_project()
81
+ if org and project:
82
+ org_name = org.get("name", "unknown")
83
+ project_name = project.get("name", "unknown")
84
+ console.print(f" Active: {org_name} / {project_name}")
85
+
86
+ log.info(f"User {whoami.email} logged in via OAuth.")
87
+
88
+ except OAuthLoginError as e:
89
+ console.print(f"\n[red]✗ Login failed:[/red] {escape(str(e))}")
90
+ raise typer.Exit(code=1)
91
+ except Exception as e:
92
+ log.exception("Unexpected error during login.")
93
+ console.print(f"\n[red]✗ Unexpected error:[/red] {escape(str(e))}")
94
+ raise typer.Exit(code=1)
95
+
96
+
97
+ def logout() -> None:
98
+ """Log out of Arcade Cloud by removing stored credentials."""
99
+ try:
100
+ cred_path = Config.get_config_file_path()
101
+
102
+ if cred_path.exists():
103
+ cred_path.unlink()
104
+ console.print("[green]✓ Logged out successfully![/green]")
105
+ log.info("User credentials removed.")
106
+ else:
107
+ console.print("[yellow]You were not logged in.[/yellow]")
108
+ log.info("No credentials found to remove.")
109
+
110
+ except Exception as e:
111
+ log.exception("Error during logout.")
112
+ console.print(f"[red]✗ Error during logout:[/red] {escape(str(e))}")
113
+ raise typer.Exit(code=1)
114
+
115
+
116
+ def whoami() -> None:
117
+ """Show current login status and user information."""
118
+ try:
119
+ config = Config.load_from_file()
120
+
121
+ if not config.is_authenticated():
122
+ console.print("[yellow]Not logged in.[/yellow]")
123
+ console.print("[dim]Run 'cade login' to authenticate.[/dim]")
124
+ raise typer.Exit(code=1)
125
+
126
+ email = config.user.email if config.user else "unknown"
127
+ console.print(f"[green]✓[/green] Logged in as: [bold]{email}[/bold]")
128
+
129
+ if config.context:
130
+ console.print(f" Organization: {config.context.org_name}")
131
+ console.print(f" Project: {config.context.project_name}")
132
+
133
+ if config.auth and config.is_token_expired():
134
+ console.print("\n[yellow]⚠ Access token expired (will refresh on next use)[/yellow]")
135
+
136
+ except FileNotFoundError:
137
+ console.print("[yellow]Not logged in.[/yellow]")
138
+ console.print("[dim]Run 'cade login' to authenticate.[/dim]")
139
+ raise typer.Exit(code=1)
140
+ except ValueError as e:
141
+ console.print(f"[red]✗ Invalid credentials:[/red] {escape(str(e))}")
142
+ console.print("[dim]Run 'cade login' to re-authenticate.[/dim]")
143
+ raise typer.Exit(code=1)
@@ -0,0 +1,264 @@
1
+ """Interactive chat command for CadeCoder CLI."""
2
+
3
+ from typing import Annotated
4
+
5
+ import typer
6
+ from rich.console import Console
7
+ from rich.markup import escape
8
+
9
+ from cadecoder.core.config import get_config
10
+ from cadecoder.core.constants import DEFAULT_AI_MODEL
11
+ from cadecoder.core.errors import CadeCoderError, StorageError
12
+ from cadecoder.core.logging import log
13
+ from cadecoder.execution.orchestrator import create_orchestrator
14
+ from cadecoder.storage.threads import get_thread_history
15
+ from cadecoder.tools.git import get_current_branch_name
16
+ from cadecoder.ui.session import main as run_tui_main
17
+
18
+ console = Console(stderr=True)
19
+
20
+
21
+ def chat(
22
+ thread_or_name: Annotated[
23
+ str | None,
24
+ typer.Argument(help="Thread ID or Thread Name to resume (optional)"),
25
+ ] = None,
26
+ model: Annotated[
27
+ str,
28
+ typer.Option(
29
+ "--model",
30
+ "-m",
31
+ help="AI model to use for the conversation.",
32
+ ),
33
+ ] = DEFAULT_AI_MODEL,
34
+ name: Annotated[
35
+ str | None,
36
+ typer.Option(
37
+ "--name",
38
+ "-n",
39
+ help="Name for the new thread (ignored if resuming existing thread).",
40
+ ),
41
+ ] = None,
42
+ prompt: Annotated[
43
+ str | None,
44
+ typer.Option(
45
+ "--prompt",
46
+ "-p",
47
+ help="System prompt to guide the AI assistant's behavior.",
48
+ ),
49
+ ] = None,
50
+ ) -> None:
51
+ """
52
+ Start an interactive chat session with AI.
53
+
54
+ Can start a new chat or resume an existing thread. Supports multiple AI models
55
+ and custom system prompts.
56
+ """
57
+ command_name = "chat"
58
+ try:
59
+ # Preflight: ensure provider/API keys are configured before entering TUI
60
+ try:
61
+ _ = create_orchestrator()
62
+ except Exception as e:
63
+ console.print(
64
+ "[bold red]Provider configuration error:[/bold red] "
65
+ "Failed to initialize the AI provider.\n"
66
+ "Set required API keys (e.g., OPENAI_API_KEY or ANTHROPIC_API_KEY) "
67
+ "or configure the provider, then try again.\n"
68
+ f"Details: {str(e)}"
69
+ )
70
+ raise typer.Exit(code=1)
71
+
72
+ # Get the thread history manager
73
+ history_manager = get_thread_history()
74
+
75
+ # Determine current git branch (used to disambiguate names)
76
+ current_branch_raw, branch_error = get_current_branch_name()
77
+ current_branch: str | None = current_branch_raw
78
+ if branch_error:
79
+ log.warning(f"Could not get git branch: {branch_error}")
80
+ current_branch = None
81
+
82
+ thread = None
83
+ selected_thread_id: str | None = None
84
+
85
+ if thread_or_name is not None:
86
+ # First, try exact thread ID
87
+ thread = history_manager.get_thread(thread_or_name)
88
+
89
+ # If not found, treat as a thread name
90
+ if not thread:
91
+ # Prefer branch-scoped lookup if available
92
+ if current_branch:
93
+ thread = history_manager.find_thread_by_name_and_branch(
94
+ thread_or_name, current_branch
95
+ )
96
+ # Fallback: search most recently updated thread with that exact name
97
+ if not thread:
98
+ all_threads = history_manager.list_threads()
99
+ for t in all_threads:
100
+ if t.name == thread_or_name:
101
+ thread = t
102
+ break
103
+
104
+ # If still not found, create a new thread with this name
105
+ if not thread:
106
+ try:
107
+ user_id = get_config().user_email
108
+ except Exception:
109
+ user_id = None
110
+ thread = history_manager.create_thread(
111
+ name=thread_or_name,
112
+ git_branch=current_branch,
113
+ model=model,
114
+ user_id=user_id,
115
+ )
116
+ log.info(
117
+ f"Created new thread '{thread_or_name}' with ID: {thread.thread_id} for branch: {current_branch}"
118
+ )
119
+
120
+ selected_thread_id = thread.thread_id
121
+ else:
122
+ # No positional provided: follow original behavior (optionally use --name)
123
+ if name and current_branch:
124
+ thread = history_manager.find_thread_by_name_and_branch(name, current_branch)
125
+ if thread:
126
+ selected_thread_id = thread.thread_id
127
+ log.info(
128
+ f"Resuming existing thread '{name}' (ID: {selected_thread_id}) for branch: {current_branch}"
129
+ )
130
+
131
+ if not thread:
132
+ try:
133
+ user_id = get_config().user_email
134
+ except Exception:
135
+ user_id = None
136
+ thread = history_manager.create_thread(
137
+ name=name,
138
+ git_branch=current_branch,
139
+ model=model,
140
+ user_id=user_id,
141
+ )
142
+ selected_thread_id = thread.thread_id
143
+ if name:
144
+ log.info(
145
+ f"Created new thread '{name}' with ID: {selected_thread_id} for branch: {current_branch}"
146
+ )
147
+ else:
148
+ log.info(
149
+ f"Created new thread with ID: {selected_thread_id} for branch: {current_branch}"
150
+ )
151
+
152
+ # Safety check
153
+ if not selected_thread_id:
154
+ console.print("[red]Failed to determine or create a thread to run.[/red]")
155
+ raise typer.Exit(code=1)
156
+
157
+ # Launch TUI
158
+ run_tui_main(
159
+ thread_id_to_run=str(selected_thread_id),
160
+ model=model,
161
+ stream=True,
162
+ system_prompt=prompt,
163
+ )
164
+ except (StorageError, CadeCoderError) as e:
165
+ console.print(":x: [bold red]Error:[/bold red] " + escape(str(e)))
166
+ raise typer.Exit(code=1)
167
+ except KeyboardInterrupt:
168
+ console.print("\n[yellow]Chat session ended by user.[/yellow]")
169
+ raise typer.Exit(code=0)
170
+ except Exception as e:
171
+ log.exception(f"An unexpected error occurred during '{command_name}'.")
172
+ console.print(":x: [bold red]Unexpected Error:[/bold red] " + escape(str(e)))
173
+ raise typer.Exit(code=1)
174
+
175
+
176
+ def resume(
177
+ name: Annotated[
178
+ str | None,
179
+ typer.Argument(help="Thread name to resume (optional)"),
180
+ ] = None,
181
+ model: Annotated[
182
+ str,
183
+ typer.Option(
184
+ "--model",
185
+ "-m",
186
+ help="AI model to use for the conversation.",
187
+ ),
188
+ ] = DEFAULT_AI_MODEL,
189
+ prompt: Annotated[
190
+ str | None,
191
+ typer.Option(
192
+ "--prompt",
193
+ "-p",
194
+ help="System prompt to guide the AI assistant's behavior.",
195
+ ),
196
+ ] = None,
197
+ ) -> None:
198
+ """Resume a saved chat thread.
199
+
200
+ If a thread name is provided, the most recently updated thread matching that
201
+ name will be resumed. Otherwise, the most recently updated thread overall is resumed.
202
+
203
+ Examples
204
+ --------
205
+ - cade resume
206
+ - cade resume "Onboarding Tasks"
207
+ - cade -r # resumes most recent thread (shortcut)
208
+ """
209
+ command_name = "resume"
210
+ try:
211
+ # Preflight provider before entering TUI
212
+ try:
213
+ _ = create_orchestrator()
214
+ except Exception as e:
215
+ console.print(
216
+ "[bold red]Provider configuration error:[/bold red] "
217
+ "Failed to initialize the AI provider.\n"
218
+ "Set required API keys (e.g., OPENAI_API_KEY or ANTHROPIC_API_KEY) "
219
+ "or configure the provider, then try again.\n"
220
+ f"Details: {str(e)}"
221
+ )
222
+ raise typer.Exit(code=1)
223
+
224
+ history_manager = get_thread_history()
225
+
226
+ threads = history_manager.list_threads()
227
+ if not threads:
228
+ console.print("[yellow]No saved threads found.[/yellow]")
229
+ raise typer.Exit(code=0)
230
+
231
+ target_thread = None
232
+ if name:
233
+ # Filter exact name matches, threads are already sorted by last_modified_at DESC
234
+ matching = [t for t in threads if t.name and t.name == name]
235
+ if matching:
236
+ target_thread = matching[0]
237
+ else:
238
+ console.print(f"[red]No thread found with name '{name}'.[/red]")
239
+ # Provide a small suggestion list
240
+ unique_names = [t.name for t in threads if t.name]
241
+ if unique_names:
242
+ console.print("[dim]Available thread names (recent first):[/dim]")
243
+ for n in unique_names[:10]:
244
+ console.print(f" - {n}")
245
+ raise typer.Exit(code=1)
246
+ else:
247
+ target_thread = threads[0]
248
+
249
+ run_tui_main(
250
+ thread_id_to_run=str(target_thread.thread_id),
251
+ model=model,
252
+ stream=True,
253
+ system_prompt=prompt,
254
+ )
255
+ except (StorageError, CadeCoderError) as e:
256
+ console.print(":x: [bold red]Error:[/bold red] " + escape(str(e)))
257
+ raise typer.Exit(code=1)
258
+ except KeyboardInterrupt:
259
+ console.print("\n[yellow]Chat session ended by user.[/yellow]")
260
+ raise typer.Exit(code=0)
261
+ except Exception as e:
262
+ log.exception(f"An unexpected error occurred during '{command_name}'.")
263
+ console.print(":x: [bold red]Unexpected Error:[/bold red] " + escape(str(e)))
264
+ raise typer.Exit(code=1)