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.
Files changed (27) hide show
  1. {hypercli_cli-1.0.9 → hypercli_cli-2026.3.8}/PKG-INFO +11 -4
  2. {hypercli_cli-1.0.9 → 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.9 → hypercli_cli-2026.3.8}/hypercli_cli/agents.py +93 -4
  5. {hypercli_cli-1.0.9 → hypercli_cli-2026.3.8}/hypercli_cli/claw.py +90 -24
  6. {hypercli_cli-1.0.9 → hypercli_cli-2026.3.8}/hypercli_cli/jobs.py +19 -2
  7. {hypercli_cli-1.0.9 → hypercli_cli-2026.3.8}/pyproject.toml +4 -4
  8. hypercli_cli-2026.3.8/tests/test_exec_shell_dryrun.py +128 -0
  9. hypercli_cli-2026.3.8/tests/test_jobs_list_tags.py +39 -0
  10. hypercli_cli-1.0.9/hypercli_cli/__init__.py +0 -1
  11. {hypercli_cli-1.0.9 → hypercli_cli-2026.3.8}/.gitignore +0 -0
  12. {hypercli_cli-1.0.9 → hypercli_cli-2026.3.8}/hypercli_cli/billing.py +0 -0
  13. {hypercli_cli-1.0.9 → hypercli_cli-2026.3.8}/hypercli_cli/cli.py +0 -0
  14. {hypercli_cli-1.0.9 → hypercli_cli-2026.3.8}/hypercli_cli/comfyui.py +0 -0
  15. {hypercli_cli-1.0.9 → hypercli_cli-2026.3.8}/hypercli_cli/embed.py +0 -0
  16. {hypercli_cli-1.0.9 → hypercli_cli-2026.3.8}/hypercli_cli/flow.py +0 -0
  17. {hypercli_cli-1.0.9 → hypercli_cli-2026.3.8}/hypercli_cli/instances.py +0 -0
  18. {hypercli_cli-1.0.9 → hypercli_cli-2026.3.8}/hypercli_cli/keys.py +0 -0
  19. {hypercli_cli-1.0.9 → hypercli_cli-2026.3.8}/hypercli_cli/onboard.py +0 -0
  20. {hypercli_cli-1.0.9 → hypercli_cli-2026.3.8}/hypercli_cli/output.py +0 -0
  21. {hypercli_cli-1.0.9 → hypercli_cli-2026.3.8}/hypercli_cli/renders.py +0 -0
  22. {hypercli_cli-1.0.9 → hypercli_cli-2026.3.8}/hypercli_cli/stt.py +0 -0
  23. {hypercli_cli-1.0.9 → hypercli_cli-2026.3.8}/hypercli_cli/tui/__init__.py +0 -0
  24. {hypercli_cli-1.0.9 → hypercli_cli-2026.3.8}/hypercli_cli/tui/job_monitor.py +0 -0
  25. {hypercli_cli-1.0.9 → hypercli_cli-2026.3.8}/hypercli_cli/user.py +0 -0
  26. {hypercli_cli-1.0.9 → hypercli_cli-2026.3.8}/hypercli_cli/voice.py +0 -0
  27. {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: 1.0.9
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
@@ -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 from the public /models endpoint."""
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
- url = f"{api_base}/models"
354
-
355
- try:
356
- response = httpx.get(url, timeout=15)
357
- response.raise_for_status()
358
- payload = response.json()
359
- except Exception as e:
360
- 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
+ )
361
377
  raise typer.Exit(1)
362
378
 
363
- models_data = payload.get("models")
364
- if not isinstance(models_data, list):
365
- 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]")
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(payload))
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: {url}")
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": 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": "openai",
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
- 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']}"
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
- for m in models:
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
- if default:
644
- 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']}")
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": 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 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},
685
729
  },
686
730
  "memorySearch": {
687
- "provider": "openai",
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(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")
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "hypercli-cli"
7
- version = "1.0.9"
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