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.
- cade_cli-0.3.3.dist-info/METADATA +151 -0
- cade_cli-0.3.3.dist-info/RECORD +44 -0
- cade_cli-0.3.3.dist-info/WHEEL +4 -0
- cade_cli-0.3.3.dist-info/entry_points.txt +2 -0
- cadecoder/__init__.py +1 -0
- cadecoder/ai/__init__.py +6 -0
- cadecoder/ai/prompts.py +572 -0
- cadecoder/cli/__init__.py +0 -0
- cadecoder/cli/app.py +147 -0
- cadecoder/cli/auth.py +483 -0
- cadecoder/cli/commands/__init__.py +5 -0
- cadecoder/cli/commands/auth.py +143 -0
- cadecoder/cli/commands/chat.py +264 -0
- cadecoder/cli/commands/mcp.py +477 -0
- cadecoder/cli/commands/tools.py +226 -0
- cadecoder/core/__init__.py +12 -0
- cadecoder/core/config.py +380 -0
- cadecoder/core/constants.py +281 -0
- cadecoder/core/errors.py +145 -0
- cadecoder/core/logging.py +148 -0
- cadecoder/core/types.py +235 -0
- cadecoder/core/utils.py +279 -0
- cadecoder/execution/__init__.py +46 -0
- cadecoder/execution/context_window.py +521 -0
- cadecoder/execution/orchestrator.py +562 -0
- cadecoder/execution/parallel.py +287 -0
- cadecoder/providers/__init__.py +60 -0
- cadecoder/providers/base.py +294 -0
- cadecoder/providers/openai.py +251 -0
- cadecoder/storage/__init__.py +0 -0
- cadecoder/storage/threads.py +489 -0
- cadecoder/templates/login_failed.html +21 -0
- cadecoder/templates/login_success.html +21 -0
- cadecoder/templates/styles.css +87 -0
- cadecoder/tools/__init__.py +19 -0
- cadecoder/tools/builtin.py +644 -0
- cadecoder/tools/filesystem.py +315 -0
- cadecoder/tools/git.py +221 -0
- cadecoder/tools/manager.py +1635 -0
- cadecoder/ui/__init__.py +7 -0
- cadecoder/ui/display.py +338 -0
- cadecoder/ui/input.py +145 -0
- cadecoder/ui/session.py +455 -0
- 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)
|