@hypercli.com/cli 0.0.0-stage → 0.9.2

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 (98) hide show
  1. package/README.md +25 -2
  2. package/dist/commands/acp.d.ts +5 -0
  3. package/dist/commands/acp.js +51 -0
  4. package/dist/commands/acp.js.map +1 -0
  5. package/dist/commands/agent-login.d.ts +97 -0
  6. package/dist/commands/agent-login.js +547 -0
  7. package/dist/commands/agent-login.js.map +1 -0
  8. package/dist/commands/agents.d.ts +48 -0
  9. package/dist/commands/agents.js +2026 -0
  10. package/dist/commands/agents.js.map +1 -0
  11. package/dist/commands/configure.d.ts +11 -0
  12. package/dist/commands/configure.js +59 -0
  13. package/dist/commands/configure.js.map +1 -0
  14. package/dist/commands/files.d.ts +15 -0
  15. package/dist/commands/files.js +176 -0
  16. package/dist/commands/files.js.map +1 -0
  17. package/dist/commands/flow.d.ts +20 -0
  18. package/dist/commands/flow.js +360 -0
  19. package/dist/commands/flow.js.map +1 -0
  20. package/dist/commands/integrations.d.ts +32 -0
  21. package/dist/commands/integrations.js +460 -0
  22. package/dist/commands/integrations.js.map +1 -0
  23. package/dist/commands/jobs.d.ts +5 -0
  24. package/dist/commands/jobs.js +496 -0
  25. package/dist/commands/jobs.js.map +1 -0
  26. package/dist/commands/me.d.ts +23 -0
  27. package/dist/commands/me.js +258 -0
  28. package/dist/commands/me.js.map +1 -0
  29. package/dist/commands/memory.d.ts +5 -0
  30. package/dist/commands/memory.js +110 -0
  31. package/dist/commands/memory.js.map +1 -0
  32. package/dist/commands/routines.d.ts +27 -0
  33. package/dist/commands/routines.js +493 -0
  34. package/dist/commands/routines.js.map +1 -0
  35. package/dist/commands/skills.d.ts +36 -0
  36. package/dist/commands/skills.js +369 -0
  37. package/dist/commands/skills.js.map +1 -0
  38. package/dist/commands/voice.d.ts +15 -0
  39. package/dist/commands/voice.js +378 -0
  40. package/dist/commands/voice.js.map +1 -0
  41. package/dist/commands/websearch-core.d.ts +10 -0
  42. package/dist/commands/websearch-core.js +56 -0
  43. package/dist/commands/websearch-core.js.map +1 -0
  44. package/dist/commands/websearch.d.ts +13 -0
  45. package/dist/commands/websearch.js +20 -0
  46. package/dist/commands/websearch.js.map +1 -0
  47. package/dist/core/argv.d.ts +51 -0
  48. package/dist/core/argv.js +81 -0
  49. package/dist/core/argv.js.map +1 -0
  50. package/dist/core/captured-logs.d.ts +9 -0
  51. package/dist/core/captured-logs.js +52 -0
  52. package/dist/core/captured-logs.js.map +1 -0
  53. package/dist/core/client.d.ts +18 -0
  54. package/dist/core/client.js +31 -0
  55. package/dist/core/client.js.map +1 -0
  56. package/dist/core/config-file.d.ts +13 -0
  57. package/dist/core/config-file.js +79 -0
  58. package/dist/core/config-file.js.map +1 -0
  59. package/dist/core/errors.d.ts +22 -0
  60. package/dist/core/errors.js +37 -0
  61. package/dist/core/errors.js.map +1 -0
  62. package/dist/core/help.d.ts +13 -0
  63. package/dist/core/help.js +55 -0
  64. package/dist/core/help.js.map +1 -0
  65. package/dist/core/integrations-mcp.d.ts +2 -0
  66. package/dist/core/integrations-mcp.js +115 -0
  67. package/dist/core/integrations-mcp.js.map +1 -0
  68. package/dist/core/output.d.ts +29 -0
  69. package/dist/core/output.js +34 -0
  70. package/dist/core/output.js.map +1 -0
  71. package/dist/core/skills-runtime.d.ts +37 -0
  72. package/dist/core/skills-runtime.js +101 -0
  73. package/dist/core/skills-runtime.js.map +1 -0
  74. package/dist/core/table.d.ts +17 -0
  75. package/dist/core/table.js +34 -0
  76. package/dist/core/table.js.map +1 -0
  77. package/dist/core/types.d.ts +65 -0
  78. package/dist/core/types.js +40 -0
  79. package/dist/core/types.js.map +1 -0
  80. package/dist/index.d.ts +11 -0
  81. package/dist/index.js +73 -0
  82. package/dist/index.js.map +1 -0
  83. package/dist/registry.d.ts +10 -0
  84. package/dist/registry.js +39 -0
  85. package/dist/registry.js.map +1 -0
  86. package/dist/version.d.ts +1 -0
  87. package/dist/version.js +3 -0
  88. package/dist/version.js.map +1 -0
  89. package/package.json +33 -4
  90. package/skills/hypercli-account.json +17 -0
  91. package/skills/hypercli-agents.json +23 -0
  92. package/skills/hypercli-auth.json +11 -0
  93. package/skills/hypercli-compute.json +20 -0
  94. package/skills/hypercli-flows.json +11 -0
  95. package/skills/hypercli-knowledge.json +13 -0
  96. package/skills/hypercli-voice.json +10 -0
  97. package/skills/hypercli.json +23 -0
  98. package/skills/index.json +124 -0
