hypercli-cli 2026.4.24__tar.gz → 2026.5.21__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 (43) hide show
  1. {hypercli_cli-2026.4.24 → hypercli_cli-2026.5.21}/.gitignore +1 -0
  2. {hypercli_cli-2026.4.24 → hypercli_cli-2026.5.21}/PKG-INFO +5 -5
  3. {hypercli_cli-2026.4.24 → hypercli_cli-2026.5.21}/README.md +1 -0
  4. hypercli_cli-2026.5.21/hypercli_cli/__init__.py +1 -0
  5. {hypercli_cli-2026.4.24 → hypercli_cli-2026.5.21}/hypercli_cli/agent.py +63 -6
  6. {hypercli_cli-2026.4.24 → hypercli_cli-2026.5.21}/hypercli_cli/agents.py +3 -5
  7. {hypercli_cli-2026.4.24 → hypercli_cli-2026.5.21}/hypercli_cli/cli.py +63 -1
  8. {hypercli_cli-2026.4.24 → hypercli_cli-2026.5.21}/hypercli_cli/voice.py +22 -12
  9. {hypercli_cli-2026.4.24 → hypercli_cli-2026.5.21}/pyproject.toml +4 -5
  10. hypercli_cli-2026.5.21/tests/test_agent_env_resolution.py +139 -0
  11. {hypercli_cli-2026.4.24 → hypercli_cli-2026.5.21}/tests/test_agent_subscribe_command.py +29 -0
  12. hypercli_cli-2026.5.21/tests/test_me_command.py +91 -0
  13. hypercli_cli-2026.4.24/hypercli_cli/__init__.py +0 -1
  14. hypercli_cli-2026.4.24/tests/test_agent_env_resolution.py +0 -72
  15. hypercli_cli-2026.4.24/tests/test_me_command.py +0 -36
  16. {hypercli_cli-2026.4.24 → hypercli_cli-2026.5.21}/hypercli_cli/billing.py +0 -0
  17. {hypercli_cli-2026.4.24 → hypercli_cli-2026.5.21}/hypercli_cli/comfyui.py +0 -0
  18. {hypercli_cli-2026.4.24 → hypercli_cli-2026.5.21}/hypercli_cli/embed.py +0 -0
  19. {hypercli_cli-2026.4.24 → hypercli_cli-2026.5.21}/hypercli_cli/files.py +0 -0
  20. {hypercli_cli-2026.4.24 → hypercli_cli-2026.5.21}/hypercli_cli/flow.py +0 -0
  21. {hypercli_cli-2026.4.24 → hypercli_cli-2026.5.21}/hypercli_cli/instances.py +0 -0
  22. {hypercli_cli-2026.4.24 → hypercli_cli-2026.5.21}/hypercli_cli/jobs.py +0 -0
  23. {hypercli_cli-2026.4.24 → hypercli_cli-2026.5.21}/hypercli_cli/keys.py +0 -0
  24. {hypercli_cli-2026.4.24 → hypercli_cli-2026.5.21}/hypercli_cli/llm.py +0 -0
  25. {hypercli_cli-2026.4.24 → hypercli_cli-2026.5.21}/hypercli_cli/onboard.py +0 -0
  26. {hypercli_cli-2026.4.24 → hypercli_cli-2026.5.21}/hypercli_cli/output.py +0 -0
  27. {hypercli_cli-2026.4.24 → hypercli_cli-2026.5.21}/hypercli_cli/renders.py +0 -0
  28. {hypercli_cli-2026.4.24 → hypercli_cli-2026.5.21}/hypercli_cli/stt.py +0 -0
  29. {hypercli_cli-2026.4.24 → hypercli_cli-2026.5.21}/hypercli_cli/tui/__init__.py +0 -0
  30. {hypercli_cli-2026.4.24 → hypercli_cli-2026.5.21}/hypercli_cli/tui/job_monitor.py +0 -0
  31. {hypercli_cli-2026.4.24 → hypercli_cli-2026.5.21}/hypercli_cli/user.py +0 -0
  32. {hypercli_cli-2026.4.24 → hypercli_cli-2026.5.21}/hypercli_cli/wallet.py +0 -0
  33. {hypercli_cli-2026.4.24 → hypercli_cli-2026.5.21}/tests/test_config_command.py +0 -0
  34. {hypercli_cli-2026.4.24 → hypercli_cli-2026.5.21}/tests/test_exec_shell_dryrun.py +0 -0
  35. {hypercli_cli-2026.4.24 → hypercli_cli-2026.5.21}/tests/test_flow_command.py +0 -0
  36. {hypercli_cli-2026.4.24 → hypercli_cli-2026.5.21}/tests/test_flow_visibility.py +0 -0
  37. {hypercli_cli-2026.4.24 → hypercli_cli-2026.5.21}/tests/test_jobs_list_tags.py +0 -0
  38. {hypercli_cli-2026.4.24 → hypercli_cli-2026.5.21}/tests/test_keys_command.py +0 -0
  39. {hypercli_cli-2026.4.24 → hypercli_cli-2026.5.21}/tests/test_llm_command.py +0 -0
  40. {hypercli_cli-2026.4.24 → hypercli_cli-2026.5.21}/tests/test_openclaw_config.py +0 -0
  41. {hypercli_cli-2026.4.24 → hypercli_cli-2026.5.21}/tests/test_voice_command.py +0 -0
  42. {hypercli_cli-2026.4.24 → hypercli_cli-2026.5.21}/tests/test_wallet_command.py +0 -0
  43. {hypercli_cli-2026.4.24 → hypercli_cli-2026.5.21}/tests/test_wallet_migration_integration.py +0 -0
