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.
Files changed (38) hide show
  1. {hypercli_cli-2026.4.5 → hypercli_cli-2026.4.6}/PKG-INFO +5 -4
  2. hypercli_cli-2026.4.6/hypercli_cli/__init__.py +1 -0
  3. {hypercli_cli-2026.4.5 → hypercli_cli-2026.4.6}/hypercli_cli/agent.py +71 -43
  4. {hypercli_cli-2026.4.5 → hypercli_cli-2026.4.6}/hypercli_cli/cli.py +49 -2
  5. {hypercli_cli-2026.4.5 → hypercli_cli-2026.4.6}/hypercli_cli/flow.py +168 -90
  6. hypercli_cli-2026.4.6/hypercli_cli/llm.py +155 -0
  7. {hypercli_cli-2026.4.5 → hypercli_cli-2026.4.6}/hypercli_cli/wallet.py +233 -120
  8. {hypercli_cli-2026.4.5 → hypercli_cli-2026.4.6}/pyproject.toml +10 -4
  9. hypercli_cli-2026.4.6/tests/test_config_command.py +50 -0
  10. hypercli_cli-2026.4.6/tests/test_flow_command.py +231 -0
  11. hypercli_cli-2026.4.6/tests/test_llm_command.py +63 -0
  12. hypercli_cli-2026.4.6/tests/test_openclaw_config.py +159 -0
  13. hypercli_cli-2026.4.6/tests/test_wallet_command.py +144 -0
  14. hypercli_cli-2026.4.6/tests/test_wallet_migration_integration.py +71 -0
  15. hypercli_cli-2026.4.5/hypercli_cli/__init__.py +0 -1
  16. hypercli_cli-2026.4.5/tests/test_openclaw_config.py +0 -63
  17. {hypercli_cli-2026.4.5 → hypercli_cli-2026.4.6}/.gitignore +0 -0
  18. {hypercli_cli-2026.4.5 → hypercli_cli-2026.4.6}/README.md +0 -0
  19. {hypercli_cli-2026.4.5 → hypercli_cli-2026.4.6}/hypercli_cli/agents.py +0 -0
  20. {hypercli_cli-2026.4.5 → hypercli_cli-2026.4.6}/hypercli_cli/billing.py +0 -0
  21. {hypercli_cli-2026.4.5 → hypercli_cli-2026.4.6}/hypercli_cli/comfyui.py +0 -0
  22. {hypercli_cli-2026.4.5 → hypercli_cli-2026.4.6}/hypercli_cli/embed.py +0 -0
  23. {hypercli_cli-2026.4.5 → hypercli_cli-2026.4.6}/hypercli_cli/files.py +0 -0
  24. {hypercli_cli-2026.4.5 → hypercli_cli-2026.4.6}/hypercli_cli/instances.py +0 -0
  25. {hypercli_cli-2026.4.5 → hypercli_cli-2026.4.6}/hypercli_cli/jobs.py +0 -0
  26. {hypercli_cli-2026.4.5 → hypercli_cli-2026.4.6}/hypercli_cli/keys.py +0 -0
  27. {hypercli_cli-2026.4.5 → hypercli_cli-2026.4.6}/hypercli_cli/onboard.py +0 -0
  28. {hypercli_cli-2026.4.5 → hypercli_cli-2026.4.6}/hypercli_cli/output.py +0 -0
  29. {hypercli_cli-2026.4.5 → hypercli_cli-2026.4.6}/hypercli_cli/renders.py +0 -0
  30. {hypercli_cli-2026.4.5 → hypercli_cli-2026.4.6}/hypercli_cli/stt.py +0 -0
  31. {hypercli_cli-2026.4.5 → hypercli_cli-2026.4.6}/hypercli_cli/tui/__init__.py +0 -0
  32. {hypercli_cli-2026.4.5 → hypercli_cli-2026.4.6}/hypercli_cli/tui/job_monitor.py +0 -0
  33. {hypercli_cli-2026.4.5 → hypercli_cli-2026.4.6}/hypercli_cli/user.py +0 -0
  34. {hypercli_cli-2026.4.5 → hypercli_cli-2026.4.6}/hypercli_cli/voice.py +0 -0
  35. {hypercli_cli-2026.4.5 → hypercli_cli-2026.4.6}/tests/test_agent_env_resolution.py +0 -0
  36. {hypercli_cli-2026.4.5 → hypercli_cli-2026.4.6}/tests/test_exec_shell_dryrun.py +0 -0
  37. {hypercli_cli-2026.4.5 → hypercli_cli-2026.4.6}/tests/test_jobs_list_tags.py +0 -0
  38. {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.5
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.5
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.5; extra == 'all'
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.5; extra == 'comfyui'
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 agent openclaw-setup[/bold]")
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 agent config openclaw --apply[/bold]")
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
- openai_chat_models = [
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"kimi-coding/{kimi_models[0]['id']}" if kimi_models else (
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-openai/{openai_chat_models[0]['id']}" if openai_chat_models else None
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
- "models": glm_models,
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"kimi-coding/{m['id']}": {"alias": "kimi"} for m in kimi_models},
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 agent config openclaw # OpenClaw snippet
868
+ hyper config openclaw # OpenClaw snippet
894
869
  hyper agent config opencode --key sk-... # OpenCode with explicit key
895
- hyper agent config openclaw --base-url https://api.dev.hypercli.com
896
- hyper agent config openclaw --apply # Write directly to openclaw.json
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
- _deep_merge(existing, data)
941
- merged = existing
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
- output(auth_me, fmt)
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 either via account billing or x402."""
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("renders")
167
- def list_renders(
168
- limit: int = typer.Option(10, "--limit", "-n", help="Number of recent renders to show"),
169
- check: bool = typer.Option(False, "--check", "-c", help="Check current status of each render"),
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 saved x402 render history from ~/.hypercli/x402_renders.jsonl"""
173
- if not X402_RENDERS_FILE.exists():
174
- console.print("[dim]No x402 renders recorded yet.[/dim]")
175
- raise typer.Exit(0)
176
-
177
- entries = []
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(entries, "json")
225
+ output(renders, "json")
212
226
  return
213
227
 
214
- from rich.table import Table
215
- table = Table(title="x402 Renders")
216
- table.add_column("Time", style="dim")
217
- table.add_column("Flow")
218
- table.add_column("Render ID", style="cyan")
219
- table.add_column("USD", justify="right")
220
- if check:
221
- table.add_column("State")
222
- table.add_column("Result")
223
-
224
- for e in entries:
225
- ts = e.get("ts", "?")[:19].replace("T", " ")
226
- row = [ts, e.get("flow_type", "?"), e.get("render_id", "?")[:13] + "…", f"${e.get('amount_usd', 0):.2f}"]
227
- if check:
228
- state = e.get("live_state", "?")
229
- style = "green" if state == "completed" else "red" if state == "failed" else "yellow"
230
- row.append(f"[{style}]{state}[/{style}]")
231
- result = e.get("result_url") or e.get("error") or ""
232
- row.append(result[:60] if result else "")
233
- table.add_row(*row)
234
-
235
- console.print(table)
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")