hypercli-cli 0.7.12__tar.gz → 0.8.1__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hypercli-cli
3
- Version: 0.7.12
3
+ Version: 0.8.1
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
@@ -21,7 +21,7 @@ Requires-Dist: argon2-cffi>=25.0.0; extra == 'all'
21
21
  Requires-Dist: eth-account>=0.13.0; extra == 'all'
22
22
  Requires-Dist: hypercli-sdk[comfyui]>=0.7.1; extra == 'all'
23
23
  Requires-Dist: web3>=7.0.0; extra == 'all'
24
- Requires-Dist: x402[httpx]>=2.0.0; extra == 'all'
24
+ Requires-Dist: x402[evm,httpx]>=2.0.0; extra == 'all'
25
25
  Provides-Extra: comfyui
26
26
  Requires-Dist: hypercli-sdk[comfyui]>=0.7.1; extra == 'comfyui'
27
27
  Provides-Extra: dev
@@ -32,7 +32,7 @@ Requires-Dist: argon2-cffi>=25.0.0; extra == 'wallet'
32
32
  Requires-Dist: eth-account>=0.13.0; extra == 'wallet'
33
33
  Requires-Dist: qrcode[pil]>=7.4.0; extra == 'wallet'
34
34
  Requires-Dist: web3>=7.0.0; extra == 'wallet'
35
- Requires-Dist: x402[httpx]>=2.0.0; extra == 'wallet'
35
+ Requires-Dist: x402[evm,httpx]>=2.0.0; extra == 'wallet'
36
36
  Description-Content-Type: text/markdown
37
37
 
38
38
  # HyperCLI CLI
@@ -7,7 +7,7 @@ from rich.prompt import Prompt
7
7
  from hypercli import HyperCLI, APIError, configure
8
8
  from hypercli.config import CONFIG_FILE
9
9
 
10
- from . import billing, claw, comfyui, flow, instances, jobs, llm, renders, user, wallet
10
+ from . import billing, claw, comfyui, flow, instances, jobs, keys, llm, renders, user, wallet
11
11
 
12
12
  console = Console()
13
13
 
@@ -61,6 +61,7 @@ app.add_typer(claw.app, name="claw")
61
61
  app.add_typer(comfyui.app, name="comfyui")
62
62
  app.add_typer(flow.app, name="flow")
63
63
  app.add_typer(instances.app, name="instances")
64
+ app.add_typer(keys.app, name="keys")
64
65
  app.add_typer(jobs.app, name="jobs")
65
66
  app.add_typer(llm.app, name="llm")
66
67
  app.add_typer(renders.app, name="renders")
