hypercli-cli 0.9.2__tar.gz → 1.0.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.
- {hypercli_cli-0.9.2 → hypercli_cli-1.0.1}/PKG-INFO +4 -4
- {hypercli_cli-0.9.2 → hypercli_cli-1.0.1}/hypercli_cli/agents.py +231 -40
- {hypercli_cli-0.9.2 → hypercli_cli-1.0.1}/hypercli_cli/instances.py +29 -8
- {hypercli_cli-0.9.2 → hypercli_cli-1.0.1}/hypercli_cli/jobs.py +226 -0
- {hypercli_cli-0.9.2 → hypercli_cli-1.0.1}/pyproject.toml +4 -4
- {hypercli_cli-0.9.2 → hypercli_cli-1.0.1}/.gitignore +0 -0
- {hypercli_cli-0.9.2 → hypercli_cli-1.0.1}/README.md +0 -0
- {hypercli_cli-0.9.2 → hypercli_cli-1.0.1}/hypercli_cli/__init__.py +0 -0
- {hypercli_cli-0.9.2 → hypercli_cli-1.0.1}/hypercli_cli/billing.py +0 -0
- {hypercli_cli-0.9.2 → hypercli_cli-1.0.1}/hypercli_cli/claw.py +0 -0
- {hypercli_cli-0.9.2 → hypercli_cli-1.0.1}/hypercli_cli/cli.py +0 -0
- {hypercli_cli-0.9.2 → hypercli_cli-1.0.1}/hypercli_cli/comfyui.py +0 -0
- {hypercli_cli-0.9.2 → hypercli_cli-1.0.1}/hypercli_cli/flow.py +0 -0
- {hypercli_cli-0.9.2 → hypercli_cli-1.0.1}/hypercli_cli/keys.py +0 -0
- {hypercli_cli-0.9.2 → hypercli_cli-1.0.1}/hypercli_cli/onboard.py +0 -0
- {hypercli_cli-0.9.2 → hypercli_cli-1.0.1}/hypercli_cli/output.py +0 -0
- {hypercli_cli-0.9.2 → hypercli_cli-1.0.1}/hypercli_cli/renders.py +0 -0
- {hypercli_cli-0.9.2 → hypercli_cli-1.0.1}/hypercli_cli/tui/__init__.py +0 -0
- {hypercli_cli-0.9.2 → hypercli_cli-1.0.1}/hypercli_cli/tui/job_monitor.py +0 -0
- {hypercli_cli-0.9.2 → hypercli_cli-1.0.1}/hypercli_cli/user.py +0 -0
- {hypercli_cli-0.9.2 → hypercli_cli-1.0.1}/hypercli_cli/voice.py +0 -0
- {hypercli_cli-0.9.2 → hypercli_cli-1.0.1}/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.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
|
|
@@ -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'
|
|
@@ -137,7 +137,7 @@ def budget():
|
|
|
137
137
|
|
|
138
138
|
@app.command("create")
|
|
139
139
|
def create(
|
|
140
|
-
name: str = typer.Option(
|
|
140
|
+
name: str = typer.Option(None, "--name", "-n", help="Agent name (auto-generated if omitted, becomes {name}.hyperclaw.app)"),
|
|
141
141
|
size: str = typer.Option(None, "--size", "-s", help="Size preset: small, medium, large"),
|
|
142
142
|
cpu: int = typer.Option(None, "--cpu", help="Custom CPU in cores"),
|
|
143
143
|
memory: int = typer.Option(None, "--memory", help="Custom memory in GB"),
|
|
@@ -399,41 +399,26 @@ def shell(
|
|
|
399
399
|
):
|
|
400
400
|
"""Open an interactive shell on an agent pod (WebSocket PTY).
|
|
401
401
|
|
|
402
|
-
Connects
|
|
402
|
+
Connects via the HyperClaw backend WebSocket proxy. Press Ctrl+] to disconnect.
|
|
403
403
|
"""
|
|
404
404
|
agent_id = _resolve_agent(agent_id)
|
|
405
|
+
agents = _get_agents_client()
|
|
405
406
|
|
|
406
|
-
|
|
407
|
-
pod = _get_pod_with_token(agent_id)
|
|
408
|
-
except Exception as e:
|
|
409
|
-
console.print(f"[red]❌ Failed to get agent: {e}[/red]")
|
|
410
|
-
raise typer.Exit(1)
|
|
411
|
-
|
|
412
|
-
if not pod.executor_url:
|
|
413
|
-
console.print("[red]❌ Agent has no executor URL[/red]")
|
|
414
|
-
raise typer.Exit(1)
|
|
415
|
-
|
|
416
|
-
ws_url = pod.executor_url.replace("https://", "wss://").replace("http://", "ws://")
|
|
417
|
-
ws_url = f"{ws_url}/shell"
|
|
418
|
-
|
|
419
|
-
console.print(f"[dim]Connecting to {ws_url}...[/dim]")
|
|
407
|
+
console.print(f"[dim]Connecting to shell...[/dim]")
|
|
420
408
|
|
|
421
409
|
try:
|
|
422
|
-
import websockets
|
|
423
410
|
import asyncio
|
|
424
411
|
import termios
|
|
425
412
|
import tty
|
|
426
413
|
except ImportError:
|
|
427
|
-
console.print("[red]❌
|
|
414
|
+
console.print("[red]❌ TTY libraries required[/red]")
|
|
428
415
|
raise typer.Exit(1)
|
|
429
416
|
|
|
430
417
|
async def _run_shell():
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
headers["Authorization"] = f"Bearer {pod.jwt_token}"
|
|
434
|
-
headers["Cookie"] = f"{pod.pod_name}-token={pod.jwt_token}"
|
|
418
|
+
# Connect via backend WebSocket
|
|
419
|
+
ws = await agents.shell_connect(agent_id)
|
|
435
420
|
|
|
436
|
-
|
|
421
|
+
try:
|
|
437
422
|
console.print("[green]Connected.[/green] Ctrl+] to disconnect.\n")
|
|
438
423
|
|
|
439
424
|
old_settings = termios.tcgetattr(sys.stdin)
|
|
@@ -453,7 +438,7 @@ def shell(
|
|
|
453
438
|
elif isinstance(msg, bytes):
|
|
454
439
|
sys.stdout.buffer.write(msg)
|
|
455
440
|
sys.stdout.buffer.flush()
|
|
456
|
-
except
|
|
441
|
+
except Exception:
|
|
457
442
|
pass
|
|
458
443
|
|
|
459
444
|
async def read_stdin():
|
|
@@ -463,10 +448,10 @@ def shell(
|
|
|
463
448
|
data = await loop.run_in_executor(None, lambda: os.read(sys.stdin.fileno(), 1024))
|
|
464
449
|
if not data:
|
|
465
450
|
break
|
|
466
|
-
if b"\x1d" in data:
|
|
451
|
+
if b"\x1d" in data: # Ctrl+]
|
|
467
452
|
break
|
|
468
453
|
await ws.send(data.decode(errors="replace"))
|
|
469
|
-
except
|
|
454
|
+
except Exception:
|
|
470
455
|
pass
|
|
471
456
|
|
|
472
457
|
done, pending = await asyncio.wait(
|
|
@@ -478,6 +463,8 @@ def shell(
|
|
|
478
463
|
finally:
|
|
479
464
|
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_settings)
|
|
480
465
|
console.print("\n[dim]Disconnected.[/dim]")
|
|
466
|
+
finally:
|
|
467
|
+
await ws.close()
|
|
481
468
|
|
|
482
469
|
try:
|
|
483
470
|
asyncio.run(_run_shell())
|
|
@@ -493,26 +480,46 @@ def logs(
|
|
|
493
480
|
agent_id: str = typer.Argument(..., help="Agent ID (or prefix)"),
|
|
494
481
|
lines: int = typer.Option(100, "-n", "--lines", help="Number of lines to show"),
|
|
495
482
|
follow: bool = typer.Option(True, "-f/--no-follow", help="Follow log output"),
|
|
483
|
+
ws: bool = typer.Option(False, "--ws", help="Use WebSocket instead of SSE (via backend)"),
|
|
496
484
|
):
|
|
497
485
|
"""Stream logs from an agent pod."""
|
|
498
486
|
agent_id = _resolve_agent(agent_id)
|
|
487
|
+
agents = _get_agents_client()
|
|
499
488
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
console.print(f"[red]❌ Failed to get agent: {e}[/red]")
|
|
504
|
-
raise typer.Exit(1)
|
|
489
|
+
if ws:
|
|
490
|
+
# WebSocket mode via backend
|
|
491
|
+
import asyncio
|
|
505
492
|
|
|
506
|
-
|
|
493
|
+
async def _stream_ws():
|
|
494
|
+
try:
|
|
495
|
+
async for line in agents.logs_stream_ws(agent_id, tail_lines=lines):
|
|
496
|
+
console.print(line)
|
|
497
|
+
except KeyboardInterrupt:
|
|
498
|
+
pass
|
|
499
|
+
except Exception as e:
|
|
500
|
+
console.print(f"[red]❌ Logs failed: {e}[/red]")
|
|
501
|
+
raise typer.Exit(1)
|
|
507
502
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
503
|
+
try:
|
|
504
|
+
asyncio.run(_stream_ws())
|
|
505
|
+
except KeyboardInterrupt:
|
|
506
|
+
pass
|
|
507
|
+
else:
|
|
508
|
+
# SSE mode via executor (legacy)
|
|
509
|
+
try:
|
|
510
|
+
pod = _get_pod_with_token(agent_id)
|
|
511
|
+
except Exception as e:
|
|
512
|
+
console.print(f"[red]❌ Failed to get agent: {e}[/red]")
|
|
513
|
+
raise typer.Exit(1)
|
|
514
|
+
|
|
515
|
+
try:
|
|
516
|
+
for line in agents.logs_stream(pod, lines=lines, follow=follow):
|
|
517
|
+
console.print(line)
|
|
518
|
+
except KeyboardInterrupt:
|
|
519
|
+
pass
|
|
520
|
+
except Exception as e:
|
|
521
|
+
console.print(f"[red]❌ Logs failed: {e}[/red]")
|
|
522
|
+
raise typer.Exit(1)
|
|
516
523
|
|
|
517
524
|
|
|
518
525
|
@app.command("chat")
|
|
@@ -596,3 +603,187 @@ def token(
|
|
|
596
603
|
|
|
597
604
|
console.print(f"[green]✅ Token refreshed[/green]")
|
|
598
605
|
console.print(f" Expires: {result.get('expires_at', 'unknown')}")
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
# ---------------------------------------------------------------------------
|
|
609
|
+
# Gateway commands (OpenClaw Gateway RPC via WebSocket)
|
|
610
|
+
# ---------------------------------------------------------------------------
|
|
611
|
+
|
|
612
|
+
def _run_async(coro):
|
|
613
|
+
"""Run an async coroutine from sync CLI."""
|
|
614
|
+
import asyncio
|
|
615
|
+
return asyncio.run(coro)
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
@app.command("config")
|
|
619
|
+
def gateway_config(
|
|
620
|
+
agent_id: str = typer.Argument(None, help="Agent ID or name"),
|
|
621
|
+
schema: bool = typer.Option(False, "--schema", help="Show config schema instead of current config"),
|
|
622
|
+
):
|
|
623
|
+
"""Get the OpenClaw gateway config for an agent."""
|
|
624
|
+
from hypercli.gateway import GatewayClient
|
|
625
|
+
|
|
626
|
+
pod = _get_pod_with_token(agent_id)
|
|
627
|
+
|
|
628
|
+
async def _run():
|
|
629
|
+
async with pod.gateway() as gw:
|
|
630
|
+
if schema:
|
|
631
|
+
result = await gw.config_schema()
|
|
632
|
+
else:
|
|
633
|
+
result = await gw.config_get()
|
|
634
|
+
console.print_json(json.dumps(result, default=str))
|
|
635
|
+
|
|
636
|
+
_run_async(_run())
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
@app.command("config-patch")
|
|
640
|
+
def gateway_config_patch(
|
|
641
|
+
agent_id: str = typer.Argument(..., help="Agent ID or name"),
|
|
642
|
+
patch: str = typer.Argument(..., help="JSON patch to apply"),
|
|
643
|
+
):
|
|
644
|
+
"""Patch the OpenClaw gateway config (merges with existing). Restarts gateway."""
|
|
645
|
+
from hypercli.gateway import GatewayClient
|
|
646
|
+
|
|
647
|
+
pod = _get_pod_with_token(agent_id)
|
|
648
|
+
patch_data = json.loads(patch)
|
|
649
|
+
|
|
650
|
+
async def _run():
|
|
651
|
+
async with pod.gateway() as gw:
|
|
652
|
+
result = await gw.config_patch(patch_data)
|
|
653
|
+
console.print("[green]✅ Config patched. Gateway restarting.[/green]")
|
|
654
|
+
|
|
655
|
+
_run_async(_run())
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
@app.command("models")
|
|
659
|
+
def gateway_models(
|
|
660
|
+
agent_id: str = typer.Argument(None, help="Agent ID or name"),
|
|
661
|
+
):
|
|
662
|
+
"""List available models on an agent's gateway."""
|
|
663
|
+
from hypercli.gateway import GatewayClient
|
|
664
|
+
|
|
665
|
+
pod = _get_pod_with_token(agent_id)
|
|
666
|
+
|
|
667
|
+
async def _run():
|
|
668
|
+
async with pod.gateway() as gw:
|
|
669
|
+
models = await gw.models_list()
|
|
670
|
+
if not models:
|
|
671
|
+
console.print("[dim]No models configured[/dim]")
|
|
672
|
+
return
|
|
673
|
+
for m in models:
|
|
674
|
+
ctx = m.get("contextWindow", "?")
|
|
675
|
+
console.print(f" {m['provider']}/{m['name']} (ctx={ctx})")
|
|
676
|
+
|
|
677
|
+
_run_async(_run())
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
@app.command("files")
|
|
681
|
+
def gateway_files(
|
|
682
|
+
agent_id: str = typer.Argument(None, help="Agent ID or name"),
|
|
683
|
+
get: str = typer.Option(None, "--get", help="Read a specific file"),
|
|
684
|
+
set_file: str = typer.Option(None, "--set", help="Write a file (name=content)"),
|
|
685
|
+
):
|
|
686
|
+
"""List or read/write workspace files on an agent via Gateway."""
|
|
687
|
+
from hypercli.gateway import GatewayClient
|
|
688
|
+
|
|
689
|
+
pod = _get_pod_with_token(agent_id)
|
|
690
|
+
|
|
691
|
+
async def _run():
|
|
692
|
+
async with pod.gateway() as gw:
|
|
693
|
+
# Get the default agent ID from the gateway
|
|
694
|
+
agents = await gw.agents_list()
|
|
695
|
+
gw_agent_id = agents[0]["id"] if agents else "main"
|
|
696
|
+
|
|
697
|
+
if get:
|
|
698
|
+
content = await gw.file_get(gw_agent_id, get)
|
|
699
|
+
console.print(content)
|
|
700
|
+
elif set_file:
|
|
701
|
+
name, _, content = set_file.partition("=")
|
|
702
|
+
if not content:
|
|
703
|
+
console.print("[red]Usage: --set 'SOUL.md=# My Agent'[/red]")
|
|
704
|
+
raise typer.Exit(1)
|
|
705
|
+
await gw.file_set(gw_agent_id, name, content)
|
|
706
|
+
console.print(f"[green]✅ Written {name}[/green]")
|
|
707
|
+
else:
|
|
708
|
+
files = await gw.files_list(gw_agent_id)
|
|
709
|
+
if not files:
|
|
710
|
+
console.print("[dim]No workspace files[/dim]")
|
|
711
|
+
return
|
|
712
|
+
for f in files:
|
|
713
|
+
icon = "📄" if not f.get("missing") else "❌"
|
|
714
|
+
size = f.get("size", 0)
|
|
715
|
+
console.print(f" {icon} {f['name']:30s} {size:>8,} bytes")
|
|
716
|
+
|
|
717
|
+
_run_async(_run())
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
@app.command("sessions")
|
|
721
|
+
def gateway_sessions(
|
|
722
|
+
agent_id: str = typer.Argument(None, help="Agent ID or name"),
|
|
723
|
+
limit: int = typer.Option(20, "--limit", "-n"),
|
|
724
|
+
):
|
|
725
|
+
"""List chat sessions on an agent's gateway."""
|
|
726
|
+
from hypercli.gateway import GatewayClient
|
|
727
|
+
|
|
728
|
+
pod = _get_pod_with_token(agent_id)
|
|
729
|
+
|
|
730
|
+
async def _run():
|
|
731
|
+
async with pod.gateway() as gw:
|
|
732
|
+
sessions = await gw.sessions_list(limit=limit)
|
|
733
|
+
if not sessions:
|
|
734
|
+
console.print("[dim]No sessions[/dim]")
|
|
735
|
+
return
|
|
736
|
+
for s in sessions:
|
|
737
|
+
console.print(f" {s.get('key','?'):20s} {s.get('status','?'):10s} {s.get('lastActivity','')}")
|
|
738
|
+
|
|
739
|
+
_run_async(_run())
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
@app.command("cron")
|
|
743
|
+
def gateway_cron(
|
|
744
|
+
agent_id: str = typer.Argument(None, help="Agent ID or name"),
|
|
745
|
+
):
|
|
746
|
+
"""List cron jobs on an agent's gateway."""
|
|
747
|
+
from hypercli.gateway import GatewayClient
|
|
748
|
+
|
|
749
|
+
pod = _get_pod_with_token(agent_id)
|
|
750
|
+
|
|
751
|
+
async def _run():
|
|
752
|
+
async with pod.gateway() as gw:
|
|
753
|
+
jobs = await gw.cron_list()
|
|
754
|
+
if not jobs:
|
|
755
|
+
console.print("[dim]No cron jobs[/dim]")
|
|
756
|
+
return
|
|
757
|
+
for j in jobs:
|
|
758
|
+
enabled = "✅" if j.get("enabled", True) else "⏸️"
|
|
759
|
+
console.print(f" {enabled} {j.get('id','?'):20s} {j.get('name','unnamed'):20s} {j.get('schedule','')}")
|
|
760
|
+
|
|
761
|
+
_run_async(_run())
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
@app.command("gateway-chat")
|
|
765
|
+
def gateway_chat(
|
|
766
|
+
agent_id: str = typer.Argument(None, help="Agent ID or name"),
|
|
767
|
+
message: str = typer.Argument(..., help="Message to send"),
|
|
768
|
+
):
|
|
769
|
+
"""Send a chat message to an agent via the Gateway and stream the response."""
|
|
770
|
+
from hypercli.gateway import GatewayClient, ChatEvent
|
|
771
|
+
|
|
772
|
+
pod = _get_pod_with_token(agent_id)
|
|
773
|
+
|
|
774
|
+
async def _run():
|
|
775
|
+
async with pod.gateway() as gw:
|
|
776
|
+
async for event in gw.chat_send(message):
|
|
777
|
+
if event.type == "content":
|
|
778
|
+
print(event.text, end="", flush=True)
|
|
779
|
+
elif event.type == "thinking":
|
|
780
|
+
console.print(f"[dim]{event.text}[/dim]", end="")
|
|
781
|
+
elif event.type == "tool_call":
|
|
782
|
+
console.print(f"\n[yellow]🔧 {event.data}[/yellow]")
|
|
783
|
+
elif event.type == "error":
|
|
784
|
+
console.print(f"\n[red]❌ {event.text}[/red]")
|
|
785
|
+
elif event.type == "done":
|
|
786
|
+
print()
|
|
787
|
+
print()
|
|
788
|
+
|
|
789
|
+
_run_async(_run())
|
|
@@ -196,6 +196,7 @@ def launch(
|
|
|
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
198
|
dockerfile: Optional[str] = typer.Option(None, "--dockerfile", "-d", help="Path to Dockerfile (built on GPU node, overrides image as base)"),
|
|
199
|
+
dry_run: bool = typer.Option(False, "--dry-run", help="Validate configuration without creating job or reserving funds"),
|
|
199
200
|
x402: bool = typer.Option(False, "--x402", help="Pay per-use via embedded x402 wallet"),
|
|
200
201
|
amount: Optional[float] = typer.Option(None, "--amount", help="USDC amount to spend with --x402"),
|
|
201
202
|
follow: bool = typer.Option(False, "--follow", "-f", help="Follow logs after creation"),
|
|
@@ -308,7 +309,8 @@ def launch(
|
|
|
308
309
|
else:
|
|
309
310
|
client = get_client()
|
|
310
311
|
|
|
311
|
-
|
|
312
|
+
spinner_msg = "Validating configuration..." if dry_run else "Launching instance..."
|
|
313
|
+
with spinner(spinner_msg):
|
|
312
314
|
job = client.jobs.create(
|
|
313
315
|
image=image,
|
|
314
316
|
command=command,
|
|
@@ -322,18 +324,37 @@ def launch(
|
|
|
322
324
|
auth=lb_auth,
|
|
323
325
|
registry_auth=registry_auth,
|
|
324
326
|
dockerfile=dockerfile_b64,
|
|
327
|
+
dry_run=dry_run,
|
|
325
328
|
)
|
|
326
329
|
|
|
327
330
|
if fmt == "json":
|
|
328
331
|
output(job, "json")
|
|
329
332
|
else:
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
333
|
+
if dry_run:
|
|
334
|
+
console.print("[bold green]✓[/] Dry run successful - configuration valid")
|
|
335
|
+
console.print(f" GPU: {job.gpu_type} x{job.gpu_count}")
|
|
336
|
+
console.print(f" Region: {job.region}")
|
|
337
|
+
console.print(f" Price: ${job.price_per_hour:.2f}/hr")
|
|
338
|
+
console.print(f" Runtime: {job.runtime}s")
|
|
339
|
+
# Display cold boot status
|
|
340
|
+
import sys
|
|
341
|
+
if job.cold_boot:
|
|
342
|
+
console.print("[yellow]⏳ Cold boot — instance provisioning may take up to 15 minutes[/]", file=sys.stderr)
|
|
343
|
+
else:
|
|
344
|
+
console.print("[green]🚀 Warm instance available — should be ready in under a minute[/]", file=sys.stderr)
|
|
345
|
+
else:
|
|
346
|
+
success(f"Instance launched: {job.job_id}")
|
|
347
|
+
console.print(f" State: {job.state}")
|
|
348
|
+
console.print(f" GPU: {job.gpu_type} x{job.gpu_count}")
|
|
349
|
+
console.print(f" Region: {job.region}")
|
|
350
|
+
console.print(f" Price: ${job.price_per_hour:.2f}/hr")
|
|
351
|
+
if job.hostname:
|
|
352
|
+
console.print(f" Hostname: {job.hostname}")
|
|
353
|
+
# Display cold boot status for real launches too
|
|
354
|
+
if job.cold_boot:
|
|
355
|
+
console.print("[yellow]⏳ Cold boot — instance provisioning may take up to 15 minutes[/]")
|
|
356
|
+
else:
|
|
357
|
+
console.print("[green]🚀 Warm instance available — should be ready in under a minute[/]")
|
|
337
358
|
|
|
338
359
|
if follow:
|
|
339
360
|
console.print()
|
|
@@ -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
|
|
@@ -296,3 +396,129 @@ def _render_metrics(m):
|
|
|
296
396
|
|
|
297
397
|
panels.append(Panel(table, title="[bold]GPU Metrics[/bold]", border_style="green"))
|
|
298
398
|
return Group(*panels)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
@app.command("exec")
|
|
402
|
+
def exec_command(
|
|
403
|
+
job_id: str = typer.Argument(..., help="Job ID (full or prefix)"),
|
|
404
|
+
command: str = typer.Argument(..., help="Command to execute"),
|
|
405
|
+
timeout: int = typer.Option(30, "--timeout", "-t", help="Timeout in seconds"),
|
|
406
|
+
):
|
|
407
|
+
"""Execute a command non-interactively on a running job container.
|
|
408
|
+
|
|
409
|
+
Runs the command and returns stdout/stderr. For interactive shells, use 'hyper jobs shell'.
|
|
410
|
+
|
|
411
|
+
Examples:
|
|
412
|
+
hyper jobs exec <job_id> "nvidia-smi"
|
|
413
|
+
hyper jobs exec <job_id> "ps aux" --timeout 10
|
|
414
|
+
"""
|
|
415
|
+
import sys
|
|
416
|
+
|
|
417
|
+
client = get_client()
|
|
418
|
+
job_id = _resolve_job_id(client, job_id)
|
|
419
|
+
|
|
420
|
+
with spinner("Executing command..."):
|
|
421
|
+
try:
|
|
422
|
+
result = client.jobs.exec(job_id, command, timeout=timeout)
|
|
423
|
+
except Exception as e:
|
|
424
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
425
|
+
raise typer.Exit(1)
|
|
426
|
+
|
|
427
|
+
# Print stdout to stdout
|
|
428
|
+
if result.stdout:
|
|
429
|
+
sys.stdout.write(result.stdout)
|
|
430
|
+
sys.stdout.flush()
|
|
431
|
+
|
|
432
|
+
# Print stderr to stderr
|
|
433
|
+
if result.stderr:
|
|
434
|
+
sys.stderr.write(result.stderr)
|
|
435
|
+
sys.stderr.flush()
|
|
436
|
+
|
|
437
|
+
# Exit with remote exit code
|
|
438
|
+
if result.exit_code != 0:
|
|
439
|
+
raise typer.Exit(result.exit_code)
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
@app.command("shell")
|
|
443
|
+
def shell(
|
|
444
|
+
job_id: str = typer.Argument(..., help="Job ID (or prefix/hostname)"),
|
|
445
|
+
shell_bin: str = typer.Option("/bin/bash", "--shell", "-s", help="Shell binary"),
|
|
446
|
+
):
|
|
447
|
+
"""Open an interactive shell on a running job container (WebSocket PTY).
|
|
448
|
+
|
|
449
|
+
Connects via the director WebSocket proxy. Press Ctrl+] to disconnect.
|
|
450
|
+
"""
|
|
451
|
+
import asyncio
|
|
452
|
+
import os
|
|
453
|
+
import sys
|
|
454
|
+
|
|
455
|
+
client = get_client()
|
|
456
|
+
job_id = _resolve_job_id(client, job_id)
|
|
457
|
+
|
|
458
|
+
console.print(f"[dim]Connecting to shell...[/dim]")
|
|
459
|
+
|
|
460
|
+
try:
|
|
461
|
+
import termios
|
|
462
|
+
import tty
|
|
463
|
+
except ImportError:
|
|
464
|
+
console.print("[red]❌ TTY libraries required (not available on Windows)[/red]")
|
|
465
|
+
raise typer.Exit(1)
|
|
466
|
+
|
|
467
|
+
async def _run_shell():
|
|
468
|
+
ws = await client.jobs.shell_connect(job_id, shell=shell_bin)
|
|
469
|
+
|
|
470
|
+
try:
|
|
471
|
+
console.print("[green]Connected.[/green] Ctrl+] to disconnect.\n")
|
|
472
|
+
|
|
473
|
+
old_settings = termios.tcgetattr(sys.stdin)
|
|
474
|
+
try:
|
|
475
|
+
tty.setraw(sys.stdin.fileno())
|
|
476
|
+
|
|
477
|
+
import shutil
|
|
478
|
+
cols, rows = shutil.get_terminal_size()
|
|
479
|
+
await ws.send(f"\x1b[8;{rows};{cols}t")
|
|
480
|
+
|
|
481
|
+
async def read_ws():
|
|
482
|
+
try:
|
|
483
|
+
async for msg in ws:
|
|
484
|
+
if isinstance(msg, str):
|
|
485
|
+
sys.stdout.write(msg)
|
|
486
|
+
sys.stdout.flush()
|
|
487
|
+
elif isinstance(msg, bytes):
|
|
488
|
+
sys.stdout.buffer.write(msg)
|
|
489
|
+
sys.stdout.buffer.flush()
|
|
490
|
+
except Exception:
|
|
491
|
+
pass
|
|
492
|
+
|
|
493
|
+
async def read_stdin():
|
|
494
|
+
loop = asyncio.get_event_loop()
|
|
495
|
+
try:
|
|
496
|
+
while True:
|
|
497
|
+
data = await loop.run_in_executor(None, lambda: os.read(sys.stdin.fileno(), 1024))
|
|
498
|
+
if not data:
|
|
499
|
+
break
|
|
500
|
+
if b"\x1d" in data: # Ctrl+]
|
|
501
|
+
break
|
|
502
|
+
await ws.send(data.decode(errors="replace"))
|
|
503
|
+
except Exception:
|
|
504
|
+
pass
|
|
505
|
+
|
|
506
|
+
done, pending = await asyncio.wait(
|
|
507
|
+
[asyncio.create_task(read_ws()), asyncio.create_task(read_stdin())],
|
|
508
|
+
return_when=asyncio.FIRST_COMPLETED,
|
|
509
|
+
)
|
|
510
|
+
for t in pending:
|
|
511
|
+
t.cancel()
|
|
512
|
+
finally:
|
|
513
|
+
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_settings)
|
|
514
|
+
console.print("\n[dim]Disconnected.[/dim]")
|
|
515
|
+
finally:
|
|
516
|
+
await ws.close()
|
|
517
|
+
|
|
518
|
+
try:
|
|
519
|
+
asyncio.run(_run_shell())
|
|
520
|
+
except KeyboardInterrupt:
|
|
521
|
+
console.print("\n[dim]Disconnected.[/dim]")
|
|
522
|
+
except Exception as e:
|
|
523
|
+
console.print(f"[red]❌ Shell failed: {e}[/red]")
|
|
524
|
+
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
|
+
version = "1.0.1"
|
|
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",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|