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.
Files changed (22) hide show
  1. {hypercli_cli-0.8.1 → hypercli_cli-0.8.3}/PKG-INFO +1 -1
  2. hypercli_cli-0.8.3/hypercli_cli/__init__.py +1 -0
  3. {hypercli_cli-0.8.1 → hypercli_cli-0.8.3}/hypercli_cli/claw.py +5 -2
  4. hypercli_cli-0.8.3/hypercli_cli/onboard.py +706 -0
  5. {hypercli_cli-0.8.1 → hypercli_cli-0.8.3}/pyproject.toml +1 -1
  6. hypercli_cli-0.8.1/hypercli_cli/__init__.py +0 -1
  7. {hypercli_cli-0.8.1 → hypercli_cli-0.8.3}/.gitignore +0 -0
  8. {hypercli_cli-0.8.1 → hypercli_cli-0.8.3}/README.md +0 -0
  9. {hypercli_cli-0.8.1 → hypercli_cli-0.8.3}/hypercli_cli/billing.py +0 -0
  10. {hypercli_cli-0.8.1 → hypercli_cli-0.8.3}/hypercli_cli/cli.py +0 -0
  11. {hypercli_cli-0.8.1 → hypercli_cli-0.8.3}/hypercli_cli/comfyui.py +0 -0
  12. {hypercli_cli-0.8.1 → hypercli_cli-0.8.3}/hypercli_cli/flow.py +0 -0
  13. {hypercli_cli-0.8.1 → hypercli_cli-0.8.3}/hypercli_cli/instances.py +0 -0
  14. {hypercli_cli-0.8.1 → hypercli_cli-0.8.3}/hypercli_cli/jobs.py +0 -0
  15. {hypercli_cli-0.8.1 → hypercli_cli-0.8.3}/hypercli_cli/keys.py +0 -0
  16. {hypercli_cli-0.8.1 → hypercli_cli-0.8.3}/hypercli_cli/llm.py +0 -0
  17. {hypercli_cli-0.8.1 → hypercli_cli-0.8.3}/hypercli_cli/output.py +0 -0
  18. {hypercli_cli-0.8.1 → hypercli_cli-0.8.3}/hypercli_cli/renders.py +0 -0
  19. {hypercli_cli-0.8.1 → hypercli_cli-0.8.3}/hypercli_cli/tui/__init__.py +0 -0
  20. {hypercli_cli-0.8.1 → hypercli_cli-0.8.3}/hypercli_cli/tui/job_monitor.py +0 -0
  21. {hypercli_cli-0.8.1 → hypercli_cli-0.8.3}/hypercli_cli/user.py +0 -0
  22. {hypercli_cli-0.8.1 → hypercli_cli-0.8.3}/hypercli_cli/wallet.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hypercli-cli
3
- Version: 0.8.1
3
+ Version: 0.8.3
4
4
  Summary: CLI for HyperCLI - GPU orchestration and LLM API
5
5
  Project-URL: Homepage, https://hypercli.com
6
6
  Project-URL: Documentation, https://docs.hypercli.com
@@ -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}")
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "hypercli-cli"
7
- version = "0.8.1"
7
+ version = "0.8.3"
8
8
  description = "CLI for HyperCLI - GPU orchestration and LLM API"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1 +0,0 @@
1
- __version__ = "0.6.0"
File without changes
File without changes