hypercli-cli 0.8.1__tar.gz → 0.8.3__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.1 → hypercli_cli-0.8.3}/PKG-INFO +1 -1
- hypercli_cli-0.8.3/hypercli_cli/__init__.py +1 -0
- {hypercli_cli-0.8.1 → hypercli_cli-0.8.3}/hypercli_cli/claw.py +5 -2
- hypercli_cli-0.8.3/hypercli_cli/onboard.py +706 -0
- {hypercli_cli-0.8.1 → hypercli_cli-0.8.3}/pyproject.toml +1 -1
- hypercli_cli-0.8.1/hypercli_cli/__init__.py +0 -1
- {hypercli_cli-0.8.1 → hypercli_cli-0.8.3}/.gitignore +0 -0
- {hypercli_cli-0.8.1 → hypercli_cli-0.8.3}/README.md +0 -0
- {hypercli_cli-0.8.1 → hypercli_cli-0.8.3}/hypercli_cli/billing.py +0 -0
- {hypercli_cli-0.8.1 → hypercli_cli-0.8.3}/hypercli_cli/cli.py +0 -0
- {hypercli_cli-0.8.1 → hypercli_cli-0.8.3}/hypercli_cli/comfyui.py +0 -0
- {hypercli_cli-0.8.1 → hypercli_cli-0.8.3}/hypercli_cli/flow.py +0 -0
- {hypercli_cli-0.8.1 → hypercli_cli-0.8.3}/hypercli_cli/instances.py +0 -0
- {hypercli_cli-0.8.1 → hypercli_cli-0.8.3}/hypercli_cli/jobs.py +0 -0
- {hypercli_cli-0.8.1 → hypercli_cli-0.8.3}/hypercli_cli/keys.py +0 -0
- {hypercli_cli-0.8.1 → hypercli_cli-0.8.3}/hypercli_cli/llm.py +0 -0
- {hypercli_cli-0.8.1 → hypercli_cli-0.8.3}/hypercli_cli/output.py +0 -0
- {hypercli_cli-0.8.1 → hypercli_cli-0.8.3}/hypercli_cli/renders.py +0 -0
- {hypercli_cli-0.8.1 → hypercli_cli-0.8.3}/hypercli_cli/tui/__init__.py +0 -0
- {hypercli_cli-0.8.1 → hypercli_cli-0.8.3}/hypercli_cli/tui/job_monitor.py +0 -0
- {hypercli_cli-0.8.1 → hypercli_cli-0.8.3}/hypercli_cli/user.py +0 -0
- {hypercli_cli-0.8.1 → hypercli_cli-0.8.3}/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
|
|
@@ -320,7 +325,6 @@ def fetch_models(api_key: str, api_base: str = PROD_API_BASE) -> list[dict]:
|
|
|
320
325
|
"reasoning": False,
|
|
321
326
|
"input": ["text"],
|
|
322
327
|
"contextWindow": 200000,
|
|
323
|
-
"maxTokens": 8192,
|
|
324
328
|
}
|
|
325
329
|
for m in data
|
|
326
330
|
if m.get("id")
|
|
@@ -335,7 +339,6 @@ def fetch_models(api_key: str, api_base: str = PROD_API_BASE) -> list[dict]:
|
|
|
335
339
|
"reasoning": False,
|
|
336
340
|
"input": ["text"],
|
|
337
341
|
"contextWindow": 200000,
|
|
338
|
-
"maxTokens": 8192,
|
|
339
342
|
},
|
|
340
343
|
]
|
|
341
344
|
|
|
@@ -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
|