hypercli-cli 1.0.8__tar.gz → 2026.3.8__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-1.0.8 → hypercli_cli-2026.3.8}/PKG-INFO +11 -4
- {hypercli_cli-1.0.8 → hypercli_cli-2026.3.8}/README.md +7 -0
- hypercli_cli-2026.3.8/hypercli_cli/__init__.py +1 -0
- {hypercli_cli-1.0.8 → hypercli_cli-2026.3.8}/hypercli_cli/agents.py +93 -4
- {hypercli_cli-1.0.8 → hypercli_cli-2026.3.8}/hypercli_cli/claw.py +97 -41
- {hypercli_cli-1.0.8 → hypercli_cli-2026.3.8}/hypercli_cli/instances.py +26 -0
- {hypercli_cli-1.0.8 → hypercli_cli-2026.3.8}/hypercli_cli/jobs.py +19 -2
- {hypercli_cli-1.0.8 → hypercli_cli-2026.3.8}/hypercli_cli/voice.py +22 -13
- {hypercli_cli-1.0.8 → hypercli_cli-2026.3.8}/pyproject.toml +4 -4
- hypercli_cli-2026.3.8/tests/test_exec_shell_dryrun.py +128 -0
- hypercli_cli-2026.3.8/tests/test_jobs_list_tags.py +39 -0
- hypercli_cli-1.0.8/hypercli_cli/__init__.py +0 -1
- {hypercli_cli-1.0.8 → hypercli_cli-2026.3.8}/.gitignore +0 -0
- {hypercli_cli-1.0.8 → hypercli_cli-2026.3.8}/hypercli_cli/billing.py +0 -0
- {hypercli_cli-1.0.8 → hypercli_cli-2026.3.8}/hypercli_cli/cli.py +0 -0
- {hypercli_cli-1.0.8 → hypercli_cli-2026.3.8}/hypercli_cli/comfyui.py +0 -0
- {hypercli_cli-1.0.8 → hypercli_cli-2026.3.8}/hypercli_cli/embed.py +0 -0
- {hypercli_cli-1.0.8 → hypercli_cli-2026.3.8}/hypercli_cli/flow.py +0 -0
- {hypercli_cli-1.0.8 → hypercli_cli-2026.3.8}/hypercli_cli/keys.py +0 -0
- {hypercli_cli-1.0.8 → hypercli_cli-2026.3.8}/hypercli_cli/onboard.py +0 -0
- {hypercli_cli-1.0.8 → hypercli_cli-2026.3.8}/hypercli_cli/output.py +0 -0
- {hypercli_cli-1.0.8 → hypercli_cli-2026.3.8}/hypercli_cli/renders.py +0 -0
- {hypercli_cli-1.0.8 → hypercli_cli-2026.3.8}/hypercli_cli/stt.py +0 -0
- {hypercli_cli-1.0.8 → hypercli_cli-2026.3.8}/hypercli_cli/tui/__init__.py +0 -0
- {hypercli_cli-1.0.8 → hypercli_cli-2026.3.8}/hypercli_cli/tui/job_monitor.py +0 -0
- {hypercli_cli-1.0.8 → hypercli_cli-2026.3.8}/hypercli_cli/user.py +0 -0
- {hypercli_cli-1.0.8 → hypercli_cli-2026.3.8}/hypercli_cli/wallet.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hypercli-cli
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2026.3.8
|
|
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>=
|
|
12
|
+
Requires-Dist: hypercli-sdk>=2026.3.8
|
|
13
13
|
Requires-Dist: mutagen>=1.47.0
|
|
14
14
|
Requires-Dist: pyyaml>=6.0
|
|
15
15
|
Requires-Dist: rich>=14.2.0
|
|
@@ -19,11 +19,11 @@ 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
21
|
Requires-Dist: faster-whisper>=1.1.0; extra == 'all'
|
|
22
|
-
Requires-Dist: hypercli-sdk[comfyui]>=
|
|
22
|
+
Requires-Dist: hypercli-sdk[comfyui]>=2026.3.8; extra == 'all'
|
|
23
23
|
Requires-Dist: web3>=7.0.0; extra == 'all'
|
|
24
24
|
Requires-Dist: x402[evm,httpx]>=2.0.0; extra == 'all'
|
|
25
25
|
Provides-Extra: comfyui
|
|
26
|
-
Requires-Dist: hypercli-sdk[comfyui]>=
|
|
26
|
+
Requires-Dist: hypercli-sdk[comfyui]>=2026.3.8; extra == 'comfyui'
|
|
27
27
|
Provides-Extra: dev
|
|
28
28
|
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
29
29
|
Requires-Dist: ruff>=0.3.0; extra == 'dev'
|
|
@@ -67,6 +67,11 @@ hyper instances launch nvidia/cuda:12.6.3-base-ubuntu22.04 -g l4 -c "nvidia-smi"
|
|
|
67
67
|
hyper jobs list
|
|
68
68
|
hyper jobs logs <job_id>
|
|
69
69
|
hyper jobs metrics <job_id>
|
|
70
|
+
hyper jobs exec <job_id> "nvidia-smi"
|
|
71
|
+
hyper jobs shell <job_id>
|
|
72
|
+
|
|
73
|
+
# Dry-run launch validation
|
|
74
|
+
hyper instances launch nvidia/cuda:12.6.3-base-ubuntu22.04 -g l4 -c "nvidia-smi" --dry-run
|
|
70
75
|
|
|
71
76
|
# Flows (recommended media path)
|
|
72
77
|
hyper flow text-to-image "a cinematic portrait"
|
|
@@ -76,6 +81,8 @@ hyper flow text-to-image "a cinematic portrait" --x402
|
|
|
76
81
|
hyper claw plans
|
|
77
82
|
hyper claw subscribe 1aiu
|
|
78
83
|
hyper claw config env
|
|
84
|
+
hyper claw exec <agent_id> "ls -la"
|
|
85
|
+
hyper claw shell <agent_id>
|
|
79
86
|
```
|
|
80
87
|
|
|
81
88
|
## Notes
|
|
@@ -28,6 +28,11 @@ hyper instances launch nvidia/cuda:12.6.3-base-ubuntu22.04 -g l4 -c "nvidia-smi"
|
|
|
28
28
|
hyper jobs list
|
|
29
29
|
hyper jobs logs <job_id>
|
|
30
30
|
hyper jobs metrics <job_id>
|
|
31
|
+
hyper jobs exec <job_id> "nvidia-smi"
|
|
32
|
+
hyper jobs shell <job_id>
|
|
33
|
+
|
|
34
|
+
# Dry-run launch validation
|
|
35
|
+
hyper instances launch nvidia/cuda:12.6.3-base-ubuntu22.04 -g l4 -c "nvidia-smi" --dry-run
|
|
31
36
|
|
|
32
37
|
# Flows (recommended media path)
|
|
33
38
|
hyper flow text-to-image "a cinematic portrait"
|
|
@@ -37,6 +42,8 @@ hyper flow text-to-image "a cinematic portrait" --x402
|
|
|
37
42
|
hyper claw plans
|
|
38
43
|
hyper claw subscribe 1aiu
|
|
39
44
|
hyper claw config env
|
|
45
|
+
hyper claw exec <agent_id> "ls -la"
|
|
46
|
+
hyper claw shell <agent_id>
|
|
40
47
|
```
|
|
41
48
|
|
|
42
49
|
## Notes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2026.3.8"
|
|
@@ -107,6 +107,71 @@ def _get_pod_with_token(agent_id: str) -> ReefPod:
|
|
|
107
107
|
return pod
|
|
108
108
|
|
|
109
109
|
|
|
110
|
+
def _parse_env_vars(values: list[str] | None) -> dict | None:
|
|
111
|
+
"""Parse repeated --env KEY=VALUE options into a dict."""
|
|
112
|
+
if not values:
|
|
113
|
+
return None
|
|
114
|
+
env: dict[str, str] = {}
|
|
115
|
+
for item in values:
|
|
116
|
+
if "=" not in item:
|
|
117
|
+
raise typer.BadParameter(f"Invalid --env '{item}'. Expected KEY=VALUE.")
|
|
118
|
+
key, value = item.split("=", 1)
|
|
119
|
+
if not key:
|
|
120
|
+
raise typer.BadParameter(f"Invalid --env '{item}'. KEY cannot be empty.")
|
|
121
|
+
env[key] = value
|
|
122
|
+
return env
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _parse_ports(values: list[str] | None) -> list[dict] | None:
|
|
126
|
+
"""Parse repeated --port PORT[:noauth] options."""
|
|
127
|
+
if not values:
|
|
128
|
+
return None
|
|
129
|
+
ports: list[dict] = []
|
|
130
|
+
for item in values:
|
|
131
|
+
if ":" in item:
|
|
132
|
+
port_text, suffix = item.split(":", 1)
|
|
133
|
+
if suffix != "noauth":
|
|
134
|
+
raise typer.BadParameter(
|
|
135
|
+
f"Invalid --port '{item}'. Expected PORT or PORT:noauth."
|
|
136
|
+
)
|
|
137
|
+
auth = False
|
|
138
|
+
else:
|
|
139
|
+
port_text = item
|
|
140
|
+
auth = True
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
port_num = int(port_text)
|
|
144
|
+
except ValueError as e:
|
|
145
|
+
raise typer.BadParameter(
|
|
146
|
+
f"Invalid --port '{item}'. PORT must be an integer."
|
|
147
|
+
) from e
|
|
148
|
+
|
|
149
|
+
if port_num < 1 or port_num > 65535:
|
|
150
|
+
raise typer.BadParameter(
|
|
151
|
+
f"Invalid --port '{item}'. PORT must be between 1 and 65535."
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
ports.append({"port": port_num, "auth": auth})
|
|
155
|
+
return ports
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _port_url(pod: ReefPod, port: dict) -> str:
|
|
159
|
+
if port.get("url"):
|
|
160
|
+
return str(port["url"])
|
|
161
|
+
hostname = pod.hostname or ""
|
|
162
|
+
prefix = port.get("prefix")
|
|
163
|
+
if hostname and prefix:
|
|
164
|
+
return f"https://{prefix}-{hostname}"
|
|
165
|
+
if hostname:
|
|
166
|
+
return f"https://{hostname}"
|
|
167
|
+
return ""
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _port_summary(port: dict) -> str:
|
|
171
|
+
auth_text = "auth" if port.get("auth", True) else "noauth"
|
|
172
|
+
return f"{port.get('port', '?')} ({auth_text})"
|
|
173
|
+
|
|
174
|
+
|
|
110
175
|
@app.command("budget")
|
|
111
176
|
def budget():
|
|
112
177
|
"""Show your agent resource budget and usage."""
|
|
@@ -141,16 +206,28 @@ def create(
|
|
|
141
206
|
size: str = typer.Option(None, "--size", "-s", help="Size preset: small, medium, large"),
|
|
142
207
|
cpu: int = typer.Option(None, "--cpu", help="Custom CPU in cores"),
|
|
143
208
|
memory: int = typer.Option(None, "--memory", help="Custom memory in GB"),
|
|
209
|
+
env: list[str] = typer.Option(None, "--env", "-e", help="Environment variable (KEY=VALUE). Repeatable."),
|
|
210
|
+
port: list[str] = typer.Option(None, "--port", help="Expose port as PORT or PORT:noauth. Repeatable."),
|
|
144
211
|
no_start: bool = typer.Option(False, "--no-start", help="Create without starting"),
|
|
145
212
|
wait: bool = typer.Option(True, "--wait/--no-wait", help="Wait for pod to be running"),
|
|
146
213
|
):
|
|
147
214
|
"""Create a new OpenClaw agent pod."""
|
|
148
215
|
agents = _get_agents_client()
|
|
216
|
+
env_dict = _parse_env_vars(env)
|
|
217
|
+
ports_list = _parse_ports(port)
|
|
149
218
|
|
|
150
219
|
console.print("\n[bold]Creating agent pod...[/bold]")
|
|
151
220
|
|
|
152
221
|
try:
|
|
153
|
-
pod = agents.create(
|
|
222
|
+
pod = agents.create(
|
|
223
|
+
name=name,
|
|
224
|
+
size=size,
|
|
225
|
+
cpu=cpu,
|
|
226
|
+
memory=memory,
|
|
227
|
+
env=env_dict,
|
|
228
|
+
ports=ports_list,
|
|
229
|
+
start=not no_start,
|
|
230
|
+
)
|
|
154
231
|
except Exception as e:
|
|
155
232
|
console.print(f"[red]❌ Create failed: {e}[/red]")
|
|
156
233
|
raise typer.Exit(1)
|
|
@@ -163,6 +240,10 @@ def create(
|
|
|
163
240
|
console.print(f" State: {pod.state}")
|
|
164
241
|
console.print(f" Desktop: {pod.vnc_url}")
|
|
165
242
|
console.print(f" Shell: {pod.shell_url}")
|
|
243
|
+
display_ports = pod.ports or ports_list or []
|
|
244
|
+
for p in display_ports:
|
|
245
|
+
auth_text = "auth" if p.get("auth", True) else "noauth"
|
|
246
|
+
console.print(f" Port {p.get('port')}: {_port_url(pod, p)} ({auth_text})")
|
|
166
247
|
|
|
167
248
|
if wait:
|
|
168
249
|
console.print("\n[dim]Waiting for pod to start...[/dim]")
|
|
@@ -210,6 +291,7 @@ def list_agents(
|
|
|
210
291
|
console.print_json(json.dumps([{
|
|
211
292
|
"id": p.id, "pod_name": p.pod_name, "state": p.state,
|
|
212
293
|
"hostname": p.hostname, "vnc_url": p.vnc_url,
|
|
294
|
+
"ports": p.ports,
|
|
213
295
|
} for p in pods], indent=2, default=str))
|
|
214
296
|
return
|
|
215
297
|
|
|
@@ -224,20 +306,27 @@ def list_agents(
|
|
|
224
306
|
table.add_column("Size")
|
|
225
307
|
table.add_column("State")
|
|
226
308
|
table.add_column("Desktop URL")
|
|
309
|
+
has_ports = any(pod.ports for pod in pods)
|
|
310
|
+
if has_ports:
|
|
311
|
+
table.add_column("Ports")
|
|
227
312
|
table.add_column("Created")
|
|
228
313
|
|
|
229
314
|
for pod in pods:
|
|
230
315
|
style = {"running": "green", "pending": "yellow", "starting": "yellow"}.get(pod.state, "red")
|
|
231
316
|
created = pod.created_at.strftime("%Y-%m-%d %H:%M") if pod.created_at else ""
|
|
232
317
|
size_str = f"{pod.cpu}c/{pod.memory}G" if pod.cpu else ""
|
|
233
|
-
|
|
318
|
+
row = [
|
|
234
319
|
pod.id[:12],
|
|
235
320
|
pod.name or pod.pod_name or "",
|
|
236
321
|
size_str,
|
|
237
322
|
f"[{style}]{pod.state}[/{style}]",
|
|
238
323
|
pod.vnc_url or "",
|
|
239
|
-
|
|
240
|
-
|
|
324
|
+
]
|
|
325
|
+
if has_ports:
|
|
326
|
+
ports_text = ", ".join(_port_summary(p) for p in pod.ports) if pod.ports else ""
|
|
327
|
+
row.append(ports_text)
|
|
328
|
+
row.append(created)
|
|
329
|
+
table.add_row(*row)
|
|
241
330
|
_save_pod_state(pod)
|
|
242
331
|
|
|
243
332
|
console.print()
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""HyperClaw inference commands"""
|
|
2
2
|
import asyncio
|
|
3
3
|
import json
|
|
4
|
+
import os
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
from datetime import datetime, timedelta
|
|
6
7
|
import typer
|
|
@@ -9,7 +10,6 @@ from rich.table import Table
|
|
|
9
10
|
|
|
10
11
|
from .onboard import onboard as _onboard_fn
|
|
11
12
|
from .voice import app as voice_app
|
|
12
|
-
from .stt import app as stt_app
|
|
13
13
|
from .stt import transcribe as _stt_transcribe
|
|
14
14
|
from .embed import app as embed_app
|
|
15
15
|
|
|
@@ -19,12 +19,11 @@ console = Console()
|
|
|
19
19
|
# Register subcommands
|
|
20
20
|
app.command("onboard")(_onboard_fn)
|
|
21
21
|
app.add_typer(voice_app, name="voice")
|
|
22
|
-
app.add_typer(stt_app, name="stt")
|
|
23
22
|
app.add_typer(embed_app, name="embed")
|
|
24
23
|
|
|
25
24
|
|
|
26
25
|
@app.command("transcribe")
|
|
27
|
-
def
|
|
26
|
+
def transcribe(
|
|
28
27
|
audio_file: Path = typer.Argument(..., help="Audio file to transcribe (wav, mp3, ogg, m4a, etc.)"),
|
|
29
28
|
model: str = typer.Option("turbo", "--model", "-m", help="Whisper model: tiny, base, small, medium, large-v3, turbo"),
|
|
30
29
|
language: str = typer.Option(None, "--language", "-l", help="Language code (e.g. en, de, fr). Auto-detect if omitted."),
|
|
@@ -33,21 +32,13 @@ def transcribe_alias(
|
|
|
33
32
|
json_output: bool = typer.Option(False, "--json", help="Output as JSON with timestamps"),
|
|
34
33
|
output: Path = typer.Option(None, "--output", "-o", help="Write transcript to file"),
|
|
35
34
|
):
|
|
36
|
-
"""
|
|
37
|
-
_stt_transcribe(audio_file, model, language, device, compute_type, json_output, output)
|
|
38
|
-
|
|
35
|
+
"""Transcribe audio to text using faster-whisper (runs locally).
|
|
39
36
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
device: str = typer.Option("auto", "--device", "-d", help="Device: auto, cpu, cuda"),
|
|
46
|
-
compute_type: str = typer.Option("auto", "--compute", help="Compute type: auto, int8, float16, float32"),
|
|
47
|
-
json_output: bool = typer.Option(False, "--json", help="Output as JSON with timestamps"),
|
|
48
|
-
output: Path = typer.Option(None, "--output", "-o", help="Write transcript to file"),
|
|
49
|
-
):
|
|
50
|
-
"""Backward-compatible typo alias for transcribe."""
|
|
37
|
+
Examples:
|
|
38
|
+
hyper claw transcribe voice.ogg
|
|
39
|
+
hyper claw transcribe meeting.mp3 --model large-v3 --language en
|
|
40
|
+
hyper claw transcribe audio.wav --json -o transcript.json
|
|
41
|
+
"""
|
|
51
42
|
_stt_transcribe(audio_file, model, language, device, compute_type, json_output, output)
|
|
52
43
|
|
|
53
44
|
# Check if wallet dependencies are available
|
|
@@ -356,29 +347,47 @@ def models(
|
|
|
356
347
|
dev: bool = typer.Option(False, "--dev", help="Use dev API"),
|
|
357
348
|
json_output: bool = typer.Option(False, "--json", help="Print raw JSON response"),
|
|
358
349
|
):
|
|
359
|
-
"""List available HyperClaw models
|
|
350
|
+
"""List available HyperClaw models."""
|
|
360
351
|
import httpx
|
|
361
352
|
|
|
362
353
|
api_base = DEV_API_BASE if dev else PROD_API_BASE
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
354
|
+
key = os.getenv("HYPERCLAW_API_KEY")
|
|
355
|
+
headers = {"Authorization": f"Bearer {key}"} if key else {}
|
|
356
|
+
|
|
357
|
+
# Prefer OpenAI-compatible endpoint, then fallback to legacy.
|
|
358
|
+
urls = [f"{api_base}/v1/models", f"{api_base}/models"]
|
|
359
|
+
payload = None
|
|
360
|
+
source_url = None
|
|
361
|
+
for url in urls:
|
|
362
|
+
try:
|
|
363
|
+
response = httpx.get(url, headers=headers, timeout=15)
|
|
364
|
+
if response.status_code >= 400:
|
|
365
|
+
continue
|
|
366
|
+
payload = response.json()
|
|
367
|
+
source_url = url
|
|
368
|
+
break
|
|
369
|
+
except Exception:
|
|
370
|
+
continue
|
|
371
|
+
|
|
372
|
+
if payload is None:
|
|
373
|
+
console.print(
|
|
374
|
+
f"[red]❌ Failed to fetch models from {urls[0]} or {urls[1]} "
|
|
375
|
+
"(set HYPERCLAW_API_KEY if endpoint requires auth)[/red]"
|
|
376
|
+
)
|
|
371
377
|
raise typer.Exit(1)
|
|
372
378
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
379
|
+
if isinstance(payload, dict) and isinstance(payload.get("data"), list):
|
|
380
|
+
models_data = payload.get("data", [])
|
|
381
|
+
elif isinstance(payload, dict) and isinstance(payload.get("models"), list):
|
|
382
|
+
models_data = payload.get("models", [])
|
|
383
|
+
else:
|
|
384
|
+
console.print("[red]❌ Unexpected models response shape[/red]")
|
|
376
385
|
if json_output:
|
|
377
386
|
console.print_json(json.dumps(payload))
|
|
378
387
|
raise typer.Exit(1)
|
|
379
388
|
|
|
380
389
|
if json_output:
|
|
381
|
-
console.print_json(json.dumps(
|
|
390
|
+
console.print_json(json.dumps({"models": models_data}))
|
|
382
391
|
return
|
|
383
392
|
|
|
384
393
|
table = Table(title="HyperClaw Models")
|
|
@@ -399,7 +408,7 @@ def models(
|
|
|
399
408
|
console.print()
|
|
400
409
|
console.print(table)
|
|
401
410
|
console.print()
|
|
402
|
-
console.print(f"Source: {
|
|
411
|
+
console.print(f"Source: {source_url}")
|
|
403
412
|
|
|
404
413
|
|
|
405
414
|
@app.command("login")
|
|
@@ -556,6 +565,7 @@ def fetch_models(api_key: str, api_base: str = PROD_API_BASE) -> list[dict]:
|
|
|
556
565
|
"reasoning": MODEL_META.get(m["id"], {}).get("reasoning", False),
|
|
557
566
|
"input": ["text"],
|
|
558
567
|
"contextWindow": MODEL_META.get(m["id"], {}).get("contextWindow", 200000),
|
|
568
|
+
**({"mode": m["mode"]} if m.get("mode") else {}),
|
|
559
569
|
}
|
|
560
570
|
for m in data
|
|
561
571
|
if m.get("id")
|
|
@@ -617,17 +627,25 @@ def openclaw_setup(
|
|
|
617
627
|
|
|
618
628
|
# Patch models.providers.hyperclaw + embedding config
|
|
619
629
|
config.setdefault("models", {}).setdefault("providers", {})
|
|
630
|
+
chat_models = [m for m in models if m.get("mode") != "embedding"]
|
|
631
|
+
embedding_models = [m for m in models if m.get("mode") == "embedding"]
|
|
620
632
|
config["models"]["providers"]["hyperclaw"] = {
|
|
633
|
+
"baseUrl": "https://api.hyperclaw.app",
|
|
634
|
+
"apiKey": api_key,
|
|
635
|
+
"api": "anthropic-messages",
|
|
636
|
+
"models": chat_models,
|
|
637
|
+
}
|
|
638
|
+
config["models"]["providers"]["hyperclaw-embed"] = {
|
|
621
639
|
"baseUrl": "https://api.hyperclaw.app/v1",
|
|
622
640
|
"apiKey": api_key,
|
|
623
641
|
"api": "openai-completions",
|
|
624
|
-
"models":
|
|
642
|
+
"models": embedding_models,
|
|
625
643
|
}
|
|
626
644
|
|
|
627
645
|
# Always set embedding provider (reuses same API key)
|
|
628
646
|
config.setdefault("agents", {}).setdefault("defaults", {})
|
|
629
647
|
config["agents"]["defaults"]["memorySearch"] = {
|
|
630
|
-
"provider": "
|
|
648
|
+
"provider": "hyperclaw-embed",
|
|
631
649
|
"model": "qwen3-embedding-4b",
|
|
632
650
|
"remote": {
|
|
633
651
|
"baseUrl": "https://api.hyperclaw.app/v1/",
|
|
@@ -638,7 +656,8 @@ def openclaw_setup(
|
|
|
638
656
|
# Optionally set default model
|
|
639
657
|
if default:
|
|
640
658
|
config["agents"]["defaults"].setdefault("model", {})
|
|
641
|
-
|
|
659
|
+
if chat_models:
|
|
660
|
+
config["agents"]["defaults"]["model"]["primary"] = f"hyperclaw/{chat_models[0]['id']}"
|
|
642
661
|
|
|
643
662
|
# Write back
|
|
644
663
|
OPENCLAW_CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -648,10 +667,14 @@ def openclaw_setup(
|
|
|
648
667
|
|
|
649
668
|
console.print(f"[green]✅ Patched {OPENCLAW_CONFIG_PATH}[/green]")
|
|
650
669
|
console.print(f" provider: hyperclaw key: {api_key[:16]}...")
|
|
651
|
-
|
|
670
|
+
if embedding_models:
|
|
671
|
+
console.print(" embedding provider: hyperclaw-embed")
|
|
672
|
+
for m in chat_models:
|
|
652
673
|
console.print(f" model: hyperclaw/{m['id']}")
|
|
653
|
-
|
|
654
|
-
console.print(f"
|
|
674
|
+
for m in embedding_models:
|
|
675
|
+
console.print(f" model: hyperclaw-embed/{m['id']}")
|
|
676
|
+
if default and chat_models:
|
|
677
|
+
console.print(f" default model: hyperclaw/{chat_models[0]['id']}")
|
|
655
678
|
console.print("\nRun: [bold]openclaw gateway restart[/bold]")
|
|
656
679
|
|
|
657
680
|
|
|
@@ -676,25 +699,36 @@ def _resolve_api_key(key: str | None) -> str:
|
|
|
676
699
|
|
|
677
700
|
def _config_openclaw(api_key: str, models: list[dict], api_base: str = PROD_API_BASE) -> dict:
|
|
678
701
|
"""OpenClaw openclaw.json provider snippet (LLM + embeddings)."""
|
|
702
|
+
chat_models = [m for m in models if m.get("mode") != "embedding"]
|
|
703
|
+
embedding_models = [m for m in models if m.get("mode") == "embedding"]
|
|
679
704
|
return {
|
|
680
705
|
"models": {
|
|
681
706
|
"mode": "merge",
|
|
682
707
|
"providers": {
|
|
683
708
|
"hyperclaw": {
|
|
709
|
+
# OpenClaw/pi-ai appends /v1/messages for anthropic-messages.
|
|
710
|
+
"baseUrl": api_base,
|
|
711
|
+
"apiKey": api_key,
|
|
712
|
+
"api": "anthropic-messages",
|
|
713
|
+
"models": chat_models,
|
|
714
|
+
},
|
|
715
|
+
"hyperclaw-embed": {
|
|
716
|
+
# Embeddings go through the OpenAI-compatible /v1 endpoints.
|
|
684
717
|
"baseUrl": f"{api_base}/v1",
|
|
685
718
|
"apiKey": api_key,
|
|
686
719
|
"api": "openai-completions",
|
|
687
|
-
"models":
|
|
688
|
-
}
|
|
720
|
+
"models": embedding_models,
|
|
721
|
+
},
|
|
689
722
|
}
|
|
690
723
|
},
|
|
691
724
|
"agents": {
|
|
692
725
|
"defaults": {
|
|
693
726
|
"models": {
|
|
694
|
-
**{f"hyperclaw/{m['id']}": {"alias": m['id'].split('-')[0]} for m in
|
|
727
|
+
**{f"hyperclaw/{m['id']}": {"alias": m['id'].split('-')[0]} for m in chat_models},
|
|
728
|
+
**{f"hyperclaw-embed/{m['id']}": {"alias": m['id'].split('-')[0]} for m in embedding_models},
|
|
695
729
|
},
|
|
696
730
|
"memorySearch": {
|
|
697
|
-
"provider": "
|
|
731
|
+
"provider": "hyperclaw-embed",
|
|
698
732
|
"model": "qwen3-embedding-4b",
|
|
699
733
|
"remote": {
|
|
700
734
|
"baseUrl": f"{api_base}/v1/",
|
|
@@ -737,6 +771,28 @@ def _config_env(api_key: str, models: list[dict]) -> str:
|
|
|
737
771
|
return "\n".join(lines)
|
|
738
772
|
|
|
739
773
|
|
|
774
|
+
@app.command("exec")
|
|
775
|
+
def exec_cmd(
|
|
776
|
+
agent_id: str = typer.Argument(..., help="Agent ID (or prefix)"),
|
|
777
|
+
command: str = typer.Argument(..., help="Command to execute"),
|
|
778
|
+
timeout: int = typer.Option(30, "--timeout", "-t", help="Command timeout (seconds)"),
|
|
779
|
+
):
|
|
780
|
+
"""Execute a command on a HyperClaw agent container."""
|
|
781
|
+
from . import agents
|
|
782
|
+
|
|
783
|
+
agents.exec_cmd(agent_id=agent_id, command=command, timeout=timeout)
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
@app.command("shell")
|
|
787
|
+
def shell_cmd(
|
|
788
|
+
agent_id: str = typer.Argument(..., help="Agent ID (or prefix)"),
|
|
789
|
+
):
|
|
790
|
+
"""Open an interactive shell on a HyperClaw agent container."""
|
|
791
|
+
from . import agents
|
|
792
|
+
|
|
793
|
+
agents.shell(agent_id=agent_id)
|
|
794
|
+
|
|
795
|
+
|
|
740
796
|
FORMAT_CHOICES = ["openclaw", "opencode", "env"]
|
|
741
797
|
|
|
742
798
|
|
|
@@ -231,6 +231,12 @@ def launch(
|
|
|
231
231
|
if lb:
|
|
232
232
|
ports_dict["lb"] = lb
|
|
233
233
|
|
|
234
|
+
raw_tcp_ports = []
|
|
235
|
+
if ports_dict:
|
|
236
|
+
raw_tcp_ports = sorted(
|
|
237
|
+
int(p.split("/")[0]) for p in ports_dict.keys() if p.endswith("/tcp")
|
|
238
|
+
)
|
|
239
|
+
|
|
234
240
|
# Build registry auth if provided
|
|
235
241
|
registry_auth = None
|
|
236
242
|
if registry_user and registry_password:
|
|
@@ -302,6 +308,13 @@ def launch(
|
|
|
302
308
|
console.print(f" Price: ${job.price_per_hour:.2f}/hr")
|
|
303
309
|
if job.hostname:
|
|
304
310
|
console.print(f" Hostname: {job.hostname}")
|
|
311
|
+
if lb:
|
|
312
|
+
console.print(f" HTTPS LB: Traefik TLS on https://{job.hostname}")
|
|
313
|
+
console.print(f" LB Target: container port {lb}")
|
|
314
|
+
if lb_auth:
|
|
315
|
+
console.print(" LB Auth: enabled (Bearer token required)")
|
|
316
|
+
if raw_tcp_ports:
|
|
317
|
+
console.print(f" TCP Ports: raw TCP exposed: {', '.join(map(str, raw_tcp_ports))}")
|
|
305
318
|
console.print(f" Access Key: {x402_job.access_key}")
|
|
306
319
|
console.print(f" Status URL: {x402_job.status_url}")
|
|
307
320
|
console.print(f" Logs URL: {x402_job.logs_url}")
|
|
@@ -336,6 +349,12 @@ def launch(
|
|
|
336
349
|
console.print(f" Region: {job.region}")
|
|
337
350
|
console.print(f" Price: ${job.price_per_hour:.2f}/hr")
|
|
338
351
|
console.print(f" Runtime: {job.runtime}s")
|
|
352
|
+
if lb:
|
|
353
|
+
console.print(f" HTTPS LB: Traefik TLS will be provisioned for container port {lb}")
|
|
354
|
+
if lb_auth:
|
|
355
|
+
console.print(" LB Auth: enabled (Bearer token required)")
|
|
356
|
+
if raw_tcp_ports:
|
|
357
|
+
console.print(f" TCP: raw TCP ports: {', '.join(map(str, raw_tcp_ports))}")
|
|
339
358
|
# Display cold boot status
|
|
340
359
|
import sys
|
|
341
360
|
if job.cold_boot:
|
|
@@ -350,6 +369,13 @@ def launch(
|
|
|
350
369
|
console.print(f" Price: ${job.price_per_hour:.2f}/hr")
|
|
351
370
|
if job.hostname:
|
|
352
371
|
console.print(f" Hostname: {job.hostname}")
|
|
372
|
+
if lb and job.hostname:
|
|
373
|
+
console.print(f" HTTPS LB: Traefik TLS endpoint https://{job.hostname}")
|
|
374
|
+
console.print(f" LB Target: container port {lb}")
|
|
375
|
+
if lb_auth:
|
|
376
|
+
console.print(" LB Auth: enabled (Bearer token required)")
|
|
377
|
+
if raw_tcp_ports:
|
|
378
|
+
console.print(f" TCP: raw TCP ports: {', '.join(map(str, raw_tcp_ports))}")
|
|
353
379
|
# Display cold boot status for real launches too
|
|
354
380
|
if job.cold_boot:
|
|
355
381
|
console.print("[yellow]⏳ Cold boot — instance provisioning may take up to 15 minutes[/]")
|
|
@@ -11,6 +11,17 @@ def get_client() -> HyperCLI:
|
|
|
11
11
|
return HyperCLI()
|
|
12
12
|
|
|
13
13
|
|
|
14
|
+
def _parse_tags(tag_args: list[str]) -> dict[str, str]:
|
|
15
|
+
tags: dict[str, str] = {}
|
|
16
|
+
for tag in tag_args:
|
|
17
|
+
key, sep, value = tag.partition("=")
|
|
18
|
+
if not sep or not key or not value:
|
|
19
|
+
console.print(f"[red]Error:[/red] Invalid tag '{tag}'. Expected KEY=VALUE.")
|
|
20
|
+
raise typer.Exit(1)
|
|
21
|
+
tags[key] = value
|
|
22
|
+
return tags
|
|
23
|
+
|
|
24
|
+
|
|
14
25
|
def _resolve_job_id(client: HyperCLI, job_id: str) -> str:
|
|
15
26
|
"""Resolve a partial job ID prefix to a full UUID."""
|
|
16
27
|
if len(job_id) == 36:
|
|
@@ -32,12 +43,14 @@ def _resolve_job_id(client: HyperCLI, job_id: str) -> str:
|
|
|
32
43
|
@app.command("list")
|
|
33
44
|
def list_jobs(
|
|
34
45
|
state: Optional[str] = typer.Option(None, "--state", "-s", help="Filter by state"),
|
|
46
|
+
tag: list[str] = typer.Option([], "--tag", help="Filter by tag as KEY=VALUE", metavar="KEY=VALUE"),
|
|
35
47
|
fmt: str = typer.Option("table", "--output", "-o", help="Output format: table|json"),
|
|
36
48
|
):
|
|
37
49
|
"""List all jobs"""
|
|
38
50
|
client = get_client()
|
|
51
|
+
tags = _parse_tags(tag) if tag else None
|
|
39
52
|
with spinner("Fetching jobs..."):
|
|
40
|
-
jobs = client.jobs.list(state=state)
|
|
53
|
+
jobs = client.jobs.list(state=state, tags=tags)
|
|
41
54
|
|
|
42
55
|
if fmt == "json":
|
|
43
56
|
output(jobs, "json")
|
|
@@ -45,7 +58,11 @@ def list_jobs(
|
|
|
45
58
|
if not jobs:
|
|
46
59
|
console.print("[dim]No jobs found[/dim]")
|
|
47
60
|
return
|
|
48
|
-
output(
|
|
61
|
+
output(
|
|
62
|
+
jobs,
|
|
63
|
+
"table",
|
|
64
|
+
["job_id", "state", "gpu_type", "gpu_count", "region", "time_left", "runtime", "hostname"],
|
|
65
|
+
)
|
|
49
66
|
|
|
50
67
|
|
|
51
68
|
@app.command("get")
|
|
@@ -14,8 +14,7 @@ console = Console()
|
|
|
14
14
|
|
|
15
15
|
HYPERCLI_DIR = Path.home() / ".hypercli"
|
|
16
16
|
CLAW_KEY_PATH = HYPERCLI_DIR / "claw-key.json"
|
|
17
|
-
|
|
18
|
-
DEV_API_BASE = "https://dev-api.hyperclaw.app"
|
|
17
|
+
DEFAULT_API_BASE = "https://api.hyperclaw.app"
|
|
19
18
|
|
|
20
19
|
|
|
21
20
|
def _get_api_key(key: str | None) -> str:
|
|
@@ -35,15 +34,25 @@ def _get_api_key(key: str | None) -> str:
|
|
|
35
34
|
raise typer.Exit(1)
|
|
36
35
|
|
|
37
36
|
|
|
37
|
+
def _resolve_api_base(base_url: str | None) -> str:
|
|
38
|
+
"""Resolve API base: --base-url > HYPERCLAW_API_BASE env > default."""
|
|
39
|
+
if base_url:
|
|
40
|
+
return base_url.rstrip("/")
|
|
41
|
+
env_base = os.environ.get("HYPERCLAW_API_BASE", "").strip()
|
|
42
|
+
if env_base:
|
|
43
|
+
return env_base.rstrip("/")
|
|
44
|
+
return DEFAULT_API_BASE
|
|
45
|
+
|
|
46
|
+
|
|
38
47
|
def _post_voice(
|
|
39
48
|
endpoint: str,
|
|
40
49
|
payload: dict,
|
|
41
50
|
api_key: str,
|
|
42
51
|
output: Path,
|
|
43
|
-
|
|
52
|
+
base_url: str | None = None,
|
|
44
53
|
):
|
|
45
54
|
"""POST to voice endpoint and save audio output."""
|
|
46
|
-
api_base =
|
|
55
|
+
api_base = _resolve_api_base(base_url)
|
|
47
56
|
url = f"{api_base}/voice/{endpoint}"
|
|
48
57
|
|
|
49
58
|
console.print(f"[dim]→ POST {url}[/dim]")
|
|
@@ -83,10 +92,10 @@ def tts(
|
|
|
83
92
|
format: str = typer.Option("mp3", "--format", "-f", help="Output format: wav, mp3, opus, ogg, flac"),
|
|
84
93
|
output: Path = typer.Option(None, "--output", "-o", help="Output audio file (default: output.<format>)"),
|
|
85
94
|
key: str = typer.Option(None, "--key", "-k", help="API key (sk-...)"),
|
|
86
|
-
|
|
95
|
+
base_url: str = typer.Option(None, "--base-url", "-b", help="API base URL (default: api.hyperclaw.app)"),
|
|
87
96
|
):
|
|
88
97
|
"""Generate speech from text using a preset voice.
|
|
89
|
-
|
|
98
|
+
|
|
90
99
|
Examples:
|
|
91
100
|
hyper claw voice tts "Hello world"
|
|
92
101
|
hyper claw voice tts "Bonjour" -v Etienne -l french -f opus -o hello.opus
|
|
@@ -100,7 +109,7 @@ def tts(
|
|
|
100
109
|
"language": language,
|
|
101
110
|
"response_format": format,
|
|
102
111
|
}
|
|
103
|
-
_post_voice("tts", payload, api_key, output,
|
|
112
|
+
_post_voice("tts", payload, api_key, output, base_url)
|
|
104
113
|
|
|
105
114
|
|
|
106
115
|
@app.command("clone")
|
|
@@ -112,10 +121,10 @@ def clone(
|
|
|
112
121
|
format: str = typer.Option("mp3", "--format", "-f", help="Output format: wav, mp3, opus, ogg, flac"),
|
|
113
122
|
output: Path = typer.Option(None, "--output", "-o", help="Output audio file (default: output.<format>)"),
|
|
114
123
|
key: str = typer.Option(None, "--key", "-k", help="API key (sk-...)"),
|
|
115
|
-
|
|
124
|
+
base_url: str = typer.Option(None, "--base-url", "-b", help="API base URL (default: api.hyperclaw.app)"),
|
|
116
125
|
):
|
|
117
126
|
"""Clone a voice from reference audio.
|
|
118
|
-
|
|
127
|
+
|
|
119
128
|
Examples:
|
|
120
129
|
hyper claw voice clone "Hello" --ref voice.wav
|
|
121
130
|
hyper claw voice clone "Test" -r ref.wav -l english -f mp3 -o cloned.mp3
|
|
@@ -140,7 +149,7 @@ def clone(
|
|
|
140
149
|
"x_vector_only": x_vector_only,
|
|
141
150
|
"response_format": format,
|
|
142
151
|
}
|
|
143
|
-
_post_voice("clone", payload, api_key, output,
|
|
152
|
+
_post_voice("clone", payload, api_key, output, base_url)
|
|
144
153
|
|
|
145
154
|
|
|
146
155
|
@app.command("design")
|
|
@@ -151,10 +160,10 @@ def design(
|
|
|
151
160
|
format: str = typer.Option("mp3", "--format", "-f", help="Output format: wav, mp3, opus, ogg, flac"),
|
|
152
161
|
output: Path = typer.Option(None, "--output", "-o", help="Output audio file (default: output.<format>)"),
|
|
153
162
|
key: str = typer.Option(None, "--key", "-k", help="API key (sk-...)"),
|
|
154
|
-
|
|
163
|
+
base_url: str = typer.Option(None, "--base-url", "-b", help="API base URL (default: api.hyperclaw.app)"),
|
|
155
164
|
):
|
|
156
165
|
"""Design a voice from a text description.
|
|
157
|
-
|
|
166
|
+
|
|
158
167
|
Examples:
|
|
159
168
|
hyper claw voice design "Hello" --desc "deep male voice, British accent"
|
|
160
169
|
hyper claw voice design "Test" -d "young woman, cheerful" -f mp3 -o designed.mp3
|
|
@@ -168,4 +177,4 @@ def design(
|
|
|
168
177
|
"language": language,
|
|
169
178
|
"response_format": format,
|
|
170
179
|
}
|
|
171
|
-
_post_voice("design", payload, api_key, output,
|
|
180
|
+
_post_voice("design", payload, api_key, output, base_url)
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "hypercli-cli"
|
|
7
|
-
version = "
|
|
7
|
+
version = "2026.3.8"
|
|
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>=
|
|
16
|
+
"hypercli-sdk>=2026.3.8",
|
|
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]>=
|
|
27
|
+
"hypercli-sdk[comfyui]>=2026.3.8",
|
|
28
28
|
]
|
|
29
29
|
wallet = [
|
|
30
30
|
"x402[httpx,evm]>=2.0.0",
|
|
@@ -37,7 +37,7 @@ stt = [
|
|
|
37
37
|
"faster-whisper>=1.1.0",
|
|
38
38
|
]
|
|
39
39
|
all = [
|
|
40
|
-
"hypercli-sdk[comfyui]>=
|
|
40
|
+
"hypercli-sdk[comfyui]>=2026.3.8",
|
|
41
41
|
"x402[httpx,evm]>=2.0.0",
|
|
42
42
|
"eth-account>=0.13.0",
|
|
43
43
|
"web3>=7.0.0",
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
from types import SimpleNamespace
|
|
2
|
+
|
|
3
|
+
from typer.testing import CliRunner
|
|
4
|
+
|
|
5
|
+
from hypercli_cli.cli import app
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
runner = CliRunner()
|
|
9
|
+
FULL_JOB_ID = "123e4567-e89b-12d3-a456-426614174000"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_jobs_exec_mock(monkeypatch):
|
|
13
|
+
class FakeJobs:
|
|
14
|
+
def exec(self, job_id, command, timeout=30):
|
|
15
|
+
assert job_id == FULL_JOB_ID
|
|
16
|
+
assert command == "echo hi"
|
|
17
|
+
assert timeout == 9
|
|
18
|
+
return SimpleNamespace(stdout="hi\n", stderr="", exit_code=0)
|
|
19
|
+
|
|
20
|
+
fake_client = SimpleNamespace(jobs=FakeJobs())
|
|
21
|
+
|
|
22
|
+
monkeypatch.setattr("hypercli_cli.jobs.get_client", lambda: fake_client)
|
|
23
|
+
monkeypatch.setattr("hypercli_cli.jobs._resolve_job_id", lambda client, job_id: job_id)
|
|
24
|
+
|
|
25
|
+
result = runner.invoke(app, ["jobs", "exec", FULL_JOB_ID, "echo hi", "--timeout", "9"])
|
|
26
|
+
|
|
27
|
+
assert result.exit_code == 0
|
|
28
|
+
assert "hi" in result.stdout
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_jobs_get_shows_command_and_env(monkeypatch):
|
|
32
|
+
class FakeJobs:
|
|
33
|
+
def list(self):
|
|
34
|
+
return [SimpleNamespace(job_id=FULL_JOB_ID)]
|
|
35
|
+
|
|
36
|
+
def get(self, job_id):
|
|
37
|
+
assert job_id == FULL_JOB_ID
|
|
38
|
+
return SimpleNamespace(
|
|
39
|
+
job_id=job_id,
|
|
40
|
+
state="terminated",
|
|
41
|
+
gpu_type="H200",
|
|
42
|
+
gpu_count=8,
|
|
43
|
+
region="oh",
|
|
44
|
+
docker_image="vllm/vllm-openai:glm5",
|
|
45
|
+
command="vllm serve zai-org/GLM-5-FP8 --host 0.0.0.0 --port 8000",
|
|
46
|
+
env_vars={"LD_LIBRARY_PATH": "/usr/local/nvidia/lib64:/usr/local/nvidia/lib:/usr/lib/x86_64-linux-gnu"},
|
|
47
|
+
runtime=3600,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
fake_client = SimpleNamespace(jobs=FakeJobs())
|
|
51
|
+
|
|
52
|
+
monkeypatch.setattr("hypercli_cli.jobs.get_client", lambda: fake_client)
|
|
53
|
+
|
|
54
|
+
result = runner.invoke(app, ["jobs", "get", FULL_JOB_ID])
|
|
55
|
+
|
|
56
|
+
assert result.exit_code == 0
|
|
57
|
+
assert "vllm serve zai-org/GLM-5-FP8" in result.stdout
|
|
58
|
+
assert "LD_LIBRARY_PATH" in result.stdout
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_instances_launch_dry_run_mock(monkeypatch):
|
|
62
|
+
captured = {}
|
|
63
|
+
|
|
64
|
+
class FakeJobs:
|
|
65
|
+
def create(self, **kwargs):
|
|
66
|
+
captured.update(kwargs)
|
|
67
|
+
return SimpleNamespace(
|
|
68
|
+
job_id="job-dryrun",
|
|
69
|
+
state="validated",
|
|
70
|
+
gpu_type="l40s",
|
|
71
|
+
gpu_count=1,
|
|
72
|
+
region="oh",
|
|
73
|
+
price_per_hour=1.23,
|
|
74
|
+
runtime=300,
|
|
75
|
+
cold_boot=False,
|
|
76
|
+
hostname=None,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
fake_client = SimpleNamespace(jobs=FakeJobs())
|
|
80
|
+
monkeypatch.setattr("hypercli_cli.instances.get_client", lambda: fake_client)
|
|
81
|
+
|
|
82
|
+
result = runner.invoke(
|
|
83
|
+
app,
|
|
84
|
+
[
|
|
85
|
+
"instances",
|
|
86
|
+
"launch",
|
|
87
|
+
"nvidia/cuda:12.0-base-ubuntu22.04",
|
|
88
|
+
"--dry-run",
|
|
89
|
+
"--command",
|
|
90
|
+
"echo hi",
|
|
91
|
+
"--output",
|
|
92
|
+
"json",
|
|
93
|
+
],
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
assert result.exit_code == 0
|
|
97
|
+
assert captured["dry_run"] is True
|
|
98
|
+
assert "job-dryrun" in result.stdout
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_claw_exec_alias(monkeypatch):
|
|
102
|
+
called = {}
|
|
103
|
+
|
|
104
|
+
def fake_exec_cmd(agent_id, command, timeout=30):
|
|
105
|
+
called["agent_id"] = agent_id
|
|
106
|
+
called["command"] = command
|
|
107
|
+
called["timeout"] = timeout
|
|
108
|
+
|
|
109
|
+
monkeypatch.setattr("hypercli_cli.agents.exec_cmd", fake_exec_cmd)
|
|
110
|
+
|
|
111
|
+
result = runner.invoke(app, ["claw", "exec", "agent-1", "echo ok", "--timeout", "7"])
|
|
112
|
+
|
|
113
|
+
assert result.exit_code == 0
|
|
114
|
+
assert called == {"agent_id": "agent-1", "command": "echo ok", "timeout": 7}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_claw_shell_alias(monkeypatch):
|
|
118
|
+
called = {}
|
|
119
|
+
|
|
120
|
+
def fake_shell(agent_id):
|
|
121
|
+
called["agent_id"] = agent_id
|
|
122
|
+
|
|
123
|
+
monkeypatch.setattr("hypercli_cli.agents.shell", fake_shell)
|
|
124
|
+
|
|
125
|
+
result = runner.invoke(app, ["claw", "shell", "agent-xyz"])
|
|
126
|
+
|
|
127
|
+
assert result.exit_code == 0
|
|
128
|
+
assert called["agent_id"] == "agent-xyz"
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from types import SimpleNamespace
|
|
2
|
+
|
|
3
|
+
from typer.testing import CliRunner
|
|
4
|
+
|
|
5
|
+
from hypercli_cli.cli import app
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
runner = CliRunner()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_jobs_list_passes_tags(monkeypatch):
|
|
12
|
+
captured = {}
|
|
13
|
+
|
|
14
|
+
class FakeJobs:
|
|
15
|
+
def list(self, state=None, tags=None):
|
|
16
|
+
captured["state"] = state
|
|
17
|
+
captured["tags"] = tags
|
|
18
|
+
return []
|
|
19
|
+
|
|
20
|
+
fake_client = SimpleNamespace(jobs=FakeJobs())
|
|
21
|
+
monkeypatch.setattr("hypercli_cli.jobs.get_client", lambda: fake_client)
|
|
22
|
+
|
|
23
|
+
result = runner.invoke(
|
|
24
|
+
app,
|
|
25
|
+
["jobs", "list", "--state", "running", "--tag", "team=ml", "--tag", "env=prod"],
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
assert result.exit_code == 0
|
|
29
|
+
assert captured == {"state": "running", "tags": {"team": "ml", "env": "prod"}}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_jobs_list_rejects_invalid_tag(monkeypatch):
|
|
33
|
+
fake_client = SimpleNamespace(jobs=SimpleNamespace(list=lambda state=None, tags=None: []))
|
|
34
|
+
monkeypatch.setattr("hypercli_cli.jobs.get_client", lambda: fake_client)
|
|
35
|
+
|
|
36
|
+
result = runner.invoke(app, ["jobs", "list", "--tag", "broken"])
|
|
37
|
+
|
|
38
|
+
assert result.exit_code == 1
|
|
39
|
+
assert "Expected KEY=VALUE" in result.stdout
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "1.0.8"
|
|
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
|