hypercli-sdk 1.0.0__tar.gz → 1.0.2__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 (29) hide show
  1. {hypercli_sdk-1.0.0 → hypercli_sdk-1.0.2}/PKG-INFO +1 -1
  2. {hypercli_sdk-1.0.0 → hypercli_sdk-1.0.2}/hypercli/__init__.py +10 -0
  3. hypercli_sdk-1.0.2/hypercli/agents.py +537 -0
  4. {hypercli_sdk-1.0.0 → hypercli_sdk-1.0.2}/hypercli/client.py +2 -0
  5. hypercli_sdk-1.0.2/hypercli/gateway.py +490 -0
  6. {hypercli_sdk-1.0.0 → hypercli_sdk-1.0.2}/hypercli/jobs.py +71 -0
  7. {hypercli_sdk-1.0.0 → hypercli_sdk-1.0.2}/pyproject.toml +1 -1
  8. hypercli_sdk-1.0.2/tests/test_agents.py +525 -0
  9. {hypercli_sdk-1.0.0 → hypercli_sdk-1.0.2}/.gitignore +0 -0
  10. {hypercli_sdk-1.0.0 → hypercli_sdk-1.0.2}/README.md +0 -0
  11. {hypercli_sdk-1.0.0 → hypercli_sdk-1.0.2}/hypercli/billing.py +0 -0
  12. {hypercli_sdk-1.0.0 → hypercli_sdk-1.0.2}/hypercli/claw.py +0 -0
  13. {hypercli_sdk-1.0.0 → hypercli_sdk-1.0.2}/hypercli/config.py +0 -0
  14. {hypercli_sdk-1.0.0 → hypercli_sdk-1.0.2}/hypercli/files.py +0 -0
  15. {hypercli_sdk-1.0.0 → hypercli_sdk-1.0.2}/hypercli/http.py +0 -0
  16. {hypercli_sdk-1.0.0 → hypercli_sdk-1.0.2}/hypercli/instances.py +0 -0
  17. {hypercli_sdk-1.0.0 → hypercli_sdk-1.0.2}/hypercli/job/__init__.py +0 -0
  18. {hypercli_sdk-1.0.0 → hypercli_sdk-1.0.2}/hypercli/job/base.py +0 -0
  19. {hypercli_sdk-1.0.0 → hypercli_sdk-1.0.2}/hypercli/job/comfyui.py +0 -0
  20. {hypercli_sdk-1.0.0 → hypercli_sdk-1.0.2}/hypercli/job/gradio.py +0 -0
  21. {hypercli_sdk-1.0.0 → hypercli_sdk-1.0.2}/hypercli/keys.py +0 -0
  22. {hypercli_sdk-1.0.0 → hypercli_sdk-1.0.2}/hypercli/logs.py +0 -0
  23. {hypercli_sdk-1.0.0 → hypercli_sdk-1.0.2}/hypercli/renders.py +0 -0
  24. {hypercli_sdk-1.0.0 → hypercli_sdk-1.0.2}/hypercli/shell.py +0 -0
  25. {hypercli_sdk-1.0.0 → hypercli_sdk-1.0.2}/hypercli/user.py +0 -0
  26. {hypercli_sdk-1.0.0 → hypercli_sdk-1.0.2}/hypercli/x402.py +0 -0
  27. {hypercli_sdk-1.0.0 → hypercli_sdk-1.0.2}/tests/test_apply_params.py +0 -0
  28. {hypercli_sdk-1.0.0 → hypercli_sdk-1.0.2}/tests/test_claw.py +0 -0
  29. {hypercli_sdk-1.0.0 → hypercli_sdk-1.0.2}/tests/test_graph_to_api.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hypercli-sdk
3
- Version: 1.0.0
3
+ Version: 1.0.2
4
4
  Summary: Python SDK 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,10 @@ from .x402 import X402Client, X402JobLaunch, X402FlowCreate, X402RenderCreate, F
9
9
  from .files import File, AsyncFiles
10
10
  from .job import BaseJob, ComfyUIJob, GradioJob, apply_params, apply_graph_modes, find_node, find_nodes, load_template, graph_to_api, expand_subgraphs, DEFAULT_OBJECT_INFO
11
11
  from .logs import LogStream, stream_logs, fetch_logs
12
+ from .agents import Agents, ReefPod, ExecResult
12
13
  from .shell import ShellSession, shell_connect
13
14
  from .claw import Claw, ClawKey, ClawPlan, ClawModel
