hypercli-cli 0.9.1__tar.gz → 1.0.0__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.9.1 → hypercli_cli-1.0.0}/PKG-INFO +4 -4
  2. {hypercli_cli-0.9.1 → hypercli_cli-1.0.0}/hypercli_cli/claw.py +2 -144
  3. {hypercli_cli-0.9.1 → hypercli_cli-1.0.0}/hypercli_cli/cli.py +1 -2
  4. {hypercli_cli-0.9.1 → hypercli_cli-1.0.0}/hypercli_cli/instances.py +0 -12
  5. {hypercli_cli-0.9.1 → hypercli_cli-1.0.0}/hypercli_cli/jobs.py +100 -0
  6. {hypercli_cli-0.9.1 → hypercli_cli-1.0.0}/pyproject.toml +4 -4
  7. hypercli_cli-0.9.1/hypercli_cli/agents.py +0 -559
  8. hypercli_cli-0.9.1/hypercli_cli/voice.py +0 -167
  9. {hypercli_cli-0.9.1 → hypercli_cli-1.0.0}/.gitignore +0 -0
  10. {hypercli_cli-0.9.1 → hypercli_cli-1.0.0}/README.md +0 -0
  11. {hypercli_cli-0.9.1 → hypercli_cli-1.0.0}/hypercli_cli/__init__.py +0 -0
  12. {hypercli_cli-0.9.1 → hypercli_cli-1.0.0}/hypercli_cli/billing.py +0 -0
  13. {hypercli_cli-0.9.1 → hypercli_cli-1.0.0}/hypercli_cli/comfyui.py +0 -0
  14. {hypercli_cli-0.9.1 → hypercli_cli-1.0.0}/hypercli_cli/flow.py +0 -0
  15. {hypercli_cli-0.9.1 → hypercli_cli-1.0.0}/hypercli_cli/keys.py +0 -0
  16. {hypercli_cli-0.9.1 → hypercli_cli-1.0.0}/hypercli_cli/onboard.py +0 -0
  17. {hypercli_cli-0.9.1 → hypercli_cli-1.0.0}/hypercli_cli/output.py +0 -0
  18. {hypercli_cli-0.9.1 → hypercli_cli-1.0.0}/hypercli_cli/renders.py +0 -0
  19. {hypercli_cli-0.9.1 → hypercli_cli-1.0.0}/hypercli_cli/tui/__init__.py +0 -0
  20. {hypercli_cli-0.9.1 → hypercli_cli-1.0.0}/hypercli_cli/tui/job_monitor.py +0 -0
  21. {hypercli_cli-0.9.1 → hypercli_cli-1.0.0}/hypercli_cli/user.py +0 -0
  22. {hypercli_cli-0.9.1 → hypercli_cli-1.0.0}/hypercli_cli/wallet.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hypercli-cli
3
- Version: 0.9.1
3
+ Version: 1.0.0
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
@@ -9,7 +9,7 @@ Author-email: HyperCLI <support@hypercli.com>
9
9
  License: MIT
10
10
  Requires-Python: >=3.10
11
11
  Requires-Dist: httpx>=0.27.0
12
- Requires-Dist: hypercli-sdk>=0.8.9
12
+ Requires-Dist: hypercli-sdk>=1.0.0
13
13
  Requires-Dist: mutagen>=1.47.0
14
14
  Requires-Dist: pyyaml>=6.0
15
15
  Requires-Dist: rich>=14.2.0
@@ -18,11 +18,11 @@ Requires-Dist: websocket-client>=1.6.0
18
18
  Provides-Extra: all
19
19
  Requires-Dist: argon2-cffi>=25.0.0; extra == 'all'
20
20
  Requires-Dist: eth-account>=0.13.0; extra == 'all'
21
- Requires-Dist: hypercli-sdk[comfyui]>=0.8.9; extra == 'all'
21
+ Requires-Dist: hypercli-sdk[comfyui]>=1.0.0; extra == 'all'
22
22
  Requires-Dist: web3>=7.0.0; extra == 'all'
23
23
  Requires-Dist: x402[evm,httpx]>=2.0.0; extra == 'all'
24
24
  Provides-Extra: comfyui
25
- Requires-Dist: hypercli-sdk[comfyui]>=0.8.9; extra == 'comfyui'
25
+ Requires-Dist: hypercli-sdk[comfyui]>=1.0.0; extra == 'comfyui'
26
26
  Provides-Extra: dev
27
27
  Requires-Dist: pytest>=8.0.0; extra == 'dev'
28
28
  Requires-Dist: ruff>=0.3.0; extra == 'dev'
@@ -8,14 +8,12 @@ from rich.console import Console
8
8
  from rich.table import Table
9
9
 
10
10
  from .onboard import onboard as _onboard_fn
11
- from .voice import app as voice_app
12
11
 
13
12
  app = typer.Typer(help="HyperClaw inference commands")
14
13
  console = Console()
15
14
 
16
- # Register subcommands
15
+ # Register onboard as a subcommand
17
16
  app.command("onboard")(_onboard_fn)
18
- app.add_typer(voice_app, name="voice")
19
17
 
20
18
  # Check if wallet dependencies are available
21
19
  try:
