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.
Files changed (27) hide show
  1. {hypercli_cli-1.0.8 → hypercli_cli-2026.3.8}/PKG-INFO +11 -4
  2. {hypercli_cli-1.0.8 → hypercli_cli-2026.3.8}/README.md +7 -0
  3. hypercli_cli-2026.3.8/hypercli_cli/__init__.py +1 -0
  4. {hypercli_cli-1.0.8 → hypercli_cli-2026.3.8}/hypercli_cli/agents.py +93 -4
  5. {hypercli_cli-1.0.8 → hypercli_cli-2026.3.8}/hypercli_cli/claw.py +97 -41
  6. {hypercli_cli-1.0.8 → hypercli_cli-2026.3.8}/hypercli_cli/instances.py +26 -0
  7. {hypercli_cli-1.0.8 → hypercli_cli-2026.3.8}/hypercli_cli/jobs.py +19 -2
  8. {hypercli_cli-1.0.8 → hypercli_cli-2026.3.8}/hypercli_cli/voice.py +22 -13
  9. {hypercli_cli-1.0.8 → hypercli_cli-2026.3.8}/pyproject.toml +4 -4
  10. hypercli_cli-2026.3.8/tests/test_exec_shell_dryrun.py +128 -0
  11. hypercli_cli-2026.3.8/tests/test_jobs_list_tags.py +39 -0
  12. hypercli_cli-1.0.8/hypercli_cli/__init__.py +0 -1
  13. {hypercli_cli-1.0.8 → hypercli_cli-2026.3.8}/.gitignore +0 -0
  14. {hypercli_cli-1.0.8 → hypercli_cli-2026.3.8}/hypercli_cli/billing.py +0 -0
  15. {hypercli_cli-1.0.8 → hypercli_cli-2026.3.8}/hypercli_cli/cli.py +0 -0
  16. {hypercli_cli-1.0.8 → hypercli_cli-2026.3.8}/hypercli_cli/comfyui.py +0 -0
  17. {hypercli_cli-1.0.8 → hypercli_cli-2026.3.8}/hypercli_cli/embed.py +0 -0
  18. {hypercli_cli-1.0.8 → hypercli_cli-2026.3.8}/hypercli_cli/flow.py +0 -0
  19. {hypercli_cli-1.0.8 → hypercli_cli-2026.3.8}/hypercli_cli/keys.py +0 -0
  20. {hypercli_cli-1.0.8 → hypercli_cli-2026.3.8}/hypercli_cli/onboard.py +0 -0
  21. {hypercli_cli-1.0.8 → hypercli_cli-2026.3.8}/hypercli_cli/output.py +0 -0
  22. {hypercli_cli-1.0.8 → hypercli_cli-2026.3.8}/hypercli_cli/renders.py +0 -0
  23. {hypercli_cli-1.0.8 → hypercli_cli-2026.3.8}/hypercli_cli/stt.py +0 -0
  24. {hypercli_cli-1.0.8 → hypercli_cli-2026.3.8}/hypercli_cli/tui/__init__.py +0 -0
  25. {hypercli_cli-1.0.8 → hypercli_cli-2026.3.8}/hypercli_cli/tui/job_monitor.py +0 -0
  26. {hypercli_cli-1.0.8 → hypercli_cli-2026.3.8}/hypercli_cli/user.py +0 -0
  27. {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: 1.0.8
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>=1.0.0
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]>=1.0.0; extra == 'all'
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]>=1.0.0; extra == '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(name=name, size=size, cpu=cpu, memory=memory, start=not no_start)
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
- table.add_row(
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
- created,
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 transcribe_alias(
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
- """Alias for `hyper claw stt transcribe` (local faster-whisper)."""
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
- @app.command("trascribe", hidden=True)
41
- def trascribe_alias(
42
- audio_file: Path = typer.Argument(..., help="Audio file to transcribe (wav, mp3, ogg, m4a, etc.)"),
43
- model: str = typer.Option("turbo", "--model", "-m", help="Whisper model: tiny, base, small, medium, large-v3, turbo"),
44
- language: str = typer.Option(None, "--language", "-l", help="Language code (e.g. en, de, fr). Auto-detect if omitted."),
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 from the public /models endpoint."""
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
- url = f"{api_base}/models"
364
-
365
- try:
366
- response = httpx.get(url, timeout=15)
367
- response.raise_for_status()
368
- payload = response.json()
369
- except Exception as e:
370
- console.print(f"[red]❌ Failed to fetch models from {url}: {e}[/red]")
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
- models_data = payload.get("models")
374
- if not isinstance(models_data, list):
375
- console.print("[red]❌ Unexpected /models response shape (expected top-level models list)[/red]")
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(payload))
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: {url}")
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": 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": "openai",
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
- config["agents"]["defaults"]["model"]["primary"] = f"hyperclaw/{models[0]['id']}"
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
- for m in models:
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
- if default:
654
- console.print(f" default model: hyperclaw/{models[0]['id']}")
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": 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 models}
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": "openai",
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(jobs, "table", ["job_id", "state", "gpu_type", "gpu_count", "region", "hostname"])
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
- PROD_API_BASE = "https://api.hyperclaw.app"
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
- dev: bool = False,
52
+ base_url: str | None = None,
44
53
  ):
45
54
  """POST to voice endpoint and save audio output."""
46
- api_base = DEV_API_BASE if dev else PROD_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
- dev: bool = typer.Option(False, "--dev", help="Use dev API"),
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, dev)
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
- dev: bool = typer.Option(False, "--dev", help="Use dev API"),
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, dev)
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
- dev: bool = typer.Option(False, "--dev", help="Use dev API"),
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, dev)
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 = "1.0.8"
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>=1.0.0",
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]>=1.0.0",
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]>=1.0.0",
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