Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 56 additions & 80 deletions kalibr/cli/signup_cmd.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"""kalibr signup - Create a Kalibr account from the CLI."""
"""kalibr signup - Create a Kalibr account from the CLI (agent-driven onboarding)."""

import os
import time

import requests
import typer
Expand All @@ -12,19 +11,19 @@


def signup(
email: str = typer.Argument(..., help="Real email address for the account owner — a verification email will be sent here"),
org_name: str = typer.Option(None, "--org", help="Organization name (default: email prefix)"),
email: str = typer.Argument(..., help="Real email address for the account owner — credentials are returned immediately and a claim link is sent to this address"),
agent_name: str = typer.Option("kalibr-agent", "--agent-name", help="Name for this agent (for identification in the dashboard)"),
org_name: str = typer.Option(None, "--org", help="(unused, kept for backward compat)"),
) -> None:
"""Create a Kalibr account and get API credentials. Recommended for agent-driven onboarding. Agent runs this with the real user email, human clicks one verification link."""
"""Create a Kalibr account and get API credentials immediately. Recommended for agent-driven onboarding."""

console.print(f"[bold]Creating Kalibr account for {email}...[/bold]")

# Start signup
try:
resp = requests.post(
f"{BACKEND_URL}/api/cli-auth/signup",
json={"email": email, "org_name": org_name},
timeout=15,
f"{BACKEND_URL}/api/cli-auth/signup-and-provision",
json={"human_email": email, "agent_name": agent_name},
timeout=20,
)

if resp.status_code == 409:
Expand All @@ -36,79 +35,56 @@ def signup(
console.print("[red]Too many signup attempts. Try again in an hour.[/red]")
raise typer.Exit(1)

resp.raise_for_status()
if resp.status_code != 200:
console.print(f"[red]Signup failed (HTTP {resp.status_code}): {resp.text[:200]}[/red]")
raise typer.Exit(1)

data = resp.json()

except requests.RequestException as e:
console.print(f"[red]Signup failed: {e}[/red]")
raise typer.Exit(1)

signup_id = data["signup_id"]
console.print(f"\n[green]✓ Verification email sent to {email}[/green]")
console.print("[cyan]Click the link in your email to activate your account...[/cyan]\n")

# Poll for verification
poll_url = f"{BACKEND_URL}/api/cli-auth/signup/{signup_id}/status"
max_wait = 300 # 5 minutes
start = time.time()

with console.status("[bold cyan]Waiting for email verification...") as spinner:
while time.time() - start < max_wait:
try:
resp = requests.get(poll_url, timeout=10)
if resp.status_code == 200:
result = resp.json()

if result["status"] == "verified":
api_key = result["api_key"]
tenant_id = result["tenant_id"]

# Write to .env
env_path = os.path.join(os.getcwd(), ".env")
lines = []
if os.path.exists(env_path):
with open(env_path) as f:
lines = f.readlines()

key_map = {
"KALIBR_API_KEY": api_key,
"KALIBR_TENANT_ID": tenant_id,
}
updated = set()
new_lines = []
for line in lines:
replaced = False
for k, v in key_map.items():
if line.startswith(f"{k}="):
new_lines.append(f"{k}={v}\n")
updated.add(k)
replaced = True
break
if not replaced:
new_lines.append(line)

for k, v in key_map.items():
if k not in updated:
new_lines.append(f"{k}={v}\n")

with open(env_path, "w") as f:
f.writelines(new_lines)

console.print("\n[bold green]✓ Account created![/bold green]")
console.print(f" API Key: {api_key[:12]}...")
console.print(f" Tenant ID: {tenant_id}")
console.print(f" Saved to: {env_path}")
console.print("\n[bold]Next: run 'kalibr init' to instrument your code[/bold]")
return

elif result["status"] == "expired":
console.print("\n[red]Signup expired. Please try again.[/red]")
raise typer.Exit(1)

except requests.RequestException:
pass # Retry on network errors

time.sleep(5)

console.print("\n[red]Timed out waiting for email verification (5 minutes).[/red]")
console.print("[yellow]Check your inbox and spam folder, then run 'kalibr signup' again.[/yellow]")
raise typer.Exit(1)
api_key = data["api_key"]
tenant_id = data["tenant_id"]
claim_url = data.get("claim_url")

# Write to .env
env_path = os.path.join(os.getcwd(), ".env")
lines = []
if os.path.exists(env_path):
with open(env_path) as f:
lines = f.readlines()

key_map = {
"KALIBR_API_KEY": api_key,
"KALIBR_TENANT_ID": tenant_id,
}
updated = set()
new_lines = []
for line in lines:
replaced = False
for k, v in key_map.items():
if line.startswith(f"{k}="):
new_lines.append(f"{k}={v}\n")
updated.add(k)
replaced = True
break
if not replaced:
new_lines.append(line)

for k, v in key_map.items():
if k not in updated:
new_lines.append(f"{k}={v}\n")

with open(env_path, "w") as f:
f.writelines(new_lines)

console.print("\n[bold green]✓ Account created![/bold green]")
console.print(f" API Key: {api_key[:12]}...")
console.print(f" Tenant ID: {tenant_id}")
console.print(f" Saved to: {env_path}")
if claim_url:
console.print(f"\n[cyan]Dashboard access:[/cyan] {claim_url}")
console.print("[dim]Share this link with the account owner to claim the dashboard.[/dim]")
console.print("\n[bold]Next: run kalibr verify to confirm routing is live[/bold]")
Loading