@hypercli.com/sdk 2026.6.21 → 2026.7.31

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 (87) hide show
  1. package/README.md +82 -1
  2. package/dist/agent.d.ts +19 -2
  3. package/dist/agent.d.ts.map +1 -1
  4. package/dist/agent.js +24 -0
  5. package/dist/agent.js.map +1 -1
  6. package/dist/agents.d.ts +561 -41
  7. package/dist/agents.d.ts.map +1 -1
  8. package/dist/agents.js +2016 -106
  9. package/dist/agents.js.map +1 -1
  10. package/dist/browser.d.ts +3 -1
  11. package/dist/browser.d.ts.map +1 -1
  12. package/dist/browser.js +9 -2
  13. package/dist/browser.js.map +1 -1
  14. package/dist/channels.d.ts +156 -0
  15. package/dist/channels.d.ts.map +1 -0
  16. package/dist/channels.js +110 -0
  17. package/dist/channels.js.map +1 -0
  18. package/dist/client.d.ts +2 -0
  19. package/dist/client.d.ts.map +1 -1
  20. package/dist/client.js +9 -3
  21. package/dist/client.js.map +1 -1
  22. package/dist/connectors.d.ts +79 -0
  23. package/dist/connectors.d.ts.map +1 -0
  24. package/dist/connectors.js +2 -0
  25. package/dist/connectors.js.map +1 -0
  26. package/dist/http.d.ts +10 -3
  27. package/dist/http.d.ts.map +1 -1
  28. package/dist/http.js +135 -61
  29. package/dist/http.js.map +1 -1
  30. package/dist/index.d.ts +12 -4
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +8 -2
  33. package/dist/index.js.map +1 -1
  34. package/dist/keys.d.ts +6 -1
  35. package/dist/keys.d.ts.map +1 -1
  36. package/dist/keys.js +8 -1
  37. package/dist/keys.js.map +1 -1
  38. package/dist/openclaw/channels.d.ts +86 -0
  39. package/dist/openclaw/channels.d.ts.map +1 -0
  40. package/dist/openclaw/channels.js +290 -0
  41. package/dist/openclaw/channels.js.map +1 -0
  42. package/dist/openclaw/connectors.d.ts +61 -0
  43. package/dist/openclaw/connectors.d.ts.map +1 -0
  44. package/dist/openclaw/connectors.js +174 -0
  45. package/dist/openclaw/connectors.js.map +1 -0
  46. package/dist/openclaw/gateway.d.ts +733 -15
  47. package/dist/openclaw/gateway.d.ts.map +1 -1
  48. package/dist/openclaw/gateway.js +1720 -181
  49. package/dist/openclaw/gateway.js.map +1 -1
  50. package/dist/openclaw/skills.d.ts +106 -0
  51. package/dist/openclaw/skills.d.ts.map +1 -0
  52. package/dist/openclaw/skills.js +575 -0
  53. package/dist/openclaw/skills.js.map +1 -0
  54. package/dist/openclaw/slack.d.ts +818 -0
  55. package/dist/openclaw/slack.d.ts.map +1 -0
  56. package/dist/openclaw/slack.js +536 -0
  57. package/dist/openclaw/slack.js.map +1 -0
  58. package/dist/openclaw/whatsapp.d.ts +63 -0
  59. package/dist/openclaw/whatsapp.d.ts.map +1 -0
  60. package/dist/openclaw/whatsapp.js +264 -0
  61. package/dist/openclaw/whatsapp.js.map +1 -0
  62. package/dist/renders.js +11 -11
  63. package/dist/renders.js.map +1 -1
  64. package/dist/skills.d.ts +126 -0
  65. package/dist/skills.d.ts.map +1 -0
  66. package/dist/skills.js +2 -0
  67. package/dist/skills.js.map +1 -0
  68. package/dist/user.d.ts +15 -2
  69. package/dist/user.d.ts.map +1 -1
  70. package/dist/user.js +51 -4
  71. package/dist/user.js.map +1 -1
  72. package/dist/voice-session.d.ts +74 -0
  73. package/dist/voice-session.d.ts.map +1 -0
  74. package/dist/voice-session.js +255 -0
  75. package/dist/voice-session.js.map +1 -0
  76. package/dist/voice.d.ts +27 -3
  77. package/dist/voice.d.ts.map +1 -1
  78. package/dist/voice.js +89 -6
  79. package/dist/voice.js.map +1 -1
  80. package/dist/workspaces.d.ts +181 -0
  81. package/dist/workspaces.d.ts.map +1 -0
  82. package/dist/workspaces.js +495 -0
  83. package/dist/workspaces.js.map +1 -0
  84. package/dist/x402.d.ts.map +1 -1
  85. package/dist/x402.js +1 -2
  86. package/dist/x402.js.map +1 -1
  87. package/package.json +38 -2
package/dist/agents.js CHANGED
@@ -6,17 +6,404 @@ import { getAgentsApiBaseUrl, getConfigValue } from './config.js';
6
6
  import { APIError } from './errors.js';
7
7
  import { HTTPClient } from './http.js';
8
8
  import { GatewayClient, } from './openclaw/gateway.js';
9
+ import { normalizeSlackRelayBaseUrl } from './channels.js';
9
10
  const AGENTS_API_BASE = 'https://api.hypercli.com/agents';
10
11
  const DEV_AGENTS_API_BASE = 'https://api.dev.hypercli.com/agents';
11
12
  const DEPLOYMENTS_API_PREFIX = '/deployments';
12
13
  const AGENTS_WS_URL = 'wss://api.agents.hypercli.com/ws';
13
14
  const DEV_AGENTS_WS_URL = 'wss://api.agents.dev.hypercli.com/ws';
14
- export const DEFAULT_OPENCLAW_IMAGE = 'ghcr.io/hypercli/hypercli-openclaw:prod';
15
- const LAUNCH_CONFIG_KEYS = new Set(['image', 'env', 'routes', 'ports', 'command', 'entrypoint', 'sync_root', 'sync_enabled', 'sync_uid', 'sync_gid', 'registry_url', 'registry_auth']);
15
+ export const DEFAULT_OPENCLAW_IMAGE = 'ghcr.io/hypercli/hypercli-openclaw:pro-latest';
16
+ export const DEFAULT_OPENCLAW_PRO_IMAGE = 'ghcr.io/hypercli/hypercli-openclaw:pro-latest';
17
+ export const DEFAULT_OPENCODE_IMAGE = 'ghcr.io/hypercli/hypercli-opencode:latest';
18
+ export const DEFAULT_CODEX_IMAGE = 'ghcr.io/hypercli/hypercli-codex:latest';
19
+ export const DEFAULT_CLAUDE_CODE_IMAGE = 'ghcr.io/hypercli/hypercli-claude-code:latest';
20
+ export const DEFAULT_GOOSE_IMAGE = 'ghcr.io/hypercli/hypercli-goose:latest';
21
+ export const DEFAULT_KIMI_CODE_IMAGE = 'ghcr.io/hypercli/hypercli-kimi-code:latest';
22
+ export const DEFAULT_BUZZ_OPENCODE_IMAGE = 'ghcr.io/hypercli/hypercli-buzz-opencode:latest';
23
+ export const DEFAULT_BUZZ_CODEX_IMAGE = 'ghcr.io/hypercli/hypercli-buzz-codex:latest';
24
+ export const DEFAULT_BUZZ_CLAUDE_CODE_IMAGE = 'ghcr.io/hypercli/hypercli-buzz-claude:latest';
25
+ export const DEFAULT_BUZZ_GOOSE_IMAGE = 'ghcr.io/hypercli/hypercli-buzz-goose:latest';
26
+ export const DEFAULT_BUZZ_KIMI_CODE_IMAGE = 'ghcr.io/hypercli/hypercli-buzz-kimi-code:latest';
27
+ export const DEFAULT_CODING_AGENT_SYNC_ROOT = '/home/node';
28
+ export const DEFAULT_CODING_AGENT_IMAGES = {
29
+ opencode: DEFAULT_OPENCODE_IMAGE,
30
+ codex: DEFAULT_CODEX_IMAGE,
31
+ 'claude-code': DEFAULT_CLAUDE_CODE_IMAGE,
32
+ goose: DEFAULT_GOOSE_IMAGE,
33
+ 'kimi-code': DEFAULT_KIMI_CODE_IMAGE,
34
+ };
35
+ export const DEFAULT_BUZZ_CODING_AGENT_IMAGES = {
36
+ opencode: DEFAULT_BUZZ_OPENCODE_IMAGE,
37
+ codex: DEFAULT_BUZZ_CODEX_IMAGE,
38
+ 'claude-code': DEFAULT_BUZZ_CLAUDE_CODE_IMAGE,
39
+ goose: DEFAULT_BUZZ_GOOSE_IMAGE,
40
+ 'kimi-code': DEFAULT_BUZZ_KIMI_CODE_IMAGE,
41
+ };
42
+ const CODING_AGENT_RUNTIMES = new Set(['opencode', 'codex', 'claude-code', 'goose', 'kimi-code']);
43
+ const BUZZ_RUNTIME_COMMANDS = {
44
+ opencode: {
45
+ command: '/usr/local/bin/opencode',
46
+ args: ['acp'],
47
+ mcpCommand: '/usr/local/bin/buzz-dev-mcp',
48
+ },
49
+ codex: {
50
+ command: '/usr/local/bin/codex-acp',
51
+ args: [],
52
+ mcpCommand: '/usr/local/bin/buzz-dev-mcp',
53
+ },
54
+ 'claude-code': {
55
+ command: '/usr/local/bin/claude-agent-acp',
56
+ args: [],
57
+ mcpCommand: '',
58
+ },
59
+ goose: {
60
+ command: '/usr/local/bin/goose',
61
+ args: ['acp'],
62
+ mcpCommand: '',
63
+ },
64
+ 'kimi-code': {
65
+ command: '/usr/local/bin/kimi',
66
+ args: ['acp'],
67
+ mcpCommand: '',
68
+ },
69
+ };
70
+ export const DEFAULT_BUZZ_RUST_LOG = 'buzz_acp=info,pool::prompt=info,acp::stream=off';
71
+ const BUZZ_RESERVED_ENV_KEYS = new Set([
72
+ 'BUZZ_PRIVATE_KEY',
73
+ 'NOSTR_PRIVATE_KEY',
74
+ 'BUZZ_AUTH_TAG',
75
+ 'BUZZ_API_TOKEN',
76
+ 'BUZZ_ACP_PRIVATE_KEY',
77
+ 'BUZZ_ACP_API_TOKEN',
78
+ 'BUZZ_RELAY_URL',
79
+ 'BUZZ_ACP_AGENT_OWNER',
80
+ 'BUZZ_ACP_AGENT_COMMAND',
81
+ 'BUZZ_ACP_AGENT_ARGS',
82
+ 'BUZZ_ACP_MCP_COMMAND',
83
+ 'BUZZ_ACP_LAZY_POOL',
84
+ 'BUZZ_ACP_RELAY_OBSERVER',
85
+ 'BUZZ_ACP_SESSION_TITLE',
86
+ 'BUZZ_ACP_SYSTEM_PROMPT',
87
+ 'BUZZ_ACP_MODEL',
88
+ 'BUZZ_ACP_IDLE_TIMEOUT',
89
+ 'BUZZ_ACP_MAX_TURN_DURATION',
90
+ 'BUZZ_ACP_AGENTS',
91
+ 'BUZZ_ACP_RESPOND_TO',
92
+ 'BUZZ_ACP_RESPOND_TO_ALLOWLIST',
93
+ 'BUZZ_ACP_MULTIPLE_EVENT_HANDLING',
94
+ 'BUZZ_ACP_DEDUP',
95
+ 'BUZZ_ACP_SETUP_PAYLOAD',
96
+ 'BUZZ_MANAGED_AGENT',
97
+ 'BUZZ_MANAGED_AGENT_START_NONCE',
98
+ ]);
99
+ export const OPENCLAW_MEMORY_SEARCH_ENV_DEFAULTS = {
100
+ OPENCLAW_MEMORY_SEARCH_ENABLED: '1',
101
+ OPENCLAW_MEMORY_SEARCH_SYNC_ON_SESSION_START: '0',
102
+ OPENCLAW_MEMORY_SEARCH_SYNC_ON_SEARCH: '0',
103
+ OPENCLAW_MEMORY_SEARCH_SYNC_WATCH: '0',
104
+ OPENCLAW_MEMORY_SEARCH_SYNC_WATCH_DEBOUNCE_MS: '30000',
105
+ OPENCLAW_MEMORY_SEARCH_SYNC_INTERVAL_MINUTES: '0',
106
+ };
107
+ export const OPENCLAW_WORKSPACES_SYNC_ENV_DEFAULTS = {
108
+ HYPER_WORKSPACES_BOOT_SYNC: '1',
109
+ HYPER_WORKSPACES_DIR: '/home/node/workspaces',
110
+ HYPER_WORKSPACES_SYNC_READY_ONLY: '1',
111
+ };
112
+ const LAUNCH_CONFIG_KEYS = new Set([
113
+ 'image',
114
+ 'env',
115
+ 'routes',
116
+ 'ports',
117
+ 'command',
118
+ 'entrypoint',
119
+ 'sync_root',
120
+ 'sync_enabled',
121
+ 'sync_uid',
122
+ 'sync_gid',
123
+ 'registry_url',
124
+ 'registry_auth',
125
+ 'restart',
126
+ ]);
16
127
  const DEFAULT_OPENCLAW_SYNC_ROOT = '/home/node';
17
- export const AGENT_FILE_MAX_BYTES = 50 * 1024 * 1024;
128
+ export const AGENT_FILE_MAX_BYTES = 250 * 1024 * 1024;
18
129
  export const AGENT_FILE_TRANSFER_CHUNK_BYTES = 64 * 1024;
19
130
  export const AGENT_FILE_OPERATION_TIMEOUT_MS = 300_000;
