hypercli-cli 0.8.11__tar.gz → 0.8.13__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hypercli-cli
3
- Version: 0.8.11
3
+ Version: 0.8.13
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
@@ -1,5 +1,7 @@
1
1
  """hyper flow commands - simplified flow interfaces"""
2
+ import json
2
3
  import math
4
+ from datetime import datetime, timezone
3
5
  from decimal import Decimal, InvalidOperation, ROUND_HALF_UP
4
6
  import typer
5
7
  from pathlib import Path
@@ -8,6 +10,8 @@ from typing import Any, Optional, List
8
10
  from hypercli import HyperCLI, X402Client
9
11
  from .output import output, console, spinner
10
12
 
13
+ X402_RENDERS_FILE = Path.home() / ".hypercli" / "x402_renders.jsonl"
14
+
11
15
  HUMO_FPS = 25 # HuMo video_humo template frame rate
12
16
  USDC_ATOMIC_UNITS = Decimal("1000000")
13
17
 
@@ -52,6 +56,25 @@ def _usd_to_atomic(amount_usd: float) -> int:
52
56
  return int((usd * USDC_ATOMIC_UNITS).to_integral_value(rounding=ROUND_HALF_UP))
53
57
 
54
58
 
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
+
55
78
  def _create_flow_render(
56
79
  flow_type: str,
57
80
  payload: dict[str, Any],
@@ -90,6 +113,7 @@ def _create_flow_render(
90
113
  params=clean_payload,
91
114
  notify_url=notify_url,
92
115
  )
116
+ _save_x402_render(flow_type, amount, x402_result)
93
117
  return x402_result.render, x402_result
94
118
 
95
119
  with spinner("Creating render..."):
@@ -139,6 +163,78 @@ OPT_AMOUNT = typer.Option(None, "--amount", help="USDC amount to spend with --x4
139
163
  OPT_FMT = typer.Option("table", "--output", "-o", help="Output format: table|json")
140
164
 
141
165
 
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"),
170
+ fmt: str = typer.Option("table", "--output", "-o", help="Output format: table|json"),
171
+ ):
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"
209
+
210
+ if fmt == "json":
211
+ output(entries, "json")
212
+ return
213
+
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)
236
+
237
+
142
238
  @app.command("text-to-image")
143
239
  def text_to_image(
144
240
  prompt: str = typer.Argument(..., help="Text description of the image"),
@@ -11,6 +11,24 @@ def get_client() -> HyperCLI:
11
11
  return HyperCLI()
12
12
 
13
13
 
14
+ def _resolve_job_id(client: HyperCLI, job_id: str) -> str:
15
+ """Resolve a partial job ID prefix to a full UUID."""
16
+ if len(job_id) == 36:
17
+ return job_id
18
+ jobs = client.jobs.list()
19
+ matches = [j.job_id for j in jobs if j.job_id.startswith(job_id)]
20
+ if len(matches) == 1:
21
+ return matches[0]
22
+ elif len(matches) == 0:
23
+ console.print(f"[red]Error:[/red] No job matching '{job_id}' in recent jobs. Try the full UUID.")
24
+ raise typer.Exit(1)
25
+ else:
26
+ console.print(f"[red]Error:[/red] Ambiguous prefix '{job_id}' — {len(matches)} matches:")
27
+ for m in matches[:5]:
28
+ console.print(f" {m}")
29
+ raise typer.Exit(1)
30
+
31
+
14
32
  @app.command("list")
15
33
  def list_jobs(
16
34
  state: Optional[str] = typer.Option(None, "--state", "-s", help="Filter by state"),
@@ -37,6 +55,7 @@ def get_job(
37
55
  ):
38
56
  """Get job details"""
39
57
  client = get_client()
58
+ job_id = _resolve_job_id(client, job_id)
40
59
  with spinner("Fetching job..."):
41
60
  job = client.jobs.get(job_id)
42
61
  output(job, fmt)
@@ -52,6 +71,7 @@ def logs(
52
71
  ):
53
72
  """Get job logs"""
54
73
  client = get_client()
74
+ job_id = _resolve_job_id(client, job_id)
55
75
 
56
76
  if tui:
57
77
  _follow_job(job_id, cancel_on_exit=cancel_on_exit)
@@ -77,6 +97,7 @@ def metrics(
77
97
  ):
78
98
  """Get job GPU metrics"""
79
99
  client = get_client()
100
+ job_id = _resolve_job_id(client, job_id)
80
101
 
81
102
  if watch:
82
103
  _watch_metrics(job_id)
@@ -95,6 +116,7 @@ def cancel(
95
116
  ):
96
117
  """Cancel a running job"""
97
118
  client = get_client()
119
+ job_id = _resolve_job_id(client, job_id)
98
120
  with spinner("Cancelling job..."):
99
121
  client.jobs.cancel(job_id)
100
122
  success(f"Job {job_id} cancelled")
@@ -108,6 +130,7 @@ def extend(
108
130
  ):
109
131
  """Extend job runtime"""
110
132
  client = get_client()
133
+ job_id = _resolve_job_id(client, job_id)
111
134
  with spinner("Extending runtime..."):
112
135
  job = client.jobs.extend(job_id, runtime)
113
136
  if fmt == "json":
@@ -42,7 +42,7 @@ def table_list(items: list, columns: list[str] = None):
42
42
  table = Table(show_header=True, header_style="bold cyan")
43
43
 
44
44
  for col in cols:
45
- table.add_column(col)
45
+ table.add_column(col, no_wrap=True if col.endswith("_id") else False)
46
46
 
47
47
  for item in items:
48
48
  row = [str(item.get(col, "")) for col in cols]
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "hypercli-cli"
7
- version = "0.8.11"
7
+ version = "0.8.13"
8
8
  description = "CLI for HyperCLI - GPU orchestration and LLM API"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
File without changes
File without changes