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.
- {hypercli_cli-0.9.1 → hypercli_cli-1.0.0}/PKG-INFO +4 -4
- {hypercli_cli-0.9.1 → hypercli_cli-1.0.0}/hypercli_cli/claw.py +2 -144
- {hypercli_cli-0.9.1 → hypercli_cli-1.0.0}/hypercli_cli/cli.py +1 -2
- {hypercli_cli-0.9.1 → hypercli_cli-1.0.0}/hypercli_cli/instances.py +0 -12
- {hypercli_cli-0.9.1 → hypercli_cli-1.0.0}/hypercli_cli/jobs.py +100 -0
- {hypercli_cli-0.9.1 → hypercli_cli-1.0.0}/pyproject.toml +4 -4
- hypercli_cli-0.9.1/hypercli_cli/agents.py +0 -559
- hypercli_cli-0.9.1/hypercli_cli/voice.py +0 -167
- {hypercli_cli-0.9.1 → hypercli_cli-1.0.0}/.gitignore +0 -0
- {hypercli_cli-0.9.1 → hypercli_cli-1.0.0}/README.md +0 -0
- {hypercli_cli-0.9.1 → hypercli_cli-1.0.0}/hypercli_cli/__init__.py +0 -0
- {hypercli_cli-0.9.1 → hypercli_cli-1.0.0}/hypercli_cli/billing.py +0 -0
- {hypercli_cli-0.9.1 → hypercli_cli-1.0.0}/hypercli_cli/comfyui.py +0 -0
- {hypercli_cli-0.9.1 → hypercli_cli-1.0.0}/hypercli_cli/flow.py +0 -0
- {hypercli_cli-0.9.1 → hypercli_cli-1.0.0}/hypercli_cli/keys.py +0 -0
- {hypercli_cli-0.9.1 → hypercli_cli-1.0.0}/hypercli_cli/onboard.py +0 -0
- {hypercli_cli-0.9.1 → hypercli_cli-1.0.0}/hypercli_cli/output.py +0 -0
- {hypercli_cli-0.9.1 → hypercli_cli-1.0.0}/hypercli_cli/renders.py +0 -0
- {hypercli_cli-0.9.1 → hypercli_cli-1.0.0}/hypercli_cli/tui/__init__.py +0 -0
- {hypercli_cli-0.9.1 → hypercli_cli-1.0.0}/hypercli_cli/tui/job_monitor.py +0 -0
- {hypercli_cli-0.9.1 → hypercli_cli-1.0.0}/hypercli_cli/user.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|