131
+ function shellAbortError() {
132
+ const error = new Error('Shell connection cancelled');
133
+ error.name = 'AbortError';
134
+ return error;
135
+ }
136
+ function runShellOperation(operation, signal, timeoutMs, timeoutMessage) {
137
+ return new Promise((resolve, reject) => {
138
+ const controller = new AbortController();
139
+ let settled = false;
140
+ const cleanup = () => {
141
+ clearTimeout(timer);
142
+ signal?.removeEventListener('abort', abortOperation);
143
+ };
144
+ const finish = (error, value) => {
145
+ if (settled)
146
+ return;
147
+ settled = true;
148
+ cleanup();
149
+ if (error)
150
+ reject(error);
151
+ else
152
+ resolve(value);
153
+ };
154
+ const abortOperation = () => {
155
+ controller.abort(signal?.reason);
156
+ finish(shellAbortError());
157
+ };
158
+ const timer = setTimeout(() => {
159
+ controller.abort();
160
+ finish(new Error(timeoutMessage));
161
+ }, timeoutMs);
162
+ if (signal?.aborted) {
163
+ abortOperation();
164
+ return;
165
+ }
166
+ signal?.addEventListener('abort', abortOperation, { once: true });
167
+ void operation(controller.signal).then((value) => finish(undefined, value), (error) => {
168
+ if (error instanceof Error && error.name === 'AbortError' && !signal?.aborted) {
169
+ finish(new Error(timeoutMessage));
170
+ }
171
+ else {
172
+ finish(error);
173
+ }
174
+ });
175
+ });
176
+ }
177
+ function buildBuzzLaunchEnv(runtime, buzz, defaultSessionTitle) {
178
+ if (!buzz.privateKeyNsec.trim())
179
+ throw new Error('buzz.privateKeyNsec is required');
180
+ if (!buzz.relayUrl.trim())
181
+ throw new Error('buzz.relayUrl is required');
182
+ const parallelism = buzz.parallelism ?? 1;
183
+ if (!Number.isInteger(parallelism) || parallelism < 1 || parallelism > 32) {
184
+ throw new Error('buzz.parallelism must be between 1 and 32');
185
+ }
186
+ const harness = BUZZ_RUNTIME_COMMANDS[runtime];
187
+ const env = {
188
+ BUZZ_PRIVATE_KEY: buzz.privateKeyNsec,
189
+ NOSTR_PRIVATE_KEY: buzz.privateKeyNsec,
190
+ BUZZ_RELAY_URL: buzz.relayUrl,
191
+ BUZZ_ACP_AGENT_COMMAND: harness.command,
192
+ BUZZ_ACP_AGENT_ARGS: harness.args.join(','),
193
+ BUZZ_ACP_MCP_COMMAND: harness.mcpCommand,
194
+ BUZZ_ACP_LAZY_POOL: 'true',
195
+ BUZZ_ACP_RELAY_OBSERVER: 'true',
196
+ BUZZ_ACP_AGENTS: String(parallelism),
197
+ BUZZ_ACP_MULTIPLE_EVENT_HANDLING: 'steer',
198
+ BUZZ_ACP_DEDUP: 'queue',
199
+ };
200
+ if (buzz.rustLog)
201
+ env.RUST_LOG = buzz.rustLog;
202
+ const optional = {
203
+ BUZZ_AUTH_TAG: buzz.authTag,
204
+ BUZZ_ACP_SESSION_TITLE: buzz.sessionTitle || defaultSessionTitle,
205
+ BUZZ_ACP_SYSTEM_PROMPT: buzz.systemPrompt,
206
+ BUZZ_ACP_MODEL: buzz.model,
207
+ BUZZ_ACP_IDLE_TIMEOUT: buzz.idleTimeoutSeconds == null
208
+ ? undefined
209
+ : String(buzz.idleTimeoutSeconds),
210
+ BUZZ_ACP_MAX_TURN_DURATION: buzz.maxTurnDurationSeconds == null
211
+ ? undefined
212
+ : String(buzz.maxTurnDurationSeconds),
213
+ BUZZ_ACP_RESPOND_TO: buzz.respondTo,
214
+ BUZZ_ACP_RESPOND_TO_ALLOWLIST: buzz.respondToAllowlist?.length
215
+ ? buzz.respondToAllowlist.join(',')
216
+ : undefined,
217
+ };
218
+ for (const [key, value] of Object.entries(optional)) {
219
+ if (value)
220
+ env[key] = value;
221
+ }
222
+ return env;
223
+ }
224
+ /** The agent's sync root and workspace, owned by the SDK so paths are unambiguous. */
225
+ export const OPENCLAW_SYNC_ROOT = '/home/node';
226
+ export const OPENCLAW_WORKSPACE_PREFIX = '.openclaw/workspace';
227
+ /** Map the friendly `agent`/`backup` names to their on-the-wire `pod`/`s3` values. */
228
+ export function toWireFileSource(source) {
229
+ switch (source) {
230
+ case 'agent': return 'pod';
231
+ case 'backup': return 's3';
232
+ default: return source; // 'auto' | 'pod' | 's3'
233
+ }
234
+ }
235
+ const GATEWAY_FILE_SOURCES = new Set(['gateway']);
236
+ const FULL_FS_FILE_SOURCES = new Set(['agent', 'pod']);
237
+ /**
238
+ * Resolve a caller path to the deployment HTTP files path (sync-root relative).
239
+ * Workspace-relative by default (prefixed with the workspace); an absolute `/…`
240
+ * path is read-only full-fs and only valid for the `agent` source.
241
+ */
242
+ function resolveBackendFilePath(path, source) {
243
+ if (path.startsWith('/')) {
244
+ if (!FULL_FS_FILE_SOURCES.has(source)) {
245
+ throw new Error(`absolute paths need the 'agent' source (full pod filesystem); ` +
246
+ `'${source}' is scoped to ${source === 'backup' || source === 's3' ? 'the sync root' : 'the workspace'}.`);
247
+ }
248
+ return path; // full-fs path, passed through to source=pod
249
+ }
250
+ const rel = stripRelPrefix(path);
251
+ return rel ? `${OPENCLAW_WORKSPACE_PREFIX}/${rel}` : OPENCLAW_WORKSPACE_PREFIX;
252
+ }
253
+ function normalizeWritableBackendFilePath(path) {
254
+ const normalized = path.replace(/\\/g, '/');
255
+ if (normalized.startsWith('/')) {
256
+ const parts = normalized.split('/');
257
+ const resolved = [];
258
+ for (const part of parts) {
259
+ if (!part || part === '.')
260
+ continue;
261
+ if (part === '..') {
262
+ resolved.pop();
263
+ continue;
264
+ }
265
+ resolved.push(part);
266
+ }
267
+ const absolute = `/${resolved.join('/')}`;
268
+ const prefix = `${OPENCLAW_SYNC_ROOT}/`;
269
+ if (!absolute.startsWith(prefix)) {
270
+ throw new Error(`absolute write paths must stay within the sync root (${OPENCLAW_SYNC_ROOT}).`);
271
+ }
272
+ return absolute.slice(prefix.length);
273
+ }
274
+ if (normalized.split('/').includes('..')) {
275
+ throw new Error("paths containing '..' are not writable; writes and deletes must stay within the sync root.");
276
+ }
277
+ return stripRelPrefix(normalized);
278
+ }
279
+ /** Strip leading `./` segments and slashes without eating a dotfile's dot. */
280
+ function stripRelPrefix(path) {
281
+ return path.replace(/^(?:\.\/)+/, '').replace(/^\/+/, '');
282
+ }
283
+ function isUuidRef(value) {
284
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value.trim());
285
+ }
286
+ function isDirectAgentIdRef(value) {
287
+ const raw = value.trim();
288
+ return isUuidRef(raw) || /^[0-9a-f]{6,}$/i.test(raw) || /^(agent|external)[-_:]/i.test(raw);
289
+ }
290
+ /** Strip a workspace prefix so gateway (name-addressed) sees the bare name. */
291
+ function resolveGatewayFileName(path) {
292
+ if (path.startsWith('/')) {
293
+ throw new Error("absolute paths are not valid for the 'gateway' source (scoped to the workspace).");
294
+ }
295
+ const rel = stripRelPrefix(path);
296
+ return rel.startsWith(`${OPENCLAW_WORKSPACE_PREFIX}/`)
297
+ ? rel.slice(OPENCLAW_WORKSPACE_PREFIX.length + 1)
298
+ : rel;
299
+ }
300
+ /**
301
+ * ONE client wrapping all three agent file-access paths behind a single `source`
302
+ * switch (`agent` | `backup` | `gateway`, plus `auto`). The SDK owns the roots,
303
+ * so a workspace-relative path is the same file on every source; `agent` also
304
+ * takes absolute `/…` paths for read-only full-filesystem browsing. The
305
+ * underlying APIs are left as-is — this only wraps them.
306
+ */
307
+ export class AgentFiles {
308
+ agent;
309
+ deployments;
310
+ gatewayHooks;
311
+ constructor(agent, deployments, gatewayHooks) {
312
+ this.agent = agent;
313
+ this.deployments = deployments;
314
+ this.gatewayHooks = gatewayHooks;
315
+ }
316
+ requireGateway() {
317
+ if (!this.gatewayHooks) {
318
+ throw new Error('gateway file source requires an OpenClaw agent with a gateway URL/token.');
319
+ }
320
+ return this.gatewayHooks;
321
+ }
322
+ async list(path = '', source = 'auto') {
323
+ if (!GATEWAY_FILE_SOURCES.has(source)) {
324
+ return this.deployments.filesList(this.agent, resolveBackendFilePath(path, source), toWireFileSource(source));
325
+ }
326
+ const gw = this.requireGateway();
327
+ const name = resolveGatewayFileName(path);
328
+ const files = await gw.withGateway(async (c) => c.filesList(await gw.resolveAgentId(c)));
329
+ return (files ?? [])
330
+ .map((f) => ({ name: f.name, path: f.name, type: 'file', size: f.size }))
331
+ .filter((f) => !name || f.name === name || f.name.startsWith(name.endsWith('/') ? name : `${name}/`));
332
+ }
333
+ async readBytes(path, source = 'auto', options) {
334
+ if (!GATEWAY_FILE_SOURCES.has(source)) {
335
+ const resolvedPath = resolveBackendFilePath(path, source);
336
+ const wireSource = toWireFileSource(source);
337
+ return options
338
+ ? this.deployments.fileReadBytes(this.agent, resolvedPath, wireSource, options)
339
+ : this.deployments.fileReadBytes(this.agent, resolvedPath, wireSource);
340
+ }
341
+ return (await this.readBytesWithMetadata(path, source, options)).content;
342
+ }
343
+ async readBytesWithMetadata(path, source = 'auto', options) {
344
+ if (!GATEWAY_FILE_SOURCES.has(source)) {
345
+ return this.deployments.fileReadBytesWithMetadata(this.agent, resolveBackendFilePath(path, source), toWireFileSource(source), options);
346
+ }
347
+ if (options?.signal?.aborted) {
348
+ const error = new Error('File read cancelled');
349
+ error.name = 'AbortError';
350
+ throw error;
351
+ }
352
+ const content = encodeUtf8(await this.read(path, source));
353
+ if (options?.maxBytes !== undefined && content.byteLength > options.maxBytes) {
354
+ throw fileReadLimitError(path, options.maxBytes);
355
+ }
356
+ return { content, mimeType: 'text/plain' };
357
+ }
358
+ async read(path, source = 'auto', options) {
359
+ if (!GATEWAY_FILE_SOURCES.has(source)) {
360
+ return decodeUtf8(await this.readBytes(path, source, options));
361
+ }
362
+ const gw = this.requireGateway();
363
+ return gw.withGateway(async (c) => c.fileGet(await gw.resolveAgentId(c), resolveGatewayFileName(path)));
364
+ }
365
+ async writeBytes(path, content, source = 'auto') {
366
+ if (!GATEWAY_FILE_SOURCES.has(source)) {
367
+ const writablePath = normalizeWritableBackendFilePath(path);
368
+ const resolvedPath = path.startsWith('/')
369
+ ? writablePath
370
+ : resolveBackendFilePath(writablePath, source);
371
+ return this.deployments.fileWriteBytes(this.agent, resolvedPath, content, toWireFileSource(source));
372
+ }
373
+ return this.write(path, coerceToUtf8String(content), source);
374
+ }
375
+ async write(path, content, source = 'auto') {
376
+ if (!GATEWAY_FILE_SOURCES.has(source)) {
377
+ const writablePath = normalizeWritableBackendFilePath(path);
378
+ const resolvedPath = path.startsWith('/')
379
+ ? writablePath
380
+ : resolveBackendFilePath(writablePath, source);
381
+ return this.deployments.fileWrite(this.agent, resolvedPath, content, toWireFileSource(source));
382
+ }
383
+ const gw = this.requireGateway();
384
+ const name = resolveGatewayFileName(path);
385
+ const agentId = await gw.withGateway(async (c) => {
386
+ const aid = await gw.resolveAgentId(c);
387
+ await c.fileSet(aid, name, content);
388
+ return aid;
389
+ });
390
+ return { name, source: 'gateway', agentId };
391
+ }
392
+ async delete(path, options = {}) {
393
+ const source = options.source ?? 'auto';
394
+ if (GATEWAY_FILE_SOURCES.has(source)) {
395
+ throw new Error('delete is not supported over the gateway file source; use the agent/backup source.');
396
+ }
397
+ const writablePath = normalizeWritableBackendFilePath(path);
398
+ const resolvedPath = path.startsWith('/')
399
+ ? writablePath
400
+ : resolveBackendFilePath(writablePath, source);
401
+ return this.deployments.fileDelete(this.agent, resolvedPath, {
402
+ recursive: options.recursive,
403
+ source: toWireFileSource(source),
404
+ });
405
+ }
406
+ }
20
407
  function parseDate(value) {
21
408
  if (typeof value !== 'string' || !value)
22
409
  return null;
@@ -43,6 +430,8 @@ function sleep(ms) {
43
430
  return new Promise((resolve) => setTimeout(resolve, ms));
44
431
  }
45
432
  function isOpenClawHydrationData(data) {
433
+ if (data.runtime === 'openclaw' || data.runtime === 'openclaw-pro')
434
+ return true;
46
435
  const routes = data.routes;
47
436
  if (routes && typeof routes === 'object' && !Array.isArray(routes) && routes.openclaw) {
48
437
  return true;
@@ -50,6 +439,133 @@ function isOpenClawHydrationData(data) {
50
439
  const launchRoutes = data.launch_config?.routes;
51
440
  return !!(launchRoutes && typeof launchRoutes === 'object' && !Array.isArray(launchRoutes) && launchRoutes.openclaw);
52
441
  }
442
+ function isCodingAgentRuntime(runtime) {
443
+ return typeof runtime === 'string' && CODING_AGENT_RUNTIMES.has(runtime);
444
+ }
445
+ function isTruthyEnv(value) {
446
+ return ['1', 'true', 'yes', 'on', 'enabled'].includes(String(value ?? '').trim().toLowerCase());
447
+ }
448
+ function isPlainRecord(value) {
449
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
450
+ }
451
+ function flattenConfigValue(value, prefix, out) {
452
+ if (!prefix) {
453
+ if (isPlainRecord(value)) {
454
+ for (const [key, child] of Object.entries(value)) {
455
+ flattenConfigValue(child, key, out);
456
+ }
457
+ return;
458
+ }
459
+ out[''] = value;
460
+ return;
461
+ }
462
+ out[prefix] = value;
463
+ if (Array.isArray(value)) {
464
+ value.forEach((child, index) => flattenConfigValue(child, `${prefix}[${index}]`, out));
465
+ return;
466
+ }
467
+ if (isPlainRecord(value)) {
468
+ for (const [key, child] of Object.entries(value)) {
469
+ flattenConfigValue(child, `${prefix}.${key}`, out);
470
+ }
471
+ }
472
+ }
473
+ export function flattenLaunchConfig(launchConfig) {
474
+ const flat = {};
475
+ if (!isPlainRecord(launchConfig))
476
+ return flat;
477
+ flattenConfigValue(launchConfig, '', flat);
478
+ return flat;
479
+ }
480
+ function pathParts(path) {
481
+ if (Array.isArray(path))
482
+ return path;
483
+ const parts = [];
484
+ for (const part of path.replace(/\[(\d+)\]/g, '.$1').split('.')) {
485
+ if (!part)
486
+ continue;
487
+ parts.push(/^\d+$/.test(part) ? Number(part) : part);
488
+ }
489
+ return parts;
490
+ }
491
+ export function getLaunchConfigValue(launchConfig, path) {
492
+ let current = launchConfig;
493
+ for (const part of pathParts(path)) {
494
+ if (typeof part === 'number') {
495
+ if (!Array.isArray(current))
496
+ return undefined;
497
+ current = current[part];
498
+ continue;
499
+ }
500
+ if (!isPlainRecord(current))
501
+ return undefined;
502
+ current = current[part];
503
+ }
504
+ return current;
505
+ }
506
+ export function routesHaveDesktop(routes) {
507
+ if (!isPlainRecord(routes))
508
+ return false;
509
+ if (isPlainRecord(routes.desktop))
510
+ return true;
511
+ return Object.values(routes).some((route) => isPlainRecord(route) && route.prefix === 'desktop');
512
+ }
513
+ export function portsHaveDesktop(ports) {
514
+ return Array.isArray(ports) && ports.some((port) => isPlainRecord(port) && Number(port.port) === 3000);
515
+ }
516
+ export function launchConfigHasDesktop(launchConfig) {
517
+ if (!isPlainRecord(launchConfig))
518
+ return false;
519
+ if (isTruthyEnv(getLaunchConfigValue(launchConfig, 'env.OPENCLAW_DESKTOP_ENABLED')))
520
+ return true;
521
+ if (routesHaveDesktop(getLaunchConfigValue(launchConfig, 'routes')))
522
+ return true;
523
+ return portsHaveDesktop(getLaunchConfigValue(launchConfig, 'ports'));
524
+ }
525
+ export function agentConfigHasDesktop(source) {
526
+ if (!source)
527
+ return false;
528
+ return (launchConfigHasDesktop(source.launchConfig ?? source.launch_config) ||
529
+ routesHaveDesktop(source.routes) ||
530
+ portsHaveDesktop(source.ports));
531
+ }
532
+ function browserDesktopRedirectPath(options = {}) {
533
+ const redirect = (options.redirect ?? 'vnc.html').trim() || 'vnc.html';
534
+ if (redirect.includes('\\')) {
535
+ throw new Error('Desktop redirect must be a relative path');
536
+ }
537
+ const base = 'https://desktop.local';
538
+ const parsed = new URL(redirect, `${base}/`);
539
+ if (parsed.origin !== base) {
540
+ throw new Error('Desktop redirect must be a relative path');
541
+ }
542
+ if (options.resize !== null) {
543
+ const resize = options.resize ?? 'scale';
544
+ if (resize.trim())
545
+ parsed.searchParams.set('resize', resize);
546
+ }
547
+ const pathname = parsed.pathname.replace(/^\/+/, '') || 'vnc.html';
548
+ return `${pathname}${parsed.search}${parsed.hash}`;
549
+ }
550
+ export function buildBrowserDesktopUrl(desktopBaseUrl, token, options = {}) {
551
+ const jwt = token.trim();
552
+ if (!jwt)
553
+ throw new Error('Desktop token is required');
554
+ const url = new URL('/_jwt_auth', desktopBaseUrl);
555
+ url.searchParams.set('jwt', jwt);
556
+ url.searchParams.set('redirect', browserDesktopRedirectPath(options));
557
+ return url.toString();
558
+ }
559
+ function isOpenClawProHydrationData(data) {
560
+ const launchConfig = data.launch_config;
561
+ if (!launchConfig || typeof launchConfig !== 'object' || Array.isArray(launchConfig))
562
+ return false;
563
+ if (launchConfigHasDesktop(launchConfig)) {
564
+ return true;
565
+ }
566
+ const image = String(launchConfig.image ?? '');
567
+ return image.includes('hypercli-openclaw:pro') || image.endsWith('-pro');
568
+ }
53
569
  function isDirectoryListingPayload(value) {
54
570
  if (!value || typeof value !== 'object')
55
571
  return false;
@@ -141,12 +657,78 @@ function encodeFilePath(path) {
141
657
  .map((part) => encodeURIComponent(part))
142
658
  .join('/');
143
659
  }
660
+ function deploymentFileReadRequest(path, source) {
661
+ const params = new URLSearchParams({ source });
662
+ if (path.startsWith('/')) {
663
+ if (source !== 'pod') {
664
+ throw new Error("absolute paths require source='pod'.");
665
+ }
666
+ params.set('absolute_path', path);
667
+ return { suffix: '', params };
668
+ }
669
+ const encodedPath = encodeFilePath(path);
670
+ return { suffix: encodedPath ? `/${encodedPath}` : '', params };
671
+ }
144
672
  function decodeUtf8(content) {
145
673
  return new TextDecoder().decode(content);
146
674
  }
147
675
  function encodeUtf8(content) {
148
676
  return new TextEncoder().encode(content);
149
677
  }
678
+ function fileReadLimitError(path, maxBytes) {
679
+ return new Error(`File ${path} exceeds the ${maxBytes / 1024 / 1024} MiB read limit`);
680
+ }
681
+ async function readResponseBytes(response, path, maxBytes) {
682
+ if (maxBytes === undefined)
683
+ return new Uint8Array(await response.arrayBuffer());
684
+ if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) {
685
+ throw new RangeError('maxBytes must be a non-negative safe integer');
686
+ }
687
+ const contentLength = Number(response.headers.get('content-length'));
688
+ if (Number.isFinite(contentLength) && contentLength > maxBytes) {
689
+ await response.body?.cancel();
690
+ throw fileReadLimitError(path, maxBytes);
691
+ }
692
+ if (!response.body) {
693
+ const bytes = new Uint8Array(await response.arrayBuffer());
694
+ if (bytes.byteLength > maxBytes)
695
+ throw fileReadLimitError(path, maxBytes);
696
+ return bytes;
697
+ }
698
+ const reader = response.body.getReader();
699
+ const chunks = [];
700
+ let totalBytes = 0;
701
+ try {
702
+ while (true) {
703
+ const { done, value } = await reader.read();
704
+ if (done)
705
+ break;
706
+ totalBytes += value.byteLength;
707
+ if (totalBytes > maxBytes) {
708
+ await reader.cancel();
709
+ throw fileReadLimitError(path, maxBytes);
710
+ }
711
+ chunks.push(value);
712
+ }
713
+ }
714
+ finally {
715
+ reader.releaseLock();
716
+ }
717
+ const bytes = new Uint8Array(totalBytes);
718
+ let offset = 0;
719
+ for (const chunk of chunks) {
720
+ bytes.set(chunk, offset);
721
+ offset += chunk.byteLength;
722
+ }
723
+ return bytes;
724
+ }
725
+ /** Coerce write content to a string for the gateway (text-only) file source. */
726
+ function coerceToUtf8String(content) {
727
+ if (typeof content === 'string')
728
+ return content;
729
+ const bytes = content instanceof Uint8Array ? content : new Uint8Array(content);
730
+ return new TextDecoder('utf-8', { fatal: true }).decode(bytes);
731
+ }
150
732
  function toUint8Array(content) {
151
733
  if (typeof content === 'string')
152
734
  return encodeUtf8(content);
@@ -169,6 +751,16 @@ function agentStateFromDict(data) {
169
751
  podName: data.pod_name ?? '',
170
752
  state: data.state ?? 'unknown',
171
753
  name: data.name ?? null,
754
+ handle: data.handle ?? null,
755
+ displayName: data.display_name ?? data.name ?? null,
756
+ avatarUrl: data.avatar_url ?? null,
757
+ displayIdentity: data.display_identity ? structuredClone(data.display_identity) : null,
758
+ runtime: data.runtime ?? null,
759
+ managed: data.managed ?? null,
760
+ isLaunchable: data.is_launchable ?? data.managed !== false,
761
+ gatewayId: data.gateway_id ?? null,
762
+ runtimeKeyAlias: data.runtime_key_alias ?? null,
763
+ relayKey: data.relay_key ?? null,
172
764
  cpu: data.cpu ?? 0,
173
765
  memory: data.memory ?? 0,
174
766
  hostname: data.hostname ?? null,
@@ -210,15 +802,18 @@ export function buildAgentConfig(config = {}, options = {}) {
210
802
  preparedConfig.agents = agentsConfig;
211
803
  }
212
804
  const env = { ...(options.env ?? {}) };
213
- let gatewayToken = options.gatewayToken?.trim() || env.OPENCLAW_GATEWAY_TOKEN?.trim() || '';
214
- if (!gatewayToken) {
215
- gatewayToken = randomHexToken(32);
216
- }
217
- env.OPENCLAW_GATEWAY_TOKEN = gatewayToken;
218
- if (options.controlUiOriginLock !== false && !env.OPENCLAW_CONTROL_UI_ALLOWED_ORIGIN?.trim()) {
219
- const controlUiOrigin = defaultControlUiAllowedOrigin();
220
- if (controlUiOrigin) {
221
- env.OPENCLAW_CONTROL_UI_ALLOWED_ORIGIN = controlUiOrigin;
805
+ let gatewayToken = '';
806
+ if (options.injectGatewayToken !== false) {
807
+ gatewayToken = options.gatewayToken?.trim() || env.OPENCLAW_GATEWAY_TOKEN?.trim() || '';
808
+ if (!gatewayToken) {
809
+ gatewayToken = randomHexToken(32);
810
+ }
811
+ env.OPENCLAW_GATEWAY_TOKEN = gatewayToken;
812
+ if (options.controlUiOriginLock !== false && !env.OPENCLAW_CONTROL_UI_ALLOWED_ORIGIN?.trim()) {
813
+ const controlUiOrigin = defaultControlUiAllowedOrigin();
814
+ if (controlUiOrigin) {
815
+ env.OPENCLAW_CONTROL_UI_ALLOWED_ORIGIN = controlUiOrigin;
816
+ }
222
817
  }
223
818
  }
224
819
  const prepared = {};
@@ -248,6 +843,8 @@ export function buildAgentConfig(config = {}, options = {}) {
248
843
  prepared.registry_url = options.registryUrl;
249
844
  if (options.registryAuth !== undefined && options.registryAuth !== null)
250
845
  prepared.registry_auth = options.registryAuth;
846
+ if (options.restart !== undefined && options.restart !== null)
847
+ prepared.restart = options.restart;
251
848
  return { config: prepared, gatewayToken };
252
849
  }
253
850
  function defaultOpenClawImage(image) {
@@ -255,6 +852,208 @@ function defaultOpenClawImage(image) {
255
852
  return image;
256
853
  return DEFAULT_OPENCLAW_IMAGE;
257
854
  }
855
+ function defaultOpenClawProImage(image) {
856
+ if (image !== undefined && image !== null)
857
+ return image;
858
+ return DEFAULT_OPENCLAW_PRO_IMAGE;
859
+ }
860
+ function productApiBaseFromAgentsApiBase(apiBase) {
861
+ const normalized = apiBase.replace(/\/+$/, '');
862
+ const agentsSuffix = '/agents';
863
+ return normalized.endsWith(agentsSuffix) ? normalized.slice(0, -agentsSuffix.length) : normalized;
864
+ }
865
+ export async function startSlackOAuth(options) {
866
+ const relayBaseUrl = normalizeSlackRelayBaseUrl(options.relayBaseUrl);
867
+ if (!relayBaseUrl)
868
+ throw new Error('Slack relay base URL is required');
869
+ if (!options.token)
870
+ throw new Error('Slack OAuth requires an app token');
871
+ const response = await fetch(`${relayBaseUrl}/slack/oauth/start`, {
872
+ method: 'GET',
873
+ headers: {
874
+ 'Authorization': `Bearer ${options.token}`,
875
+ },
876
+ });
877
+ if (!response.ok) {
878
+ let detail = response.statusText || 'Slack OAuth start failed';
879
+ try {
880
+ const payload = await response.json();
881
+ if (typeof payload.detail === 'string' && payload.detail)
882
+ detail = payload.detail;
883
+ }
884
+ catch {
885
+ // Keep the HTTP status text when the error body is not JSON.
886
+ }
887
+ throw new APIError(response.status, detail);
888
+ }
889
+ const payload = await response.json();
890
+ const authorizeUrl = typeof payload.authorize_url === 'string' ? payload.authorize_url : '';
891
+ if (!authorizeUrl)
892
+ throw new Error('Slack OAuth start did not return an authorization URL');
893
+ return {
894
+ authorizeUrl,
895
+ expiresAt: typeof payload.expires_at === 'string' ? payload.expires_at : null,
896
+ };
897
+ }
898
+ export async function getSlackInstallStatus(options) {
899
+ const relayBaseUrl = normalizeSlackRelayBaseUrl(options.relayBaseUrl);
900
+ if (!relayBaseUrl)
901
+ throw new Error('Slack relay base URL is required');
902
+ if (!options.token)
903
+ throw new Error('Slack install status requires an app token');
904
+ const response = await fetch(`${relayBaseUrl}/slack/install`, {
905
+ method: 'GET',
906
+ headers: { 'Authorization': `Bearer ${options.token}` },
907
+ });
908
+ if (!response.ok) {
909
+ let detail = response.statusText || 'Slack install status failed';
910
+ try {
911
+ const payload = await response.json();
912
+ if (typeof payload.detail === 'string' && payload.detail)
913
+ detail = payload.detail;
914
+ }
915
+ catch {
916
+ // Keep the HTTP status text when the error body is not JSON.
917
+ }
918
+ throw new APIError(response.status, detail);
919
+ }
920
+ const payload = await response.json();
921
+ return {
922
+ connected: payload.connected === true,
923
+ teamId: typeof payload.team_id === 'string' ? payload.team_id : null,
924
+ teamName: typeof payload.team_name === 'string' ? payload.team_name : null,
925
+ botUserId: typeof payload.bot_user_id === 'string' ? payload.bot_user_id : null,
926
+ installerUserId: typeof payload.installer_user_id === 'string' ? payload.installer_user_id : null,
927
+ updatedAt: typeof payload.updated_at === 'string' ? payload.updated_at : null,
928
+ };
929
+ }
930
+ function slackRelayAuthorizedUrl(path, options) {
931
+ const relayBaseUrl = normalizeSlackRelayBaseUrl(options.relayBaseUrl);
932
+ if (!relayBaseUrl)
933
+ throw new Error('Slack relay base URL is required');
934
+ if (!options.token)
935
+ throw new Error('Slack directory lookup requires an app token');
936
+ const url = new URL(path, `${relayBaseUrl}/`);
937
+ if (options.cursor)
938
+ url.searchParams.set('cursor', options.cursor);
939
+ if (typeof options.limit === 'number' && Number.isFinite(options.limit)) {
940
+ url.searchParams.set('limit', String(Math.trunc(options.limit)));
941
+ }
942
+ return url;
943
+ }
944
+ export async function listSlackDirectoryConversations(options) {
945
+ const url = slackRelayAuthorizedUrl('/slack/directory/conversations', options);
946
+ if (options.types)
947
+ url.searchParams.set('types', options.types);
948
+ const response = await fetch(url.toString(), {
949
+ method: 'GET',
950
+ headers: { 'Authorization': `Bearer ${options.token}` },
951
+ });
952
+ if (!response.ok) {
953
+ let detail = response.statusText || 'Slack conversation lookup failed';
954
+ try {
955
+ const payload = await response.json();
956
+ if (typeof payload.detail === 'string' && payload.detail)
957
+ detail = payload.detail;
958
+ }
959
+ catch {
960
+ // Keep the HTTP status text when the error body is not JSON.
961
+ }
962
+ throw new APIError(response.status, detail);
963
+ }
964
+ const payload = await response.json();
965
+ const conversations = Array.isArray(payload.conversations) ? payload.conversations : [];
966
+ return {
967
+ conversations: conversations
968
+ .filter((item) => Boolean(item && typeof item === 'object' && !Array.isArray(item) && typeof item.id === 'string'))
969
+ .map((item) => ({
970
+ id: item.id,
971
+ name: typeof item.name === 'string' ? item.name : null,
972
+ isChannel: typeof item.is_channel === 'boolean' ? item.is_channel : null,
973
+ isGroup: typeof item.is_group === 'boolean' ? item.is_group : null,
974
+ isIm: typeof item.is_im === 'boolean' ? item.is_im : null,
975
+ isMpim: typeof item.is_mpim === 'boolean' ? item.is_mpim : null,
976
+ isMember: typeof item.is_member === 'boolean' ? item.is_member : null,
977
+ isPrivate: typeof item.is_private === 'boolean' ? item.is_private : null,
978
+ })),
979
+ nextCursor: typeof payload.next_cursor === 'string' ? payload.next_cursor : null,
980
+ };
981
+ }
982
+ export async function listSlackDirectoryUsers(options) {
983
+ const url = slackRelayAuthorizedUrl('/slack/directory/users', options);
984
+ const response = await fetch(url.toString(), {
985
+ method: 'GET',
986
+ headers: { 'Authorization': `Bearer ${options.token}` },
987
+ });
988
+ if (!response.ok) {
989
+ let detail = response.statusText || 'Slack user lookup failed';
990
+ try {
991
+ const payload = await response.json();
992
+ if (typeof payload.detail === 'string' && payload.detail)
993
+ detail = payload.detail;
994
+ }
995
+ catch {
996
+ // Keep the HTTP status text when the error body is not JSON.
997
+ }
998
+ throw new APIError(response.status, detail);
999
+ }
1000
+ const payload = await response.json();
1001
+ const users = Array.isArray(payload.users) ? payload.users : [];
1002
+ return {
1003
+ users: users
1004
+ .filter((item) => Boolean(item && typeof item === 'object' && !Array.isArray(item) && typeof item.id === 'string'))
1005
+ .map((item) => ({
1006
+ id: item.id,
1007
+ name: typeof item.name === 'string' ? item.name : null,
1008
+ realName: typeof item.real_name === 'string' ? item.real_name : null,
1009
+ teamId: typeof item.team_id === 'string' ? item.team_id : null,
1010
+ isBot: typeof item.is_bot === 'boolean' ? item.is_bot : null,
1011
+ deleted: typeof item.deleted === 'boolean' ? item.deleted : null,
1012
+ })),
1013
+ nextCursor: typeof payload.next_cursor === 'string' ? payload.next_cursor : null,
1014
+ };
1015
+ }
1016
+ export async function attachSlackRelayAgent(options) {
1017
+ const relayBaseUrl = normalizeSlackRelayBaseUrl(options.relayBaseUrl);
1018
+ const agentId = options.agentId.trim();
1019
+ if (!relayBaseUrl)
1020
+ throw new Error('Slack relay base URL is required');
1021
+ if (!options.token)
1022
+ throw new Error('Slack relay attach requires an app token');
1023
+ if (!agentId)
1024
+ throw new Error('Slack relay attach requires an agent id');
1025
+ const response = await fetch(`${relayBaseUrl}/slack/agents/${encodeURIComponent(agentId)}/relay`, {
1026
+ method: 'POST',
1027
+ headers: { 'Authorization': `Bearer ${options.token}` },
1028
+ });
1029
+ if (!response.ok) {
1030
+ let detail = response.statusText || 'Slack relay attach failed';
1031
+ try {
1032
+ const payload = await response.json();
1033
+ if (typeof payload.detail === 'string' && payload.detail)
1034
+ detail = payload.detail;
1035
+ }
1036
+ catch {
1037
+ // Keep the HTTP status text when the error body is not JSON.
1038
+ }
1039
+ throw new APIError(response.status, detail);
1040
+ }
1041
+ const payload = await response.json();
1042
+ const agentIdValue = typeof payload.agent_id === 'string' ? payload.agent_id : '';
1043
+ const gatewayIdValue = typeof payload.gateway_id === 'string' ? payload.gateway_id : '';
1044
+ if (!agentIdValue || !gatewayIdValue)
1045
+ throw new Error('Slack relay attach response is missing agent identity');
1046
+ return {
1047
+ connected: payload.connected === true,
1048
+ agentId: agentIdValue,
1049
+ gatewayId: gatewayIdValue,
1050
+ config: payload.config && typeof payload.config === 'object' && !Array.isArray(payload.config) ? payload.config : {},
1051
+ restartRequired: payload.restart_required !== false,
1052
+ teamId: typeof payload.team_id === 'string' ? payload.team_id : null,
1053
+ teamName: typeof payload.team_name === 'string' ? payload.team_name : null,
1054
+ botUserId: typeof payload.bot_user_id === 'string' ? payload.bot_user_id : null,
1055
+ };
1056
+ }
258
1057
  export function buildOpenClawRoutes(options = {}) {
259
1058
  const routes = {};
260
1059
  if (options.includeGateway ?? true) {
@@ -264,7 +1063,7 @@ export function buildOpenClawRoutes(options = {}) {
264
1063
  prefix: options.gatewayPrefix ?? '',
265
1064
  };
266
1065
  }
267
- if (options.includeDesktop ?? true) {
1066
+ if (options.includeDesktop ?? false) {
268
1067
  routes.desktop = {
269
1068
  port: options.desktopPort ?? 3000,
270
1069
  auth: options.desktopAuth ?? true,
@@ -273,6 +1072,61 @@ export function buildOpenClawRoutes(options = {}) {
273
1072
  }
274
1073
  return routes;
275
1074
  }
1075
+ function envBool(value) {
1076
+ return value ? '1' : '0';
1077
+ }
1078
+ function envNonNegativeInteger(name, value) {
1079
+ const integer = Number(value);
1080
+ if (!Number.isInteger(integer) || integer < 0) {
1081
+ throw new Error(`${name} must be a non-negative integer`);
1082
+ }
1083
+ return String(integer);
1084
+ }
1085
+ export function buildOpenClawMemoryIndexEnv(memoryIndex = null) {
1086
+ if (!memoryIndex)
1087
+ return {};
1088
+ const env = { ...OPENCLAW_MEMORY_SEARCH_ENV_DEFAULTS };
1089
+ if (memoryIndex.enabled !== undefined && memoryIndex.enabled !== null) {
1090
+ env.OPENCLAW_MEMORY_SEARCH_ENABLED = envBool(memoryIndex.enabled);
1091
+ }
1092
+ if (memoryIndex.onSessionStart !== undefined && memoryIndex.onSessionStart !== null) {
1093
+ env.OPENCLAW_MEMORY_SEARCH_SYNC_ON_SESSION_START = envBool(memoryIndex.onSessionStart);
1094
+ }
1095
+ if (memoryIndex.onSearch !== undefined && memoryIndex.onSearch !== null) {
1096
+ env.OPENCLAW_MEMORY_SEARCH_SYNC_ON_SEARCH = envBool(memoryIndex.onSearch);
1097
+ }
1098
+ if (memoryIndex.watch !== undefined && memoryIndex.watch !== null) {
1099
+ env.OPENCLAW_MEMORY_SEARCH_SYNC_WATCH = envBool(memoryIndex.watch);
1100
+ }
1101
+ if (memoryIndex.watchDebounceMs !== undefined && memoryIndex.watchDebounceMs !== null) {
1102
+ env.OPENCLAW_MEMORY_SEARCH_SYNC_WATCH_DEBOUNCE_MS = envNonNegativeInteger('watchDebounceMs', memoryIndex.watchDebounceMs);
1103
+ }
1104
+ if (memoryIndex.intervalMinutes !== undefined && memoryIndex.intervalMinutes !== null) {
1105
+ env.OPENCLAW_MEMORY_SEARCH_SYNC_INTERVAL_MINUTES = envNonNegativeInteger('intervalMinutes', memoryIndex.intervalMinutes);
1106
+ }
1107
+ return env;
1108
+ }
1109
+ export function buildOpenClawWorkspacesSyncEnv(workspacesSync = null) {
1110
+ if (workspacesSync === false)
1111
+ return { HYPER_WORKSPACES_BOOT_SYNC: '0' };
1112
+ const options = typeof workspacesSync === 'object' && workspacesSync !== null ? workspacesSync : {};
1113
+ if (options.enabled === false)
1114
+ return { HYPER_WORKSPACES_BOOT_SYNC: '0' };
1115
+ const env = { ...OPENCLAW_WORKSPACES_SYNC_ENV_DEFAULTS };
1116
+ if (options.enabled !== undefined && options.enabled !== null) {
1117
+ env.HYPER_WORKSPACES_BOOT_SYNC = envBool(options.enabled);
1118
+ }
1119
+ if (options.outputDir) {
1120
+ env.HYPER_WORKSPACES_DIR = options.outputDir;
1121
+ }
1122
+ if (options.readyOnly !== undefined && options.readyOnly !== null) {
1123
+ env.HYPER_WORKSPACES_SYNC_READY_ONLY = envBool(options.readyOnly);
1124
+ }
1125
+ if (options.workspace) {
1126
+ env.HYPER_WORKSPACES_SYNC_WORKSPACE = options.workspace;
1127
+ }
1128
+ return env;
1129
+ }
276
1130
  async function getFsPromises() {
277
1131
  return import('node:fs/promises');
278
1132
  }
@@ -287,6 +1141,16 @@ export class Agent {
287
1141
  podName;
288
1142
  state;
289
1143
  name;
1144
+ handle;
1145
+ displayName;
1146
+ avatarUrl;
1147
+ displayIdentity;
1148
+ runtime;
1149
+ managed;
1150
+ isLaunchable;
1151
+ gatewayId;
1152
+ runtimeKeyAlias;
1153
+ relayKey;
290
1154
  cpu;
291
1155
  memory;
292
1156
  hostname;
@@ -313,6 +1177,16 @@ export class Agent {
313
1177
  this.podName = fields.podName;
314
1178
  this.state = fields.state;
315
1179
  this.name = fields.name ?? null;
1180
+ this.handle = fields.handle ?? null;
1181
+ this.displayName = fields.displayName ?? this.name;
1182
+ this.avatarUrl = fields.avatarUrl ?? null;
1183
+ this.displayIdentity = fields.displayIdentity ? structuredClone(fields.displayIdentity) : null;
1184
+ this.runtime = fields.runtime ?? null;
1185
+ this.managed = fields.managed ?? null;
1186
+ this.isLaunchable = fields.isLaunchable ?? true;
1187
+ this.gatewayId = fields.gatewayId ?? null;
1188
+ this.runtimeKeyAlias = fields.runtimeKeyAlias ?? null;
1189
+ this.relayKey = fields.relayKey ? structuredClone(fields.relayKey) : null;
316
1190
  this.cpu = fields.cpu;
317
1191
  this.memory = fields.memory;
318
1192
  this.hostname = fields.hostname ?? null;
@@ -360,6 +1234,11 @@ export class Agent {
360
1234
  get vncUrl() {
361
1235
  return this.desktopUrl;
362
1236
  }
1237
+ browserDesktopUrl(token, options = {}) {
1238
+ if (!this.desktopUrl)
1239
+ return null;
1240
+ return buildBrowserDesktopUrl(this.desktopUrl, token, options);
1241
+ }
363
1242
  get shellUrl() {
364
1243
  return this.routeUrl('shell');
365
1244
  }
@@ -369,6 +1248,13 @@ export class Agent {
369
1248
  get isRunning() {
370
1249
  return this.state.toLowerCase() === 'running';
371
1250
  }
1251
+ get hasDesktop() {
1252
+ return agentConfigHasDesktop({
1253
+ launchConfig: this.launchConfig,
1254
+ routes: this.routes,
1255
+ ports: this.ports,
1256
+ });
1257
+ }
372
1258
  requireDeployments() {
373
1259
  if (!this._deployments) {
374
1260
  throw new Error('Agent is not bound to a Deployments client');
@@ -407,23 +1293,45 @@ export class Agent {
407
1293
  async health() {
408
1294
  return this.requireDeployments().health(this);
409
1295
  }
1296
+ /**
1297
+ * The gateway file writer a subclass supplies — the seam that enables the
1298
+ * `gateway` source. The base agent has none (gateway calls throw); an agent
1299
+ * type that speaks a gateway file protocol (e.g. OpenClaw's `agents.files.*`)
1300
+ * overloads this. Kept on the base so the *calling* is standardized through
1301
+ * the superclass even though each agent's gateway differs.
1302
+ */
1303
+ gatewayFileHooks() {
1304
+ return undefined;
1305
+ }
1306
+ /**
1307
+ * The single file client for this agent — one `source` switch (`agent` |
1308
+ * `backup` | `gateway`) routes to the three underlying APIs (pod sidecar / S3
1309
+ * backup / gateway), with the SDK owning the roots. One implementation; a
1310
+ * subclass only supplies its gateway via {@link gatewayFileHooks}.
1311
+ */
1312
+ get files() {
1313
+ return new AgentFiles(this, this.requireDeployments(), this.gatewayFileHooks());
1314
+ }
410
1315
  async filesList(path = '', source = 'auto') {
411
- return this.requireDeployments().filesList(this, path, source);
1316
+ return this.files.list(path, source);
1317
+ }
1318
+ async fileReadBytes(path, source = 'auto', options) {
1319
+ return this.files.readBytes(path, source, options);
412
1320
  }
413
- async fileReadBytes(path, source = 'auto') {
414
- return this.requireDeployments().fileReadBytes(this, path, source);
1321
+ async fileReadBytesWithMetadata(path, source = 'auto', options) {
1322
+ return this.files.readBytesWithMetadata(path, source, options);
415
1323
  }
416
- async fileRead(path, source = 'auto') {
417
- return decodeUtf8(await this.fileReadBytes(path, source));
1324
+ async fileRead(path, source = 'auto', options) {
1325
+ return this.files.read(path, source, options);
418
1326
  }
419
1327
  async fileWriteBytes(path, content, destination = 'auto') {
420
- return this.requireDeployments().fileWriteBytes(this, path, content, destination);
1328
+ return this.files.writeBytes(path, content, destination);
421
1329
  }
422
1330
  async fileWrite(path, content, destination = 'auto') {
423
- return this.requireDeployments().fileWrite(this, path, content, destination);
1331
+ return this.files.write(path, content, destination);
424
1332
  }
425
1333
  async fileDelete(path, options = {}) {
426
- return this.requireDeployments().fileDelete(this, path, options);
1334
+ return this.files.delete(path, options);
427
1335
  }
428
1336
  async cpTo(localPath, remotePath) {
429
1337
  return this.requireDeployments().cpTo(this, localPath, remotePath);
@@ -431,8 +1339,346 @@ export class Agent {
431
1339
  async cpFrom(remotePath, localPath) {
432
1340
  return this.requireDeployments().cpFrom(this, remotePath, localPath);
433
1341
  }
434
- async shellConnect(shell) {
435
- return this.requireDeployments().shellConnect(this.id, shell);
1342
+ async shellConnect(shell, options) {
1343
+ return this.requireDeployments().shellConnect(this.id, shell, options);
1344
+ }
1345
+ }
1346
+ const RUNTIME_AUTH_CONFIG = {
1347
+ opencode: {
1348
+ agentCommand: ['opencode', 'acp'],
1349
+ statusCommand: ['buzz-acp', 'models', '--agent-command', 'opencode', '--agent-args', 'acp', '--json'],
1350
+ logoutCommand: ['opencode', 'auth', 'logout'],
1351
+ nativeMethods: [],
1352
+ },
1353
+ codex: {
1354
+ agentCommand: ['codex-acp'],
1355
+ statusCommand: ['codex', 'login', 'status'],
1356
+ logoutCommand: ['codex', 'logout'],
1357
+ nativeMethods: [{
1358
+ id: 'device',
1359
+ name: 'Device authentication',
1360
+ description: 'Authenticate Codex with a device code.',
1361
+ kind: 'native',
1362
+ command: ['codex', 'login', '--device-auth'],
1363
+ metadata: {},
1364
+ }],
1365
+ },
1366
+ 'claude-code': {
1367
+ agentCommand: ['claude-agent-acp'],
1368
+ statusCommand: ['claude', 'auth', 'status', '--json'],
1369
+ logoutCommand: ['claude', 'auth', 'logout'],
1370
+ nativeMethods: [
1371
+ { id: 'claude-ai', name: 'Claude.ai', description: '', kind: 'native', command: ['claude', 'auth', 'login', '--claudeai'], metadata: {} },
1372
+ { id: 'console', name: 'Anthropic Console', description: '', kind: 'native', command: ['claude', 'auth', 'login', '--console'], metadata: {} },
1373
+ { id: 'sso', name: 'Enterprise SSO', description: '', kind: 'native', command: ['claude', 'auth', 'login', '--sso'], metadata: {} },
1374
+ ],
1375
+ },
1376
+ goose: {
1377
+ agentCommand: ['goose', 'acp'],
1378
+ statusCommand: ['buzz-acp', 'models', '--agent-command', 'goose', '--agent-args', 'acp', '--json'],
1379
+ logoutCommand: null,
1380
+ nativeMethods: [],
1381
+ },
1382
+ 'kimi-code': {
1383
+ agentCommand: ['kimi', 'acp'],
1384
+ statusCommand: ['buzz-acp', 'models', '--agent-command', 'kimi', '--agent-args', 'acp', '--json'],
1385
+ logoutCommand: null,
1386
+ nativeMethods: [],
1387
+ },
1388
+ };
1389
+ function shellQuote(value) {
1390
+ return `'${value.replace(/'/g, `'"'"'`)}'`;
1391
+ }
1392
+ function commandString(command) {
1393
+ return command.map(shellQuote).join(' ');
1394
+ }
1395
+ // Terminal control-sequence matching intentionally includes ESC and BEL.
1396
+ /* eslint-disable no-control-regex */
1397
+ const TERMINAL_ESCAPE_PATTERN = new RegExp('\\x1B(?:\\[[0-?]*[ -/]*[@-~]|\\][^\\x07]*(?:\\x07|\\x1B\\\\))', 'g');
1398
+ /* eslint-enable no-control-regex */
1399
+ function stripTerminalCodes(value) {
1400
+ return value.replace(TERMINAL_ESCAPE_PATTERN, '').replace(/\r/g, '');
1401
+ }
1402
+ function authMethodFromPayload(value) {
1403
+ if (!isPlainRecord(value))
1404
+ return null;
1405
+ const id = typeof value.id === 'string' ? value.id : '';
1406
+ if (!id)
1407
+ return null;
1408
+ const metadata = isPlainRecord(value._meta) ? { ...value._meta } : {};
1409
+ let command = [];
1410
+ const terminal = isPlainRecord(metadata['terminal-auth']) ? metadata['terminal-auth'] : null;
1411
+ const source = terminal ?? value;
1412
+ if (Array.isArray(source.command) && source.command.every((part) => typeof part === 'string')) {
1413
+ command = [...source.command];
1414
+ }
1415
+ else if (typeof source.command === 'string') {
1416
+ const args = Array.isArray(source.args) ? source.args.filter((part) => typeof part === 'string') : [];
1417
+ command = [source.command, ...args];
1418
+ }
1419
+ if (id === 'claude-login' && command.length > 0 && !command.includes('login')) {
1420
+ command.push('auth', 'login');
1421
+ }
1422
+ return {
1423
+ id,
1424
+ name: typeof value.name === 'string' ? value.name : id,
1425
+ description: typeof value.description === 'string' ? value.description : '',
1426
+ kind: typeof value.kind === 'string' ? value.kind : 'acp',
1427
+ command,
1428
+ metadata,
1429
+ };
1430
+ }
1431
+ async function websocketMessageText(data) {
1432
+ if (typeof data === 'string')
1433
+ return data;
1434
+ if (data instanceof ArrayBuffer)
1435
+ return decodeUtf8(new Uint8Array(data));
1436
+ if (ArrayBuffer.isView(data))
1437
+ return decodeUtf8(new Uint8Array(data.buffer, data.byteOffset, data.byteLength));
1438
+ if (typeof Blob !== 'undefined' && data instanceof Blob)
1439
+ return data.text();
1440
+ return String(data ?? '');
1441
+ }
1442
+ export class RuntimeLoginSession {
1443
+ authClient;
1444
+ socket;
1445
+ output = '';
1446
+ verificationUrl = null;
1447
+ userCode = null;
1448
+ interactiveRequired = false;
1449
+ exitCode = null;
1450
+ marker;
1451
+ completion;
1452
+ ready;
1453
+ complete;
1454
+ markReady;
1455
+ constructor(authClient, socket, command) {
1456
+ this.authClient = authClient;
1457
+ this.socket = socket;
1458
+ this.marker = `__HYPERCLI_AUTH_EXIT_${randomHexToken(12)}__=`;
1459
+ this.completion = new Promise((resolve) => { this.complete = resolve; });
1460
+ this.ready = new Promise((resolve) => { this.markReady = resolve; });
1461
+ socket.onmessage = (event) => {
1462
+ void websocketMessageText(event.data).then((chunk) => this.consume(chunk));
1463
+ };
1464
+ socket.onclose = () => {
1465
+ this.markReady();
1466
+ this.complete();
1467
+ };
1468
+ socket.send(`${commandString(command)}; _hypercli_auth_rc=$?; printf '\\n${this.marker}%s\\n' "$_hypercli_auth_rc"\n`);
1469
+ }
1470
+ static async start(authClient, command, challengeTimeoutMs = 45_000) {
1471
+ const socket = await authClient.agent.shellConnect();
1472
+ const session = new RuntimeLoginSession(authClient, socket, command);
1473
+ let timer;
1474
+ try {
1475
+ await Promise.race([
1476
+ session.ready,
1477
+ new Promise((_, reject) => {
1478
+ timer = setTimeout(() => reject(new Error('Timed out waiting for runtime login instructions')), challengeTimeoutMs);
1479
+ }),
1480
+ ]);
1481
+ return session;
1482
+ }
1483
+ catch (error) {
1484
+ session.cancel();
1485
+ throw error;
1486
+ }
1487
+ finally {
1488
+ if (timer)
1489
+ clearTimeout(timer);
1490
+ }
1491
+ }
1492
+ consume(chunk) {
1493
+ this.output += stripTerminalCodes(chunk);
1494
+ const markerIndex = this.output.lastIndexOf(this.marker);
1495
+ if (markerIndex >= 0) {
1496
+ const match = this.output.slice(markerIndex + this.marker.length).match(/^(-?\d+)/);
1497
+ if (match) {
1498
+ this.exitCode = Number(match[1]);
1499
+ this.markReady();
1500
+ this.complete();
1501
+ }
1502
+ }
1503
+ this.verificationUrl ??= this.output.match(/https?:\/\/[^\s"'<>]+/)?.[0] ?? null;
1504
+ this.userCode ??= this.output.match(/(?:user|device|verification|one[- ]time)?\s*code\s*(?:is|:)?\s*([A-Z0-9][A-Z0-9-]{3,})/i)?.[1] ?? null;
1505
+ this.interactiveRequired ||= /\b(select|choose)\b.*\b(provider|login method)\b/i.test(this.output);
1506
+ if (this.verificationUrl || this.userCode || this.interactiveRequired)
1507
+ this.markReady();
1508
+ }
1509
+ send(text) {
1510
+ this.socket.send(text.endsWith('\n') ? text : `${text}\n`);
1511
+ }
1512
+ async wait(timeoutMs = 600_000) {
1513
+ let timer;
1514
+ try {
1515
+ await Promise.race([
1516
+ this.completion,
1517
+ new Promise((_, reject) => {
1518
+ timer = setTimeout(() => reject(new Error('Runtime authentication timed out')), timeoutMs);
1519
+ }),
1520
+ ]);
1521
+ }
1522
+ finally {
1523
+ if (timer)
1524
+ clearTimeout(timer);
1525
+ }
1526
+ if (this.exitCode !== null && this.exitCode !== 0) {
1527
+ throw new Error(`Runtime authentication failed (${this.exitCode}): ${this.output.trim()}`);
1528
+ }
1529
+ return this.authClient.status();
1530
+ }
1531
+ cancel() {
1532
+ if (this.socket.readyState === WebSocket.OPEN)
1533
+ this.socket.send('\x03');
1534
+ this.socket.close();
1535
+ }
1536
+ }
1537
+ export class RuntimeAuthClient {
1538
+ agent;
1539
+ config;
1540
+ constructor(agent) {
1541
+ this.agent = agent;
1542
+ this.config = RUNTIME_AUTH_CONFIG[agent.runtime];
1543
+ }
1544
+ async methods() {
1545
+ const [agentCommand, ...agentArgs] = this.config.agentCommand;
1546
+ const command = ['buzz-acp', 'auth-methods', '--agent-command', agentCommand];
1547
+ if (agentArgs.length)
1548
+ command.push('--agent-args', agentArgs.join(','));
1549
+ command.push('--json');
1550
+ const result = await this.agent.exec(commandString(command));
1551
+ const discovered = [];
1552
+ if (result.exitCode === 0) {
1553
+ try {
1554
+ const payload = JSON.parse(stripTerminalCodes(result.stdout));
1555
+ const values = isPlainRecord(payload) && Array.isArray(payload.methods) ? payload.methods : [];
1556
+ for (const value of values) {
1557
+ const method = authMethodFromPayload(value);
1558
+ if (method)
1559
+ discovered.push(method);
1560
+ }
1561
+ }
1562
+ catch {
1563
+ // Native fallbacks still make authentication available.
1564
+ }
1565
+ }
1566
+ if (this.agent.runtime === 'opencode' && discovered.length === 0) {
1567
+ discovered.push({
1568
+ id: 'provider',
1569
+ name: 'Provider login',
1570
+ description: '',
1571
+ kind: 'native',
1572
+ command: ['opencode', 'auth', 'login'],
1573
+ metadata: {},
1574
+ });
1575
+ }
1576
+ const seen = new Set(discovered.map((method) => method.id));
1577
+ for (const method of this.config.nativeMethods) {
1578
+ if (!seen.has(method.id))
1579
+ discovered.push({ ...method, command: [...method.command], metadata: { ...method.metadata } });
1580
+ }
1581
+ return discovered;
1582
+ }
1583
+ async status() {
1584
+ const result = await this.agent.exec(commandString(this.config.statusCommand));
1585
+ const output = stripTerminalCodes([result.stdout, result.stderr].filter(Boolean).join('\n')).trim();
1586
+ const detail = { exitCode: result.exitCode, output };
1587
+ if (this.agent.runtime === 'claude-code') {
1588
+ try {
1589
+ const payload = JSON.parse(stripTerminalCodes(result.stdout));
1590
+ Object.assign(detail, payload);
1591
+ const loginMethod = payload.loginMethod ?? payload.authMethod;
1592
+ return {
1593
+ authenticated: payload.loggedIn === true || payload.authenticated === true ||
1594
+ (typeof loginMethod === 'string' && loginMethod.toLowerCase() !== 'none'),
1595
+ provider: typeof (payload.subscriptionType ?? payload.provider ?? payload.apiProvider) === 'string'
1596
+ ? String(payload.subscriptionType ?? payload.provider ?? payload.apiProvider) : null,
1597
+ account: typeof payload.email === 'string' ? payload.email : null,
1598
+ method: typeof loginMethod === 'string' ? loginMethod : null,
1599
+ detail,
1600
+ };
1601
+ }
1602
+ catch {
1603
+ // Fall through to the generic status parser.
1604
+ }
1605
+ }
1606
+ const negative = /\b(not logged|not authenticated|unauthenticated|no credentials|0 credentials)\b/i.test(output);
1607
+ return { authenticated: result.exitCode === 0 && !negative, detail };
1608
+ }
1609
+ async login(options = {}) {
1610
+ const methods = await this.methods();
1611
+ const method = options.method
1612
+ ? methods.find((candidate) => candidate.id === options.method)
1613
+ : methods.find((candidate) => candidate.command.length > 0) ?? methods[0];
1614
+ if (!method) {
1615
+ throw new Error(options.method
1616
+ ? `Unknown authentication method: ${options.method}`
1617
+ : 'No authentication methods are available');
1618
+ }
1619
+ let command = [...method.command];
1620
+ if (!command.length) {
1621
+ const [agentCommand, ...agentArgs] = this.config.agentCommand;
1622
+ command = ['buzz-acp', 'authenticate', '--agent-command', agentCommand];
1623
+ if (agentArgs.length)
1624
+ command.push('--agent-args', agentArgs.join(','));
1625
+ command.push('--method-id', method.id);
1626
+ }
1627
+ if (this.agent.runtime === 'opencode') {
1628
+ if (options.provider)
1629
+ command.push('--provider', options.provider);
1630
+ if (options.providerMethod)
1631
+ command.push('--method', options.providerMethod);
1632
+ }
1633
+ if (this.agent.runtime === 'claude-code' && options.email)
1634
+ command.push('--email', options.email);
1635
+ return RuntimeLoginSession.start(this, command, options.challengeTimeoutMs);
1636
+ }
1637
+ async logout(provider) {
1638
+ if (this.config.logoutCommand === null) {
1639
+ const reason = this.agent.runtime === 'goose'
1640
+ ? 'uses its injected deployment credential'
1641
+ : 'does not expose a noninteractive logout command';
1642
+ throw new Error(`${this.agent.runtime} ${reason} and cannot log out`);
1643
+ }
1644
+ const command = [...this.config.logoutCommand];
1645
+ if (this.agent.runtime === 'opencode' && provider)
1646
+ command.push(provider);
1647
+ const result = await this.agent.exec(commandString(command));
1648
+ if (result.exitCode !== 0) {
1649
+ throw new Error(`Runtime logout failed (${result.exitCode}): ${stripTerminalCodes(result.stderr || result.stdout).trim()}`);
1650
+ }
1651
+ return this.status();
1652
+ }
1653
+ }
1654
+ export class CodingAgent extends Agent {
1655
+ get auth() {
1656
+ return new RuntimeAuthClient(this);
1657
+ }
1658
+ }
1659
+ export class OpenCodeAgent extends CodingAgent {
1660
+ static fromDict(data) {
1661
+ return new OpenCodeAgent(agentStateFromDict(data));
1662
+ }
1663
+ }
1664
+ export class CodexAgent extends CodingAgent {
1665
+ static fromDict(data) {
1666
+ return new CodexAgent(agentStateFromDict(data));
1667
+ }
1668
+ }
1669
+ export class ClaudeCodeAgent extends CodingAgent {
1670
+ static fromDict(data) {
1671
+ return new ClaudeCodeAgent(agentStateFromDict(data));
1672
+ }
1673
+ }
1674
+ export class GooseAgent extends CodingAgent {
1675
+ static fromDict(data) {
1676
+ return new GooseAgent(agentStateFromDict(data));
1677
+ }
1678
+ }
1679
+ export class KimiCodeAgent extends CodingAgent {
1680
+ static fromDict(data) {
1681
+ return new KimiCodeAgent(agentStateFromDict(data));
436
1682
  }
437
1683
  }
438
1684
  export class OpenClawAgent extends Agent {
@@ -446,7 +1692,7 @@ export class OpenClawAgent extends Agent {
446
1692
  static fromDict(data) {
447
1693
  return new OpenClawAgent({
448
1694
  ...agentStateFromDict(data),
449
- gatewayUrl: null,
1695
+ gatewayUrl: this.gatewayUrlFromHostname(data.hostname),
450
1696
  gatewayToken: data.gateway_token ?? null,
451
1697
  });
452
1698
  }
@@ -475,6 +1721,15 @@ export class OpenClawAgent extends Agent {
475
1721
  * token from `OPENCLAW_GATEWAY_TOKEN` in the agent env route.
476
1722
  */
477
1723
  async waitForGatewayContext(options = {}) {
1724
+ const callerAbortError = () => {
1725
+ if (options.signal?.reason instanceof Error)
1726
+ return options.signal.reason;
1727
+ const error = new Error('OpenClaw gateway context wait cancelled');
1728
+ error.name = 'AbortError';
1729
+ return error;
1730
+ };
1731
+ if (options.signal?.aborted)
1732
+ throw callerAbortError();
478
1733
  if (this.gatewayToken && this.gatewayUrl) {
479
1734
  return {
480
1735
  agent_id: this.id,
@@ -487,29 +1742,98 @@ export class OpenClawAgent extends Agent {
487
1742
  const deployments = this.requireDeployments();
488
1743
  const deadline = Date.now() + timeoutMs;
489
1744
  let lastError = null;
490
- while (true) {
491
- try {
492
- const refreshed = await deployments.get(this.id);
493
- const hostname = refreshed.hostname ?? null;
494
- const gatewayUrl = OpenClawAgent.gatewayUrlFromHostname(hostname);
495
- const envData = await deployments.env(this.id);
496
- const gatewayToken = envData.env?.OPENCLAW_GATEWAY_TOKEN?.trim() || null;
497
- if (gatewayToken && gatewayUrl) {
498
- this.gatewayToken = gatewayToken;
499
- this.gatewayUrl = gatewayUrl;
500
- return { agent_id: this.id, gateway_token: gatewayToken, hostname };
1745
+ const timeoutError = () => {
1746
+ return lastError instanceof Error
1747
+ ? lastError
1748
+ : new Error('Timed out waiting for OpenClaw gateway context');
1749
+ };
1750
+ const controller = new AbortController();
1751
+ const forwardAbort = () => controller.abort(callerAbortError());
1752
+ options.signal?.addEventListener('abort', forwardAbort, { once: true });
1753
+ const timeoutId = setTimeout(() => controller.abort(timeoutError()), Math.max(0, timeoutMs));
1754
+ const waitAbortError = () => {
1755
+ return controller.signal.reason instanceof Error ? controller.signal.reason : callerAbortError();
1756
+ };
1757
+ const runWithAbort = (operation) => {
1758
+ if (controller.signal.aborted)
1759
+ return Promise.reject(waitAbortError());
1760
+ return new Promise((resolve, reject) => {
1761
+ const abortOperation = () => {
1762
+ cleanup();
1763
+ reject(waitAbortError());
1764
+ };
1765
+ const cleanup = () => controller.signal.removeEventListener('abort', abortOperation);
1766
+ controller.signal.addEventListener('abort', abortOperation, { once: true });
1767
+ operation.then((value) => {
1768
+ cleanup();
1769
+ resolve(value);
1770
+ }, (error) => {
1771
+ cleanup();
1772
+ reject(error);
1773
+ });
1774
+ });
1775
+ };
1776
+ const waitForRetry = (delayMs) => {
1777
+ if (controller.signal.aborted)
1778
+ return Promise.reject(waitAbortError());
1779
+ return new Promise((resolve, reject) => {
1780
+ const finish = () => {
1781
+ controller.signal.removeEventListener('abort', abortWait);
1782
+ resolve();
1783
+ };
1784
+ const timer = setTimeout(finish, delayMs);
1785
+ const abortWait = () => {
1786
+ clearTimeout(timer);
1787
+ controller.signal.removeEventListener('abort', abortWait);
1788
+ reject(waitAbortError());
1789
+ };
1790
+ controller.signal.addEventListener('abort', abortWait, { once: true });
1791
+ });
1792
+ };
1793
+ try {
1794
+ while (true) {
1795
+ const remainingMs = deadline - Date.now();
1796
+ if (remainingMs <= 0)
1797
+ throw timeoutError();
1798
+ const requestOptions = {
1799
+ signal: controller.signal,
1800
+ timeout: Math.max(1, remainingMs),
1801
+ };
1802
+ const requests = [];
1803
+ if (!this.gatewayUrl) {
1804
+ requests.push(deployments.get(this.id, requestOptions).then((refreshed) => {
1805
+ const gatewayUrl = OpenClawAgent.gatewayUrlFromHostname(refreshed.hostname);
1806
+ if (gatewayUrl)
1807
+ this.gatewayUrl = gatewayUrl;
1808
+ }));
501
1809
  }
502
- lastError = new Error('missing gateway context');
503
- }
504
- catch (error) {
505
- lastError = error;
506
- }
507
- if (Date.now() >= deadline) {
508
- if (lastError instanceof Error)
509
- throw lastError;
510
- throw new Error('Timed out waiting for OpenClaw gateway context');
1810
+ if (!this.gatewayToken) {
1811
+ requests.push(deployments.env(this.id, requestOptions).then((envData) => {
1812
+ const gatewayToken = envData.env?.OPENCLAW_GATEWAY_TOKEN?.trim() || null;
1813
+ if (gatewayToken)
1814
+ this.gatewayToken = gatewayToken;
1815
+ }));
1816
+ }
1817
+ const results = await runWithAbort(Promise.allSettled(requests));
1818
+ const rejected = results.find((result) => result.status === 'rejected');
1819
+ if (this.gatewayToken && this.gatewayUrl) {
1820
+ return {
1821
+ agent_id: this.id,
1822
+ hostname: this.currentGatewayHostname(),
1823
+ gateway_token: this.gatewayToken,
1824
+ };
1825
+ }
1826
+ lastError = rejected?.reason ?? new Error('missing gateway context');
1827
+ const remainingAfterRequestMs = deadline - Date.now();
1828
+ if (remainingAfterRequestMs <= 0)
1829
+ throw timeoutError();
1830
+ const retryDelayMs = Math.min(Math.max(0, retryIntervalMs), remainingAfterRequestMs);
1831
+ await waitForRetry(retryDelayMs);
511
1832
  }
512
- await new Promise((resolve) => setTimeout(resolve, retryIntervalMs));
1833
+ }
1834
+ finally {
1835
+ clearTimeout(timeoutId);
1836
+ options.signal?.removeEventListener('abort', forwardAbort);
513
1837
  }
514
1838
  }
515
1839
  async resolveGatewayToken() {
@@ -541,6 +1865,7 @@ export class OpenClawAgent extends Agent {
541
1865
  onHello: options.onHello,
542
1866
  onClose: options.onClose,
543
1867
  onGap: options.onGap,
1868
+ onProtocolError: options.onProtocolError,
544
1869
  onPairing: options.onPairing,
545
1870
  });
546
1871
  }
@@ -552,6 +1877,47 @@ export class OpenClawAgent extends Agent {
552
1877
  await client.connect();
553
1878
  return client;
554
1879
  }
1880
+ /** Run a fn against a connected gateway client, then close it. */
1881
+ async withGateway(fn) {
1882
+ const client = await this.connect();
1883
+ try {
1884
+ return await fn(client);
1885
+ }
1886
+ finally {
1887
+ client.close();
1888
+ }
1889
+ }
1890
+ _gatewayAgentId;
1891
+ /**
1892
+ * The in-gateway agent id for `agents.files.*` — NOT the deployment id. A
1893
+ * deployment's gateway hosts an agent named `main` (or a named agent);
1894
+ * matches the existing fileGet/fileSet/workspaceFiles convention.
1895
+ */
1896
+ async resolveGatewayAgentId(client) {
1897
+ if (this._gatewayAgentId)
1898
+ return this._gatewayAgentId;
1899
+ let agents = [];
1900
+ try {
1901
+ agents = await client.agentsList();
1902
+ }
1903
+ catch {
1904
+ // fall through to the canonical default
1905
+ }
1906
+ const resolved = agents[0]?.id ?? 'main';
1907
+ this._gatewayAgentId = resolved;
1908
+ return resolved;
1909
+ }
1910
+ /**
1911
+ * Overload the base gateway seam with OpenClaw's `agents.files.*` — the actual
1912
+ * OpenClaw gateway file methods. The base `files`/`fileX` calling surface is
1913
+ * inherited unchanged; only the gateway writer is supplied here.
1914
+ */
1915
+ gatewayFileHooks() {
1916
+ return {
1917
+ withGateway: (fn) => this.withGateway(fn),
1918
+ resolveAgentId: (c) => this.resolveGatewayAgentId(c),
1919
+ };
1920
+ }
555
1921
  async gatewayStatus(options = {}) {
556
1922
  const client = await this.connect(options);
557
1923
  try {
@@ -603,6 +1969,64 @@ export class OpenClawAgent extends Agent {
603
1969
  client.close();
604
1970
  }
605
1971
  }
1972
+ async configureSlackRelay(options, gatewayOptions = {}) {
1973
+ const { accountId, ...connectOptions } = gatewayOptions;
1974
+ const client = await this.connect(connectOptions);
1975
+ try {
1976
+ if ('relay' in options) {
1977
+ await client.configureSlackRelay(options, accountId);
1978
+ }
1979
+ else {
1980
+ await client.configureSlackRelay({
1981
+ ...options,
1982
+ gatewayId: options.gatewayId ?? this.gatewayId ?? `agent:${this.id}`,
1983
+ });
1984
+ }
1985
+ }
1986
+ finally {
1987
+ client.close();
1988
+ }
1989
+ }
1990
+ async configureSlackSocket(config, options = {}) {
1991
+ const { accountId, ...gatewayOptions } = options;
1992
+ const client = await this.connect(gatewayOptions);
1993
+ try {
1994
+ await client.configureSlackSocket(config, accountId);
1995
+ }
1996
+ finally {
1997
+ client.close();
1998
+ }
1999
+ }
2000
+ async configureSlackHttp(config, options = {}) {
2001
+ const { accountId, ...gatewayOptions } = options;
2002
+ const client = await this.connect(gatewayOptions);
2003
+ try {
2004
+ await client.configureSlackHttp(config, accountId);
2005
+ }
2006
+ finally {
2007
+ client.close();
2008
+ }
2009
+ }
2010
+ async configureTelegram(config, options = {}) {
2011
+ const { accountId, ...gatewayOptions } = options;
2012
+ const client = await this.connect(gatewayOptions);
2013
+ try {
2014
+ await client.configureTelegram(config, accountId);
2015
+ }
2016
+ finally {
2017
+ client.close();
2018
+ }
2019
+ }
2020
+ async configureWhatsapp(config, options = {}) {
2021
+ const { accountId, ...gatewayOptions } = options;
2022
+ const client = await this.connect(gatewayOptions);
2023
+ try {
2024
+ await client.configureWhatsapp(config, accountId);
2025
+ }
2026
+ finally {
2027
+ client.close();
2028
+ }
2029
+ }
606
2030
  async configApply(config, options = {}) {
607
2031
  const client = await this.connect(options);
608
2032
  try {
@@ -630,6 +2054,15 @@ export class OpenClawAgent extends Agent {
630
2054
  client.close();
631
2055
  }
632
2056
  }
2057
+ async sessionsListResult(options = {}) {
2058
+ const client = await this.connect(options);
2059
+ try {
2060
+ return await client.sessionsListResult();
2061
+ }
2062
+ finally {
2063
+ client.close();
2064
+ }
2065
+ }
633
2066
  async *chatSend(message, sessionKey, options = {}) {
634
2067
  const client = await this.connect(options);
635
2068
  try {
@@ -644,7 +2077,25 @@ export class OpenClawAgent extends Agent {
644
2077
  async channelsStatus(options = {}) {
645
2078
  const client = await this.connect(options);
646
2079
  try {
647
- return await client.channelsStatus(options.probe ?? false, options.timeoutMs);
2080
+ return await client.channelsStatus(options.probe ?? false, options.timeoutMs, options.channel);
2081
+ }
2082
+ finally {
2083
+ client.close();
2084
+ }
2085
+ }
2086
+ async channelsStart(channel, accountId, options = {}) {
2087
+ const client = await this.connect(options);
2088
+ try {
2089
+ return await client.channelsStart(channel, accountId);
2090
+ }
2091
+ finally {
2092
+ client.close();
2093
+ }
2094
+ }
2095
+ async channelsStop(channel, accountId, options = {}) {
2096
+ const client = await this.connect(options);
2097
+ try {
2098
+ return await client.channelsStop(channel, accountId);
648
2099
  }
649
2100
  finally {
650
2101
  client.close();
@@ -679,12 +2130,49 @@ export class OpenClawAgent extends Agent {
679
2130
  return await client.webLoginWait({
680
2131
  timeoutMs: options.timeoutMs,
681
2132
  accountId: options.accountId,
2133
+ currentQrDataUrl: options.currentQrDataUrl,
682
2134
  });
683
2135
  }
684
2136
  finally {
685
2137
  client.close();
686
2138
  }
687
2139
  }
2140
+ async integrationsAuthStart(params, options = {}) {
2141
+ const client = await this.connect(options);
2142
+ try {
2143
+ return await client.integrationsAuthStart(params);
2144
+ }
2145
+ finally {
2146
+ client.close();
2147
+ }
2148
+ }
2149
+ async integrationsAuthStatus(params, options = {}) {
2150
+ const client = await this.connect(options);
2151
+ try {
2152
+ return await client.integrationsAuthStatus(params);
2153
+ }
2154
+ finally {
2155
+ client.close();
2156
+ }
2157
+ }
2158
+ async integrationsStatus(params = {}, options = {}) {
2159
+ const client = await this.connect(options);
2160
+ try {
2161
+ return await client.integrationsStatus(params);
2162
+ }
2163
+ finally {
2164
+ client.close();
2165
+ }
2166
+ }
2167
+ async integrationsDisconnect(params, options = {}) {
2168
+ const client = await this.connect(options);
2169
+ try {
2170
+ return await client.integrationsDisconnect(params);
2171
+ }
2172
+ finally {
2173
+ client.close();
2174
+ }
2175
+ }
688
2176
  async workspaceFiles(options = {}) {
689
2177
  const client = await this.connect(options);
690
2178
  try {
@@ -743,7 +2231,7 @@ export class OpenClawAgent extends Agent {
743
2231
  async chatSendMessage(message, options = {}) {
744
2232
  const client = await this.connect(options);
745
2233
  try {
746
- return await client.sendChat(message, options.sessionKey ?? 'main', options.agentId, options.attachments);
2234
+ return await client.sendChat(message, options.sessionKey, options.agentId, options.attachments);
747
2235
  }
748
2236
  finally {
749
2237
  client.close();
@@ -886,6 +2374,42 @@ export class OpenClawAgent extends Agent {
886
2374
  client.close();
887
2375
  }
888
2376
  }
2377
+ async cronAdd(job, options = {}) {
2378
+ const client = await this.connect(options);
2379
+ try {
2380
+ return await client.cronAdd(job);
2381
+ }
2382
+ finally {
2383
+ client.close();
2384
+ }
2385
+ }
2386
+ async cronRemove(jobId, options = {}) {
2387
+ const client = await this.connect(options);
2388
+ try {
2389
+ await client.cronRemove(jobId);
2390
+ }
2391
+ finally {
2392
+ client.close();
2393
+ }
2394
+ }
2395
+ async cronRun(jobId, options = {}) {
2396
+ const client = await this.connect(options);
2397
+ try {
2398
+ return await client.cronRun(jobId);
2399
+ }
2400
+ finally {
2401
+ client.close();
2402
+ }
2403
+ }
2404
+ }
2405
+ export class OpenClawProAgent extends OpenClawAgent {
2406
+ static fromDict(data) {
2407
+ return new OpenClawProAgent({
2408
+ ...agentStateFromDict(data),
2409
+ gatewayUrl: this.gatewayUrlFromHostname(data.hostname),
2410
+ gatewayToken: data.gateway_token ?? null,
2411
+ });
2412
+ }
889
2413
  }
890
2414
  export class Deployments {
891
2415
  http;
@@ -893,12 +2417,13 @@ export class Deployments {
893
2417
  apiBase;
894
2418
  agentsWsUrl;
895
2419
  agentHttp;
896
- constructor(http, agentApiKey, agentApiBase, agentsWsUrl) {
2420
+ constructor(http, agentApiKey, agentApiBase, agentsWsUrl, requestTimeout) {
897
2421
  this.http = http;
898
2422
  this.apiKey = agentApiKey || http.apiKey;
899
2423
  this.apiBase = resolveAgentsApiBase(agentApiBase || getAgentsApiBaseUrl());
900
2424
  this.agentsWsUrl = normalizeAgentsWsUrl(agentsWsUrl || getConfigValue('AGENTS_WS_URL') || defaultAgentsWsUrl(this.apiBase));
901
- this.agentHttp = http instanceof HTTPClient ? new HTTPClient(this.apiBase, this.apiKey) : http;
2425
+ const agentTimeout = requestTimeout ?? (http instanceof HTTPClient ? http.timeout : undefined);
2426
+ this.agentHttp = http instanceof HTTPClient ? new HTTPClient(this.apiBase, this.apiKey, agentTimeout) : http;
902
2427
  }
903
2428
  get agentApiKey() {
904
2429
  return this.apiKey;
@@ -907,13 +2432,79 @@ export class Deployments {
907
2432
  return this.apiBase;
908
2433
  }
909
2434
  hydrateAgent(data) {
910
- const agent = isOpenClawHydrationData(data)
911
- ? OpenClawAgent.fromDict(data)
912
- : Agent.fromDict(data);
2435
+ let agent;
2436
+ if (data.runtime === 'opencode') {
2437
+ agent = OpenCodeAgent.fromDict(data);
2438
+ }
2439
+ else if (data.runtime === 'codex') {
2440
+ agent = CodexAgent.fromDict(data);
2441
+ }
2442
+ else if (data.runtime === 'claude-code') {
2443
+ agent = ClaudeCodeAgent.fromDict(data);
2444
+ }
2445
+ else if (data.runtime === 'goose') {
2446
+ agent = GooseAgent.fromDict(data);
2447
+ }
2448
+ else if (data.runtime === 'kimi-code') {
2449
+ agent = KimiCodeAgent.fromDict(data);
2450
+ }
2451
+ else if (data.runtime === 'openclaw-pro' || isOpenClawProHydrationData(data)) {
2452
+ agent = OpenClawProAgent.fromDict(data);
2453
+ }
2454
+ else if (isOpenClawHydrationData(data)) {
2455
+ agent = OpenClawAgent.fromDict(data);
2456
+ }
2457
+ else {
2458
+ agent = Agent.fromDict(data);
2459
+ }
913
2460
  return bindAgent(agent, this);
914
2461
  }
915
- agentIdFor(target) {
916
- return typeof target === 'string' ? target : target.id;
2462
+ async getById(agentId, requestOptions = {}) {
2463
+ const path = `${DEPLOYMENTS_API_PREFIX}/${agentId}`;
2464
+ const data = Object.keys(requestOptions).length === 0
2465
+ ? await this.agentHttp.get(path)
2466
+ : await this.agentHttp.get(path, undefined, requestOptions);
2467
+ return this.hydrateAgent(data);
2468
+ }
2469
+ async resolveAgent(agentIdOrName, requestOptions = {}) {
2470
+ const raw = String(agentIdOrName || '').trim();
2471
+ if (!raw) {
2472
+ throw new Error('agentIdOrName is required');
2473
+ }
2474
+ if (isUuidRef(raw)) {
2475
+ return this.getById(raw, requestOptions);
2476
+ }
2477
+ const matches = [];
2478
+ for (const agent of await this.list(requestOptions)) {
2479
+ const values = [agent.id, agent.name, agent.handle, agent.podName, agent.hostname];
2480
+ if (values.some((value) => String(value || '') === raw)) {
2481
+ matches.push(agent);
2482
+ continue;
2483
+ }
2484
+ if (values.some((value) => String(value || '').startsWith(raw))) {
2485
+ matches.push(agent);
2486
+ }
2487
+ }
2488
+ if (matches.length === 0) {
2489
+ throw new Error(`Agent not found: ${raw}`);
2490
+ }
2491
+ if (matches.length > 1) {
2492
+ throw new Error(`Agent reference is ambiguous: ${raw} (${matches.slice(0, 5).map((agent) => agent.id).join(', ')})`);
2493
+ }
2494
+ return this.getById(matches[0].id, requestOptions);
2495
+ }
2496
+ async resolveAgentId(agentIdOrName, requestOptions = {}) {
2497
+ const raw = String(agentIdOrName || '').trim();
2498
+ if (!raw) {
2499
+ throw new Error('agentIdOrName is required');
2500
+ }
2501
+ if (isDirectAgentIdRef(raw)) {
2502
+ return raw;
2503
+ }
2504
+ return (await this.resolveAgent(raw, requestOptions)).id;
2505
+ }
2506
+ async agentIdFor(target) {
2507
+ return typeof target === 'string' ? this.resolveAgentId(target) : target.id;
917
2508
  }
918
2509
  async fetchRaw(path, init = {}) {
919
2510
  const headers = new Headers(init.headers ?? {});
@@ -942,18 +2533,25 @@ export class Deployments {
942
2533
  return response;
943
2534
  }
944
2535
  async create(options = {}) {
945
- const { config, gatewayToken } = buildAgentConfig(options.config ?? {}, options);
2536
+ const { config, gatewayToken } = buildAgentConfig(options.config ?? {}, {
2537
+ ...options,
2538
+ injectGatewayToken: options.injectGatewayToken ?? !isCodingAgentRuntime(options.runtime),
2539
+ });
946
2540
  const body = { ...config, start: options.start ?? true };
947
2541
  if (options.dryRun)
948
2542
  body.dry_run = true;
949
2543
  if (options.name)
950
2544
  body.name = options.name;
2545
+ if (options.handle !== undefined)
2546
+ body.handle = options.handle;
951
2547
  if (options.size)
952
2548
  body.size = options.size;
953
2549
  if (options.meta?.ui)
954
2550
  body.meta = { ui: structuredClone(options.meta.ui) };
955
2551
  if (options.tags?.length)
956
2552
  body.tags = [...options.tags];
2553
+ if (options.runtime)
2554
+ body.runtime = options.runtime;
957
2555
  const data = await this.agentHttp.post(DEPLOYMENTS_API_PREFIX, body);
958
2556
  const agent = this.hydrateAgent(data);
959
2557
  if (agent instanceof OpenClawAgent) {
@@ -965,8 +2563,13 @@ export class Deployments {
965
2563
  return agent;
966
2564
  }
967
2565
  async createOpenClaw(options = {}) {
968
- const effectiveOptions = { ...options };
969
- effectiveOptions.env = { ...(options.env ?? {}) };
2566
+ const effectiveOptions = { ...options, runtime: options.runtime ?? 'openclaw' };
2567
+ effectiveOptions.env = {
2568
+ HYPER_API_BASE: productApiBaseFromAgentsApiBase(this.apiBase),
2569
+ ...buildOpenClawWorkspacesSyncEnv(options.workspacesSync ?? null),
2570
+ ...buildOpenClawMemoryIndexEnv(options.memoryIndex),
2571
+ ...(options.env ?? {}),
2572
+ };
970
2573
  if (options.routes === undefined) {
971
2574
  effectiveOptions.routes = buildOpenClawRoutes(options.openClawRoutes ?? {});
972
2575
  }
@@ -977,42 +2580,229 @@ export class Deployments {
977
2580
  effectiveOptions.syncEnabled = true;
978
2581
  return this.create(effectiveOptions);
979
2582
  }
2583
+ async createOpenClawPro(options = {}) {
2584
+ return this.createOpenClaw({
2585
+ ...options,
2586
+ runtime: 'openclaw-pro',
2587
+ env: { OPENCLAW_DESKTOP_ENABLED: '1', ...(options.env ?? {}) },
2588
+ image: defaultOpenClawProImage(options.image),
2589
+ openClawRoutes: { includeDesktop: true, ...(options.openClawRoutes ?? {}) },
2590
+ });
2591
+ }
2592
+ async createCodingAgent(runtime, options) {
2593
+ if (options.buzzEnabled && options.buzz) {
2594
+ throw new Error('buzzEnabled cannot be combined with buzz');
2595
+ }
2596
+ if ((options.buzzEnabled || options.buzz) && options.command !== undefined && options.command !== null) {
2597
+ throw new Error('Buzz launch cannot be combined with an explicit command');
2598
+ }
2599
+ const buzzLaunch = options.buzzEnabled || options.buzz !== undefined && options.buzz !== null;
2600
+ if (buzzLaunch && options.size !== undefined && options.size !== 'large') {
2601
+ throw new Error("Buzz coding agents require size='large'");
2602
+ }
2603
+ const effectiveEnv = {
2604
+ HYPER_API_BASE: productApiBaseFromAgentsApiBase(this.apiBase),
2605
+ ...buildOpenClawWorkspacesSyncEnv(options.workspacesSync ?? null),
2606
+ ...(options.env ?? {}),
2607
+ };
2608
+ if (options.buzz) {
2609
+ for (const key of BUZZ_RESERVED_ENV_KEYS)
2610
+ delete effectiveEnv[key];
2611
+ Object.assign(effectiveEnv, buildBuzzLaunchEnv(runtime, options.buzz, options.name));
2612
+ }
2613
+ if (buzzLaunch) {
2614
+ effectiveEnv.RUST_LOG ??= DEFAULT_BUZZ_RUST_LOG;
2615
+ }
2616
+ const effectiveOptions = {
2617
+ ...options,
2618
+ runtime,
2619
+ size: buzzLaunch ? 'large' : options.size,
2620
+ injectGatewayToken: false,
2621
+ env: effectiveEnv,
2622
+ routes: options.routes ?? {},
2623
+ image: options.image ?? (buzzLaunch
2624
+ ? DEFAULT_BUZZ_CODING_AGENT_IMAGES[runtime]
2625
+ : DEFAULT_CODING_AGENT_IMAGES[runtime]),
2626
+ command: options.buzzEnabled || options.buzz
2627
+ ? ['/usr/local/bin/buzz-acp']
2628
+ : options.command,
2629
+ syncRoot: options.syncRoot ?? DEFAULT_CODING_AGENT_SYNC_ROOT,
2630
+ syncEnabled: options.syncEnabled ?? true,
2631
+ syncUid: options.syncUid ?? 1000,
2632
+ syncGid: options.syncGid ?? 1000,
2633
+ restart: buzzLaunch
2634
+ ? options.restart ?? options.buzz?.restart ?? false
2635
+ : options.restart,
2636
+ };
2637
+ return await this.create(effectiveOptions);
2638
+ }
2639
+ async createOpenCode(options = {}) {
2640
+ return await this.createCodingAgent('opencode', options);
2641
+ }
2642
+ async createCodex(options = {}) {
2643
+ return await this.createCodingAgent('codex', options);
2644
+ }
2645
+ async createClaudeCode(options = {}) {
2646
+ return await this.createCodingAgent('claude-code', options);
2647
+ }
2648
+ async createGoose(options = {}) {
2649
+ return await this.createCodingAgent('goose', options);
2650
+ }
2651
+ async createKimiCode(options = {}) {
2652
+ return await this.createCodingAgent('kimi-code', options);
2653
+ }
980
2654
  async budget() {
981
2655
  return this.agentHttp.get(`${DEPLOYMENTS_API_PREFIX}/budget`);
982
2656
  }
983
- async metrics(agentId) {
2657
+ async bootstrapInference(messages, responseFormat = { type: 'json_object' }, requestOptions = {}) {
2658
+ return this.agentHttp.post('/bootstrap', {
2659
+ messages,
2660
+ response_format: responseFormat,
2661
+ }, requestOptions);
2662
+ }
2663
+ async metrics(agentIdOrName) {
2664
+ const agentId = await this.resolveAgentId(agentIdOrName);
984
2665
  return this.agentHttp.get(`${DEPLOYMENTS_API_PREFIX}/${agentId}/metrics`);
985
2666
  }
986
- async list() {
987
- const data = await this.agentHttp.get(DEPLOYMENTS_API_PREFIX);
2667
+ async list(options = {}) {
2668
+ const params = {};
2669
+ if (options.state)
2670
+ params.state = options.state;
2671
+ if (options.handle)
2672
+ params.handle = options.handle;
2673
+ if (options.name)
2674
+ params.name = options.name;
2675
+ if (options.query)
2676
+ params.q = options.query;
2677
+ if (options.includeDeleted !== undefined && options.includeDeleted !== null) {
2678
+ params.include_deleted = options.includeDeleted ? 'true' : 'false';
2679
+ }
2680
+ const requestOptions = {
2681
+ retries: options.retries,
2682
+ backoff: options.backoff,
2683
+ timeout: options.timeout,
2684
+ signal: options.signal,
2685
+ retryStatuses: options.retryStatuses,
2686
+ };
2687
+ const cleanRequestOptions = {};
2688
+ for (const key of Object.keys(requestOptions)) {
2689
+ const value = requestOptions[key];
2690
+ if (value !== undefined) {
2691
+ cleanRequestOptions[key] = value;
2692
+ }
2693
+ }
2694
+ const data = await this.agentHttp.get(DEPLOYMENTS_API_PREFIX, Object.keys(params).length ? params : undefined, Object.keys(cleanRequestOptions).length ? cleanRequestOptions : undefined);
988
2695
  const items = Array.isArray(data) ? data : data.items ?? [];
989
2696
  return items.map((item) => this.hydrateAgent(item));
990
2697
  }
991
- async get(agentId) {
992
- const data = await this.agentHttp.get(`${DEPLOYMENTS_API_PREFIX}/${agentId}`);
2698
+ async get(agentIdOrName, requestOptions = {}) {
2699
+ const raw = String(agentIdOrName || '').trim();
2700
+ if (!raw)
2701
+ throw new Error('agentIdOrName is required');
2702
+ if (!isDirectAgentIdRef(raw)) {
2703
+ return this.resolveAgent(raw, requestOptions);
2704
+ }
2705
+ try {
2706
+ return await this.getById(raw, requestOptions);
2707
+ }
2708
+ catch (error) {
2709
+ if (!(error instanceof APIError) || ![404, 422].includes(error.statusCode) || isUuidRef(raw)) {
2710
+ throw error;
2711
+ }
2712
+ return this.resolveAgent(raw, requestOptions);
2713
+ }
2714
+ }
2715
+ async createExternalAgent(options) {
2716
+ const body = {
2717
+ name: options.name,
2718
+ runtime: options.runtime ?? 'openclaw',
2719
+ status: options.status ?? 'active',
2720
+ };
2721
+ if (options.displayName !== undefined)
2722
+ body.display_name = options.displayName;
2723
+ if (options.handle !== undefined)
2724
+ body.handle = options.handle;
2725
+ if (options.meta !== undefined)
2726
+ body.meta = options.meta;
2727
+ const data = await this.agentHttp.post('/external-agents', body);
2728
+ return this.hydrateAgent(data);
2729
+ }
2730
+ async updateExternalAgent(externalAgentId, options) {
2731
+ const body = {};
2732
+ if (options.name !== undefined)
2733
+ body.name = options.name;
2734
+ if (options.displayName !== undefined)
2735
+ body.display_name = options.displayName;
2736
+ if (options.handle !== undefined)
2737
+ body.handle = options.handle;
2738
+ if (options.runtime !== undefined)
2739
+ body.runtime = options.runtime;
2740
+ if (options.status !== undefined)
2741
+ body.status = options.status;
2742
+ if (options.meta !== undefined)
2743
+ body.meta = options.meta;
2744
+ const data = await this.agentHttp.patch(`/external-agents/${externalAgentId}`, body);
2745
+ return this.hydrateAgent(data);
2746
+ }
2747
+ async getExternalAgent(externalAgentId) {
2748
+ const data = await this.agentHttp.get(`/external-agents/${externalAgentId}`);
993
2749
  return this.hydrateAgent(data);
994
2750
  }
995
- async waitRunning(agentId, timeoutMs = 300_000, pollIntervalMs = 5_000) {
2751
+ async uploadExternalAgentProfileImage(externalAgentId, content, contentType) {
2752
+ const resolvedContentType = contentType || (content instanceof Blob ? content.type : '') || 'image/png';
2753
+ return this.agentHttp.postRaw(`/external-agents/${externalAgentId}/profile-image`, content, resolvedContentType);
2754
+ }
2755
+ async deleteExternalAgentProfileImage(externalAgentId) {
2756
+ return this.agentHttp.delete(`/external-agents/${externalAgentId}/profile-image`);
2757
+ }
2758
+ async rotateExternalAgentKey(agentIdOrName) {
2759
+ const agentId = await this.resolveAgentId(agentIdOrName);
2760
+ return this.agentHttp.post(`/external-agents/${agentId}/keys/rotate`);
2761
+ }
2762
+ async attachSlackRelayAgent(agentIdOrName, options) {
2763
+ const agentId = await this.resolveAgentId(agentIdOrName);
2764
+ return attachSlackRelayAgent({
2765
+ relayBaseUrl: options.relayBaseUrl,
2766
+ token: options.token || this.apiKey,
2767
+ agentId,
2768
+ });
2769
+ }
2770
+ async waitRunning(agentIdOrName, timeoutMs = 300_000, pollIntervalMs = 5_000) {
2771
+ const agentId = await this.resolveAgentId(agentIdOrName);
996
2772
  const deadline = Date.now() + timeoutMs;
997
2773
  let lastState = '';
2774
+ let lastAgent = null;
2775
+ const diagnostics = (agent) => {
2776
+ if (!agent)
2777
+ return '';
2778
+ const details = [];
2779
+ if (agent.lastError)
2780
+ details.push(`lastError=${JSON.stringify(agent.lastError)}`);
2781
+ if (agent.updatedAt && !Number.isNaN(agent.updatedAt.getTime())) {
2782
+ details.push(`updatedAt=${agent.updatedAt.toISOString()}`);
2783
+ }
2784
+ return details.length ? `, ${details.join(', ')}` : '';
2785
+ };
998
2786
  while (Date.now() < deadline) {
999
2787
  const agent = await this.get(agentId);
2788
+ lastAgent = agent;
1000
2789
  lastState = String(agent.state || '');
1001
2790
  if (lastState.toLowerCase() === 'running') {
1002
2791
  return agent;
1003
2792
  }
1004
- if (lastState.toLowerCase() === 'failed' || lastState.toLowerCase() === 'error') {
1005
- throw new Error(`Agent entered ${lastState} while waiting for RUNNING`);
2793
+ if (['failed', 'error', 'restore_failed', 'sync_failed'].includes(lastState.toLowerCase())) {
2794
+ throw new Error(`Agent entered ${lastState} while waiting for RUNNING${diagnostics(agent)}`);
1006
2795
  }
1007
2796
  await sleep(pollIntervalMs);
1008
2797
  }
1009
- throw new Error(`Timed out waiting for agent ${agentId} to reach RUNNING (last=${lastState})`);
2798
+ throw new Error(`Timed out waiting for agent ${agentId} to reach RUNNING (last=${lastState || 'unknown'}${diagnostics(lastAgent)})`);
1010
2799
  }
1011
- async start(agentId, options = {}) {
2800
+ async start(agentIdOrName, options = {}) {
1012
2801
  const { config, gatewayToken } = buildAgentConfig(options.config ?? {}, options);
1013
2802
  const body = { ...config };
1014
2803
  if (options.dryRun)
1015
2804
  body.dry_run = true;
2805
+ const agentId = await this.resolveAgentId(agentIdOrName);
1016
2806
  const data = await this.agentHttp.post(`${DEPLOYMENTS_API_PREFIX}/${agentId}/start`, body);
1017
2807
  const agent = this.hydrateAgent(data);
1018
2808
  if (agent instanceof OpenClawAgent) {
@@ -1023,9 +2813,14 @@ export class Deployments {
1023
2813
  agent.entrypoint = [...(config.entrypoint ?? [])];
1024
2814
  return agent;
1025
2815
  }
1026
- async startOpenClaw(agentId, options = {}) {
2816
+ async startOpenClaw(agentIdOrName, options = {}) {
1027
2817
  const effectiveOptions = { ...options };
1028
- effectiveOptions.env = { ...(options.env ?? {}) };
2818
+ effectiveOptions.env = {
2819
+ HYPER_API_BASE: productApiBaseFromAgentsApiBase(this.apiBase),
2820
+ ...buildOpenClawWorkspacesSyncEnv(options.workspacesSync ?? null),
2821
+ ...buildOpenClawMemoryIndexEnv(options.memoryIndex),
2822
+ ...(options.env ?? {}),
2823
+ };
1029
2824
  if (options.routes === undefined) {
1030
2825
  effectiveOptions.routes = buildOpenClawRoutes(options.openClawRoutes ?? {});
1031
2826
  }
@@ -1034,38 +2829,71 @@ export class Deployments {
1034
2829
  effectiveOptions.syncRoot = DEFAULT_OPENCLAW_SYNC_ROOT;
1035
2830
  if (effectiveOptions.syncEnabled === undefined)
1036
2831
  effectiveOptions.syncEnabled = true;
1037
- return this.start(agentId, effectiveOptions);
2832
+ return this.start(agentIdOrName, effectiveOptions);
2833
+ }
2834
+ async startOpenClawPro(agentIdOrName, options = {}) {
2835
+ return this.startOpenClaw(agentIdOrName, {
2836
+ ...options,
2837
+ env: { OPENCLAW_DESKTOP_ENABLED: '1', ...(options.env ?? {}) },
2838
+ image: defaultOpenClawProImage(options.image),
2839
+ openClawRoutes: { includeDesktop: true, ...(options.openClawRoutes ?? {}) },
2840
+ });
1038
2841
  }
1039
- async update(agentId, options = {}) {
2842
+ async update(agentIdOrName, options = {}) {
1040
2843
  const body = {};
1041
2844
  if (options.name !== undefined)
1042
2845
  body.name = options.name;
2846
+ if (options.handle !== undefined)
2847
+ body.handle = options.handle;
1043
2848
  if (options.size !== undefined)
1044
2849
  body.size = options.size;
2850
+ if (options.launchConfig !== undefined)
2851
+ body.launch_config = options.launchConfig;
1045
2852
  if (options.refreshFromLagoon !== undefined)
1046
2853
  body.refresh_from_lagoon = options.refreshFromLagoon;
1047
2854
  if (options.lastError !== undefined)
1048
2855
  body.last_error = options.lastError;
2856
+ const agentId = await this.resolveAgentId(agentIdOrName);
1049
2857
  const data = await this.agentHttp.patch(`${DEPLOYMENTS_API_PREFIX}/${agentId}`, body);
1050
2858
  return this.hydrateAgent(data);
1051
2859
  }
2860
+ async uploadProfileImage(agentId, content, contentType) {
2861
+ const resolvedAgentId = await this.resolveAgentId(agentId);
2862
+ const resolvedContentType = contentType || (content instanceof Blob ? content.type : '') || 'image/png';
2863
+ return this.agentHttp.postRaw(`${DEPLOYMENTS_API_PREFIX}/${resolvedAgentId}/profile-image`, content, resolvedContentType);
2864
+ }
2865
+ async deleteProfileImage(agentId) {
2866
+ const resolvedAgentId = await this.resolveAgentId(agentId);
2867
+ return this.agentHttp.delete(`${DEPLOYMENTS_API_PREFIX}/${resolvedAgentId}/profile-image`);
2868
+ }
1052
2869
  async resize(agentId, options) {
1053
2870
  return this.update(agentId, options);
1054
2871
  }
1055
- async stop(agentId) {
2872
+ /**
2873
+ * Request an agent stop.
2874
+ *
2875
+ * The returned agent remains `STOPPING` while runtime cleanup is in
2876
+ * progress. Fetch it again until it becomes `STOPPED` before treating its
2877
+ * deployment slot as released.
2878
+ */
2879
+ async stop(agentIdOrName) {
2880
+ const agentId = await this.resolveAgentId(agentIdOrName);
1056
2881
  const data = await this.agentHttp.post(`${DEPLOYMENTS_API_PREFIX}/${agentId}/stop`);
1057
2882
  return this.hydrateAgent(data);
1058
2883
  }
1059
- async delete(agentId) {
2884
+ async delete(agentIdOrName) {
2885
+ const agentId = await this.resolveAgentId(agentIdOrName);
1060
2886
  return this.agentHttp.delete(`${DEPLOYMENTS_API_PREFIX}/${agentId}`);
1061
2887
  }
1062
- async refreshToken(agentId) {
2888
+ async refreshToken(agentIdOrName) {
2889
+ const agentId = await this.resolveAgentId(agentIdOrName);
1063
2890
  return this.agentHttp.get(`${DEPLOYMENTS_API_PREFIX}/${agentId}/token`);
1064
2891
  }
1065
- async createScopedKey(agentId, name) {
2892
+ async createScopedKey(agentIdOrName, name) {
1066
2893
  const payload = {};
1067
2894
  if (name)
1068
2895
  payload.name = name;
2896
+ const agentId = await this.resolveAgentId(agentIdOrName);
1069
2897
  return this.agentHttp.post(`${DEPLOYMENTS_API_PREFIX}/${agentId}/keys`, Object.keys(payload).length ? payload : undefined);
1070
2898
  }
1071
2899
  async webSearch(query, options = {}) {
@@ -1088,14 +2916,19 @@ export class Deployments {
1088
2916
  const response = await this.fetchRaw(`/brave/res/v1/web/search?${new URLSearchParams(Object.fromEntries(Object.entries(params).map(([key, value]) => [key, String(value)]))).toString()}`, { headers });
1089
2917
  return (await response.json());
1090
2918
  }
1091
- async logsToken(agentId) {
2919
+ async logsToken(agentIdOrName) {
2920
+ const agentId = await this.resolveAgentId(agentIdOrName);
1092
2921
  return this.agentHttp.post(`${DEPLOYMENTS_API_PREFIX}/${agentId}/logs/token`);
1093
2922
  }
1094
- async env(agentId) {
1095
- return this.agentHttp.get(`${DEPLOYMENTS_API_PREFIX}/${agentId}/env`);
2923
+ async env(agentIdOrName, requestOptions = {}) {
2924
+ const agentId = await this.resolveAgentId(agentIdOrName, requestOptions);
2925
+ const path = `${DEPLOYMENTS_API_PREFIX}/${agentId}/env`;
2926
+ return Object.keys(requestOptions).length === 0
2927
+ ? this.agentHttp.get(path)
2928
+ : this.agentHttp.get(path, undefined, requestOptions);
1096
2929
  }
1097
2930
  async exec(target, command, options = {}) {
1098
- const agentId = this.agentIdFor(target);
2931
+ const agentId = await this.agentIdFor(target);
1099
2932
  const payload = {
1100
2933
  command,
1101
2934
  timeout: options.timeout ?? 30,
@@ -1121,20 +2954,20 @@ export class Deployments {
1121
2954
  return (await response.json());
1122
2955
  }
1123
2956
  async filesList(target, path = '', source = 'auto') {
1124
- const encodedPath = encodeFilePath(path);
1125
- const suffix = encodedPath ? `/${encodedPath}` : '';
1126
- const params = new URLSearchParams({ source });
1127
- const response = await this.fetchRaw(`${DEPLOYMENTS_API_PREFIX}/${this.agentIdFor(target)}/files${suffix}?${params.toString()}`);
2957
+ const { suffix, params } = deploymentFileReadRequest(path, source);
2958
+ const agentId = await this.agentIdFor(target);
2959
+ const response = await this.fetchRaw(`${DEPLOYMENTS_API_PREFIX}/${agentId}/files${suffix}?${params.toString()}`);
1128
2960
  const payload = (await response.json());
1129
2961
  return [...(payload.directories ?? []), ...(payload.files ?? [])];
1130
2962
  }
1131
- async fileReadBytes(target, path, source = 'auto') {
1132
- const encodedPath = encodeFilePath(path);
1133
- const params = new URLSearchParams({ source });
1134
- const response = await this.fetchRaw(`${DEPLOYMENTS_API_PREFIX}/${this.agentIdFor(target)}/files/${encodedPath}?${params.toString()}`, {
2963
+ async fileReadBytesWithMetadata(target, path, source = 'auto', options) {
2964
+ const { suffix, params } = deploymentFileReadRequest(path, source);
2965
+ const agentId = await this.agentIdFor(target);
2966
+ const response = await this.fetchRaw(`${DEPLOYMENTS_API_PREFIX}/${agentId}/files${suffix}?${params.toString()}`, {
1135
2967
  redirect: 'follow',
2968
+ signal: options?.signal,
1136
2969
  });
1137
- const bytes = new Uint8Array(await response.arrayBuffer());
2970
+ const bytes = await readResponseBytes(response, path, options?.maxBytes);
1138
2971
  const contentType = response.headers.get('content-type') || '';
1139
2972
  if (contentType.includes('application/json')) {
1140
2973
  try {
@@ -1149,19 +2982,24 @@ export class Deployments {
1149
2982
  }
1150
2983
  }
1151
2984
  }
1152
- return bytes;
2985
+ return { content: bytes, mimeType: contentType || undefined };
1153
2986
  }
1154
- async fileRead(target, path, source = 'auto') {
1155
- return decodeUtf8(await this.fileReadBytes(target, path, source));
2987
+ async fileReadBytes(target, path, source = 'auto', options) {
2988
+ return (await this.fileReadBytesWithMetadata(target, path, source, options)).content;
2989
+ }
2990
+ async fileRead(target, path, source = 'auto', options) {
2991
+ return decodeUtf8(await this.fileReadBytes(target, path, source, options));
1156
2992
  }
1157
2993
  async fileWriteBytes(target, path, content, destination = 'auto') {
2994
+ path = normalizeWritableBackendFilePath(path);
1158
2995
  const encodedPath = encodeFilePath(path);
1159
2996
  const params = new URLSearchParams({ destination });
1160
2997
  const bytes = toUint8Array(content);
1161
2998
  if (bytes.byteLength > AGENT_FILE_MAX_BYTES) {
1162
2999
  throw new Error(`Agent file writes are limited to ${AGENT_FILE_MAX_BYTES / 1024 / 1024} MiB`);
1163
3000
  }
1164
- const response = await this.fetchRaw(`${DEPLOYMENTS_API_PREFIX}/${this.agentIdFor(target)}/files/${encodedPath}?${params.toString()}`, {
3001
+ const agentId = await this.agentIdFor(target);
3002
+ const response = await this.fetchRaw(`${DEPLOYMENTS_API_PREFIX}/${agentId}/files/${encodedPath}?${params.toString()}`, {
1165
3003
  method: 'POST',
1166
3004
  headers: { 'Content-Type': 'application/octet-stream' },
1167
3005
  body: bytes,
@@ -1172,12 +3010,16 @@ export class Deployments {
1172
3010
  return this.fileWriteBytes(target, path, content, destination);
1173
3011
  }
1174
3012
  async fileDelete(target, path, options = {}) {
3013
+ path = normalizeWritableBackendFilePath(path);
1175
3014
  const encodedPath = encodeFilePath(path);
1176
3015
  const params = new URLSearchParams();
1177
3016
  if (options.recursive)
1178
3017
  params.set('recursive', 'true');
3018
+ if (options.source && options.source !== 'auto')
3019
+ params.set('source', options.source);
1179
3020
  const suffix = params.toString() ? `?${params.toString()}` : '';
1180
- const response = await this.fetchRaw(`${DEPLOYMENTS_API_PREFIX}/${this.agentIdFor(target)}/files/${encodedPath}${suffix}`, { method: 'DELETE' });
3021
+ const agentId = await this.agentIdFor(target);
3022
+ const response = await this.fetchRaw(`${DEPLOYMENTS_API_PREFIX}/${agentId}/files/${encodedPath}${suffix}`, { method: 'DELETE' });
1181
3023
  return (await response.json());
1182
3024
  }
1183
3025
  async cpTo(target, localPath, remotePath) {
@@ -1196,7 +3038,8 @@ export class Deployments {
1196
3038
  await fs.writeFile(destination, content);
1197
3039
  return destination;
1198
3040
  }
1199
- async logsConnect(agentId, options = {}) {
3041
+ async logsConnect(agentIdOrName, options = {}) {
3042
+ const agentId = await this.resolveAgentId(agentIdOrName);
1200
3043
  const tokenData = await this.logsToken(agentId);
1201
3044
  const container = options.container ?? 'reef';
1202
3045
  const tailLines = options.tailLines ?? 100;
@@ -1218,32 +3061,92 @@ export class Deployments {
1218
3061
  };
1219
3062
  });
1220
3063
  }
1221
- async shellToken(agentId, shell, dryRun = false) {
3064
+ async shellToken(agentIdOrName, shell, dryRun = false, requestOptions = {}) {
1222
3065
  const selectedShell = shell ?? '/bin/bash';
1223
3066
  const payload = { shell: selectedShell };
1224
3067
  if (dryRun)
1225
3068
  payload.dry_run = true;
1226
- return this.agentHttp.post(`${DEPLOYMENTS_API_PREFIX}/${agentId}/shell/token`, payload);
1227
- }
1228
- async shellConnect(agentId, shell) {
3069
+ const agentId = await this.resolveAgentId(agentIdOrName, requestOptions);
3070
+ const path = `${DEPLOYMENTS_API_PREFIX}/${agentId}/shell/token`;
3071
+ if (Object.keys(requestOptions).length === 0)
3072
+ return this.agentHttp.post(path, payload);
3073
+ return this.agentHttp.post(path, payload, requestOptions);
3074
+ }
3075
+ async shellConnect(agentIdOrName, shell, options = {}) {
3076
+ if (options.signal?.aborted)
3077
+ throw shellAbortError();
3078
+ const tokenTimeoutMs = options.tokenTimeoutMs ?? 15_000;
3079
+ const openTimeoutMs = options.openTimeoutMs ?? 10_000;
3080
+ const tokenDeadline = Date.now() + tokenTimeoutMs;
3081
+ const remainingTokenTime = () => Math.max(0, tokenDeadline - Date.now());
3082
+ const runBeforeTokenDeadline = (operation) => {
3083
+ const remaining = remainingTokenTime();
3084
+ if (remaining <= 0)
3085
+ return Promise.reject(new Error('Shell token request timed out'));
3086
+ return runShellOperation(operation, options.signal, remaining, 'Shell token request timed out');
3087
+ };
3088
+ const agentId = await runBeforeTokenDeadline((signal) => this.resolveAgentId(agentIdOrName, { signal }));
1229
3089
  const connectWithShell = async (requestedShell) => {
1230
- const tokenData = await this.shellToken(agentId, requestedShell);
3090
+ const remaining = remainingTokenTime();
3091
+ if (remaining <= 0)
3092
+ throw new Error('Shell token request timed out');
3093
+ const requestOptions = {
3094
+ retries: 3,
3095
+ timeout: Math.max(500, Math.floor((tokenTimeoutMs - 3_000) / 3)),
3096
+ retryStatuses: [429, 502, 503, 504],
3097
+ };
3098
+ const tokenData = await runBeforeTokenDeadline((signal) => {
3099
+ requestOptions.signal = signal;
3100
+ return this.shellToken(agentId, requestedShell, false, requestOptions);
3101
+ });
1231
3102
  const baseUrl = `${this.agentsWsUrl}/shell/${agentId}`;
1232
3103
  const separator = baseUrl.includes("?") ? "&" : "?";
1233
3104
  const wsUrl = `${baseUrl}${separator}jwt=${encodeURIComponent(tokenData.jwt)}` +
1234
3105
  `&shell=${encodeURIComponent(tokenData.shell || requestedShell)}`;
1235
3106
  const ws = new WebSocket(wsUrl);
3107
+ ws.binaryType = 'arraybuffer';
1236
3108
  return await new Promise((resolve, reject) => {
1237
3109
  let settled = false;
1238
- ws.onopen = () => {
1239
- settled = true;
1240
- resolve(ws);
3110
+ const abortConnection = () => finish(shellAbortError());
3111
+ const openTimer = setTimeout(() => finish(new Error('Shell connection timed out')), openTimeoutMs);
3112
+ const cleanup = () => {
3113
+ clearTimeout(openTimer);
3114
+ options.signal?.removeEventListener('abort', abortConnection);
1241
3115
  };
1242
- ws.onerror = () => {
1243
- if (!settled) {
1244
- reject(new Error('WebSocket connection failed'));
3116
+ const finish = (error) => {
3117
+ if (settled)
3118
+ return;
3119
+ settled = true;
3120
+ cleanup();
3121
+ if (error) {
3122
+ try {
3123
+ ws.close();
3124
+ }
3125
+ catch {
3126
+ // The browser may have already finalized a failed socket.
3127
+ }
3128
+ reject(error);
3129
+ }
3130
+ else {
3131
+ resolve(ws);
1245
3132
  }
1246
3133
  };
3134
+ if (options.signal?.aborted) {
3135
+ finish(shellAbortError());
3136
+ return;
3137
+ }
3138
+ options.signal?.addEventListener('abort', abortConnection, { once: true });
3139
+ ws.onopen = () => {
3140
+ finish();
3141
+ };
3142
+ ws.onerror = () => undefined;
3143
+ ws.onclose = (event) => {
3144
+ const reason = event.reason ? `: ${event.reason}` : '';
3145
+ const error = new Error(`WebSocket closed before opening${reason}`);
3146
+ error.closeCode = event.code;
3147
+ error.closeReason = event.reason;
3148
+ finish(error);
3149
+ };
1247
3150
  });
1248
3151
  };
1249
3152
  if (shell) {
@@ -1252,7 +3155,14 @@ export class Deployments {
1252
3155
  try {
1253
3156
  return await connectWithShell('/bin/bash');
1254
3157
  }
1255
- catch {
3158
+ catch (error) {
3159
+ if (options.signal?.aborted)
3160
+ throw error;
3161
+ const detail = error instanceof APIError ? error.detail : error instanceof Error ? error.message : '';
3162
+ const bashUnavailable = /(\/bin\/bash|\bbash\b)/i.test(detail)
3163
+ && /(not found|no such file|missing|unavailable|unsupported)/i.test(detail);
3164
+ if (!bashUnavailable)
3165
+ throw error;
1256
3166
  return connectWithShell('/bin/sh');
1257
3167
  }
1258
3168
  }