@@ -196,19 +194,7 @@ async def _subscribe_async(account, plan_id: str, api_base: str, amount: str = N
196
194
  payment_headers = http_client.encode_payment_signature_header(payment_payload)
197
195
  console.print(f"[green]✓[/green] Payment signed")
198
196
 
199
- # Step 6: Retry with payment (include JWT if available from claw login)
200
- jwt_path = HYPERCLI_DIR / "claw-jwt.json"
201
- if jwt_path.exists():
202
- try:
203
- with open(jwt_path) as f:
204
- jwt_data = json.load(f)
205
- jwt_token = jwt_data.get("token", "")
206
- if jwt_token:
207
- payment_headers["Authorization"] = f"Bearer {jwt_token}"
208
- console.print("[green]✓[/green] Attaching user auth (from claw login)")
209
- except Exception:
210
- pass
211
-
197
+ # Step 6: Retry with payment
212
198
  console.print("[bold]Sending payment...[/bold]")
213
199
  retry_response = await http.post(url, headers=payment_headers)
214
200
 
@@ -369,134 +355,6 @@ def models(
369
355
  console.print(f"Source: {url}")
370
356
 
371
357
 
372
- @app.command("login")
373
- def login(
374
- api_url: str = typer.Option(None, "--api-url", help="API base URL override"),
375
- ):
376
- """Login to HyperClaw with your wallet.
377
-
378
- Signs a challenge message with your wallet key to authenticate,
379
- then creates a user-bound API key for agent management.
380
-
381
- Prerequisite: hyper wallet create (if you don't have a wallet yet)
382
-
383
- Flow:
384
- 1. Signs a challenge with your wallet private key
385
- 2. Backend verifies signature, creates/finds your user
386
- 3. Creates an API key bound to your user account
387
- 4. Saves the key to ~/.hypercli/claw-key.json
388
-
389
- After login, you can use:
390
- hyper agents create Launch an OpenClaw agent pod
391
- hyper agents list List your agents
392
- hyper claw config Generate provider configs
393
- """
394
- try:
395
- from eth_account.messages import encode_defunct
396
- from eth_account import Account
397
- except ImportError:
398
- console.print("[red]❌ Wallet dependencies required[/red]")
399
- console.print("Install with: [bold]pip install 'hypercli-cli[wallet]'[/bold]")
400
- raise typer.Exit(1)
401
-
402
- import httpx
403
-
404
- # Import wallet loader from wallet module
405
- from .wallet import load_wallet
406
-
407
- base_url = (api_url or PROD_API_BASE).rstrip("/")
408
-
409
- # Step 1: Load wallet
410
- account = load_wallet()
411
- console.print(f"\n[green]✓[/green] Wallet: [bold]{account.address}[/bold]\n")
412
-
413
- # Step 2: Get challenge
414
- console.print("[bold]Requesting challenge...[/bold]")
415
- with httpx.Client(timeout=15) as client:
416
- resp = client.post(
417
- f"{base_url}/api/auth/wallet/challenge",
418
- json={"wallet": account.address},
419
- )
420
- if resp.status_code != 200:
421
- console.print(f"[red]❌ Challenge failed: {resp.text}[/red]")
422
- raise typer.Exit(1)
423
- challenge = resp.json()
424
-
425
- # Step 3: Sign
426
- console.print("[bold]Signing...[/bold]")
427
- message = encode_defunct(text=challenge["message"])
428
- signed = account.sign_message(message)
429
-
430
- # Step 4: Verify signature and login
431
- console.print("[bold]Authenticating...[/bold]")
432
- with httpx.Client(timeout=15) as client:
433
- resp = client.post(
434
- f"{base_url}/api/auth/wallet/login",
435
- json={
436
- "wallet": account.address,
437
- "signature": signed.signature.hex(),
438
- "timestamp": challenge["timestamp"],
439
- },
440
- )
441
- if resp.status_code != 200:
442
- console.print(f"[red]❌ Login failed: {resp.text}[/red]")
443
- raise typer.Exit(1)
444
- login_data = resp.json()
445
- jwt_token = login_data["token"]
446
-
447
- console.print("[green]✓[/green] Authenticated\n")
448
-
449
- user_id = login_data.get("user_id", "")
450
- team_id = login_data.get("team_id", "")
451
- wallet_addr = login_data.get("wallet_address", account.address)
452
-
453
- # Step 5: Create a claw API key using the JWT
454
- console.print("[bold]Creating API key...[/bold]")
455
- with httpx.Client(timeout=15) as client:
456
- resp = client.post(
457
- f"{base_url}/api/keys",
458
- json={"name": "claw-cli"},
459
- headers={"Authorization": f"Bearer {jwt_token}"},
460
- )
461
- if resp.status_code != 200:
462
- # Save JWT anyway so user can still auth
463
- jwt_path = HYPERCLI_DIR / "claw-jwt.json"
464
- HYPERCLI_DIR.mkdir(parents=True, exist_ok=True)
465
- with open(jwt_path, "w") as f:
466
- json.dump({"token": jwt_token, "user_id": user_id, "team_id": team_id}, f, indent=2)
467
- console.print(f"[yellow]⚠ Key creation failed: {resp.text}[/yellow]")
468
- console.print(f"[green]✓[/green] JWT saved to {jwt_path} (use for direct auth)")
469
- raise typer.Exit(1)
470
-
471
- key_data = resp.json()
472
-
473
- api_key = key_data.get("api_key", key_data.get("key", ""))
474
-
475
- # Step 6: Save as claw key
476
- HYPERCLI_DIR.mkdir(parents=True, exist_ok=True)
477
- claw_key_data = {
478
- "key": api_key,
479
- "plan_id": login_data.get("plan_id", "free"),
480
- "user_id": user_id,
481
- "team_id": team_id,
482
- "wallet_address": wallet_addr,
483
- "tpm_limit": 0,
484
- "rpm_limit": 0,
485
- "expires_at": "",
486
- }
487
- with open(CLAW_KEY_PATH, "w") as f:
488
- json.dump(claw_key_data, f, indent=2)
489
-
490
- console.print(f"[green]✓[/green] API key saved to [bold]{CLAW_KEY_PATH}[/bold]\n")
491
- console.print(f" User: {user_id[:12]}...")
492
- console.print(f" Team: {team_id[:12]}...")
493
- console.print(f" Key: {api_key[:20]}...")
494
- console.print(f" Wallet: {wallet_addr}")
495
- console.print(f"\n[green]You're all set![/green]")
496
- console.print(f" Launch agent: [bold]hyper agents create[/bold]")
497
- console.print(f" Configure: [bold]hyper claw config openclaw --apply[/bold]")
498
-
499
-
500
358
  OPENCLAW_CONFIG_PATH = Path.home() / ".openclaw" / "openclaw.json"
501
359
 
502
360
 
@@ -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 agents, billing, claw, comfyui, flow, instances, jobs, keys, user, wallet
10
+ from . import billing, claw, comfyui, flow, instances, jobs, keys, user, wallet
11
11
 
12
12
  console = Console()
13
13
 
@@ -56,7 +56,6 @@ app = typer.Typer(
56
56
  )
57
57
 
58
58
  # Register subcommands
59
- app.add_typer(agents.app, name="agents")
60
59
  app.add_typer(billing.app, name="billing")
61
60
  app.add_typer(claw.app, name="claw")
62
61
  app.add_typer(comfyui.app, name="comfyui")
@@ -195,7 +195,6 @@ def launch(
195
195
  lb_auth: bool = typer.Option(False, "--lb-auth", help="Enable auth on load balancer"),
196
196
  registry_user: Optional[str] = typer.Option(None, "--registry-user", help="Private registry username"),
197
197
  registry_password: Optional[str] = typer.Option(None, "--registry-password", help="Private registry password"),
198
- dockerfile: Optional[str] = typer.Option(None, "--dockerfile", "-d", help="Path to Dockerfile (built on GPU node, overrides image as base)"),
199
198
  x402: bool = typer.Option(False, "--x402", help="Pay per-use via embedded x402 wallet"),
200
199
  amount: Optional[float] = typer.Option(None, "--amount", help="USDC amount to spend with --x402"),
201
200
  follow: bool = typer.Option(False, "--follow", "-f", help="Follow logs after creation"),
@@ -235,16 +234,6 @@ def launch(
235
234
  if registry_user and registry_password:
236
235
  registry_auth = {"username": registry_user, "password": registry_password}
237
236
 
238
- # Read and base64-encode Dockerfile if provided
239
- dockerfile_b64 = None
240
- if dockerfile:
241
- import base64
242
- from pathlib import Path
243
- df_path = Path(dockerfile)
244
- if not df_path.exists():
245
- raise typer.BadParameter(f"Dockerfile not found: {dockerfile}")
246
- dockerfile_b64 = base64.b64encode(df_path.read_bytes()).decode()
247
-
248
237
  # Auto-wrap command in sh -c if it contains shell operators
249
238
  if command and any(op in command for op in ["&&", "||", "|", ";", ">", "<", "$"]):
250
239
  command = f'sh -c "{command}"'
@@ -321,7 +310,6 @@ def launch(
321
310
  ports=ports_dict,
322
311
  auth=lb_auth,
323
312
  registry_auth=registry_auth,
324
- dockerfile=dockerfile_b64,
325
313
  )
326
314
 
327
315
  if fmt == "json":
@@ -232,6 +232,106 @@ def _follow_job(job_id: str, cancel_on_exit: bool = False):
232
232
  run_job_monitor(job_id, cancel_on_exit=cancel_on_exit)
233
233
 
234
234
 
235
+ @app.command("shell")
236
+ def shell(
237
+ job_id: str = typer.Argument(..., help="Job ID (full or prefix)"),
238
+ shell_cmd: str = typer.Option("/bin/bash", "--shell", "-s", help="Shell to use"),
239
+ ):
240
+ """Open an interactive shell on a running job container (WebSocket PTY)"""
241
+ import asyncio
242
+ import sys
243
+
244
+ client = get_client()
245
+ job_id = _resolve_job_id(client, job_id)
246
+
247
+ with spinner("Connecting to shell..."):
248
+ job = client.jobs.get(job_id)
249
+
250
+ if job.state != "running":
251
+ console.print(f"[red]Error:[/red] Job is {job.state}, not running")
252
+ raise typer.Exit(1)
253
+
254
+ console.print(f"[dim]Connected to job {job_id[:8]}... (press Ctrl+D or type 'exit' to disconnect)[/dim]")
255
+
256
+ asyncio.run(_run_shell(client, job_id, job.job_key, shell_cmd))
257
+
258
+
259
+ async def _run_shell(client, job_id: str, job_key: str, shell_cmd: str):
260
+ """Run interactive shell with raw terminal mode."""
261
+ import os
262
+ import sys
263
+ import signal
264
+ import struct
265
+ import termios
266
+ import tty
267
+ import fcntl
268
+
269
+ from hypercli.shell import shell_connect
270
+
271
+ loop = asyncio.get_event_loop()
272
+
273
+ # Save terminal state
274
+ stdin_fd = sys.stdin.fileno()
275
+ old_settings = termios.tcgetattr(stdin_fd)
276
+
277
+ session = None
278
+ try:
279
+ # Connect
280
+ session = await shell_connect(
281
+ client,
282
+ job_id,
283
+ job_key=job_key,
284
+ shell=shell_cmd,
285
+ on_output=lambda data: sys.stdout.write(data) or sys.stdout.flush(),
286
+ on_close=lambda reason: None,
287
+ )
288
+
289
+ # Send initial terminal size
290
+ try:
291
+ sz = struct.unpack("hh", fcntl.ioctl(stdin_fd, termios.TIOCGWINSZ, b"\x00" * 4))
292
+ await session.resize(cols=sz[1], rows=sz[0])
293
+ except Exception:
294
+ await session.resize(cols=80, rows=24)
295
+
296
+ # Handle SIGWINCH (terminal resize)
297
+ def on_resize(*_):
298
+ try:
299
+ sz = struct.unpack("hh", fcntl.ioctl(stdin_fd, termios.TIOCGWINSZ, b"\x00" * 4))
300
+ asyncio.run_coroutine_threadsafe(session.resize(cols=sz[1], rows=sz[0]), loop)
301
+ except Exception:
302
+ pass
303
+
304
+ signal.signal(signal.SIGWINCH, on_resize)
305
+
306
+ # Enter raw mode
307
+ tty.setraw(stdin_fd)
308
+
309
+ # Read stdin and forward to shell
310
+ reader = asyncio.StreamReader()
311
+ protocol = asyncio.StreamReaderProtocol(reader)
312
+ await loop.connect_read_pipe(lambda: protocol, sys.stdin)
313
+
314
+ while not session.closed:
315
+ try:
316
+ data = await asyncio.wait_for(reader.read(4096), timeout=0.5)
317
+ if not data:
318
+ break
319
+ await session.send(data.decode(errors="replace"))
320
+ except asyncio.TimeoutError:
321
+ continue
322
+ except Exception:
323
+ break
324
+
325
+ finally:
326
+ # Restore terminal
327
+ termios.tcsetattr(stdin_fd, termios.TCSADRAIN, old_settings)
328
+ signal.signal(signal.SIGWINCH, signal.SIG_DFL)
329
+ if session:
330
+ await session.close()
331
+ sys.stdout.write("\r\n")
332
+ sys.stdout.flush()
333
+
334
+
235
335
  def _watch_metrics(job_id: str):
236
336
  """Watch metrics live"""
237
337
  import time
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "hypercli-cli"
7
- version = "0.9.1"
7
+ version = "1.0.0"
8
8
  description = "CLI for HyperCLI - GPU orchestration and LLM API"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -13,7 +13,7 @@ authors = [
13
13
  { name = "HyperCLI", email = "support@hypercli.com" }
14
14
  ]
15
15
  dependencies = [
16
- "hypercli-sdk>=0.8.9",
16
+ "hypercli-sdk>=1.0.0",
17
17
  "typer>=0.20.0",
18
18
  "rich>=14.2.0",
19
19
  "websocket-client>=1.6.0",
@@ -24,7 +24,7 @@ dependencies = [
24
24
 
25
25
  [project.optional-dependencies]
26
26
  comfyui = [
27
- "hypercli-sdk[comfyui]>=0.8.9",
27
+ "hypercli-sdk[comfyui]>=1.0.0",
28
28
  ]
29
29
  wallet = [
30
30
  "x402[httpx,evm]>=2.0.0",
@@ -34,7 +34,7 @@ wallet = [
34
34
  "qrcode[pil]>=7.4.0",
35
35
  ]
36
36
  all = [
37
- "hypercli-sdk[comfyui]>=0.8.9",
37
+ "hypercli-sdk[comfyui]>=1.0.0",
38
38
  "x402[httpx,evm]>=2.0.0",
39
39
  "eth-account>=0.13.0",
40
40
  "web3>=7.0.0",
@@ -1,559 +0,0 @@
1
- """HyperClaw Agents — Reef Pod Management CLI"""
2
- from __future__ import annotations
3
-
4
- import json
5
- import os
6
- import sys
7
- import time
8
- from pathlib import Path
9
-
10
- import typer
11
- from rich.console import Console
12
- from rich.table import Table
13
-
14
- from hypercli.agents import Agents, ReefPod
15
-
16
- app = typer.Typer(help="Manage OpenClaw agent pods (reef containers)")
17
- console = Console()
18
-
19
- # Config — uses HyperClaw API key (sk-...) for backend auth
20
- CLAW_KEY_PATH = Path.home() / ".hypercli" / "claw-key.json"
21
- STATE_DIR = Path.home() / ".hypercli"
22
- AGENTS_STATE = STATE_DIR / "agents.json"
23
-
24
-
25
- def _get_claw_api_key() -> str:
26
- """Resolve HyperClaw API key from env or saved key file."""
27
- key = os.environ.get("HYPERCLAW_API_KEY", "")
28
- if key:
29
- return key
30
- if CLAW_KEY_PATH.exists():
31
- with open(CLAW_KEY_PATH) as f:
32
- data = json.load(f)
33
- key = data.get("key", "")
34
- if key:
35
- return key
36
- console.print("[red]❌ No HyperClaw API key found.[/red]")
37
- console.print("Set HYPERCLAW_API_KEY or subscribe: [bold]hyper claw subscribe 1aiu[/bold]")
38
- raise typer.Exit(1)
39
-
40
-
41
- def _get_agents_client() -> Agents:
42
- """Create an Agents client using the HyperClaw API key."""
43
- from hypercli.http import HTTPClient
44
- api_key = _get_claw_api_key()
45
- api_base = os.environ.get("HYPERCLAW_API_BASE", "https://api.hyperclaw.app")
46
- http = HTTPClient(api_base, api_key)
47
- return Agents(http, claw_api_key=api_key, claw_api_base=api_base)
48
-
49
-
50
- def _save_pod_state(pod: ReefPod):
51
- """Save pod info locally for quick reference."""
52
- STATE_DIR.mkdir(parents=True, exist_ok=True)
53
- state = _load_state()
54
- state[pod.id] = {
55
- "id": pod.id,
56
- "pod_id": pod.pod_id,
57
- "pod_name": pod.pod_name,
58
- "user_id": pod.user_id,
59
- "hostname": pod.hostname,
60
- "jwt_token": pod.jwt_token,
61
- "state": pod.state,
62
- }
63
- with open(AGENTS_STATE, "w") as f:
64
- json.dump(state, f, indent=2, default=str)
65
-
66
-
67
- def _load_state() -> dict:
68
- if AGENTS_STATE.exists():
69
- with open(AGENTS_STATE) as f:
70
- return json.load(f)
71
- return {}
72
-
73
-
74
- def _remove_pod_state(agent_id: str):
75
- state = _load_state()
76
- state.pop(agent_id, None)
77
- with open(AGENTS_STATE, "w") as f:
78
- json.dump(state, f, indent=2, default=str)
79
-
80
-
81
- def _resolve_agent(agent_id: str) -> str:
82
- """Resolve agent_id with prefix matching from local state."""
83
- state = _load_state()
84
- if agent_id in state:
85
- return agent_id
86
- matches = [k for k in state if k.startswith(agent_id)]
87
- if len(matches) == 1:
88
- return matches[0]
89
- if len(matches) > 1:
90
- console.print(f"[yellow]Ambiguous ID prefix '{agent_id}'. Matches:[/yellow]")
91
- for m in matches:
92
- s = state[m]
93
- console.print(f" {m[:12]} {s.get('pod_name', '')} {s.get('state', '')}")
94
- raise typer.Exit(1)
95
- return agent_id
96
-
97
-
98
- def _get_pod_with_token(agent_id: str) -> ReefPod:
99
- """Get a ReefPod, filling JWT from local state if needed."""
100
- agents = _get_agents_client()
101
- pod = agents.get(agent_id)
102
- if not pod.jwt_token:
103
- state = _load_state()
104
- local = state.get(agent_id, {})
105
- if local.get("jwt_token"):
106
- pod.jwt_token = local["jwt_token"]
107
- return pod
108
-
109
-
110
- @app.command("create")
111
- def create(
112
- wait: bool = typer.Option(True, "--wait/--no-wait", help="Wait for pod to be running"),
113
- ):
114
- """Create a new OpenClaw agent pod."""
115
- agents = _get_agents_client()
116
-
117
- console.print("\n[bold]Creating agent pod...[/bold]")
118
-
119
- try:
120
- pod = agents.create()
121
- except Exception as e:
122
- console.print(f"[red]❌ Create failed: {e}[/red]")
123
- raise typer.Exit(1)
124
-
125
- _save_pod_state(pod)
126
-
127
- console.print(f"[green]✓[/green] Agent created: [bold]{pod.id[:12]}[/bold]")
128
- console.print(f" Pod: {pod.pod_name}")
129
- console.print(f" State: {pod.state}")
130
- console.print(f" Desktop: {pod.vnc_url}")
131
- console.print(f" Shell: {pod.shell_url}")
132
-
133
- if wait:
134
- console.print("\n[dim]Waiting for pod to start...[/dim]")
135
- for i in range(60):
136
- time.sleep(5)
137
- try:
138
- pod = agents.get(pod.id)
139
- _save_pod_state(pod)
140
- if pod.is_running:
141
- console.print(f"[green]✅ Agent is running![/green]")
142
- break
143
- elif pod.state in ("failed", "stopped"):
144
- console.print(f"[red]❌ Agent failed: {pod.state}[/red]")
145
- if pod.last_error:
146
- console.print(f" Error: {pod.last_error}")
147
- raise typer.Exit(1)
148
- else:
149
- console.print(f" [{i*5}s] State: {pod.state}")
150
- except typer.Exit:
151
- raise
152
- except Exception as e:
153
- console.print(f" [{i*5}s] Checking... ({e})")
154
- else:
155
- console.print("[yellow]⚠ Timed out (5 min). Pod may still be starting.[/yellow]")
156
-
157
- console.print(f"\nExec: [bold]hyper agents exec {pod.id[:8]} 'echo hello'[/bold]")
158
- console.print(f"Shell: [bold]hyper agents shell {pod.id[:8]}[/bold]")
159
- console.print(f"Desktop: {pod.vnc_url}")
160
-
161
-
162
- @app.command("list")
163
- def list_agents(
164
- json_output: bool = typer.Option(False, "--json", help="JSON output"),
165
- ):
166
- """List all agent pods."""
167
- agents = _get_agents_client()
168
-
169
- try:
170
- pods = agents.list()
171
- except Exception as e:
172
- console.print(f"[red]❌ Failed to list agents: {e}[/red]")
173
- raise typer.Exit(1)
174
-
175
- if json_output:
176
- console.print_json(json.dumps([{
177
- "id": p.id, "pod_name": p.pod_name, "state": p.state,
178
- "hostname": p.hostname, "vnc_url": p.vnc_url,
179
- } for p in pods], indent=2, default=str))
180
- return
181
-
182
- if not pods:
183
- console.print("[dim]No agents found.[/dim]")
184
- console.print("Create one: [bold]hyper agents create[/bold]")
185
- return
186
-
187
- table = Table(title="Agents")
188
- table.add_column("ID", style="cyan", no_wrap=True)
189
- table.add_column("Pod", style="blue")
190
- table.add_column("State")
191
- table.add_column("Desktop URL")
192
- table.add_column("Created")
193
-
194
- for pod in pods:
195
- style = {"running": "green", "pending": "yellow", "starting": "yellow"}.get(pod.state, "red")
196
- created = pod.created_at.strftime("%Y-%m-%d %H:%M") if pod.created_at else ""
197
- table.add_row(
198
- pod.id[:12],
199
- pod.pod_name or "",
200
- f"[{style}]{pod.state}[/{style}]",
201
- pod.vnc_url or "",
202
- created,
203
- )
204
- _save_pod_state(pod)
205
-
206
- console.print()
207
- console.print(table)
208
- console.print()
209
-
210
-
211
- @app.command("status")
212
- def status(
213
- agent_id: str = typer.Argument(..., help="Agent ID (or prefix)"),
214
- ):
215
- """Get detailed status of an agent."""
216
- agent_id = _resolve_agent(agent_id)
217
- agents = _get_agents_client()
218
-
219
- try:
220
- pod = agents.get(agent_id)
221
- except Exception as e:
222
- console.print(f"[red]❌ Failed to get agent: {e}[/red]")
223
- raise typer.Exit(1)
224
-
225
- _save_pod_state(pod)
226
-
227
- console.print(f"\n[bold]Agent {pod.id[:12]}[/bold]")
228
- console.print(f" Pod: {pod.pod_name}")
229
- console.print(f" State: {pod.state}")
230
- console.print(f" Desktop: {pod.vnc_url}")
231
- console.print(f" Shell: {pod.shell_url}")
232
- console.print(f" Created: {pod.created_at}")
233
- if pod.started_at:
234
- console.print(f" Started: {pod.started_at}")
235
- if pod.stopped_at:
236
- console.print(f" Stopped: {pod.stopped_at}")
237
- if pod.jwt_expires_at:
238
- console.print(f" JWT Expires: {pod.jwt_expires_at}")
239
- if pod.last_error:
240
- console.print(f" Error: [red]{pod.last_error}[/red]")
241
-
242
- if pod.is_running and pod.executor_url:
243
- try:
244
- health = agents.health(pod)
245
- console.print(f"\n[bold]Executor:[/bold]")
246
- console.print(f" Status: {health.get('status', 'unknown')}")
247
- console.print(f" Disk Free: {health.get('disk_free_mb', '?')} MB")
248
- except Exception as e:
249
- console.print(f"\n[dim]Executor not reachable: {e}[/dim]")
250
-
251
-
252
- @app.command("start")
253
- def start(
254
- agent_id: str = typer.Argument(..., help="Agent ID (or prefix)"),
255
- ):
256
- """Start a previously stopped agent."""
257
- agent_id = _resolve_agent(agent_id)
258
- agents = _get_agents_client()
259
-
260
- try:
261
- pod = agents.start(agent_id)
262
- except Exception as e:
263
- console.print(f"[red]❌ Failed to start agent: {e}[/red]")
264
- raise typer.Exit(1)
265
-
266
- _save_pod_state(pod)
267
- console.print(f"[green]✓[/green] Agent starting: {pod.pod_name}")
268
- console.print(f" Desktop: {pod.vnc_url}")
269
-
270
-
271
- @app.command("stop")
272
- def stop(
273
- agent_id: str = typer.Argument(..., help="Agent ID (or prefix)"),
274
- force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation"),
275
- ):
276
- """Stop an agent (keeps DB record, destroys pod)."""
277
- agent_id = _resolve_agent(agent_id)
278
-
279
- if not force:
280
- confirm = typer.confirm(f"Stop agent {agent_id[:12]}?")
281
- if not confirm:
282
- raise typer.Exit(0)
283
-
284
- agents = _get_agents_client()
285
-
286
- try:
287
- pod = agents.stop(agent_id)
288
- except Exception as e:
289
- console.print(f"[red]❌ Failed to stop agent: {e}[/red]")
290
- raise typer.Exit(1)
291
-
292
- _save_pod_state(pod)
293
- console.print(f"[green]✅ Agent stopped[/green]")
294
- console.print(f"Restart with: [bold]hyper agents start {agent_id[:8]}[/bold]")
295
-
296
-
297
- @app.command("delete")
298
- def delete(
299
- agent_id: str = typer.Argument(..., help="Agent ID (or prefix)"),
300
- force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation"),
301
- ):
302
- """Delete an agent entirely (pod + record)."""
303
- agent_id = _resolve_agent(agent_id)
304
-
305
- if not force:
306
- confirm = typer.confirm(f"Permanently delete agent {agent_id[:12]}?")
307
- if not confirm:
308
- raise typer.Exit(0)
309
-
310
- agents = _get_agents_client()
311
-
312
- try:
313
- agents.delete(agent_id)
314
- except Exception as e:
315
- console.print(f"[red]❌ Failed to delete agent: {e}[/red]")
316
- raise typer.Exit(1)
317
-
318
- _remove_pod_state(agent_id)
319
- console.print(f"[green]✅ Agent {agent_id[:12]} deleted[/green]")
320
-
321
-
322
- @app.command("exec")
323
- def exec_cmd(
324
- agent_id: str = typer.Argument(..., help="Agent ID (or prefix)"),
325
- command: str = typer.Argument(..., help="Command to execute"),
326
- timeout: int = typer.Option(30, "--timeout", "-t", help="Command timeout (seconds)"),
327
- ):
328
- """Execute a command on an agent pod."""
329
- agent_id = _resolve_agent(agent_id)
330
-
331
- try:
332
- pod = _get_pod_with_token(agent_id)
333
- except Exception as e:
334
- console.print(f"[red]❌ Failed to get agent: {e}[/red]")
335
- raise typer.Exit(1)
336
-
337
- agents = _get_agents_client()
338
-
339
- try:
340
- result = agents.exec(pod, command, timeout=timeout)
341
- except Exception as e:
342
- console.print(f"[red]❌ Exec failed: {e}[/red]")
343
- raise typer.Exit(1)
344
-
345
- if result.stdout:
346
- sys.stdout.write(result.stdout)
347
- if not result.stdout.endswith("\n"):
348
- sys.stdout.write("\n")
349
- if result.stderr:
350
- sys.stderr.write(result.stderr)
351
- if not result.stderr.endswith("\n"):
352
- sys.stderr.write("\n")
353
-
354
- raise typer.Exit(result.exit_code)
355
-
356
-
357
- @app.command("shell")
358
- def shell(
359
- agent_id: str = typer.Argument(..., help="Agent ID (or prefix)"),
360
- ):
361
- """Open an interactive shell on an agent pod (WebSocket PTY).
362
-
363
- Connects to the executor's /shell endpoint. Press Ctrl+] to disconnect.
364
- """
365
- agent_id = _resolve_agent(agent_id)
366
-
367
- try:
368
- pod = _get_pod_with_token(agent_id)
369
- except Exception as e:
370
- console.print(f"[red]❌ Failed to get agent: {e}[/red]")
371
- raise typer.Exit(1)
372
-
373
- if not pod.executor_url:
374
- console.print("[red]❌ Agent has no executor URL[/red]")
375
- raise typer.Exit(1)
376
-
377
- ws_url = pod.executor_url.replace("https://", "wss://").replace("http://", "ws://")
378
- ws_url = f"{ws_url}/shell"
379
-
380
- console.print(f"[dim]Connecting to {ws_url}...[/dim]")
381
-
382
- try:
383
- import websockets
384
- import asyncio
385
- import termios
386
- import tty
387
- except ImportError:
388
- console.print("[red]❌ 'websockets' required: pip install websockets[/red]")
389
- raise typer.Exit(1)
390
-
391
- async def _run_shell():
392
- headers = {}
393
- if pod.jwt_token:
394
- headers["Authorization"] = f"Bearer {pod.jwt_token}"
395
- headers["Cookie"] = f"{pod.pod_name}-token={pod.jwt_token}"
396
-
397
- async with websockets.connect(ws_url, additional_headers=headers) as ws:
398
- console.print("[green]Connected.[/green] Ctrl+] to disconnect.\n")
399
-
400
- old_settings = termios.tcgetattr(sys.stdin)
401
- try:
402
- tty.setraw(sys.stdin.fileno())
403
-
404
- import shutil
405
- cols, rows = shutil.get_terminal_size()
406
- await ws.send(f"\x1b[8;{rows};{cols}t")
407
-
408
- async def read_ws():
409
- try:
410
- async for msg in ws:
411
- if isinstance(msg, str):
412
- sys.stdout.write(msg)
413
- sys.stdout.flush()
414
- elif isinstance(msg, bytes):
415
- sys.stdout.buffer.write(msg)
416
- sys.stdout.buffer.flush()
417
- except websockets.ConnectionClosed:
418
- pass
419
-
420
- async def read_stdin():
421
- loop = asyncio.get_event_loop()
422
- try:
423
- while True:
424
- data = await loop.run_in_executor(None, lambda: os.read(sys.stdin.fileno(), 1024))
425
- if not data:
426
- break
427
- if b"\x1d" in data:
428
- break
429
- await ws.send(data.decode(errors="replace"))
430
- except (websockets.ConnectionClosed, OSError):
431
- pass
432
-
433
- done, pending = await asyncio.wait(
434
- [asyncio.create_task(read_ws()), asyncio.create_task(read_stdin())],
435
- return_when=asyncio.FIRST_COMPLETED,
436
- )
437
- for t in pending:
438
- t.cancel()
439
- finally:
440
- termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_settings)
441
- console.print("\n[dim]Disconnected.[/dim]")
442
-
443
- try:
444
- asyncio.run(_run_shell())
445
- except KeyboardInterrupt:
446
- console.print("\n[dim]Disconnected.[/dim]")
447
- except Exception as e:
448
- console.print(f"[red]❌ Shell failed: {e}[/red]")
449
- raise typer.Exit(1)
450
-
451
-
452
- @app.command("logs")
453
- def logs(
454
- agent_id: str = typer.Argument(..., help="Agent ID (or prefix)"),
455
- lines: int = typer.Option(100, "-n", "--lines", help="Number of lines to show"),
456
- follow: bool = typer.Option(True, "-f/--no-follow", help="Follow log output"),
457
- ):
458
- """Stream logs from an agent pod."""
459
- agent_id = _resolve_agent(agent_id)
460
-
461
- try:
462
- pod = _get_pod_with_token(agent_id)
463
- except Exception as e:
464
- console.print(f"[red]❌ Failed to get agent: {e}[/red]")
465
- raise typer.Exit(1)
466
-
467
- agents = _get_agents_client()
468
-
469
- try:
470
- for line in agents.logs_stream(pod, lines=lines, follow=follow):
471
- console.print(line)
472
- except KeyboardInterrupt:
473
- pass
474
- except Exception as e:
475
- console.print(f"[red]❌ Logs failed: {e}[/red]")
476
- raise typer.Exit(1)
477
-
478
-
479
- @app.command("chat")
480
- def chat(
481
- agent_id: str = typer.Argument(..., help="Agent ID (or prefix)"),
482
- model: str = typer.Option("hyperclaw/kimi-k2.5", "--model", "-m", help="Model to use"),
483
- ):
484
- """Interactive chat with an agent's OpenClaw instance.
485
-
486
- Connects to the OpenClaw gateway running inside the agent pod.
487
- Type your messages, get streaming responses. Ctrl+C or 'exit' to quit.
488
- """
489
- agent_id = _resolve_agent(agent_id)
490
-
491
- try:
492
- pod = _get_pod_with_token(agent_id)
493
- except Exception as e:
494
- console.print(f"[red]❌ Failed to get agent: {e}[/red]")
495
- raise typer.Exit(1)
496
-
497
- agents = _get_agents_client()
498
- messages = []
499
-
500
- console.print(f"\n[bold]Chat with agent {pod.pod_name}[/bold] (model: {model})")
501
- console.print("[dim]Type your message. 'exit' or Ctrl+C to quit.[/dim]\n")
502
-
503
- while True:
504
- try:
505
- user_input = console.input("[bold cyan]> [/bold cyan]")
506
- except (EOFError, KeyboardInterrupt):
507
- console.print("\n[dim]Bye.[/dim]")
508
- break
509
-
510
- user_input = user_input.strip()
511
- if not user_input:
512
- continue
513
- if user_input.lower() in ("exit", "quit", "/exit", "/quit"):
514
- console.print("[dim]Bye.[/dim]")
515
- break
516
-
517
- messages.append({"role": "user", "content": user_input})
518
-
519
- try:
520
- full_response = ""
521
- for chunk in agents.chat_stream(pod, messages, model=model):
522
- sys.stdout.write(chunk)
523
- sys.stdout.flush()
524
- full_response += chunk
525
- sys.stdout.write("\n\n")
526
- sys.stdout.flush()
527
-
528
- messages.append({"role": "assistant", "content": full_response})
529
- except KeyboardInterrupt:
530
- sys.stdout.write("\n")
531
- continue
532
- except Exception as e:
533
- console.print(f"\n[red]Error: {e}[/red]\n")
534
- # Remove failed user message
535
- messages.pop()
536
-
537
-
538
- @app.command("token")
539
- def token(
540
- agent_id: str = typer.Argument(..., help="Agent ID (or prefix)"),
541
- ):
542
- """Refresh the JWT token for an agent."""
543
- agent_id = _resolve_agent(agent_id)
544
- agents = _get_agents_client()
545
-
546
- try:
547
- result = agents.refresh_token(agent_id)
548
- except Exception as e:
549
- console.print(f"[red]❌ Failed to refresh token: {e}[/red]")
550
- raise typer.Exit(1)
551
-
552
- state = _load_state()
553
- if agent_id in state:
554
- state[agent_id]["jwt_token"] = result.get("token", "")
555
- with open(AGENTS_STATE, "w") as f:
556
- json.dump(state, f, indent=2, default=str)
557
-
558
- console.print(f"[green]✅ Token refreshed[/green]")
559
- console.print(f" Expires: {result.get('expires_at', 'unknown')}")
@@ -1,167 +0,0 @@
1
- """HyperClaw Voice API commands — TTS, clone, design"""
2
- import base64
3
- import json
4
- import sys
5
- from pathlib import Path
6
-
7
- import httpx
8
- import typer
9
- from rich.console import Console
10
-
11
- app = typer.Typer(help="Voice API — text-to-speech, voice cloning, voice design")
12
- console = Console()
13
-
14
- HYPERCLI_DIR = Path.home() / ".hypercli"
15
- CLAW_KEY_PATH = HYPERCLI_DIR / "claw-key.json"
16
- PROD_API_BASE = "https://api.hyperclaw.app"
17
- DEV_API_BASE = "https://dev-api.hyperclaw.app"
18
-
19
-
20
- def _get_api_key(key: str | None) -> str:
21
- """Resolve API key from flag or saved claw key."""
22
- if key:
23
- return key
24
- if CLAW_KEY_PATH.exists():
25
- with open(CLAW_KEY_PATH) as f:
26
- k = json.load(f).get("key", "")
27
- if k:
28
- return k
29
- console.print("[red]❌ No API key found.[/red]")
30
- console.print("Pass [bold]--key sk-...[/bold] or run [bold]hyper claw subscribe[/bold]")
31
- raise typer.Exit(1)
32
-
33
-
34
- def _post_voice(
35
- endpoint: str,
36
- payload: dict,
37
- api_key: str,
38
- output: Path,
39
- dev: bool = False,
40
- ):
41
- """POST to voice endpoint and save audio output."""
42
- api_base = DEV_API_BASE if dev else PROD_API_BASE
43
- url = f"{api_base}/voice/{endpoint}"
44
-
45
- console.print(f"[dim]→ POST {url}[/dim]")
46
-
47
- try:
48
- with httpx.Client(timeout=600.0) as client:
49
- resp = client.post(
50
- url,
51
- json=payload,
52
- headers={
53
- "Authorization": f"Bearer {api_key}",
54
- "Content-Type": "application/json",
55
- },
56
- )
57
-
58
- if resp.status_code != 200:
59
- console.print(f"[red]❌ {resp.status_code}: {resp.text[:500]}[/red]")
60
- raise typer.Exit(1)
61
-
62
- output.parent.mkdir(parents=True, exist_ok=True)
63
- with open(output, "wb") as f:
64
- f.write(resp.content)
65
-
66
- size_kb = len(resp.content) / 1024
67
- console.print(f"[green]✅ Saved {output} ({size_kb:.1f} KB)[/green]")
68
-
69
- except httpx.HTTPError as e:
70
- console.print(f"[red]❌ Request failed: {e}[/red]")
71
- raise typer.Exit(1)
72
-
73
-
74
- @app.command("tts")
75
- def tts(
76
- text: str = typer.Argument(..., help="Text to synthesize"),
77
- voice: str = typer.Option("Chelsie", "--voice", "-v", help="Voice name (CustomVoice preset)"),
78
- language: str = typer.Option("auto", "--language", "-l", help="Language: auto, english, chinese, etc."),
79
- format: str = typer.Option("mp3", "--format", "-f", help="Output format: wav, mp3, opus, ogg, flac"),
80
- output: Path = typer.Option(None, "--output", "-o", help="Output audio file (default: output.<format>)"),
81
- key: str = typer.Option(None, "--key", "-k", help="API key (sk-...)"),
82
- dev: bool = typer.Option(False, "--dev", help="Use dev API"),
83
- ):
84
- """Generate speech from text using a preset voice.
85
-
86
- Examples:
87
- hyper claw voice tts "Hello world"
88
- hyper claw voice tts "Bonjour" -v Etienne -l french -f opus -o hello.opus
89
- """
90
- api_key = _get_api_key(key)
91
- if output is None:
92
- output = Path(f"output.{format}")
93
- payload = {
94
- "text": text,
95
- "voice": voice,
96
- "language": language,
97
- "response_format": format,
98
- }
99
- _post_voice("tts", payload, api_key, output, dev)
100
-
101
-
102
- @app.command("clone")
103
- def clone(
104
- text: str = typer.Argument(..., help="Text to synthesize"),
105
- ref_audio: Path = typer.Option(..., "--ref", "-r", help="Reference audio file (wav/mp3/ogg)"),
106
- language: str = typer.Option("auto", "--language", "-l", help="Language: auto, english, chinese, etc."),
107
- x_vector_only: bool = typer.Option(True, "--x-vector-only/--full-clone", help="Use x_vector_only mode (recommended)"),
108
- format: str = typer.Option("mp3", "--format", "-f", help="Output format: wav, mp3, opus, ogg, flac"),
109
- output: Path = typer.Option(None, "--output", "-o", help="Output audio file (default: output.<format>)"),
110
- key: str = typer.Option(None, "--key", "-k", help="API key (sk-...)"),
111
- dev: bool = typer.Option(False, "--dev", help="Use dev API"),
112
- ):
113
- """Clone a voice from reference audio.
114
-
115
- Examples:
116
- hyper claw voice clone "Hello" --ref voice.wav
117
- hyper claw voice clone "Test" -r ref.wav -l english -f mp3 -o cloned.mp3
118
- """
119
- api_key = _get_api_key(key)
120
- if output is None:
121
- output = Path(f"output.{format}")
122
-
123
- if not ref_audio.exists():
124
- console.print(f"[red]❌ Reference audio not found: {ref_audio}[/red]")
125
- raise typer.Exit(1)
126
-
127
- with open(ref_audio, "rb") as f:
128
- ref_b64 = base64.b64encode(f.read()).decode()
129
-
130
- console.print(f"[dim]Reference: {ref_audio} ({ref_audio.stat().st_size / 1024:.1f} KB)[/dim]")
131
-
132
- payload = {
133
- "text": text,
134
- "ref_audio_base64": ref_b64,
135
- "language": language,
136
- "x_vector_only": x_vector_only,
137
- "response_format": format,
138
- }
139
- _post_voice("clone", payload, api_key, output, dev)
140
-
141
-
142
- @app.command("design")
143
- def design(
144
- text: str = typer.Argument(..., help="Text to synthesize"),
145
- description: str = typer.Option(..., "--desc", "-d", help="Voice description (e.g. 'young female, warm, American accent')"),
146
- language: str = typer.Option("auto", "--language", "-l", help="Language: auto, english, chinese, etc."),
147
- format: str = typer.Option("mp3", "--format", "-f", help="Output format: wav, mp3, opus, ogg, flac"),
148
- output: Path = typer.Option(None, "--output", "-o", help="Output audio file (default: output.<format>)"),
149
- key: str = typer.Option(None, "--key", "-k", help="API key (sk-...)"),
150
- dev: bool = typer.Option(False, "--dev", help="Use dev API"),
151
- ):
152
- """Design a voice from a text description.
153
-
154
- Examples:
155
- hyper claw voice design "Hello" --desc "deep male voice, British accent"
156
- hyper claw voice design "Test" -d "young woman, cheerful" -f mp3 -o designed.mp3
157
- """
158
- api_key = _get_api_key(key)
159
- if output is None:
160
- output = Path(f"output.{format}")
161
- payload = {
162
- "text": text,
163
- "instruct": description,
164
- "language": language,
165
- "response_format": format,
166
- }
167
- _post_voice("design", payload, api_key, output, dev)
File without changes
File without changes