Skip to content

Commit eae5c2b

Browse files
authored
Merge pull request #111 from WecoAI/dev
Add run sharing + run credit cost lookup + bump version (0.3.14)
2 parents a410a7f + 2817886 commit eae5c2b

File tree

6 files changed

+193
-31
lines changed

6 files changed

+193
-31
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ name = "weco"
88
authors = [{ name = "Weco AI Team", email = "contact@weco.ai" }]
99
description = "Documentation for `weco`, a CLI for using Weco AI's code optimizer."
1010
readme = "README.md"
11-
version = "0.3.13"
11+
version = "0.3.14"
1212
license = { file = "LICENSE" }
1313
requires-python = ">=3.8"
1414
dependencies = [

weco/api.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,3 +450,33 @@ def submit_execution_result(
450450
return None
451451
except Exception:
452452
return None
453+
454+
455+
# --- Share API Functions ---
456+
457+
458+
def create_share_link(
459+
console: Console, run_id: str, auth_headers: dict = {}, timeout: Union[int, Tuple[int, int]] = (5, 30)
460+
) -> Optional[str]:
461+
"""Create a public share link for a run.
462+
463+
Args:
464+
console: Rich console for output.
465+
run_id: The run ID to share.
466+
auth_headers: Authentication headers.
467+
timeout: Request timeout.
468+
469+
Returns:
470+
The share ID if successful, or None on failure.
471+
"""
472+
try:
473+
response = requests.post(f"{__base_url__}/runs/{run_id}/share", headers=auth_headers, timeout=timeout)
474+
response.raise_for_status()
475+
result = response.json()
476+
return result.get("share_id")
477+
except requests.exceptions.HTTPError as e:
478+
handle_api_error(e, console)
479+
return None
480+
except Exception as e:
481+
console.print(f"[bold red]Error creating share link: {e}[/]")
482+
return None

weco/cli.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,12 @@ def _parse_credit_amount(value: str) -> float:
182182
help="Amount of credits to purchase (minimum 2, defaults to 10)",
183183
)
184184

185+
# Credits cost command
186+
cost_parser = credits_subparsers.add_parser("cost", help="Check credit spend for a run")
187+
cost_parser.add_argument(
188+
"run_id", type=str, help="The run ID to check credit spend for (e.g., '0002e071-1b67-411f-a514-36947f0c4b31')"
189+
)
190+
185191
# Credits autotopup command
186192
autotopup_parser = credits_subparsers.add_parser("autotopup", help="Configure automatic top-up")
187193
autotopup_parser.add_argument("--enable", action="store_true", help="Enable automatic top-up")
@@ -387,6 +393,21 @@ def _main() -> None:
387393
)
388394
configure_resume_parser(resume_parser)
389395

396+
# --- Share Command Parser Setup ---
397+
share_parser = subparsers.add_parser(
398+
"share", help="Create a public share link for a run", formatter_class=argparse.RawTextHelpFormatter
399+
)
400+
share_parser.add_argument(
401+
"run_id", type=str, help="The UUID of the run to share (e.g., '0002e071-1b67-411f-a514-36947f0c4b31')"
402+
)
403+
share_parser.add_argument(
404+
"--output",
405+
type=str,
406+
choices=["rich", "plain"],
407+
default="rich",
408+
help="Output mode: 'rich' for interactive terminal UI (default), 'plain' for machine-readable text output suitable for LLM agents.",
409+
)
410+
390411
# --- Setup Command Parser Setup ---
391412
setup_parser = subparsers.add_parser("setup", help="Set up Weco for use with AI tools")
392413
configure_setup_parser(setup_parser)
@@ -426,6 +447,11 @@ def _main() -> None:
426447
sys.exit(0)
427448
elif args.command == "resume":
428449
execute_resume_command(args)
450+
elif args.command == "share":
451+
from .share import handle_share_command
452+
453+
handle_share_command(run_id=args.run_id, output_mode=args.output, console=console)
454+
sys.exit(0)
429455
elif args.command == "setup":
430456
from .setup import handle_setup_command
431457

weco/credits.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ def handle_credits_command(args, console: Console) -> None:
2626

2727
if args.credits_command == "balance" or args.credits_command is None:
2828
check_balance(console, auth_headers)
29+
elif args.credits_command == "cost":
30+
check_run_cost(console, auth_headers, args.run_id)
2931
elif args.credits_command == "topup":
3032
topup_credits(console, auth_headers, args.amount)
3133
elif args.credits_command == "autotopup":
@@ -65,6 +67,45 @@ def check_balance(console: Console, auth_headers: dict) -> None:
6567
console.print(f"[bold red]Unexpected error: {e}[/]")
6668

