hypercli-cli 1.0.9__tar.gz → 2026.3.10__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.10}/PKG-INFO +11 -4
- {hypercli_cli-1.0.9 → hypercli_cli-2026.3.10}/README.md +7 -0
- hypercli_cli-2026.3.10/hypercli_cli/__init__.py +1 -0
- {hypercli_cli-1.0.9 → hypercli_cli-2026.3.10}/hypercli_cli/agents.py +93 -4
- {hypercli_cli-1.0.9 → hypercli_cli-2026.3.10}/hypercli_cli/claw.py +105 -29
- {hypercli_cli-1.0.9 → hypercli_cli-2026.3.10}/hypercli_cli/jobs.py +19 -2
- {hypercli_cli-1.0.9 → hypercli_cli-2026.3.10}/pyproject.toml +4 -4
- hypercli_cli-2026.3.10/tests/test_exec_shell_dryrun.py +128 -0
- hypercli_cli-2026.3.10/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.10}/.gitignore +0 -0
- {hypercli_cli-1.0.9 → hypercli_cli-2026.3.10}/hypercli_cli/billing.py +0 -0
- {hypercli_cli-1.0.9 → hypercli_cli-2026.3.10}/hypercli_cli/cli.py +0 -0
- {hypercli_cli-1.0.9 → hypercli_cli-2026.3.10}/hypercli_cli/comfyui.py +0 -0
- {hypercli_cli-1.0.9 → hypercli_cli-2026.3.10}/hypercli_cli/embed.py +0 -0
- {hypercli_cli-1.0.9 → hypercli_cli-2026.3.10}/hypercli_cli/flow.py +0 -0
- {hypercli_cli-1.0.9 → hypercli_cli-2026.3.10}/hypercli_cli/instances.py +0 -0
- {hypercli_cli-1.0.9 → hypercli_cli-2026.3.10}/hypercli_cli/keys.py +0 -0
- {hypercli_cli-1.0.9 → hypercli_cli-2026.3.10}/hypercli_cli/onboard.py +0 -0
- {hypercli_cli-1.0.9 → hypercli_cli-2026.3.10}/hypercli_cli/output.py +0 -0
- {hypercli_cli-1.0.9 → hypercli_cli-2026.3.10}/hypercli_cli/renders.py +0 -0
- {hypercli_cli-1.0.9 → hypercli_cli-2026.3.10}/hypercli_cli/stt.py +0 -0
- {hypercli_cli-1.0.9 → hypercli_cli-2026.3.10}/hypercli_cli/tui/__init__.py +0 -0
- {hypercli_cli-1.0.9 → hypercli_cli-2026.3.10}/hypercli_cli/tui/job_monitor.py +0 -0
- {hypercli_cli-1.0.9 → hypercli_cli-2026.3.10}/hypercli_cli/user.py +0 -0
- {hypercli_cli-1.0.9 → hypercli_cli-2026.3.10}/hypercli_cli/voice.py +0 -0
- {hypercli_cli-1.0.9 → hypercli_cli-2026.3.10}/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.10
|
|
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.10
|
|
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.10; 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.10; 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.10"
|
|
@@ -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")
|
|
@@ -523,6 +542,11 @@ def login(
|
|
|
523
542
|
OPENCLAW_CONFIG_PATH = Path.home() / ".openclaw" / "openclaw.json"
|
|
524
543
|
|
|
525
544
|
|
|
545
|
+
def _resolve_api_base(base_url: str | None = None, dev: bool = False) -> str:
|
|
546
|
+
"""Resolve API base from flag/env, then fall back to dev/prod defaults."""
|
|
547
|
+
return (base_url or os.environ.get("HYPERCLAW_API_BASE") or (DEV_API_BASE if dev else PROD_API_BASE)).rstrip("/")
|
|
548
|
+
|
|
549
|
+
|
|
526
550
|
def fetch_models(api_key: str, api_base: str = PROD_API_BASE) -> list[dict]:
|
|
527
551
|
"""Fetch available models from LiteLLM /v1/models (served by HyperClaw)."""
|
|
528
552
|
import httpx
|
|
@@ -546,6 +570,7 @@ def fetch_models(api_key: str, api_base: str = PROD_API_BASE) -> list[dict]:
|
|
|
546
570
|
"reasoning": MODEL_META.get(m["id"], {}).get("reasoning", False),
|
|
547
571
|
"input": ["text"],
|
|
548
572
|
"contextWindow": MODEL_META.get(m["id"], {}).get("contextWindow", 200000),
|
|
573
|
+
**({"mode": m["mode"]} if m.get("mode") else {}),
|
|
549
574
|
}
|
|
550
575
|
for m in data
|
|
551
576
|
if m.get("id")
|
|
@@ -607,11 +632,19 @@ def openclaw_setup(
|
|
|
607
632
|
|
|
608
633
|
# Patch models.providers.hyperclaw + embedding config
|
|
609
634
|
config.setdefault("models", {}).setdefault("providers", {})
|
|
635
|
+
chat_models = [m for m in models if m.get("mode") != "embedding"]
|
|
636
|
+
embedding_models = [m for m in models if m.get("mode") == "embedding"]
|
|
610
637
|
config["models"]["providers"]["hyperclaw"] = {
|
|
638
|
+
"baseUrl": "https://api.hyperclaw.app",
|
|
639
|
+
"apiKey": api_key,
|
|
640
|
+
"api": "anthropic-messages",
|
|
641
|
+
"models": chat_models,
|
|
642
|
+
}
|
|
643
|
+
config["models"]["providers"]["hyperclaw-embed"] = {
|
|
611
644
|
"baseUrl": "https://api.hyperclaw.app/v1",
|
|
612
645
|
"apiKey": api_key,
|
|
613
646
|
"api": "openai-completions",
|
|
614
|
-
"models":
|
|
647
|
+
"models": embedding_models,
|
|
615
648
|
}
|
|
616
649
|
|
|
617
650
|
# Always set embedding provider (reuses same API key)
|
|
@@ -628,7 +661,8 @@ def openclaw_setup(
|
|
|
628
661
|
# Optionally set default model
|
|
629
662
|
if default:
|
|
630
663
|
config["agents"]["defaults"].setdefault("model", {})
|
|
631
|
-
|
|
664
|
+
if chat_models:
|
|
665
|
+
config["agents"]["defaults"]["model"]["primary"] = f"hyperclaw/{chat_models[0]['id']}"
|
|
632
666
|
|
|
633
667
|
# Write back
|
|
634
668
|
OPENCLAW_CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -638,10 +672,14 @@ def openclaw_setup(
|
|
|
638
672
|
|
|
639
673
|
console.print(f"[green]✅ Patched {OPENCLAW_CONFIG_PATH}[/green]")
|
|
640
674
|
console.print(f" provider: hyperclaw key: {api_key[:16]}...")
|
|
641
|
-
|
|
675
|
+
if embedding_models:
|
|
676
|
+
console.print(" embedding provider: hyperclaw-embed")
|
|
677
|
+
for m in chat_models:
|
|
642
678
|
console.print(f" model: hyperclaw/{m['id']}")
|
|
643
|
-
|
|
644
|
-
console.print(f"
|
|
679
|
+
for m in embedding_models:
|
|
680
|
+
console.print(f" model: hyperclaw-embed/{m['id']}")
|
|
681
|
+
if default and chat_models:
|
|
682
|
+
console.print(f" default model: hyperclaw/{chat_models[0]['id']}")
|
|
645
683
|
console.print("\nRun: [bold]openclaw gateway restart[/bold]")
|
|
646
684
|
|
|
647
685
|
|
|
@@ -666,22 +704,34 @@ def _resolve_api_key(key: str | None) -> str:
|
|
|
666
704
|
|
|
667
705
|
def _config_openclaw(api_key: str, models: list[dict], api_base: str = PROD_API_BASE) -> dict:
|
|
668
706
|
"""OpenClaw openclaw.json provider snippet (LLM + embeddings)."""
|
|
707
|
+
api_base = api_base.rstrip("/")
|
|
708
|
+
chat_models = [m for m in models if m.get("mode") != "embedding"]
|
|
709
|
+
embedding_models = [m for m in models if m.get("mode") == "embedding"]
|
|
669
710
|
return {
|
|
670
711
|
"models": {
|
|
671
712
|
"mode": "merge",
|
|
672
713
|
"providers": {
|
|
673
714
|
"hyperclaw": {
|
|
715
|
+
# OpenClaw/pi-ai appends /v1/messages for anthropic-messages.
|
|
716
|
+
"baseUrl": api_base,
|
|
717
|
+
"apiKey": api_key,
|
|
718
|
+
"api": "anthropic-messages",
|
|
719
|
+
"models": chat_models,
|
|
720
|
+
},
|
|
721
|
+
"hyperclaw-embed": {
|
|
722
|
+
# Embeddings go through the OpenAI-compatible /v1 endpoints.
|
|
674
723
|
"baseUrl": f"{api_base}/v1",
|
|
675
724
|
"apiKey": api_key,
|
|
676
725
|
"api": "openai-completions",
|
|
677
|
-
"models":
|
|
678
|
-
}
|
|
726
|
+
"models": embedding_models,
|
|
727
|
+
},
|
|
679
728
|
}
|
|
680
729
|
},
|
|
681
730
|
"agents": {
|
|
682
731
|
"defaults": {
|
|
683
732
|
"models": {
|
|
684
|
-
**{f"hyperclaw/{m['id']}": {"alias": m['id'].split('-')[0]} for m in
|
|
733
|
+
**{f"hyperclaw/{m['id']}": {"alias": m['id'].split('-')[0]} for m in chat_models},
|
|
734
|
+
**{f"hyperclaw-embed/{m['id']}": {"alias": m['id'].split('-')[0]} for m in embedding_models},
|
|
685
735
|
},
|
|
686
736
|
"memorySearch": {
|
|
687
737
|
"provider": "openai",
|
|
@@ -696,8 +746,9 @@ def _config_openclaw(api_key: str, models: list[dict], api_base: str = PROD_API_
|
|
|
696
746
|
}
|
|
697
747
|
|
|
698
748
|
|
|
699
|
-
def _config_opencode(api_key: str, models: list[dict]) -> dict:
|
|
749
|
+
def _config_opencode(api_key: str, models: list[dict], api_base: str = PROD_API_BASE) -> dict:
|
|
700
750
|
"""OpenCode opencode.json provider snippet."""
|
|
751
|
+
api_base = api_base.rstrip("/")
|
|
701
752
|
model_entries = {}
|
|
702
753
|
for m in models:
|
|
703
754
|
model_entries[m["id"]] = {"name": m["id"]}
|
|
@@ -708,7 +759,7 @@ def _config_opencode(api_key: str, models: list[dict]) -> dict:
|
|
|
708
759
|
"npm": "@ai-sdk/openai-compatible",
|
|
709
760
|
"name": "HyperCLI",
|
|
710
761
|
"options": {
|
|
711
|
-
"baseURL": "
|
|
762
|
+
"baseURL": f"{api_base}/v1",
|
|
712
763
|
"apiKey": api_key,
|
|
713
764
|
},
|
|
714
765
|
"models": model_entries,
|
|
@@ -717,16 +768,39 @@ def _config_opencode(api_key: str, models: list[dict]) -> dict:
|
|
|
717
768
|
}
|
|
718
769
|
|
|
719
770
|
|
|
720
|
-
def _config_env(api_key: str, models: list[dict]) -> str:
|
|
771
|
+
def _config_env(api_key: str, models: list[dict], api_base: str = PROD_API_BASE) -> str:
|
|
721
772
|
"""Shell env vars for generic OpenAI-compatible tools."""
|
|
773
|
+
api_base = api_base.rstrip("/")
|
|
722
774
|
lines = [
|
|
723
775
|
f'export OPENAI_API_KEY="{api_key}"',
|
|
724
|
-
'export OPENAI_BASE_URL="
|
|
776
|
+
f'export OPENAI_BASE_URL="{api_base}/v1"',
|
|
725
777
|
f'# Available models: {", ".join(m["id"] for m in models)}',
|
|
726
778
|
]
|
|
727
779
|
return "\n".join(lines)
|
|
728
780
|
|
|
729
781
|
|
|
782
|
+
@app.command("exec")
|
|
783
|
+
def exec_cmd(
|
|
784
|
+
agent_id: str = typer.Argument(..., help="Agent ID (or prefix)"),
|
|
785
|
+
command: str = typer.Argument(..., help="Command to execute"),
|
|
786
|
+
timeout: int = typer.Option(30, "--timeout", "-t", help="Command timeout (seconds)"),
|
|
787
|
+
):
|
|
788
|
+
"""Execute a command on a HyperClaw agent container."""
|
|
789
|
+
from . import agents
|
|
790
|
+
|
|
791
|
+
agents.exec_cmd(agent_id=agent_id, command=command, timeout=timeout)
|
|
792
|
+
|
|
793
|
+
|
|
794
|
+
@app.command("shell")
|
|
795
|
+
def shell_cmd(
|
|
796
|
+
agent_id: str = typer.Argument(..., help="Agent ID (or prefix)"),
|
|
797
|
+
):
|
|
798
|
+
"""Open an interactive shell on a HyperClaw agent container."""
|
|
799
|
+
from . import agents
|
|
800
|
+
|
|
801
|
+
agents.shell(agent_id=agent_id)
|
|
802
|
+
|
|
803
|
+
|
|
730
804
|
FORMAT_CHOICES = ["openclaw", "opencode", "env"]
|
|
731
805
|
|
|
732
806
|
|
|
@@ -737,6 +811,7 @@ def config_cmd(
|
|
|
737
811
|
help=f"Output format: {', '.join(FORMAT_CHOICES)}. Omit to show all.",
|
|
738
812
|
),
|
|
739
813
|
key: str = typer.Option(None, "--key", "-k", help="API key (sk-...). Falls back to ~/.hypercli/claw-key.json"),
|
|
814
|
+
base_url: str = typer.Option(None, "--base-url", help="HyperClaw API base URL. Falls back to HYPERCLAW_API_BASE, then --dev/prod defaults"),
|
|
740
815
|
apply: bool = typer.Option(False, "--apply", help="Write config to the appropriate file (openclaw/opencode only)"),
|
|
741
816
|
dev: bool = typer.Option(False, "--dev", help="Use dev API"),
|
|
742
817
|
):
|
|
@@ -746,11 +821,12 @@ def config_cmd(
|
|
|
746
821
|
hyper claw config # Show all configs
|
|
747
822
|
hyper claw config openclaw # OpenClaw snippet
|
|
748
823
|
hyper claw config opencode --key sk-... # OpenCode with explicit key
|
|
824
|
+
hyper claw config openclaw --base-url https://dev-api.hyperclaw.app
|
|
749
825
|
hyper claw config openclaw --apply # Write directly to openclaw.json
|
|
750
826
|
hyper claw config env # Shell export lines
|
|
751
827
|
"""
|
|
752
828
|
api_key = _resolve_api_key(key)
|
|
753
|
-
api_base =
|
|
829
|
+
api_base = _resolve_api_base(base_url, dev)
|
|
754
830
|
|
|
755
831
|
# Validate key & fetch models
|
|
756
832
|
console.print(f"[dim]Validating key against {api_base}...[/dim]")
|
|
@@ -770,12 +846,12 @@ def config_cmd(
|
|
|
770
846
|
snippet = _config_openclaw(api_key, models, api_base)
|
|
771
847
|
_show_snippet("OpenClaw", "~/.openclaw/openclaw.json", snippet, apply, OPENCLAW_CONFIG_PATH)
|
|
772
848
|
elif fmt == "opencode":
|
|
773
|
-
snippet = _config_opencode(api_key, models)
|
|
849
|
+
snippet = _config_opencode(api_key, models, api_base)
|
|
774
850
|
target = Path.cwd() / "opencode.json"
|
|
775
851
|
_show_snippet("OpenCode", "opencode.json", snippet, apply, target)
|
|
776
852
|
elif fmt == "env":
|
|
777
853
|
console.print("[bold]── Shell Environment ──[/bold]")
|
|
778
|
-
console.print(_config_env(api_key, models))
|
|
854
|
+
console.print(_config_env(api_key, models, api_base))
|
|
779
855
|
console.print()
|
|
780
856
|
|
|
781
857
|
|
|
@@ -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.10"
|
|
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.10",
|
|
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.10",
|
|
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.10",
|
|
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
|