15
+ from .gateway import GatewayClient, GatewayError, ChatEvent
14
16
 
15
17
  __version__ = "1.0.0"
16
18
  __all__ = [
@@ -65,6 +67,10 @@ __all__ = [
65
67
  "LogStream",
66
68
  "stream_logs",
67
69
  "fetch_logs",
70
+ # Agents (Reef Pods)
71
+ "Agents",
72
+ "ReefPod",
73
+ "ExecResult",
68
74
  # Shell
69
75
  "ShellSession",
70
76
  "shell_connect",
@@ -73,4 +79,8 @@ __all__ = [
73
79
  "ClawKey",
74
80
  "ClawPlan",
75
81
  "ClawModel",
82
+ # OpenClaw Gateway
83
+ "GatewayClient",
84
+ "GatewayError",
85
+ "ChatEvent",
76
86
  ]
@@ -0,0 +1,537 @@
1
+ """
2
+ HyperClaw Agents API — Reef Pod Management
3
+
4
+ Client for HyperClaw backend agent endpoints. Manages OpenClaw desktop containers
5
+ (reef pods) via the authenticated backend API at api.hyperclaw.app/api/agents.
6
+
7
+ The backend proxies to Lagoon internally, handles auth, plan enforcement,
8
+ runtime key generation, and DB persistence.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import dataclass
13
+ from datetime import datetime
14
+ from typing import Optional, Any, AsyncIterator
15
+
16
+ import httpx
17
+
18
+ from .http import HTTPClient, APIError
19
+
20
+
21
+ CLAW_API_BASE = "https://api.hyperclaw.app"
22
+
23
+
24
+ @dataclass
25
+ class ReefPod:
26
+ """A reef pod (OpenClaw desktop container)."""
27
+ id: str # Agent UUID from backend
28
+ user_id: str
29
+ pod_id: str
30
+ pod_name: str
31
+ state: str
32
+ name: Optional[str] = None
33
+ cpu: int = 0 # cores
34
+ memory: int = 0 # GB
35
+ hostname: Optional[str] = None
36
+ openclaw_url: Optional[str] = None
37
+ jwt_token: Optional[str] = None
38
+ jwt_expires_at: Optional[datetime] = None
39
+ started_at: Optional[datetime] = None
40
+ stopped_at: Optional[datetime] = None
41
+ last_error: Optional[str] = None
42
+ created_at: Optional[datetime] = None
43
+ updated_at: Optional[datetime] = None
44
+
45
+ @classmethod
46
+ def from_dict(cls, data: dict) -> ReefPod:
47
+ def _parse_dt(val):
48
+ if isinstance(val, str) and val:
49
+ return datetime.fromisoformat(val.replace("Z", "+00:00"))
50
+ return None
51
+
52
+ return cls(
53
+ id=data.get("id", ""),
54
+ user_id=data.get("user_id", ""),
55
+ pod_id=data.get("pod_id", ""),
56
+ pod_name=data.get("pod_name", ""),
57
+ state=data.get("state", "unknown"),
58
+ name=data.get("name"),
59
+ cpu=data.get("cpu", 0),
60
+ memory=data.get("memory", 0),
61
+ hostname=data.get("hostname"),
62
+ openclaw_url=data.get("openclaw_url"),
63
+ jwt_token=data.get("jwt_token"),
64
+ jwt_expires_at=_parse_dt(data.get("jwt_expires_at")),
65
+ started_at=_parse_dt(data.get("started_at")),
66
+ stopped_at=_parse_dt(data.get("stopped_at")),
67
+ last_error=data.get("last_error"),
68
+ created_at=_parse_dt(data.get("created_at")),
69
+ updated_at=_parse_dt(data.get("updated_at")),
70
+ )
71
+
72
+ @property
73
+ def vnc_url(self) -> Optional[str]:
74
+ if self.hostname:
75
+ return f"https://{self.hostname}"
76
+ return None
77
+
78
+ @property
79
+ def shell_url(self) -> Optional[str]:
80
+ if self.hostname:
81
+ return f"https://shell-{self.hostname}"
82
+ return None
83
+
84
+ @property
85
+ def executor_url(self) -> Optional[str]:
86
+ return self.shell_url
87
+
88
+ @property
89
+ def is_running(self) -> bool:
90
+ return self.state == "running"
91
+
92
+ def gateway(self, **kwargs) -> "GatewayClient":
93
+ """Create a GatewayClient for this pod.
94
+
95
+ Requires the pod to be running with a valid JWT token.
96
+ Returns an unconnected client — use `async with pod.gateway() as gw:`.
97
+ """
98
+ from .gateway import GatewayClient
99
+ if not self.openclaw_url:
100
+ if self.hostname:
101
+ url = f"wss://openclaw-{self.hostname}"
102
+ else:
103
+ raise ValueError("Pod has no openclaw_url or hostname")
104
+ else:
105
+ url = self.openclaw_url
106
+ if not self.jwt_token:
107
+ raise ValueError("Pod has no JWT token — refresh it first")
108
+ return GatewayClient(url=url, token=self.jwt_token, **kwargs)
109
+
110
+
111
+ @dataclass
112
+ class ExecResult:
113
+ """Result of a one-shot command execution."""
114
+ exit_code: int
115
+ stdout: str
116
+ stderr: str
117
+
118
+ @classmethod
119
+ def from_dict(cls, data: dict) -> ExecResult:
120
+ return cls(
121
+ exit_code=data.get("exit_code", -1),
122
+ stdout=data.get("stdout", ""),
123
+ stderr=data.get("stderr", ""),
124
+ )
125
+
126
+
127
+ class Agents:
128
+ """
129
+ HyperClaw Agents API — manage reef pods (OpenClaw desktop containers).
130
+
131
+ Uses the authenticated backend API (api.hyperclaw.app/api/agents).
132
+ Auth: pass your HyperClaw API key (sk-...) as the claw_api_key.
133
+
134
+ Usage:
135
+ from hypercli import HyperCLI
136
+ client = HyperCLI(api_key="...", claw_api_key="sk-...")
137
+
138
+ # Launch
139
+ pod = client.agents.create()
140
+ print(f"Desktop: {pod.vnc_url}")
141
+
142
+ # Execute a command
143
+ result = client.agents.exec(pod, "echo hello")
144
+
145
+ # List
146
+ pods = client.agents.list()
147
+
148
+ # Stop
149
+ client.agents.stop(pod.id)
150
+ """
151
+
152
+ def __init__(self, http: HTTPClient, claw_api_key: str = None, claw_api_base: str = None):
153
+ self._http = http
154
+ self._api_key = claw_api_key or http.api_key
155
+ self._api_base = (claw_api_base or CLAW_API_BASE).rstrip("/")
156
+
157
+ @property
158
+ def _headers(self) -> dict:
159
+ return {
160
+ "Authorization": f"Bearer {self._api_key}",
161
+ "Content-Type": "application/json",
162
+ }
163
+
164
+ def _get(self, path: str, params: dict = None) -> Any:
165
+ with httpx.Client(timeout=30) as client:
166
+ resp = client.get(f"{self._api_base}{path}", headers=self._headers, params=params)
167
+ if resp.status_code >= 400:
168
+ try:
169
+ detail = resp.json().get("detail", resp.text)
170
+ except Exception:
171
+ detail = resp.text
172
+ raise APIError(resp.status_code, detail)
173
+ return resp.json()
174
+
175
+ def _post(self, path: str, json: dict = None) -> Any:
176
+ with httpx.Client(timeout=30) as client:
177
+ resp = client.post(f"{self._api_base}{path}", headers=self._headers, json=json)
178
+ if resp.status_code >= 400:
179
+ try:
180
+ detail = resp.json().get("detail", resp.text)
181
+ except Exception:
182
+ detail = resp.text
183
+ raise APIError(resp.status_code, detail)
184
+ return resp.json()
185
+
186
+ def _delete(self, path: str) -> Any:
187
+ with httpx.Client(timeout=30) as client:
188
+ resp = client.delete(f"{self._api_base}{path}", headers=self._headers)
189
+ if resp.status_code >= 400:
190
+ try:
191
+ detail = resp.json().get("detail", resp.text)
192
+ except Exception:
193
+ detail = resp.text
194
+ raise APIError(resp.status_code, detail)
195
+ return resp.json()
196
+
197
+ # -----------------------------------------------------------------------
198
+ # Agent lifecycle (HyperClaw backend → Lagoon)
199
+ # -----------------------------------------------------------------------
200
+
201
+ def create(
202
+ self,
203
+ name: str = None,
204
+ size: str = None,
205
+ cpu: int = None,
206
+ memory: int = None,
207
+ config: dict = None,
208
+ start: bool = True,
209
+ ) -> ReefPod:
210
+ """Create a new agent (provisions a reef pod via the backend).
211
+
212
+ Args:
213
+ name: Agent name.
214
+ size: Size preset (small/medium/large). Default: medium.
215
+ cpu: Custom CPU in cores (overrides size).
216
+ memory: Custom memory in GB (overrides size).
217
+ config: Optional config overrides.
218
+ start: Start the agent immediately (default: True).
219
+
220
+ Returns:
221
+ ReefPod with connection details.
222
+ """
223
+ body: dict = {"config": config or {}, "start": start}
224
+ if name:
225
+ body["name"] = name
226
+ if size:
227
+ body["size"] = size
228
+ if cpu is not None:
229
+ body["cpu"] = cpu
230
+ if memory is not None:
231
+ body["memory"] = memory
232
+ data = self._post("/api/agents", json=body)
233
+ return ReefPod.from_dict(data)
234
+
235
+ def budget(self) -> dict:
236
+ """Get the user's current agent resource budget and usage.
237
+
238
+ Returns:
239
+ Dict with budget, used, available (all in cores/GB).
240
+ """
241
+ return self._get("/api/agents/budget")
242
+
243
+ def metrics(self, agent_id: str) -> dict:
244
+ """Get live CPU/memory metrics for a running agent.
245
+
246
+ Args:
247
+ agent_id: Agent UUID.
248
+
249
+ Returns:
250
+ Dict with container metrics from k8s metrics-server.
251
+ """
252
+ return self._get(f"/api/agents/{agent_id}/metrics")
253
+
254
+ def list(self) -> list[ReefPod]:
255
+ """List all agents for the authenticated user.
256
+
257
+ Returns:
258
+ List of ReefPod objects.
259
+ """
260
+ data = self._get("/api/agents")
261
+ items = data.get("items", data) if isinstance(data, dict) else data
262
+ return [ReefPod.from_dict(p) for p in items]
263
+
264
+ def get(self, agent_id: str) -> ReefPod:
265
+ """Get agent details by ID (refreshes status from Lagoon).
266
+
267
+ Args:
268
+ agent_id: Agent UUID.
269
+
270
+ Returns:
271
+ ReefPod with current status.
272
+ """
273
+ data = self._get(f"/api/agents/{agent_id}")
274
+ return ReefPod.from_dict(data)
275
+
276
+ def start(self, agent_id: str) -> ReefPod:
277
+ """Start a previously stopped agent.
278
+
279
+ Args:
280
+ agent_id: Agent UUID.
281
+
282
+ Returns:
283
+ ReefPod with new pod details.
284
+ """
285
+ data = self._post(f"/api/agents/{agent_id}/start")
286
+ return ReefPod.from_dict(data)
287
+
288
+ def stop(self, agent_id: str) -> ReefPod:
289
+ """Stop an agent (tears down pod, keeps DB record).
290
+
291
+ Args:
292
+ agent_id: Agent UUID.
293
+
294
+ Returns:
295
+ ReefPod in stopped state.
296
+ """
297
+ data = self._post(f"/api/agents/{agent_id}/stop")
298
+ return ReefPod.from_dict(data)
299
+
300
+ def delete(self, agent_id: str) -> dict:
301
+ """Delete an agent entirely (pod + DB record).
302
+
303
+ Args:
304
+ agent_id: Agent UUID.
305
+
306
+ Returns:
307
+ Deletion status dict.
308
+ """
309
+ return self._delete(f"/api/agents/{agent_id}")
310
+
311
+ def refresh_token(self, agent_id: str) -> dict:
312
+ """Refresh the JWT token for an agent.
313
+
314
+ Args:
315
+ agent_id: Agent UUID.
316
+
317
+ Returns:
318
+ Dict with agent_id, pod_id, token, expires_at.
319
+ """
320
+ return self._get(f"/api/agents/{agent_id}/token")
321
+
322
+ # -----------------------------------------------------------------------
323
+ # Executor API (direct to reef pod via shell-{hostname})
324
+ # -----------------------------------------------------------------------
325
+
326
+ def _executor_headers(self, pod: ReefPod) -> dict:
327
+ h = {}
328
+ if pod.jwt_token:
329
+ h["Authorization"] = f"Bearer {pod.jwt_token}"
330
+ h["Cookie"] = f"{pod.pod_name}-token={pod.jwt_token}"
331
+ return h
332
+
333
+ def exec(self, pod: ReefPod, command: str, timeout: int = 30) -> ExecResult:
334
+ """Execute a one-shot command on a reef pod via the executor API.
335
+
336
+ Args:
337
+ pod: ReefPod to execute on (needs jwt_token).
338
+ command: Shell command to run.
339
+ timeout: Command timeout in seconds.
340
+
341
+ Returns:
342
+ ExecResult with exit_code, stdout, stderr.
343
+ """
344
+ if not pod.executor_url:
345
+ raise ValueError("Pod has no executor URL (missing hostname)")
346
+ with httpx.Client(timeout=max(timeout + 5, 35)) as client:
347
+ resp = client.post(
348
+ f"{pod.executor_url}/exec",
349
+ headers=self._executor_headers(pod),
350
+ json={"command": command, "timeout": timeout},
351
+ )
352
+ if resp.status_code >= 400:
353
+ try:
354
+ detail = resp.json().get("detail", resp.text)
355
+ except Exception:
356
+ detail = resp.text
357
+ raise APIError(resp.status_code, detail)
358
+ return ExecResult.from_dict(resp.json())
359
+
360
+ def health(self, pod: ReefPod) -> dict:
361
+ """Check executor health on a pod."""
362
+ if not pod.executor_url:
363
+ raise ValueError("Pod has no executor URL")
364
+ with httpx.Client(timeout=10) as client:
365
+ resp = client.get(
366
+ f"{pod.executor_url}/health",
367
+ headers=self._executor_headers(pod),
368
+ )
369
+ if resp.status_code >= 400:
370
+ raise APIError(resp.status_code, resp.text)
371
+ return resp.json()
372
+
373
+ def files_list(self, pod: ReefPod, path: str = ".") -> list[dict]:
374
+ """List files on a pod."""
375
+ if not pod.executor_url:
376
+ raise ValueError("Pod has no executor URL")
377
+ with httpx.Client(timeout=10) as client:
378
+ resp = client.get(
379
+ f"{pod.executor_url}/files",
380
+ headers=self._executor_headers(pod),
381
+ params={"path": path},
382
+ )
383
+ if resp.status_code >= 400:
384
+ raise APIError(resp.status_code, resp.text)
385
+ return resp.json().get("entries", [])
386
+
387
+ def file_read(self, pod: ReefPod, path: str) -> str:
388
+ """Read a file from a pod."""
389
+ if not pod.executor_url:
390
+ raise ValueError("Pod has no executor URL")
391
+ with httpx.Client(timeout=10) as client:
392
+ resp = client.get(
393
+ f"{pod.executor_url}/files/read",
394
+ headers=self._executor_headers(pod),
395
+ params={"path": path},
396
+ )
397
+ if resp.status_code >= 400:
398
+ raise APIError(resp.status_code, resp.text)
399
+ return resp.text
400
+
401
+ def file_write(self, pod: ReefPod, path: str, content: str) -> dict:
402
+ """Write a file to a pod."""
403
+ if not pod.executor_url:
404
+ raise ValueError("Pod has no executor URL")
405
+ with httpx.Client(timeout=10) as client:
406
+ resp = client.put(
407
+ f"{pod.executor_url}/files/write",
408
+ headers=self._executor_headers(pod),
409
+ params={"path": path},
410
+ content=content.encode(),
411
+ )
412
+ if resp.status_code >= 400:
413
+ raise APIError(resp.status_code, resp.text)
414
+ return resp.json()
415
+
416
+ def chat_stream(self, pod: ReefPod, messages: list[dict], model: str = "hyperclaw/kimi-k2.5"):
417
+ """Stream chat completions from the pod's OpenClaw gateway via executor proxy.
418
+
419
+ Yields content delta strings as they arrive.
420
+ """
421
+ if not pod.executor_url:
422
+ raise ValueError("Pod has no executor URL")
423
+
424
+ body = {
425
+ "model": model,
426
+ "messages": messages,
427
+ "stream": True,
428
+ }
429
+
430
+ with httpx.Client(timeout=300) as client:
431
+ with client.stream(
432
+ "POST",
433
+ f"{pod.executor_url}/chat",
434
+ headers=self._executor_headers(pod),
435
+ json=body,
436
+ ) as resp:
437
+ if resp.status_code >= 400:
438
+ raise APIError(resp.status_code, resp.read().decode())
439
+ for line in resp.iter_lines():
440
+ line = line.strip()
441
+ if not line or not line.startswith("data: "):
442
+ continue
443
+ data = line[6:]
444
+ if data == "[DONE]":
445
+ break
446
+ try:
447
+ import json
448
+ chunk = json.loads(data)
449
+ delta = chunk.get("choices", [{}])[0].get("delta", {})
450
+ content = delta.get("content")
451
+ if content:
452
+ yield content
453
+ except (json.JSONDecodeError, IndexError, KeyError):
454
+ continue
455
+
456
+ def logs_stream(self, pod: ReefPod, lines: int = 100, follow: bool = True):
457
+ """Stream logs from the pod via executor SSE.
458
+
459
+ Yields log lines as they arrive.
460
+ """
461
+ if not pod.executor_url:
462
+ raise ValueError("Pod has no executor URL")
463
+
464
+ params = {"lines": lines, "follow": "true" if follow else "false"}
465
+
466
+ with httpx.Client(timeout=None) as client:
467
+ with client.stream(
468
+ "GET",
469
+ f"{pod.executor_url}/logs",
470
+ headers=self._executor_headers(pod),
471
+ params=params,
472
+ ) as resp:
473
+ if resp.status_code >= 400:
474
+ raise APIError(resp.status_code, resp.read().decode())
475
+ for line in resp.iter_lines():
476
+ line = line.strip()
477
+ if line.startswith("data: "):
478
+ data = line[6:]
479
+ if data == "[keepalive]":
480
+ continue
481
+ yield data
482
+
483
+ # -----------------------------------------------------------------------
484
+ # WebSocket API (via HyperClaw backend)
485
+ # -----------------------------------------------------------------------
486
+
487
+ async def logs_stream_ws(self, agent_id: str, tail_lines: int = 100, container: str = "reef") -> AsyncIterator[str]:
488
+ """Stream logs via backend WebSocket.
489
+
490
+ Connects to the HyperClaw backend WebSocket endpoint which proxies
491
+ to the lagoon log buffer.
492
+
493
+ Args:
494
+ agent_id: Agent UUID.
495
+ tail_lines: Number of historical lines to fetch first.
496
+ container: Container name (default: reef).
497
+
498
+ Yields:
499
+ Log lines as they arrive.
500
+ """
501
+ import websockets
502
+
503
+ # Get JWT token
504
+ token_data = self.refresh_token(agent_id)
505
+ jwt = token_data["token"]
506
+
507
+ # Convert HTTP base to WebSocket base
508
+ ws_base = self._api_base.replace("https://", "wss://").replace("http://", "ws://")
509
+ url = f"{ws_base}/ws/{agent_id}?jwt={jwt}&container={container}&tail_lines={tail_lines}"
510
+
511
+ async with websockets.connect(url) as ws:
512
+ async for msg in ws:
513
+ yield msg
514
+
515
+ async def shell_connect(self, agent_id: str):
516
+ """Connect to agent shell via backend WebSocket proxy.
517
+
518
+ Connects to the HyperClaw backend shell WebSocket which proxies
519
+ to lagoon → k8s exec for bidirectional PTY access.
520
+
521
+ Args:
522
+ agent_id: Agent UUID.
523
+
524
+ Returns:
525
+ WebSocket connection for bidirectional shell I/O.
526
+ """
527
+ import websockets
528
+
529
+ # Get shell token
530
+ token_data = self._post(f"/api/agents/{agent_id}/shell/token")
531
+ jwt = token_data["token"]
532
+
533
+ # Convert HTTP base to WebSocket base
534
+ ws_base = self._api_base.replace("https://", "wss://").replace("http://", "ws://")
535
+ url = f"{ws_base}/ws/shell/{agent_id}?jwt={jwt}"
536
+
537
+ return await websockets.connect(url, ping_interval=20, ping_timeout=20)
@@ -7,6 +7,7 @@ from .user import UserAPI
7
7
  from .instances import Instances
8
8
  from .renders import Renders
9
9
  from .files import Files
10
+ from .agents import Agents
10
11
  from .claw import Claw
11
12
  from .keys import KeysAPI
12
13
 
@@ -50,6 +51,7 @@ class HyperCLI:
50
51
  self._http = HTTPClient(self._api_url, self._api_key)
51
52
 
52
53
  # API namespaces
54
+ self.agents = Agents(self._http, claw_api_key=claw_api_key)
53
55
  self.billing = Billing(self._http)
54
56
  self.jobs = Jobs(self._http)
55
57
  self.user = UserAPI(self._http)