6769

70+
def check_run_cost(console: Console, auth_headers: dict, run_id: str) -> None:
71+
"""Check and display credit spend for a specific run."""
72+
try:
73+
response = requests.get(f"{__base_url__}/billing/run/{run_id}/cost", headers=auth_headers, timeout=10)
74+
response.raise_for_status()
75+
data = response.json()
76+
77+
total = data.get("credits_spent_total", 0)
78+
steps = data.get("steps", [])
79+
80+
table = Table(title=f"Credit Spend for Run {run_id}", show_header=True, header_style="bold cyan")
81+
table.add_column("Step", style="dim", justify="right")
82+
table.add_column("Node ID", style="dim")
83+
table.add_column("Cost", style="green", justify="right")
84+
85+
for step in steps:
86+
step_num = str(step.get("step", "-"))
87+
node_id = step.get("node_id", "-") or "-"
88+
credits = step.get("credits_spent", 0)
89+
table.add_row(step_num, node_id, f"${credits:.2f}")
90+
91+
table.add_section()
92+
table.add_row("", "[bold]Total[/]", f"[bold]${total:.2f}[/]")
93+
94+
console.print(table)
95+
96+
except requests.exceptions.HTTPError as e:
97+
if e.response.status_code == 401:
98+
console.print("[bold red]Authentication failed. Please log in again with 'weco login'.[/]")
99+
elif e.response.status_code == 403:
100+
console.print("[bold red]Permission denied. You do not own this run.[/]")
101+
elif e.response.status_code == 404:
102+
console.print(f"[bold red]Run not found: {run_id}[/]")
103+
else:
104+
console.print(f"[bold red]Error checking run cost: {e}[/]")
105+
except Exception as e:
106+
console.print(f"[bold red]Unexpected error: {e}[/]")
107+
108+
68109
def topup_credits(console: Console, auth_headers: dict, amount: float) -> None:
69110
"""Initiate a credit top-up via Stripe."""
70111
try:

weco/setup.py

Lines changed: 52 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from urllib.request import urlopen
1515

1616
from rich.console import Console
17-
from rich.prompt import Confirm
17+
from rich.prompt import Confirm, Prompt
1818

