hypercli-cli 2026.4.5__tar.gz → 2026.4.6__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.4.5 → hypercli_cli-2026.4.6}/PKG-INFO +5 -4
- hypercli_cli-2026.4.6/hypercli_cli/__init__.py +1 -0
- {hypercli_cli-2026.4.5 → hypercli_cli-2026.4.6}/hypercli_cli/agent.py +71 -43
- {hypercli_cli-2026.4.5 → hypercli_cli-2026.4.6}/hypercli_cli/cli.py +49 -2
- {hypercli_cli-2026.4.5 → hypercli_cli-2026.4.6}/hypercli_cli/flow.py +168 -90
- hypercli_cli-2026.4.6/hypercli_cli/llm.py +155 -0
- {hypercli_cli-2026.4.5 → hypercli_cli-2026.4.6}/hypercli_cli/wallet.py +233 -120
- {hypercli_cli-2026.4.5 → hypercli_cli-2026.4.6}/pyproject.toml +10 -4
- hypercli_cli-2026.4.6/tests/test_config_command.py +50 -0
- hypercli_cli-2026.4.6/tests/test_flow_command.py +231 -0
- hypercli_cli-2026.4.6/tests/test_llm_command.py +63 -0
- hypercli_cli-2026.4.6/tests/test_openclaw_config.py +159 -0
- hypercli_cli-2026.4.6/tests/test_wallet_command.py +144 -0
- hypercli_cli-2026.4.6/tests/test_wallet_migration_integration.py +71 -0
- hypercli_cli-2026.4.5/hypercli_cli/__init__.py +0 -1
- hypercli_cli-2026.4.5/tests/test_openclaw_config.py +0 -63
- {hypercli_cli-2026.4.5 → hypercli_cli-2026.4.6}/.gitignore +0 -0
- {hypercli_cli-2026.4.5 → hypercli_cli-2026.4.6}/README.md +0 -0
- {hypercli_cli-2026.4.5 → hypercli_cli-2026.4.6}/hypercli_cli/agents.py +0 -0
- {hypercli_cli-2026.4.5 → hypercli_cli-2026.4.6}/hypercli_cli/billing.py +0 -0
- {hypercli_cli-2026.4.5 → hypercli_cli-2026.4.6}/hypercli_cli/comfyui.py +0 -0
- {hypercli_cli-2026.4.5 → hypercli_cli-2026.4.6}/hypercli_cli/embed.py +0 -0
- {hypercli_cli-2026.4.5 → hypercli_cli-2026.4.6}/hypercli_cli/files.py +0 -0
- {hypercli_cli-2026.4.5 → hypercli_cli-2026.4.6}/hypercli_cli/instances.py +0 -0
- {hypercli_cli-2026.4.5 → hypercli_cli-2026.4.6}/hypercli_cli/jobs.py +0 -0
- {hypercli_cli-2026.4.5 → hypercli_cli-2026.4.6}/hypercli_cli/keys.py +0 -0
- {hypercli_cli-2026.4.5 → hypercli_cli-2026.4.6}/hypercli_cli/onboard.py +0 -0
- {hypercli_cli-2026.4.5 → hypercli_cli-2026.4.6}/hypercli_cli/output.py +0 -0
- {hypercli_cli-2026.4.5 → hypercli_cli-2026.4.6}/hypercli_cli/renders.py +0 -0
- {hypercli_cli-2026.4.5 → hypercli_cli-2026.4.6}/hypercli_cli/stt.py +0 -0
- {hypercli_cli-2026.4.5 → hypercli_cli-2026.4.6}/hypercli_cli/tui/__init__.py +0 -0
- {hypercli_cli-2026.4.5 → hypercli_cli-2026.4.6}/hypercli_cli/tui/job_monitor.py +0 -0
- {hypercli_cli-2026.4.5 → hypercli_cli-2026.4.6}/hypercli_cli/user.py +0 -0
- {hypercli_cli-2026.4.5 → hypercli_cli-2026.4.6}/hypercli_cli/voice.py +0 -0
- {hypercli_cli-2026.4.5 → hypercli_cli-2026.4.6}/tests/test_agent_env_resolution.py +0 -0
- {hypercli_cli-2026.4.5 → hypercli_cli-2026.4.6}/tests/test_exec_shell_dryrun.py +0 -0
- {hypercli_cli-2026.4.5 → hypercli_cli-2026.4.6}/tests/test_jobs_list_tags.py +0 -0
- {hypercli_cli-2026.4.5 → hypercli_cli-2026.4.6}/tests/test_me_command.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hypercli-cli
|
|
3
|
-
Version: 2026.4.
|
|
3
|
+
Version: 2026.4.6
|
|
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,8 +9,9 @@ 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.
|
|
12
|
+
Requires-Dist: hypercli-sdk>=2026.4.6
|
|
13
13
|
Requires-Dist: mutagen>=1.47.0
|
|
14
|
+
Requires-Dist: openai>=2.8.1
|
|
14
15
|
Requires-Dist: pyyaml>=6.0
|
|
15
16
|
Requires-Dist: rich>=14.2.0
|
|
16
17
|
Requires-Dist: typer>=0.20.0
|
|
@@ -19,11 +20,11 @@ Provides-Extra: all
|
|
|
19
20
|
Requires-Dist: argon2-cffi>=25.0.0; extra == 'all'
|
|
20
21
|
Requires-Dist: eth-account>=0.13.0; extra == 'all'
|
|
21
22
|
Requires-Dist: faster-whisper>=1.1.0; extra == 'all'
|
|
22
|
-
Requires-Dist: hypercli-sdk[comfyui]>=2026.4.
|
|
23
|
+
Requires-Dist: hypercli-sdk[comfyui]>=2026.4.6; extra == 'all'
|
|
23
24
|
Requires-Dist: web3>=7.0.0; extra == 'all'
|
|
24
25
|
Requires-Dist: x402[evm,httpx]>=2.0.0; extra == 'all'
|
|
25
26
|
Provides-Extra: comfyui
|
|
26
|
-
Requires-Dist: hypercli-sdk[comfyui]>=2026.4.
|
|
27
|
+
Requires-Dist: hypercli-sdk[comfyui]>=2026.4.6; extra == 'comfyui'
|
|
27
28
|
Provides-Extra: dev
|
|
28
29
|
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
29
30
|
Requires-Dist: ruff>=0.3.0; extra == 'dev'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2026.4.6"
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
import asyncio
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
5
7
|
from pathlib import Path
|
|
6
8
|
from datetime import datetime, timedelta
|
|
7
9
|
import typer
|
|
@@ -151,7 +153,7 @@ def subscribe(
|
|
|
151
153
|
console.print(f"Limits: {result['tpm_limit']:,} TPM / {result['rpm_limit']:,} RPM")
|
|
152
154
|
console.print(f"\n[green]✓[/green] Key saved to [bold]{AGENT_KEY_PATH}[/bold]")
|
|
153
155
|
console.print(f"[green]✓[/green] Key history: [bold]{keys_history_path}[/bold]")
|
|
154
|
-
console.print("\nConfigure OpenClaw with: [bold]hyper
|
|
156
|
+
console.print("\nConfigure OpenClaw with: [bold]hyper config openclaw --apply[/bold]")
|
|
155
157
|
|
|
156
158
|
|
|
157
159
|
async def _subscribe_async(account, plan_id: str, api_base: str, amount: str = None):
|
|
@@ -538,7 +540,7 @@ def login(
|
|
|
538
540
|
console.print(f" Wallet: {wallet_addr}")
|
|
539
541
|
console.print(f"\n[green]You're all set![/green]")
|
|
540
542
|
console.print(f" Launch agent: [bold]hyper agents create[/bold]")
|
|
541
|
-
console.print(f" Configure: [bold]hyper
|
|
543
|
+
console.print(f" Configure: [bold]hyper config openclaw --apply[/bold]")
|
|
542
544
|
|
|
543
545
|
|
|
544
546
|
OPENCLAW_CONFIG_PATH = Path.home() / ".openclaw" / "openclaw.json"
|
|
@@ -734,15 +736,16 @@ def _config_openclaw(
|
|
|
734
736
|
embedding_models = [m for m in supported_models if m.get("mode") == "embedding"]
|
|
735
737
|
kimi_models = [m for m in chat_models if "kimi" in _model_suffix(m.get("id", ""))]
|
|
736
738
|
glm_models = [m for m in chat_models if _model_suffix(m.get("id", "")) == "glm-5"]
|
|
737
|
-
|
|
739
|
+
other_chat_models = [
|
|
738
740
|
m for m in chat_models
|
|
739
741
|
if m not in kimi_models and m not in glm_models
|
|
740
742
|
]
|
|
743
|
+
provider_models = kimi_models + glm_models + other_chat_models
|
|
741
744
|
embedding_model_id = embedding_models[0]["id"] if embedding_models else None
|
|
742
745
|
primary_model = (
|
|
743
|
-
f"
|
|
746
|
+
f"hyperclaw/{kimi_models[0]['id']}" if kimi_models else (
|
|
744
747
|
f"hyperclaw/{glm_models[0]['id']}" if glm_models else (
|
|
745
|
-
f"hyperclaw
|
|
748
|
+
f"hyperclaw/{other_chat_models[0]['id']}" if other_chat_models else None
|
|
746
749
|
)
|
|
747
750
|
)
|
|
748
751
|
)
|
|
@@ -756,46 +759,18 @@ def _config_openclaw(
|
|
|
756
759
|
"baseUrl": api_base,
|
|
757
760
|
"apiKey": config_api_key,
|
|
758
761
|
"api": "anthropic-messages",
|
|
759
|
-
"
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
{
|
|
763
|
-
"kimi-coding": {
|
|
764
|
-
# Use the upstream Kimi provider semantics while still
|
|
765
|
-
# routing requests through the HyperClaw Anthropic proxy.
|
|
766
|
-
"baseUrl": api_base,
|
|
767
|
-
"apiKey": config_api_key,
|
|
768
|
-
"api": "anthropic-messages",
|
|
769
|
-
"headers": {
|
|
770
|
-
"User-Agent": "claude-code/0.1.0",
|
|
771
|
-
},
|
|
772
|
-
"models": kimi_models,
|
|
773
|
-
},
|
|
774
|
-
}
|
|
775
|
-
if kimi_models
|
|
776
|
-
else {}
|
|
777
|
-
),
|
|
778
|
-
**(
|
|
779
|
-
{
|
|
780
|
-
"hyperclaw-openai": {
|
|
781
|
-
"baseUrl": f"{api_base}/v1",
|
|
782
|
-
"apiKey": config_api_key,
|
|
783
|
-
"api": "openai-completions",
|
|
784
|
-
"models": openai_chat_models,
|
|
785
|
-
},
|
|
786
|
-
}
|
|
787
|
-
if openai_chat_models
|
|
788
|
-
else {}
|
|
789
|
-
),
|
|
762
|
+
"authHeader": True,
|
|
763
|
+
"models": provider_models,
|
|
764
|
+
}
|
|
790
765
|
}
|
|
791
766
|
},
|
|
792
767
|
"agents": {
|
|
793
768
|
"defaults": {
|
|
794
769
|
**({"model": {"primary": primary_model}} if primary_model else {}),
|
|
795
770
|
"models": {
|
|
771
|
+
**{f"hyperclaw/{m['id']}": {"alias": "kimi"} for m in kimi_models},
|
|
796
772
|
**{f"hyperclaw/{m['id']}": {"alias": "glm"} for m in glm_models},
|
|
797
|
-
**{f"
|
|
798
|
-
**{f"hyperclaw-openai/{m['id']}": {"alias": m['id'].split('-')[0]} for m in openai_chat_models},
|
|
773
|
+
**{f"hyperclaw/{m['id']}": {"alias": m['id'].split('-')[0]} for m in other_chat_models},
|
|
799
774
|
},
|
|
800
775
|
**(
|
|
801
776
|
{
|
|
@@ -890,10 +865,10 @@ def config_cmd(
|
|
|
890
865
|
|
|
891
866
|
Examples:
|
|
892
867
|
hyper agent config # Show all configs
|
|
893
|
-
hyper
|
|
868
|
+
hyper config openclaw # OpenClaw snippet
|
|
894
869
|
hyper agent config opencode --key sk-... # OpenCode with explicit key
|
|
895
|
-
hyper
|
|
896
|
-
hyper
|
|
870
|
+
hyper config openclaw --base-url https://api.dev.hypercli.com
|
|
871
|
+
hyper config openclaw --apply # Write directly to openclaw.json
|
|
897
872
|
hyper agent config env # Shell export lines
|
|
898
873
|
"""
|
|
899
874
|
api_key = _resolve_api_key(key)
|
|
@@ -937,8 +912,11 @@ def _show_snippet(name: str, path_hint: str, data: dict, apply: bool, target_pat
|
|
|
937
912
|
if target_path.exists():
|
|
938
913
|
with open(target_path) as f:
|
|
939
914
|
existing = json.load(f)
|
|
940
|
-
|
|
941
|
-
|
|
915
|
+
if name == "OpenClaw":
|
|
916
|
+
merged = _merge_openclaw_config(existing, data)
|
|
917
|
+
else:
|
|
918
|
+
_deep_merge(existing, data)
|
|
919
|
+
merged = existing
|
|
942
920
|
else:
|
|
943
921
|
merged = data
|
|
944
922
|
|
|
@@ -947,6 +925,31 @@ def _show_snippet(name: str, path_hint: str, data: dict, apply: bool, target_pat
|
|
|
947
925
|
json.dump(merged, f, indent=2)
|
|
948
926
|
f.write("\n")
|
|
949
927
|
console.print(f"[green]✅ Written to {target_path}[/green]\n")
|
|
928
|
+
if name == "OpenClaw":
|
|
929
|
+
_refresh_openclaw_runtime()
|
|
930
|
+
|
|
931
|
+
|
|
932
|
+
def _refresh_openclaw_runtime():
|
|
933
|
+
"""Best-effort refresh of OpenClaw generated runtime state after config changes."""
|
|
934
|
+
if shutil.which("openclaw") is None:
|
|
935
|
+
console.print("[yellow]⚠[/yellow] OpenClaw CLI not found in PATH.")
|
|
936
|
+
console.print("Run after install: [bold]openclaw models list[/bold]")
|
|
937
|
+
console.print("Then restart when ready: [bold]openclaw gateway restart[/bold]\n")
|
|
938
|
+
return
|
|
939
|
+
|
|
940
|
+
try:
|
|
941
|
+
subprocess.run(
|
|
942
|
+
["openclaw", "models", "list"],
|
|
943
|
+
capture_output=True,
|
|
944
|
+
text=True,
|
|
945
|
+
timeout=30,
|
|
946
|
+
check=True,
|
|
947
|
+
)
|
|
948
|
+
console.print("[green]✓[/green] Regenerated OpenClaw model cache.")
|
|
949
|
+
except Exception:
|
|
950
|
+
console.print("[yellow]⚠[/yellow] Could not regenerate OpenClaw model cache automatically.")
|
|
951
|
+
console.print("Run manually: [bold]openclaw models list[/bold]")
|
|
952
|
+
console.print("Restart when ready: [bold]openclaw gateway restart[/bold]\n")
|
|
950
953
|
|
|
951
954
|
|
|
952
955
|
def _deep_merge(base: dict, overlay: dict):
|
|
@@ -956,3 +959,28 @@ def _deep_merge(base: dict, overlay: dict):
|
|
|
956
959
|
_deep_merge(base[k], v)
|
|
957
960
|
else:
|
|
958
961
|
base[k] = v
|
|
962
|
+
|
|
963
|
+
|
|
964
|
+
def _merge_openclaw_config(existing: dict, snippet: dict) -> dict:
|
|
965
|
+
"""Merge OpenClaw config while replacing generated provider/model sections exactly."""
|
|
966
|
+
merged = dict(existing)
|
|
967
|
+
_deep_merge(merged, snippet)
|
|
968
|
+
|
|
969
|
+
snippet_models = ((snippet.get("models") or {}).get("providers") or {})
|
|
970
|
+
if snippet_models:
|
|
971
|
+
merged.setdefault("models", {})
|
|
972
|
+
merged["models"]["providers"] = snippet_models
|
|
973
|
+
|
|
974
|
+
snippet_defaults = (((snippet.get("agents") or {}).get("defaults") or {}).get("models") or {})
|
|
975
|
+
if snippet_defaults:
|
|
976
|
+
merged.setdefault("agents", {})
|
|
977
|
+
merged["agents"].setdefault("defaults", {})
|
|
978
|
+
merged["agents"]["defaults"]["models"] = snippet_defaults
|
|
979
|
+
|
|
980
|
+
snippet_model_config = (((snippet.get("agents") or {}).get("defaults") or {}).get("model") or {})
|
|
981
|
+
if snippet_model_config:
|
|
982
|
+
merged.setdefault("agents", {})
|
|
983
|
+
merged["agents"].setdefault("defaults", {})
|
|
984
|
+
merged["agents"]["defaults"]["model"] = snippet_model_config
|
|
985
|
+
|
|
986
|
+
return merged
|
|
@@ -4,11 +4,12 @@ import json
|
|
|
4
4
|
import typer
|
|
5
5
|
from rich.console import Console
|
|
6
6
|
from rich.prompt import Prompt
|
|
7
|
+
from rich.table import Table
|
|
7
8
|
|
|
8
9
|
from hypercli import HyperCLI, APIError, configure
|
|
9
10
|
from hypercli.config import CONFIG_FILE
|
|
10
11
|
|
|
11
|
-
from . import agent, agents, billing, comfyui, files, flow, instances, jobs, keys, user, wallet
|
|
12
|
+
from . import agent, agents, billing, comfyui, files, flow, instances, jobs, keys, llm, user, wallet
|
|
12
13
|
from .output import output, spinner
|
|
13
14
|
|
|
14
15
|
console = Console()
|
|
@@ -56,10 +57,16 @@ app = typer.Typer(
|
|
|
56
57
|
no_args_is_help=True,
|
|
57
58
|
rich_markup_mode="rich",
|
|
58
59
|
)
|
|
60
|
+
config_app = typer.Typer(
|
|
61
|
+
help="Generate config for OpenClaw and other tools",
|
|
62
|
+
no_args_is_help=True,
|
|
63
|
+
rich_markup_mode="rich",
|
|
64
|
+
)
|
|
59
65
|
|
|
60
66
|
# Register subcommands
|
|
61
67
|
app.add_typer(agents.app, name="agents")
|
|
62
68
|
app.add_typer(agent.app, name="agent")
|
|
69
|
+
app.add_typer(config_app, name="config")
|
|
63
70
|
app.add_typer(billing.app, name="billing")
|
|
64
71
|
app.add_typer(comfyui.app, name="comfyui")
|
|
65
72
|
app.add_typer(files.app, name="files")
|
|
@@ -67,10 +74,30 @@ app.add_typer(flow.app, name="flow")
|
|
|
67
74
|
app.add_typer(instances.app, name="instances")
|
|
68
75
|
app.add_typer(keys.app, name="keys")
|
|
69
76
|
app.add_typer(jobs.app, name="jobs")
|
|
77
|
+
app.add_typer(llm.app, name="llm")
|
|
70
78
|
app.add_typer(user.app, name="user")
|
|
71
79
|
app.add_typer(wallet.app, name="wallet")
|
|
72
80
|
|
|
73
81
|
|
|
82
|
+
@config_app.command("openclaw")
|
|
83
|
+
def config_openclaw_cmd(
|
|
84
|
+
key: str = typer.Option(None, "--key", "-k", help="API key. Falls back to ~/.hypercli/agent-key.json"),
|
|
85
|
+
base_url: str = typer.Option(None, "--base-url", help="HyperClaw API base URL. Falls back to HYPER_API_BASE, then --dev/prod defaults"),
|
|
86
|
+
placeholder_env: str = typer.Option(None, "--placeholder-env", help="Write ${ENV_VAR} placeholders into generated config instead of literal API keys"),
|
|
87
|
+
apply: bool = typer.Option(False, "--apply", help="Write directly to ~/.openclaw/openclaw.json"),
|
|
88
|
+
dev: bool = typer.Option(False, "--dev", help="Use dev API"),
|
|
89
|
+
):
|
|
90
|
+
"""Generate or apply OpenClaw config."""
|
|
91
|
+
agent.config_cmd(
|
|
92
|
+
format="openclaw",
|
|
93
|
+
key=key,
|
|
94
|
+
base_url=base_url,
|
|
95
|
+
placeholder_env=placeholder_env,
|
|
96
|
+
apply=apply,
|
|
97
|
+
dev=dev,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
74
101
|
@app.command("me")
|
|
75
102
|
def me_cmd(
|
|
76
103
|
fmt: str = typer.Option("table", "--output", "-o", help="Output format: table|json"),
|
|
@@ -79,7 +106,27 @@ def me_cmd(
|
|
|
79
106
|
client = HyperCLI()
|
|
80
107
|
with spinner("Resolving auth context..."):
|
|
81
108
|
auth_me = client.user.auth_me()
|
|
82
|
-
|
|
109
|
+
if fmt == "json":
|
|
110
|
+
output(auth_me, fmt)
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
table = Table(show_header=False, box=None)
|
|
114
|
+
table.add_column("Key", style="bold cyan")
|
|
115
|
+
table.add_column("Value")
|
|
116
|
+
table.add_row("user_id", auth_me.user_id)
|
|
117
|
+
table.add_row("orchestra_user_id", str(auth_me.orchestra_user_id or ""))
|
|
118
|
+
table.add_row("team_id", auth_me.team_id)
|
|
119
|
+
table.add_row("plan_id", auth_me.plan_id)
|
|
120
|
+
table.add_row("email", str(auth_me.email or ""))
|
|
121
|
+
table.add_row("auth_type", auth_me.auth_type)
|
|
122
|
+
has_active_subscription = bool(getattr(auth_me, "has_active_subscription", False))
|
|
123
|
+
table.add_row("has_active_subscription", "yes" if has_active_subscription else "no")
|
|
124
|
+
table.add_row("key_id", str(getattr(auth_me, "key_id", None) or ""))
|
|
125
|
+
table.add_row("key_name", str(getattr(auth_me, "key_name", None) or ""))
|
|
126
|
+
raw_capabilities = list(getattr(auth_me, "capabilities", []) or [])
|
|
127
|
+
capabilities = "\n".join(raw_capabilities) if raw_capabilities else ""
|
|
128
|
+
table.add_row("capabilities", capabilities)
|
|
129
|
+
console.print(table)
|
|
83
130
|
|
|
84
131
|
|
|
85
132
|
@app.command("configure")
|
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
"""hyper flow commands - simplified flow interfaces"""
|
|
2
|
-
import json
|
|
3
2
|
import math
|
|
4
|
-
from datetime import datetime, timezone
|
|
5
3
|
from decimal import Decimal, InvalidOperation, ROUND_HALF_UP
|
|
6
4
|
import typer
|
|
7
5
|
from pathlib import Path
|
|
8
6
|
from typing import Any, Optional, List
|
|
7
|
+
from urllib.parse import urlparse
|
|
9
8
|
|
|
10
9
|
from hypercli import HyperCLI, X402Client
|
|
11
|
-
from .output import output, console, spinner
|
|
12
|
-
|
|
13
|
-
X402_RENDERS_FILE = Path.home() / ".hypercli" / "x402_renders.jsonl"
|
|
10
|
+
from .output import output, console, spinner, success
|
|
14
11
|
|
|
15
12
|
HUMO_FPS = 25 # HuMo video_humo template frame rate
|
|
16
13
|
USDC_ATOMIC_UNITS = Decimal("1000000")
|
|
@@ -23,6 +20,16 @@ def get_client() -> HyperCLI:
|
|
|
23
20
|
return HyperCLI()
|
|
24
21
|
|
|
25
22
|
|
|
23
|
+
def _get_flow_client(x402: bool = False) -> HyperCLI:
|
|
24
|
+
if not x402:
|
|
25
|
+
return get_client()
|
|
26
|
+
|
|
27
|
+
from .wallet import get_wallet_auth_token, require_wallet_deps
|
|
28
|
+
|
|
29
|
+
require_wallet_deps()
|
|
30
|
+
return HyperCLI(api_key=get_wallet_auth_token())
|
|
31
|
+
|
|
32
|
+
|
|
26
33
|
def get_audio_duration(file_path: str) -> float | None:
|
|
27
34
|
"""Get audio duration in seconds using mutagen. Returns None on failure."""
|
|
28
35
|
try:
|
|
@@ -56,32 +63,13 @@ def _usd_to_atomic(amount_usd: float) -> int:
|
|
|
56
63
|
return int((usd * USDC_ATOMIC_UNITS).to_integral_value(rounding=ROUND_HALF_UP))
|
|
57
64
|
|
|
58
65
|
|
|
59
|
-
def _save_x402_render(flow_type: str, amount: float, x402_result) -> None:
|
|
60
|
-
"""Append x402 render info to ~/.hypercli/x402_renders.jsonl"""
|
|
61
|
-
try:
|
|
62
|
-
X402_RENDERS_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
63
|
-
entry = {
|
|
64
|
-
"ts": datetime.now(timezone.utc).isoformat(),
|
|
65
|
-
"flow_type": flow_type,
|
|
66
|
-
"render_id": x402_result.render.render_id,
|
|
67
|
-
"amount_usd": amount,
|
|
68
|
-
"access_key": x402_result.access_key,
|
|
69
|
-
"status_url": x402_result.status_url,
|
|
70
|
-
"cancel_url": x402_result.cancel_url,
|
|
71
|
-
}
|
|
72
|
-
with open(X402_RENDERS_FILE, "a") as f:
|
|
73
|
-
f.write(json.dumps(entry) + "\n")
|
|
74
|
-
except Exception:
|
|
75
|
-
pass # best-effort, don't break the flow
|
|
76
|
-
|
|
77
|
-
|
|
78
66
|
def _create_flow_render(
|
|
79
67
|
flow_type: str,
|
|
80
68
|
payload: dict[str, Any],
|
|
81
69
|
x402: bool,
|
|
82
70
|
amount: Optional[float],
|
|
83
71
|
):
|
|
84
|
-
"""Create a flow render
|
|
72
|
+
"""Create a flow render via subscription/account billing unless --x402 is explicit."""
|
|
85
73
|
clean_payload = {k: v for k, v in payload.items() if v is not None}
|
|
86
74
|
|
|
87
75
|
if x402:
|
|
@@ -113,7 +101,6 @@ def _create_flow_render(
|
|
|
113
101
|
params=clean_payload,
|
|
114
102
|
notify_url=notify_url,
|
|
115
103
|
)
|
|
116
|
-
_save_x402_render(flow_type, amount, x402_result)
|
|
117
104
|
return x402_result.render, x402_result
|
|
118
105
|
|
|
119
106
|
with spinner("Creating render..."):
|
|
@@ -150,6 +137,61 @@ def print_render_result(render, fmt: str, x402_result=None):
|
|
|
150
137
|
console.print(f" Cancel URL: {x402_result.cancel_url}")
|
|
151
138
|
|
|
152
139
|
|
|
140
|
+
def _watch_flow_status(client: HyperCLI, render_id: str, poll_interval: float = 2.0):
|
|
141
|
+
"""Watch flow render status live."""
|
|
142
|
+
import time
|
|
143
|
+
from rich.live import Live
|
|
144
|
+
from rich.panel import Panel
|
|
145
|
+
from rich.table import Table
|
|
146
|
+
|
|
147
|
+
def render_status_panel(status, render=None):
|
|
148
|
+
table = Table(show_header=False, box=None)
|
|
149
|
+
table.add_column("Key", style="cyan")
|
|
150
|
+
table.add_column("Value")
|
|
151
|
+
table.add_row("ID", status.render_id)
|
|
152
|
+
table.add_row("State", status.state)
|
|
153
|
+
if status.progress is not None:
|
|
154
|
+
table.add_row("Progress", f"{status.progress:.0%}")
|
|
155
|
+
if render and render.result_url:
|
|
156
|
+
table.add_row("Result", render.result_url)
|
|
157
|
+
if render and render.error:
|
|
158
|
+
table.add_row("Error", render.error)
|
|
159
|
+
return Panel(table, title="[bold]Flow Status[/bold]")
|
|
160
|
+
|
|
161
|
+
with Live(console=console, refresh_per_second=2) as live:
|
|
162
|
+
while True:
|
|
163
|
+
status = client.renders.status(render_id)
|
|
164
|
+
render = None
|
|
165
|
+
if status.state in ("completed", "failed", "cancelled"):
|
|
166
|
+
render = client.renders.get(render_id)
|
|
167
|
+
live.update(render_status_panel(status, render))
|
|
168
|
+
break
|
|
169
|
+
live.update(render_status_panel(status))
|
|
170
|
+
time.sleep(poll_interval)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _download_flow_result(render, destination: Path) -> Path:
|
|
174
|
+
import httpx
|
|
175
|
+
|
|
176
|
+
if not render.result_url:
|
|
177
|
+
raise typer.BadParameter("Render has no result URL to download yet.")
|
|
178
|
+
|
|
179
|
+
target = destination.expanduser()
|
|
180
|
+
if target.is_dir():
|
|
181
|
+
parsed = urlparse(render.result_url)
|
|
182
|
+
filename = Path(parsed.path).name or f"{render.render_id}.bin"
|
|
183
|
+
target = target / filename
|
|
184
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
185
|
+
|
|
186
|
+
with httpx.stream("GET", render.result_url, follow_redirects=True, timeout=60) as response:
|
|
187
|
+
response.raise_for_status()
|
|
188
|
+
with target.open("wb") as handle:
|
|
189
|
+
for chunk in response.iter_bytes():
|
|
190
|
+
handle.write(chunk)
|
|
191
|
+
|
|
192
|
+
return target
|
|
193
|
+
|
|
194
|
+
|
|
153
195
|
# =============================================================================
|
|
154
196
|
# Common option definitions
|
|
155
197
|
# =============================================================================
|
|
@@ -163,76 +205,112 @@ OPT_AMOUNT = typer.Option(None, "--amount", help="USDC amount to spend with --x4
|
|
|
163
205
|
OPT_FMT = typer.Option("table", "--output", "-o", help="Output format: table|json")
|
|
164
206
|
|
|
165
207
|
|
|
166
|
-
@app.command("
|
|
167
|
-
def
|
|
168
|
-
limit: int = typer.Option(
|
|
169
|
-
|
|
208
|
+
@app.command("history")
|
|
209
|
+
def flow_history(
|
|
210
|
+
limit: int = typer.Option(20, "--limit", "-n", help="Number of recent runs to show"),
|
|
211
|
+
state: Optional[str] = typer.Option(None, "--state", "-s", help="Filter by state"),
|
|
212
|
+
template: Optional[str] = typer.Option(None, "--template", "-t", help="Filter by template"),
|
|
213
|
+
type: Optional[str] = typer.Option(None, "--type", help="Filter by render type"),
|
|
214
|
+
x402: bool = typer.Option(False, "--x402", help="Use the local wallet's backend x402 user context."),
|
|
170
215
|
fmt: str = typer.Option("table", "--output", "-o", help="Output format: table|json"),
|
|
171
216
|
):
|
|
172
|
-
"""List
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
with open(X402_RENDERS_FILE) as f:
|
|
179
|
-
for line in f:
|
|
180
|
-
line = line.strip()
|
|
181
|
-
if line:
|
|
182
|
-
try:
|
|
183
|
-
entries.append(json.loads(line))
|
|
184
|
-
except json.JSONDecodeError:
|
|
185
|
-
pass
|
|
186
|
-
|
|
187
|
-
if not entries:
|
|
188
|
-
console.print("[dim]No x402 renders recorded yet.[/dim]")
|
|
189
|
-
raise typer.Exit(0)
|
|
190
|
-
|
|
191
|
-
entries = entries[-limit:]
|
|
192
|
-
|
|
193
|
-
if check:
|
|
194
|
-
import httpx
|
|
195
|
-
for entry in entries:
|
|
196
|
-
access_key = entry.get("access_key", "")
|
|
197
|
-
status_path = entry.get("status_url", "")
|
|
198
|
-
if access_key and status_path:
|
|
199
|
-
try:
|
|
200
|
-
url = f"https://api.hypercli.com/api{status_path}" if status_path.startswith("/flow") else f"https://api.hypercli.com{status_path}"
|
|
201
|
-
resp = httpx.get(url, headers={"Authorization": f"Bearer {access_key}"}, timeout=10)
|
|
202
|
-
if resp.status_code == 200:
|
|
203
|
-
data = resp.json()
|
|
204
|
-
entry["live_state"] = data.get("state", "?")
|
|
205
|
-
entry["result_url"] = data.get("result_url")
|
|
206
|
-
entry["error"] = data.get("error")
|
|
207
|
-
except Exception:
|
|
208
|
-
entry["live_state"] = "error"
|
|
217
|
+
"""List recent flow renders from the remote render API."""
|
|
218
|
+
client = _get_flow_client(x402=x402)
|
|
219
|
+
with spinner("Fetching flow history..."):
|
|
220
|
+
renders = client.renders.list(state=state, template=template, type=type)
|
|
221
|
+
|
|
222
|
+
renders = renders[:limit]
|
|
209
223
|
|
|
210
224
|
if fmt == "json":
|
|
211
|
-
output(
|
|
225
|
+
output(renders, "json")
|
|
212
226
|
return
|
|
213
227
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
228
|
+
if not renders:
|
|
229
|
+
console.print("[dim]No flow renders found[/dim]")
|
|
230
|
+
return
|
|
231
|
+
|
|
232
|
+
output(renders, "table", ["render_id", "state", "template", "render_type", "created_at"])
|
|
233
|
+
|
|
234
|
+
@app.command("list")
|
|
235
|
+
def flow_list(
|
|
236
|
+
state: Optional[str] = typer.Option(None, "--state", "-s", help="Filter by state"),
|
|
237
|
+
template: Optional[str] = typer.Option(None, "--template", "-t", help="Filter by template"),
|
|
238
|
+
type: Optional[str] = typer.Option(None, "--type", help="Filter by render type"),
|
|
239
|
+
x402: bool = typer.Option(False, "--x402", help="Use the local wallet's backend x402 user context."),
|
|
240
|
+
fmt: str = typer.Option("table", "--output", "-o", help="Output format: table|json"),
|
|
241
|
+
):
|
|
242
|
+
"""List flow renders available to the current auth context."""
|
|
243
|
+
client = _get_flow_client(x402=x402)
|
|
244
|
+
with spinner("Fetching flow renders..."):
|
|
245
|
+
renders = client.renders.list(state=state, template=template, type=type)
|
|
246
|
+
|
|
247
|
+
if fmt == "json":
|
|
248
|
+
output(renders, "json")
|
|
249
|
+
return
|
|
250
|
+
|
|
251
|
+
if not renders:
|
|
252
|
+
console.print("[dim]No flow renders found[/dim]")
|
|
253
|
+
return
|
|
254
|
+
|
|
255
|
+
output(renders, "table", ["render_id", "state", "template", "render_type", "created_at"])
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
@app.command("get")
|
|
259
|
+
def flow_get(
|
|
260
|
+
render_id: str = typer.Argument(..., help="Flow render ID"),
|
|
261
|
+
output_path: Optional[str] = typer.Option(
|
|
262
|
+
None,
|
|
263
|
+
"--output",
|
|
264
|
+
"-o",
|
|
265
|
+
help="Write completed render output to file. Legacy: `-o json` or `-o table`.",
|
|
266
|
+
),
|
|
267
|
+
x402: bool = typer.Option(False, "--x402", help="Use the local wallet's backend x402 user context."),
|
|
268
|
+
fmt: str = typer.Option("table", "--format", "-f", help="Output format: table|json"),
|
|
269
|
+
):
|
|
270
|
+
"""Get flow render details."""
|
|
271
|
+
client = _get_flow_client(x402=x402)
|
|
272
|
+
with spinner("Fetching flow render..."):
|
|
273
|
+
render = client.renders.get(render_id)
|
|
274
|
+
|
|
275
|
+
if output_path in {"json", "table"} and fmt == "table":
|
|
276
|
+
fmt = output_path
|
|
277
|
+
output_path = None
|
|
278
|
+
|
|
279
|
+
if output_path:
|
|
280
|
+
saved = _download_flow_result(render, Path(output_path))
|
|
281
|
+
success(f"Saved flow output to {saved}")
|
|
282
|
+
return
|
|
283
|
+
|
|
284
|
+
output(render, fmt)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
@app.command("status")
|
|
288
|
+
def flow_status(
|
|
289
|
+
render_id: str = typer.Argument(..., help="Flow render ID"),
|
|
290
|
+
watch: bool = typer.Option(False, "--watch", "-w", help="Watch status live"),
|
|
291
|
+
x402: bool = typer.Option(False, "--x402", help="Use the local wallet's backend x402 user context."),
|
|
292
|
+
fmt: str = typer.Option("table", "--output", "-o", help="Output format: table|json"),
|
|
293
|
+
):
|
|
294
|
+
"""Get flow render status."""
|
|
295
|
+
client = _get_flow_client(x402=x402)
|
|
296
|
+
if watch:
|
|
297
|
+
_watch_flow_status(client, render_id)
|
|
298
|
+
return
|
|
299
|
+
with spinner("Fetching flow status..."):
|
|
300
|
+
status = client.renders.status(render_id)
|
|
301
|
+
output(status, fmt)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
@app.command("cancel")
|
|
305
|
+
def flow_cancel(
|
|
306
|
+
render_id: str = typer.Argument(..., help="Flow render ID"),
|
|
307
|
+
x402: bool = typer.Option(False, "--x402", help="Use the local wallet's backend x402 user context."),
|
|
308
|
+
):
|
|
309
|
+
"""Cancel a flow render."""
|
|
310
|
+
client = _get_flow_client(x402=x402)
|
|
311
|
+
with spinner("Cancelling flow render..."):
|
|
312
|
+
client.renders.cancel(render_id)
|
|
313
|
+
success(f"Flow render {render_id} cancelled")
|
|
236
314
|
|
|
237
315
|
|
|
238
316
|
@app.command("text-to-image")
|