@@ -0,0 +1,83 @@
1
+ """API Key management CLI commands"""
2
+ from datetime import datetime, timezone
3
+ import typer
4
+ from rich.console import Console
5
+ from rich.table import Table
6
+
7
+ app = typer.Typer(help="API key management")
8
+
9
+
10
+ def _fmt_ts(ts) -> str:
11
+ """Format a timestamp (epoch float or ISO string) for display."""
12
+ if not ts:
13
+ return ""
14
+ if isinstance(ts, (int, float)):
15
+ return datetime.fromtimestamp(ts, tz=timezone.utc).strftime("%Y-%m-%d %H:%M")
16
+ return str(ts)[:16]
17
+ console = Console()
18
+
19
+
20
+ def _get_client():
21
+ from hypercli import HyperCLI
22
+ return HyperCLI()
23
+
24
+
25
+ @app.command("create")
26
+ def create_key(name: str = typer.Option("default", help="Key name")):
27
+ """Create a new API key"""
28
+ client = _get_client()
29
+ key = client.keys.create(name=name)
30
+ console.print(f"\n[bold green]API key created![/bold green]\n")
31
+ console.print(f" Key ID: {key.key_id}")
32
+ console.print(f" Name: {key.name}")
33
+ console.print(f" API Key: [bold]{key.api_key}[/bold]")
34
+ console.print(f"\n[yellow]⚠ Save this key now — it won't be shown again.[/yellow]\n")
35
+
36
+
37
+ @app.command("list")
38
+ def list_keys():
39
+ """List all API keys"""
40
+ client = _get_client()
41
+ keys = client.keys.list()
42
+
43
+ if not keys:
44
+ console.print("[dim]No API keys found.[/dim]")
45
+ return
46
+
47
+ table = Table(title="API Keys")
48
+ table.add_column("Key ID", style="dim")
49
+ table.add_column("Name")
50
+ table.add_column("Key Preview")
51
+ table.add_column("Active", justify="center")
52
+ table.add_column("Created")
53
+ table.add_column("Last Used")
54
+
55
+ for key in keys:
56
+ active = "✓" if key.is_active else "✗"
57
+ active_style = "green" if key.is_active else "red"
58
+ table.add_row(
59
+ key.key_id[:8] + "...",
60
+ key.name or "",
61
+ key.api_key_preview or "",
62
+ f"[{active_style}]{active}[/{active_style}]",
63
+ _fmt_ts(key.created_at),
64
+ _fmt_ts(key.last_used_at) or "never",
65
+ )
66
+
67
+ console.print(table)
68
+
69
+
70
+ @app.command("disable")
71
+ def disable_key(
72
+ key_id: str = typer.Argument(help="Key ID to disable"),
73
+ yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation"),
74
+ ):
75
+ """Disable an API key (irreversible)"""
76
+ if not yes:
77
+ confirm = typer.confirm(f"Disable key {key_id[:8]}...? This cannot be undone")
78
+ if not confirm:
79
+ raise typer.Abort()
80
+
81
+ client = _get_client()
82
+ result = client.keys.disable(key_id)
83
+ console.print(f"[red]Key {key_id[:8]}... disabled.[/red]")
@@ -0,0 +1,519 @@
1
+ """Wallet management commands for HyperCLI"""
2
+ import json
3
+ import getpass
4
+ import os
5
+ from pathlib import Path
6
+ import typer
7
+ from rich.console import Console
8
+ from rich.table import Table
9
+
10
+ app = typer.Typer(help="Wallet management commands")
11
+ console = Console()
12
+
13
+ # Check if wallet dependencies are available
14
+ try:
15
+ from web3 import Web3
16
+ from eth_account import Account
17
+ WALLET_AVAILABLE = True
18
+ except ImportError:
19
+ WALLET_AVAILABLE = False
20
+
21
+ WALLET_DIR = Path.home() / ".hypercli"
22
+ WALLET_PATH = WALLET_DIR / "wallet.json"
23
+ BASE_RPC = "https://mainnet.base.org"
24
+ USDC_CONTRACT = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
25
+
26
+
27
+ def require_wallet_deps():
28
+ """Check if wallet dependencies are installed"""
29
+ if not WALLET_AVAILABLE:
30
+ console.print("[red]❌ Wallet commands require crypto dependencies[/red]")
31
+ console.print("\nInstall with:")
32
+ console.print(" [bold]pip install 'hypercli-cli[wallet]'[/bold]")
33
+ raise typer.Exit(1)
34
+
35
+
36
+ @app.command("create")
37
+ def create():
38
+ """Create a new wallet with encrypted keystore"""
39
+ require_wallet_deps()
40
+
41
+ if WALLET_PATH.exists():
42
+ console.print(f"[yellow]Wallet already exists at {WALLET_PATH}[/yellow]")
43
+ overwrite = typer.confirm("Overwrite existing wallet?", default=False)
44
+ if not overwrite:
45
+ console.print("Cancelled.")
46
+ raise typer.Exit(0)
47
+
48
+ console.print("\n[bold]Creating new Ethereum wallet...[/bold]\n")
49
+
50
+ # Generate random account
51
+ account = Account.create()
52
+
53
+ console.print(f"[green]✓[/green] Wallet address: [bold]{account.address}[/bold]")
54
+
55
+ # Get passphrase (check env first)
56
+ passphrase_env = os.getenv("HYPERCLI_WALLET_PASSPHRASE")
57
+ if passphrase_env:
58
+ passphrase = passphrase_env
59
+ console.print("[dim]Using passphrase from HYPERCLI_WALLET_PASSPHRASE[/dim]")
60
+ else:
61
+ passphrase = getpass.getpass("Set keystore passphrase: ")
62
+ confirm = getpass.getpass("Confirm passphrase: ")
63
+
64
+ if passphrase != confirm:
65
+ console.print("[red]❌ Passphrases don't match![/red]")
66
+ raise typer.Exit(1)
67
+
68
+ # Encrypt and save keystore
69
+ keystore = account.encrypt(passphrase)
70
+
71
+ WALLET_DIR.mkdir(parents=True, exist_ok=True)
72
+ with open(WALLET_PATH, "w") as f:
73
+ json.dump(keystore, f, indent=2)
74
+
75
+ # Set restrictive permissions
76
+ os.chmod(WALLET_PATH, 0o600)
77
+
78
+ console.print(f"\n[green]✓[/green] Keystore saved to [bold]{WALLET_PATH}[/bold]")
79
+ console.print(f"[green]✓[/green] Address: [bold]{account.address}[/bold]")
80
+ console.print("\n[yellow]⚠️ Fund this address with USDC on Base to use for payments![/yellow]")
81
+
82
+ # Show QR code for easy mobile scanning
83
+ try:
84
+ import qrcode
85
+ console.print("\n[bold]Scan to send USDC:[/bold]\n")
86
+ qr_obj = qrcode.QRCode(
87
+ version=1,
88
+ error_correction=qrcode.constants.ERROR_CORRECT_L,
89
+ box_size=1,
90
+ border=1,
91
+ )
92
+ qr_obj.add_data(account.address)
93
+ qr_obj.make(fit=True)
94
+ qr_obj.print_ascii(invert=True)
95
+ except ImportError:
96
+ console.print("\n[dim]Tip: run 'hyper wallet qr' to display address as QR code[/dim]")
97
+
98
+
99
+ @app.command("address")
100
+ def address():
101
+ """Show wallet address"""
102
+ require_wallet_deps()
103
+
104
+ if not WALLET_PATH.exists():
105
+ console.print(f"[red]❌ No wallet found at {WALLET_PATH}[/red]")
106
+ console.print("Create one with: [bold]hyper wallet create[/bold]")
107
+ raise typer.Exit(1)
108
+
109
+ with open(WALLET_PATH) as f:
110
+ keystore = json.load(f)
111
+
112
+ addr = keystore.get("address", "unknown")
113
+ console.print(f"\n[bold]Wallet address:[/bold] 0x{addr}")
114
+
115
+
116
+ @app.command("qr")
117
+ def qr(
118
+ output: str = typer.Option(None, "--output", "-o", help="Save QR code as PNG file"),
119
+ ):
120
+ """Display wallet address as QR code (for easy mobile scanning)"""
121
+ require_wallet_deps()
122
+
123
+ try:
124
+ import qrcode
125
+ except ImportError:
126
+ console.print("[red]❌ QR code support requires the qrcode package[/red]")
127
+ console.print("\nInstall with:")
128
+ console.print(" [bold]pip install 'hypercli-cli[wallet]'[/bold]")
129
+ raise typer.Exit(1)
130
+
131
+ if not WALLET_PATH.exists():
132
+ console.print(f"[red]❌ No wallet found at {WALLET_PATH}[/red]")
133
+ console.print("Create one with: [bold]hyper wallet create[/bold]")
134
+ raise typer.Exit(1)
135
+
136
+ with open(WALLET_PATH) as f:
137
+ keystore = json.load(f)
138
+
139
+ addr = "0x" + keystore.get("address", "")
140
+
141
+ if output:
142
+ # Save as PNG
143
+ try:
144
+ qr_obj = qrcode.QRCode(
145
+ version=1,
146
+ error_correction=qrcode.constants.ERROR_CORRECT_L,
147
+ box_size=10,
148
+ border=2,
149
+ )
150
+ qr_obj.add_data(addr)
151
+ qr_obj.make(fit=True)
152
+
153
+ img = qr_obj.make_image(fill_color="black", back_color="white")
154
+
155
+ # Ensure .png extension
156
+ if not output.lower().endswith('.png'):
157
+ output = output + '.png'
158
+
159
+ img.save(output)
160
+ console.print(f"[green]✓[/green] QR code saved to [bold]{output}[/bold]")
161
+ console.print(f"[dim]Address: {addr}[/dim]")
162
+ except Exception as e:
163
+ console.print(f"[red]❌ Failed to save PNG: {e}[/red]")
164
+ console.print("[dim]Try: pip install pillow[/dim]")
165
+ raise typer.Exit(1)
166
+ else:
167
+ # Print ASCII to terminal
168
+ console.print(f"\n[bold]Wallet Address:[/bold] {addr}")
169
+ console.print("[dim]Scan to send USDC on Base network[/dim]\n")
170
+
171
+ qr_obj = qrcode.QRCode(
172
+ version=1,
173
+ error_correction=qrcode.constants.ERROR_CORRECT_L,
174
+ box_size=1,
175
+ border=1,
176
+ )
177
+ qr_obj.add_data(addr)
178
+ qr_obj.make(fit=True)
179
+ qr_obj.print_ascii(invert=True)
180
+
181
+ console.print(f"\n[bold]{addr}[/bold]")
182
+ console.print("\n[dim]Tip: use --output wallet.png to save as image[/dim]")
183
+
184
+
185
+ @app.command("balance")
186
+ def balance():
187
+ """Check USDC balance on Base"""
188
+ require_wallet_deps()
189
+
190
+ if not WALLET_PATH.exists():
191
+ console.print(f"[red]❌ No wallet found at {WALLET_PATH}[/red]")
192
+ console.print("Create one with: [bold]hyper wallet create[/bold]")
193
+ raise typer.Exit(1)
194
+
195
+ with open(WALLET_PATH) as f:
196
+ keystore = json.load(f)
197
+
198
+ # Get passphrase
199
+ passphrase_env = os.getenv("HYPERCLI_WALLET_PASSPHRASE")
200
+ if passphrase_env:
201
+ passphrase = passphrase_env
202
+ else:
203
+ passphrase = getpass.getpass("Unlock keystore passphrase: ")
204
+
205
+ try:
206
+ private_key = Account.decrypt(keystore, passphrase)
207
+ account = Account.from_key(private_key)
208
+ except Exception as e:
209
+ console.print(f"[red]❌ Failed to unlock wallet: {e}[/red]")
210
+ raise typer.Exit(1)
211
+
212
+ console.print(f"\n[bold]Checking USDC balance on Base...[/bold]\n")
213
+
214
+ # Connect to Base
215
+ w3 = Web3(Web3.HTTPProvider(BASE_RPC))
216
+
217
+ # USDC ERC20 ABI (minimal - just balanceOf)
218
+ usdc_abi = [
219
+ {
220
+ "constant": True,
221
+ "inputs": [{"name": "_owner", "type": "address"}],
222
+ "name": "balanceOf",
223
+ "outputs": [{"name": "balance", "type": "uint256"}],
224
+ "type": "function",
225
+ }
226
+ ]
227
+
228
+ usdc = w3.eth.contract(address=USDC_CONTRACT, abi=usdc_abi)
229
+
230
+ # Get balance (USDC has 6 decimals)
231
+ balance_raw = usdc.functions.balanceOf(account.address).call()
232
+ balance_usdc = balance_raw / 1_000_000
233
+
234
+ console.print(f"Wallet: [bold]{account.address}[/bold]")
235
+ console.print(f"USDC Balance: [bold green]{balance_usdc:.6f}[/bold green]")
236
+
237
+ if balance_usdc == 0:
238
+ console.print("\n[yellow]⚠️ No USDC! Send some to this address on Base network.[/yellow]")
239
+ else:
240
+ console.print(f"\n[green]✓[/green] You have [bold]{balance_usdc:.2f} USDC[/bold]")
241
+
242
+
243
+ @app.command("topup")
244
+ def topup(
245
+ amount: str = typer.Argument(help="Amount in USDC to top up (max 6 decimals)"),
246
+ api_url: str = typer.Option(None, help="API URL override"),
247
+ ):
248
+ """Top up account balance via Orchestra x402 endpoint.
249
+
250
+ Flow:
251
+ 1) Resolve user_id via GET /api/user using your existing API key
252
+ 2) POST /api/x402/top_up with {user_id, amount}
253
+ 3) Handle 402 and retry with x402 payment headers
254
+
255
+ Examples:
256
+ hyper wallet topup 10
257
+ hyper wallet topup 25.50
258
+ """
259
+ require_wallet_deps()
260
+ from decimal import Decimal, InvalidOperation
261
+ import httpx
262
+ from hypercli.config import get_api_key, get_api_url
263
+
264
+ try:
265
+ from x402 import x402ClientSync
266
+ from x402.http import x402HTTPClientSync
267
+ from x402.mechanisms.evm import EthAccountSigner
268
+ from x402.mechanisms.evm.exact.register import register_exact_evm_client
269
+ except ImportError:
270
+ console.print("[red]❌ x402 payment requires wallet/x402 dependencies[/red]")
271
+ console.print("\nInstall with:")
272
+ console.print(" [bold]pip install 'hypercli-cli[wallet]'[/bold]")
273
+ raise typer.Exit(1)
274
+
275
+ try:
276
+ amount_dec = Decimal(amount)
277
+ except InvalidOperation:
278
+ console.print(f"[red]❌ Invalid amount: {amount}[/red]")
279
+ raise typer.Exit(1)
280
+
281
+ if amount_dec <= 0:
282
+ console.print("[red]❌ Amount must be greater than 0[/red]")
283
+ raise typer.Exit(1)
284
+ if amount_dec.as_tuple().exponent < -6:
285
+ console.print("[red]❌ Amount supports at most 6 decimals[/red]")
286
+ raise typer.Exit(1)
287
+
288
+ amount_atomic = int(amount_dec * Decimal("1000000"))
289
+
290
+ # Step 1: Load wallet
291
+ account = load_wallet()
292
+ console.print(f"[green]✓[/green] Wallet: {account.address}")
293
+
294
+ # Step 2: Check USDC balance
295
+ w3 = Web3(Web3.HTTPProvider(BASE_RPC))
296
+ usdc_abi = [
297
+ {
298
+ "constant": True,
299
+ "inputs": [{"name": "_owner", "type": "address"}],
300
+ "name": "balanceOf",
301
+ "outputs": [{"name": "balance", "type": "uint256"}],
302
+ "type": "function",
303
+ }
304
+ ]
305
+ usdc = w3.eth.contract(address=USDC_CONTRACT, abi=usdc_abi)
306
+ balance_raw = usdc.functions.balanceOf(account.address).call()
307
+ balance_usdc = Decimal(balance_raw) / Decimal("1000000")
308
+
309
+ console.print(f"[green]✓[/green] Balance: {balance_usdc:.6f} USDC")
310
+
311
+ if balance_raw < amount_atomic:
312
+ console.print(
313
+ f"\n[red]❌ Insufficient balance: {balance_usdc:.6f} < {amount_dec:.6f} USDC[/red]"
314
+ )
315
+ console.print(f"Send USDC on Base to: [bold]{account.address}[/bold]")
316
+ raise typer.Exit(1)
317
+
318
+ # Step 3: Set up x402 v2 client
319
+ api_key = get_api_key()
320
+ if not api_key:
321
+ console.print("[red]❌ API key required for top-up[/red]")
322
+ console.print(
323
+ "Set it with: [bold]hyper configure[/bold] or [bold]hyper wallet login[/bold]"
324
+ )
325
+ raise typer.Exit(1)
326
+
327
+ base_url = (api_url or get_api_url()).rstrip("/")
328
+ user_endpoint = f"{base_url}/api/user"
329
+ topup_endpoint = f"{base_url}/api/x402/top_up"
330
+ auth_headers = {
331
+ "Authorization": f"Bearer {api_key}",
332
+ "Content-Type": "application/json",
333
+ }
334
+
335
+ signer = EthAccountSigner(account)
336
+ x402_client = x402ClientSync()
337
+ register_exact_evm_client(x402_client, signer)
338
+ http_client = x402HTTPClientSync(x402_client)
339
+
340
+ with httpx.Client(timeout=30) as client:
341
+ # Step 4: Resolve user_id from API key
342
+ console.print("\n[bold]Resolving user...[/bold]")
343
+ user_resp = client.get(user_endpoint, headers=auth_headers)
344
+ if user_resp.status_code != 200:
345
+ console.print(
346
+ f"[red]❌ Failed to get user: {user_resp.status_code} {user_resp.text}[/red]"
347
+ )
348
+ raise typer.Exit(1)
349
+
350
+ user_id = user_resp.json().get("user_id")
351
+ if not user_id:
352
+ console.print("[red]❌ /api/user response missing user_id[/red]")
353
+ raise typer.Exit(1)
354
+
355
+ payload = {"user_id": user_id, "amount": float(amount_dec)}
356
+
357
+ # Step 5: Request top-up (expect 402 then retry with payment)
358
+ console.print(f"[bold]Requesting top-up of ${amount_dec:.2f}...[/bold]")
359
+ resp = client.post(topup_endpoint, headers=auth_headers, json=payload)
360
+
361
+ if resp.status_code == 402:
362
+ console.print("[bold]Signing x402 payment...[/bold]")
363
+ try:
364
+ payment_headers, _ = http_client.handle_402_response(
365
+ dict(resp.headers), resp.content
366
+ )
367
+ except Exception as e:
368
+ console.print(f"[red]❌ Failed to build payment header: {e}[/red]")
369
+ raise typer.Exit(1)
370
+
371
+ console.print("[bold]Submitting payment...[/bold]")
372
+ retry_headers = {**auth_headers, **payment_headers}
373
+ retry_headers["Access-Control-Expose-Headers"] = "PAYMENT-RESPONSE,X-PAYMENT-RESPONSE"
374
+ resp = client.post(topup_endpoint, headers=retry_headers, json=payload)
375
+
376
+ if resp.status_code != 200:
377
+ console.print(f"[red]❌ Top-up failed: {resp.status_code} {resp.text}[/red]")
378
+ try:
379
+ settle = http_client.get_payment_settle_response(lambda name: resp.headers.get(name))
380
+ if getattr(settle, "error_reason", None):
381
+ console.print(f"[red]❌ Payment error: {settle.error_reason}[/red]")
382
+ except Exception:
383
+ pass
384
+ raise typer.Exit(1)
385
+
386
+ result = resp.json()
387
+
388
+ credited = result.get("amount", float(amount_dec))
389
+ wallet = result.get("wallet", account.address)
390
+ tx_id = result.get("transaction_id", "")
391
+ msg = result.get("message", "Top-up successful")
392
+
393
+ console.print(f"\n[bold green]✓ Top-up successful![/bold green]\n")
394
+ console.print(f" User: {result.get('user_id', 'N/A')}")
395
+ console.print(f" Credited: ${credited} USDC")
396
+ console.print(f" Wallet: {wallet}")
397
+ console.print(f" Tx ID: {tx_id or 'N/A'}")
398
+ console.print(f" Message: {msg}")
399
+
400
+ try:
401
+ settle = http_client.get_payment_settle_response(lambda name: resp.headers.get(name))
402
+ if getattr(settle, "transaction", None):
403
+ console.print(f" On-chain: {settle.transaction}")
404
+ if getattr(settle, "network", None):
405
+ console.print(f" Network: {settle.network}")
406
+ except Exception:
407
+ pass
408
+
409
+ console.print()
410
+
411
+
412
+ @app.command("login")
413
+ def wallet_login(
414
+ name: str = typer.Option("cli", help="Name for the generated API key"),
415
+ api_url: str = typer.Option(None, help="API URL override"),
416
+ ):
417
+ """Login with wallet signature, create an API key, and save it.
418
+
419
+ This is the recommended way to set up HyperCLI from scratch:
420
+ 1. hyper wallet create
421
+ 2. hyper wallet login
422
+ """
423
+ require_wallet_deps()
424
+ from eth_account.messages import encode_defunct
425
+ import httpx
426
+ from hypercli.config import get_api_url, configure
427
+
428
+ base_url = (api_url or get_api_url()).rstrip("/")
429
+
430
+ # Step 1: Load wallet
431
+ account = load_wallet()
432
+ console.print(f"[green]✓[/green] Wallet: {account.address}\n")
433
+
434
+ # Step 2: Get challenge
435
+ console.print("[bold]Requesting login challenge...[/bold]")
436
+ with httpx.Client(timeout=15) as client:
437
+ resp = client.post(
438
+ f"{base_url}/api/auth/wallet/challenge",
439
+ json={"wallet": account.address},
440
+ )
441
+ if resp.status_code != 200:
442
+ console.print(f"[red]❌ Challenge failed: {resp.text}[/red]")
443
+ raise typer.Exit(1)
444
+ challenge = resp.json()
445
+
446
+ # Step 3: Sign the challenge
447
+ console.print("[bold]Signing challenge...[/bold]")
448
+ message = encode_defunct(text=challenge["message"])
449
+ signed = account.sign_message(message)
450
+
451
+ # Step 4: Login
452
+ console.print("[bold]Logging in...[/bold]")
453
+ with httpx.Client(timeout=15) as client:
454
+ resp = client.post(
455
+ f"{base_url}/api/auth/wallet/login",
456
+ json={
457
+ "wallet": account.address,
458
+ "signature": signed.signature.hex(),
459
+ "timestamp": challenge["timestamp"],
460
+ },
461
+ )
462
+ if resp.status_code != 200:
463
+ console.print(f"[red]❌ Login failed: {resp.text}[/red]")
464
+ raise typer.Exit(1)
465
+ jwt_token = resp.json()["token"]
466
+
467
+ console.print("[green]✓[/green] Authenticated\n")
468
+
469
+ # Step 5: Create API key using JWT
470
+ console.print(f"[bold]Creating API key '{name}'...[/bold]")
471
+ with httpx.Client(timeout=15) as client:
472
+ resp = client.post(
473
+ f"{base_url}/api/keys",
474
+ json={"name": name},
475
+ headers={"Authorization": f"Bearer {jwt_token}"},
476
+ )
477
+ if resp.status_code != 200:
478
+ console.print(f"[red]❌ Key creation failed: {resp.text}[/red]")
479
+ raise typer.Exit(1)
480
+ key_data = resp.json()
481
+
482
+ api_key = key_data["api_key"]
483
+
484
+ # Step 6: Save to config
485
+ configure(api_key, api_url)
486
+
487
+ console.print(f"[green]✓[/green] API key created and saved!\n")
488
+ console.print(f" Name: {key_data['name']}")
489
+ console.print(f" Key: [bold]{api_key}[/bold]")
490
+ console.print(f" Saved: ~/.hypercli/config")
491
+ console.print(f"\n[green]You're all set! Try:[/green] hyper keys list\n")
492
+
493
+
494
+ def load_wallet():
495
+ """Load and decrypt wallet (helper function for other commands)"""
496
+ require_wallet_deps()
497
+
498
+ if not WALLET_PATH.exists():
499
+ console.print(f"[red]❌ No wallet found at {WALLET_PATH}[/red]")
500
+ console.print("Create one with: [bold]hyper wallet create[/bold]")
501
+ raise typer.Exit(1)
502
+
503
+ with open(WALLET_PATH) as f:
504
+ keystore = json.load(f)
505
+
506
+ # Get passphrase
507
+ passphrase_env = os.getenv("HYPERCLI_WALLET_PASSPHRASE")
508
+ if passphrase_env:
509
+ passphrase = passphrase_env
510
+ else:
511
+ passphrase = getpass.getpass("Unlock keystore passphrase: ")
512
+
513
+ try:
514
+ private_key = Account.decrypt(keystore, passphrase)
515
+ account = Account.from_key(private_key)
516
+ return account
517
+ except Exception as e:
518
+ console.print(f"[red]❌ Failed to unlock wallet: {e}[/red]")
519
+ raise typer.Exit(1)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "hypercli-cli"
7
- version = "0.7.12"
7
+ version = "0.8.1"
8
8
  description = "CLI for HyperCLI - GPU orchestration and LLM API"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -28,7 +28,7 @@ comfyui = [
28
28
  "hypercli-sdk[comfyui]>=0.7.1",
29
29
  ]