1919
from .events import (
2020
send_event,
@@ -512,56 +512,78 @@ def setup_cursor(console: Console, local_path: pathlib.Path | None = None) -> No
512512
SETUP_HANDLERS = {"claude-code": setup_claude_code, "cursor": setup_cursor}
513513

514514

515-
def handle_setup_command(args, console: Console) -> None:
516-
"""Handle the setup command with its subcommands."""
517-
available_tools = ", ".join(SETUP_HANDLERS)
518-
ctx = create_event_context()
515+
def prompt_tool_selection(console: Console) -> list[str]:
516+
"""Prompt the user to select which tool(s) to set up.
519517
520-
if args.tool is None:
521-
console.print("[bold red]Error:[/] Please specify a tool to set up.")
522-
console.print(f"Available tools: {available_tools}")
523-
console.print("\nUsage: weco setup <tool>")
524-
sys.exit(1)
518+
Returns:
519+
List of tool names to set up.
520+
"""
521+
tool_names = list(SETUP_HANDLERS.keys())
522+
all_option = len(tool_names) + 1
525523

526-
handler = SETUP_HANDLERS.get(args.tool)
527-
if handler is None:
528-
console.print(f"[bold red]Error:[/] Unknown tool: {args.tool}")
529-
console.print(f"Available tools: {available_tools}")
530-
sys.exit(1)
524+
console.print("\n[bold cyan]Available tools to set up:[/]\n")
525+
for i, name in enumerate(tool_names, 1):
526+
console.print(f" {i}. {name}")
527+
console.print(f" {all_option}. All of the above")
528+
529+
valid_choices = [str(i) for i in range(1, all_option + 1)]
530+
choice = Prompt.ask("\n[bold]Select an option[/]", choices=valid_choices, show_choices=True)
531+
532+
idx = int(choice)
533+
if idx == all_option:
534+
return tool_names
535+
return [tool_names[idx - 1]]
531536

532-
# Extract local path if provided
533-
local_path = None
534-
if hasattr(args, "local") and args.local:
535-
local_path = pathlib.Path(args.local).expanduser().resolve()
536537

537-
# Determine source type for event reporting
538+
def _run_setup_for_tool(tool: str, console: Console, local_path: pathlib.Path | None, ctx) -> None:
539+
"""Run setup for a single tool with event tracking and error handling."""
538540
source = "local" if local_path else "download"
539541

540-
# Send skill install started event
541-
send_event(SkillInstallStartedEvent(tool=args.tool, source=source), ctx)
542+
send_event(SkillInstallStartedEvent(tool=tool, source=source), ctx)
542543

543544
start_time = time.time()
544545

545546
try:
547+
handler = SETUP_HANDLERS[tool]
546548
handler(console, local_path=local_path)
547549

548-
# Send successful completion event
549550
duration_ms = int((time.time() - start_time) * 1000)
550-
send_event(SkillInstallCompletedEvent(tool=args.tool, source=source, duration_ms=duration_ms), ctx)
551+
send_event(SkillInstallCompletedEvent(tool=tool, source=source, duration_ms=duration_ms), ctx)
551552

552553
except DownloadError as e:
553-
# Send failure event
554-
send_event(SkillInstallFailedEvent(tool=args.tool, source=source, error_type="download_error", stage="download"), ctx)
554+
send_event(SkillInstallFailedEvent(tool=tool, source=source, error_type="download_error", stage="download"), ctx)
555555
console.print(f"[bold red]Error:[/] {e}")
556556
sys.exit(1)
557557
except SafetyError as e:
558-
# Send failure event
559-
send_event(SkillInstallFailedEvent(tool=args.tool, source=source, error_type="safety_error", stage="setup"), ctx)
558+
send_event(SkillInstallFailedEvent(tool=tool, source=source, error_type="safety_error", stage="setup"), ctx)
560559
console.print(f"[bold red]Safety Error:[/] {e}")
561560
sys.exit(1)
562561
except (SetupError, FileNotFoundError, OSError, ValueError) as e:
563-
# Send failure event
564562
error_type = type(e).__name__
565-
send_event(SkillInstallFailedEvent(tool=args.tool, source=source, error_type=error_type, stage="setup"), ctx)
563+
send_event(SkillInstallFailedEvent(tool=tool, source=source, error_type=error_type, stage="setup"), ctx)
566564
console.print(f"[bold red]Error:[/] {e}")
567565
sys.exit(1)
566+
567+
568+
def handle_setup_command(args, console: Console) -> None:
569+
"""Handle the setup command with its subcommands."""
570+
ctx = create_event_context()
571+
572+
if args.tool is None:
573+
selected_tools = prompt_tool_selection(console)
574+
else:
575+
handler = SETUP_HANDLERS.get(args.tool)
576+
if handler is None:
577+
available_tools = ", ".join(SETUP_HANDLERS)
578+
console.print(f"[bold red]Error:[/] Unknown tool: {args.tool}")
579+
console.print(f"Available tools: {available_tools}")
580+
sys.exit(1)
581+
selected_tools = [args.tool]
582+
583+
# Extract local path if provided
584+
local_path = None
585+
if hasattr(args, "local") and args.local:
586+
local_path = pathlib.Path(args.local).expanduser().resolve()
587+
588+
for tool in selected_tools:
589+
_run_setup_for_tool(tool, console, local_path, ctx)

weco/share.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
"""Share command handler for the Weco CLI."""
2+
3+
import sys
4+
from rich.console import Console
5+
6+
from . import __dashboard_url__
7+
from .api import create_share_link
8+
from .auth import handle_authentication
9+
10+
11+
def handle_share_command(run_id: str, output_mode: str, console: Console) -> None:
12+
"""Handle the `weco share <run_id>` command.
13+
14+
Creates a public share link for the given run. Requires CLI sharing
15+
to be enabled in the user's account settings on the dashboard.
16+
17+
Args:
18+
run_id: UUID of the run to share.
19+
output_mode: 'rich' or 'plain'.
20+
console: Rich console instance for output.
21+
"""
22+
# Authenticate
23+
_, auth_headers = handle_authentication(console)
24+
if not auth_headers:
25+
sys.exit(1)
26+
27+
# Attempt to create the share link
28+
share_id = create_share_link(console=console, run_id=run_id, auth_headers=auth_headers)
29+
30+
if share_id is None:
31+
# Error message was already printed by create_share_link / handle_api_error
32+
sys.exit(1)
33+
34+
share_url = f"{__dashboard_url__}/share/{share_id}"
35+
36+
if output_mode == "plain":
37+
console.print(share_url)
38+
else:
39+
console.print()
40+
console.print("[bold green]Share link created![/]")
41+
console.print(f"[bold]{share_url}[/]")
42+
console.print()
43+
console.print("[dim]Anyone with this link can view the run's optimization progress and results.[/]")

0 commit comments

Comments
 (0)