@@ -26,6 +26,7 @@ coverage
26
26
 
27
27
  # Misc
28
28
  .DS_Store
29
+ .agents/
29
30
  *.pem
30
31
 
31
32
  # IDE
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hypercli-cli
3
- Version: 2026.4.24
3
+ Version: 2026.5.21
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,22 +9,21 @@ 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>=2026.4.24
12
+ Requires-Dist: hypercli-sdk>=2026.5.21
13
13
  Requires-Dist: mutagen>=1.47.0
14
14
  Requires-Dist: openai>=2.8.1
15
15
  Requires-Dist: pyyaml>=6.0
16
16
  Requires-Dist: rich>=14.2.0
17
17
  Requires-Dist: typer>=0.20.0
18
- Requires-Dist: websocket-client>=1.6.0
19
18
  Provides-Extra: all
20
19
  Requires-Dist: argon2-cffi>=25.0.0; extra == 'all'
21
20
  Requires-Dist: eth-account>=0.13.0; extra == 'all'
22
21
  Requires-Dist: faster-whisper>=1.1.0; extra == 'all'
23
- Requires-Dist: hypercli-sdk[comfyui]>=2026.4.24; extra == 'all'
22
+ Requires-Dist: hypercli-sdk[comfyui]>=2026.5.21; extra == 'all'
24
23
  Requires-Dist: web3>=7.0.0; extra == 'all'
25
24
  Requires-Dist: x402[evm,httpx]>=2.0.0; extra == 'all'
26
25
  Provides-Extra: comfyui
27
- Requires-Dist: hypercli-sdk[comfyui]>=2026.4.24; extra == 'comfyui'
26
+ Requires-Dist: hypercli-sdk[comfyui]>=2026.5.21; extra == 'comfyui'
28
27
  Provides-Extra: dev
29
28
  Requires-Dist: pytest>=8.0.0; extra == 'dev'
30
29
  Requires-Dist: ruff>=0.3.0; extra == 'dev'
@@ -81,6 +80,7 @@ hyper flow text-to-image "a cinematic portrait" --x402
81
80
  # HyperClaw checkout/config
82
81
  hyper agent plans
83
82
  hyper agent subscribe basic
83
+ hyper agent activate-code PROMO123
84
84
  hyper agent config env
85
85
  hyper agent exec <agent_id> "ls -la"
86
86
  hyper agent shell <agent_id>
@@ -41,6 +41,7 @@ hyper flow text-to-image "a cinematic portrait" --x402
41
41
  # HyperClaw checkout/config
42
42
  hyper agent plans
43
43
  hyper agent subscribe basic
44
+ hyper agent activate-code PROMO123
44
45
  hyper agent config env
45
46
  hyper agent exec <agent_id> "ls -la"
46
47
  hyper agent shell <agent_id>
@@ -0,0 +1 @@
1
+ __version__ = "2026.5.21"
@@ -12,7 +12,7 @@ from rich.console import Console
12
12
  from rich.table import Table
13
13
 
14
14
  from hypercli import HyperCLI
15
- from hypercli.config import get_agents_api_base_url_from_product_base
15
+ from hypercli.config import get_agent_api_key, get_agents_api_base_url_from_product_base
16
16
 
17
17
  from .onboard import onboard as _onboard_fn
18
18
  from .voice import app as voice_app
@@ -54,7 +54,7 @@ def require_x402_deps():
54
54
 
55
55
 
56
56
  def _resolve_agent_query_key() -> str:
57
- key = os.getenv("HYPER_AGENTS_API_KEY", "").strip() or os.getenv("HYPER_API_KEY", "").strip()
57
+ key = (get_agent_api_key() or "").strip()
58
58
  if key:
59
59
  return key
60
60
  if AGENT_KEY_PATH.exists():
@@ -498,6 +498,34 @@ def subscription_summary(
498
498
  )
499
499
 
500
500
 
501
+ @app.command("activate-code")
502
+ def activate_code(
503
+ code: str = typer.Argument(..., help="Activation or promo code to redeem"),
504
+ dev: bool = typer.Option(False, "--dev", help="Use dev API"),
505
+ json_output: bool = typer.Option(False, "--json", help="Print raw JSON response"),
506
+ ):
507
+ """Redeem a HyperClaw activation code for the current account."""
508
+ client = _get_agent_query_client(dev)
509
+ result = client.agent.redeem_grant_code(code)
510
+
511
+ if json_output:
512
+ console.print_json(json.dumps(result, indent=2, default=str))
513
+ return
514
+
515
+ grant = result.get("grant") or {}
516
+ entitlement = result.get("entitlement") or {}
517
+ console.print("\n[bold]HyperClaw Code Activated[/bold]\n")
518
+ console.print(f"Code: [bold]{grant.get('code') or code}[/bold]")
519
+ console.print(f"Plan: [bold]{entitlement.get('plan_name') or entitlement.get('plan_id') or grant.get('plan_id') or ''}[/bold]")
520
+ if entitlement.get("starts_at"):
521
+ console.print(f"Starts: {entitlement.get('starts_at')}")
522
+ if entitlement.get("expires_at"):
523
+ console.print(f"Expires: {entitlement.get('expires_at')}")
524
+ tags = entitlement.get("tags") or grant.get("tags") or []
525
+ if tags:
526
+ console.print(f"Tags: {', '.join(str(tag) for tag in tags)}")
527
+
528
+
501
529
  @app.command("models")