30
30
  wallet = [
31
- "x402[httpx]>=2.0.0",
31
+ "x402[httpx,evm]>=2.0.0",
32
32
  "eth-account>=0.13.0",
33
33
  "web3>=7.0.0",
34
34
  "argon2-cffi>=25.0.0",
@@ -36,7 +36,7 @@ wallet = [
36
36
  ]
37
37
  all = [
38
38
  "hypercli-sdk[comfyui]>=0.7.1",
39
- "x402[httpx]>=2.0.0",
39
+ "x402[httpx,evm]>=2.0.0",
40
40
  "eth-account>=0.13.0",
41
41
  "web3>=7.0.0",
42
42
  "argon2-cffi>=25.0.0",
@@ -1,268 +0,0 @@
1
- """Wallet management commands for HyperCLI"""
2
- import json
3
- import getpass
4
- import os
5
- from pathlib import Path
6
- import typer
7
- from rich.console import Console
8
- from rich.table import Table
9
-
10
- app = typer.Typer(help="Wallet management commands")
11
- console = Console()
12
-
13
- # Check if wallet dependencies are available
14
- try:
15
- from web3 import Web3
16
- from eth_account import Account
17
- WALLET_AVAILABLE = True
18
- except ImportError:
19
- WALLET_AVAILABLE = False
20
-
21
- WALLET_DIR = Path.home() / ".hypercli"
22
- WALLET_PATH = WALLET_DIR / "wallet.json"
23
- BASE_RPC = "https://mainnet.base.org"
24
- USDC_CONTRACT = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
25
-
26
-
27
- def require_wallet_deps():
28
- """Check if wallet dependencies are installed"""
29
- if not WALLET_AVAILABLE:
30
- console.print("[red]❌ Wallet commands require crypto dependencies[/red]")
31
- console.print("\nInstall with:")
32
- console.print(" [bold]pip install 'hypercli-cli[wallet]'[/bold]")
33
- raise typer.Exit(1)
34
-
35
-
36
- @app.command("create")
37
- def create():
38
- """Create a new wallet with encrypted keystore"""
39
- require_wallet_deps()
40
-
41
- if WALLET_PATH.exists():
42
- console.print(f"[yellow]Wallet already exists at {WALLET_PATH}[/yellow]")
43
- overwrite = typer.confirm("Overwrite existing wallet?", default=False)
44
- if not overwrite:
45
- console.print("Cancelled.")
46
- raise typer.Exit(0)
47
-
48
- console.print("\n[bold]Creating new Ethereum wallet...[/bold]\n")
49
-
50
- # Generate random account
51
- account = Account.create()
52
-
53
- console.print(f"[green]✓[/green] Wallet address: [bold]{account.address}[/bold]")
54
-
55
- # Get passphrase (check env first)
56
- passphrase_env = os.getenv("HYPERCLI_WALLET_PASSPHRASE")
57
- if passphrase_env:
58
- passphrase = passphrase_env
59
- console.print("[dim]Using passphrase from HYPERCLI_WALLET_PASSPHRASE[/dim]")
60
- else:
61
- passphrase = getpass.getpass("Set keystore passphrase: ")
62
- confirm = getpass.getpass("Confirm passphrase: ")
63
-
64
- if passphrase != confirm:
65
- console.print("[red]❌ Passphrases don't match![/red]")
66
- raise typer.Exit(1)
67
-
68
- # Encrypt and save keystore
69
- keystore = account.encrypt(passphrase)
70
-
71
- WALLET_DIR.mkdir(parents=True, exist_ok=True)
72
- with open(WALLET_PATH, "w") as f:
73
- json.dump(keystore, f, indent=2)
74
-
75
- # Set restrictive permissions
76
- os.chmod(WALLET_PATH, 0o600)
77
-
78
- console.print(f"\n[green]✓[/green] Keystore saved to [bold]{WALLET_PATH}[/bold]")
79
- console.print(f"[green]✓[/green] Address: [bold]{account.address}[/bold]")
80
- console.print("\n[yellow]⚠️ Fund this address with USDC on Base to use for payments![/yellow]")
81
-
82
- # Show QR code for easy mobile scanning
83
- try:
84
- import qrcode
85
- console.print("\n[bold]Scan to send USDC:[/bold]\n")
86
- qr_obj = qrcode.QRCode(
87
- version=1,
88
- error_correction=qrcode.constants.ERROR_CORRECT_L,
89
- box_size=1,
90
- border=1,
91
- )
92
- qr_obj.add_data(account.address)
93
- qr_obj.make(fit=True)
94
- qr_obj.print_ascii(invert=True)
95
- except ImportError:
96
- console.print("\n[dim]Tip: run 'hyper wallet qr' to display address as QR code[/dim]")
97
-
98
-
99
- @app.command("address")
100
- def address():
101
- """Show wallet address"""
102
- require_wallet_deps()
103
-
104
- if not WALLET_PATH.exists():
105
- console.print(f"[red]❌ No wallet found at {WALLET_PATH}[/red]")
106
- console.print("Create one with: [bold]hyper wallet create[/bold]")
107
- raise typer.Exit(1)
108
-
109
- with open(WALLET_PATH) as f:
110
- keystore = json.load(f)
111
-
112
- addr = keystore.get("address", "unknown")
113
- console.print(f"\n[bold]Wallet address:[/bold] 0x{addr}")
114
-
115
-
116
- @app.command("qr")
117
- def qr(
118
- output: str = typer.Option(None, "--output", "-o", help="Save QR code as PNG file"),
119
- ):
120
- """Display wallet address as QR code (for easy mobile scanning)"""
121
- require_wallet_deps()
122
-
123
- try:
124
- import qrcode
125
- except ImportError:
126
- console.print("[red]❌ QR code support requires the qrcode package[/red]")
127
- console.print("\nInstall with:")
128
- console.print(" [bold]pip install 'hypercli-cli[wallet]'[/bold]")
129
- raise typer.Exit(1)
130
-
131
- if not WALLET_PATH.exists():
132
- console.print(f"[red]❌ No wallet found at {WALLET_PATH}[/red]")
133
- console.print("Create one with: [bold]hyper wallet create[/bold]")
134
- raise typer.Exit(1)
135
-
136
- with open(WALLET_PATH) as f:
137
- keystore = json.load(f)
138
-
139
- addr = "0x" + keystore.get("address", "")
140
-
141
- if output:
142
- # Save as PNG
143
- try:
144
- qr_obj = qrcode.QRCode(
145
- version=1,
146
- error_correction=qrcode.constants.ERROR_CORRECT_L,
147
- box_size=10,
148
- border=2,
149
- )
150
- qr_obj.add_data(addr)
151
- qr_obj.make(fit=True)
152
-
153
- img = qr_obj.make_image(fill_color="black", back_color="white")
154
-
155
- # Ensure .png extension
156
- if not output.lower().endswith('.png'):
157
- output = output + '.png'
158
-
159
- img.save(output)
160
- console.print(f"[green]✓[/green] QR code saved to [bold]{output}[/bold]")
161
- console.print(f"[dim]Address: {addr}[/dim]")
162
- except Exception as e:
163
- console.print(f"[red]❌ Failed to save PNG: {e}[/red]")
164
- console.print("[dim]Try: pip install pillow[/dim]")
165
- raise typer.Exit(1)
166
- else:
167
- # Print ASCII to terminal
168
- console.print(f"\n[bold]Wallet Address:[/bold] {addr}")
169
- console.print("[dim]Scan to send USDC on Base network[/dim]\n")
170
-
171
- qr_obj = qrcode.QRCode(
172
- version=1,
173
- error_correction=qrcode.constants.ERROR_CORRECT_L,
174
- box_size=1,
175
- border=1,
176
- )
177
- qr_obj.add_data(addr)
178
- qr_obj.make(fit=True)
179
- qr_obj.print_ascii(invert=True)
180
-
181
- console.print(f"\n[bold]{addr}[/bold]")
182
- console.print("\n[dim]Tip: use --output wallet.png to save as image[/dim]")
183
-
184
-
185
- @app.command("balance")
186
- def balance():
187
- """Check USDC balance on Base"""
188
- require_wallet_deps()
189
-
190
- if not WALLET_PATH.exists():
191
- console.print(f"[red]❌ No wallet found at {WALLET_PATH}[/red]")
192
- console.print("Create one with: [bold]hyper wallet create[/bold]")
193
- raise typer.Exit(1)
194
-
195
- with open(WALLET_PATH) as f:
196
- keystore = json.load(f)
197
-
198
- # Get passphrase
199
- passphrase_env = os.getenv("HYPERCLI_WALLET_PASSPHRASE")
200
- if passphrase_env:
201
- passphrase = passphrase_env
202
- else:
203
- passphrase = getpass.getpass("Unlock keystore passphrase: ")
204
-
205
- try:
206
- private_key = Account.decrypt(keystore, passphrase)
207
- account = Account.from_key(private_key)
208
- except Exception as e:
209
- console.print(f"[red]❌ Failed to unlock wallet: {e}[/red]")
210
- raise typer.Exit(1)
211
-
212
- console.print(f"\n[bold]Checking USDC balance on Base...[/bold]\n")
213
-
214
- # Connect to Base
215
- w3 = Web3(Web3.HTTPProvider(BASE_RPC))
216
-
217
- # USDC ERC20 ABI (minimal - just balanceOf)
218
- usdc_abi = [
219
- {
220
- "constant": True,
221
- "inputs": [{"name": "_owner", "type": "address"}],
222
- "name": "balanceOf",
223
- "outputs": [{"name": "balance", "type": "uint256"}],
224
- "type": "function",
225
- }
226
- ]
227
-
228
- usdc = w3.eth.contract(address=USDC_CONTRACT, abi=usdc_abi)
229
-
230
- # Get balance (USDC has 6 decimals)
231
- balance_raw = usdc.functions.balanceOf(account.address).call()
232
- balance_usdc = balance_raw / 1_000_000
233
-
234
- console.print(f"Wallet: [bold]{account.address}[/bold]")
235
- console.print(f"USDC Balance: [bold green]{balance_usdc:.6f}[/bold green]")
236
-
237
- if balance_usdc == 0:
238
- console.print("\n[yellow]⚠️ No USDC! Send some to this address on Base network.[/yellow]")
239
- else:
240
- console.print(f"\n[green]✓[/green] You have [bold]{balance_usdc:.2f} USDC[/bold]")
241
-
242
-
243
- def load_wallet():
244
- """Load and decrypt wallet (helper function for other commands)"""
245
- require_wallet_deps()
246
-
247
- if not WALLET_PATH.exists():
248
- console.print(f"[red]❌ No wallet found at {WALLET_PATH}[/red]")
249
- console.print("Create one with: [bold]hyper wallet create[/bold]")
250
- raise typer.Exit(1)
251
-
252
- with open(WALLET_PATH) as f:
253
- keystore = json.load(f)
254
-
255
- # Get passphrase
256
- passphrase_env = os.getenv("HYPERCLI_WALLET_PASSPHRASE")
257
- if passphrase_env:
258
- passphrase = passphrase_env
259
- else:
260
- passphrase = getpass.getpass("Unlock keystore passphrase: ")
261
-
262
- try:
263
- private_key = Account.decrypt(keystore, passphrase)
264
- account = Account.from_key(private_key)
265
- return account
266
- except Exception as e:
267
- console.print(f"[red]❌ Failed to unlock wallet: {e}[/red]")
268
- raise typer.Exit(1)
File without changes
File without changes