hypercli-cli 2026.5.5__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.
- {hypercli_cli-2026.5.5 → hypercli_cli-2026.5.21}/.gitignore +1 -0
- {hypercli_cli-2026.5.5 → hypercli_cli-2026.5.21}/PKG-INFO +4 -5
- hypercli_cli-2026.5.21/hypercli_cli/__init__.py +1 -0
- {hypercli_cli-2026.5.5 → hypercli_cli-2026.5.21}/hypercli_cli/agent.py +35 -6
- {hypercli_cli-2026.5.5 → hypercli_cli-2026.5.21}/hypercli_cli/agents.py +3 -5
- {hypercli_cli-2026.5.5 → hypercli_cli-2026.5.21}/hypercli_cli/cli.py +63 -1
- {hypercli_cli-2026.5.5 → hypercli_cli-2026.5.21}/hypercli_cli/voice.py +22 -12
- {hypercli_cli-2026.5.5 → hypercli_cli-2026.5.21}/pyproject.toml +4 -5
- hypercli_cli-2026.5.21/tests/test_agent_env_resolution.py +139 -0
- hypercli_cli-2026.5.21/tests/test_me_command.py +91 -0
- hypercli_cli-2026.5.5/hypercli_cli/__init__.py +0 -1
- hypercli_cli-2026.5.5/tests/test_agent_env_resolution.py +0 -72
- hypercli_cli-2026.5.5/tests/test_me_command.py +0 -36
- {hypercli_cli-2026.5.5 → hypercli_cli-2026.5.21}/README.md +0 -0
- {hypercli_cli-2026.5.5 → hypercli_cli-2026.5.21}/hypercli_cli/billing.py +0 -0
- {hypercli_cli-2026.5.5 → hypercli_cli-2026.5.21}/hypercli_cli/comfyui.py +0 -0
- {hypercli_cli-2026.5.5 → hypercli_cli-2026.5.21}/hypercli_cli/embed.py +0 -0
- {hypercli_cli-2026.5.5 → hypercli_cli-2026.5.21}/hypercli_cli/files.py +0 -0
- {hypercli_cli-2026.5.5 → hypercli_cli-2026.5.21}/hypercli_cli/flow.py +0 -0
- {hypercli_cli-2026.5.5 → hypercli_cli-2026.5.21}/hypercli_cli/instances.py +0 -0
- {hypercli_cli-2026.5.5 → hypercli_cli-2026.5.21}/hypercli_cli/jobs.py +0 -0
- {hypercli_cli-2026.5.5 → hypercli_cli-2026.5.21}/hypercli_cli/keys.py +0 -0
- {hypercli_cli-2026.5.5 → hypercli_cli-2026.5.21}/hypercli_cli/llm.py +0 -0
- {hypercli_cli-2026.5.5 → hypercli_cli-2026.5.21}/hypercli_cli/onboard.py +0 -0
- {hypercli_cli-2026.5.5 → hypercli_cli-2026.5.21}/hypercli_cli/output.py +0 -0
- {hypercli_cli-2026.5.5 → hypercli_cli-2026.5.21}/hypercli_cli/renders.py +0 -0
- {hypercli_cli-2026.5.5 → hypercli_cli-2026.5.21}/hypercli_cli/stt.py +0 -0
- {hypercli_cli-2026.5.5 → hypercli_cli-2026.5.21}/hypercli_cli/tui/__init__.py +0 -0
- {hypercli_cli-2026.5.5 → hypercli_cli-2026.5.21}/hypercli_cli/tui/job_monitor.py +0 -0
- {hypercli_cli-2026.5.5 → hypercli_cli-2026.5.21}/hypercli_cli/user.py +0 -0
- {hypercli_cli-2026.5.5 → hypercli_cli-2026.5.21}/hypercli_cli/wallet.py +0 -0
- {hypercli_cli-2026.5.5 → hypercli_cli-2026.5.21}/tests/test_agent_subscribe_command.py +0 -0
- {hypercli_cli-2026.5.5 → hypercli_cli-2026.5.21}/tests/test_config_command.py +0 -0
- {hypercli_cli-2026.5.5 → hypercli_cli-2026.5.21}/tests/test_exec_shell_dryrun.py +0 -0
- {hypercli_cli-2026.5.5 → hypercli_cli-2026.5.21}/tests/test_flow_command.py +0 -0
- {hypercli_cli-2026.5.5 → hypercli_cli-2026.5.21}/tests/test_flow_visibility.py +0 -0
- {hypercli_cli-2026.5.5 → hypercli_cli-2026.5.21}/tests/test_jobs_list_tags.py +0 -0
- {hypercli_cli-2026.5.5 → hypercli_cli-2026.5.21}/tests/test_keys_command.py +0 -0
- {hypercli_cli-2026.5.5 → hypercli_cli-2026.5.21}/tests/test_llm_command.py +0 -0
- {hypercli_cli-2026.5.5 → hypercli_cli-2026.5.21}/tests/test_openclaw_config.py +0 -0
- {hypercli_cli-2026.5.5 → hypercli_cli-2026.5.21}/tests/test_voice_command.py +0 -0
- {hypercli_cli-2026.5.5 → hypercli_cli-2026.5.21}/tests/test_wallet_command.py +0 -0
- {hypercli_cli-2026.5.5 → hypercli_cli-2026.5.21}/tests/test_wallet_migration_integration.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hypercli-cli
|
|
3
|
-
Version: 2026.5.
|
|
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.5.
|
|
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.5.
|
|
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.5.
|
|
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'
|
|
@@ -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 =
|
|
57
|
+
key = (get_agent_api_key() or "").strip()
|
|
58
58
|
if key:
|
|
59
59
|
return key
|
|
60
60
|
if AGENT_KEY_PATH.exists():
|
|
@@ -749,7 +749,21 @@ def fetch_models(api_key: str, api_base: str = PROD_INFERENCE_API_BASE) -> list[
|
|
|
749
749
|
normalized = (model_id or "").strip().lower()
|
|
750
750
|
aliases = {
|
|
751
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},
|
|
752
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
|
+
},
|
|
753
767
|
"glm-5": {"name": "GLM-5", "reasoning": True, "contextWindow": 202752},
|
|
754
768
|
"zai-org/glm-5": {"name": "GLM-5", "reasoning": True, "contextWindow": 202752},
|
|
755
769
|
"qwen3-embedding-4b": {
|
|
@@ -814,9 +828,21 @@ def fetch_models(api_key: str, api_base: str = PROD_INFERENCE_API_BASE) -> list[
|
|
|
814
828
|
]
|
|
815
829
|
|
|
816
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
|
+
|
|
817
843
|
@app.command("openclaw-setup")
|
|
818
844
|
def openclaw_setup(
|
|
819
|
-
default: bool = typer.Option(False, "--default", help="Set hyperclaw/kimi-k2.
|
|
845
|
+
default: bool = typer.Option(False, "--default", help="Set hyperclaw/kimi-k2.6-anthropic as the default model"),
|
|
820
846
|
):
|
|
821
847
|
"""Patch OpenClaw config with your HyperClaw API key.
|
|
822
848
|
|
|
@@ -912,8 +938,8 @@ def _config_openclaw(
|
|
|
912
938
|
|
|
913
939
|
api_base = api_base.rstrip("/")
|
|
914
940
|
supported_models = [m for m in models if _is_supported_openclaw_model(m)]
|
|
915
|
-
chat_models = [m for m in supported_models if m.get("mode") != "embedding"]
|
|
916
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"])
|
|
917
943
|
kimi_models = [m for m in chat_models if "kimi" in _model_suffix(m.get("id", ""))]
|
|
918
944
|
glm_models = [m for m in chat_models if _model_suffix(m.get("id", "")) == "glm-5"]
|
|
919
945
|
other_chat_models = [
|
|
@@ -974,6 +1000,7 @@ def _config_openclaw(
|
|
|
974
1000
|
def _config_opencode(api_key: str, models: list[dict], api_base: str = PROD_INFERENCE_API_BASE) -> dict:
|
|
975
1001
|
"""OpenCode opencode.json provider snippet."""
|
|
976
1002
|
api_base = api_base.rstrip("/")
|
|
1003
|
+
models = _preferred_agent_models(models)
|
|
977
1004
|
model_entries = {}
|
|
978
1005
|
for m in models:
|
|
979
1006
|
model_entries[m["id"]] = {"name": m["id"]}
|
|
@@ -981,7 +1008,7 @@ def _config_opencode(api_key: str, models: list[dict], api_base: str = PROD_INFE
|
|
|
981
1008
|
"$schema": "https://opencode.ai/config.json",
|
|
982
1009
|
"provider": {
|
|
983
1010
|
"hypercli": {
|
|
984
|
-
"npm": "@ai-sdk/
|
|
1011
|
+
"npm": "@ai-sdk/anthropic",
|
|
985
1012
|
"name": "HyperCLI",
|
|
986
1013
|
"options": {
|
|
987
1014
|
"baseURL": f"{api_base}/v1",
|
|
@@ -989,13 +1016,15 @@ def _config_opencode(api_key: str, models: list[dict], api_base: str = PROD_INFE
|
|
|
989
1016
|
},
|
|
990
1017
|
"models": model_entries,
|
|
991
1018
|
}
|
|
992
|
-
}
|
|
1019
|
+
},
|
|
1020
|
+
"model": f"hypercli/{models[0]['id']}",
|
|
993
1021
|
}
|
|
994
1022
|
|
|
995
1023
|
|
|
996
1024
|
def _config_env(api_key: str, models: list[dict], api_base: str = PROD_INFERENCE_API_BASE) -> str:
|
|
997
1025
|
"""Shell env vars for generic OpenAI-compatible tools."""
|
|
998
1026
|
api_base = api_base.rstrip("/")
|
|
1027
|
+
models = _preferred_agent_models(models)
|
|
999
1028
|
lines = [
|
|
1000
1029
|
f'export OPENAI_API_KEY="{api_key}"',
|
|
1001
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
|
|
50
|
-
key =
|
|
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
|
-
|
|
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
|
|
22
|
+
"""Resolve API key from canonical config before legacy agent-key storage."""
|
|
21
23
|
if key:
|
|
22
24
|
return key
|
|
23
|
-
|
|
24
|
-
if
|
|
25
|
-
return
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
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.5.
|
|
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.5.
|
|
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.5.
|
|
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.5.
|
|
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"
|
|
@@ -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.5.5"
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|