@@ -0,0 +1,2026 @@
1
+ /**
2
+ * `hyper agents` — manage hosted agents (deployments).
3
+ *
4
+ * Golden-pattern duties carried by this group:
5
+ * - <id> arguments accept an unambiguous id prefix (list + prefix match,
6
+ * ambiguous -> UsageError listing candidates). Later groups copy this.
7
+ * - Records render as label/value blocks and curated JSON bags; gateway
8
+ * tokens, jwt tokens, and secret-shaped values are redacted to last-4.
9
+ *
10
+ * Substitutions against the original spec (records.ts / Commands.adopt do not
11
+ * exist in the frozen core, so this module ships local equivalents):
12
+ * - adopt(ctx) resolves the lazy SDK client (ctx.client()) once per call.
13
+ * - routines ride client.routines (RoutinesAPI), not d.routines.
14
+ * - activate calls client.agent.redeemGrantCode (agent.ts), not a
15
+ * deployments method.
16
+ * - token calls d.createScopedKey(id, name); the SDK has no typed scopes
17
+ * field, so repeated --scope values are joined into the key name.
18
+ */
19
+ import { mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
20
+ import { dirname } from 'node:path';
21
+ import { createInterface } from 'node:readline/promises';
22
+ import { AGENT_EXEC_STDIN_MAX_BYTES, APIError, applySessionReactionOperation, newerSessionMessageMeta, sessionReactionDisplay, getAgentsApiBaseUrl, } from '@hypercli.com/sdk';
23
+ import { parseCommandArgs } from '../core/argv.js';
24
+ import { cmdAgentsLogin } from './agent-login.js';
25
+ import { capturedLogRenderer } from '../core/captured-logs.js';
26
+ import { CliError, UsageError } from '../core/errors.js';
27
+ import { renderGroupHelp } from '../core/help.js';
28
+ export const name = 'agents';
29
+ export const summary = 'Manage hosted agents (deployments, shells, logs, files, routines).';
30
+ export const usage = [
31
+ 'hyper agents ls [--state X]',
32
+ 'hyper agents status <id> [--verbose]',
33
+ 'hyper agents wait <id> [--state X] [--timeout S] [--interval S]',
34
+ 'hyper agents create <name> --runtime openclaw|hermes|generic|goose|opencode|codex|claude-code|kimi-code|pi|buzz [--model M] [--plan P] [--size S] [--env K=V ...] [--param k=v ...] [--secret K=V ...] [--image IMG] [--sh SCRIPT] [--sync-root P] [--sync-include P ... | --sync-exclude G ...] [--registry-url U --registry-username U --registry-password P] [--restart | --no-restart] [--runner-tags a,b] [--runner-id UUID] [--executor process|docker] [--cwd RUNNER_HOST_PATH] [--dry-run] [-- CMD [ARGS...]]',
35
+ 'hyper agents start <id>',
36
+ 'hyper agents set runtime <id> <runtime> (resets the launch image to the runtime default)',
37
+ 'hyper agents chat <id> <prompt...> [-s|--session NAME] [--cwd PATH] [--mcp-servers FILE|-] [--permissions auto|none] [--timeout S] [--stream]',
38
+ 'hyper agents messages <session-id> [--cursor CURSOR] [--limit N] [--follow] (read-only; follow emits current snapshots)',
39
+ 'hyper agents stop <id> [--yes]',
40
+ 'hyper agents delete <id> [--yes]',
41
+ 'hyper agents exec <id> [--] CMD [ARGS...]',
42
+ 'hyper agents login <id> [--flow F] [--provider X] [--session SECS] [--key-stdin] [--from-host-creds]',
43
+ 'hyper agents shell <id>',
44
+ 'hyper agents logs <id> [-f|--follow] [-n LINES] [--source both|stdout|stderr] [--generation current|previous] [--max-bytes N] [--max-lines N]',
45
+ 'hyper agents cp <src> <dst> (exactly one side must be <id>:<path>; remote path may be sync-root-relative, /absolute, or ~/…)',
46
+ 'hyper agents activate <code> [--extend-existing]',
47
+ 'hyper agents routines list [--agent ID]',
48
+ 'hyper agents routines create (--cron EXPR | --run-at ISO) --prompt TEXT [--agent ID] [--name N] [--session ID] [--disabled]',
49
+ 'hyper agents routines delete <routine-id> [--yes]',
50
+ ];
51
+ /** Hidden commands work but stay out of the help listing. */
52
+ const HIDDEN = ['archive', 'restore', 'token', 'config', 'routes', 'models'];
53
+ /** CLI --runtime name → SDK runtime label; everything rides the flat createAgent. */
54
+ const RUNTIME_LABELS = new Map([
55
+ ['openclaw', 'openclaw_acp'],
56
+ ['openclaw_acp', 'openclaw_acp'],
57
+ ['hermes', 'hermes_acp'],
58
+ ['hermes_acp', 'hermes_acp'],
59
+ ['generic', 'generic'],
60
+ ['goose', 'goose'],
61
+ ['opencode', 'opencode'],
62
+ ['codex', 'codex'],
63
+ ['claude-code', 'claude-code'],
64
+ ['kimi-code', 'kimi-code'],
65
+ ['pi', 'pi'],
66
+ ['buzz', 'buzz-agent'],
67
+ ]);
68
+ /** desktop/src/agent-utils parity: only these runtimes get the token ceremony. */
69
+ const OPENCLAW_SET = new Set(['openclaw_acp']);
70
+ const HERMES_SET = new Set(['hermes_acp']);
71
+ /** Agent family: chat rides the pod-side ACP bridge, never a gateway. */
72
+ const ACP_SET = new Set(['opencode', 'goose', 'codex', 'claude-code', 'kimi-code', 'pi', 'buzz-agent']);
73
+ const OPENCLAW_RUNTIMES = OPENCLAW_SET;
74
+ const HERMES_RUNTIMES = HERMES_SET;
75
+ const ACP_RUNTIMES = ACP_SET;
76
+ const KNOWN_COMMANDS = new Set([
77
+ 'ls', 'list', 'status', 'wait', 'create', 'start', 'chat', 'stop', 'delete', 'exec',
78
+ 'shell', 'logs', 'cp', 'activate', 'routines', 'set', 'login', 'messages', ...HIDDEN,
79
+ ]);
80
+ const MANAGED_RUNTIMES = new Set([
81
+ 'generic', ...OPENCLAW_SET, ...HERMES_SET, ...ACP_SET,
82
+ ]);
83
+ // Backend-folded legacy wire spellings (launch_contract.py
84
+ // LEGACY_RUNTIME_MIGRATIONS): existing DB rows may still serve them and users
85
+ // still type them, so the CLI folds them to canonical at the read/display
86
+ // boundary instead of rejecting legacy payloads.
87
+ const LEGACY_RUNTIME_FOLD = {
88
+ openclaw: 'openclaw_acp',
89
+ 'hermes-agent': 'hermes_acp',
90
+ };
91
+ /** Map a stored or user-supplied legacy runtime spelling to its canonical name. */
92
+ export function canonicalRuntimeName(runtime) {
93
+ return LEGACY_RUNTIME_FOLD[runtime.toLowerCase()] ?? runtime;
94
+ }
95
+ // ---------------------------------------------------------------------------
96
+ // record helpers (local stand-ins for the records.ts contract)
97
+ // ---------------------------------------------------------------------------
98
+ function shortId(id) {
99
+ return id.length > 12 ? id.slice(0, 12) : id;
100
+ }
101
+ /** Label/value block (ut-style): two columns, labels left-padded. */
102
+ function recordLabelValue(rows) {
103
+ const width = Math.max(0, ...rows.map(([key]) => key.length));
104
+ return rows.map(([key, value]) => `${key.padEnd(width)} ${value}`.trimEnd()).join('\n');
105
+ }
106
+ const SENSITIVE_KEY = /(secret|token|api[-_]?key|password|credential|jwt)/i;
107
+ function maskSecret(value) {
108
+ return value.length > 4 ? `...${value.slice(-4)}` : '****';
109
+ }
110
+ /** Deep-copy a record bag, masking any string stored under a secret-shaped key. */
111
+ function redactRecord(value, key) {
112
+ if (typeof value === 'string')
113
+ return key && SENSITIVE_KEY.test(key) ? maskSecret(value) : value;
114
+ if (value instanceof Date)
115
+ return value.toISOString();
116
+ if (Array.isArray(value))
117
+ return value.map((item) => redactRecord(item));
118
+ if (value && typeof value === 'object') {
119
+ return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, redactRecord(v, k)]));
120
+ }
121
+ return value;
122
+ }
123
+ /** Launch configs arrive with secrets already stripped; env can still carry tokens. */
124
+ function redactLaunchConfig(config) {
125
+ if (!config)
126
+ return null;
127
+ const copy = { ...config };
128
+ delete copy.secrets;
129
+ delete copy.registry_auth;
130
+ return redactRecord(copy);
131
+ }
132
+ function iso(value) {
133
+ return value instanceof Date && Number.isFinite(value.getTime()) ? value.toISOString() : null;
134
+ }
135
+ /** The JSON record bag for an Agent. Never carries jwt or full tokens. */
136
+ function recordJsonRecord(agent, dashboard) {
137
+ const bag = {
138
+ id: agent.id,
139
+ name: agent.name,
140
+ display_name: agent.displayName,
141
+ handle: agent.handle,
142
+ runtime: agent.runtime,
143
+ state: agent.state,
144
+ hostname: agent.hostname,
145
+ dashboard: dashboard ?? null,
146
+ public_url: agent.publicUrl ?? null,
147
+ desktop_url: agent.desktopUrl ?? null,
148
+ shell_url: agent.shellUrl ?? null,
149
+ size: agent.requestedSize,
150
+ cpu: agent.cpu,
151
+ memory: agent.memory,
152
+ tags: agent.tags,
153
+ launch_epoch: agent.launchEpoch,
154
+ agent_slot_id: agent.agentSlotId,
155
+ cluster_id: agent.clusterId,
156
+ created_at: iso(agent.createdAt),
157
+ updated_at: iso(agent.updatedAt),
158
+ started_at: iso(agent.startedAt),
159
+ stopped_at: iso(agent.stoppedAt),
160
+ archived_at: iso(agent.archivedAt),
161
+ routes: redactRecord(agent.routes),
162
+ launch_config: redactLaunchConfig(agent.launchConfig),
163
+ meta: agent.meta ? redactRecord(agent.meta) : null,
164
+ };
165
+ return bag;
166
+ }
167
+ function dashboardBase() {
168
+ return getAgentsApiBaseUrl() === 'https://api.dev.hypercli.com/agents'
169
+ ? 'https://console.dev.hypercli.com' : 'https://console.hypercli.com';
170
+ }
171
+ // ---------------------------------------------------------------------------
172
+ // error mapping — bare catches convert to CliError with API status detail
173
+ // ---------------------------------------------------------------------------
174
+ /**
175
+ * The control plane's structured AGENT_SLOT_EXHAUSTED contract names its own
176
+ * clean message beside HTTP `detail`. Prefer it over whatever intermediate
177
+ * layers wrapped into `detail`, and never re-print the admin-style inventory
178
+ * dump a legacy proxy may carry.
179
+ */
180
+ function slotExhaustionMessage(err) {
181
+ const text = err.responseText;
182
+ if (typeof text !== 'string' || !text)
183
+ return null;
184
+ let payload;
185
+ try {
186
+ payload = JSON.parse(text);
187
+ }
188
+ catch {
189
+ return null;
190
+ }
191
+ const wire = payload?.error;
192
+ if (!wire || typeof wire !== 'object')
193
+ return null;
194
+ const record = wire;
195
+ if (record.code !== 'AGENT_SLOT_EXHAUSTED')
196
+ return null;
197
+ return typeof record.message === 'string' && record.message ? record.message : null;
198
+ }
199
+ function describeFailure(err) {
200
+ if (err instanceof APIError) {
201
+ const slotMessage = slotExhaustionMessage(err);
202
+ if (slotMessage)
203
+ return `HTTP ${err.statusCode}: ${slotMessage}`;
204
+ return `HTTP ${err.statusCode}: ${err.detail}`;
205
+ }
206
+ if (err instanceof Error)
207
+ return err.message;
208
+ return String(err);
209
+ }
210
+ /** Wrap one SDK call: UsageError/CliError pass through, everything else -> CliError. */
211
+ async function api(what, call) {
212
+ try {
213
+ return await call();
214
+ }
215
+ catch (err) {
216
+ if (err instanceof CliError)
217
+ throw err;
218
+ throw new CliError(`${what} failed: ${describeFailure(err)}`);
219
+ }
220
+ }
221
+ // ---------------------------------------------------------------------------
222
+ // adopt + agent reference resolution (the golden id-prefix pattern)
223
+ // ---------------------------------------------------------------------------
224
+ /** Resolve the deployments API through the lazy client. */
225
+ async function adopt(ctx) {
226
+ const client = await ctx.client();
227
+ return { client, d: client.deployments };
228
+ }
229
+ const FULL_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
230
+ /**
231
+ * Resolve a user-typed <id> reference to a full agent id.
232
+ *
233
+ * hyper agents status a1b2 — prefix of exactly one id/name/handle/hostname
234
+ * ambiguous — UsageError (exit 2) listing candidates
235
+ * no match — CliError (exit 1)
236
+ *
237
+ * Exported for `hyper routines` (routines.ts) so --agent resolves identically.
238
+ */
239
+ export async function resolveAgentRef(d, ref) {
240
+ const raw = String(ref ?? '').trim();
241
+ if (!raw)
242
+ throw new UsageError('missing agent id. See hyper agents --help.');
243
+ if (FULL_UUID.test(raw))
244
+ return raw;
245
+ const agents = await api('list agents', () => d.list());
246
+ const fields = (a) => [a.id, a.name, a.handle, a.hostname]
247
+ .filter((v) => typeof v === 'string' && v.length > 0);
248
+ const exact = agents.filter((a) => fields(a).some((v) => v === raw));
249
+ if (exact.length === 1)
250
+ return exact[0].id;
251
+ const matches = exact.length > 1
252
+ ? exact
253
+ : agents.filter((a) => fields(a).some((v) => v.startsWith(raw)));
254
+ if (matches.length === 1)
255
+ return matches[0].id;
256
+ if (matches.length > 1) {
257
+ const lines = matches
258
+ .slice(0, 10)
259
+ .map((a) => ` ${a.id} ${a.displayName ?? a.name ?? ''} ${a.state}`)
260
+ .join('\n');
261
+ throw new UsageError(`ambiguous agent reference '${raw}':\n${lines}`);
262
+ }
263
+ throw new CliError(`no agent matches '${raw}'`);
264
+ }
265
+ // ---------------------------------------------------------------------------
266
+ // shared argv helpers
267
+ // ---------------------------------------------------------------------------
268
+ function onePositional(parsed, what) {
269
+ if (parsed.positionals.length === 0)
270
+ throw new UsageError(`missing ${what}`);
271
+ if (parsed.positionals.length > 1) {
272
+ throw new UsageError(`unexpected extra arguments: ${parsed.positionals.slice(1).join(' ')}`);
273
+ }
274
+ return parsed.positionals[0];
275
+ }
276
+ function str(parsed, key) {
277
+ const value = parsed.values[key];
278
+ return typeof value === 'string' ? value : undefined;
279
+ }
280
+ function strList(parsed, key) {
281
+ const value = parsed.values[key];
282
+ if (typeof value === 'string')
283
+ return [value];
284
+ if (Array.isArray(value))
285
+ return value.filter((v) => typeof v === 'string');
286
+ return [];
287
+ }
288
+ /** Parse a seconds flag to milliseconds; bad input -> UsageError. */
289
+ function secondsFlag(parsed, key, fallback) {
290
+ const raw = str(parsed, key);
291
+ if (raw === undefined)
292
+ return Math.round(fallback * 1000);
293
+ const seconds = Number(raw);
294
+ if (!Number.isFinite(seconds) || seconds <= 0) {
295
+ throw new UsageError(`--${key} must be a positive number of seconds (got '${raw}')`);
296
+ }
297
+ return Math.round(seconds * 1000);
298
+ }
299
+ /** Repeated --param k=v pairs into a flat map of strings. */
300
+ function parseParams(items) {
301
+ const out = {};
302
+ for (const item of items) {
303
+ const eq = item.indexOf('=');
304
+ if (eq <= 0)
305
+ throw new UsageError(`--param '${item}' must be KEY=VALUE with a nonempty key`);
306
+ out[item.slice(0, eq)] = item.slice(eq + 1);
307
+ }
308
+ return out;
309
+ }
310
+ // ---------------------------------------------------------------------------
311
+ // confirmation gate — only when interactive stdout, no --yes, not --json
312
+ // ---------------------------------------------------------------------------
313
+ function needsConfirmation(ctx, yes) {
314
+ return !yes && ctx.format !== 'json' && process.stdout.isTTY === true;
315
+ }
316
+ async function confirm(question) {
317
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
318
+ try {
319
+ return /^\s*y(es)?\s*$/i.test(await rl.question(question));
320
+ }
321
+ finally {
322
+ rl.close();
323
+ }
324
+ }
325
+ // ---------------------------------------------------------------------------
326
+ // ls
327
+ // ---------------------------------------------------------------------------
328
+ async function cmdLs(ctx, args) {
329
+ const parsed = parseCommandArgs(args, { state: { type: 'string' } });
330
+ if (parsed.help)
331
+ return printHelp();
332
+ const { d } = await adopt(ctx);
333
+ const state = str(parsed, 'state');
334
+ const agents = await api('list agents', () => d.list(state ? { state } : {}));
335
+ const counts = new Map();
336
+ for (const agent of agents) {
337
+ const key = String(agent.state ?? 'unknown').toUpperCase();
338
+ counts.set(key, (counts.get(key) ?? 0) + 1);
339
+ }
340
+ const breakdown = [...counts.entries()]
341
+ .sort(([a], [b]) => a.localeCompare(b))
342
+ .map(([stateName, n]) => `${stateName} ${n}`)
343
+ .join(', ');
344
+ ctx.output.info(`total ${agents.length}${breakdown ? ` (${breakdown})` : ''}`);
345
+ ctx.output.result(agents.map((agent) => recordJsonRecord(agent, `${dashboardBase()}/agents/${agent.id}`)), agents.length === 0
346
+ ? 'No agents found.'
347
+ : {
348
+ columns: ['ID', 'NAME', 'RUNTIME', 'STATE'],
349
+ rows: agents.map((agent) => [
350
+ shortId(agent.id),
351
+ agent.displayName ?? agent.name ?? '',
352
+ agent.runtime ?? '',
353
+ agent.state,
354
+ ]),
355
+ });
356
+ }
357
+ // ---------------------------------------------------------------------------
358
+ // status
359
+ // ---------------------------------------------------------------------------
360
+ const PLAN_META_KEY = /plan|entitlement/i;
361
+ async function cmdStatus(ctx, args) {
362
+ const parsed = parseCommandArgs(args, { verbose: { type: 'boolean', default: false } });
363
+ if (parsed.help)
364
+ return printHelp();
365
+ const ref = onePositional(parsed, 'agent id');
366
+ const { d } = await adopt(ctx);
367
+ const agent = await api('get agent', async () => d.get(await resolveAgentRef(d, ref)));
368
+ const dashboard = `${dashboardBase()}/agents/${agent.id}`;
369
+ const record = recordJsonRecord(agent, dashboard);
370
+ const rows = [
371
+ ['id', agent.id],
372
+ ['name', agent.displayName ?? agent.name ?? ''],
373
+ ['handle', agent.handle ?? ''],
374
+ ['runtime', agent.runtime ?? ''],
375
+ ['state', agent.state],
376
+ ['dashboard', dashboard],
377
+ ['hostname', agent.hostname ?? ''],
378
+ ['size', agent.requestedSize ?? (agent.cpu ? `${agent.cpu}c/${agent.memory}G` : '')],
379
+ ['launch_epoch', String(agent.launchEpoch)],
380
+ ['agent_slot_id', agent.agentSlotId ?? ''],
381
+ ['created_at', iso(agent.createdAt) ?? ''],
382
+ ['started_at', iso(agent.startedAt) ?? ''],
383
+ ['stopped_at', iso(agent.stoppedAt) ?? ''],
384
+ ];
385
+ // plan/entitlement fields, as returned on the record meta bag
386
+ if (agent.meta) {
387
+ for (const [key, value] of Object.entries(agent.meta)) {
388
+ if (!PLAN_META_KEY.test(key))
389
+ continue;
390
+ rows.push([key, typeof value === 'string' ? value : JSON.stringify(value)]);
391
+ }
392
+ const status = agent.meta.status;
393
+ if (status && status.status === 'error') {
394
+ rows.push(['error', [status.reason, status.message].filter(Boolean).join(': ') || 'error']);
395
+ }
396
+ }
397
+ let table = recordLabelValue(rows.filter(([, value]) => value !== ''));
398
+ if (parsed.values.verbose === true) {
399
+ table += `\n\n${JSON.stringify(record, null, 2)}`;
400
+ }
401
+ ctx.output.result(record, table);
402
+ }
403
+ // ---------------------------------------------------------------------------
404
+ // wait
405
+ // ---------------------------------------------------------------------------
406
+ async function cmdWait(ctx, args) {
407
+ const parsed = parseCommandArgs(args, {
408
+ state: { type: 'string' },
409
+ timeout: { type: 'string' },
410
+ interval: { type: 'string' },
411
+ });
412
+ if (parsed.help)
413
+ return printHelp();
414
+ const ref = onePositional(parsed, 'agent id');
415
+ const target = (str(parsed, 'state') ?? 'RUNNING').toUpperCase();
416
+ const timeoutMs = secondsFlag(parsed, 'timeout', 300);
417
+ const intervalMs = secondsFlag(parsed, 'interval', 5);
418
+ const { d } = await adopt(ctx);
419
+ const id = await resolveAgentRef(d, ref);
420
+ ctx.output.info(`waiting for agent ${shortId(id)} to reach ${target} (timeout ${timeoutMs / 1000}s)`);
421
+ let agent;
422
+ try {
423
+ agent = target === 'RUNNING'
424
+ ? await d.waitRunning(id, timeoutMs, intervalMs)
425
+ : await d.waitForState(id, [target], timeoutMs, ['FAILED', 'DELETED'].filter((s) => s !== target), undefined, intervalMs);
426
+ }
427
+ catch (err) {
428
+ if (err instanceof CliError)
429
+ throw err;
430
+ throw new CliError(`wait failed: ${describeFailure(err)}`);
431
+ }
432
+ ctx.output.result(recordJsonRecord(agent, `${dashboardBase()}/agents/${agent.id}`), `agent ${shortId(agent.id)} reached ${agent.state}`);
433
+ }
434
+ // ---------------------------------------------------------------------------
435
+ // create
436
+ // ---------------------------------------------------------------------------
437
+ /**
438
+ * Launch-contract flags shared by every runtime (--image, --sh, trailing
439
+ * `--` command argv, --secret, --sync-root/--sync-include/--sync-exclude,
440
+ * --registry-*, --restart). They map straight onto the SDK create options
441
+ * bag (BuildAgentConfigOptions), which every create entry honors; the
442
+ * Backend rejects combinations a runtime does not support.
443
+ */
444
+ function launchOptionsFromCreateFlags(parsed, commandArgv) {
445
+ const image = str(parsed, 'image');
446
+ const shScript = str(parsed, 'sh');
447
+ if (commandArgv.length > 0 && shScript !== undefined) {
448
+ throw new UsageError('pass only one of --sh SCRIPT or the command argv after --');
449
+ }
450
+ const syncRoot = str(parsed, 'sync-root');
451
+ const syncInclude = strList(parsed, 'sync-include');
452
+ const syncExclude = strList(parsed, 'sync-exclude');
453
+ if (syncInclude.length > 0 && syncExclude.length > 0) {
454
+ throw new UsageError('pass only one of --sync-include or --sync-exclude');
455
+ }
456
+ const registryUrl = str(parsed, 'registry-url');
457
+ const registryUsername = str(parsed, 'registry-username');
458
+ const registryPassword = str(parsed, 'registry-password');
459
+ if ((registryUsername === undefined) !== (registryPassword === undefined)) {
460
+ throw new UsageError('--registry-username and --registry-password must be given together');
461
+ }
462
+ const restartFlag = parsed.values.restart === true;
463
+ const noRestart = parsed.values['no-restart'] === true;
464
+ if (restartFlag && noRestart) {
465
+ throw new UsageError('pass only one of --restart or --no-restart');
466
+ }
467
+ const secrets = parseParams(strList(parsed, 'secret'));
468
+ const launch = {};
469
+ if (image)
470
+ launch.image = image;
471
+ if (commandArgv.length > 0)
472
+ launch.command = commandArgv;
473
+ if (shScript !== undefined)
474
+ launch.command = ['sh', '-c', shScript];
475
+ if (Object.keys(secrets).length > 0)
476
+ launch.secrets = secrets;
477
+ if (syncRoot)
478
+ launch.syncRoot = syncRoot;
479
+ if (syncInclude.length > 0)
480
+ launch.syncInclude = syncInclude;
481
+ if (syncExclude.length > 0)
482
+ launch.syncExclude = syncExclude;
483
+ if (registryUrl)
484
+ launch.registryUrl = registryUrl;
485
+ if (registryUsername !== undefined && registryPassword !== undefined) {
486
+ launch.registryAuth = { username: registryUsername, password: registryPassword };
487
+ }
488
+ if (noRestart)
489
+ launch.restart = false;
490
+ if (restartFlag)
491
+ launch.restart = true;
492
+ return launch;
493
+ }
494
+ /** Dry-run prints the resolved payload; secrets and registry passwords never print. */
495
+ function maskCreatePayloadSecrets(payload) {
496
+ const masked = { ...payload };
497
+ if (masked.secrets && typeof masked.secrets === 'object') {
498
+ masked.secrets = Object.fromEntries(Object.keys(masked.secrets).map((key) => [key, '****']));
499
+ }
500
+ if (masked.registryAuth && typeof masked.registryAuth === 'object') {
501
+ const auth = masked.registryAuth;
502
+ masked.registryAuth = { ...auth, password: '****' };
503
+ }
504
+ return masked;
505
+ }
506
+ /** Runner create accepts --cwd PATH as an absolute path on the Runner host. */
507
+ async function cmdCreate(ctx, args) {
508
+ const parsed = parseCommandArgs(args, {
509
+ runtime: { type: 'string' },
510
+ model: { type: 'string' },
511
+ plan: { type: 'string' },
512
+ size: { type: 'string' },
513
+ env: { type: 'string', multiple: true },
514
+ param: { type: 'string', multiple: true },
515
+ secret: { type: 'string', multiple: true },
516
+ image: { type: 'string' },
517
+ sh: { type: 'string' },
518
+ 'sync-root': { type: 'string' },
519
+ 'sync-include': { type: 'string', multiple: true },
520
+ 'sync-exclude': { type: 'string', multiple: true },
521
+ 'registry-url': { type: 'string' },
522
+ 'registry-username': { type: 'string' },
523
+ 'registry-password': { type: 'string' },
524
+ restart: { type: 'boolean', default: false },
525
+ 'no-restart': { type: 'boolean', default: false },
526
+ 'runner-tags': { type: 'string' },
527
+ 'runner-id': { type: 'string' },
528
+ executor: { type: 'string' },
529
+ cwd: { type: 'string' },
530
+ 'dry-run': { type: 'boolean', default: false },
531
+ });
532
+ if (parsed.help)
533
+ return printHelp();
534
+ // `hyper agents create <name> [flags] [-- CMD [ARGS...]]` — the argv after
535
+ // the agent name (typically after --) is the pod command, mirroring
536
+ // `agents exec <id> -- CMD`.
537
+ const [agentName, ...commandArgv] = parsed.positionals;
538
+ if (!agentName)
539
+ throw new UsageError('missing agent name');
540
+ const runtime = str(parsed, 'runtime')?.toLowerCase();
541
+ if (!runtime) {
542
+ throw new UsageError(`--runtime is required (one of: ${[...RUNTIME_LABELS.keys()].join(', ')})`);
543
+ }
544
+ const runtimeLabel = RUNTIME_LABELS.get(runtime);
545
+ if (!runtimeLabel) {
546
+ throw new UsageError(`unknown runtime '${runtime}' (expected one of: ${[...RUNTIME_LABELS.keys()].join(', ')})`);
547
+ }
548
+ const model = str(parsed, 'model');
549
+ const plan = str(parsed, 'plan');
550
+ const size = str(parsed, 'size');
551
+ const env = parseParams(strList(parsed, 'env'));
552
+ const params = parseParams(strList(parsed, 'param'));
553
+ const dryRun = parsed.values['dry-run'] === true;
554
+ // The openclaw family strips the generic config bag in createAgent, so a
555
+ // model there would be silently dropped; refuse instead of pretending.
556
+ if (model && OPENCLAW_SET.has(runtimeLabel)) {
557
+ throw new UsageError(`--model is not supported for runtime '${runtime}' at create time; `
558
+ + 'configure the model after launch with hyper agents config set <id> --param ...');
559
+ }
560
+ // The SDK create contract has no plan field; --plan rides as a visible tag.
561
+ const tags = plan ? [`plan:${plan}`] : undefined;
562
+ // Self-hosted runner placement (monorepo acp/hyper-runner/RUNNER.md): --runner-tags is a
563
+ // comma list; --runner-id pins one runner when tags are ambiguous.
564
+ const runnerTagsRaw = str(parsed, 'runner-tags');
565
+ const runnerId = str(parsed, 'runner-id') || undefined;
566
+ const runnerTags = runnerTagsRaw
567
+ ? runnerTagsRaw.split(',').map((tag) => tag.trim()).filter(Boolean)
568
+ : [];
569
+ if (runnerTagsRaw && runnerTags.length === 0) {
570
+ throw new UsageError('--runner-tags must name at least one tag when given');
571
+ }
572
+ const runner = (runnerTags.length > 0 || runnerId)
573
+ ? { ...(runnerTags.length > 0 ? { tags: runnerTags } : {}), ...(runnerId ? { runnerId } : {}) }
574
+ : undefined;
575
+ // Runner executor choice (rulings: required at create for runner placements,
576
+ // forbidden for hosted agents; the Backend owns both rejections).
577
+ const executor = str(parsed, 'executor')?.toLowerCase();
578
+ const cwd = str(parsed, 'cwd');
579
+ if (cwd !== undefined && !runner) {
580
+ throw new UsageError('--cwd requires runner placement');
581
+ }
582
+ if (executor && executor !== 'process' && executor !== 'docker') {
583
+ throw new UsageError("--executor must be 'process' or 'docker'");
584
+ }
585
+ // The openclaw family strips the generic config bag in createAgent, so
586
+ // --param rides env there; the other runtimes carry a free-form config bag.
587
+ const mergedEnv = { ...env, ...(OPENCLAW_SET.has(runtimeLabel) ? params : {}) };
588
+ const configBag = OPENCLAW_SET.has(runtimeLabel) ? {} : { ...(model ? { model } : {}), ...params };
589
+ // Coding runtimes own their pod boot command; a caller command there would
590
+ // replace the ACP entrypoint silently. openclaw/generic/hermes honor it.
591
+ if (commandArgv.length > 0 && ACP_SET.has(runtimeLabel)) {
592
+ throw new UsageError(`a command argv is not supported for coding runtime '${runtime}'; the runtime owns its boot command`);
593
+ }
594
+ const launch = launchOptionsFromCreateFlags(parsed, commandArgv);
595
+ const payload = {
596
+ name: agentName,
597
+ ...(size ? { size } : {}),
598
+ ...(tags ? { tags } : {}),
599
+ ...(runner ? { runner } : {}),
600
+ ...(executor ? { executor } : {}),
601
+ ...(cwd !== undefined ? { cwd } : {}),
602
+ ...launch,
603
+ ...(Object.keys(mergedEnv).length > 0 ? { env: mergedEnv } : {}),
604
+ ...(Object.keys(configBag).length > 0 ? { config: configBag } : {}),
605
+ dryRun,
606
+ };
607
+ if (dryRun) {
608
+ // Print the resolved payload, call nothing (not even ctx.client()).
609
+ const printable = { runtime, method: 'createAgent', options: maskCreatePayloadSecrets(payload) };
610
+ ctx.output.result(printable, JSON.stringify(printable, null, 2));
611
+ return;
612
+ }
613
+ const { d } = await adopt(ctx);
614
+ const created = await api('create agent', async () => d.createAgent(runtimeLabel, payload));
615
+ ctx.output.result(recordJsonRecord(created, `${dashboardBase()}/agents/${created.id}`), recordLabelValue([
616
+ ['created', shortId(created.id)],
617
+ ['name', created.displayName ?? created.name ?? ''],
618
+ ['runtime', created.runtime ?? runtime],
619
+ ['state', created.state],
620
+ ]));
621
+ ctx.output.info(runner
622
+ ? 'runner launch requested'
623
+ : `start it with: hyper agents start ${shortId(created.id)}`);
624
+ }
625
+ // ---------------------------------------------------------------------------
626
+ // start — runtime dispatch (desktop/src/api.ts startAgent parity)
627
+ // ---------------------------------------------------------------------------
628
+ /**
629
+ * The start shared by `agents start` and `agents chat`: every runtime starts
630
+ * the Backend-stored launch config without rewriting it.
631
+ */
632
+ async function startAgentForRuntime(d, agent) {
633
+ return api('start agent', () => d.start(agent.id));
634
+ }
635
+ async function cmdStart(ctx, args) {
636
+ const parsed = parseCommandArgs(args);
637
+ if (parsed.help)
638
+ return printHelp();
639
+ const ref = onePositional(parsed, 'agent id');
640
+ const { d } = await adopt(ctx);
641
+ const agent = await api('get agent', async () => d.get(await resolveAgentRef(d, ref)));
642
+ const started = await startAgentForRuntime(d, agent);
643
+ ctx.output.result(recordJsonRecord(started, `${dashboardBase()}/agents/${started.id}`), `starting ${shortId(started.id)} (${started.state})`);
644
+ }
645
+ // ---------------------------------------------------------------------------
646
+ // set — mutate one field on an existing agent
647
+ // ---------------------------------------------------------------------------
648
+ /**
649
+ * Mirror of the backend's lifecycle_admission.py ACTIVE_OPERATION_STATES: a
650
+ * runtime change is only safe while the agent is fully at rest.
651
+ */
652
+ const ACTIVE_OPERATION_STATES = new Set([
653
+ 'CREATING', 'STARTING', 'RUNNING', 'STOPPING', 'RESTORING', 'UPGRADING',
654
+ ]);
655
+ /** The actionable stop/set/start recovery sequence for a running-agent refusal. */
656
+ function runtimeChangeRecovery(id, runtime) {
657
+ return 'run:\n'
658
+ + ` hyper agents stop ${shortId(id)}\n`
659
+ + ` hyper agents set runtime ${shortId(id)} ${runtime}\n`
660
+ + ` hyper agents start ${shortId(id)}`;
661
+ }
662
+ async function cmdSet(ctx, args) {
663
+ const [field, ...rest] = args;
664
+ if (!field || field === '--help' || field === '-h')
665
+ return printHelp();
666
+ if (field !== 'runtime') {
667
+ throw new UsageError(`unknown agents set field '${field}' (expected: runtime)`);
668
+ }
669
+ const parsed = parseCommandArgs(rest, {});
670
+ if (parsed.help)
671
+ return printHelp();
672
+ if (parsed.positionals.length < 2)
673
+ throw new UsageError('missing agent id or runtime');
674
+ if (parsed.positionals.length > 2) {
675
+ throw new UsageError(`unexpected extra arguments: ${parsed.positionals.slice(2).join(' ')}`);
676
+ }
677
+ const [ref, runtimeInput] = parsed.positionals;
678
+ const runtime = canonicalRuntimeName(runtimeInput);
679
+ if (!MANAGED_RUNTIMES.has(runtime)) {
680
+ throw new UsageError(`unknown runtime '${runtime}' (expected one of: ${[...MANAGED_RUNTIMES].join(', ')})`);
681
+ }
682
+ const { d } = await adopt(ctx);
683
+ const id = await resolveAgentRef(d, ref);
684
+ // The backend 409s a runtime PATCH against an active agent; refuse early with
685
+ // the same recovery sequence instead of paying for the failing round-trip.
686
+ const agent = await api('get agent', () => d.get(id));
687
+ if (ACTIVE_OPERATION_STATES.has(agent.state.toUpperCase())) {
688
+ throw new CliError(`cannot change the runtime of a running agent (state=${agent.state})\n${runtimeChangeRecovery(id, runtime)}`);
689
+ }
690
+ let updated;
691
+ try {
692
+ // The backend resets the launch image to the new runtime's platform
693
+ // default on a runtime change; resetImage is ignored server-side.
694
+ updated = await d.update(id, { runtime: runtime });
695
+ }
696
+ catch (err) {
697
+ // Backstop for races: the agent may have started between the get and the PATCH.
698
+ if (err instanceof APIError
699
+ && err.statusCode === 409
700
+ && /runtime/i.test(err.detail)
701
+ && /stop/i.test(err.detail)) {
702
+ throw new CliError(`update agent failed: HTTP 409: ${err.detail}\n${runtimeChangeRecovery(id, runtime)}`);
703
+ }
704
+ throw new CliError(`update agent failed: ${describeFailure(err)}`);
705
+ }
706
+ ctx.output.result(recordJsonRecord(updated, `${dashboardBase()}/agents/${updated.id}`), `updated ${shortId(id)} runtime=${updated.runtime}`
707
+ + (runtime === 'generic' ? '' : ` (launch image reset to the ${runtime} default)`));
708
+ }
709
+ // ---------------------------------------------------------------------------
710
+ // stop / delete / archive / restore
711
+ // ---------------------------------------------------------------------------
712
+ async function cmdStop(ctx, args) {
713
+ const parsed = parseCommandArgs(args, { yes: { type: 'boolean', short: 'y', default: false } });
714
+ if (parsed.help)
715
+ return printHelp();
716
+ const ref = onePositional(parsed, 'agent id');
717
+ const { d } = await adopt(ctx);
718
+ const id = await resolveAgentRef(d, ref);
719
+ if (needsConfirmation(ctx, parsed.values.yes === true)) {
720
+ if (!(await confirm(`Stop agent ${shortId(id)}? [y/N] `))) {
721
+ ctx.output.info('aborted');
722
+ return;
723
+ }
724
+ }
725
+ const stopped = await api('stop agent', () => d.stop(id));
726
+ ctx.output.result(recordJsonRecord(stopped, `${dashboardBase()}/agents/${stopped.id}`), stopped.state.toUpperCase() === 'STOPPED'
727
+ ? `stopped ${shortId(id)}`
728
+ : `stopping ${shortId(id)} (${stopped.state}); cleanup may still be in progress`);
729
+ }
730
+ async function cmdDelete(ctx, args) {
731
+ const parsed = parseCommandArgs(args, { yes: { type: 'boolean', short: 'y', default: false } });
732
+ if (parsed.help)
733
+ return printHelp();
734
+ const ref = onePositional(parsed, 'agent id');
735
+ const { d } = await adopt(ctx);
736
+ const id = await resolveAgentRef(d, ref);
737
+ if (needsConfirmation(ctx, parsed.values.yes === true)) {
738
+ if (!(await confirm(`Permanently delete agent ${shortId(id)}? [y/N] `))) {
739
+ ctx.output.info('aborted');
740
+ return;
741
+ }
742
+ }
743
+ await api('delete agent', () => d.delete(id));
744
+ ctx.output.result({ deleted: id }, `deleted ${shortId(id)}`);
745
+ }
746
+ async function cmdArchiveRestore(ctx, args, verb) {
747
+ const parsed = parseCommandArgs(args);
748
+ if (parsed.help)
749
+ return printHelp();
750
+ const ref = onePositional(parsed, 'agent id');
751
+ const { d } = await adopt(ctx);
752
+ const id = await resolveAgentRef(d, ref);
753
+ const agent = await api(`${verb} agent`, () => (verb === 'archive' ? d.archive(id) : d.restore(id)));
754
+ ctx.output.result(recordJsonRecord(agent, `${dashboardBase()}/agents/${agent.id}`), `${verb === 'archive' ? 'archiving' : 'restoring'} ${shortId(id)} (${agent.state})`);
755
+ }
756
+ // ---------------------------------------------------------------------------
757
+ // exec
758
+ // ---------------------------------------------------------------------------
759
+ /**
760
+ * Piped stdin collector for cmdExec. A TTY keeps exec strictly one-shot:
761
+ * stdin stays untouched and the pod command sees no stdin attachment at all.
762
+ * A pipe is drained to EOF and forwarded; an unbounded pipe is a caller error,
763
+ * exactly like `kubectl exec -i` on a never-closing stream.
764
+ */
765
+ async function readPipedExecStdin() {
766
+ const stdin = process.stdin;
767
+ if (stdin.isTTY === true)
768
+ return undefined;
769
+ const chunks = [];
770
+ let total = 0;
771
+ for await (const chunk of stdin) {
772
+ const data = typeof chunk === 'string' ? Buffer.from(chunk, 'utf8') : chunk;
773
+ total += data.byteLength;
774
+ if (total > AGENT_EXEC_STDIN_MAX_BYTES) {
775
+ throw new CliError(`agents exec stdin exceeds the ${AGENT_EXEC_STDIN_MAX_BYTES}-byte limit`);
776
+ }
777
+ chunks.push(data);
778
+ }
779
+ return Buffer.concat(chunks);
780
+ }
781
+ /** Test seam: tests replace `collect` instead of piping process.stdin. */
782
+ export const execStdin = { collect: readPipedExecStdin };
783
+ async function cmdExec(ctx, args) {
784
+ const parsed = parseCommandArgs(args, { timeout: { type: 'string' } });
785
+ if (parsed.help) {
786
+ printHelp();
787
+ return 0;
788
+ }
789
+ const [ref, ...command] = parsed.positionals;
790
+ if (!ref)
791
+ throw new UsageError('missing agent id');
792
+ const timeoutRaw = str(parsed, 'timeout');
793
+ let timeout = 30;
794
+ if (timeoutRaw !== undefined) {
795
+ timeout = Number(timeoutRaw);
796
+ if (!Number.isInteger(timeout) || timeout < 1 || timeout > 300) {
797
+ throw new UsageError('--timeout must be an integer from 1 through 300');
798
+ }
799
+ }
800
+ if (command.length === 0) {
801
+ throw new UsageError(`usage: hyper agents exec <id> [--] CMD [ARGS...]`);
802
+ }
803
+ const { d } = await adopt(ctx);
804
+ const id = await resolveAgentRef(d, ref);
805
+ const stdin = await execStdin.collect();
806
+ const result = await api('exec', () => d.exec(id, command, { timeout, ...(stdin !== undefined ? { stdin } : {}) }));
807
+ if (ctx.format === 'json') {
808
+ ctx.output.result(result);
809
+ }
810
+ else {
811
+ if (result.stdout)
812
+ ctx.output.result({}, result.stdout.replace(/\n$/, ''));
813
+ if (result.stderr)
814
+ ctx.output.info(result.stderr.replace(/\n$/, ''));
815
+ }
816
+ return result.exitCode;
817
+ }
818
+ // ---------------------------------------------------------------------------
819
+ // shell — raw-mode passthrough over the SDK shell WebSocket (no node-pty)
820
+ // ---------------------------------------------------------------------------
821
+ async function cmdShell(ctx, args) {
822
+ const parsed = parseCommandArgs(args, { shell: { type: 'string' } });
823
+ if (parsed.help) {
824
+ printHelp();
825
+ return 0;
826
+ }
827
+ const ref = onePositional(parsed, 'agent id');
828
+ // No half-broken TTY: without a real terminal we refuse, pointing at exec.
829
+ if (process.stdin.isTTY !== true || process.stdout.isTTY !== true) {
830
+ throw new UsageError('agents shell needs an interactive TTY; use hyper agents exec <id> -- CMD for non-interactive runs');
831
+ }
832
+ const { d } = await adopt(ctx);
833
+ const id = await resolveAgentRef(d, ref);
834
+ ctx.output.info(`connecting to shell on ${shortId(id)} (Ctrl+] to disconnect)`);
835
+ const ws = await api('open shell', () => d.shellConnect(id, str(parsed, 'shell')));
836
+ return await new Promise((resolve, reject) => {
837
+ const stdin = process.stdin;
838
+ const stdout = process.stdout;
839
+ let settled = false;
840
+ const sendResize = () => {
841
+ try {
842
+ ws.send(`\x1b[8;${stdout.rows ?? 24};${stdout.columns ?? 80}t`);
843
+ }
844
+ catch {
845
+ // A socket already closing needs no resize.
846
+ }
847
+ };
848
+ const finish = (error, code = 0) => {
849
+ if (settled)
850
+ return;
851
+ settled = true;
852
+ stdin.setRawMode(false);
853
+ stdin.pause();
854
+ stdin.removeListener('data', onData);
855
+ process.removeListener('SIGWINCH', sendResize);
856
+ try {
857
+ ws.close();
858
+ }
859
+ catch {
860
+ // Peer already closed the socket.
861
+ }
862
+ if (error)
863
+ reject(error);
864
+ else
865
+ resolve(code);
866
+ };
867
+ const onData = (chunk) => {
868
+ if (chunk.includes(0x1d)) {
869
+ finish();
870
+ return;
871
+ }
872
+ try {
873
+ ws.send(chunk.toString('utf8'));
874
+ }
875
+ catch {
876
+ finish();
877
+ }
878
+ };
879
+ ws.onmessage = (event) => {
880
+ const data = event.data;
881
+ if (typeof data === 'string')
882
+ stdout.write(data);
883
+ else if (data instanceof ArrayBuffer)
884
+ stdout.write(Buffer.from(data));
885
+ else if (ArrayBuffer.isView(data))
886
+ stdout.write(Buffer.from(data.buffer, data.byteOffset, data.byteLength));
887
+ };
888
+ ws.onclose = () => {
889
+ ctx.output.info('disconnected');
890
+ finish();
891
+ };
892
+ ws.onerror = () => finish(new CliError('shell connection failed'));
893
+ stdin.setRawMode(true);
894
+ stdin.resume();
895
+ stdin.on('data', onData);
896
+ process.on('SIGWINCH', sendResize);
897
+ sendResize();
898
+ });
899
+ }
900
+ // ---------------------------------------------------------------------------
901
+ // logs
902
+ // ---------------------------------------------------------------------------
903
+ async function cmdLogs(ctx, args) {
904
+ const parsed = parseCommandArgs(args, {
905
+ follow: { type: 'boolean', short: 'f', default: false },
906
+ lines: { type: 'string', short: 'n' },
907
+ container: { type: 'string' },
908
+ source: { type: 'string' },
909
+ generation: { type: 'string' },
910
+ 'max-bytes': { type: 'string' },
911
+ 'max-lines': { type: 'string' },
912
+ });
913
+ if (parsed.help) {
914
+ printHelp();
915
+ return 0;
916
+ }
917
+ const ref = onePositional(parsed, 'agent id');
918
+ const follow = parsed.values.follow === true;
919
+ const linesRaw = str(parsed, 'lines');
920
+ let tailLines = 100;
921
+ if (linesRaw !== undefined) {
922
+ tailLines = Number(linesRaw);
923
+ if (!Number.isInteger(tailLines) || tailLines < 0) {
924
+ throw new UsageError('--lines must be a non-negative integer');
925
+ }
926
+ }
927
+ const source = str(parsed, 'source');
928
+ const generation = str(parsed, 'generation');
929
+ if (source !== undefined && source !== 'both' && source !== 'stdout' && source !== 'stderr')
930
+ throw new UsageError('--source must be both, stdout or stderr');
931
+ if (generation !== undefined && generation !== 'current' && generation !== 'previous')
932
+ throw new UsageError('--generation must be current or previous');
933
+ const bound = (key, max) => {
934
+ const raw = str(parsed, key);
935
+ if (raw === undefined)
936
+ return undefined;
937
+ const value = Number(raw);
938
+ if (!Number.isInteger(value) || value < 1 || value > max)
939
+ throw new UsageError(`--${key} must be an integer from 1 through ${max}`);
940
+ return value;
941
+ };
942
+ const maxBytes = bound('max-bytes', 65536);
943
+ const maxLines = bound('max-lines', 1000);
944
+ const { d } = await adopt(ctx);
945
+ const id = await resolveAgentRef(d, ref);
946
+ const container = str(parsed, 'container');
947
+ const captureOptions = { source, generation, maxBytes, maxLines };
948
+ const unavailable = (snapshot) => snapshot.unavailable ?? snapshot.captures.find(c => c.unavailable)?.unavailable;
949
+ if (!follow) {
950
+ // One-shot: history only, promise resolves at history_end.
951
+ const lines = [];
952
+ let captured;
953
+ await api('read logs', () => d.subscribeLogs(id, (line) => { lines.push(line); }, {
954
+ follow: false,
955
+ tailLines,
956
+ ...captureOptions,
957
+ onCapturedLogs: (snapshot) => { captured = snapshot; },
958
+ ...(container ? { container } : {}),
959
+ }));
960
+ if (captured) {
961
+ if (ctx.format === 'json')
962
+ ctx.output.result(captured);
963
+ else
964
+ capturedLogRenderer(text => process.stdout.write(text), text => ctx.output.info(text))(captured);
965
+ const reason = unavailable(captured);
966
+ if (reason) {
967
+ ctx.output.info(`runner logs unavailable: ${reason}`);
968
+ return 1;
969
+ }
970
+ }
971
+ else
972
+ ctx.output.result(lines, lines.length ? lines.join('\n') : '(no logs)');
973
+ return 0;
974
+ }
975
+ // Follow: stream lines verbatim; reconnect OFF (SDK default). SIGINT aborts
976
+ // and exits 130.
977
+ const controller = new AbortController();
978
+ let interrupted = false;
979
+ const onSigint = () => {
980
+ interrupted = true;
981
+ controller.abort();
982
+ };
983
+ process.once('SIGINT', onSigint);
984
+ const renderCaptured = capturedLogRenderer(text => process.stdout.write(text), text => ctx.output.info(text));
985
+ let previousSnapshot;
986
+ try {
987
+ await api('stream logs', () => d.subscribeLogs(id, (line) => {
988
+ process.stdout.write(`${ctx.format === 'json' ? JSON.stringify(line) : line}\n`);
989
+ }, {
990
+ follow: true,
991
+ tailLines,
992
+ signal: controller.signal,
993
+ ...captureOptions,
994
+ onCapturedLogs: (snapshot) => {
995
+ const serialized = JSON.stringify(snapshot);
996
+ if (serialized === previousSnapshot)
997
+ return;
998
+ previousSnapshot = serialized;
999
+ if (ctx.format === 'json') {
1000
+ ctx.output.result(snapshot);
1001
+ const reason = unavailable(snapshot);
1002
+ if (reason)
1003
+ ctx.output.info(`runner logs unavailable: ${reason}`);
1004
+ }
1005
+ else
1006
+ renderCaptured(snapshot);
1007
+ },
1008
+ ...(container ? { container } : {}),
1009
+ }));
1010
+ }
1011
+ finally {
1012
+ process.removeListener('SIGINT', onSigint);
1013
+ }
1014
+ return interrupted ? 130 : 0;
1015
+ }
1016
+ // ---------------------------------------------------------------------------
1017
+ // cp
1018
+ // ---------------------------------------------------------------------------
1019
+ function parseCpTarget(value) {
1020
+ // A Windows drive path ("C:\dir\file") is never a remote target.
1021
+ if (/^[A-Za-z]:[\\/]/.test(value))
1022
+ return { path: value };
1023
+ const colon = value.indexOf(':');
1024
+ if (colon < 0)
1025
+ return { path: value };
1026
+ const head = value.slice(0, colon);
1027
+ // A path separator before the colon means it was a local relative path.
1028
+ if (head.includes('/') || head.includes('\\'))
1029
+ return { path: value };
1030
+ const remotePath = value.slice(colon + 1);
1031
+ if (!head || !remotePath) {
1032
+ throw new UsageError(`remote targets must use <id>:<path> (got '${value}')`);
1033
+ }
1034
+ return { agentRef: head, path: remotePath };
1035
+ }
1036
+ function formatBytes(n) {
1037
+ if (n < 1024)
1038
+ return `${n} B`;
1039
+ if (n < 1024 * 1024)
1040
+ return `${(n / 1024).toFixed(1)} KiB`;
1041
+ return `${(n / (1024 * 1024)).toFixed(1)} MiB`;
1042
+ }
1043
+ // Debug note: the Reef file API behind Deployments.cpTo/cpFrom only accepts
1044
+ // paths relative to the agent's sync root (ts-sdk resolveSyncRootFilePath
1045
+ // rejects a leading '/'). Pod-side absolute paths and '~' therefore ride the
1046
+ // exec channel instead: base64 piped through sh, chunked well under exec's
1047
+ // 65_536-byte argv cap, with the local mode re-applied via chmod.
1048
+ /** '~' on the pod expands to the conventional coding/openclaw home. */
1049
+ const CP_POD_HOME = '/home/node';
1050
+ /** Raw bytes per exec chunk: 24 KiB -> 32_768 base64 chars, far under the argv cap. */
1051
+ const CP_EXEC_CHUNK_BYTES = 24 * 1024;
1052
+ /** File transfers may outlive the interactive 30s exec default; use the exec max. */
1053
+ const CP_EXEC_TIMEOUT = 300;
1054
+ /** Pod-side paths the sync-root-relative Reef file API cannot serve. */
1055
+ function needsExecTransfer(podPath) {
1056
+ return podPath.startsWith('/') || podPath === '~' || podPath.startsWith('~/');
1057
+ }
1058
+ function expandPodPath(podPath) {
1059
+ if (podPath === '~')
1060
+ return CP_POD_HOME;
1061
+ if (podPath.startsWith('~/'))
1062
+ return `${CP_POD_HOME}/${podPath.slice(2)}`;
1063
+ return podPath;
1064
+ }
1065
+ /** POSIX-shell single-quote of one argument. */
1066
+ function shQuote(value) {
1067
+ return `'${value.replace(/'/g, `'\\''`)}'`;
1068
+ }
1069
+ function podDirname(path) {
1070
+ const index = path.lastIndexOf('/');
1071
+ return index <= 0 ? '/' : path.slice(0, index);
1072
+ }
1073
+ async function execOrThrow(d, id, argv, what) {
1074
+ const result = await api(what, () => d.exec(id, argv, { timeout: CP_EXEC_TIMEOUT }));
1075
+ if (result.exitCode !== 0) {
1076
+ const detail = (result.stderr || result.stdout).trim();
1077
+ throw new CliError(`${what} failed on the agent (exit ${result.exitCode})${detail ? `: ${detail}` : ''}`);
1078
+ }
1079
+ }
1080
+ /** Upload via exec: mkdir -p, chunked base64 decode appends, chmod preserved. */
1081
+ async function cpUpViaExec(d, id, localPath, podPath) {
1082
+ const remote = expandPodPath(podPath);
1083
+ const content = readFileSync(localPath);
1084
+ let mode;
1085
+ try {
1086
+ mode = statSync(localPath).mode & 0o777;
1087
+ }
1088
+ catch {
1089
+ mode = undefined;
1090
+ }
1091
+ await execOrThrow(d, id, ['mkdir', '-p', podDirname(remote)], 'mkdir on agent');
1092
+ for (let offset = 0, first = true; first || offset < content.length; first = false) {
1093
+ const chunk = content.subarray(offset, offset + CP_EXEC_CHUNK_BYTES);
1094
+ offset += chunk.length;
1095
+ const redirect = first ? '>' : '>>';
1096
+ await execOrThrow(d, id, ['sh', '-c', `printf %s ${shQuote(chunk.toString('base64'))} | base64 -d ${redirect} ${shQuote(remote)}`], 'copy up');
1097
+ }
1098
+ if (mode !== undefined) {
1099
+ await execOrThrow(d, id, ['chmod', mode.toString(8), remote], 'chmod on agent');
1100
+ }
1101
+ }
1102
+ /** Download via exec: base64 the pod file into exec stdout, decode locally. */
1103
+ async function cpDownViaExec(d, id, podPath, localPath) {
1104
+ const remote = expandPodPath(podPath);
1105
+ const result = await api('copy down', () => d.exec(id, ['sh', '-c', `base64 < ${shQuote(remote)}`], { timeout: CP_EXEC_TIMEOUT }));
1106
+ if (result.exitCode !== 0) {
1107
+ const detail = (result.stderr || result.stdout).trim();
1108
+ throw new CliError(`copy down failed on the agent (exit ${result.exitCode})${detail ? `: ${detail}` : ''}`);
1109
+ }
1110
+ const content = Buffer.from(result.stdout.replace(/\s+/g, ''), 'base64');
1111
+ const parent = dirname(localPath);
1112
+ if (parent && parent !== '.')
1113
+ mkdirSync(parent, { recursive: true });
1114
+ writeFileSync(localPath, content);
1115
+ return localPath;
1116
+ }
1117
+ async function cmdCp(ctx, args) {
1118
+ const parsed = parseCommandArgs(args);
1119
+ if (parsed.help)
1120
+ return printHelp();
1121
+ if (parsed.positionals.length !== 2) {
1122
+ throw new UsageError('usage: hyper agents cp <src> <dst> (exactly one side must be <id>:<path>)');
1123
+ }
1124
+ const src = parseCpTarget(parsed.positionals[0]);
1125
+ const dst = parseCpTarget(parsed.positionals[1]);
1126
+ if ((src.agentRef === undefined) === (dst.agentRef === undefined)) {
1127
+ throw new UsageError('exactly one side of cp must be remote (<id>:<path>)');
1128
+ }
1129
+ const { d } = await adopt(ctx);
1130
+ if (dst.agentRef) {
1131
+ const id = await resolveAgentRef(d, dst.agentRef);
1132
+ if (needsExecTransfer(dst.path)) {
1133
+ await cpUpViaExec(d, id, src.path, dst.path);
1134
+ }
1135
+ else {
1136
+ await api('copy up', () => d.cpTo(id, src.path, dst.path));
1137
+ }
1138
+ let bytes = 0;
1139
+ try {
1140
+ bytes = statSync(src.path).size;
1141
+ }
1142
+ catch {
1143
+ bytes = 0;
1144
+ }
1145
+ ctx.output.info(`copied ${formatBytes(bytes)} up to ${shortId(id)}:${dst.path}`);
1146
+ ctx.output.result({ direction: 'up', agent_id: id, local: src.path, remote: dst.path, bytes }, `copied ${src.path} -> ${shortId(id)}:${dst.path}`);
1147
+ return;
1148
+ }
1149
+ const id = await resolveAgentRef(d, src.agentRef);
1150
+ const destination = needsExecTransfer(src.path)
1151
+ ? await cpDownViaExec(d, id, src.path, dst.path)
1152
+ : await api('copy down', () => d.cpFrom(id, src.path, dst.path));
1153
+ let bytes = 0;
1154
+ try {
1155
+ bytes = statSync(destination).size;
1156
+ }
1157
+ catch {
1158
+ bytes = 0;
1159
+ }
1160
+ ctx.output.info(`copied ${formatBytes(bytes)} down from ${shortId(id)}:${src.path}`);
1161
+ ctx.output.result({ direction: 'down', agent_id: id, remote: src.path, local: destination, bytes }, `copied ${shortId(id)}:${src.path} -> ${destination}`);
1162
+ }
1163
+ // ---------------------------------------------------------------------------
1164
+ // activate
1165
+ // ---------------------------------------------------------------------------
1166
+ async function cmdActivate(ctx, args) {
1167
+ const parsed = parseCommandArgs(args, {
1168
+ 'extend-existing': { type: 'boolean', default: false },
1169
+ });
1170
+ if (parsed.help)
1171
+ return printHelp();
1172
+ const code = onePositional(parsed, 'code');
1173
+ const { client } = await adopt(ctx);
1174
+ const result = await api('redeem code', () => client.agent.redeemGrantCode(code, {
1175
+ extendExisting: parsed.values['extend-existing'] === true,
1176
+ }));
1177
+ printActivation(ctx, result);
1178
+ }
1179
+ function printActivation(ctx, result) {
1180
+ const { grant, entitlement } = result;
1181
+ const rows = [
1182
+ ['code', grant.code ?? ''],
1183
+ ['plan', entitlement.planName || entitlement.planId || grant.planId],
1184
+ ['plan_id', entitlement.planId || grant.planId],
1185
+ ['starts_at', iso(entitlement.startsAt) ?? ''],
1186
+ ['expires_at', iso(entitlement.expiresAt) ?? ''],
1187
+ ['tags', (entitlement.tags.length ? entitlement.tags : grant.tags).join(', ')],
1188
+ ['entitlement', entitlement.id],
1189
+ ];
1190
+ ctx.output.result(result, ['Code activated', recordLabelValue(rows.filter(([, v]) => v !== ''))].join('\n'));
1191
+ }
1192
+ // ---------------------------------------------------------------------------
1193
+ // routines
1194
+ // ---------------------------------------------------------------------------
1195
+ function routineJson(routine) {
1196
+ return {
1197
+ id: routine.id,
1198
+ name: routine.name,
1199
+ agent_id: routine.agentId,
1200
+ cron: routine.cron,
1201
+ run_at: routine.runAt,
1202
+ session_id: routine.sessionId,
1203
+ next_run_at: routine.nextRunAt,
1204
+ prompt: routine.prompt,
1205
+ enabled: routine.enabled,
1206
+ created_at: routine.createdAt,
1207
+ updated_at: routine.updatedAt,
1208
+ };
1209
+ }
1210
+ function routineSchedule(routine) {
1211
+ if (routine.cron)
1212
+ return `cron: ${routine.cron}`;
1213
+ if (routine.runAt)
1214
+ return `once: ${routine.runAt}`;
1215
+ return '';
1216
+ }
1217
+ async function cmdRoutines(ctx, args) {
1218
+ const [verb = 'list', ...rest] = args;
1219
+ switch (verb) {
1220
+ case 'list': {
1221
+ const parsed = parseCommandArgs(rest, { agent: { type: 'string' } });
1222
+ if (parsed.help)
1223
+ return printHelp();
1224
+ const { client } = await adopt(ctx);
1225
+ const agentFilter = str(parsed, 'agent');
1226
+ const routines = await api('list routines', () => client.routines.list(agentFilter ? { agentId: agentFilter } : {}));
1227
+ ctx.output.info(`total ${routines.length}`);
1228
+ ctx.output.result(routines.map(routineJson), routines.length === 0
1229
+ ? 'No routines found.'
1230
+ : {
1231
+ columns: ['NAME', 'SCHEDULE', 'ENABLED', 'ID'],
1232
+ rows: routines.map((r) => [
1233
+ r.name ?? '',
1234
+ routineSchedule(r),
1235
+ r.enabled ? 'yes' : 'no',
1236
+ r.id,
1237
+ ]),
1238
+ });
1239
+ return;
1240
+ }
1241
+ case 'create': {
1242
+ const parsed = parseCommandArgs(rest, {
1243
+ name: { type: 'string' },
1244
+ cron: { type: 'string' },
1245
+ 'run-at': { type: 'string' },
1246
+ prompt: { type: 'string' },
1247
+ agent: { type: 'string' },
1248
+ session: { type: 'string' },
1249
+ disabled: { type: 'boolean', default: false },
1250
+ });
1251
+ if (parsed.help)
1252
+ return printHelp();
1253
+ if (parsed.positionals.length > 0) {
1254
+ throw new UsageError(`unexpected arguments: ${parsed.positionals.join(' ')}`);
1255
+ }
1256
+ const prompt = str(parsed, 'prompt');
1257
+ if (!prompt)
1258
+ throw new UsageError('--prompt is required');
1259
+ const cron = str(parsed, 'cron');
1260
+ const runAt = str(parsed, 'run-at');
1261
+ if ((cron === undefined) === (runAt === undefined)) {
1262
+ throw new UsageError('exactly one of --cron or --run-at is required');
1263
+ }
1264
+ const agentRef = str(parsed, 'agent');
1265
+ const { client, d } = await adopt(ctx);
1266
+ const body = {
1267
+ ...(agentRef ? { agentId: await resolveAgentRef(d, agentRef) } : {}),
1268
+ prompt,
1269
+ ...(cron !== undefined ? { cron } : {}),
1270
+ ...(runAt !== undefined ? { runAt } : {}),
1271
+ ...(str(parsed, 'name') ? { name: str(parsed, 'name') } : {}),
1272
+ ...(str(parsed, 'session') ? { sessionId: str(parsed, 'session') } : {}),
1273
+ enabled: parsed.values.disabled !== true,
1274
+ };
1275
+ const routine = await api('create routine', () => client.routines.create(body));
1276
+ ctx.output.result(routineJson(routine), recordLabelValue([
1277
+ ['created', routine.id],
1278
+ ['name', routine.name ?? ''],
1279
+ ['schedule', routineSchedule(routine)],
1280
+ ['agent', routine.agentId],
1281
+ ['next_run_at', routine.nextRunAt ?? ''],
1282
+ ]));
1283
+ return;
1284
+ }
1285
+ case 'delete': {
1286
+ const parsed = parseCommandArgs(rest, { yes: { type: 'boolean', short: 'y', default: false } });
1287
+ if (parsed.help)
1288
+ return printHelp();
1289
+ const routineId = onePositional(parsed, 'routine id');
1290
+ if (needsConfirmation(ctx, parsed.values.yes === true)) {
1291
+ if (!(await confirm(`Delete routine ${routineId}? [y/N] `))) {
1292
+ ctx.output.info('aborted');
1293
+ return;
1294
+ }
1295
+ }
1296
+ const { client } = await adopt(ctx);
1297
+ await api('delete routine', () => client.routines.delete(routineId));
1298
+ ctx.output.result({ deleted: routineId }, `deleted ${routineId}`);
1299
+ return;
1300
+ }
1301
+ // Hidden: works, stays out of the help listing.
1302
+ case 'update': {
1303
+ const parsed = parseCommandArgs(rest, {
1304
+ name: { type: 'string' },
1305
+ cron: { type: 'string' },
1306
+ 'run-at': { type: 'string' },
1307
+ prompt: { type: 'string' },
1308
+ enable: { type: 'boolean', default: false },
1309
+ disable: { type: 'boolean', default: false },
1310
+ agent: { type: 'string' },
1311
+ session: { type: 'string' },
1312
+ });
1313
+ if (parsed.help)
1314
+ return printHelp();
1315
+ const routineId = onePositional(parsed, 'routine id');
1316
+ const body = {};
1317
+ if (parsed.values.enable === true && parsed.values.disable === true) {
1318
+ throw new UsageError('pass only one of --enable or --disable');
1319
+ }
1320
+ if (parsed.values.enable === true)
1321
+ body.enabled = true;
1322
+ if (parsed.values.disable === true)
1323
+ body.enabled = false;
1324
+ if (str(parsed, 'name') !== undefined)
1325
+ body.name = str(parsed, 'name');
1326
+ if (str(parsed, 'cron') !== undefined)
1327
+ body.cron = str(parsed, 'cron');
1328
+ if (str(parsed, 'run-at') !== undefined)
1329
+ body.runAt = str(parsed, 'run-at');
1330
+ if (str(parsed, 'prompt') !== undefined)
1331
+ body.prompt = str(parsed, 'prompt');
1332
+ const session = str(parsed, 'session');
1333
+ if (session !== undefined)
1334
+ body.sessionId = session === 'null' ? '' : session;
1335
+ if (Object.keys(body).length === 0) {
1336
+ throw new UsageError('nothing to update; pass --name, --cron, --run-at, --prompt, --session, --enable or --disable');
1337
+ }
1338
+ const { client } = await adopt(ctx);
1339
+ const routine = await api('update routine', () => client.routines.update(routineId, body));
1340
+ ctx.output.result(routineJson(routine), `updated ${routine.id}`);
1341
+ return;
1342
+ }
1343
+ default:
1344
+ throw new UsageError(`unknown routines command '${verb}' (expected: list, create, delete)`);
1345
+ }
1346
+ }
1347
+ // ---------------------------------------------------------------------------
1348
+ // token (hidden)
1349
+ // ---------------------------------------------------------------------------
1350
+ async function cmdToken(ctx, args) {
1351
+ const parsed = parseCommandArgs(args, { scope: { type: 'string', multiple: true } });
1352
+ if (parsed.help)
1353
+ return printHelp();
1354
+ const ref = onePositional(parsed, 'agent id');
1355
+ const scopes = strList(parsed, 'scope');
1356
+ if (scopes.some((scope) => !scope.trim()))
1357
+ throw new UsageError('--scope must not be empty');
1358
+ const { d } = await adopt(ctx);
1359
+ const id = await resolveAgentRef(d, ref);
1360
+ // The SDK signs the request body with an optional key name only; repeated
1361
+ // scopes are carried as that name until createScopedKey grows a scopes field.
1362
+ const record = await api('create token', () => d.createScopedKey(id, scopes.length > 0 ? scopes.join(',') : undefined));
1363
+ const key = ['key', 'api_key', 'apiKey', 'token']
1364
+ .map((field) => record[field])
1365
+ .find((value) => typeof value === 'string' && value.length > 0);
1366
+ ctx.output.info('this key is shown once and is not stored; save it now');
1367
+ if (ctx.format !== 'json') {
1368
+ // Table mode: the key is the single line on stdout — once, never embedded
1369
+ // in a record table.
1370
+ if (!key)
1371
+ throw new CliError('create token failed: the response carried no key value');
1372
+ ctx.output.result({ id, agent_id: id }, key);
1373
+ return;
1374
+ }
1375
+ ctx.output.result({ ...record, agent_id: record.agent_id ?? id });
1376
+ }
1377
+ // ---------------------------------------------------------------------------
1378
+ // config (hidden)
1379
+ // ---------------------------------------------------------------------------
1380
+ async function cmdConfig(ctx, args) {
1381
+ const [verb, ...rest] = args;
1382
+ if (verb === 'set') {
1383
+ throw new UsageError('hyper agents config is read-only: it dumps the launch config applied at agent start. '
1384
+ + 'Mutate it with hyper agents routes add|remove or the deployments env API.');
1385
+ }
1386
+ const parsed = parseCommandArgs(verb === 'get' ? rest : args);
1387
+ if (parsed.help)
1388
+ return printHelp();
1389
+ const ref = onePositional(parsed, 'agent id');
1390
+ const { d } = await adopt(ctx);
1391
+ const agent = await api('get agent', async () => d.get(await resolveAgentRef(d, ref)));
1392
+ // launch_config IS the agent config: the complete contract applied once at
1393
+ // launch. It arrives with secrets already stripped; redact defense-in-depth.
1394
+ const config = redactLaunchConfig((agent.launchConfig && typeof agent.launchConfig === 'object'
1395
+ ? agent.launchConfig
1396
+ : {}));
1397
+ ctx.output.result(config, JSON.stringify(config, null, 2));
1398
+ }
1399
+ function catalogModelFromJson(value) {
1400
+ if (!value || typeof value !== 'object')
1401
+ return null;
1402
+ const record = value;
1403
+ if (typeof record.id !== 'string' || record.id.length === 0)
1404
+ return null;
1405
+ return {
1406
+ id: record.id,
1407
+ name: typeof record.name === 'string' ? record.name : null,
1408
+ context_window: typeof record.contextWindow === 'number' ? record.contextWindow : null,
1409
+ reasoning: typeof record.reasoning === 'boolean' ? record.reasoning : null,
1410
+ };
1411
+ }
1412
+ /** The openclaw model catalog lives in the pod config (under the sync root). */
1413
+ const OPENCLAW_CONFIG_PATH = '.openclaw/openclaw.json';
1414
+ /**
1415
+ * The openclaw family keeps its provider model catalog in
1416
+ * ~/.openclaw/openclaw.json (models.providers.<name>.models). The Reef file
1417
+ * API serves sync-root-relative reads from the stored volume, so this works
1418
+ * on STOPPED agents and needs no exec or RUNNING gate.
1419
+ */
1420
+ async function cmdModels(ctx, args) {
1421
+ const parsed = parseCommandArgs(args);
1422
+ if (parsed.help)
1423
+ return printHelp();
1424
+ const ref = onePositional(parsed, 'agent id');
1425
+ const { d } = await adopt(ctx);
1426
+ const agent = await api('get agent', async () => d.get(await resolveAgentRef(d, ref)));
1427
+ if (!OPENCLAW_SET.has(canonicalRuntimeName(agent.runtime ?? ''))) {
1428
+ throw new CliError(`agents models is openclaw-only: agent ${shortId(agent.id)} runs '${agent.runtime ?? 'unknown'}'`);
1429
+ }
1430
+ let openclawConfig;
1431
+ try {
1432
+ openclawConfig = JSON.parse(await d.fileRead(agent.id, OPENCLAW_CONFIG_PATH));
1433
+ }
1434
+ catch (err) {
1435
+ if (err instanceof SyntaxError) {
1436
+ throw new CliError(`${OPENCLAW_CONFIG_PATH} on ${shortId(agent.id)} is not valid JSON`);
1437
+ }
1438
+ throw new CliError(`read ${OPENCLAW_CONFIG_PATH} failed: ${describeFailure(err)}`);
1439
+ }
1440
+ const modelsSection = (openclawConfig && typeof openclawConfig === 'object'
1441
+ ? openclawConfig.models
1442
+ : undefined);
1443
+ const providerMap = (modelsSection && typeof modelsSection === 'object'
1444
+ ? modelsSection.providers
1445
+ : undefined) ?? {};
1446
+ const catalog = {};
1447
+ for (const [provider, providerConfig] of Object.entries(providerMap)) {
1448
+ const models = (providerConfig && typeof providerConfig === 'object'
1449
+ ? providerConfig.models
1450
+ : undefined);
1451
+ catalog[provider] = (Array.isArray(models) ? models : [])
1452
+ .map(catalogModelFromJson)
1453
+ .filter((model) => model !== null);
1454
+ }
1455
+ const rows = Object.entries(catalog)
1456
+ .flatMap(([provider, models]) => models.map((model) => [provider, model.id]));
1457
+ ctx.output.result({ agent_id: agent.id, providers: catalog }, rows.length === 0
1458
+ ? `No models configured on ${shortId(agent.id)}.`
1459
+ : { columns: ['PROVIDER', 'MODEL'], rows });
1460
+ }
1461
+ function requireChatRuntime(runtime) {
1462
+ const normalized = canonicalRuntimeName(runtime);
1463
+ const supported = [...OPENCLAW_RUNTIMES, ...HERMES_RUNTIMES, ...ACP_RUNTIMES];
1464
+ if (supported.some((r) => r === normalized))
1465
+ return runtime;
1466
+ throw new CliError(`chat is not supported on runtime '${runtime || 'unknown'}' ` + `(supported: ${supported.join(', ')})`);
1467
+ }
1468
+ /** Text of one ACP content block (or block array); non-text blocks fold to ''. */
1469
+ function acpContentText(content) {
1470
+ if (typeof content === 'string')
1471
+ return content;
1472
+ if (Array.isArray(content))
1473
+ return content.map(acpContentText).join('');
1474
+ if (content && typeof content === 'object') {
1475
+ const block = content;
1476
+ if (block.type === 'text' && typeof block.text === 'string')
1477
+ return block.text;
1478
+ }
1479
+ return '';
1480
+ }
1481
+ async function readChatMcpServers(file) {
1482
+ let value;
1483
+ try {
1484
+ let text;
1485
+ if (file === '-') {
1486
+ const chunks = [];
1487
+ for await (const chunk of process.stdin)
1488
+ chunks.push(Buffer.from(chunk));
1489
+ text = Buffer.concat(chunks).toString('utf8');
1490
+ }
1491
+ else
1492
+ text = readFileSync(file, 'utf8');
1493
+ value = JSON.parse(text);
1494
+ }
1495
+ catch {
1496
+ throw new UsageError('--mcp-servers: could not read JSON array');
1497
+ }
1498
+ const object = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
1499
+ const pairs = (v) => Array.isArray(v) && v.every((entry) => object(entry) && typeof entry.name === 'string' && typeof entry.value === 'string');
1500
+ if (!Array.isArray(value) || !value.every((server) => {
1501
+ if (!object(server) || typeof server.name !== 'string')
1502
+ return false;
1503
+ if (server.type === 'http' || server.type === 'sse') {
1504
+ return typeof server.url === 'string' && pairs(server.headers);
1505
+ }
1506
+ return server.type === undefined && typeof server.command === 'string'
1507
+ && Array.isArray(server.args) && server.args.every((arg) => typeof arg === 'string')
1508
+ && pairs(server.env);
1509
+ })) {
1510
+ throw new UsageError('--mcp-servers must be a JSON array of ACP MCP servers (stdio command/args/env or http/sse url/headers)');
1511
+ }
1512
+ return value;
1513
+ }
1514
+ async function cmdChat(ctx, args) {
1515
+ const parsed = parseCommandArgs(args, {
1516
+ session: { type: 'string', short: 's' },
1517
+ timeout: { type: 'string' },
1518
+ stream: { type: 'boolean', default: false },
1519
+ permissions: { type: 'string' },
1520
+ cwd: { type: 'string' },
1521
+ 'mcp-servers': { type: 'string' },
1522
+ });
1523
+ if (parsed.help)
1524
+ return printHelp();
1525
+ const permissions = str(parsed, 'permissions');
1526
+ if (permissions === 'ask') {
1527
+ throw new UsageError('--permissions ask requires an interactive permission UI, which agents chat does not support');
1528
+ }
1529
+ if (permissions !== undefined && permissions !== 'auto' && permissions !== 'none') {
1530
+ throw new UsageError('--permissions must be auto or none');
1531
+ }
1532
+ const [ref, ...promptParts] = parsed.positionals;
1533
+ if (!ref)
1534
+ throw new UsageError('missing agent id');
1535
+ const promptText = promptParts.join(' ').trim();
1536
+ if (!promptText)
1537
+ throw new UsageError('missing prompt: hyper agents chat <id> <prompt...>');
1538
+ const sessionName = str(parsed, 'session');
1539
+ if (sessionName !== undefined && !sessionName.trim())
1540
+ throw new UsageError('--session must not be empty');
1541
+ const stream = parsed.values.stream === true;
1542
+ const timeoutMs = secondsFlag(parsed, 'timeout', 120);
1543
+ const cwd = str(parsed, 'cwd');
1544
+ const mcpFile = str(parsed, 'mcp-servers');
1545
+ const mcpServers = mcpFile === undefined ? undefined : await readChatMcpServers(mcpFile);
1546
+ const { client, d } = await adopt(ctx);
1547
+ const resolved = await api('get agent', async () => d.get(await resolveAgentRef(d, ref)));
1548
+ requireChatRuntime(resolved.runtime ?? '');
1549
+ let stage = 'start';
1550
+ let lastCompletedStage;
1551
+ let stageError;
1552
+ const activeOperations = new Map();
1553
+ let timedOut = false;
1554
+ // The SDK's prompt path takes no AbortSignal; cancel the turn before
1555
+ // closing the connection, with a bounded cleanup deadline.
1556
+ let closeActive;
1557
+ let cancelActive;
1558
+ let terminalError;
1559
+ let terminalPermissionError;
1560
+ const controller = new AbortController();
1561
+ const startedAt = Date.now();
1562
+ const remainingMs = () => Math.max(1, timeoutMs - (Date.now() - startedAt));
1563
+ const evidence = () => ({ elapsedMs: Date.now() - startedAt, timeoutMs,
1564
+ deadlineAt: startedAt + timeoutMs, lastCompletedStage, timedOut,
1565
+ ...(stageError ? { error: stageError } : {}) });
1566
+ const onDiagnostic = (event) => {
1567
+ if (event.stage !== 'transport' && !timedOut) {
1568
+ if (event.phase === 'started') {
1569
+ activeOperations.set(event.operationId, event.stage);
1570
+ stage = event.stage;
1571
+ stageError = undefined;
1572
+ }
1573
+ else {
1574
+ activeOperations.delete(event.operationId);
1575
+ if (event.phase === 'failed') {
1576
+ stage = event.stage;
1577
+ stageError = event.error;
1578
+ }
1579
+ else {
1580
+ lastCompletedStage = event.stage;
1581
+ stage = [...activeOperations.values()].at(-1) ?? event.stage;
1582
+ }
1583
+ }
1584
+ }
1585
+ ctx.output.info(`chat diagnostic: ${JSON.stringify({ ...event, chat: evidence() })}`);
1586
+ };
1587
+ let rejectGuard;
1588
+ const stop = (error, cancelOwnedPrompt = true) => {
1589
+ if (terminalError)
1590
+ return;
1591
+ terminalError = error;
1592
+ void (async () => {
1593
+ // Cancellation is a standard ACP notification. Bound cleanup even if
1594
+ // delivery stalls; a socket close alone does not cancel the native turn.
1595
+ let cleanupTimer;
1596
+ try {
1597
+ await Promise.race([
1598
+ Promise.resolve().then(() => {
1599
+ if (cancelOwnedPrompt && !terminalPermissionError)
1600
+ return cancelActive?.();
1601
+ }).catch(() => { }),
1602
+ new Promise((resolve) => { cleanupTimer = setTimeout(resolve, 250); }),
1603
+ ]);
1604
+ }
1605
+ finally {
1606
+ clearTimeout(cleanupTimer);
1607
+ controller.abort();
1608
+ closeActive?.();
1609
+ rejectGuard(error);
1610
+ }
1611
+ })();
1612
+ };
1613
+ const onSigint = () => stop(new CliError('chat interrupted', 130));
1614
+ let timer;
1615
+ const guard = new Promise((_, reject) => {
1616
+ rejectGuard = reject;
1617
+ timer = setTimeout(() => {
1618
+ timedOut = true;
1619
+ ctx.output.info(`chat diagnostic: ${JSON.stringify({ stage, phase: 'timeout', ...evidence() })}`);
1620
+ stop(new CliError(`chat: ${stage} stage failed: timed out after ${timeoutMs / 1000}s (${JSON.stringify(evidence())})`));
1621
+ }, timeoutMs);
1622
+ if (typeof timer.unref === 'function')
1623
+ timer.unref();
1624
+ });
1625
+ process.on('SIGINT', onSigint);
1626
+ const atStage = async (name, fn) => {
1627
+ stage = name;
1628
+ try {
1629
+ if (terminalError)
1630
+ throw terminalError;
1631
+ const result = await fn();
1632
+ if (terminalError)
1633
+ throw terminalError;
1634
+ return result;
1635
+ }
1636
+ catch (err) {
1637
+ // Pairing callbacks retag the stage while a connect awaits approval.
1638
+ const failure = new CliError(`chat: ${stage} stage failed: ${describeFailure(err)} (${JSON.stringify(evidence())})`);
1639
+ failure.cause = err;
1640
+ throw failure;
1641
+ }
1642
+ };
1643
+ const runAll = async () => {
1644
+ let agent = resolved;
1645
+ if (agent.state.toUpperCase() !== 'RUNNING') {
1646
+ const started = await atStage('start', () => startAgentForRuntime(d, agent));
1647
+ ctx.output.info(`started agent ${shortId(agent.id)} (${started.state})`);
1648
+ // waitRunning resolves with the RUNNING hydration or throws (timeout /
1649
+ // terminal state), so the state needs no recheck here.
1650
+ agent = await atStage('wait', () => d.waitRunning(agent.id, remainingMs(), 5_000));
1651
+ }
1652
+ const runtime = agent.runtime ?? resolved.runtime ?? '';
1653
+ let reply = '';
1654
+ let streamedText = '';
1655
+ let sessionId = '';
1656
+ // acpConnect resumes the selected session before returning.
1657
+ let sessionResumed = false;
1658
+ // --stream deltas go to stdout in table mode; under --json stdout belongs
1659
+ // to the result bag, so deltas ride stderr there instead.
1660
+ const emitDelta = (text) => {
1661
+ streamedText += text;
1662
+ if (!stream)
1663
+ return;
1664
+ if (ctx.format === 'json')
1665
+ process.stderr.write(text);
1666
+ else
1667
+ process.stdout.write(text);
1668
+ };
1669
+ // ACP is the single chat surface for every supported runtime: the flat
1670
+ // Agent owns acpConnect and call-time gates on the runtime label (a
1671
+ // non-ACP label throws rather than dialing). The dial rides the backend
1672
+ // session proxy (/ws/acp) by default; --session awaits native v1 resume
1673
+ // inside acpConnect. A refusal fails attachment without a replacement.
1674
+ const acp = await atStage('connect', () => agent.acpConnect({
1675
+ ...(cwd !== undefined ? { cwd } : {}),
1676
+ ...(mcpServers !== undefined ? { mcpServers } : {}),
1677
+ signal: controller.signal,
1678
+ onDiagnostic,
1679
+ clientInfo: { name: 'hypercli-cli' },
1680
+ onPermissionRequest: () => {
1681
+ ctx.output.info('chat diagnostic: {"stage":"permission","phase":"pending","resolution":"cancelled","reason":"interactive UI unavailable"}');
1682
+ const error = new CliError('chat: permission required, but agents chat has no interactive permission UI; the request was cancelled');
1683
+ // Abstain locally. Another observer may answer this shared request;
1684
+ // inability to answer must never cancel the shared native turn.
1685
+ setImmediate(() => stop(error, false));
1686
+ terminalPermissionError = error;
1687
+ return { outcome: { outcome: 'cancelled' } };
1688
+ },
1689
+ ...(sessionName !== undefined ? { sessionId: sessionName } : {}),
1690
+ onUpdate: (notification) => {
1691
+ if (terminalError || terminalPermissionError)
1692
+ return;
1693
+ const update = notification.update;
1694
+ if (update.sessionUpdate !== 'agent_message_chunk')
1695
+ return;
1696
+ const text = acpContentText(update.content);
1697
+ if (!text)
1698
+ return;
1699
+ reply += text;
1700
+ emitDelta(text);
1701
+ },
1702
+ }));
1703
+ try {
1704
+ closeActive = () => acp.close();
1705
+ if (terminalError)
1706
+ throw terminalError;
1707
+ sessionId = sessionName ?? (await atStage('connect', () => acp.newSession())).sessionId;
1708
+ sessionResumed = sessionName !== undefined;
1709
+ ctx.output.info(`session opened ${sessionId}`);
1710
+ if (permissions !== undefined) {
1711
+ await atStage('policy', () => client.sessions.setMode(sessionId, permissions));
1712
+ }
1713
+ try {
1714
+ // Only this command's submitted prompt makes it eligible to cancel.
1715
+ // Attaching or waiting for policy does not confer turn ownership.
1716
+ cancelActive = () => acp.cancel(sessionId);
1717
+ await atStage('prompt', () => acp.prompt(sessionId, promptText));
1718
+ cancelActive = undefined;
1719
+ }
1720
+ catch (error) {
1721
+ if (terminalPermissionError) {
1722
+ stop(terminalPermissionError, false);
1723
+ await guard;
1724
+ }
1725
+ throw error;
1726
+ }
1727
+ if (terminalPermissionError) {
1728
+ stop(terminalPermissionError, false);
1729
+ await guard;
1730
+ }
1731
+ if (!reply)
1732
+ throw new Error('the turn ended without an assistant reply');
1733
+ }
1734
+ finally {
1735
+ cancelActive = undefined;
1736
+ closeActive = undefined;
1737
+ acp.close();
1738
+ }
1739
+ const payload = {
1740
+ reply,
1741
+ session_id: sessionId,
1742
+ session: { id: sessionId, resumed: sessionResumed },
1743
+ runtime,
1744
+ agent_id: agent.id,
1745
+ };
1746
+ if (ctx.format === 'json') {
1747
+ ctx.output.result(payload);
1748
+ return;
1749
+ }
1750
+ if (stream && streamedText) {
1751
+ if (!streamedText.endsWith('\n'))
1752
+ process.stdout.write('\n');
1753
+ // The deltas already showed the reply; reprint only when replace events
1754
+ // made the final text differ from what was streamed.
1755
+ ctx.output.result(payload, reply === streamedText ? '' : reply);
1756
+ return;
1757
+ }
1758
+ ctx.output.result(payload, reply);
1759
+ };
1760
+ const work = runAll();
1761
+ // The timeout guard can win the race while work is still in flight — mark
1762
+ // that rejection handled so it never surfaces as an unhandled rejection.
1763
+ work.catch(() => { });
1764
+ try {
1765
+ await Promise.race([work, guard]);
1766
+ }
1767
+ catch (err) {
1768
+ if (terminalError)
1769
+ throw terminalError;
1770
+ if (err instanceof CliError)
1771
+ throw err;
1772
+ throw new CliError(`chat: ${stage} stage failed: ${describeFailure(err)} (${JSON.stringify(evidence())})`);
1773
+ }
1774
+ finally {
1775
+ clearTimeout(timer);
1776
+ process.removeListener('SIGINT', onSigint);
1777
+ }
1778
+ }
1779
+ // ---------------------------------------------------------------------------
1780
+ // routes (hidden)
1781
+ // ---------------------------------------------------------------------------
1782
+ function routesTable(state) {
1783
+ const entries = Object.entries(state.routes);
1784
+ if (entries.length === 0)
1785
+ return `No routes on ${state.agentId}.`;
1786
+ return {
1787
+ columns: ['NAME', 'PORT', 'PREFIX', 'AUTH', 'DNS'],
1788
+ rows: entries.map(([routeName, route]) => [
1789
+ routeName,
1790
+ String(route.port),
1791
+ route.prefix ?? '',
1792
+ route.auth === undefined ? '' : route.auth ? 'yes' : 'no',
1793
+ String(state.routeStatuses[routeName]?.dns_state ?? ''),
1794
+ ]),
1795
+ };
1796
+ }
1797
+ function routesJson(state) {
1798
+ return {
1799
+ agent_id: state.agentId,
1800
+ routes: state.routes,
1801
+ cors: state.cors,
1802
+ route_statuses: state.routeStatuses,
1803
+ };
1804
+ }
1805
+ async function cmdRoutes(ctx, args) {
1806
+ const [verb, ...rest] = args;
1807
+ switch (verb) {
1808
+ case 'list': {
1809
+ const parsed = parseCommandArgs(rest);
1810
+ if (parsed.help)
1811
+ return printHelp();
1812
+ const ref = onePositional(parsed, 'agent id');
1813
+ const { d } = await adopt(ctx);
1814
+ const id = ref.trim().toLowerCase() === 'self' ? 'self' : await resolveAgentRef(d, ref);
1815
+ const state = await api('list routes', () => d.getRoutes(id));
1816
+ ctx.output.result(routesJson(state), routesTable(state));
1817
+ return;
1818
+ }
1819
+ case 'add': {
1820
+ const parsed = parseCommandArgs(rest, {
1821
+ port: { type: 'string' },
1822
+ prefix: { type: 'string' },
1823
+ auth: { type: 'boolean', default: false },
1824
+ 'no-auth': { type: 'boolean', default: false },
1825
+ });
1826
+ if (parsed.help)
1827
+ return printHelp();
1828
+ const [ref, routeName] = parsed.positionals;
1829
+ if (!ref || !routeName || parsed.positionals.length > 2) {
1830
+ throw new UsageError('usage: hyper agents routes add <id> <name> --port N [--prefix P] [--no-auth]');
1831
+ }
1832
+ const portRaw = str(parsed, 'port');
1833
+ if (!portRaw)
1834
+ throw new UsageError('--port is required');
1835
+ const port = Number(portRaw);
1836
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
1837
+ throw new UsageError(`--port must be an integer from 1 through 65535 (got '${portRaw}')`);
1838
+ }
1839
+ if (parsed.values.auth === true && parsed.values['no-auth'] === true) {
1840
+ throw new UsageError('pass only one of --auth or --no-auth');
1841
+ }
1842
+ const route = { port };
1843
+ const prefix = str(parsed, 'prefix');
1844
+ if (prefix !== undefined)
1845
+ route.prefix = prefix;
1846
+ if (parsed.values.auth === true)
1847
+ route.auth = true;
1848
+ if (parsed.values['no-auth'] === true)
1849
+ route.auth = false;
1850
+ const { d } = await adopt(ctx);
1851
+ const id = ref.trim().toLowerCase() === 'self' ? 'self' : await resolveAgentRef(d, ref);
1852
+ const state = await api('add route', () => d.setRoute(id, routeName, route));
1853
+ ctx.output.result(routesJson(state), routesTable(state));
1854
+ return;
1855
+ }
1856
+ case 'remove': {
1857
+ const parsed = parseCommandArgs(rest);
1858
+ if (parsed.help)
1859
+ return printHelp();
1860
+ const [ref, routeName] = parsed.positionals;
1861
+ if (!ref || !routeName || parsed.positionals.length > 2) {
1862
+ throw new UsageError('usage: hyper agents routes remove <id> <name>');
1863
+ }
1864
+ const { d } = await adopt(ctx);
1865
+ const id = ref.trim().toLowerCase() === 'self' ? 'self' : await resolveAgentRef(d, ref);
1866
+ const state = await api('remove route', () => d.removeRoute(id, routeName));
1867
+ ctx.output.result(routesJson(state), routesTable(state));
1868
+ return;
1869
+ }
1870
+ default:
1871
+ throw new UsageError(`unknown routes command '${verb ?? ''}' (expected: list, add, remove)`);
1872
+ }
1873
+ }
1874
+ // ---------------------------------------------------------------------------
1875
+ // help + dispatch
1876
+ // ---------------------------------------------------------------------------
1877
+ async function cmdMessages(ctx, args) {
1878
+ const parsed = parseCommandArgs(args, { cursor: { type: 'string' }, limit: { type: 'string' }, follow: { type: 'boolean', short: 'f' } });
1879
+ if (parsed.help)
1880
+ return printHelp();
1881
+ const sessionId = onePositional(parsed, 'session id');
1882
+ const limit = Number(str(parsed, 'limit') ?? 20);
1883
+ if (!Number.isInteger(limit) || limit < 1 || limit > 200)
1884
+ throw new UsageError('--limit must be an integer from 1 through 200');
1885
+ const client = await ctx.client();
1886
+ const state = await client.sessions.getSession(sessionId);
1887
+ const agents = new Set(state.participants.filter(p => p.kind === 'agent').map(p => p.participantId));
1888
+ let previousSnapshot;
1889
+ let page;
1890
+ const render = (type) => {
1891
+ if (!page)
1892
+ return;
1893
+ // Receipts are native removal evidence, not display data. Retain counts,
1894
+ // completeness and scoped actors in both JSON and human snapshots.
1895
+ const result = { ...page, items: page.items.map(row => ({ ...row,
1896
+ ...(row.meta ? { meta: { ...row.meta, reactions: Object.fromEntries(Object.entries(row.meta.reactions).map(([key, group]) => [key, { ...group,
1897
+ actors: Object.fromEntries(Object.entries(group.actors).map(([id, actor]) => [id, { isBot: actor.isBot }])),
1898
+ }])) } } : {}),
1899
+ })) };
1900
+ const snapshot = JSON.stringify(result);
1901
+ if (parsed.values.follow && snapshot === previousSnapshot)
1902
+ return;
1903
+ previousSnapshot = snapshot;
1904
+ if (ctx.format === 'json' && parsed.values.follow)
1905
+ process.stdout.write(`${JSON.stringify({ ...result, ...(type ? { type } : {}) })}\n`);
1906
+ else
1907
+ ctx.output.result(result, { columns: ['SEQ', 'MESSAGE', 'ROLE', 'REACTIONS'], rows: result.items.map(row => [row.seq, row.messageId ?? '', row.role,
1908
+ row.meta ? Object.entries(row.meta.reactions).map(([key, group]) => `${sessionReactionDisplay(key)}: ${group.count ?? '?'} (${Object.keys(group.actors).join(', ')}${group.actorsComplete ? '' : '; actors incomplete'})`).join(' · ') : 'unknown',
1909
+ ]) });
1910
+ };
1911
+ const read = async () => {
1912
+ const fresh = await client.sessions.getMessages(sessionId, { cursor: str(parsed, 'cursor'), limit });
1913
+ const items = fresh.items.map(row => {
1914
+ const prior = page?.items.find(prior => prior.sessionId === row.sessionId && prior.seq === row.seq && prior.messageId === row.messageId);
1915
+ const meta = newerSessionMessageMeta(prior?.meta, row.meta);
1916
+ return meta === row.meta ? row : { ...row, meta };
1917
+ });
1918
+ page = { ...fresh, items };
1919
+ render();
1920
+ };
1921
+ if (!parsed.values.follow)
1922
+ return read();
1923
+ const controller = new AbortController();
1924
+ const onSigint = () => controller.abort();
1925
+ process.once('SIGINT', onSigint);
1926
+ try {
1927
+ await client.deployments.subscribe(async (event) => {
1928
+ if ((event.type === 'addReaction' || event.type === 'removeReaction') && agents.has(event.agent_id) && event.session_id === sessionId) {
1929
+ if (page) {
1930
+ const items = applySessionReactionOperation(page.items, event);
1931
+ if (items !== page.items) {
1932
+ page = { ...page, items };
1933
+ render(event.type);
1934
+ }
1935
+ }
1936
+ }
1937
+ }, { signal: controller.signal, onReady: read });
1938
+ }
1939
+ finally {
1940
+ process.removeListener('SIGINT', onSigint);
1941
+ }
1942
+ return controller.signal.aborted ? 130 : 0;
1943
+ }
1944
+ function printHelp() {
1945
+ process.stdout.write(`${renderGroupHelp({ name, summary, usage, run })}\n`);
1946
+ process.stdout.write('Chat permissions: omission preserves the mode of an existing session; new sessions default to ask.\n'
1947
+ + 'Explicit auto/none updates the session mode over HTTP before prompting and persists until changed.\n'
1948
+ + 'ask needs an interactive permission UI and is unsupported; unanswered requests are cancelled.\n');
1949
+ process.stdout.write('Runner logs are bounded replacement snapshots, not a continuous stream. JSON follow emits one snapshot per line with capture metadata.\n'
1950
+ + 'Human follow prints verified append suffixes; ambiguous truncation reports lost continuity. Bounds are per source: max-bytes 1..65536, max-lines 1..1000.\n');
1951
+ }
1952
+ export async function run(ctx, args) {
1953
+ const [subcommand, ...rest] = args;
1954
+ if (!subcommand) {
1955
+ printHelp();
1956
+ return;
1957
+ }
1958
+ if (subcommand === '--help' || subcommand === '-h') {
1959
+ printHelp();
1960
+ return;
1961
+ }
1962
+ if (subcommand.startsWith('-')) {
1963
+ // e.g. bare `hyper agents --json` with no command
1964
+ const parsed = parseCommandArgs(args);
1965
+ if (parsed.help) {
1966
+ printHelp();
1967
+ return;
1968
+ }
1969
+ throw new UsageError('missing command. See hyper agents --help.');
1970
+ }
1971
+ if (!KNOWN_COMMANDS.has(subcommand)) {
1972
+ throw new UsageError(`unknown agents command '${subcommand}'. See hyper agents --help.`);
1973
+ }
1974
+ switch (subcommand) {
1975
+ case 'ls':
1976
+ case 'list':
1977
+ return cmdLs(ctx, rest);
1978
+ case 'status':
1979
+ return cmdStatus(ctx, rest);
1980
+ case 'wait':
1981
+ return cmdWait(ctx, rest);
1982
+ case 'create':
1983
+ return cmdCreate(ctx, rest);
1984
+ case 'start':
1985
+ return cmdStart(ctx, rest);
1986
+ case 'set':
1987
+ return cmdSet(ctx, rest);
1988
+ case 'chat':
1989
+ return cmdChat(ctx, rest);
1990
+ case 'messages':
1991
+ return cmdMessages(ctx, rest);
1992
+ case 'stop':
1993
+ return cmdStop(ctx, rest);
1994
+ case 'delete':
1995
+ return cmdDelete(ctx, rest);
1996
+ case 'archive':
1997
+ return cmdArchiveRestore(ctx, rest, 'archive');
1998
+ case 'restore':
1999
+ return cmdArchiveRestore(ctx, rest, 'restore');
2000
+ case 'exec':
2001
+ return cmdExec(ctx, rest);
2002
+ case 'login':
2003
+ return cmdAgentsLogin(ctx, rest);
2004
+ case 'shell':
2005
+ return cmdShell(ctx, rest);
2006
+ case 'logs':
2007
+ return cmdLogs(ctx, rest);
2008
+ case 'cp':
2009
+ return cmdCp(ctx, rest);
2010
+ case 'activate':
2011
+ return cmdActivate(ctx, rest);
2012
+ case 'routines':
2013
+ return cmdRoutines(ctx, rest);
2014
+ case 'token':
2015
+ return cmdToken(ctx, rest);
2016
+ case 'config':
2017
+ return cmdConfig(ctx, rest);
2018
+ case 'models':
2019
+ return cmdModels(ctx, rest);
2020
+ case 'routes':
2021
+ return cmdRoutes(ctx, rest);
2022
+ default:
2023
+ throw new UsageError(`unknown agents command '${subcommand}'. See hyper agents --help.`);
2024
+ }
2025
+ }
2026
+ //# sourceMappingURL=agents.js.map