hypercli-cli 1.0.9__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.9 → hypercli_cli-2026.3.8}/PKG-INFO +11 -4
- {hypercli_cli-1.0.9 → hypercli_cli-2026.3.8}/README.md +7 -0
- hypercli_cli-2026.3.8/hypercli_cli/__init__.py +1 -0
- {hypercli_cli-1.0.9 → hypercli_cli-2026.3.8}/hypercli_cli/agents.py +93 -4
- {hypercli_cli-1.0.9 → hypercli_cli-2026.3.8}/hypercli_cli/claw.py +90 -24
- {hypercli_cli-1.0.9 → hypercli_cli-2026.3.8}/hypercli_cli/jobs.py +19 -2
- {hypercli_cli-1.0.9 → 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.9/hypercli_cli/__init__.py +0 -1
- {hypercli_cli-1.0.9 → hypercli_cli-2026.3.8}/.gitignore +0 -0
- {hypercli_cli-1.0.9 → hypercli_cli-2026.3.8}/hypercli_cli/billing.py +0 -0
- {hypercli_cli-1.0.9 → hypercli_cli-2026.3.8}/hypercli_cli/cli.py +0 -0
- {hypercli_cli-1.0.9 → hypercli_cli-2026.3.8}/hypercli_cli/comfyui.py +0 -0
- {hypercli_cli-1.0.9 → hypercli_cli-2026.3.8}/hypercli_cli/embed.py +0 -0
- {hypercli_cli-1.0.9 → hypercli_cli-2026.3.8}/hypercli_cli/flow.py +0 -0
- {hypercli_cli-1.0.9 → hypercli_cli-2026.3.8}/hypercli_cli/instances.py +0 -0
- {hypercli_cli-1.0.9 → hypercli_cli-2026.3.8}/hypercli_cli/keys.py +0 -0
- {hypercli_cli-1.0.9 → hypercli_cli-2026.3.8}/hypercli_cli/onboard.py +0 -0
- {hypercli_cli-1.0.9 → hypercli_cli-2026.3.8}/hypercli_cli/output.py +0 -0
- {hypercli_cli-1.0.9 → hypercli_cli-2026.3.8}/hypercli_cli/renders.py +0 -0
- {hypercli_cli-1.0.9 → hypercli_cli-2026.3.8}/hypercli_cli/stt.py +0 -0
- {hypercli_cli-1.0.9 → hypercli_cli-2026.3.8}/hypercli_cli/tui/__init__.py +0 -0
- {hypercli_cli-1.0.9 → hypercli_cli-2026.3.8}/hypercli_cli/tui/job_monitor.py +0 -0
- {hypercli_cli-1.0.9 → hypercli_cli-2026.3.8}/hypercli_cli/user.py +0 -0
- {hypercli_cli-1.0.9 → hypercli_cli-2026.3.8}/hypercli_cli/voice.py +0 -0
- {hypercli_cli-1.0.9 → 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
|
|
@@ -346,29 +347,47 @@ def models(
|
|
|
346
347
|
dev: bool = typer.Option(False, "--dev", help="Use dev API"),
|
|
347
348
|
json_output: bool = typer.Option(False, "--json", help="Print raw JSON response"),
|
|
348
349
|
):
|
|
349
|
-
"""List available HyperClaw models
|
|
350
|
+
"""List available HyperClaw models."""
|
|
350
351
|
import httpx
|
|
351
352
|
|
|
352
353
|
api_base = DEV_API_BASE if dev else PROD_API_BASE
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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
|
+
)
|
|
361
377
|
raise typer.Exit(1)
|
|
362
378
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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]")
|
|
366
385
|
if json_output:
|
|
367
386
|
console.print_json(json.dumps(payload))
|
|
368
387
|
raise typer.Exit(1)
|
|
369
388
|
|
|
370
389
|
if json_output:
|
|
371
|
-
console.print_json(json.dumps(
|
|
390
|
+
console.print_json(json.dumps({"models": models_data}))
|
|
372
391
|
return
|
|
373
392
|
|
|
374
393
|
table = Table(title="HyperClaw Models")
|
|
@@ -389,7 +408,7 @@ def models(
|
|
|
389
408
|
console.print()
|
|
390
409
|
console.print(table)
|
|
391
410
|
console.print()
|
|
392
|
-
console.print(f"Source: {
|
|
411
|
+
console.print(f"Source: {source_url}")
|
|
393
412
|
|
|
394
413
|
|
|
395
414
|
@app.command("login")
|
|
@@ -546,6 +565,7 @@ def fetch_models(api_key: str, api_base: str = PROD_API_BASE) -> list[dict]:
|
|
|
546
565
|
"reasoning": MODEL_META.get(m["id"], {}).get("reasoning", False),
|
|
547
566
|
"input": ["text"],
|
|
548
567
|
"contextWindow": MODEL_META.get(m["id"], {}).get("contextWindow", 200000),
|
|
568
|
+
**({"mode": m["mode"]} if m.get("mode") else {}),
|
|
549
569
|
}
|
|
550
570
|
for m in data
|
|
551
571
|
if m.get("id")
|
|
@@ -607,17 +627,25 @@ def openclaw_setup(
|
|
|
607
627
|
|
|
608
628
|
# Patch models.providers.hyperclaw + embedding config
|
|
609
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"]
|
|
610
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"] = {
|
|
611
639
|
"baseUrl": "https://api.hyperclaw.app/v1",
|
|
612
640
|
"apiKey": api_key,
|
|
613
641
|
"api": "openai-completions",
|
|
614
|
-
"models":
|
|
642
|
+
"models": embedding_models,
|
|
615
643
|
}
|
|
616
644
|
|
|
617
645
|
# Always set embedding provider (reuses same API key)
|
|
618
646
|
config.setdefault("agents", {}).setdefault("defaults", {})
|
|
619
647
|
config["agents"]["defaults"]["memorySearch"] = {
|
|
620
|
-
"provider": "
|
|
648
|
+
"provider": "hyperclaw-embed",
|
|
621
649
|
"model": "qwen3-embedding-4b",
|
|
622
650
|
"remote": {
|
|
623
651
|
"baseUrl": "https://api.hyperclaw.app/v1/",
|
|
@@ -628,7 +656,8 @@ def openclaw_setup(
|
|
|
628
656
|
# Optionally set default model
|
|
629
657
|
if default:
|
|
630
658
|
config["agents"]["defaults"].setdefault("model", {})
|
|
631
|
-
|
|
659
|
+
if chat_models:
|
|
660
|
+
config["agents"]["defaults"]["model"]["primary"] = f"hyperclaw/{chat_models[0]['id']}"
|
|
632
661
|
|
|
633
662
|
# Write back
|
|
634
663
|
OPENCLAW_CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -638,10 +667,14 @@ def openclaw_setup(
|
|
|
638
667
|
|
|
639
668
|
console.print(f"[green]✅ Patched {OPENCLAW_CONFIG_PATH}[/green]")
|
|
640
669
|
console.print(f" provider: hyperclaw key: {api_key[:16]}...")
|
|
641
|
-
|
|
670
|
+
if embedding_models:
|
|
671
|
+
console.print(" embedding provider: hyperclaw-embed")
|
|
672
|
+
for m in chat_models:
|
|
642
673
|
console.print(f" model: hyperclaw/{m['id']}")
|
|
643
|
-
|
|
644
|
-
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']}")
|
|
645
678
|
console.print("\nRun: [bold]openclaw gateway restart[/bold]")
|
|
646
679
|
|
|
647
680
|
|
|
@@ -666,25 +699,36 @@ def _resolve_api_key(key: str | None) -> str:
|
|
|
666
699
|
|
|
667
700
|
def _config_openclaw(api_key: str, models: list[dict], api_base: str = PROD_API_BASE) -> dict:
|
|
668
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"]
|
|
669
704
|
return {
|
|
670
705
|
"models": {
|
|
671
706
|
"mode": "merge",
|
|
672
707
|
"providers": {
|
|
673
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.
|
|
674
717
|
"baseUrl": f"{api_base}/v1",
|
|
675
718
|
"apiKey": api_key,
|
|
676
719
|
"api": "openai-completions",
|
|
677
|
-
"models":
|
|
678
|
-
}
|
|
720
|
+
"models": embedding_models,
|
|
721
|
+
},
|
|
679
722
|
}
|
|
680
723
|
},
|
|
681
724
|
"agents": {
|
|
682
725
|
"defaults": {
|
|
683
726
|
"models": {
|
|
684
|
-
**{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},
|
|
685
729
|
},
|
|
686
730
|
"memorySearch": {
|
|
687
|
-
"provider": "
|
|
731
|
+
"provider": "hyperclaw-embed",
|
|
688
732
|
"model": "qwen3-embedding-4b",
|
|
689
733
|
"remote": {
|
|
690
734
|
"baseUrl": f"{api_base}/v1/",
|
|
@@ -727,6 +771,28 @@ def _config_env(api_key: str, models: list[dict]) -> str:
|
|
|
727
771
|
return "\n".join(lines)
|
|
728
772
|
|
|
729
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
|
+
|
|
730
796
|
FORMAT_CHOICES = ["openclaw", "opencode", "env"]
|
|
731
797
|
|
|
732
798
|
|
|
@@ -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")
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|