502
530
  def models(
503
531
  dev: bool = typer.Option(False, "--dev", help="Use dev API"),
@@ -721,7 +749,21 @@ def fetch_models(api_key: str, api_base: str = PROD_INFERENCE_API_BASE) -> list[
721
749
  normalized = (model_id or "").strip().lower()
722
750
  aliases = {
723
751
  "kimi-k2.5": {"name": "Kimi K2.5", "reasoning": True, "contextWindow": 262144},
752
+ "kimi-k2.5-anthropic": {"name": "Kimi K2.5 Anthropic", "reasoning": True, "contextWindow": 262144},
753
+ "kimi-k2.6": {"name": "Kimi K2.6", "reasoning": True, "contextWindow": 262144},
754
+ "kimi-k2.6-anthropic": {"name": "Kimi K2.6 Anthropic", "reasoning": True, "contextWindow": 262144},
724
755
  "moonshotai/kimi-k2.5": {"name": "Kimi K2.5", "reasoning": True, "contextWindow": 262144},
756
+ "moonshotai/kimi-k2.5-anthropic": {
757
+ "name": "Kimi K2.5 Anthropic",
758
+ "reasoning": True,
759
+ "contextWindow": 262144,
760
+ },
761
+ "moonshotai/kimi-k2.6": {"name": "Kimi K2.6", "reasoning": True, "contextWindow": 262144},
762
+ "moonshotai/kimi-k2.6-anthropic": {
763
+ "name": "Kimi K2.6 Anthropic",
764
+ "reasoning": True,
765
+ "contextWindow": 262144,
766
+ },
725
767
  "glm-5": {"name": "GLM-5", "reasoning": True, "contextWindow": 202752},
726
768
  "zai-org/glm-5": {"name": "GLM-5", "reasoning": True, "contextWindow": 202752},
727
769
  "qwen3-embedding-4b": {
@@ -786,9 +828,21 @@ def fetch_models(api_key: str, api_base: str = PROD_INFERENCE_API_BASE) -> list[
786
828
  ]
787
829
 
788
830
 
831
+ def _preferred_agent_models(models: list[dict]) -> list[dict]:
832
+ """Return the recommended agent models in priority order."""
833
+ preferred = ["kimi-k2.6-anthropic", "kimi-k2.5-anthropic"]
834
+ picked = [model for model_id in preferred for model in models if model["id"] == model_id]
835
+ if picked:
836
+ return picked
837
+ anthropic = [model for model in models if model["id"].endswith("-anthropic")]
838
+ if anthropic:
839
+ return anthropic
840
+ return models[:1]
841
+
842
+
789
843
  @app.command("openclaw-setup")
790
844
  def openclaw_setup(
791
- default: bool = typer.Option(False, "--default", help="Set hyperclaw/kimi-k2.5 as the default model"),
845
+ default: bool = typer.Option(False, "--default", help="Set hyperclaw/kimi-k2.6-anthropic as the default model"),
792
846
  ):
793
847
  """Patch OpenClaw config with your HyperClaw API key.
794
848
 
@@ -884,8 +938,8 @@ def _config_openclaw(
884
938
 
885
939
  api_base = api_base.rstrip("/")
886
940
  supported_models = [m for m in models if _is_supported_openclaw_model(m)]
887
- chat_models = [m for m in supported_models if m.get("mode") != "embedding"]
888
941
  embedding_models = [m for m in supported_models if m.get("mode") == "embedding"]
942
+ chat_models = _preferred_agent_models([m for m in supported_models if m.get("mode") != "embedding"])
889
943
  kimi_models = [m for m in chat_models if "kimi" in _model_suffix(m.get("id", ""))]
890
944
  glm_models = [m for m in chat_models if _model_suffix(m.get("id", "")) == "glm-5"]
891
945
  other_chat_models = [
@@ -946,6 +1000,7 @@ def _config_openclaw(
946
1000
  def _config_opencode(api_key: str, models: list[dict], api_base: str = PROD_INFERENCE_API_BASE) -> dict:
947
1001
  """OpenCode opencode.json provider snippet."""
948
1002
  api_base = api_base.rstrip("/")
1003
+ models = _preferred_agent_models(models)
949
1004
  model_entries = {}
950
1005
  for m in models:
951
1006
  model_entries[m["id"]] = {"name": m["id"]}
@@ -953,7 +1008,7 @@ def _config_opencode(api_key: str, models: list[dict], api_base: str = PROD_INFE
953
1008
  "$schema": "https://opencode.ai/config.json",
954
1009
  "provider": {
955
1010
  "hypercli": {
956
- "npm": "@ai-sdk/openai-compatible",
1011
+ "npm": "@ai-sdk/anthropic",
957
1012
  "name": "HyperCLI",
958
1013
  "options": {
959
1014
  "baseURL": f"{api_base}/v1",
@@ -961,13 +1016,15 @@ def _config_opencode(api_key: str, models: list[dict], api_base: str = PROD_INFE
961
1016
  },
962
1017
  "models": model_entries,
963
1018
  }
964
- }
1019
+ },
1020
+ "model": f"hypercli/{models[0]['id']}",
965
1021
  }
966
1022
 
967
1023
 
968
1024
  def _config_env(api_key: str, models: list[dict], api_base: str = PROD_INFERENCE_API_BASE) -> str:
969
1025
  """Shell env vars for generic OpenAI-compatible tools."""
970
1026
  api_base = api_base.rstrip("/")
1027
+ models = _preferred_agent_models(models)
971
1028
  lines = [
972
1029
  f'export OPENAI_API_KEY="{api_key}"',
973
1030
  f'export OPENAI_BASE_URL="{api_base}/v1"',
@@ -13,6 +13,7 @@ from rich.console import Console
13
13
  from rich.table import Table
14
14
 
15
15
  from hypercli.agents import Agent, Deployments, OpenClawAgent, DEFAULT_OPENCLAW_IMAGE
16
+ from hypercli.config import get_agent_api_key as get_config_agent_api_key
16
17
 
17
18
  app = typer.Typer(help="Manage OpenClaw agent pods")
18
19
  console = Console()
@@ -46,11 +47,8 @@ def agents_root(
46
47
 
47
48
 
48
49
  def _get_agent_api_key() -> str:
49
- """Resolve HyperClaw API key from env or saved key file."""
50
- key = os.environ.get("HYPER_AGENTS_API_KEY", "").strip()
51
- if key:
52
- return key
53
- key = os.environ.get("HYPER_API_KEY", "").strip()
50
+ """Resolve HyperClaw API key from canonical config before legacy key file."""
51
+ key = (get_config_agent_api_key() or "").strip()
54
52
  if key:
55
53
  return key
56
54
  if AGENT_KEY_PATH.exists():
@@ -1,6 +1,7 @@
1
1
  """HyperCLI - Main entry point"""
2
2
  import sys
3
3
  import json
4
+ from datetime import datetime, timezone
4
5
  import typer
5
6
  from rich.console import Console
6
7
  from rich.prompt import Prompt
@@ -15,6 +16,24 @@ from .output import output, spinner
15
16
  console = Console()
16
17
 
17
18
 
19
+ def _format_time_left(expires_at: datetime | None) -> str:
20
+ if not expires_at:
21
+ return ""
22
+ if expires_at.tzinfo is None:
23
+ expires_at = expires_at.replace(tzinfo=timezone.utc)
24
+ remaining = expires_at - datetime.now(timezone.utc)
25
+ if remaining.total_seconds() <= 0:
26
+ return "expired"
27
+ days = remaining.days
28
+ hours = remaining.seconds // 3600
29
+ minutes = (remaining.seconds % 3600) // 60
30
+ if days:
31
+ return f"{days}d {hours}h"
32
+ if hours:
33
+ return f"{hours}h {minutes}m"
34
+ return f"{minutes}m"
35
+
36
+
18
37
  def fuzzy_match(input_str: str, options: list[str], threshold: float = 0.5) -> list[str]:
19
38
  """Find similar strings using multiple heuristics"""
20
39
  def similarity(a: str, b: str) -> float:
@@ -107,8 +126,32 @@ def me_cmd(
107
126
  client = HyperCLI()
108
127
  with spinner("Resolving auth context..."):
109
128
  auth_me = client.user.auth_me()
129
+ entitlement_summary = None
130
+ entitlement_error = None
131
+ try:
132
+ entitlement_summary = client.agent.subscription_summary()
133
+ except APIError as exc:
134
+ entitlement_error = f"{exc.status_code}: {exc.detail}"
135
+ except Exception as exc:
136
+ entitlement_error = str(exc)
110
137
  if fmt == "json":
111
- output(auth_me, fmt)
138
+ payload = dict(getattr(auth_me, "__dict__", {}))
139
+ if entitlement_summary is not None:
140
+ payload["agents_entitlements"] = {
141
+ "effective_plan_id": entitlement_summary.effective_plan_id,
142
+ "current_subscription_id": entitlement_summary.current_subscription_id,
143
+ "current_entitlement_id": entitlement_summary.current_entitlement_id,
144
+ "active_subscription_count": entitlement_summary.active_subscription_count,
145
+ "active_entitlement_count": entitlement_summary.active_entitlement_count,
146
+ "pooled_tpm_limit": entitlement_summary.pooled_tpm_limit,
147
+ "pooled_rpm_limit": entitlement_summary.pooled_rpm_limit,
148
+ "pooled_tpd": entitlement_summary.pooled_tpd,
149
+ "billing_reset_at": entitlement_summary.billing_reset_at,
150
+ "entitlement_items": [item.__dict__ for item in entitlement_summary.entitlement_items],
151
+ }
152
+ elif entitlement_error:
153
+ payload["agents_entitlements_error"] = entitlement_error
154
+ output(payload, fmt)
112
155
  return
113
156
 
114
157
  table = Table(show_header=False, box=None)
@@ -127,6 +170,25 @@ def me_cmd(
127
170
  raw_capabilities = list(getattr(auth_me, "capabilities", []) or [])
128
171
  capabilities = "\n".join(raw_capabilities) if raw_capabilities else ""
129
172
  table.add_row("capabilities", capabilities)
173
+ if entitlement_summary is not None:
174
+ table.add_row("agents_effective_plan", entitlement_summary.effective_plan_id)
175
+ table.add_row("agents_current_entitlement", str(entitlement_summary.current_entitlement_id or ""))
176
+ table.add_row("agents_active_entitlements", str(entitlement_summary.active_entitlement_count))
177
+ active_items = [item for item in entitlement_summary.entitlement_items if str(item.status).upper() == "ACTIVE"]
178
+ expires_at = max((item.expires_at for item in active_items if item.expires_at), default=None)
179
+ if expires_at:
180
+ table.add_row("agents_expires_at", expires_at.isoformat())
181
+ table.add_row("agents_time_left", _format_time_left(expires_at))
182
+ table.add_row(
183
+ "agents_limits",
184
+ (
185
+ f"{entitlement_summary.pooled_tpm_limit:,} TPM / "
186
+ f"{entitlement_summary.pooled_rpm_limit:,} RPM / "
187
+ f"{entitlement_summary.pooled_tpd:,} TPD"
188
+ ),
189
+ )
190
+ elif entitlement_error:
191
+ table.add_row("agents_entitlements", f"unavailable ({entitlement_error})")
130
192
  console.print(table)
131
193
 
132
194
 
@@ -1,11 +1,13 @@
1
1
  """HyperClaw Voice API commands — TTS, clone, design"""
2
2
  import json
3
3
  import os
4
+ from datetime import datetime, timezone
4
5
  from pathlib import Path
5
6
 
6
7
  import typer
7
8
  from rich.console import Console
8
9
  from hypercli import HyperCLI, APIError
10
+ from hypercli.config import get_agent_api_key, get_api_key
9
11
  from .stt import transcribe as _stt_transcribe
10
12
 
11
13
  app = typer.Typer(help="Voice commands — text-to-speech, voice cloning, voice design, and local transcription")
@@ -17,22 +19,30 @@ DEFAULT_API_BASE = "https://api.hypercli.com"
17
19
 
18
20
 
19
21
  def _get_api_key(key: str | None) -> str:
20
- """Resolve API key: --key > HYPER_API_KEY > HYPER_AGENTS_API_KEY > agent-key.json."""
22
+ """Resolve API key from canonical config before legacy agent-key storage."""
21
23
  if key:
22
24
  return key
23
- env_key = os.environ.get("HYPER_API_KEY", "").strip()
24
- if env_key:
25
- return env_key
26
- env_key = os.environ.get("HYPER_AGENTS_API_KEY", "").strip()
27
- if env_key:
28
- return env_key
25
+ configured = (get_api_key() or get_agent_api_key() or "").strip()
26
+ if configured:
27
+ return configured
29
28
  if AGENT_KEY_PATH.exists():
30
- with open(AGENT_KEY_PATH) as f:
31
- k = json.load(f).get("key", "")
32
- if k:
33
- return k
29
+ try:
30
+ with open(AGENT_KEY_PATH) as f:
31
+ saved = json.load(f)
32
+ expires_at = saved.get("expires_at")
33
+ if expires_at:
34
+ expires = datetime.fromisoformat(str(expires_at).replace("Z", "+00:00"))
35
+ if expires.tzinfo is None:
36
+ expires = expires.replace(tzinfo=timezone.utc)
37
+ if expires <= datetime.now(timezone.utc):
38
+ saved = {}
39
+ k = str(saved.get("key", "")).strip()
40
+ if k:
41
+ return k
42
+ except Exception:
43
+ pass
34
44
  console.print("[red]❌ No API key found.[/red]")
35
- console.print("Pass [bold]--key sk-...[/bold], set [bold]HYPER_API_KEY[/bold] or [bold]HYPER_AGENTS_API_KEY[/bold], or run [bold]hyper agent subscribe[/bold]")
45
+ console.print("Pass [bold]--key[/bold], set [bold]HYPER_API_KEY[/bold], or run [bold]hyper configure[/bold].")
36
46
  raise typer.Exit(1)
37
47
 
38
48
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "hypercli-cli"
7
- version = "2026.4.24"
7
+ version = "2026.5.21"
8
8
  description = "CLI for HyperCLI - GPU orchestration and LLM API"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -13,11 +13,10 @@ authors = [
13
13
  { name = "HyperCLI", email = "support@hypercli.com" }
14
14
  ]
15
15
  dependencies = [
16
- "hypercli-sdk>=2026.4.24",
16
+ "hypercli-sdk>=2026.5.21",
17
17
  "openai>=2.8.1",
18
18
  "typer>=0.20.0",
19
19
  "rich>=14.2.0",
20
- "websocket-client>=1.6.0",
21
20
  "mutagen>=1.47.0",
22
21
  "httpx>=0.27.0",
23
22
  "pyyaml>=6.0",
@@ -25,7 +24,7 @@ dependencies = [
25
24
 
26
25
  [project.optional-dependencies]
27
26
  comfyui = [
28
- "hypercli-sdk[comfyui]>=2026.4.24",
27
+ "hypercli-sdk[comfyui]>=2026.5.21",
29
28
  ]
30
29
  wallet = [
31
30
  "x402[httpx,evm]>=2.0.0",
@@ -38,7 +37,7 @@ stt = [
38
37
  "faster-whisper>=1.1.0",
39
38
  ]
40
39
  all = [
41
- "hypercli-sdk[comfyui]>=2026.4.24",
40
+ "hypercli-sdk[comfyui]>=2026.5.21",
42
41
  "x402[httpx,evm]>=2.0.0",
43
42
  "eth-account>=0.13.0",
44
43
  "web3>=7.0.0",
@@ -0,0 +1,139 @@
1
+ import importlib
2
+ import json
3
+ from pathlib import Path
4
+
5
+
6
+ def test_agents_cli_prefers_agent_key_env(monkeypatch):
7
+ monkeypatch.setenv("HYPER_API_KEY", "sk-product")
8
+ monkeypatch.setenv("HYPER_AGENTS_API_KEY", "sk-agent")
9
+
10
+ import hypercli_cli.agents as agents
11
+
12
+ importlib.reload(agents)
13
+
14
+ assert agents._get_agent_api_key() == "sk-agent"
15
+
16
+
17
+ def test_agents_cli_uses_config_without_legacy_agent_key(monkeypatch, tmp_path):
18
+ monkeypatch.delenv("HYPER_API_KEY", raising=False)
19
+ monkeypatch.delenv("HYPERCLI_API_KEY", raising=False)
20
+ monkeypatch.delenv("HYPER_AGENTS_API_KEY", raising=False)
21
+
22
+ config_path = tmp_path / "config"
23
+ config_path.write_text("HYPER_API_KEY=hyper_api_config\n")
24
+
25
+ import hypercli.config as config
26
+ import hypercli_cli.agents as agents
27
+
28
+ importlib.reload(config)
29
+ importlib.reload(agents)
30
+ monkeypatch.setattr(config, "CONFIG_FILE", config_path)
31
+ monkeypatch.setattr(agents, "AGENT_KEY_PATH", tmp_path / "missing-agent-key.json")
32
+
33
+ assert agents._get_agent_api_key() == "hyper_api_config"
34
+
35
+
36
+ def test_agent_activate_uses_config_without_legacy_agent_key(monkeypatch, tmp_path):
37
+ monkeypatch.delenv("HYPER_API_KEY", raising=False)
38
+ monkeypatch.delenv("HYPERCLI_API_KEY", raising=False)
39
+ monkeypatch.delenv("HYPER_AGENTS_API_KEY", raising=False)
40
+
41
+ config_path = tmp_path / "config"
42
+ config_path.write_text("HYPER_API_KEY=hyper_api_config\n")
43
+
44
+ import hypercli.config as config
45
+ import hypercli_cli.agent as agent
46
+
47
+ importlib.reload(config)
48
+ importlib.reload(agent)
49
+ monkeypatch.setattr(config, "CONFIG_FILE", config_path)
50
+ monkeypatch.setattr(agent, "AGENT_KEY_PATH", tmp_path / "missing-agent-key.json")
51
+
52
+ assert agent._resolve_agent_query_key() == "hyper_api_config"
53
+
54
+
55
+ def test_agents_cli_prefers_agent_base_env(monkeypatch):
56
+ monkeypatch.setenv("AGENTS_API_BASE_URL", "https://api.agents.dev.hypercli.com")
57
+ monkeypatch.setenv("AGENTS_WS_URL", "wss://api.agents.dev.hypercli.com/ws")
58
+ monkeypatch.setenv("HYPER_AGENTS_API_KEY", "sk-agent")
59
+
60
+ import hypercli_cli.agents as agents
61
+
62
+ importlib.reload(agents)
63
+
64
+ client = agents._get_deployments_client()
65
+ assert client._api_base == "https://api.dev.hypercli.com/agents"
66
+
67
+
68
+ def test_voice_cli_prefers_product_envs(monkeypatch):
69
+ monkeypatch.setenv("HYPER_API_KEY", "sk-product")
70
+ monkeypatch.setenv("HYPER_AGENTS_API_KEY", "sk-agent")
71
+ monkeypatch.setenv("HYPER_API_BASE", "https://api.hypercli.com")
72
+ monkeypatch.setenv("HYPERCLI_API_URL", "https://api.dev.hypercli.com")
73
+
74
+ import hypercli_cli.voice as voice
75
+
76
+ importlib.reload(voice)
77
+
78
+ assert voice._get_api_key(None) == "sk-product"
79
+ assert voice._resolve_api_base(None) == "https://api.hypercli.com"
80
+
81
+
82
+ def test_voice_cli_uses_config_before_expired_agent_key(monkeypatch, tmp_path):
83
+ monkeypatch.delenv("HYPER_API_KEY", raising=False)
84
+ monkeypatch.delenv("HYPERCLI_API_KEY", raising=False)
85
+ monkeypatch.delenv("HYPER_AGENTS_API_KEY", raising=False)
86
+
87
+ config_path = tmp_path / "config"
88
+ config_path.write_text("HYPER_API_KEY=hyper_api_config\n")
89
+ agent_key_path = tmp_path / "agent-key.json"
90
+ agent_key_path.write_text(
91
+ json.dumps(
92
+ {
93
+ "key": "hyper_api_expired",
94
+ "expires_at": "2026-01-01T00:00:00+00:00",
95
+ }
96
+ )
97
+ )
98
+
99
+ import hypercli.config as config
100
+ import hypercli_cli.voice as voice
101
+
102
+ importlib.reload(config)
103
+ importlib.reload(voice)
104
+ monkeypatch.setattr(config, "CONFIG_FILE", config_path)
105
+ monkeypatch.setattr(voice, "AGENT_KEY_PATH", agent_key_path)
106
+
107
+ assert voice._get_api_key(None) == "hyper_api_config"
108
+
109
+
110
+ def test_voice_cli_posts_to_agents_voice_prefix(monkeypatch, tmp_path):
111
+ monkeypatch.setenv("HYPER_API_KEY", "sk-product")
112
+ monkeypatch.setenv("HYPER_API_BASE", "https://api.dev.hypercli.com")
113
+
114
+ import hypercli_cli.voice as voice
115
+
116
+ importlib.reload(voice)
117
+
118
+ called = {}
119
+
120
+ class _FakeVoice:
121
+ def tts(self, **kwargs):
122
+ called["kwargs"] = kwargs
123
+ return b"audio"
124
+
125
+ class _FakeHyperCLI:
126
+ def __init__(self, *, api_key, api_url):
127
+ called["api_key"] = api_key
128
+ called["api_url"] = api_url
129
+ self.voice = _FakeVoice()
130
+
131
+ monkeypatch.setattr(voice, "HyperCLI", _FakeHyperCLI)
132
+
133
+ out = tmp_path / "voice.wav"
134
+ voice._post_voice("tts", "sk-product", out, text="hello", voice="Chelsie")
135
+
136
+ assert called["api_url"] == "https://api.dev.hypercli.com"
137
+ assert called["api_key"] == "sk-product"
138
+ assert called["kwargs"] == {"text": "hello", "voice": "Chelsie"}
139
+ assert out.read_bytes() == b"audio"
@@ -108,3 +108,32 @@ def test_extract_plan_purchase_url_from_discovery_ignores_nonmatching_resources(
108
108
  }
109
109
 
110
110
  assert agent_mod._extract_plan_purchase_url_from_discovery(discovery, "basic") is None
111
+
112
+
113
+ def test_agent_activate_code_redeems_via_sdk(monkeypatch):
114
+ class _FakeAgent:
115
+ def redeem_grant_code(self, code: str):
116
+ assert code == "promo-123"
117
+ return {
118
+ "grant": {"id": "grant-1", "type": "ACTIVATION_CODE", "code": "promo-123", "plan_id": "basic"},
119
+ "entitlement": {
120
+ "id": "ent-1",
121
+ "plan_id": "basic",
122
+ "plan_name": "Basic",
123
+ "starts_at": "2026-04-27T00:00:00Z",
124
+ "expires_at": "2026-05-27T00:00:00Z",
125
+ "tags": ["customer=acme"],
126
+ },
127
+ }
128
+
129
+ class _FakeClient:
130
+ agent = _FakeAgent()
131
+
132
+ monkeypatch.setattr(agent_mod, "_get_agent_query_client", lambda dev: _FakeClient())
133
+
134
+ result = runner.invoke(app, ["agent", "activate-code", "promo-123"])
135
+
136
+ assert result.exit_code == 0
137
+ assert "HyperClaw Code Activated" in result.output
138
+ assert "promo-123" in result.output
139
+ assert "Basic" in result.output
@@ -0,0 +1,91 @@
1
+ from datetime import datetime, timezone
2
+ from types import SimpleNamespace
3
+
4
+ from typer.testing import CliRunner
5
+
6
+ from hypercli_cli.cli import app
7
+
8
+
9
+ runner = CliRunner()
10
+
11
+
12
+ def test_me_command_outputs_capabilities(monkeypatch):
13
+ class FakeUserAPI:
14
+ def auth_me(self):
15
+ return SimpleNamespace(
16
+ user_id="user-123",
17
+ orchestra_user_id="orch-123",
18
+ team_id="team-123",
19
+ plan_id="pro",
20
+ email="user@example.com",
21
+ auth_type="orchestra_key",
22
+ capabilities=["models:*", "voice:*"],
23
+ key_id="key-123",
24
+ key_name="runtime-key",
25
+ )
26
+
27
+ class FakeClient:
28
+ user = FakeUserAPI()
29
+
30
+ monkeypatch.setattr("hypercli_cli.cli.HyperCLI", lambda: FakeClient())
31
+
32
+ result = runner.invoke(app, ["me"])
33
+
34
+ assert result.exit_code == 0
35
+ assert "models:*" in result.stdout
36
+ assert "voice:*" in result.stdout
37
+ assert "runtime-key" in result.stdout
38
+
39
+
40
+ def test_me_command_outputs_agents_entitlement_summary(monkeypatch):
41
+ class FakeUserAPI:
42
+ def auth_me(self):
43
+ return SimpleNamespace(
44
+ user_id="user-123",
45
+ orchestra_user_id=None,
46
+ team_id="",
47
+ plan_id="",
48
+ email=None,
49
+ auth_type="api_key",
50
+ capabilities=["*:*"],
51
+ has_active_subscription=False,
52
+ key_id="key-123",
53
+ key_name="gpu-operator-prod",
54
+ )
55
+
56
+ class FakeAgentAPI:
57
+ def subscription_summary(self):
58
+ return SimpleNamespace(
59
+ effective_plan_id="pro",
60
+ current_subscription_id=None,
61
+ current_entitlement_id="ent-123",
62
+ active_subscription_count=0,
63
+ active_entitlement_count=1,
64
+ pooled_tpm_limit=17_361_100,
65
+ pooled_rpm_limit=1_736,
66
+ pooled_tpd=500_000_000,
67
+ billing_reset_at=None,
68
+ entitlement_items=[
69
+ SimpleNamespace(
70
+ plan_id="pro",
71
+ status="ACTIVE",
72
+ expires_at=datetime(2036, 5, 16, 11, 36, 49, tzinfo=timezone.utc),
73
+ )
74
+ ],
75
+ )
76
+
77
+ class FakeClient:
78
+ user = FakeUserAPI()
79
+ agent = FakeAgentAPI()
80
+
81
+ monkeypatch.setattr("hypercli_cli.cli.HyperCLI", lambda: FakeClient())
82
+
83
+ result = runner.invoke(app, ["me"])
84
+
85
+ assert result.exit_code == 0
86
+ assert "has_active_subscription" in result.stdout
87
+ assert "no" in result.stdout
88
+ assert "agents_effective_plan" in result.stdout
89
+ assert "pro" in result.stdout
90
+ assert "agents_time_left" in result.stdout
91
+ assert "17,361,100 TPM / 1,736 RPM / 500,000,000 TPD" in result.stdout
@@ -1 +0,0 @@
1
- __version__ = "2026.4.24"
@@ -1,72 +0,0 @@
1
- import importlib
2
- from pathlib import Path
3
-
4
-
5
- def test_agents_cli_prefers_agent_key_env(monkeypatch):
6
- monkeypatch.setenv("HYPER_API_KEY", "sk-product")
7
- monkeypatch.setenv("HYPER_AGENTS_API_KEY", "sk-agent")
8
-
9
- import hypercli_cli.agents as agents
10
-
11
- importlib.reload(agents)
12
-
13
- assert agents._get_agent_api_key() == "sk-agent"
14
-
15
-
16
- def test_agents_cli_prefers_agent_base_env(monkeypatch):
17
- monkeypatch.setenv("AGENTS_API_BASE_URL", "https://api.agents.dev.hypercli.com")
18
- monkeypatch.setenv("AGENTS_WS_URL", "wss://api.agents.dev.hypercli.com/ws")
19
- monkeypatch.setenv("HYPER_AGENTS_API_KEY", "sk-agent")
20
-
21
- import hypercli_cli.agents as agents
22
-
23
- importlib.reload(agents)
24
-
25
- client = agents._get_deployments_client()
26
- assert client._api_base == "https://api.dev.hypercli.com/agents"
27
-
28
-
29
- def test_voice_cli_prefers_product_envs(monkeypatch):
30
- monkeypatch.setenv("HYPER_API_KEY", "sk-product")
31
- monkeypatch.setenv("HYPER_AGENTS_API_KEY", "sk-agent")
32
- monkeypatch.setenv("HYPER_API_BASE", "https://api.hypercli.com")
33
- monkeypatch.setenv("HYPERCLI_API_URL", "https://api.dev.hypercli.com")
34
-
35
- import hypercli_cli.voice as voice
36
-
37
- importlib.reload(voice)
38
-
39
- assert voice._get_api_key(None) == "sk-product"
40
- assert voice._resolve_api_base(None) == "https://api.hypercli.com"
41
-
42
-
43
- def test_voice_cli_posts_to_agents_voice_prefix(monkeypatch, tmp_path):
44
- monkeypatch.setenv("HYPER_API_KEY", "sk-product")
45
- monkeypatch.setenv("HYPER_API_BASE", "https://api.dev.hypercli.com")
46
-
47
- import hypercli_cli.voice as voice
48
-
49
- importlib.reload(voice)
50
-
51
- called = {}
52
-
53
- class _FakeVoice:
54
- def tts(self, **kwargs):
55
- called["kwargs"] = kwargs
56
- return b"audio"
57
-
58
- class _FakeHyperCLI:
59
- def __init__(self, *, api_key, api_url):
60
- called["api_key"] = api_key
61
- called["api_url"] = api_url
62
- self.voice = _FakeVoice()
63
-
64
- monkeypatch.setattr(voice, "HyperCLI", _FakeHyperCLI)
65
-
66
- out = tmp_path / "voice.wav"
67
- voice._post_voice("tts", "sk-product", out, text="hello", voice="Chelsie")
68
-
69
- assert called["api_url"] == "https://api.dev.hypercli.com"
70
- assert called["api_key"] == "sk-product"
71
- assert called["kwargs"] == {"text": "hello", "voice": "Chelsie"}
72
- assert out.read_bytes() == b"audio"
@@ -1,36 +0,0 @@
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_me_command_outputs_capabilities(monkeypatch):
12
- class FakeUserAPI:
13
- def auth_me(self):
14
- return SimpleNamespace(
15
- user_id="user-123",
16
- orchestra_user_id="orch-123",
17
- team_id="team-123",
18
- plan_id="pro",
19
- email="user@example.com",
20
- auth_type="orchestra_key",
21
- capabilities=["models:*", "voice:*"],
22
- key_id="key-123",
23
- key_name="runtime-key",
24
- )
25
-
26
- class FakeClient:
27
- user = FakeUserAPI()
28
-
29
- monkeypatch.setattr("hypercli_cli.cli.HyperCLI", lambda: FakeClient())
30
-
31
- result = runner.invoke(app, ["me"])
32
-
33
- assert result.exit_code == 0
34
- assert "models:*" in result.stdout
35
- assert "voice:*" in result.stdout
36
- assert "runtime-key" in result.stdout