hypercli-cli 0.8.2__tar.gz → 0.8.4__tar.gz
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.
- {hypercli_cli-0.8.2 → hypercli_cli-0.8.4}/PKG-INFO +1 -1
- hypercli_cli-0.8.4/hypercli_cli/__init__.py +1 -0
- {hypercli_cli-0.8.2 → hypercli_cli-0.8.4}/hypercli_cli/claw.py +163 -0
- hypercli_cli-0.8.4/hypercli_cli/onboard.py +706 -0
- {hypercli_cli-0.8.2 → hypercli_cli-0.8.4}/pyproject.toml +1 -1
- hypercli_cli-0.8.2/hypercli_cli/__init__.py +0 -1
- {hypercli_cli-0.8.2 → hypercli_cli-0.8.4}/.gitignore +0 -0
- {hypercli_cli-0.8.2 → hypercli_cli-0.8.4}/README.md +0 -0
- {hypercli_cli-0.8.2 → hypercli_cli-0.8.4}/hypercli_cli/billing.py +0 -0
- {hypercli_cli-0.8.2 → hypercli_cli-0.8.4}/hypercli_cli/cli.py +0 -0
- {hypercli_cli-0.8.2 → hypercli_cli-0.8.4}/hypercli_cli/comfyui.py +0 -0
- {hypercli_cli-0.8.2 → hypercli_cli-0.8.4}/hypercli_cli/flow.py +0 -0
- {hypercli_cli-0.8.2 → hypercli_cli-0.8.4}/hypercli_cli/instances.py +0 -0
- {hypercli_cli-0.8.2 → hypercli_cli-0.8.4}/hypercli_cli/jobs.py +0 -0
- {hypercli_cli-0.8.2 → hypercli_cli-0.8.4}/hypercli_cli/keys.py +0 -0
- {hypercli_cli-0.8.2 → hypercli_cli-0.8.4}/hypercli_cli/llm.py +0 -0
- {hypercli_cli-0.8.2 → hypercli_cli-0.8.4}/hypercli_cli/output.py +0 -0
- {hypercli_cli-0.8.2 → hypercli_cli-0.8.4}/hypercli_cli/renders.py +0 -0
- {hypercli_cli-0.8.2 → hypercli_cli-0.8.4}/hypercli_cli/tui/__init__.py +0 -0
- {hypercli_cli-0.8.2 → hypercli_cli-0.8.4}/hypercli_cli/tui/job_monitor.py +0 -0
- {hypercli_cli-0.8.2 → hypercli_cli-0.8.4}/hypercli_cli/user.py +0 -0
- {hypercli_cli-0.8.2 → hypercli_cli-0.8.4}/hypercli_cli/wallet.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.8.3"
|
|
@@ -7,9 +7,14 @@ import typer
|
|
|
7
7
|
from rich.console import Console
|
|
8
8
|
from rich.table import Table
|
|
9
9
|
|
|
10
|
+
from .onboard import onboard as _onboard_fn
|
|
11
|
+
|
|
10
12
|
app = typer.Typer(help="HyperClaw inference commands")
|
|
11
13
|
console = Console()
|
|
12
14
|
|
|
15
|
+
# Register onboard as a subcommand
|
|
16
|
+
app.command("onboard")(_onboard_fn)
|
|
17
|
+
|
|
13
18
|
# Check if wallet dependencies are available
|
|
14
19
|
try:
|
|
15
20
|
from x402 import x402Client
|
|
@@ -399,3 +404,161 @@ def openclaw_setup(
|
|
|
399
404
|
if default:
|
|
400
405
|
console.print(f" default model: hyperclaw/{models[0]['id']}")
|
|
401
406
|
console.print("\nRun: [bold]openclaw gateway restart[/bold]")
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
# ---------------------------------------------------------------------------
|
|
410
|
+
# hyper claw config — generate / apply provider configs for various tools
|
|
411
|
+
# ---------------------------------------------------------------------------
|
|
412
|
+
|
|
413
|
+
def _resolve_api_key(key: str | None) -> str:
|
|
414
|
+
"""Resolve API key from --key flag or ~/.hypercli/claw-key.json."""
|
|
415
|
+
if key:
|
|
416
|
+
return key
|
|
417
|
+
if CLAW_KEY_PATH.exists():
|
|
418
|
+
with open(CLAW_KEY_PATH) as f:
|
|
419
|
+
k = json.load(f).get("key", "")
|
|
420
|
+
if k:
|
|
421
|
+
return k
|
|
422
|
+
console.print("[red]❌ No API key found.[/red]")
|
|
423
|
+
console.print("Either pass [bold]--key sk-...[/bold] or subscribe first:")
|
|
424
|
+
console.print(" [bold]hyper claw subscribe 1aiu[/bold]")
|
|
425
|
+
raise typer.Exit(1)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def _config_openclaw(api_key: str, models: list[dict]) -> dict:
|
|
429
|
+
"""OpenClaw openclaw.json provider snippet."""
|
|
430
|
+
return {
|
|
431
|
+
"models": {
|
|
432
|
+
"mode": "merge",
|
|
433
|
+
"providers": {
|
|
434
|
+
"hyperclaw": {
|
|
435
|
+
"baseUrl": "https://api.hyperclaw.app/v1",
|
|
436
|
+
"apiKey": api_key,
|
|
437
|
+
"api": "openai-completions",
|
|
438
|
+
"models": models,
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
},
|
|
442
|
+
"agents": {
|
|
443
|
+
"defaults": {
|
|
444
|
+
"models": {
|
|
445
|
+
f"hyperclaw/{models[0]['id']}": {"alias": "kimi"}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def _config_opencode(api_key: str, models: list[dict]) -> dict:
|
|
453
|
+
"""OpenCode opencode.json provider snippet."""
|
|
454
|
+
model_entries = {}
|
|
455
|
+
for m in models:
|
|
456
|
+
model_entries[m["id"]] = {"name": m["id"]}
|
|
457
|
+
return {
|
|
458
|
+
"$schema": "https://opencode.ai/config.json",
|
|
459
|
+
"provider": {
|
|
460
|
+
"hypercli": {
|
|
461
|
+
"npm": "@ai-sdk/openai-compatible",
|
|
462
|
+
"name": "HyperCLI",
|
|
463
|
+
"options": {
|
|
464
|
+
"baseURL": "https://api.hyperclaw.app/v1",
|
|
465
|
+
"apiKey": api_key,
|
|
466
|
+
},
|
|
467
|
+
"models": model_entries,
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def _config_env(api_key: str, models: list[dict]) -> str:
|
|
474
|
+
"""Shell env vars for generic OpenAI-compatible tools."""
|
|
475
|
+
lines = [
|
|
476
|
+
f'export OPENAI_API_KEY="{api_key}"',
|
|
477
|
+
'export OPENAI_BASE_URL="https://api.hyperclaw.app/v1"',
|
|
478
|
+
f'# Available models: {", ".join(m["id"] for m in models)}',
|
|
479
|
+
]
|
|
480
|
+
return "\n".join(lines)
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
FORMAT_CHOICES = ["openclaw", "opencode", "env"]
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
@app.command("config")
|
|
487
|
+
def config_cmd(
|
|
488
|
+
format: str = typer.Argument(
|
|
489
|
+
None,
|
|
490
|
+
help=f"Output format: {', '.join(FORMAT_CHOICES)}. Omit to show all.",
|
|
491
|
+
),
|
|
492
|
+
key: str = typer.Option(None, "--key", "-k", help="API key (sk-...). Falls back to ~/.hypercli/claw-key.json"),
|
|
493
|
+
apply: bool = typer.Option(False, "--apply", help="Write config to the appropriate file (openclaw/opencode only)"),
|
|
494
|
+
dev: bool = typer.Option(False, "--dev", help="Use dev API"),
|
|
495
|
+
):
|
|
496
|
+
"""Generate provider configs for OpenClaw, OpenCode, and other tools.
|
|
497
|
+
|
|
498
|
+
Examples:
|
|
499
|
+
hyper claw config # Show all configs
|
|
500
|
+
hyper claw config openclaw # OpenClaw snippet
|
|
501
|
+
hyper claw config opencode --key sk-... # OpenCode with explicit key
|
|
502
|
+
hyper claw config openclaw --apply # Write directly to openclaw.json
|
|
503
|
+
hyper claw config env # Shell export lines
|
|
504
|
+
"""
|
|
505
|
+
api_key = _resolve_api_key(key)
|
|
506
|
+
api_base = DEV_API_BASE if dev else PROD_API_BASE
|
|
507
|
+
|
|
508
|
+
# Validate key & fetch models
|
|
509
|
+
console.print(f"[dim]Validating key against {api_base}...[/dim]")
|
|
510
|
+
models = fetch_models(api_key, api_base)
|
|
511
|
+
model_names = ", ".join(m["id"] for m in models)
|
|
512
|
+
console.print(f"[green]✓[/green] Key valid — models: [bold]{model_names}[/bold]\n")
|
|
513
|
+
|
|
514
|
+
formats = [format] if format else FORMAT_CHOICES
|
|
515
|
+
for fmt in formats:
|
|
516
|
+
if fmt not in FORMAT_CHOICES:
|
|
517
|
+
console.print(f"[red]Unknown format: {fmt}[/red]")
|
|
518
|
+
console.print(f"Choose from: {', '.join(FORMAT_CHOICES)}")
|
|
519
|
+
raise typer.Exit(1)
|
|
520
|
+
|
|
521
|
+
for fmt in formats:
|
|
522
|
+
if fmt == "openclaw":
|
|
523
|
+
snippet = _config_openclaw(api_key, models)
|
|
524
|
+
_show_snippet("OpenClaw", "~/.openclaw/openclaw.json", snippet, apply, OPENCLAW_CONFIG_PATH)
|
|
525
|
+
elif fmt == "opencode":
|
|
526
|
+
snippet = _config_opencode(api_key, models)
|
|
527
|
+
target = Path.cwd() / "opencode.json"
|
|
528
|
+
_show_snippet("OpenCode", "opencode.json", snippet, apply, target)
|
|
529
|
+
elif fmt == "env":
|
|
530
|
+
console.print("[bold]── Shell Environment ──[/bold]")
|
|
531
|
+
console.print(_config_env(api_key, models))
|
|
532
|
+
console.print()
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def _show_snippet(name: str, path_hint: str, data: dict, apply: bool, target_path: Path):
|
|
536
|
+
"""Print a JSON snippet and optionally apply it."""
|
|
537
|
+
console.print(f"[bold]── {name} ({path_hint}) ──[/bold]")
|
|
538
|
+
formatted = json.dumps(data, indent=2)
|
|
539
|
+
console.print(formatted)
|
|
540
|
+
console.print()
|
|
541
|
+
|
|
542
|
+
if apply:
|
|
543
|
+
if target_path.exists():
|
|
544
|
+
with open(target_path) as f:
|
|
545
|
+
existing = json.load(f)
|
|
546
|
+
_deep_merge(existing, data)
|
|
547
|
+
merged = existing
|
|
548
|
+
else:
|
|
549
|
+
merged = data
|
|
550
|
+
|
|
551
|
+
target_path.parent.mkdir(parents=True, exist_ok=True)
|
|
552
|
+
with open(target_path, "w") as f:
|
|
553
|
+
json.dump(merged, f, indent=2)
|
|
554
|
+
f.write("\n")
|
|
555
|
+
console.print(f"[green]✅ Written to {target_path}[/green]\n")
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def _deep_merge(base: dict, overlay: dict):
|
|
559
|
+
"""Recursively merge overlay into base (mutates base)."""
|
|
560
|
+
for k, v in overlay.items():
|
|
561
|
+
if k in base and isinstance(base[k], dict) and isinstance(v, dict):
|
|
562
|
+
_deep_merge(base[k], v)
|
|
563
|
+
else:
|
|
564
|
+
base[k] = v
|
|
@@ -0,0 +1,706 @@
|
|
|
1
|
+
"""HyperClaw onboarding flow — TUI + JSON mode"""
|
|
2
|
+
import asyncio
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import getpass
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from decimal import Decimal
|
|
12
|
+
|
|
13
|
+
import typer
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
from rich.table import Table
|
|
16
|
+
|
|
17
|
+
console = Console()
|
|
18
|
+
|
|
19
|
+
# Paths
|
|
20
|
+
HYPERCLI_DIR = Path.home() / ".hypercli"
|
|
21
|
+
ONBOARD_DIR = HYPERCLI_DIR / "onboard"
|
|
22
|
+
STATE_PATH = ONBOARD_DIR / "state.json"
|
|
23
|
+
QR_PATH = ONBOARD_DIR / "wallet_qr.png"
|
|
24
|
+
WALLET_PATH = HYPERCLI_DIR / "wallet.json"
|
|
25
|
+
CLAW_KEY_PATH = HYPERCLI_DIR / "claw-key.json"
|
|
26
|
+
OPENCLAW_CONFIG_PATH = Path.home() / ".openclaw" / "openclaw.json"
|
|
27
|
+
|
|
28
|
+
PROD_API_BASE = "https://api.hyperclaw.app"
|
|
29
|
+
DEV_API_BASE = "https://dev-api.hyperclaw.app"
|
|
30
|
+
|
|
31
|
+
BASE_RPC = "https://mainnet.base.org"
|
|
32
|
+
USDC_CONTRACT = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
|
|
33
|
+
|
|
34
|
+
TOTAL_STEPS = 6
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _require_deps():
|
|
38
|
+
"""Check wallet/x402 deps are available."""
|
|
39
|
+
try:
|
|
40
|
+
from web3 import Web3
|
|
41
|
+
from eth_account import Account
|
|
42
|
+
from x402 import x402Client
|
|
43
|
+
return True
|
|
44
|
+
except ImportError:
|
|
45
|
+
console.print("[red]❌ Onboarding requires wallet + x402 dependencies[/red]")
|
|
46
|
+
console.print("\nInstall with:")
|
|
47
|
+
console.print(' [bold]pip install "hypercli-cli[all]"[/bold]')
|
|
48
|
+
raise typer.Exit(1)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _load_state() -> dict:
|
|
52
|
+
"""Load onboard state from disk."""
|
|
53
|
+
if STATE_PATH.exists():
|
|
54
|
+
with open(STATE_PATH) as f:
|
|
55
|
+
return json.load(f)
|
|
56
|
+
return {"version": 1, "current_step": "wallet", "steps": {}}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _save_state(state: dict):
|
|
60
|
+
"""Write state to disk."""
|
|
61
|
+
ONBOARD_DIR.mkdir(parents=True, exist_ok=True)
|
|
62
|
+
state["updated_at"] = datetime.utcnow().isoformat() + "Z"
|
|
63
|
+
with open(STATE_PATH, "w") as f:
|
|
64
|
+
json.dump(state, f, indent=2)
|
|
65
|
+
f.write("\n")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _step_done(state: dict, step: str) -> bool:
|
|
69
|
+
return state.get("steps", {}).get(step, {}).get("status") == "complete"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _mark_step(state: dict, step: str, data: dict):
|
|
73
|
+
state.setdefault("steps", {})[step] = {**data, "status": "complete"}
|
|
74
|
+
state["current_step"] = step
|
|
75
|
+
_save_state(state)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _get_usdc_balance(address: str) -> Decimal:
|
|
79
|
+
"""Check USDC balance on Base."""
|
|
80
|
+
from web3 import Web3
|
|
81
|
+
w3 = Web3(Web3.HTTPProvider(BASE_RPC))
|
|
82
|
+
usdc_abi = [{
|
|
83
|
+
"constant": True,
|
|
84
|
+
"inputs": [{"name": "_owner", "type": "address"}],
|
|
85
|
+
"name": "balanceOf",
|
|
86
|
+
"outputs": [{"name": "balance", "type": "uint256"}],
|
|
87
|
+
"type": "function",
|
|
88
|
+
}]
|
|
89
|
+
usdc = w3.eth.contract(address=USDC_CONTRACT, abi=usdc_abi)
|
|
90
|
+
raw = usdc.functions.balanceOf(address).call()
|
|
91
|
+
return Decimal(raw) / Decimal("1000000")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _step_header(num: int, name: str):
|
|
95
|
+
console.print(f"\n[bold]Step {num}/{TOTAL_STEPS} — {name}[/bold]\n")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# ============================================================
|
|
99
|
+
# Step 1: Wallet
|
|
100
|
+
# ============================================================
|
|
101
|
+
def step_wallet(state: dict, json_mode: bool) -> dict:
|
|
102
|
+
from eth_account import Account
|
|
103
|
+
|
|
104
|
+
if _step_done(state, "wallet") and WALLET_PATH.exists():
|
|
105
|
+
with open(WALLET_PATH) as f:
|
|
106
|
+
addr = "0x" + json.load(f).get("address", "")
|
|
107
|
+
if not json_mode:
|
|
108
|
+
_step_header(1, "Wallet")
|
|
109
|
+
console.print(f"[green]✓[/green] Wallet: [bold]{addr}[/bold] (existing)")
|
|
110
|
+
_mark_step(state, "wallet", {"address": addr})
|
|
111
|
+
return state
|
|
112
|
+
|
|
113
|
+
if WALLET_PATH.exists():
|
|
114
|
+
with open(WALLET_PATH) as f:
|
|
115
|
+
addr = "0x" + json.load(f).get("address", "")
|
|
116
|
+
if not json_mode:
|
|
117
|
+
_step_header(1, "Wallet")
|
|
118
|
+
console.print(f"[green]✓[/green] Wallet: [bold]{addr}[/bold] (existing)")
|
|
119
|
+
_mark_step(state, "wallet", {"address": addr})
|
|
120
|
+
return state
|
|
121
|
+
|
|
122
|
+
if not json_mode:
|
|
123
|
+
_step_header(1, "Wallet")
|
|
124
|
+
console.print("No wallet found. Creating one...\n")
|
|
125
|
+
|
|
126
|
+
# Get passphrase
|
|
127
|
+
passphrase_env = os.getenv("HYPERCLI_WALLET_PASSPHRASE")
|
|
128
|
+
if passphrase_env:
|
|
129
|
+
passphrase = passphrase_env
|
|
130
|
+
elif json_mode:
|
|
131
|
+
# In JSON mode, use empty passphrase
|
|
132
|
+
passphrase = ""
|
|
133
|
+
else:
|
|
134
|
+
passphrase = getpass.getpass("Set a passphrase (or Enter for none): ")
|
|
135
|
+
if passphrase:
|
|
136
|
+
confirm = getpass.getpass("Confirm: ")
|
|
137
|
+
if passphrase != confirm:
|
|
138
|
+
console.print("[red]❌ Passphrases don't match![/red]")
|
|
139
|
+
raise typer.Exit(1)
|
|
140
|
+
|
|
141
|
+
account = Account.create()
|
|
142
|
+
keystore = account.encrypt(passphrase)
|
|
143
|
+
|
|
144
|
+
HYPERCLI_DIR.mkdir(parents=True, exist_ok=True)
|
|
145
|
+
with open(WALLET_PATH, "w") as f:
|
|
146
|
+
json.dump(keystore, f, indent=2)
|
|
147
|
+
os.chmod(WALLET_PATH, 0o600)
|
|
148
|
+
|
|
149
|
+
if not json_mode:
|
|
150
|
+
console.print(f"[green]✓[/green] Wallet: [bold]{account.address}[/bold]")
|
|
151
|
+
console.print(f"[green]✓[/green] Saved to {WALLET_PATH}")
|
|
152
|
+
|
|
153
|
+
_mark_step(state, "wallet", {"address": account.address})
|
|
154
|
+
return state
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# ============================================================
|
|
158
|
+
# Step 2: Fund wallet
|
|
159
|
+
# ============================================================
|
|
160
|
+
def step_fund(state: dict, json_mode: bool, poll_interval: int = 10) -> dict:
|
|
161
|
+
wallet_addr = state["steps"]["wallet"]["address"]
|
|
162
|
+
|
|
163
|
+
# Generate QR
|
|
164
|
+
ONBOARD_DIR.mkdir(parents=True, exist_ok=True)
|
|
165
|
+
try:
|
|
166
|
+
import qrcode
|
|
167
|
+
qr_obj = qrcode.QRCode(version=1, error_correction=qrcode.constants.ERROR_CORRECT_L,
|
|
168
|
+
box_size=10, border=2)
|
|
169
|
+
qr_obj.add_data(wallet_addr)
|
|
170
|
+
qr_obj.make(fit=True)
|
|
171
|
+
img = qr_obj.make_image(fill_color="black", back_color="white")
|
|
172
|
+
img.save(str(QR_PATH))
|
|
173
|
+
except ImportError:
|
|
174
|
+
pass
|
|
175
|
+
|
|
176
|
+
balance = _get_usdc_balance(wallet_addr)
|
|
177
|
+
|
|
178
|
+
if balance > 0:
|
|
179
|
+
if not json_mode:
|
|
180
|
+
_step_header(2, "Fund wallet")
|
|
181
|
+
console.print(f"[green]✓[/green] Balance: [bold]${balance:.2f} USDC[/bold] (skip)")
|
|
182
|
+
_mark_step(state, "funding", {"balance": str(balance), "address": wallet_addr,
|
|
183
|
+
"qr_path": str(QR_PATH)})
|
|
184
|
+
return state
|
|
185
|
+
|
|
186
|
+
if not json_mode:
|
|
187
|
+
_step_header(2, "Fund wallet")
|
|
188
|
+
console.print(f"Send USDC on [bold]Base[/bold] to:\n")
|
|
189
|
+
console.print(f" [bold]{wallet_addr}[/bold]\n")
|
|
190
|
+
|
|
191
|
+
# Show ASCII QR if possible
|
|
192
|
+
try:
|
|
193
|
+
import qrcode
|
|
194
|
+
qr_obj = qrcode.QRCode(version=1, error_correction=qrcode.constants.ERROR_CORRECT_L,
|
|
195
|
+
box_size=1, border=1)
|
|
196
|
+
qr_obj.add_data(wallet_addr)
|
|
197
|
+
qr_obj.make(fit=True)
|
|
198
|
+
qr_obj.print_ascii(invert=True)
|
|
199
|
+
console.print()
|
|
200
|
+
except ImportError:
|
|
201
|
+
pass
|
|
202
|
+
|
|
203
|
+
if QR_PATH.exists():
|
|
204
|
+
console.print(f"[dim]QR saved: {QR_PATH}[/dim]\n")
|
|
205
|
+
|
|
206
|
+
console.print("Waiting for funds (Ctrl+C to resume later)\n")
|
|
207
|
+
|
|
208
|
+
# Save intermediate state so agent can pick up QR path
|
|
209
|
+
state.setdefault("steps", {})["funding"] = {
|
|
210
|
+
"status": "waiting",
|
|
211
|
+
"address": wallet_addr,
|
|
212
|
+
"balance": "0.00",
|
|
213
|
+
"qr_path": str(QR_PATH),
|
|
214
|
+
}
|
|
215
|
+
state["current_step"] = "funding"
|
|
216
|
+
_save_state(state)
|
|
217
|
+
|
|
218
|
+
# Poll
|
|
219
|
+
while True:
|
|
220
|
+
balance = _get_usdc_balance(wallet_addr)
|
|
221
|
+
if balance > 0:
|
|
222
|
+
if not json_mode:
|
|
223
|
+
console.print(f" ${balance:.2f} [green]✓[/green]")
|
|
224
|
+
console.print(f"\n[green]✓[/green] Balance: [bold]${balance:.2f} USDC[/bold]")
|
|
225
|
+
_mark_step(state, "funding", {"balance": str(balance), "address": wallet_addr,
|
|
226
|
+
"qr_path": str(QR_PATH)})
|
|
227
|
+
return state
|
|
228
|
+
else:
|
|
229
|
+
if not json_mode:
|
|
230
|
+
console.print(f" ${balance:.2f} ⏳")
|
|
231
|
+
# Update state file for agent polling
|
|
232
|
+
state["steps"]["funding"]["balance"] = str(balance)
|
|
233
|
+
_save_state(state)
|
|
234
|
+
time.sleep(poll_interval)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
# ============================================================
|
|
238
|
+
# Step 3: Choose plan
|
|
239
|
+
# ============================================================
|
|
240
|
+
def step_plan(state: dict, json_mode: bool, api_base: str,
|
|
241
|
+
plan_override: str = None, amount_override: str = None) -> dict:
|
|
242
|
+
import httpx
|
|
243
|
+
|
|
244
|
+
if _step_done(state, "plan"):
|
|
245
|
+
plan_data = state["steps"]["plan"]
|
|
246
|
+
if not json_mode:
|
|
247
|
+
_step_header(3, "Choose plan")
|
|
248
|
+
console.print(f"[green]✓[/green] Plan: {plan_data['plan']} — ${plan_data['amount']} USDC (skip)")
|
|
249
|
+
return state
|
|
250
|
+
|
|
251
|
+
# Fetch plans from API
|
|
252
|
+
try:
|
|
253
|
+
resp = httpx.get(f"{api_base}/api/plans", timeout=10)
|
|
254
|
+
resp.raise_for_status()
|
|
255
|
+
plans = resp.json().get("plans", [])
|
|
256
|
+
except Exception as e:
|
|
257
|
+
console.print(f"[red]❌ Failed to fetch plans: {e}[/red]")
|
|
258
|
+
raise typer.Exit(1)
|
|
259
|
+
|
|
260
|
+
if not json_mode:
|
|
261
|
+
_step_header(3, "Choose plan")
|
|
262
|
+
|
|
263
|
+
table = Table(show_header=True, header_style="bold")
|
|
264
|
+
table.add_column("Plan", style="cyan")
|
|
265
|
+
table.add_column("Name", style="green")
|
|
266
|
+
table.add_column("Price", style="yellow")
|
|
267
|
+
table.add_column("TPM", style="magenta")
|
|
268
|
+
table.add_column("RPM", style="magenta")
|
|
269
|
+
|
|
270
|
+
for p in plans:
|
|
271
|
+
table.add_row(
|
|
272
|
+
p["id"],
|
|
273
|
+
p["name"],
|
|
274
|
+
f"${p['price']}/mo",
|
|
275
|
+
f"{p['tpm_limit']:,}",
|
|
276
|
+
f"{p['rpm_limit']:,}",
|
|
277
|
+
)
|
|
278
|
+
console.print(table)
|
|
279
|
+
console.print()
|
|
280
|
+
|
|
281
|
+
# Select plan
|
|
282
|
+
if plan_override:
|
|
283
|
+
plan_id = plan_override
|
|
284
|
+
elif json_mode:
|
|
285
|
+
plan_id = "1aiu"
|
|
286
|
+
else:
|
|
287
|
+
plan_id = typer.prompt("Select plan", default="1aiu")
|
|
288
|
+
|
|
289
|
+
# Find plan details
|
|
290
|
+
plan_info = next((p for p in plans if p["id"] == plan_id), None)
|
|
291
|
+
if not plan_info:
|
|
292
|
+
console.print(f"[red]❌ Unknown plan: {plan_id}[/red]")
|
|
293
|
+
raise typer.Exit(1)
|
|
294
|
+
|
|
295
|
+
# Amount
|
|
296
|
+
default_amount = str(plan_info["price"])
|
|
297
|
+
if amount_override:
|
|
298
|
+
amount = amount_override
|
|
299
|
+
elif json_mode:
|
|
300
|
+
amount = default_amount
|
|
301
|
+
else:
|
|
302
|
+
amount = typer.prompt("Payment amount", default=f"${default_amount}").replace("$", "")
|
|
303
|
+
|
|
304
|
+
if not json_mode:
|
|
305
|
+
console.print(f"\n[green]✓[/green] Plan: {plan_id} ({plan_info['name']}) — ${amount} USDC")
|
|
306
|
+
|
|
307
|
+
_mark_step(state, "plan", {"plan": plan_id, "amount": amount, "name": plan_info["name"],
|
|
308
|
+
"tpm": plan_info["tpm_limit"], "rpm": plan_info["rpm_limit"]})
|
|
309
|
+
return state
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
# ============================================================
|
|
313
|
+
# Step 4: Subscribe
|
|
314
|
+
# ============================================================
|
|
315
|
+
def step_subscribe(state: dict, json_mode: bool, api_base: str) -> dict:
|
|
316
|
+
if _step_done(state, "subscribe"):
|
|
317
|
+
sub = state["steps"]["subscribe"]
|
|
318
|
+
if not json_mode:
|
|
319
|
+
_step_header(4, "Subscribe")
|
|
320
|
+
console.print(f"[green]✓[/green] Already subscribed — key: {sub['key'][:20]}... (skip)")
|
|
321
|
+
return state
|
|
322
|
+
|
|
323
|
+
from .wallet import load_wallet
|
|
324
|
+
from .claw import _subscribe_async
|
|
325
|
+
|
|
326
|
+
plan_data = state["steps"]["plan"]
|
|
327
|
+
plan_id = plan_data["plan"]
|
|
328
|
+
amount = plan_data["amount"]
|
|
329
|
+
|
|
330
|
+
if not json_mode:
|
|
331
|
+
_step_header(4, "Subscribe")
|
|
332
|
+
|
|
333
|
+
account = load_wallet()
|
|
334
|
+
|
|
335
|
+
if not json_mode:
|
|
336
|
+
console.print(f"[green]✓[/green] Wallet loaded: {account.address}\n")
|
|
337
|
+
|
|
338
|
+
result = asyncio.run(_subscribe_async(account, plan_id, api_base, amount))
|
|
339
|
+
|
|
340
|
+
if not result:
|
|
341
|
+
console.print("[red]❌ Subscription failed[/red]")
|
|
342
|
+
raise typer.Exit(1)
|
|
343
|
+
|
|
344
|
+
# Save key
|
|
345
|
+
HYPERCLI_DIR.mkdir(parents=True, exist_ok=True)
|
|
346
|
+
with open(CLAW_KEY_PATH, "w") as f:
|
|
347
|
+
json.dump(result, f, indent=2)
|
|
348
|
+
|
|
349
|
+
# Save to key history
|
|
350
|
+
try:
|
|
351
|
+
import yaml
|
|
352
|
+
history_entry = {
|
|
353
|
+
"key": result["key"],
|
|
354
|
+
"plan": result["plan_id"],
|
|
355
|
+
"amount_usdc": result.get("amount_paid", ""),
|
|
356
|
+
"date": datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC"),
|
|
357
|
+
"expires": result.get("expires_at", ""),
|
|
358
|
+
"tpm_limit": result.get("tpm_limit", 0),
|
|
359
|
+
"rpm_limit": result.get("rpm_limit", 0),
|
|
360
|
+
}
|
|
361
|
+
keys_path = HYPERCLI_DIR / "claw-keys.yaml"
|
|
362
|
+
if keys_path.exists():
|
|
363
|
+
with open(keys_path) as f:
|
|
364
|
+
keys_data = yaml.safe_load(f) or {"keys": []}
|
|
365
|
+
else:
|
|
366
|
+
keys_data = {"keys": []}
|
|
367
|
+
keys_data["keys"].append(history_entry)
|
|
368
|
+
with open(keys_path, "w") as f:
|
|
369
|
+
yaml.dump(keys_data, f, default_flow_style=False, sort_keys=False)
|
|
370
|
+
except ImportError:
|
|
371
|
+
pass
|
|
372
|
+
|
|
373
|
+
if not json_mode:
|
|
374
|
+
console.print(f"\n[green]✅ Subscribed![/green]")
|
|
375
|
+
console.print(f" Key: [bold]{result['key']}[/bold]")
|
|
376
|
+
console.print(f" Plan: {result['plan_id']}")
|
|
377
|
+
console.print(f" Expires: {result.get('expires_at', 'N/A')}")
|
|
378
|
+
console.print(f" Limits: {result.get('tpm_limit', 0):,} TPM / {result.get('rpm_limit', 0):,} RPM")
|
|
379
|
+
console.print(f"\n[green]✓[/green] Key saved to {CLAW_KEY_PATH}")
|
|
380
|
+
|
|
381
|
+
_mark_step(state, "subscribe", {
|
|
382
|
+
"key": result["key"],
|
|
383
|
+
"plan": result["plan_id"],
|
|
384
|
+
"expires": result.get("expires_at", ""),
|
|
385
|
+
"tpm": result.get("tpm_limit", 0),
|
|
386
|
+
"rpm": result.get("rpm_limit", 0),
|
|
387
|
+
})
|
|
388
|
+
return state
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
# ============================================================
|
|
392
|
+
# Step 5: Configure OpenClaw
|
|
393
|
+
# ============================================================
|
|
394
|
+
def step_configure(state: dict, json_mode: bool, api_base: str) -> dict:
|
|
395
|
+
if _step_done(state, "configure"):
|
|
396
|
+
cfg = state["steps"]["configure"]
|
|
397
|
+
if not json_mode:
|
|
398
|
+
_step_header(5, "Configure OpenClaw")
|
|
399
|
+
console.print(f"[green]✓[/green] Already configured (skip)")
|
|
400
|
+
return state
|
|
401
|
+
|
|
402
|
+
api_key = state["steps"]["subscribe"]["key"]
|
|
403
|
+
|
|
404
|
+
if not json_mode:
|
|
405
|
+
_step_header(5, "Configure OpenClaw")
|
|
406
|
+
|
|
407
|
+
if not OPENCLAW_CONFIG_PATH.exists():
|
|
408
|
+
if not json_mode:
|
|
409
|
+
console.print("[yellow]⚠[/yellow] OpenClaw not detected (no ~/.openclaw/openclaw.json)")
|
|
410
|
+
console.print(f" Configure manually later:\n")
|
|
411
|
+
console.print(f" Base URL: {api_base}/v1")
|
|
412
|
+
console.print(f" API Key: {api_key}")
|
|
413
|
+
_mark_step(state, "configure", {"status": "complete", "openclaw_detected": False})
|
|
414
|
+
return state
|
|
415
|
+
|
|
416
|
+
# Fetch models
|
|
417
|
+
if not json_mode:
|
|
418
|
+
console.print("Detected OpenClaw config at ~/.openclaw/openclaw.json")
|
|
419
|
+
console.print("Fetching available models... ", end="")
|
|
420
|
+
|
|
421
|
+
from .claw import fetch_models
|
|
422
|
+
models = fetch_models(api_key, api_base)
|
|
423
|
+
|
|
424
|
+
if not json_mode:
|
|
425
|
+
console.print("[green]✓[/green]")
|
|
426
|
+
for m in models:
|
|
427
|
+
console.print(f" Model: {m['id']}")
|
|
428
|
+
console.print()
|
|
429
|
+
|
|
430
|
+
# Read existing config
|
|
431
|
+
with open(OPENCLAW_CONFIG_PATH) as f:
|
|
432
|
+
config = json.load(f)
|
|
433
|
+
|
|
434
|
+
# Patch
|
|
435
|
+
config.setdefault("models", {}).setdefault("providers", {})
|
|
436
|
+
config["models"]["providers"]["hyperclaw"] = {
|
|
437
|
+
"baseUrl": f"{api_base}/v1",
|
|
438
|
+
"apiKey": api_key,
|
|
439
|
+
"api": "openai-completions",
|
|
440
|
+
"models": models,
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if not json_mode:
|
|
444
|
+
console.print("Patching config... [green]✓[/green]")
|
|
445
|
+
|
|
446
|
+
# Ask about default model
|
|
447
|
+
set_default = False
|
|
448
|
+
if models:
|
|
449
|
+
if json_mode:
|
|
450
|
+
set_default = True
|
|
451
|
+
else:
|
|
452
|
+
set_default = typer.confirm("Set as default model?", default=True)
|
|
453
|
+
|
|
454
|
+
default_model = None
|
|
455
|
+
if set_default and models:
|
|
456
|
+
default_model = f"hyperclaw/{models[0]['id']}"
|
|
457
|
+
config.setdefault("agents", {}).setdefault("defaults", {}).setdefault("model", {})
|
|
458
|
+
config["agents"]["defaults"]["model"]["primary"] = default_model
|
|
459
|
+
if not json_mode:
|
|
460
|
+
console.print(f"\n[green]✓[/green] Provider: hyperclaw added")
|
|
461
|
+
console.print(f"[green]✓[/green] Default model: {default_model}")
|
|
462
|
+
else:
|
|
463
|
+
if not json_mode:
|
|
464
|
+
console.print(f"\n[green]✓[/green] Provider: hyperclaw added")
|
|
465
|
+
|
|
466
|
+
# Write config
|
|
467
|
+
with open(OPENCLAW_CONFIG_PATH, "w") as f:
|
|
468
|
+
json.dump(config, f, indent=2)
|
|
469
|
+
f.write("\n")
|
|
470
|
+
|
|
471
|
+
_mark_step(state, "configure", {
|
|
472
|
+
"openclaw_detected": True,
|
|
473
|
+
"config_path": str(OPENCLAW_CONFIG_PATH),
|
|
474
|
+
"default_model": default_model,
|
|
475
|
+
"models": [m["id"] for m in models],
|
|
476
|
+
})
|
|
477
|
+
return state
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
# ============================================================
|
|
481
|
+
# Step 6: Verify + Restart
|
|
482
|
+
# ============================================================
|
|
483
|
+
def step_verify(state: dict, json_mode: bool, api_base: str) -> dict:
|
|
484
|
+
import httpx
|
|
485
|
+
|
|
486
|
+
api_key = state["steps"]["subscribe"]["key"]
|
|
487
|
+
|
|
488
|
+
if not json_mode:
|
|
489
|
+
_step_header(6, "Verify")
|
|
490
|
+
console.print("Testing inference against kimi-k2.5...\n")
|
|
491
|
+
|
|
492
|
+
try:
|
|
493
|
+
start = time.time()
|
|
494
|
+
resp = httpx.post(
|
|
495
|
+
f"{api_base}/v1/chat/completions",
|
|
496
|
+
headers={"Authorization": f"Bearer {api_key}"},
|
|
497
|
+
json={
|
|
498
|
+
"model": "kimi-k2.5",
|
|
499
|
+
"messages": [{"role": "user", "content": "What is 2+2? Answer with just the number."}],
|
|
500
|
+
"max_tokens": 32,
|
|
501
|
+
},
|
|
502
|
+
timeout=30,
|
|
503
|
+
)
|
|
504
|
+
elapsed = time.time() - start
|
|
505
|
+
resp.raise_for_status()
|
|
506
|
+
data = resp.json()
|
|
507
|
+
content = data["choices"][0]["message"]["content"].strip()
|
|
508
|
+
tokens = data.get("usage", {}).get("total_tokens", 0)
|
|
509
|
+
|
|
510
|
+
if not json_mode:
|
|
511
|
+
console.print(f' → "What is 2+2?"')
|
|
512
|
+
console.print(f' ← "{content}" ({elapsed:.1f}s, {tokens} tokens)\n')
|
|
513
|
+
console.print("[green]✅ All good![/green]")
|
|
514
|
+
|
|
515
|
+
_mark_step(state, "verify", {"model": "kimi-k2.5", "latency_ms": int(elapsed * 1000),
|
|
516
|
+
"tokens": tokens, "response": content})
|
|
517
|
+
|
|
518
|
+
except Exception as e:
|
|
519
|
+
if not json_mode:
|
|
520
|
+
console.print(f"[yellow]⚠[/yellow] Inference test failed: {e}")
|
|
521
|
+
console.print(" Key may need a moment to propagate. Try:")
|
|
522
|
+
console.print(" hyper claw onboard")
|
|
523
|
+
_mark_step(state, "verify", {"status": "complete", "error": str(e)})
|
|
524
|
+
|
|
525
|
+
# Restart OpenClaw
|
|
526
|
+
openclaw_detected = state.get("steps", {}).get("configure", {}).get("openclaw_detected", False)
|
|
527
|
+
if openclaw_detected:
|
|
528
|
+
do_restart = False
|
|
529
|
+
if json_mode:
|
|
530
|
+
do_restart = True
|
|
531
|
+
else:
|
|
532
|
+
console.print()
|
|
533
|
+
do_restart = typer.confirm("Restart OpenClaw now?", default=True)
|
|
534
|
+
|
|
535
|
+
if do_restart:
|
|
536
|
+
if not json_mode:
|
|
537
|
+
console.print("Restarting... ", end="")
|
|
538
|
+
try:
|
|
539
|
+
subprocess.run(["openclaw", "gateway", "restart"], capture_output=True, timeout=10)
|
|
540
|
+
if not json_mode:
|
|
541
|
+
console.print("[green]✓[/green]")
|
|
542
|
+
except Exception as e:
|
|
543
|
+
if not json_mode:
|
|
544
|
+
console.print(f"[yellow]⚠ {e}[/yellow]")
|
|
545
|
+
console.print(" Run manually: [bold]openclaw gateway restart[/bold]")
|
|
546
|
+
else:
|
|
547
|
+
if not json_mode:
|
|
548
|
+
console.print("\n Run when ready: [bold]openclaw gateway restart[/bold]")
|
|
549
|
+
|
|
550
|
+
# Final banner
|
|
551
|
+
sub = state["steps"]["subscribe"]
|
|
552
|
+
if not json_mode:
|
|
553
|
+
console.print(f"\n{'─' * 40}\n")
|
|
554
|
+
console.print(f" 🐾 [bold]HyperClaw is ready.[/bold]\n")
|
|
555
|
+
console.print(f" API: {api_base}/v1")
|
|
556
|
+
console.print(f" Model: kimi-k2.5")
|
|
557
|
+
console.print(f" Key: {sub['key'][:20]}...")
|
|
558
|
+
console.print(f" Expires: {sub['expires']}")
|
|
559
|
+
console.print(f"\n Check status: [bold]hyper claw status[/bold]")
|
|
560
|
+
console.print(f"\n{'─' * 40}")
|
|
561
|
+
|
|
562
|
+
state["current_step"] = "done"
|
|
563
|
+
_save_state(state)
|
|
564
|
+
return state
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
# ============================================================
|
|
568
|
+
# Main onboard command
|
|
569
|
+
# ============================================================
|
|
570
|
+
def onboard(
|
|
571
|
+
json_mode: bool = typer.Option(False, "--json", help="JSON mode — write state to disk, minimal stdout"),
|
|
572
|
+
plan: str = typer.Option(None, "--plan", help="Plan ID (skip prompt)"),
|
|
573
|
+
amount: str = typer.Option(None, "--amount", help="USDC amount (skip prompt)"),
|
|
574
|
+
dev: bool = typer.Option(False, "--dev", help="Use dev API"),
|
|
575
|
+
reset: bool = typer.Option(False, "--reset", help="Start fresh (delete state)"),
|
|
576
|
+
status: bool = typer.Option(False, "--status", help="Show current onboard state and exit"),
|
|
577
|
+
poll_interval: int = typer.Option(10, "--poll", help="Balance poll interval in seconds"),
|
|
578
|
+
dry_run: bool = typer.Option(False, "--dry-run", help="Walk through all steps without making changes"),
|
|
579
|
+
):
|
|
580
|
+
"""Guided onboarding: wallet → fund → plan → subscribe → configure → verify"""
|
|
581
|
+
|
|
582
|
+
if status:
|
|
583
|
+
state = _load_state()
|
|
584
|
+
console.print(json.dumps(state, indent=2))
|
|
585
|
+
raise typer.Exit(0)
|
|
586
|
+
|
|
587
|
+
if reset:
|
|
588
|
+
if STATE_PATH.exists():
|
|
589
|
+
STATE_PATH.unlink()
|
|
590
|
+
console.print("[green]✓[/green] Onboard state cleared")
|
|
591
|
+
raise typer.Exit(0)
|
|
592
|
+
|
|
593
|
+
if not dry_run:
|
|
594
|
+
_require_deps()
|
|
595
|
+
|
|
596
|
+
api_base = DEV_API_BASE if dev else PROD_API_BASE
|
|
597
|
+
|
|
598
|
+
if dry_run:
|
|
599
|
+
_run_dry(api_base, plan_override=plan, amount_override=amount)
|
|
600
|
+
return
|
|
601
|
+
|
|
602
|
+
state = _load_state()
|
|
603
|
+
|
|
604
|
+
if not json_mode:
|
|
605
|
+
console.print("\n[bold]🐾 HyperClaw Onboarding[/bold]\n")
|
|
606
|
+
|
|
607
|
+
# Run steps in order, skipping completed ones
|
|
608
|
+
state = step_wallet(state, json_mode)
|
|
609
|
+
state = step_fund(state, json_mode, poll_interval)
|
|
610
|
+
state = step_plan(state, json_mode, api_base, plan_override=plan, amount_override=amount)
|
|
611
|
+
state = step_subscribe(state, json_mode, api_base)
|
|
612
|
+
state = step_configure(state, json_mode, api_base)
|
|
613
|
+
state = step_verify(state, json_mode, api_base)
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def _run_dry(api_base: str, plan_override: str = None, amount_override: str = None):
|
|
617
|
+
"""Walk through the full onboard flow without making any changes."""
|
|
618
|
+
import httpx
|
|
619
|
+
|
|
620
|
+
console.print("\n[bold]🐾 HyperClaw Onboarding (dry run)[/bold]\n")
|
|
621
|
+
console.print("[dim]No changes will be made.[/dim]\n")
|
|
622
|
+
|
|
623
|
+
# Step 1: Wallet
|
|
624
|
+
_step_header(1, "Wallet")
|
|
625
|
+
if WALLET_PATH.exists():
|
|
626
|
+
with open(WALLET_PATH) as f:
|
|
627
|
+
addr = "0x" + json.load(f).get("address", "")
|
|
628
|
+
console.print(f"[green]✓[/green] Wallet: [bold]{addr}[/bold] (existing)")
|
|
629
|
+
else:
|
|
630
|
+
console.print("[yellow]→[/yellow] Would create new wallet at ~/.hypercli/wallet.json")
|
|
631
|
+
addr = "0x0000000000000000000000000000000000000000"
|
|
632
|
+
|
|
633
|
+
# Step 2: Fund
|
|
634
|
+
_step_header(2, "Fund wallet")
|
|
635
|
+
if WALLET_PATH.exists():
|
|
636
|
+
try:
|
|
637
|
+
balance = _get_usdc_balance(addr)
|
|
638
|
+
console.print(f" Address: [bold]{addr}[/bold]")
|
|
639
|
+
console.print(f" Balance: [bold]${balance:.2f} USDC[/bold]")
|
|
640
|
+
if balance == 0:
|
|
641
|
+
console.print("[yellow]→[/yellow] Would poll every 10s until funded")
|
|
642
|
+
else:
|
|
643
|
+
console.print(f"[green]✓[/green] Already funded")
|
|
644
|
+
except Exception:
|
|
645
|
+
console.print(f" Address: [bold]{addr}[/bold]")
|
|
646
|
+
console.print("[yellow]→[/yellow] Would poll balance until funded")
|
|
647
|
+
else:
|
|
648
|
+
console.print("[yellow]→[/yellow] Would show QR code and poll balance")
|
|
649
|
+
console.print(f" QR path: {QR_PATH}")
|
|
650
|
+
|
|
651
|
+
# Step 3: Plan
|
|
652
|
+
_step_header(3, "Choose plan")
|
|
653
|
+
try:
|
|
654
|
+
resp = httpx.get(f"{api_base}/api/plans", timeout=10)
|
|
655
|
+
resp.raise_for_status()
|
|
656
|
+
plans = resp.json().get("plans", [])
|
|
657
|
+
|
|
658
|
+
from rich.table import Table
|
|
659
|
+
table = Table(show_header=True, header_style="bold")
|
|
660
|
+
table.add_column("Plan", style="cyan")
|
|
661
|
+
table.add_column("Name", style="green")
|
|
662
|
+
table.add_column("Price", style="yellow")
|
|
663
|
+
table.add_column("TPM", style="magenta")
|
|
664
|
+
table.add_column("RPM", style="magenta")
|
|
665
|
+
for p in plans:
|
|
666
|
+
table.add_row(p["id"], p["name"], f"${p['price']}/mo",
|
|
667
|
+
f"{p['tpm_limit']:,}", f"{p['rpm_limit']:,}")
|
|
668
|
+
console.print(table)
|
|
669
|
+
|
|
670
|
+
plan_id = plan_override or "1aiu"
|
|
671
|
+
plan_info = next((p for p in plans if p["id"] == plan_id), plans[0] if plans else None)
|
|
672
|
+
amt = amount_override or str(plan_info["price"]) if plan_info else "35"
|
|
673
|
+
console.print(f"\n[green]✓[/green] Would select: {plan_id} ({plan_info['name'] if plan_info else '?'}) — ${amt} USDC")
|
|
674
|
+
except Exception as e:
|
|
675
|
+
console.print(f"[yellow]⚠ Could not fetch plans: {e}[/yellow]")
|
|
676
|
+
console.print("[yellow]→[/yellow] Would prompt for plan selection")
|
|
677
|
+
|
|
678
|
+
# Step 4: Subscribe
|
|
679
|
+
_step_header(4, "Subscribe")
|
|
680
|
+
console.print("[yellow]→[/yellow] Would sign x402 payment and submit")
|
|
681
|
+
console.print("[yellow]→[/yellow] Would save key to ~/.hypercli/claw-key.json")
|
|
682
|
+
if CLAW_KEY_PATH.exists():
|
|
683
|
+
with open(CLAW_KEY_PATH) as f:
|
|
684
|
+
existing = json.load(f)
|
|
685
|
+
console.print(f" [dim]Existing key: {existing.get('key', '?')[:20]}...[/dim]")
|
|
686
|
+
|
|
687
|
+
# Step 5: Configure
|
|
688
|
+
_step_header(5, "Configure OpenClaw")
|
|
689
|
+
if OPENCLAW_CONFIG_PATH.exists():
|
|
690
|
+
console.print(f"[green]✓[/green] OpenClaw detected at {OPENCLAW_CONFIG_PATH}")
|
|
691
|
+
console.print("[yellow]→[/yellow] Would patch models.providers.hyperclaw")
|
|
692
|
+
console.print("[yellow]→[/yellow] Would prompt to set default model")
|
|
693
|
+
else:
|
|
694
|
+
console.print("[yellow]⚠[/yellow] OpenClaw not detected")
|
|
695
|
+
console.print("[yellow]→[/yellow] Would skip, show manual config instructions")
|
|
696
|
+
|
|
697
|
+
# Step 6: Verify
|
|
698
|
+
_step_header(6, "Verify")
|
|
699
|
+
console.print("[yellow]→[/yellow] Would test inference: \"What is 2+2?\"")
|
|
700
|
+
console.print("[yellow]→[/yellow] Would prompt to restart OpenClaw")
|
|
701
|
+
|
|
702
|
+
# Summary
|
|
703
|
+
console.print(f"\n{'─' * 40}")
|
|
704
|
+
console.print(f"\n [bold]Dry run complete.[/bold] No changes made.")
|
|
705
|
+
console.print(f" Run [bold]hyper claw onboard[/bold] to start for real.\n")
|
|
706
|
+
console.print(f"{'─' * 40}")
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.6.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|