hypercli-sdk 2026.6.16__tar.gz → 2026.6.26__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.

Potentially problematic release.


This version of hypercli-sdk might be problematic. Click here for more details.

Files changed (55) hide show
  1. {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/.gitignore +1 -0
  2. {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/PKG-INFO +7 -3
  3. {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/README.md +6 -2
  4. {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/hypercli/__init__.py +9 -2
  5. {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/hypercli/agent.py +5 -2
  6. {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/hypercli/agents.py +196 -5
  7. {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/hypercli/user.py +29 -0
  8. {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/pyproject.toml +1 -1
  9. {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/tests/test_agents.py +131 -6
  10. {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/tests/test_claw.py +16 -0
  11. {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/tests/test_keys.py +5 -0
  12. {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/hypercli/_compat.py +0 -0
  13. {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/hypercli/billing.py +0 -0
  14. {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/hypercli/client.py +0 -0
  15. {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/hypercli/config.py +0 -0
  16. {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/hypercli/files.py +0 -0
  17. {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/hypercli/gateway.py +0 -0
  18. {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/hypercli/http.py +0 -0
  19. {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/hypercli/instances.py +0 -0
  20. {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/hypercli/job/__init__.py +0 -0
  21. {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/hypercli/job/base.py +0 -0
  22. {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/hypercli/job/comfyui.py +0 -0
  23. {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/hypercli/job/gradio.py +0 -0
  24. {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/hypercli/jobs.py +0 -0
  25. {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/hypercli/keys.py +0 -0
  26. {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/hypercli/logs.py +0 -0
  27. {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/hypercli/models.py +0 -0
  28. {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/hypercli/openclaw/__init__.py +0 -0
  29. {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/hypercli/openclaw/gateway.py +0 -0
  30. {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/hypercli/renders.py +0 -0
  31. {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/hypercli/shell.py +0 -0
  32. {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/hypercli/voice.py +0 -0
  33. {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/hypercli/x402.py +0 -0
  34. {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/tests/integration/conftest.py +0 -0
  35. {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/tests/integration/test_agents.py +0 -0
  36. {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/tests/integration/test_auth.py +0 -0
  37. {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/tests/integration/test_billing.py +0 -0
  38. {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/tests/integration/test_instances.py +0 -0
  39. {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/tests/integration/test_jobs_dryrun.py +0 -0
  40. {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/tests/integration/test_keys.py +0 -0
  41. {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/tests/integration/test_renders.py +0 -0
  42. {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/tests/test_apply_params.py +0 -0
  43. {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/tests/test_bootstrap_console_test_key.py +0 -0
  44. {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/tests/test_bootstrap_dev_test_keys.py +0 -0
  45. {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/tests/test_config.py +0 -0
  46. {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/tests/test_exec_shell_dryrun.py +0 -0
  47. {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/tests/test_gateway.py +0 -0
  48. {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/tests/test_gateway_retry.py +0 -0
  49. {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/tests/test_graph_to_api.py +0 -0
  50. {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/tests/test_http.py +0 -0
  51. {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/tests/test_jobs.py +0 -0
  52. {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/tests/test_models.py +0 -0
  53. {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/tests/test_renders_subscription.py +0 -0
  54. {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/tests/test_status.py +0 -0
  55. {hypercli_sdk-2026.6.16 → hypercli_sdk-2026.6.26}/tests/test_voice.py +0 -0
@@ -27,6 +27,7 @@ coverage
27
27
  # Misc
28
28
  .DS_Store
29
29
  .agents/
30
+ .opencode/
30
31
  *.pem
31
32
 
32
33
  # IDE
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hypercli-sdk
3
- Version: 2026.6.16
3
+ Version: 2026.6.26
4
4
  Summary: Python SDK for HyperCLI - GPU orchestration and HyperAgent API
5
5
  Project-URL: Homepage, https://hypercli.com
6
6
  Project-URL: Documentation, https://docs.hypercli.com
@@ -121,6 +121,7 @@ from openai import OpenAI
121
121
  sdk = HyperCLI(api_key="hyper_api_key", agent_api_key="sk-agent")
122
122
  plans = sdk.agent.plans()
123
123
  activation = sdk.agent.redeem_grant_code("PROMO123")
124
+ renewal = sdk.agent.redeem_grant_code("PROMO123", extend_existing=True)
124
125
 
125
126
  client = OpenAI(
126
127
  api_key="your_hyperagent_api_key",
@@ -133,7 +134,7 @@ response = client.chat.completions.create(
133
134
  )
134
135
  ```
135
136
 
136
- `redeem_grant_code()` applies a promo/activation code to the current HyperClaw account and returns the created entitlement.
137
+ `redeem_grant_code()` applies a promo/activation code to the current HyperClaw account and returns the created entitlement. Codes create new entitlements by default; pass `extend_existing=True` only for renewal/extension behavior.
137
138
 
138
139
  ## OpenClaw Agents
139
140
 
@@ -143,14 +144,17 @@ OpenClaw uses the generic deployment launch surface. `registry_url`, `registry_a
143
144
  agent = client.deployments.create_openclaw(
144
145
  name="docs-demo",
145
146
  start=True,
146
- heartbeat={"every": "0m"}, # disable upstream OpenClaw heartbeat runs
147
147
  registry_url="git.nedos.co",
148
148
  registry_auth={"username": "ci", "password": "token"},
149
149
  )
150
150
  ```
151
151
 
152
+ Use `create_openclaw_pro(...)` for the desktop/browser image. It enables noVNC through the protected `desktop-<agent>.hypercli.app` route and sets `OPENCLAW_DESKTOP_ENABLED=1`.
153
+
152
154
  `heartbeat` maps directly to upstream OpenClaw config at `config.agents.defaults.heartbeat`. Omit it to keep upstream defaults, or pass values such as `heartbeat={"every": "1h", "target": "last"}`.
153
155
 
156
+ Automatic memory indexing is off by default. Opt in with `memory_index={"on_session_start": True, "on_search": True, "watch": True, "watch_debounce_ms": 30000, "interval_minutes": 0}`.
157
+
154
158
  ## Error Handling
155
159
 
156
160
  ```python
@@ -90,6 +90,7 @@ from openai import OpenAI
90
90
  sdk = HyperCLI(api_key="hyper_api_key", agent_api_key="sk-agent")
91
91
  plans = sdk.agent.plans()
92
92
  activation = sdk.agent.redeem_grant_code("PROMO123")
93
+ renewal = sdk.agent.redeem_grant_code("PROMO123", extend_existing=True)
93
94
 
94
95
  client = OpenAI(
95
96
  api_key="your_hyperagent_api_key",
@@ -102,7 +103,7 @@ response = client.chat.completions.create(
102
103
  )
103
104
  ```
104
105
 
105
- `redeem_grant_code()` applies a promo/activation code to the current HyperClaw account and returns the created entitlement.
106
+ `redeem_grant_code()` applies a promo/activation code to the current HyperClaw account and returns the created entitlement. Codes create new entitlements by default; pass `extend_existing=True` only for renewal/extension behavior.
106
107
 
107
108
  ## OpenClaw Agents
108
109
 
@@ -112,14 +113,17 @@ OpenClaw uses the generic deployment launch surface. `registry_url`, `registry_a
112
113
  agent = client.deployments.create_openclaw(
113
114
  name="docs-demo",
114
115
  start=True,
115
- heartbeat={"every": "0m"}, # disable upstream OpenClaw heartbeat runs
116
116
  registry_url="git.nedos.co",
117
117
  registry_auth={"username": "ci", "password": "token"},
118
118
  )
119
119
  ```
120
120
 
121
+ Use `create_openclaw_pro(...)` for the desktop/browser image. It enables noVNC through the protected `desktop-<agent>.hypercli.app` route and sets `OPENCLAW_DESKTOP_ENABLED=1`.
122
+
121
123
  `heartbeat` maps directly to upstream OpenClaw config at `config.agents.defaults.heartbeat`. Omit it to keep upstream defaults, or pass values such as `heartbeat={"every": "1h", "target": "last"}`.
122
124
 
125
+ Automatic memory indexing is off by default. Opt in with `memory_index={"on_session_start": True, "on_search": True, "watch": True, "watch_debounce_ms": 30000, "interval_minutes": 0}`.
126
+
123
127
  ## Error Handling
124
128
 
125
129
  ```python
@@ -39,9 +39,10 @@ from .voice import VoiceAPI
39
39
  from .models import Model, ModelsAPI
40
40
  from .x402 import X402Client, X402JobLaunch, X402FlowCreate, X402RenderCreate, FlowCatalogItem
41
41
  from .files import File, AsyncFiles
42
+ from .user import AuthMe, RuntimeIdentity, User, UserAPI
42
43
  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
43
44
  from .logs import LogStream, stream_logs, fetch_logs
44
- from .agents import Deployments, Agent, OpenClawAgent, ExecResult, build_openclaw_routes
45
+ from .agents import Deployments, Agent, OpenClawAgent, OpenClawProAgent, ExecResult, build_openclaw_routes, build_openclaw_memory_index_env
45
46
  from .shell import ShellSession, shell_connect
46
47
  from .agent import (
47
48
  HyperAgent,
@@ -82,7 +83,7 @@ from .gateway import (
82
83
  extract_gateway_chat_tool_calls,
83
84
  normalize_gateway_chat_message,
84
85
  )
85
- __version__ = "2026.6.16"
86
+ __version__ = "2026.6.26"
86
87
  __all__ = [
87
88
  "HyperCLI",
88
89
  "configure",
@@ -129,6 +130,10 @@ __all__ = [
129
130
  # Files API
130
131
  "File",
131
132
  "AsyncFiles",
133
+ "User",
134
+ "UserAPI",
135
+ "AuthMe",
136
+ "RuntimeIdentity",
132
137
  "AsyncHTTPClient",
133
138
  # Job lookup utils
134
139
  "find_job",
@@ -156,8 +161,10 @@ __all__ = [
156
161
  "Deployments",
157
162
  "Agent",
158
163
  "OpenClawAgent",
164
+ "OpenClawProAgent",
159
165
  "ExecResult",
160
166
  "build_openclaw_routes",
167
+ "build_openclaw_memory_index_env",
161
168
  # Shell
162
169
  "ShellSession",
163
170
  "shell_connect",
@@ -884,11 +884,14 @@ class HyperAgent:
884
884
  response.raise_for_status()
885
885
  return response.json()
886
886
 
887
- def redeem_grant_code(self, code: str) -> Dict[str, Any]:
887
+ def redeem_grant_code(self, code: str, *, extend_existing: bool | None = None) -> Dict[str, Any]:
888
+ payload: dict[str, Any] = {"code": str(code)}
889
+ if extend_existing is not None:
890
+ payload["extend_existing"] = bool(extend_existing)
888
891
  response = self._http._session.post(
889
892
  f"{self._control_base_url}/billing/grants/redeem",
890
893
  headers={"Authorization": f"Bearer {self._api_key}"},
891
- json={"code": str(code)},
894
+ json=payload,
892
895
  )
893
896
  response.raise_for_status()
894
897
  return response.json()
@@ -31,6 +31,15 @@ AGENTS_WS_URL = "wss://api.agents.hypercli.com/ws"
31
31
  DEV_AGENTS_API_BASE = "https://api.dev.hypercli.com/agents"
32
32
  DEV_AGENTS_WS_URL = "wss://api.agents.dev.hypercli.com/ws"
33
33
  DEFAULT_OPENCLAW_IMAGE = "ghcr.io/hypercli/hypercli-openclaw:prod"
34
+ DEFAULT_OPENCLAW_PRO_IMAGE = "ghcr.io/hypercli/hypercli-openclaw:pro-prod"
35
+ OPENCLAW_MEMORY_SEARCH_ENV_DEFAULTS = {
36
+ "OPENCLAW_MEMORY_SEARCH_ENABLED": "1",
37
+ "OPENCLAW_MEMORY_SEARCH_SYNC_ON_SESSION_START": "0",
38
+ "OPENCLAW_MEMORY_SEARCH_SYNC_ON_SEARCH": "0",
39
+ "OPENCLAW_MEMORY_SEARCH_SYNC_WATCH": "0",
40
+ "OPENCLAW_MEMORY_SEARCH_SYNC_WATCH_DEBOUNCE_MS": "30000",
41
+ "OPENCLAW_MEMORY_SEARCH_SYNC_INTERVAL_MINUTES": "0",
42
+ }
34
43
  LAUNCH_CONFIG_KEYS = frozenset({"image", "env", "routes", "ports", "command", "entrypoint", "sync_root", "sync_enabled", "sync_uid", "sync_gid", "registry_url", "registry_auth"})
35
44
  DEFAULT_OPENCLAW_SYNC_ROOT = "/home/node"
36
45
  AGENT_FILE_MAX_BYTES = 50 * 1024 * 1024
@@ -87,6 +96,48 @@ def _resolve_openclaw_routes(
87
96
  return build_openclaw_routes(**dict(openclaw_route_options or {}))
88
97
 
89
98
 
99
+ def _env_bool(value: object) -> str:
100
+ return "1" if bool(value) else "0"
101
+
102
+
103
+ def _env_non_negative_int(name: str, value: object) -> str:
104
+ integer = int(value)
105
+ if integer < 0:
106
+ raise ValueError(f"{name} must be non-negative")
107
+ return str(integer)
108
+
109
+
110
+ def build_openclaw_memory_index_env(memory_index: dict | None = None) -> dict[str, str]:
111
+ """Build OpenClaw memory-search indexing environment variables.
112
+
113
+ No env vars are emitted unless memory_index is provided; the image config
114
+ carries the no-auto-indexing defaults. Passing an empty dict explicitly
115
+ emits the default env block.
116
+ """
117
+ if memory_index is None:
118
+ return {}
119
+ env = dict(OPENCLAW_MEMORY_SEARCH_ENV_DEFAULTS)
120
+ if memory_index.get("enabled") is not None:
121
+ env["OPENCLAW_MEMORY_SEARCH_ENABLED"] = _env_bool(memory_index["enabled"])
122
+ if memory_index.get("on_session_start") is not None:
123
+ env["OPENCLAW_MEMORY_SEARCH_SYNC_ON_SESSION_START"] = _env_bool(memory_index["on_session_start"])
124
+ if memory_index.get("on_search") is not None:
125
+ env["OPENCLAW_MEMORY_SEARCH_SYNC_ON_SEARCH"] = _env_bool(memory_index["on_search"])
126
+ if memory_index.get("watch") is not None:
127
+ env["OPENCLAW_MEMORY_SEARCH_SYNC_WATCH"] = _env_bool(memory_index["watch"])
128
+ if memory_index.get("watch_debounce_ms") is not None:
129
+ env["OPENCLAW_MEMORY_SEARCH_SYNC_WATCH_DEBOUNCE_MS"] = _env_non_negative_int(
130
+ "watch_debounce_ms",
131
+ memory_index["watch_debounce_ms"],
132
+ )
133
+ if memory_index.get("interval_minutes") is not None:
134
+ env["OPENCLAW_MEMORY_SEARCH_SYNC_INTERVAL_MINUTES"] = _env_non_negative_int(
135
+ "interval_minutes",
136
+ memory_index["interval_minutes"],
137
+ )
138
+ return env
139
+
140
+
90
141
  def _default_gateway_timeout() -> float | None:
91
142
  raw = (
92
143
  os.environ.get("HYPERCLI_GATEWAY_TIMEOUT")
@@ -259,6 +310,16 @@ def _default_openclaw_image(image: str | None) -> str | None:
259
310
  return DEFAULT_OPENCLAW_IMAGE
260
311
 
261
312
 
313
+ def _default_openclaw_pro_image(image: str | None) -> str | None:
314
+ if image is not None:
315
+ return image
316
+ return DEFAULT_OPENCLAW_PRO_IMAGE
317
+
318
+
319
+ def _truthy_env(value: object) -> bool:
320
+ return str(value or "").strip().lower() in {"1", "true", "yes", "on", "enabled"}
321
+
322
+
262
323
  def _parse_dt(val):
263
324
  if isinstance(val, str) and val:
264
325
  return datetime.fromisoformat(val.replace("Z", "+00:00"))
@@ -317,6 +378,17 @@ def _is_openclaw_agent_data(data: dict) -> bool:
317
378
  return False
318
379
 
319
380
 
381
+ def _is_openclaw_pro_agent_data(data: dict) -> bool:
382
+ launch_config = data.get("launch_config")
383
+ if not isinstance(launch_config, dict):
384
+ return False
385
+ env = launch_config.get("env")
386
+ if isinstance(env, dict) and _truthy_env(env.get("OPENCLAW_DESKTOP_ENABLED")):
387
+ return True
388
+ image = str(launch_config.get("image") or "")
389
+ return "hypercli-openclaw:pro" in image or image.endswith("-pro")
390
+
391
+
320
392
  @dataclass
321
393
  class Agent:
322
394
  """Generic agent returned by the HyperClaw backend."""
@@ -953,6 +1025,11 @@ class OpenClawAgent(Agent):
953
1025
  return await self.channel_upsert("discord", channel_config, account_id=account_id)
954
1026
 
955
1027
 
1028
+ @dataclass
1029
+ class OpenClawProAgent(OpenClawAgent):
1030
+ """OpenClaw agent launched with the pro desktop/browser preset."""
1031
+
1032
+
956
1033
  @dataclass
957
1034
  class ExecResult:
958
1035
  """Result of a one-shot command execution."""
@@ -1009,7 +1086,9 @@ class Deployments:
1009
1086
  )
1010
1087
 
1011
1088
  def _hydrate_agent(self, data: dict) -> Agent:
1012
- if _is_openclaw_agent_data(data):
1089
+ if _is_openclaw_pro_agent_data(data):
1090
+ agent = OpenClawProAgent.from_dict(data)
1091
+ elif _is_openclaw_agent_data(data):
1013
1092
  agent = OpenClawAgent.from_dict(data)
1014
1093
  else:
1015
1094
  agent = Agent.from_dict(data)
@@ -1182,8 +1261,9 @@ class Deployments:
1182
1261
  start: bool = True,
1183
1262
  openclaw_routes: dict | None = None,
1184
1263
  openclaw_route_options: dict | None = None,
1264
+ memory_index: dict | None = None,
1185
1265
  ) -> Agent:
1186
- effective_env = dict(env or {})
1266
+ effective_env = {**build_openclaw_memory_index_env(memory_index), **dict(env or {})}
1187
1267
  return self.create(
1188
1268
  name=name,
1189
1269
  size=size,
@@ -1212,6 +1292,62 @@ class Deployments:
1212
1292
  start=start,
1213
1293
  )
1214
1294
 
1295
+ def create_openclaw_pro(
1296
+ self,
1297
+ name: str = None,
1298
+ size: str = None,
1299
+ config: dict = None,
1300
+ tags: list[str] = None,
1301
+ env: dict = None,
1302
+ ports: list = None,
1303
+ routes: dict = None,
1304
+ command: list[str] = None,
1305
+ entrypoint: list[str] = None,
1306
+ image: str = None,
1307
+ sync_root: str = None,
1308
+ sync_enabled: bool = None,
1309
+ sync_uid: int = None,
1310
+ sync_gid: int = None,
1311
+ registry_url: str = None,
1312
+ registry_auth: dict = None,
1313
+ gateway_token: str = None,
1314
+ heartbeat: dict = None,
1315
+ meta_ui: dict = None,
1316
+ dry_run: bool = False,
1317
+ start: bool = True,
1318
+ openclaw_routes: dict | None = None,
1319
+ openclaw_route_options: dict | None = None,
1320
+ memory_index: dict | None = None,
1321
+ ) -> Agent:
1322
+ effective_env = {"OPENCLAW_DESKTOP_ENABLED": "1", **dict(env or {})}
1323
+ effective_route_options = {"include_desktop": True, **dict(openclaw_route_options or {})}
1324
+ return self.create_openclaw(
1325
+ name=name,
1326
+ size=size,
1327
+ config=config,
1328
+ tags=tags,
1329
+ env=effective_env,
1330
+ ports=ports,
1331
+ routes=routes,
1332
+ command=command,
1333
+ entrypoint=entrypoint,
1334
+ image=_default_openclaw_pro_image(image),
1335
+ sync_root=sync_root,
1336
+ sync_enabled=sync_enabled,
1337
+ sync_uid=sync_uid,
1338
+ sync_gid=sync_gid,
1339
+ registry_url=registry_url,
1340
+ registry_auth=registry_auth,
1341
+ gateway_token=gateway_token,
1342
+ heartbeat=heartbeat,
1343
+ meta_ui=meta_ui,
1344
+ dry_run=dry_run,
1345
+ start=start,
1346
+ openclaw_routes=openclaw_routes,
1347
+ openclaw_route_options=effective_route_options,
1348
+ memory_index=memory_index,
1349
+ )
1350
+
1215
1351
  def budget(self) -> dict:
1216
1352
  """Get the user's current agent resource budget and usage.
1217
1353
 
@@ -1345,8 +1481,9 @@ class Deployments:
1345
1481
  dry_run: bool = False,
1346
1482
  openclaw_routes: dict | None = None,
1347
1483
  openclaw_route_options: dict | None = None,
1484
+ memory_index: dict | None = None,
1348
1485
  ) -> Agent:
1349
- effective_env = dict(env or {})
1486
+ effective_env = {**build_openclaw_memory_index_env(memory_index), **dict(env or {})}
1350
1487
  return self.start(
1351
1488
  agent_id,
1352
1489
  config=config,
@@ -1371,6 +1508,54 @@ class Deployments:
1371
1508
  dry_run=dry_run,
1372
1509
  )
1373
1510
 
1511
+ def start_openclaw_pro(
1512
+ self,
1513
+ agent_id: str,
1514
+ config: dict = None,
1515
+ env: dict = None,
1516
+ ports: list = None,
1517
+ routes: dict = None,
1518
+ command: list[str] = None,
1519
+ entrypoint: list[str] = None,
1520
+ image: str = None,
1521
+ sync_root: str = None,
1522
+ sync_enabled: bool = None,
1523
+ sync_uid: int = None,
1524
+ sync_gid: int = None,
1525
+ registry_url: str = None,
1526
+ registry_auth: dict = None,
1527
+ gateway_token: str = None,
1528
+ heartbeat: dict = None,
1529
+ dry_run: bool = False,
1530
+ openclaw_routes: dict | None = None,
1531
+ openclaw_route_options: dict | None = None,
1532
+ memory_index: dict | None = None,
1533
+ ) -> Agent:
1534
+ effective_env = {"OPENCLAW_DESKTOP_ENABLED": "1", **dict(env or {})}
1535
+ effective_route_options = {"include_desktop": True, **dict(openclaw_route_options or {})}
1536
+ return self.start_openclaw(
1537
+ agent_id,
1538
+ config=config,
1539
+ env=effective_env,
1540
+ ports=ports,
1541
+ routes=routes,
1542
+ command=command,
1543
+ entrypoint=entrypoint,
1544
+ image=_default_openclaw_pro_image(image),
1545
+ sync_root=sync_root,
1546
+ sync_enabled=sync_enabled,
1547
+ sync_uid=sync_uid,
1548
+ sync_gid=sync_gid,
1549
+ registry_url=registry_url,
1550
+ registry_auth=registry_auth,
1551
+ gateway_token=gateway_token,
1552
+ heartbeat=heartbeat,
1553
+ dry_run=dry_run,
1554
+ openclaw_routes=openclaw_routes,
1555
+ openclaw_route_options=effective_route_options,
1556
+ memory_index=memory_index,
1557
+ )
1558
+
1374
1559
  def update(
1375
1560
  self,
1376
1561
  agent_id: str,
@@ -1472,14 +1657,20 @@ class Deployments:
1472
1657
  *,
1473
1658
  duration: int,
1474
1659
  tags: list[str] | None = None,
1660
+ extend_existing: bool | None = None,
1475
1661
  ) -> dict:
1476
1662
  payload: dict[str, Any] = {"duration": int(duration)}
1477
1663
  if tags is not None:
1478
1664
  payload["tags"] = list(tags)
1665
+ if extend_existing is not None:
1666
+ payload["extend_existing"] = bool(extend_existing)
1479
1667
  return self._post(f"/billing/balance/{quote(str(plan_id), safe='')}", json=payload)
1480
1668
 
1481
- def redeem_grant_code(self, code: str) -> dict:
1482
- return self._post("/billing/grants/redeem", json={"code": str(code)})
1669
+ def redeem_grant_code(self, code: str, *, extend_existing: bool | None = None) -> dict:
1670
+ payload: dict[str, Any] = {"code": str(code)}
1671
+ if extend_existing is not None:
1672
+ payload["extend_existing"] = bool(extend_existing)
1673
+ return self._post("/billing/grants/redeem", json=payload)
1483
1674
 
1484
1675
  def logs_token(self, agent_id: str) -> dict:
1485
1676
  """Mint a short-lived JWT token for backend log streaming."""
@@ -25,6 +25,21 @@ class User:
25
25
  )
26
26
 
27
27
 
28
+ @dataclass
29
+ class RuntimeIdentity:
30
+ kind: str
31
+ agent_id: str | None = None
32
+
33
+ @classmethod
34
+ def from_dict(cls, data: dict | None) -> "RuntimeIdentity | None":
35
+ if not isinstance(data, dict):
36
+ return None
37
+ kind = str(data.get("kind") or "").strip()
38
+ if not kind:
39
+ return None
40
+ return cls(kind=kind, agent_id=data.get("agent_id"))
41
+
42
+
28
43
  @dataclass
29
44
  class AuthMe:
30
45
  user_id: str
@@ -34,6 +49,8 @@ class AuthMe:
34
49
  email: str | None
35
50
  auth_type: str
36
51
  capabilities: list[str]
52
+ tags: list[str]
53
+ runtime: RuntimeIdentity | None
37
54
  has_active_subscription: bool
38
55
  key_id: str | None
39
56
  key_name: str | None
@@ -48,11 +65,23 @@ class AuthMe:
48
65
  email=data.get("email"),
49
66
  auth_type=data.get("auth_type", ""),
50
67
  capabilities=list(data.get("capabilities") or []),
68
+ tags=list(data.get("tags") or []),
69
+ runtime=RuntimeIdentity.from_dict(data.get("runtime")),
51
70
  has_active_subscription=bool(data.get("has_active_subscription")),
52
71
  key_id=data.get("key_id"),
53
72
  key_name=data.get("key_name"),
54
73
  )
55
74
 
75
+ @property
76
+ def is_runtime_agent(self) -> bool:
77
+ return self.runtime is not None and self.runtime.kind == "agent"
78
+
79
+ @property
80
+ def runtime_agent_id(self) -> str | None:
81
+ if not self.is_runtime_agent or self.runtime is None:
82
+ return None
83
+ return self.runtime.agent_id
84
+
56
85
 
57
86
  class UserAPI:
58
87
  """User API wrapper"""
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "hypercli-sdk"
7
- version = "2026.6.16"
7
+ version = "2026.6.26"
8
8
  description = "Python SDK for HyperCLI - GPU orchestration and HyperAgent API"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -15,8 +15,10 @@ from hypercli.agents import (
15
15
  AGENT_FILE_TRANSFER_CHUNK_BYTES,
16
16
  Agent,
17
17
  DEFAULT_OPENCLAW_IMAGE,
18
+ DEFAULT_OPENCLAW_PRO_IMAGE,
18
19
  Deployments,
19
20
  OpenClawAgent,
21
+ OpenClawProAgent,
20
22
  ExecResult,
21
23
  _build_agent_launch,
22
24
  build_openclaw_routes,
@@ -454,7 +456,7 @@ def test_build_agent_launch_includes_command_and_entrypoint():
454
456
  def test_build_agent_launch_merges_heartbeat_defaults():
455
457
  launch, _gateway_token = _build_agent_launch(
456
458
  {"agents": {"defaults": {"model": "openai/gpt-5.4", "heartbeat": {"target": "last"}}}},
457
- heartbeat={"every": "0m", "includeSystemPromptSection": False},
459
+ heartbeat={"every": "1h", "target": "last"},
458
460
  gateway_token="gw-token",
459
461
  )
460
462
 
@@ -464,8 +466,7 @@ def test_build_agent_launch_merges_heartbeat_defaults():
464
466
  "model": "openai/gpt-5.4",
465
467
  "heartbeat": {
466
468
  "target": "last",
467
- "every": "0m",
468
- "includeSystemPromptSection": False,
469
+ "every": "1h",
469
470
  },
470
471
  }
471
472
  }
@@ -543,6 +544,81 @@ def test_create_openclaw_respects_explicit_empty_routes(agents_client):
543
544
  assert posted_json["routes"] == {}
544
545
 
545
546
 
547
+ def test_create_openclaw_pro_defaults_desktop_image_env_and_routes(agents_client):
548
+ with patch("httpx.Client") as mock_client_class, patch("hypercli.agents.secrets.token_hex", return_value="gw-token-123"):
549
+ mock_client = MagicMock()
550
+ mock_response = Mock()
551
+ mock_response.status_code = 200
552
+ mock_response.json.return_value = {
553
+ "id": "agent-123",
554
+ "user_id": "user-456",
555
+ "pod_id": "pod-789",
556
+ "pod_name": "test-pod",
557
+ "state": "starting",
558
+ "launch_config": {
559
+ "image": DEFAULT_OPENCLAW_PRO_IMAGE,
560
+ "env": {"OPENCLAW_DESKTOP_ENABLED": "1"},
561
+ "routes": {"openclaw": {"port": 18789, "auth": False, "prefix": ""}},
562
+ },
563
+ }
564
+ mock_client.post.return_value = mock_response
565
+ mock_client.__enter__.return_value = mock_client
566
+ mock_client.__exit__.return_value = False
567
+ mock_client_class.return_value = mock_client
568
+
569
+ agent = agents_client.create_openclaw_pro(name="test-agent")
570
+
571
+ posted_json = mock_client.post.call_args[1]["json"]
572
+ assert posted_json["image"] == DEFAULT_OPENCLAW_PRO_IMAGE
573
+ assert posted_json["env"]["OPENCLAW_DESKTOP_ENABLED"] == "1"
574
+ assert "OPENCLAW_MEMORY_SEARCH_SYNC_ON_SESSION_START" not in posted_json["env"]
575
+ assert posted_json["routes"] == {
576
+ "openclaw": {"port": 18789, "auth": False, "prefix": ""},
577
+ "desktop": {"port": 3000, "auth": True, "prefix": "desktop"},
578
+ }
579
+ assert isinstance(agent, OpenClawProAgent)
580
+
581
+
582
+ def test_create_openclaw_accepts_memory_index_options(agents_client):
583
+ with patch("httpx.Client") as mock_client_class, patch("hypercli.agents.secrets.token_hex", return_value="gw-token-123"):
584
+ mock_client = MagicMock()
585
+ mock_response = Mock()
586
+ mock_response.status_code = 200
587
+ mock_response.json.return_value = {
588
+ "id": "agent-123",
589
+ "user_id": "user-456",
590
+ "pod_id": "pod-789",
591
+ "pod_name": "test-pod",
592
+ "state": "starting",
593
+ "launch_config": {
594
+ "env": {},
595
+ "routes": {"openclaw": {"port": 18789, "auth": False, "prefix": ""}},
596
+ },
597
+ }
598
+ mock_client.post.return_value = mock_response
599
+ mock_client.__enter__.return_value = mock_client
600
+ mock_client.__exit__.return_value = False
601
+ mock_client_class.return_value = mock_client
602
+
603
+ agents_client.create_openclaw(
604
+ name="test-agent",
605
+ memory_index={
606
+ "on_session_start": True,
607
+ "on_search": True,
608
+ "watch": True,
609
+ "watch_debounce_ms": 60000,
610
+ "interval_minutes": 120,
611
+ },
612
+ )
613
+
614
+ posted_json = mock_client.post.call_args[1]["json"]
615
+ assert posted_json["env"]["OPENCLAW_MEMORY_SEARCH_SYNC_ON_SESSION_START"] == "1"
616
+ assert posted_json["env"]["OPENCLAW_MEMORY_SEARCH_SYNC_ON_SEARCH"] == "1"
617
+ assert posted_json["env"]["OPENCLAW_MEMORY_SEARCH_SYNC_WATCH"] == "1"
618
+ assert posted_json["env"]["OPENCLAW_MEMORY_SEARCH_SYNC_WATCH_DEBOUNCE_MS"] == "60000"
619
+ assert posted_json["env"]["OPENCLAW_MEMORY_SEARCH_SYNC_INTERVAL_MINUTES"] == "120"
620
+
621
+
546
622
  def test_create_openclaw_includes_heartbeat_when_requested(agents_client):
547
623
  with patch("httpx.Client") as mock_client_class, patch("hypercli.agents.secrets.token_hex", return_value="gw-token-123"):
548
624
  mock_client = MagicMock()
@@ -562,13 +638,13 @@ def test_create_openclaw_includes_heartbeat_when_requested(agents_client):
562
638
 
563
639
  agents_client.create_openclaw(
564
640
  name="test-agent",
565
- heartbeat={"every": "0m", "includeSystemPromptSection": False},
641
+ heartbeat={"every": "1h", "target": "last"},
566
642
  )
567
643
 
568
644
  posted_json = mock_client.post.call_args[1]["json"]
569
645
  assert posted_json["config"]["agents"]["defaults"]["heartbeat"] == {
570
- "every": "0m",
571
- "includeSystemPromptSection": False,
646
+ "every": "1h",
647
+ "target": "last",
572
648
  }
573
649
 
574
650
  @pytest.fixture
@@ -1111,6 +1187,35 @@ def test_agents_purchase_entitlement_from_balance(agents_client):
1111
1187
  assert mock_client.post.call_args[1]["json"] == {"duration": 3600, "tags": ["customer=acme"]}
1112
1188
 
1113
1189
 
1190
+ def test_agents_purchase_entitlement_from_balance_can_extend_existing(agents_client):
1191
+ with patch("httpx.Client") as mock_client_class:
1192
+ mock_client = MagicMock()
1193
+ mock_response = Mock()
1194
+ mock_response.status_code = 200
1195
+ mock_response.json.return_value = {
1196
+ "grant": {"id": "grant-1", "type": "BALANCE"},
1197
+ "entitlement": {"id": "ent-1", "plan_id": "basic"},
1198
+ }
1199
+ mock_client.post.return_value = mock_response
1200
+ mock_client.__enter__.return_value = mock_client
1201
+ mock_client.__exit__.return_value = False
1202
+ mock_client_class.return_value = mock_client
1203
+
1204
+ agents_client.purchase_entitlement_from_balance(
1205
+ "basic",
1206
+ duration=3600,
1207
+ tags=["customer=acme"],
1208
+ extend_existing=True,
1209
+ )
1210
+
1211
+ assert mock_client.post.call_args[0][0].endswith("/billing/balance/basic")
1212
+ assert mock_client.post.call_args[1]["json"] == {
1213
+ "duration": 3600,
1214
+ "tags": ["customer=acme"],
1215
+ "extend_existing": True,
1216
+ }
1217
+
1218
+
1114
1219
  def test_agents_redeem_grant_code(agents_client):
1115
1220
  with patch("httpx.Client") as mock_client_class:
1116
1221
  mock_client = MagicMock()
@@ -1132,6 +1237,26 @@ def test_agents_redeem_grant_code(agents_client):
1132
1237
  assert mock_client.post.call_args[1]["json"] == {"code": "promo-123"}
1133
1238
 
1134
1239
 
1240
+ def test_agents_redeem_grant_code_can_request_extension(agents_client):
1241
+ with patch("httpx.Client") as mock_client_class:
1242
+ mock_client = MagicMock()
1243
+ mock_response = Mock()
1244
+ mock_response.status_code = 200
1245
+ mock_response.json.return_value = {
1246
+ "grant": {"id": "grant-1", "type": "ACTIVATION_CODE", "code": "promo-123"},
1247
+ "entitlement": {"id": "ent-1", "plan_id": "basic"},
1248
+ }
1249
+ mock_client.post.return_value = mock_response
1250
+ mock_client.__enter__.return_value = mock_client
1251
+ mock_client.__exit__.return_value = False
1252
+ mock_client_class.return_value = mock_client
1253
+
1254
+ agents_client.redeem_grant_code("promo-123", extend_existing=True)
1255
+
1256
+ assert mock_client.post.call_args[0][0].endswith("/billing/grants/redeem")
1257
+ assert mock_client.post.call_args[1]["json"] == {"code": "promo-123", "extend_existing": True}
1258
+
1259
+
1135
1260
  def test_openclaw_agent_resolve_gateway_token_uses_env_route():
1136
1261
  manager = Mock()
1137
1262
  manager.get.return_value = OpenClawAgent.from_dict({
@@ -379,6 +379,22 @@ class TestHyperAgentClient:
379
379
  headers={"Authorization": "Bearer sk-hyper-test"},
380
380
  json={"code": "promo-123"},
381
381
  )
382
+
383
+ def test_redeem_grant_code_can_request_extension(self, mock_http):
384
+ mock_http._session.post.return_value.json.return_value = {
385
+ "grant": {"id": "grant-1", "code": "promo-123"},
386
+ "entitlement": {"id": "ent-1", "plan_id": "basic"},
387
+ }
388
+ mock_http._session.post.return_value.raise_for_status = Mock()
389
+
390
+ agent = HyperAgent(mock_http, agent_api_key="sk-hyper-test", agents_api_base_url="https://api.hypercli.com/agents")
391
+ agent.redeem_grant_code("promo-123", extend_existing=True)
392
+
393
+ mock_http._session.post.assert_called_with(
394
+ "https://api.hypercli.com/agents/billing/grants/redeem",
395
+ headers={"Authorization": "Bearer sk-hyper-test"},
396
+ json={"code": "promo-123", "extend_existing": True},
397
+ )
382
398
 
383
399
  def test_openai_client_creation(self, mock_http):
384
400
  """Test that OpenAI client is created with correct config."""
@@ -118,6 +118,8 @@ def test_user_auth_me_returns_capabilities():
118
118
  "email": "user@example.com",
119
119
  "auth_type": "orchestra_key",
120
120
  "capabilities": ["models:*", "voice:*"],
121
+ "tags": ["runtime=agent", "runtime_agent=agent-123"],
122
+ "runtime": {"kind": "agent", "agent_id": "agent-123"},
121
123
  "has_active_subscription": True,
122
124
  "key_id": "key-123",
123
125
  "key_name": "runtime-key",
@@ -127,5 +129,8 @@ def test_user_auth_me_returns_capabilities():
127
129
 
128
130
  assert auth_me.user_id == "user-123"
129
131
  assert auth_me.capabilities == ["models:*", "voice:*"]
132
+ assert auth_me.tags == ["runtime=agent", "runtime_agent=agent-123"]
133
+ assert auth_me.is_runtime_agent is True
134
+ assert auth_me.runtime_agent_id == "agent-123"
130
135
  assert auth_me.has_active_subscription is True
131
136
  assert auth_me.key_id == "key-123"