hypercli-cli 0.8.13__tar.gz → 0.9.0__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-0.8.13 → hypercli_cli-0.9.0}/PKG-INFO +1 -1
- hypercli_cli-0.9.0/hypercli_cli/agents.py +559 -0
- {hypercli_cli-0.8.13 → hypercli_cli-0.9.0}/hypercli_cli/claw.py +144 -2
- {hypercli_cli-0.8.13 → hypercli_cli-0.9.0}/hypercli_cli/cli.py +2 -1
- {hypercli_cli-0.8.13 → hypercli_cli-0.9.0}/hypercli_cli/instances.py +12 -0
- hypercli_cli-0.9.0/hypercli_cli/voice.py +155 -0
- {hypercli_cli-0.8.13 → hypercli_cli-0.9.0}/pyproject.toml +1 -1
- {hypercli_cli-0.8.13 → hypercli_cli-0.9.0}/.gitignore +0 -0
- {hypercli_cli-0.8.13 → hypercli_cli-0.9.0}/README.md +0 -0
- {hypercli_cli-0.8.13 → hypercli_cli-0.9.0}/hypercli_cli/__init__.py +0 -0
- {hypercli_cli-0.8.13 → hypercli_cli-0.9.0}/hypercli_cli/billing.py +0 -0
- {hypercli_cli-0.8.13 → hypercli_cli-0.9.0}/hypercli_cli/comfyui.py +0 -0
- {hypercli_cli-0.8.13 → hypercli_cli-0.9.0}/hypercli_cli/flow.py +0 -0
- {hypercli_cli-0.8.13 → hypercli_cli-0.9.0}/hypercli_cli/jobs.py +0 -0
- {hypercli_cli-0.8.13 → hypercli_cli-0.9.0}/hypercli_cli/keys.py +0 -0
- {hypercli_cli-0.8.13 → hypercli_cli-0.9.0}/hypercli_cli/onboard.py +0 -0
- {hypercli_cli-0.8.13 → hypercli_cli-0.9.0}/hypercli_cli/output.py +0 -0
- {hypercli_cli-0.8.13 → hypercli_cli-0.9.0}/hypercli_cli/renders.py +0 -0
- {hypercli_cli-0.8.13 → hypercli_cli-0.9.0}/hypercli_cli/tui/__init__.py +0 -0
- {hypercli_cli-0.8.13 → hypercli_cli-0.9.0}/hypercli_cli/tui/job_monitor.py +0 -0
- {hypercli_cli-0.8.13 → hypercli_cli-0.9.0}/hypercli_cli/user.py +0 -0
- {hypercli_cli-0.8.13 → hypercli_cli-0.9.0}/hypercli_cli/wallet.py +0 -0
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
"""HyperClaw Agents — Reef Pod Management CLI"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.table import Table
|
|
13
|
+
|
|
14
|
+
from hypercli.agents import Agents, ReefPod
|
|
15
|
+
|
|
16
|
+
app = typer.Typer(help="Manage OpenClaw agent pods (reef containers)")
|
|
17
|
+
console = Console()
|
|
18
|
+
|
|
19
|
+
# Config — uses HyperClaw API key (sk-...) for backend auth
|
|
20
|
+
CLAW_KEY_PATH = Path.home() / ".hypercli" / "claw-key.json"
|
|
21
|
+
STATE_DIR = Path.home() / ".hypercli"
|
|
22
|
+
AGENTS_STATE = STATE_DIR / "agents.json"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _get_claw_api_key() -> str:
|
|
26
|
+
"""Resolve HyperClaw API key from env or saved key file."""
|
|
27
|
+
key = os.environ.get("HYPERCLAW_API_KEY", "")
|
|
28
|
+
if key:
|
|
29
|
+
return key
|
|
30
|
+
if CLAW_KEY_PATH.exists():
|
|
31
|
+
with open(CLAW_KEY_PATH) as f:
|
|
32
|
+
data = json.load(f)
|
|
33
|
+
key = data.get("key", "")
|
|
34
|
+
if key:
|
|
35
|
+
return key
|
|
36
|
+
console.print("[red]❌ No HyperClaw API key found.[/red]")
|
|
37
|
+
console.print("Set HYPERCLAW_API_KEY or subscribe: [bold]hyper claw subscribe 1aiu[/bold]")
|
|
38
|
+
raise typer.Exit(1)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _get_agents_client() -> Agents:
|
|
42
|
+
"""Create an Agents client using the HyperClaw API key."""
|
|
43
|
+
from hypercli.http import HTTPClient
|
|
44
|
+
api_key = _get_claw_api_key()
|
|
45
|
+
api_base = os.environ.get("HYPERCLAW_API_BASE", "https://api.hyperclaw.app")
|
|
46
|
+
http = HTTPClient(api_base, api_key)
|
|
47
|
+
return Agents(http, claw_api_key=api_key, claw_api_base=api_base)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _save_pod_state(pod: ReefPod):
|
|
51
|
+
"""Save pod info locally for quick reference."""
|
|
52
|
+
STATE_DIR.mkdir(parents=True, exist_ok=True)
|
|
53
|
+
state = _load_state()
|
|
54
|
+
state[pod.id] = {
|
|
55
|
+
"id": pod.id,
|
|
56
|
+
"pod_id": pod.pod_id,
|
|
57
|
+
"pod_name": pod.pod_name,
|
|
58
|
+
"user_id": pod.user_id,
|
|
59
|
+
"hostname": pod.hostname,
|
|
60
|
+
"jwt_token": pod.jwt_token,
|
|
61
|
+
"state": pod.state,
|
|
62
|
+
}
|
|
63
|
+
with open(AGENTS_STATE, "w") as f:
|
|
64
|
+
json.dump(state, f, indent=2, default=str)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _load_state() -> dict:
|
|
68
|
+
if AGENTS_STATE.exists():
|
|
69
|
+
with open(AGENTS_STATE) as f:
|
|
70
|
+
return json.load(f)
|
|
71
|
+
return {}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _remove_pod_state(agent_id: str):
|
|
75
|
+
state = _load_state()
|
|
76
|
+
state.pop(agent_id, None)
|
|
77
|
+
with open(AGENTS_STATE, "w") as f:
|
|
78
|
+
json.dump(state, f, indent=2, default=str)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _resolve_agent(agent_id: str) -> str:
|
|
82
|
+
"""Resolve agent_id with prefix matching from local state."""
|
|
83
|
+
state = _load_state()
|
|
84
|
+
if agent_id in state:
|
|
85
|
+
return agent_id
|
|
86
|
+
matches = [k for k in state if k.startswith(agent_id)]
|
|
87
|
+
if len(matches) == 1:
|
|
88
|
+
return matches[0]
|
|
89
|
+
if len(matches) > 1:
|
|
90
|
+
console.print(f"[yellow]Ambiguous ID prefix '{agent_id}'. Matches:[/yellow]")
|
|
91
|
+
for m in matches:
|
|
92
|
+
s = state[m]
|
|
93
|
+
console.print(f" {m[:12]} {s.get('pod_name', '')} {s.get('state', '')}")
|
|
94
|
+
raise typer.Exit(1)
|
|
95
|
+
return agent_id
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _get_pod_with_token(agent_id: str) -> ReefPod:
|
|
99
|
+
"""Get a ReefPod, filling JWT from local state if needed."""
|
|
100
|
+
agents = _get_agents_client()
|
|
101
|
+
pod = agents.get(agent_id)
|
|
102
|
+
if not pod.jwt_token:
|
|
103
|
+
state = _load_state()
|
|
104
|
+
local = state.get(agent_id, {})
|
|
105
|
+
if local.get("jwt_token"):
|
|
106
|
+
pod.jwt_token = local["jwt_token"]
|
|
107
|
+
return pod
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@app.command("create")
|
|
111
|
+
def create(
|
|
112
|
+
wait: bool = typer.Option(True, "--wait/--no-wait", help="Wait for pod to be running"),
|
|
113
|
+
):
|
|
114
|
+
"""Create a new OpenClaw agent pod."""
|
|
115
|
+
agents = _get_agents_client()
|
|
116
|
+
|
|
117
|
+
console.print("\n[bold]Creating agent pod...[/bold]")
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
pod = agents.create()
|
|
121
|
+
except Exception as e:
|
|
122
|
+
console.print(f"[red]❌ Create failed: {e}[/red]")
|
|
123
|
+
raise typer.Exit(1)
|
|
124
|
+
|
|
125
|
+
_save_pod_state(pod)
|
|
126
|
+
|
|
127
|
+
console.print(f"[green]✓[/green] Agent created: [bold]{pod.id[:12]}[/bold]")
|
|
128
|
+
console.print(f" Pod: {pod.pod_name}")
|
|
129
|
+
console.print(f" State: {pod.state}")
|
|
130
|
+
console.print(f" Desktop: {pod.vnc_url}")
|
|
131
|
+
console.print(f" Shell: {pod.shell_url}")
|
|
132
|
+
|
|
133
|
+
if wait:
|
|
134
|
+
console.print("\n[dim]Waiting for pod to start...[/dim]")
|
|
135
|
+
for i in range(60):
|
|
136
|
+
time.sleep(5)
|
|
137
|
+
try:
|
|
138
|
+
pod = agents.get(pod.id)
|
|
139
|
+
_save_pod_state(pod)
|
|
140
|
+
if pod.is_running:
|
|
141
|
+
console.print(f"[green]✅ Agent is running![/green]")
|
|
142
|
+
break
|
|
143
|
+
elif pod.state in ("failed", "stopped"):
|
|
144
|
+
console.print(f"[red]❌ Agent failed: {pod.state}[/red]")
|
|
145
|
+
if pod.last_error:
|
|
146
|
+
console.print(f" Error: {pod.last_error}")
|
|
147
|
+
raise typer.Exit(1)
|
|
148
|
+
else:
|
|
149
|
+
console.print(f" [{i*5}s] State: {pod.state}")
|
|
150
|
+
except typer.Exit:
|
|
151
|
+
raise
|
|
152
|
+
except Exception as e:
|
|
153
|
+
console.print(f" [{i*5}s] Checking... ({e})")
|
|
154
|
+
else:
|
|
155
|
+
console.print("[yellow]⚠ Timed out (5 min). Pod may still be starting.[/yellow]")
|
|
156
|
+
|
|
157
|
+
console.print(f"\nExec: [bold]hyper agents exec {pod.id[:8]} 'echo hello'[/bold]")
|
|
158
|
+
console.print(f"Shell: [bold]hyper agents shell {pod.id[:8]}[/bold]")
|
|
159
|
+
console.print(f"Desktop: {pod.vnc_url}")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@app.command("list")
|
|
163
|
+
def list_agents(
|
|
164
|
+
json_output: bool = typer.Option(False, "--json", help="JSON output"),
|
|
165
|
+
):
|
|
166
|
+
"""List all agent pods."""
|
|
167
|
+
agents = _get_agents_client()
|
|
168
|
+
|
|
169
|
+
try:
|
|
170
|
+
pods = agents.list()
|
|
171
|
+
except Exception as e:
|
|
172
|
+
console.print(f"[red]❌ Failed to list agents: {e}[/red]")
|
|
173
|
+
raise typer.Exit(1)
|
|
174
|
+
|
|
175
|
+
if json_output:
|
|
176
|
+
console.print_json(json.dumps([{
|
|
177
|
+
"id": p.id, "pod_name": p.pod_name, "state": p.state,
|
|
178
|
+
"hostname": p.hostname, "vnc_url": p.vnc_url,
|
|
179
|
+
} for p in pods], indent=2, default=str))
|
|
180
|
+
return
|
|
181
|
+
|
|
182
|
+
if not pods:
|
|
183
|
+
console.print("[dim]No agents found.[/dim]")
|
|
184
|
+
console.print("Create one: [bold]hyper agents create[/bold]")
|
|
185
|
+
return
|
|
186
|
+
|
|
187
|
+
table = Table(title="Agents")
|
|
188
|
+
table.add_column("ID", style="cyan", no_wrap=True)
|
|
189
|
+
table.add_column("Pod", style="blue")
|
|
190
|
+
table.add_column("State")
|
|
191
|
+
table.add_column("Desktop URL")
|
|
192
|
+
table.add_column("Created")
|
|
193
|
+
|
|
194
|
+
for pod in pods:
|
|
195
|
+
style = {"running": "green", "pending": "yellow", "starting": "yellow"}.get(pod.state, "red")
|
|
196
|
+
created = pod.created_at.strftime("%Y-%m-%d %H:%M") if pod.created_at else ""
|
|
197
|
+
table.add_row(
|
|
198
|
+
pod.id[:12],
|
|
199
|
+
pod.pod_name or "",
|
|
200
|
+
f"[{style}]{pod.state}[/{style}]",
|
|
201
|
+
pod.vnc_url or "",
|
|
202
|
+
created,
|
|
203
|
+
)
|
|
204
|
+
_save_pod_state(pod)
|
|
205
|
+
|
|
206
|
+
console.print()
|
|
207
|
+
console.print(table)
|
|
208
|
+
console.print()
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@app.command("status")
|
|
212
|
+
def status(
|
|
213
|
+
agent_id: str = typer.Argument(..., help="Agent ID (or prefix)"),
|
|
214
|
+
):
|
|
215
|
+
"""Get detailed status of an agent."""
|
|
216
|
+
agent_id = _resolve_agent(agent_id)
|
|
217
|
+
agents = _get_agents_client()
|
|
218
|
+
|
|
219
|
+
try:
|
|
220
|
+
pod = agents.get(agent_id)
|
|
221
|
+
except Exception as e:
|
|
222
|
+
console.print(f"[red]❌ Failed to get agent: {e}[/red]")
|
|
223
|
+
raise typer.Exit(1)
|
|
224
|
+
|
|
225
|
+
_save_pod_state(pod)
|
|
226
|
+
|
|
227
|
+
console.print(f"\n[bold]Agent {pod.id[:12]}[/bold]")
|
|
228
|
+
console.print(f" Pod: {pod.pod_name}")
|
|
229
|
+
console.print(f" State: {pod.state}")
|
|
230
|
+
console.print(f" Desktop: {pod.vnc_url}")
|
|
231
|
+
console.print(f" Shell: {pod.shell_url}")
|
|
232
|
+
console.print(f" Created: {pod.created_at}")
|
|
233
|
+
if pod.started_at:
|
|
234
|
+
console.print(f" Started: {pod.started_at}")
|
|
235
|
+
if pod.stopped_at:
|
|
236
|
+
console.print(f" Stopped: {pod.stopped_at}")
|
|
237
|
+
if pod.jwt_expires_at:
|
|
238
|
+
console.print(f" JWT Expires: {pod.jwt_expires_at}")
|
|
239
|
+
if pod.last_error:
|
|
240
|
+
console.print(f" Error: [red]{pod.last_error}[/red]")
|
|
241
|
+
|
|
242
|
+
if pod.is_running and pod.executor_url:
|
|
243
|
+
try:
|
|
244
|
+
health = agents.health(pod)
|
|
245
|
+
console.print(f"\n[bold]Executor:[/bold]")
|
|
246
|
+
console.print(f" Status: {health.get('status', 'unknown')}")
|
|
247
|
+
console.print(f" Disk Free: {health.get('disk_free_mb', '?')} MB")
|
|
248
|
+
except Exception as e:
|
|
249
|
+
console.print(f"\n[dim]Executor not reachable: {e}[/dim]")
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
@app.command("start")
|
|
253
|
+
def start(
|
|
254
|
+
agent_id: str = typer.Argument(..., help="Agent ID (or prefix)"),
|
|
255
|
+
):
|
|
256
|
+
"""Start a previously stopped agent."""
|
|
257
|
+
agent_id = _resolve_agent(agent_id)
|
|
258
|
+
agents = _get_agents_client()
|
|
259
|
+
|
|
260
|
+
try:
|
|
261
|
+
pod = agents.start(agent_id)
|
|
262
|
+
except Exception as e:
|
|
263
|
+
console.print(f"[red]❌ Failed to start agent: {e}[/red]")
|
|
264
|
+
raise typer.Exit(1)
|
|
265
|
+
|
|
266
|
+
_save_pod_state(pod)
|
|
267
|
+
console.print(f"[green]✓[/green] Agent starting: {pod.pod_name}")
|
|
268
|
+
console.print(f" Desktop: {pod.vnc_url}")
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
@app.command("stop")
|
|
272
|
+
def stop(
|
|
273
|
+
agent_id: str = typer.Argument(..., help="Agent ID (or prefix)"),
|
|
274
|
+
force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation"),
|
|
275
|
+
):
|
|
276
|
+
"""Stop an agent (keeps DB record, destroys pod)."""
|
|
277
|
+
agent_id = _resolve_agent(agent_id)
|
|
278
|
+
|
|
279
|
+
if not force:
|
|
280
|
+
confirm = typer.confirm(f"Stop agent {agent_id[:12]}?")
|
|
281
|
+
if not confirm:
|
|
282
|
+
raise typer.Exit(0)
|
|
283
|
+
|
|
284
|
+
agents = _get_agents_client()
|
|
285
|
+
|
|
286
|
+
try:
|
|
287
|
+
pod = agents.stop(agent_id)
|
|
288
|
+
except Exception as e:
|
|
289
|
+
console.print(f"[red]❌ Failed to stop agent: {e}[/red]")
|
|
290
|
+
raise typer.Exit(1)
|
|
291
|
+
|
|
292
|
+
_save_pod_state(pod)
|
|
293
|
+
console.print(f"[green]✅ Agent stopped[/green]")
|
|
294
|
+
console.print(f"Restart with: [bold]hyper agents start {agent_id[:8]}[/bold]")
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
@app.command("delete")
|
|
298
|
+
def delete(
|
|
299
|
+
agent_id: str = typer.Argument(..., help="Agent ID (or prefix)"),
|
|
300
|
+
force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation"),
|
|
301
|
+
):
|
|
302
|
+
"""Delete an agent entirely (pod + record)."""
|
|
303
|
+
agent_id = _resolve_agent(agent_id)
|
|
304
|
+
|
|
305
|
+
if not force:
|
|
306
|
+
confirm = typer.confirm(f"Permanently delete agent {agent_id[:12]}?")
|
|
307
|
+
if not confirm:
|
|
308
|
+
raise typer.Exit(0)
|
|
309
|
+
|
|
310
|
+
agents = _get_agents_client()
|
|
311
|
+
|
|
312
|
+
try:
|
|
313
|
+
agents.delete(agent_id)
|
|
314
|
+
except Exception as e:
|
|
315
|
+
console.print(f"[red]❌ Failed to delete agent: {e}[/red]")
|
|
316
|
+
raise typer.Exit(1)
|
|
317
|
+
|
|
318
|
+
_remove_pod_state(agent_id)
|
|
319
|
+
console.print(f"[green]✅ Agent {agent_id[:12]} deleted[/green]")
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
@app.command("exec")
|
|
323
|
+
def exec_cmd(
|
|
324
|
+
agent_id: str = typer.Argument(..., help="Agent ID (or prefix)"),
|
|
325
|
+
command: str = typer.Argument(..., help="Command to execute"),
|
|
326
|
+
timeout: int = typer.Option(30, "--timeout", "-t", help="Command timeout (seconds)"),
|
|
327
|
+
):
|
|
328
|
+
"""Execute a command on an agent pod."""
|
|
329
|
+
agent_id = _resolve_agent(agent_id)
|
|
330
|
+
|
|
331
|
+
try:
|
|
332
|
+
pod = _get_pod_with_token(agent_id)
|
|
333
|
+
except Exception as e:
|
|
334
|
+
console.print(f"[red]❌ Failed to get agent: {e}[/red]")
|
|
335
|
+
raise typer.Exit(1)
|
|
336
|
+
|
|
337
|
+
agents = _get_agents_client()
|
|
338
|
+
|
|
339
|
+
try:
|
|
340
|
+
result = agents.exec(pod, command, timeout=timeout)
|
|
341
|
+
except Exception as e:
|
|
342
|
+
console.print(f"[red]❌ Exec failed: {e}[/red]")
|
|
343
|
+
raise typer.Exit(1)
|
|
344
|
+
|
|
345
|
+
if result.stdout:
|
|
346
|
+
sys.stdout.write(result.stdout)
|
|
347
|
+
if not result.stdout.endswith("\n"):
|
|
348
|
+
sys.stdout.write("\n")
|
|
349
|
+
if result.stderr:
|
|
350
|
+
sys.stderr.write(result.stderr)
|
|
351
|
+
if not result.stderr.endswith("\n"):
|
|
352
|
+
sys.stderr.write("\n")
|
|
353
|
+
|
|
354
|
+
raise typer.Exit(result.exit_code)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
@app.command("shell")
|
|
358
|
+
def shell(
|
|
359
|
+
agent_id: str = typer.Argument(..., help="Agent ID (or prefix)"),
|
|
360
|
+
):
|
|
361
|
+
"""Open an interactive shell on an agent pod (WebSocket PTY).
|
|
362
|
+
|
|
363
|
+
Connects to the executor's /shell endpoint. Press Ctrl+] to disconnect.
|
|
364
|
+
"""
|
|
365
|
+
agent_id = _resolve_agent(agent_id)
|
|
366
|
+
|
|
367
|
+
try:
|
|
368
|
+
pod = _get_pod_with_token(agent_id)
|
|
369
|
+
except Exception as e:
|
|
370
|
+
console.print(f"[red]❌ Failed to get agent: {e}[/red]")
|
|
371
|
+
raise typer.Exit(1)
|
|
372
|
+
|
|
373
|
+
if not pod.executor_url:
|
|
374
|
+
console.print("[red]❌ Agent has no executor URL[/red]")
|
|
375
|
+
raise typer.Exit(1)
|
|
376
|
+
|
|
377
|
+
ws_url = pod.executor_url.replace("https://", "wss://").replace("http://", "ws://")
|
|
378
|
+
ws_url = f"{ws_url}/shell"
|
|
379
|
+
|
|
380
|
+
console.print(f"[dim]Connecting to {ws_url}...[/dim]")
|
|
381
|
+
|
|
382
|
+
try:
|
|
383
|
+
import websockets
|
|
384
|
+
import asyncio
|
|
385
|
+
import termios
|
|
386
|
+
import tty
|
|
387
|
+
except ImportError:
|
|
388
|
+
console.print("[red]❌ 'websockets' required: pip install websockets[/red]")
|
|
389
|
+
raise typer.Exit(1)
|
|
390
|
+
|
|
391
|
+
async def _run_shell():
|
|
392
|
+
headers = {}
|
|
393
|
+
if pod.jwt_token:
|
|
394
|
+
headers["Authorization"] = f"Bearer {pod.jwt_token}"
|
|
395
|
+
headers["Cookie"] = f"{pod.pod_name}-token={pod.jwt_token}"
|
|
396
|
+
|
|
397
|
+
async with websockets.connect(ws_url, additional_headers=headers) as ws:
|
|
398
|
+
console.print("[green]Connected.[/green] Ctrl+] to disconnect.\n")
|
|
399
|
+
|
|
400
|
+
old_settings = termios.tcgetattr(sys.stdin)
|
|
401
|
+
try:
|
|
402
|
+
tty.setraw(sys.stdin.fileno())
|
|
403
|
+
|
|
404
|
+
import shutil
|
|
405
|
+
cols, rows = shutil.get_terminal_size()
|
|
406
|
+
await ws.send(f"\x1b[8;{rows};{cols}t")
|
|
407
|
+
|
|
408
|
+
async def read_ws():
|
|
409
|
+
try:
|
|
410
|
+
async for msg in ws:
|
|
411
|
+
if isinstance(msg, str):
|
|
412
|
+
sys.stdout.write(msg)
|
|
413
|
+
sys.stdout.flush()
|
|
414
|
+
elif isinstance(msg, bytes):
|
|
415
|
+
sys.stdout.buffer.write(msg)
|
|
416
|
+
sys.stdout.buffer.flush()
|
|
417
|
+
except websockets.ConnectionClosed:
|
|
418
|
+
pass
|
|
419
|
+
|
|
420
|
+
async def read_stdin():
|
|
421
|
+
loop = asyncio.get_event_loop()
|
|
422
|
+
try:
|
|
423
|
+
while True:
|
|
424
|
+
data = await loop.run_in_executor(None, lambda: os.read(sys.stdin.fileno(), 1024))
|
|
425
|
+
if not data:
|
|
426
|
+
break
|
|
427
|
+
if b"\x1d" in data:
|
|
428
|
+
break
|
|
429
|
+
await ws.send(data.decode(errors="replace"))
|
|
430
|
+
except (websockets.ConnectionClosed, OSError):
|
|
431
|
+
pass
|
|
432
|
+
|
|
433
|
+
done, pending = await asyncio.wait(
|
|
434
|
+
[asyncio.create_task(read_ws()), asyncio.create_task(read_stdin())],
|
|
435
|
+
return_when=asyncio.FIRST_COMPLETED,
|
|
436
|
+
)
|
|
437
|
+
for t in pending:
|
|
438
|
+
t.cancel()
|
|
439
|
+
finally:
|
|
440
|
+
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_settings)
|
|
441
|
+
console.print("\n[dim]Disconnected.[/dim]")
|
|
442
|
+
|
|
443
|
+
try:
|
|
444
|
+
asyncio.run(_run_shell())
|
|
445
|
+
except KeyboardInterrupt:
|
|
446
|
+
console.print("\n[dim]Disconnected.[/dim]")
|
|
447
|
+
except Exception as e:
|
|
448
|
+
console.print(f"[red]❌ Shell failed: {e}[/red]")
|
|
449
|
+
raise typer.Exit(1)
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
@app.command("logs")
|
|
453
|
+
def logs(
|
|
454
|
+
agent_id: str = typer.Argument(..., help="Agent ID (or prefix)"),
|
|
455
|
+
lines: int = typer.Option(100, "-n", "--lines", help="Number of lines to show"),
|
|
456
|
+
follow: bool = typer.Option(True, "-f/--no-follow", help="Follow log output"),
|
|
457
|
+
):
|
|
458
|
+
"""Stream logs from an agent pod."""
|
|
459
|
+
agent_id = _resolve_agent(agent_id)
|
|
460
|
+
|
|
461
|
+
try:
|
|
462
|
+
pod = _get_pod_with_token(agent_id)
|
|
463
|
+
except Exception as e:
|
|
464
|
+
console.print(f"[red]❌ Failed to get agent: {e}[/red]")
|
|
465
|
+
raise typer.Exit(1)
|
|
466
|
+
|
|
467
|
+
agents = _get_agents_client()
|
|
468
|
+
|
|
469
|
+
try:
|
|
470
|
+
for line in agents.logs_stream(pod, lines=lines, follow=follow):
|
|
471
|
+
console.print(line)
|
|
472
|
+
except KeyboardInterrupt:
|
|
473
|
+
pass
|
|
474
|
+
except Exception as e:
|
|
475
|
+
console.print(f"[red]❌ Logs failed: {e}[/red]")
|
|
476
|
+
raise typer.Exit(1)
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
@app.command("chat")
|
|
480
|
+
def chat(
|
|
481
|
+
agent_id: str = typer.Argument(..., help="Agent ID (or prefix)"),
|
|
482
|
+
model: str = typer.Option("hyperclaw/kimi-k2.5", "--model", "-m", help="Model to use"),
|
|
483
|
+
):
|
|
484
|
+
"""Interactive chat with an agent's OpenClaw instance.
|
|
485
|
+
|
|
486
|
+
Connects to the OpenClaw gateway running inside the agent pod.
|
|
487
|
+
Type your messages, get streaming responses. Ctrl+C or 'exit' to quit.
|
|
488
|
+
"""
|
|
489
|
+
agent_id = _resolve_agent(agent_id)
|
|
490
|
+
|
|
491
|
+
try:
|
|
492
|
+
pod = _get_pod_with_token(agent_id)
|
|
493
|
+
except Exception as e:
|
|
494
|
+
console.print(f"[red]❌ Failed to get agent: {e}[/red]")
|
|
495
|
+
raise typer.Exit(1)
|
|
496
|
+
|
|
497
|
+
agents = _get_agents_client()
|
|
498
|
+
messages = []
|
|
499
|
+
|
|
500
|
+
console.print(f"\n[bold]Chat with agent {pod.pod_name}[/bold] (model: {model})")
|
|
501
|
+
console.print("[dim]Type your message. 'exit' or Ctrl+C to quit.[/dim]\n")
|
|
502
|
+
|
|
503
|
+
while True:
|
|
504
|
+
try:
|
|
505
|
+
user_input = console.input("[bold cyan]> [/bold cyan]")
|
|
506
|
+
except (EOFError, KeyboardInterrupt):
|
|
507
|
+
console.print("\n[dim]Bye.[/dim]")
|
|
508
|
+
break
|
|
509
|
+
|
|
510
|
+
user_input = user_input.strip()
|
|
511
|
+
if not user_input:
|
|
512
|
+
continue
|
|
513
|
+
if user_input.lower() in ("exit", "quit", "/exit", "/quit"):
|
|
514
|
+
console.print("[dim]Bye.[/dim]")
|
|
515
|
+
break
|
|
516
|
+
|
|
517
|
+
messages.append({"role": "user", "content": user_input})
|
|
518
|
+
|
|
519
|
+
try:
|
|
520
|
+
full_response = ""
|
|
521
|
+
for chunk in agents.chat_stream(pod, messages, model=model):
|
|
522
|
+
sys.stdout.write(chunk)
|
|
523
|
+
sys.stdout.flush()
|
|
524
|
+
full_response += chunk
|
|
525
|
+
sys.stdout.write("\n\n")
|
|
526
|
+
sys.stdout.flush()
|
|
527
|
+
|
|
528
|
+
messages.append({"role": "assistant", "content": full_response})
|
|
529
|
+
except KeyboardInterrupt:
|
|
530
|
+
sys.stdout.write("\n")
|
|
531
|
+
continue
|
|
532
|
+
except Exception as e:
|
|
533
|
+
console.print(f"\n[red]Error: {e}[/red]\n")
|
|
534
|
+
# Remove failed user message
|
|
535
|
+
messages.pop()
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
@app.command("token")
|
|
539
|
+
def token(
|
|
540
|
+
agent_id: str = typer.Argument(..., help="Agent ID (or prefix)"),
|
|
541
|
+
):
|
|
542
|
+
"""Refresh the JWT token for an agent."""
|
|
543
|
+
agent_id = _resolve_agent(agent_id)
|
|
544
|
+
agents = _get_agents_client()
|
|
545
|
+
|
|
546
|
+
try:
|
|
547
|
+
result = agents.refresh_token(agent_id)
|
|
548
|
+
except Exception as e:
|
|
549
|
+
console.print(f"[red]❌ Failed to refresh token: {e}[/red]")
|
|
550
|
+
raise typer.Exit(1)
|
|
551
|
+
|
|
552
|
+
state = _load_state()
|
|
553
|
+
if agent_id in state:
|
|
554
|
+
state[agent_id]["jwt_token"] = result.get("token", "")
|
|
555
|
+
with open(AGENTS_STATE, "w") as f:
|
|
556
|
+
json.dump(state, f, indent=2, default=str)
|
|
557
|
+
|
|
558
|
+
console.print(f"[green]✅ Token refreshed[/green]")
|
|
559
|
+
console.print(f" Expires: {result.get('expires_at', 'unknown')}")
|
|
@@ -8,12 +8,14 @@ from rich.console import Console
|
|
|
8
8
|
from rich.table import Table
|
|
9
9
|
|
|
10
10
|
from .onboard import onboard as _onboard_fn
|
|
11
|
+
from .voice import app as voice_app
|
|
11
12
|
|
|
12
13
|
app = typer.Typer(help="HyperClaw inference commands")
|
|
13
14
|
console = Console()
|
|
14
15
|
|
|
15
|
-
# Register
|
|
16
|
+
# Register subcommands
|
|
16
17
|
app.command("onboard")(_onboard_fn)
|
|
18
|
+
app.add_typer(voice_app, name="voice")
|
|
17
19
|
|
|
18
20
|
# Check if wallet dependencies are available
|
|
19
21
|
try:
|
|
@@ -194,7 +196,19 @@ async def _subscribe_async(account, plan_id: str, api_base: str, amount: str = N
|
|
|
194
196
|
payment_headers = http_client.encode_payment_signature_header(payment_payload)
|
|
195
197
|
console.print(f"[green]✓[/green] Payment signed")
|
|
196
198
|
|
|
197
|
-
# Step 6: Retry with payment
|
|
199
|
+
# Step 6: Retry with payment (include JWT if available from claw login)
|
|
200
|
+
jwt_path = HYPERCLI_DIR / "claw-jwt.json"
|
|
201
|
+
if jwt_path.exists():
|
|
202
|
+
try:
|
|
203
|
+
with open(jwt_path) as f:
|
|
204
|
+
jwt_data = json.load(f)
|
|
205
|
+
jwt_token = jwt_data.get("token", "")
|
|
206
|
+
if jwt_token:
|
|
207
|
+
payment_headers["Authorization"] = f"Bearer {jwt_token}"
|
|
208
|
+
console.print("[green]✓[/green] Attaching user auth (from claw login)")
|
|
209
|
+
except Exception:
|
|
210
|
+
pass
|
|
211
|
+
|
|
198
212
|
console.print("[bold]Sending payment...[/bold]")
|
|
199
213
|
retry_response = await http.post(url, headers=payment_headers)
|
|
200
214
|
|
|
@@ -355,6 +369,134 @@ def models(
|
|
|
355
369
|
console.print(f"Source: {url}")
|
|
356
370
|
|
|
357
371
|
|
|
372
|
+
@app.command("login")
|
|
373
|
+
def login(
|
|
374
|
+
api_url: str = typer.Option(None, "--api-url", help="API base URL override"),
|
|
375
|
+
):
|
|
376
|
+
"""Login to HyperClaw with your wallet.
|
|
377
|
+
|
|
378
|
+
Signs a challenge message with your wallet key to authenticate,
|
|
379
|
+
then creates a user-bound API key for agent management.
|
|
380
|
+
|
|
381
|
+
Prerequisite: hyper wallet create (if you don't have a wallet yet)
|
|
382
|
+
|
|
383
|
+
Flow:
|
|
384
|
+
1. Signs a challenge with your wallet private key
|
|
385
|
+
2. Backend verifies signature, creates/finds your user
|
|
386
|
+
3. Creates an API key bound to your user account
|
|
387
|
+
4. Saves the key to ~/.hypercli/claw-key.json
|
|
388
|
+
|
|
389
|
+
After login, you can use:
|
|
390
|
+
hyper agents create Launch an OpenClaw agent pod
|
|
391
|
+
hyper agents list List your agents
|
|
392
|
+
hyper claw config Generate provider configs
|
|
393
|
+
"""
|
|
394
|
+
try:
|
|
395
|
+
from eth_account.messages import encode_defunct
|
|
396
|
+
from eth_account import Account
|
|
397
|
+
except ImportError:
|
|
398
|
+
console.print("[red]❌ Wallet dependencies required[/red]")
|
|
399
|
+
console.print("Install with: [bold]pip install 'hypercli-cli[wallet]'[/bold]")
|
|
400
|
+
raise typer.Exit(1)
|
|
401
|
+
|
|
402
|
+
import httpx
|
|
403
|
+
|
|
404
|
+
# Import wallet loader from wallet module
|
|
405
|
+
from .wallet import load_wallet
|
|
406
|
+
|
|
407
|
+
base_url = (api_url or PROD_API_BASE).rstrip("/")
|
|
408
|
+
|
|
409
|
+
# Step 1: Load wallet
|
|
410
|
+
account = load_wallet()
|
|
411
|
+
console.print(f"\n[green]✓[/green] Wallet: [bold]{account.address}[/bold]\n")
|
|
412
|
+
|
|
413
|
+
# Step 2: Get challenge
|
|
414
|
+
console.print("[bold]Requesting challenge...[/bold]")
|
|
415
|
+
with httpx.Client(timeout=15) as client:
|
|
416
|
+
resp = client.post(
|
|
417
|
+
f"{base_url}/api/auth/wallet/challenge",
|
|
418
|
+
json={"wallet": account.address},
|
|
419
|
+
)
|
|
420
|
+
if resp.status_code != 200:
|
|
421
|
+
console.print(f"[red]❌ Challenge failed: {resp.text}[/red]")
|
|
422
|
+
raise typer.Exit(1)
|
|
423
|
+
challenge = resp.json()
|
|
424
|
+
|
|
425
|
+
# Step 3: Sign
|
|
426
|
+
console.print("[bold]Signing...[/bold]")
|
|
427
|
+
message = encode_defunct(text=challenge["message"])
|
|
428
|
+
signed = account.sign_message(message)
|
|
429
|
+
|
|
430
|
+
# Step 4: Verify signature and login
|
|
431
|
+
console.print("[bold]Authenticating...[/bold]")
|
|
432
|
+
with httpx.Client(timeout=15) as client:
|
|
433
|
+
resp = client.post(
|
|
434
|
+
f"{base_url}/api/auth/wallet/login",
|
|
435
|
+
json={
|
|
436
|
+
"wallet": account.address,
|
|
437
|
+
"signature": signed.signature.hex(),
|
|
438
|
+
"timestamp": challenge["timestamp"],
|
|
439
|
+
},
|
|
440
|
+
)
|
|
441
|
+
if resp.status_code != 200:
|
|
442
|
+
console.print(f"[red]❌ Login failed: {resp.text}[/red]")
|
|
443
|
+
raise typer.Exit(1)
|
|
444
|
+
login_data = resp.json()
|
|
445
|
+
jwt_token = login_data["token"]
|
|
446
|
+
|
|
447
|
+
console.print("[green]✓[/green] Authenticated\n")
|
|
448
|
+
|
|
449
|
+
user_id = login_data.get("user_id", "")
|
|
450
|
+
team_id = login_data.get("team_id", "")
|
|
451
|
+
wallet_addr = login_data.get("wallet_address", account.address)
|
|
452
|
+
|
|
453
|
+
# Step 5: Create a claw API key using the JWT
|
|
454
|
+
console.print("[bold]Creating API key...[/bold]")
|
|
455
|
+
with httpx.Client(timeout=15) as client:
|
|
456
|
+
resp = client.post(
|
|
457
|
+
f"{base_url}/api/keys",
|
|
458
|
+
json={"name": "claw-cli"},
|
|
459
|
+
headers={"Authorization": f"Bearer {jwt_token}"},
|
|
460
|
+
)
|
|
461
|
+
if resp.status_code != 200:
|
|
462
|
+
# Save JWT anyway so user can still auth
|
|
463
|
+
jwt_path = HYPERCLI_DIR / "claw-jwt.json"
|
|
464
|
+
HYPERCLI_DIR.mkdir(parents=True, exist_ok=True)
|
|
465
|
+
with open(jwt_path, "w") as f:
|
|
466
|
+
json.dump({"token": jwt_token, "user_id": user_id, "team_id": team_id}, f, indent=2)
|
|
467
|
+
console.print(f"[yellow]⚠ Key creation failed: {resp.text}[/yellow]")
|
|
468
|
+
console.print(f"[green]✓[/green] JWT saved to {jwt_path} (use for direct auth)")
|
|
469
|
+
raise typer.Exit(1)
|
|
470
|
+
|
|
471
|
+
key_data = resp.json()
|
|
472
|
+
|
|
473
|
+
api_key = key_data.get("api_key", key_data.get("key", ""))
|
|
474
|
+
|
|
475
|
+
# Step 6: Save as claw key
|
|
476
|
+
HYPERCLI_DIR.mkdir(parents=True, exist_ok=True)
|
|
477
|
+
claw_key_data = {
|
|
478
|
+
"key": api_key,
|
|
479
|
+
"plan_id": login_data.get("plan_id", "free"),
|
|
480
|
+
"user_id": user_id,
|
|
481
|
+
"team_id": team_id,
|
|
482
|
+
"wallet_address": wallet_addr,
|
|
483
|
+
"tpm_limit": 0,
|
|
484
|
+
"rpm_limit": 0,
|
|
485
|
+
"expires_at": "",
|
|
486
|
+
}
|
|
487
|
+
with open(CLAW_KEY_PATH, "w") as f:
|
|
488
|
+
json.dump(claw_key_data, f, indent=2)
|
|
489
|
+
|
|
490
|
+
console.print(f"[green]✓[/green] API key saved to [bold]{CLAW_KEY_PATH}[/bold]\n")
|
|
491
|
+
console.print(f" User: {user_id[:12]}...")
|
|
492
|
+
console.print(f" Team: {team_id[:12]}...")
|
|
493
|
+
console.print(f" Key: {api_key[:20]}...")
|
|
494
|
+
console.print(f" Wallet: {wallet_addr}")
|
|
495
|
+
console.print(f"\n[green]You're all set![/green]")
|
|
496
|
+
console.print(f" Launch agent: [bold]hyper agents create[/bold]")
|
|
497
|
+
console.print(f" Configure: [bold]hyper claw config openclaw --apply[/bold]")
|
|
498
|
+
|
|
499
|
+
|
|
358
500
|
OPENCLAW_CONFIG_PATH = Path.home() / ".openclaw" / "openclaw.json"
|
|
359
501
|
|
|
360
502
|
|
|
@@ -7,7 +7,7 @@ from rich.prompt import Prompt
|
|
|
7
7
|
from hypercli import HyperCLI, APIError, configure
|
|
8
8
|
from hypercli.config import CONFIG_FILE
|
|
9
9
|
|
|
10
|
-
from . import billing, claw, comfyui, flow, instances, jobs, keys, user, wallet
|
|
10
|
+
from . import agents, billing, claw, comfyui, flow, instances, jobs, keys, user, wallet
|
|
11
11
|
|
|
12
12
|
console = Console()
|
|
13
13
|
|
|
@@ -56,6 +56,7 @@ app = typer.Typer(
|
|
|
56
56
|
)
|
|
57
57
|
|
|
58
58
|
# Register subcommands
|
|
59
|
+
app.add_typer(agents.app, name="agents")
|
|
59
60
|
app.add_typer(billing.app, name="billing")
|
|
60
61
|
app.add_typer(claw.app, name="claw")
|
|
61
62
|
app.add_typer(comfyui.app, name="comfyui")
|
|
@@ -195,6 +195,7 @@ def launch(
|
|
|
195
195
|
lb_auth: bool = typer.Option(False, "--lb-auth", help="Enable auth on load balancer"),
|
|
196
196
|
registry_user: Optional[str] = typer.Option(None, "--registry-user", help="Private registry username"),
|
|
197
197
|
registry_password: Optional[str] = typer.Option(None, "--registry-password", help="Private registry password"),
|
|
198
|
+
dockerfile: Optional[str] = typer.Option(None, "--dockerfile", "-d", help="Path to Dockerfile (built on GPU node, overrides image as base)"),
|
|
198
199
|
x402: bool = typer.Option(False, "--x402", help="Pay per-use via embedded x402 wallet"),
|
|
199
200
|
amount: Optional[float] = typer.Option(None, "--amount", help="USDC amount to spend with --x402"),
|
|
200
201
|
follow: bool = typer.Option(False, "--follow", "-f", help="Follow logs after creation"),
|
|
@@ -234,6 +235,16 @@ def launch(
|
|
|
234
235
|
if registry_user and registry_password:
|
|
235
236
|
registry_auth = {"username": registry_user, "password": registry_password}
|
|
236
237
|
|
|
238
|
+
# Read and base64-encode Dockerfile if provided
|
|
239
|
+
dockerfile_b64 = None
|
|
240
|
+
if dockerfile:
|
|
241
|
+
import base64
|
|
242
|
+
from pathlib import Path
|
|
243
|
+
df_path = Path(dockerfile)
|
|
244
|
+
if not df_path.exists():
|
|
245
|
+
raise typer.BadParameter(f"Dockerfile not found: {dockerfile}")
|
|
246
|
+
dockerfile_b64 = base64.b64encode(df_path.read_bytes()).decode()
|
|
247
|
+
|
|
237
248
|
# Auto-wrap command in sh -c if it contains shell operators
|
|
238
249
|
if command and any(op in command for op in ["&&", "||", "|", ";", ">", "<", "$"]):
|
|
239
250
|
command = f'sh -c "{command}"'
|
|
@@ -310,6 +321,7 @@ def launch(
|
|
|
310
321
|
ports=ports_dict,
|
|
311
322
|
auth=lb_auth,
|
|
312
323
|
registry_auth=registry_auth,
|
|
324
|
+
dockerfile=dockerfile_b64,
|
|
313
325
|
)
|
|
314
326
|
|
|
315
327
|
if fmt == "json":
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""HyperClaw Voice API commands — TTS, clone, design"""
|
|
2
|
+
import base64
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
import typer
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
|
|
11
|
+
app = typer.Typer(help="Voice API — text-to-speech, voice cloning, voice design")
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
HYPERCLI_DIR = Path.home() / ".hypercli"
|
|
15
|
+
CLAW_KEY_PATH = HYPERCLI_DIR / "claw-key.json"
|
|
16
|
+
PROD_API_BASE = "https://api.hyperclaw.app"
|
|
17
|
+
DEV_API_BASE = "https://dev-api.hyperclaw.app"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _get_api_key(key: str | None) -> str:
|
|
21
|
+
"""Resolve API key from flag or saved claw key."""
|
|
22
|
+
if key:
|
|
23
|
+
return key
|
|
24
|
+
if CLAW_KEY_PATH.exists():
|
|
25
|
+
with open(CLAW_KEY_PATH) as f:
|
|
26
|
+
k = json.load(f).get("key", "")
|
|
27
|
+
if k:
|
|
28
|
+
return k
|
|
29
|
+
console.print("[red]❌ No API key found.[/red]")
|
|
30
|
+
console.print("Pass [bold]--key sk-...[/bold] or run [bold]hyper claw subscribe[/bold]")
|
|
31
|
+
raise typer.Exit(1)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _post_voice(
|
|
35
|
+
endpoint: str,
|
|
36
|
+
payload: dict,
|
|
37
|
+
api_key: str,
|
|
38
|
+
output: Path,
|
|
39
|
+
dev: bool = False,
|
|
40
|
+
):
|
|
41
|
+
"""POST to voice endpoint and save audio output."""
|
|
42
|
+
api_base = DEV_API_BASE if dev else PROD_API_BASE
|
|
43
|
+
url = f"{api_base}/voice/{endpoint}"
|
|
44
|
+
|
|
45
|
+
console.print(f"[dim]→ POST {url}[/dim]")
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
with httpx.Client(timeout=600.0) as client:
|
|
49
|
+
resp = client.post(
|
|
50
|
+
url,
|
|
51
|
+
json=payload,
|
|
52
|
+
headers={
|
|
53
|
+
"Authorization": f"Bearer {api_key}",
|
|
54
|
+
"Content-Type": "application/json",
|
|
55
|
+
},
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
if resp.status_code != 200:
|
|
59
|
+
console.print(f"[red]❌ {resp.status_code}: {resp.text[:500]}[/red]")
|
|
60
|
+
raise typer.Exit(1)
|
|
61
|
+
|
|
62
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
63
|
+
with open(output, "wb") as f:
|
|
64
|
+
f.write(resp.content)
|
|
65
|
+
|
|
66
|
+
size_kb = len(resp.content) / 1024
|
|
67
|
+
console.print(f"[green]✅ Saved {output} ({size_kb:.1f} KB)[/green]")
|
|
68
|
+
|
|
69
|
+
except httpx.HTTPError as e:
|
|
70
|
+
console.print(f"[red]❌ Request failed: {e}[/red]")
|
|
71
|
+
raise typer.Exit(1)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@app.command("tts")
|
|
75
|
+
def tts(
|
|
76
|
+
text: str = typer.Argument(..., help="Text to synthesize"),
|
|
77
|
+
voice: str = typer.Option("Chelsie", "--voice", "-v", help="Voice name (CustomVoice preset)"),
|
|
78
|
+
language: str = typer.Option("auto", "--language", "-l", help="Language: auto, english, chinese, etc."),
|
|
79
|
+
output: Path = typer.Option("output.wav", "--output", "-o", help="Output audio file"),
|
|
80
|
+
key: str = typer.Option(None, "--key", "-k", help="API key (sk-...)"),
|
|
81
|
+
dev: bool = typer.Option(False, "--dev", help="Use dev API"),
|
|
82
|
+
):
|
|
83
|
+
"""Generate speech from text using a preset voice.
|
|
84
|
+
|
|
85
|
+
Examples:
|
|
86
|
+
hyper claw voice tts "Hello world"
|
|
87
|
+
hyper claw voice tts "Bonjour" -v Etienne -l french -o hello.wav
|
|
88
|
+
"""
|
|
89
|
+
api_key = _get_api_key(key)
|
|
90
|
+
payload = {
|
|
91
|
+
"text": text,
|
|
92
|
+
"voice": voice,
|
|
93
|
+
"language": language,
|
|
94
|
+
}
|
|
95
|
+
_post_voice("tts", payload, api_key, output, dev)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@app.command("clone")
|
|
99
|
+
def clone(
|
|
100
|
+
text: str = typer.Argument(..., help="Text to synthesize"),
|
|
101
|
+
ref_audio: Path = typer.Option(..., "--ref", "-r", help="Reference audio file (wav/mp3/ogg)"),
|
|
102
|
+
language: str = typer.Option("auto", "--language", "-l", help="Language: auto, english, chinese, etc."),
|
|
103
|
+
x_vector_only: bool = typer.Option(True, "--x-vector-only/--full-clone", help="Use x_vector_only mode (recommended)"),
|
|
104
|
+
output: Path = typer.Option("output.wav", "--output", "-o", help="Output audio file"),
|
|
105
|
+
key: str = typer.Option(None, "--key", "-k", help="API key (sk-...)"),
|
|
106
|
+
dev: bool = typer.Option(False, "--dev", help="Use dev API"),
|
|
107
|
+
):
|
|
108
|
+
"""Clone a voice from reference audio.
|
|
109
|
+
|
|
110
|
+
Examples:
|
|
111
|
+
hyper claw voice clone "Hello" --ref voice.wav
|
|
112
|
+
hyper claw voice clone "Test" -r ref.wav -l english -o cloned.wav
|
|
113
|
+
"""
|
|
114
|
+
api_key = _get_api_key(key)
|
|
115
|
+
|
|
116
|
+
if not ref_audio.exists():
|
|
117
|
+
console.print(f"[red]❌ Reference audio not found: {ref_audio}[/red]")
|
|
118
|
+
raise typer.Exit(1)
|
|
119
|
+
|
|
120
|
+
with open(ref_audio, "rb") as f:
|
|
121
|
+
ref_b64 = base64.b64encode(f.read()).decode()
|
|
122
|
+
|
|
123
|
+
console.print(f"[dim]Reference: {ref_audio} ({ref_audio.stat().st_size / 1024:.1f} KB)[/dim]")
|
|
124
|
+
|
|
125
|
+
payload = {
|
|
126
|
+
"text": text,
|
|
127
|
+
"ref_audio_base64": ref_b64,
|
|
128
|
+
"language": language,
|
|
129
|
+
"x_vector_only": x_vector_only,
|
|
130
|
+
}
|
|
131
|
+
_post_voice("clone", payload, api_key, output, dev)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@app.command("design")
|
|
135
|
+
def design(
|
|
136
|
+
text: str = typer.Argument(..., help="Text to synthesize"),
|
|
137
|
+
description: str = typer.Option(..., "--desc", "-d", help="Voice description (e.g. 'young female, warm, American accent')"),
|
|
138
|
+
language: str = typer.Option("auto", "--language", "-l", help="Language: auto, english, chinese, etc."),
|
|
139
|
+
output: Path = typer.Option("output.wav", "--output", "-o", help="Output audio file"),
|
|
140
|
+
key: str = typer.Option(None, "--key", "-k", help="API key (sk-...)"),
|
|
141
|
+
dev: bool = typer.Option(False, "--dev", help="Use dev API"),
|
|
142
|
+
):
|
|
143
|
+
"""Design a voice from a text description.
|
|
144
|
+
|
|
145
|
+
Examples:
|
|
146
|
+
hyper claw voice design "Hello" --desc "deep male voice, British accent"
|
|
147
|
+
hyper claw voice design "Test" -d "young woman, cheerful" -o designed.wav
|
|
148
|
+
"""
|
|
149
|
+
api_key = _get_api_key(key)
|
|
150
|
+
payload = {
|
|
151
|
+
"text": text,
|
|
152
|
+
"instruct": description,
|
|
153
|
+
"language": language,
|
|
154
|
+
}
|
|
155
|
+
_post_voice("design", payload, api_key, output, dev)
|
|
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
|