@aexhq/cli 0.25.1 → 0.26.0

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 (93) hide show
  1. package/README.md +12 -0
  2. package/dist/index.d.ts +2 -7
  3. package/dist/index.js +233 -7
  4. package/package.json +26 -29
  5. package/LICENSE +0 -201
  6. package/dist/cli.d.ts +0 -1
  7. package/dist/cli.js +0 -51
  8. package/dist/cli.js.map +0 -1
  9. package/dist/cli.mjs +0 -4657
  10. package/dist/cli.mjs.sha256 +0 -1
  11. package/dist/host/cancel.d.ts +0 -3
  12. package/dist/host/cancel.js +0 -30
  13. package/dist/host/cancel.js.map +0 -1
  14. package/dist/host/common.d.ts +0 -135
  15. package/dist/host/common.js +0 -281
  16. package/dist/host/common.js.map +0 -1
  17. package/dist/host/delete-asset.d.ts +0 -3
  18. package/dist/host/delete-asset.js +0 -34
  19. package/dist/host/delete-asset.js.map +0 -1
  20. package/dist/host/delete.d.ts +0 -3
  21. package/dist/host/delete.js +0 -30
  22. package/dist/host/delete.js.map +0 -1
  23. package/dist/host/download.d.ts +0 -3
  24. package/dist/host/download.js +0 -80
  25. package/dist/host/download.js.map +0 -1
  26. package/dist/host/events.d.ts +0 -3
  27. package/dist/host/events.js +0 -97
  28. package/dist/host/events.js.map +0 -1
  29. package/dist/host/index.d.ts +0 -17
  30. package/dist/host/index.js +0 -18
  31. package/dist/host/index.js.map +0 -1
  32. package/dist/host/outputs.d.ts +0 -3
  33. package/dist/host/outputs.js +0 -36
  34. package/dist/host/outputs.js.map +0 -1
  35. package/dist/host/run-cmd.d.ts +0 -3
  36. package/dist/host/run-cmd.js +0 -549
  37. package/dist/host/run-cmd.js.map +0 -1
  38. package/dist/host/skills-cmd.d.ts +0 -3
  39. package/dist/host/skills-cmd.js +0 -304
  40. package/dist/host/skills-cmd.js.map +0 -1
  41. package/dist/host/status.d.ts +0 -3
  42. package/dist/host/status.js +0 -31
  43. package/dist/host/status.js.map +0 -1
  44. package/dist/host/wait.d.ts +0 -3
  45. package/dist/host/wait.js +0 -88
  46. package/dist/host/wait.js.map +0 -1
  47. package/dist/host/whoami.d.ts +0 -3
  48. package/dist/host/whoami.js +0 -33
  49. package/dist/host/whoami.js.map +0 -1
  50. package/dist/index.js.map +0 -1
  51. package/dist/internal.d.ts +0 -53
  52. package/dist/internal.js +0 -10
  53. package/dist/internal.js.map +0 -1
  54. package/dist/outputs-sync.d.ts +0 -24
  55. package/dist/outputs-sync.js +0 -70
  56. package/dist/outputs-sync.js.map +0 -1
  57. package/dist/proxy.d.ts +0 -16
  58. package/dist/proxy.js +0 -316
  59. package/dist/proxy.js.map +0 -1
  60. package/dist/run.d.ts +0 -10
  61. package/dist/run.js +0 -166
  62. package/dist/run.js.map +0 -1
  63. package/dist/runtime.d.ts +0 -5
  64. package/dist/runtime.js +0 -4
  65. package/dist/runtime.js.map +0 -1
  66. package/scripts/finalize-bundle.mjs +0 -72
  67. package/src/cli.ts +0 -51
  68. package/src/host/cancel.ts +0 -39
  69. package/src/host/common.ts +0 -329
  70. package/src/host/delete-asset.ts +0 -43
  71. package/src/host/delete.ts +0 -39
  72. package/src/host/download.ts +0 -97
  73. package/src/host/events.ts +0 -113
  74. package/src/host/index.ts +0 -25
  75. package/src/host/outputs.ts +0 -45
  76. package/src/host/run-cmd.ts +0 -578
  77. package/src/host/skills-cmd.ts +0 -317
  78. package/src/host/status.ts +0 -40
  79. package/src/host/wait.ts +0 -110
  80. package/src/host/whoami.ts +0 -42
  81. package/src/index.ts +0 -7
  82. package/src/internal.ts +0 -63
  83. package/src/outputs-sync.ts +0 -76
  84. package/src/proxy.ts +0 -359
  85. package/src/run.ts +0 -187
  86. package/src/runtime.ts +0 -15
  87. package/test/cli.test.ts +0 -519
  88. package/test/host.test.ts +0 -1154
  89. package/test/no-env-vars.test.ts +0 -59
  90. package/test/outputs-sync.test.ts +0 -107
  91. package/tsconfig.build.json +0 -9
  92. package/tsconfig.json +0 -4
  93. package/vitest.config.ts +0 -14
@@ -1,578 +0,0 @@
1
- /**
2
- * `aex run` — submit a flat run via the dashboard BFF using the
3
- * same operations module the SDK uses.
4
- *
5
- * Two input modes (mutually exclusive):
6
- *
7
- * 1. `--config <path>` — plain run-request JSON:
8
- * `{ model, system?, prompt, skills?, mcpServers?, environment?,
9
- * runtimeSize?, timeout?, postHook?, proxyEndpoints?, metadata? }`. Skill
10
- * entries use storage-neutral asset refs (`{ kind: "asset", ... }`).
11
- * MCP entries may include `headers` — the CLI splits them into the
12
- * `secrets.mcpServers` bag before posting.
13
- *
14
- * 2. Flat flags — for ad-hoc / scriptable invocations:
15
- * --model <id> REQUIRED in flag mode
16
- * --system @file | --system "literal" optional system message
17
- * --prompt @file | --prompt "literal" REQUIRED in flag mode (repeatable)
18
- * --mcp name=url MCP server (repeatable)
19
- * --mcp-auth name=Header:Value adds a header on the matching --mcp (repeatable)
20
- * --metadata key=value string metadata entry (repeatable)
21
- *
22
- * Always required (both modes):
23
- * --anthropic-api-key <key> provider key (never stored)
24
- * --api-token <token> see `parseCommonHostFlags`
25
- *
26
- * Optional (both modes):
27
- * --runtime-size <size> managed runtime preset (e.g. shared-2x-2gb); default shared-1x-512mb
28
- * --run-timeout <dur> server-side run deadline (e.g. 1h); bounded [1m, 6h], default 1h
29
- * --idempotency-key <key> defaults to a fresh UUID
30
- * --proxy-endpoint '<json>' PlatformProxyEndpoint JSON (repeatable)
31
- * --proxy-auth name=<spec> bearer:tok | basic:u:p | header:v | query:v (repeatable)
32
- * --follow poll events to stdout until terminal status
33
- * --timeout <dur> with --follow: give up after this long (e.g. 8m); exit code 3
34
- */
35
- import {
36
- AEX_DEFAULT_BASE_URL,
37
- DEFAULT_RUN_PROVIDER,
38
- operations,
39
- parseRunRequestConfig,
40
- RUN_MODELS,
41
- RUNTIME_SIZES,
42
- RUN_PROVIDERS,
43
- RUNTIME_KINDS,
44
- TERMINAL_RUN_STATUSES,
45
- validateProxyAuth,
46
- type RunRequestConfig,
47
- type McpServerRef,
48
- type PlatformRunSubmissionInput,
49
- type PlatformSubmission,
50
- type PlatformInlineSecrets,
51
- type PlatformMcpServerSecret,
52
- type PlatformProxyAuthValue,
53
- type PlatformProxyEndpoint,
54
- type PlatformProxyEndpointAuth,
55
- type RunModel,
56
- type RunProvider,
57
- type RuntimeSize,
58
- type RuntimeKind,
59
- type SkillRef
60
- } from "@aexhq/contracts";
61
- import { resolve as resolvePath } from "node:path";
62
- import type { CliIO } from "../internal.js";
63
- import {
64
- type CliExitCode,
65
- RUNTIME_ERR,
66
- SUCCESS,
67
- TIMEOUT_ERR,
68
- USAGE_ERR,
69
- collectRepeated,
70
- collectRepeatedKv,
71
- collectRepeatedKvList,
72
- emitJsonError,
73
- makeHttpClient,
74
- parseCommonHostFlags,
75
- parseDuration,
76
- refuseInsideManagedRun,
77
- takeBooleanFlag,
78
- takeFlagValue,
79
- takeOptionFlag
80
- } from "./common.js";
81
-
82
- // Membership-tested against the loose `string` run status from the BFF, so we
83
- // back it with the canonical terminal set rather than a drift-prone local list.
84
- const TERMINAL_STATUSES = new Set<string>(TERMINAL_RUN_STATUSES);
85
-
86
- /* eslint-disable @typescript-eslint/no-unused-vars */ // AEX_DEFAULT_BASE_URL is re-exported only for assertion clarity.
87
- void AEX_DEFAULT_BASE_URL;
88
-
89
- export async function runRunCmd(io: CliIO, argv: readonly string[]): Promise<CliExitCode> {
90
- if (await refuseInsideManagedRun(io, "run")) return USAGE_ERR;
91
-
92
- const common = parseCommonHostFlags(argv);
93
- if (!common.ok) {
94
- io.stderr(`${common.reason}\n`);
95
- return USAGE_ERR;
96
- }
97
- let rest = common.rest;
98
-
99
- const providerFlag = takeFlagValue(rest, "--provider");
100
- if (providerFlag.error) { io.stderr(`${providerFlag.error}\n`); return USAGE_ERR; }
101
- rest = providerFlag.remaining;
102
- let provider: RunProvider = DEFAULT_RUN_PROVIDER;
103
- if (providerFlag.value !== null) {
104
- if (!(RUN_PROVIDERS as readonly string[]).includes(providerFlag.value)) {
105
- io.stderr(`--provider must be one of: ${RUN_PROVIDERS.join(", ")} (got: ${providerFlag.value})\n`);
106
- return USAGE_ERR;
107
- }
108
- provider = providerFlag.value as RunProvider;
109
- }
110
-
111
- // Provider key flag handling: each provider in RUN_PROVIDERS has its
112
- // own `--<provider>-api-key` flag. Exactly the flag matching the
113
- // selected provider must be supplied; every other provider's flag
114
- // must be absent (mirrors the secrets-coupling rule the parser
115
- // re-enforces server-side).
116
- const providerKeyValues: Partial<Record<RunProvider, string>> = {};
117
- for (const p of RUN_PROVIDERS) {
118
- const flag = takeFlagValue(rest, `--${p}-api-key`);
119
- if (flag.error) { io.stderr(`${flag.error}\n`); return USAGE_ERR; }
120
- rest = flag.remaining;
121
- if (flag.value !== null) providerKeyValues[p] = flag.value;
122
- }
123
- if (!providerKeyValues[provider]) {
124
- io.stderr(`--${provider}-api-key is required when --provider is ${provider} (the platform does not store provider keys on your behalf)\n`);
125
- return USAGE_ERR;
126
- }
127
- for (const p of RUN_PROVIDERS) {
128
- if (p === provider) continue;
129
- if (providerKeyValues[p] !== undefined) {
130
- io.stderr(`--${p}-api-key is not allowed when --provider is ${provider}\n`);
131
- return USAGE_ERR;
132
- }
133
- }
134
-
135
- // Optional runtime selector. Validate the value against the wire enum
136
- // here so the user gets an early error from the CLI; the shared parser
137
- // re-runs the check when the request lands at the API plane.
138
- const runtimeFlag = takeFlagValue(rest, "--runtime");
139
- if (runtimeFlag.error) { io.stderr(`${runtimeFlag.error}\n`); return USAGE_ERR; }
140
- rest = runtimeFlag.remaining;
141
- let runtime: RuntimeKind | undefined;
142
- if (runtimeFlag.value !== null) {
143
- if (!(RUNTIME_KINDS as readonly string[]).includes(runtimeFlag.value)) {
144
- io.stderr(`--runtime must be one of: ${RUNTIME_KINDS.join(", ")} (got: ${runtimeFlag.value})\n`);
145
- return USAGE_ERR;
146
- }
147
- runtime = runtimeFlag.value as RuntimeKind;
148
- }
149
-
150
- const idempotency = takeFlagValue(rest, "--idempotency-key");
151
- if (idempotency.error) { io.stderr(`${idempotency.error}\n`); return USAGE_ERR; }
152
- rest = idempotency.remaining;
153
-
154
- // `--runtime-size` selects a managed runtime size from the closed preset set.
155
- const runtimeSizeFlag = takeFlagValue(rest, "--runtime-size");
156
- if (runtimeSizeFlag.error) { io.stderr(`${runtimeSizeFlag.error}\n`); return USAGE_ERR; }
157
- rest = runtimeSizeFlag.remaining;
158
- if (runtimeSizeFlag.value && !(RUNTIME_SIZES as readonly string[]).includes(runtimeSizeFlag.value)) {
159
- io.stderr(`--runtime-size must be one of: ${RUNTIME_SIZES.join(", ")}\n`);
160
- return USAGE_ERR;
161
- }
162
-
163
- // `--run-timeout` is the SERVER-side run deadline (distinct from `--timeout`,
164
- // which bounds the client-side --follow loop). Format-checked locally for
165
- // fast feedback; the server applies the [1m, 6h] bounds + default.
166
- const runTimeoutFlag = takeFlagValue(rest, "--run-timeout");
167
- if (runTimeoutFlag.error) { io.stderr(`${runTimeoutFlag.error}\n`); return USAGE_ERR; }
168
- rest = runTimeoutFlag.remaining;
169
- if (runTimeoutFlag.value) {
170
- const parsed = parseDuration(runTimeoutFlag.value);
171
- if (parsed.error) {
172
- io.stderr(`--run-timeout: ${parsed.error}\n`);
173
- return USAGE_ERR;
174
- }
175
- }
176
-
177
- const follow = takeBooleanFlag(rest, "--follow");
178
- rest = follow.remaining;
179
- const timeoutFlag = takeOptionFlag(rest, "--timeout");
180
- rest = timeoutFlag.remaining;
181
- let followTimeoutMs: number | null = null;
182
- if (timeoutFlag.value !== undefined) {
183
- const parsed = parseDuration(timeoutFlag.value);
184
- if (parsed.error) {
185
- io.stderr(`--timeout: ${parsed.error}\n`);
186
- return USAGE_ERR;
187
- }
188
- followTimeoutMs = parsed.ms;
189
- }
190
-
191
- const config = takeFlagValue(rest, "--config");
192
- if (config.error) { io.stderr(`${config.error}\n`); return USAGE_ERR; }
193
- rest = config.remaining;
194
-
195
- // Flat flags
196
- const modelFlag = takeFlagValue(rest, "--model");
197
- if (modelFlag.error) { io.stderr(`${modelFlag.error}\n`); return USAGE_ERR; }
198
- rest = modelFlag.remaining;
199
-
200
- const systemFlag = takeFlagValue(rest, "--system");
201
- if (systemFlag.error) { io.stderr(`${systemFlag.error}\n`); return USAGE_ERR; }
202
- rest = systemFlag.remaining;
203
-
204
- const promptFlags = collectRepeated(rest, "--prompt");
205
- if (promptFlags.error) { io.stderr(`${promptFlags.error}\n`); return USAGE_ERR; }
206
- rest = promptFlags.remaining;
207
-
208
- const mcpFlags = collectRepeatedKv(rest, "--mcp");
209
- if (mcpFlags.error) { io.stderr(`${mcpFlags.error}\n`); return USAGE_ERR; }
210
- rest = mcpFlags.remaining;
211
-
212
- const mcpAuthFlags = collectRepeatedKvList(rest, "--mcp-auth");
213
- if (mcpAuthFlags.error) { io.stderr(`${mcpAuthFlags.error}\n`); return USAGE_ERR; }
214
- rest = mcpAuthFlags.remaining;
215
-
216
- const metadataFlags = collectRepeatedKv(rest, "--metadata");
217
- if (metadataFlags.error) { io.stderr(`${metadataFlags.error}\n`); return USAGE_ERR; }
218
- rest = metadataFlags.remaining;
219
-
220
- const proxyEndpointFlags = collectRepeated(rest, "--proxy-endpoint");
221
- if (proxyEndpointFlags.error) { io.stderr(`${proxyEndpointFlags.error}\n`); return USAGE_ERR; }
222
- rest = proxyEndpointFlags.remaining;
223
-
224
- const proxyAuthFlags = collectRepeatedKv(rest, "--proxy-auth");
225
- if (proxyAuthFlags.error) { io.stderr(`${proxyAuthFlags.error}\n`); return USAGE_ERR; }
226
- rest = proxyAuthFlags.remaining;
227
-
228
- const positional = rest.filter((a) => !a.startsWith("--"));
229
- const unknownFlags = rest.filter((a) => a.startsWith("--"));
230
- if (unknownFlags.length > 0) {
231
- io.stderr(`unknown flag: ${unknownFlags[0]}\n`);
232
- return USAGE_ERR;
233
- }
234
- if (positional.length > 0) {
235
- io.stderr(`aex run takes no positional arguments (got: ${positional.join(" ")})\n`);
236
- return USAGE_ERR;
237
- }
238
-
239
- // ---------------- Resolve run config (config XOR flat flags) ----------------
240
- let runConfig: RunRequestConfig;
241
- let mcpHeadersFromConfig: Map<string, Record<string, string>> = new Map();
242
- if (config.value) {
243
- if (modelFlag.value || systemFlag.value || promptFlags.values.length || Object.keys(mcpFlags.entries).length || Object.keys(metadataFlags.entries).length) {
244
- io.stderr("--config cannot be combined with --model/--system/--prompt/--mcp/--metadata\n");
245
- return USAGE_ERR;
246
- }
247
- try {
248
- const absPath = resolvePath(io.cwd(), config.value);
249
- const text = await io.readFile(absPath);
250
- const raw = JSON.parse(text) as unknown;
251
- const { mcpHeaders, normalised } = stripMcpHeadersForParsing(raw);
252
- mcpHeadersFromConfig = mcpHeaders;
253
- runConfig = parseRunRequestConfig(normalised);
254
- } catch (err) {
255
- io.stderr(`failed to load --config: ${(err as Error).message}\n`);
256
- return USAGE_ERR;
257
- }
258
- } else {
259
- if (!modelFlag.value) {
260
- io.stderr("--model is required when --config is not provided\n");
261
- return USAGE_ERR;
262
- }
263
- if (!(RUN_MODELS as readonly string[]).includes(modelFlag.value)) {
264
- io.stderr(`--model must be one of: ${RUN_MODELS.join(", ")} (got: ${modelFlag.value})\n`);
265
- return USAGE_ERR;
266
- }
267
- if (promptFlags.values.length === 0) {
268
- io.stderr("--prompt is required (repeatable)\n");
269
- return USAGE_ERR;
270
- }
271
- let resolvedPrompt: string[];
272
- try {
273
- resolvedPrompt = await Promise.all(promptFlags.values.map((v) => readMaybeFile(io, v)));
274
- } catch (err) {
275
- io.stderr(`failed to read --prompt file: ${(err as Error).message}\n`);
276
- return USAGE_ERR;
277
- }
278
- let resolvedSystem: string | undefined;
279
- if (systemFlag.value !== null) {
280
- try {
281
- resolvedSystem = await readMaybeFile(io, systemFlag.value);
282
- } catch (err) {
283
- io.stderr(`failed to read --system file: ${(err as Error).message}\n`);
284
- return USAGE_ERR;
285
- }
286
- }
287
-
288
- const mcpRefs: McpServerRef[] = [];
289
- for (const [name, url] of Object.entries(mcpFlags.entries)) {
290
- mcpRefs.push({ name, url });
291
- }
292
-
293
- runConfig = {
294
- model: modelFlag.value as RunModel,
295
- ...(resolvedSystem ? { system: resolvedSystem } : {}),
296
- prompt: resolvedPrompt,
297
- ...(mcpRefs.length > 0 ? { mcpServers: mcpRefs } : {}),
298
- ...(Object.keys(metadataFlags.entries).length > 0
299
- ? { metadata: { ...metadataFlags.entries } }
300
- : {})
301
- };
302
- }
303
-
304
- // ---------------- Resolve MCP secrets (--mcp-auth + config headers) ----------------
305
- const mcpServersForSubmission: McpServerRef[] = [...(runConfig.mcpServers ?? [])];
306
- const mcpServerSecrets: PlatformMcpServerSecret[] = [];
307
- const mcpHeaderBag = new Map<string, Record<string, string>>(mcpHeadersFromConfig);
308
- for (const [name, headerSpec] of mcpAuthFlags.entries) {
309
- const colon = headerSpec.indexOf(":");
310
- if (colon <= 0 || colon >= headerSpec.length - 1) {
311
- io.stderr(`--mcp-auth ${name}: expected 'HeaderName:Value' (got: ${headerSpec})\n`);
312
- return USAGE_ERR;
313
- }
314
- const headerName = headerSpec.slice(0, colon).trim();
315
- const headerValue = headerSpec.slice(colon + 1).trim();
316
- if (!headerName || !headerValue) {
317
- io.stderr(`--mcp-auth ${name}: header name and value must be non-empty\n`);
318
- return USAGE_ERR;
319
- }
320
- const existing = mcpHeaderBag.get(name) ?? {};
321
- if (Object.prototype.hasOwnProperty.call(existing, headerName)) {
322
- io.stderr(
323
- `--mcp-auth ${name}: duplicate header "${headerName}" — ` +
324
- `each header may be set only once per server\n`
325
- );
326
- return USAGE_ERR;
327
- }
328
- existing[headerName] = headerValue;
329
- mcpHeaderBag.set(name, existing);
330
- }
331
-
332
- for (const ref of mcpServersForSubmission) {
333
- const headers = mcpHeaderBag.get(ref.name);
334
- if (headers && Object.keys(headers).length > 0) {
335
- mcpServerSecrets.push({ name: ref.name, url: ref.url, headers });
336
- }
337
- }
338
- // Reject auth flags that reference an unknown MCP server.
339
- for (const name of mcpHeaderBag.keys()) {
340
- if (!mcpServersForSubmission.some((m) => m.name === name)) {
341
- io.stderr(`--mcp-auth ${name}: no matching --mcp / mcpServers entry declared\n`);
342
- return USAGE_ERR;
343
- }
344
- }
345
-
346
- // ---------------- Proxy endpoints + auth ----------------
347
- let proxyEndpoints: readonly PlatformProxyEndpoint[] = runConfig.proxyEndpoints ?? [];
348
- if (proxyEndpointFlags.values.length > 0) {
349
- try {
350
- proxyEndpoints = proxyEndpointFlags.values.map((raw, i) =>
351
- parseJsonOrThrow<PlatformProxyEndpoint>(raw, `--proxy-endpoint[${i}]`)
352
- );
353
- } catch (err) {
354
- io.stderr(`${(err as Error).message}\n`);
355
- return USAGE_ERR;
356
- }
357
- }
358
- const proxyAuth: PlatformProxyEndpointAuth[] = [];
359
- for (const [name, spec] of Object.entries(proxyAuthFlags.entries)) {
360
- const parsed = parseProxyAuth(spec);
361
- if (!parsed.ok) {
362
- io.stderr(`--proxy-auth ${name}: ${parsed.reason}\n`);
363
- return USAGE_ERR;
364
- }
365
- proxyAuth.push({ name, value: parsed.value });
366
- }
367
- if (proxyEndpoints.length > 0) {
368
- try {
369
- validateProxyAuth(proxyEndpoints, proxyAuth);
370
- } catch (err) {
371
- io.stderr(`proxy auth validation failed: ${(err as Error).message}\n`);
372
- return USAGE_ERR;
373
- }
374
- }
375
-
376
- // ---------------- Build submission ----------------
377
- const promptArray = Array.isArray(runConfig.prompt) ? [...runConfig.prompt] : [runConfig.prompt];
378
- const skills: SkillRef[] = runConfig.skills ? [...runConfig.skills] : [];
379
- const submission: PlatformSubmission = {
380
- model: runConfig.model,
381
- ...(runConfig.system ? { system: runConfig.system } : {}),
382
- prompt: promptArray,
383
- skills,
384
- agentsMd: [],
385
- files: [],
386
- mcpServers: mcpServersForSubmission,
387
- tools: [],
388
- ...(runConfig.environment ? { environment: runConfig.environment } : {}),
389
- ...(runConfig.metadata ? { metadata: runConfig.metadata } : {})
390
- };
391
-
392
- const secrets: PlatformInlineSecrets = {
393
- apiKey: providerKeyValues[provider] as string,
394
- ...(mcpServerSecrets.length > 0 ? { mcpServers: mcpServerSecrets } : {}),
395
- ...(proxyAuth.length > 0 ? { proxyEndpointAuth: proxyAuth } : {})
396
- };
397
-
398
- const request: PlatformRunSubmissionInput = {
399
- idempotencyKey: idempotency.value ?? generateIdempotencyKey(),
400
- provider,
401
- ...(runtime ? { runtime } : {}),
402
- submission,
403
- secrets,
404
- ...(runtimeSizeFlag.value
405
- ? { runtimeSize: runtimeSizeFlag.value as RuntimeSize }
406
- : runConfig.runtimeSize
407
- ? { runtimeSize: runConfig.runtimeSize }
408
- : {}),
409
- ...(runTimeoutFlag.value
410
- ? { timeout: runTimeoutFlag.value }
411
- : runConfig.timeout
412
- ? { timeout: runConfig.timeout }
413
- : {}),
414
- ...(runConfig.postHook ? { postHook: runConfig.postHook } : {}),
415
- ...(proxyEndpoints.length > 0 ? { proxyEndpoints } : {})
416
- };
417
-
418
- const http = makeHttpClient(io, common.flags);
419
- let run;
420
- try {
421
- run = await operations.submitRun(http, request);
422
- } catch (err) {
423
- return emitJsonError(io, "submit_failed", (err as Error).message ?? "submission failed");
424
- }
425
-
426
- io.stdout(JSON.stringify(run) + "\n");
427
- if (!follow.present) return SUCCESS;
428
-
429
- let emittedEventCount = 0;
430
- let currentStatus = run.status;
431
- const deadline = followTimeoutMs === null ? Number.POSITIVE_INFINITY : Date.now() + followTimeoutMs;
432
- while (!TERMINAL_STATUSES.has(currentStatus)) {
433
- await sleep(2000);
434
- try {
435
- const events = await operations.listRunEvents(http, run.id);
436
- for (let i = emittedEventCount; i < events.length; i++) {
437
- io.stdout(JSON.stringify(events[i]) + "\n");
438
- }
439
- emittedEventCount = events.length;
440
- } catch (err) {
441
- io.stderr(`(transient) event poll failed: ${(err as Error).message}\n`);
442
- }
443
- try {
444
- const updated = await operations.getRun(http, run.id);
445
- currentStatus = updated.status;
446
- } catch (err) {
447
- io.stderr(`(transient) status poll failed: ${(err as Error).message}\n`);
448
- }
449
- if (!TERMINAL_STATUSES.has(currentStatus) && Date.now() >= deadline) {
450
- emitJsonError(io, "run_follow_timeout", `timed out after ${followTimeoutMs}ms following run`, { runId: run.id });
451
- return TIMEOUT_ERR;
452
- }
453
- }
454
- try {
455
- const final = await operations.getRun(http, run.id);
456
- io.stdout(JSON.stringify(final) + "\n");
457
- return final.status === "succeeded" ? SUCCESS : RUNTIME_ERR;
458
- } catch (err) {
459
- io.stderr(`final status fetch failed: ${(err as Error).message}\n`);
460
- return RUNTIME_ERR;
461
- }
462
- }
463
-
464
- /* ---------- helpers ---------- */
465
-
466
- interface ProxyAuthOk { ok: true; value: PlatformProxyAuthValue; }
467
- interface ProxyAuthErr { ok: false; reason: string; }
468
-
469
- function parseProxyAuth(spec: string): ProxyAuthOk | ProxyAuthErr {
470
- const idx = spec.indexOf(":");
471
- if (idx <= 0) {
472
- return { ok: false, reason: `expected '<type>:<value>' (got: ${spec})` };
473
- }
474
- const type = spec.slice(0, idx);
475
- const restValue = spec.slice(idx + 1);
476
- switch (type) {
477
- case "bearer":
478
- if (!restValue) return { ok: false, reason: "bearer requires a token value" };
479
- return { ok: true, value: { type: "bearer", token: restValue } };
480
- case "header":
481
- if (!restValue) return { ok: false, reason: "header requires a value" };
482
- return { ok: true, value: { type: "header", value: restValue } };
483
- case "query":
484
- if (!restValue) return { ok: false, reason: "query requires a value" };
485
- return { ok: true, value: { type: "query", value: restValue } };
486
- case "basic": {
487
- const sep = restValue.indexOf(":");
488
- if (sep <= 0 || sep >= restValue.length - 1) {
489
- return { ok: false, reason: "basic requires <username>:<password>" };
490
- }
491
- return {
492
- ok: true,
493
- value: { type: "basic", username: restValue.slice(0, sep), password: restValue.slice(sep + 1) }
494
- };
495
- }
496
- default:
497
- return { ok: false, reason: `unknown auth type '${type}' (expected bearer|basic|header|query)` };
498
- }
499
- }
500
-
501
- function parseJsonOrThrow<T>(raw: string, label: string): T {
502
- try {
503
- return JSON.parse(raw) as T;
504
- } catch (err) {
505
- throw new Error(`${label} is not valid JSON: ${(err as Error).message}`);
506
- }
507
- }
508
-
509
- /**
510
- * Strip `headers` from each `mcpServers[i]` so the value can pass
511
- * `parseRunRequestConfig` (which forbids headers on the non-secret submission
512
- * shape). Returns the stripped object plus a name->headers map the
513
- * caller can fold into `secrets.mcpServers`.
514
- */
515
- function stripMcpHeadersForParsing(input: unknown): {
516
- readonly normalised: unknown;
517
- readonly mcpHeaders: Map<string, Record<string, string>>;
518
- } {
519
- const mcpHeaders = new Map<string, Record<string, string>>();
520
- if (input === null || typeof input !== "object" || Array.isArray(input)) {
521
- return { normalised: input, mcpHeaders };
522
- }
523
- const record = input as Record<string, unknown>;
524
- if (!Array.isArray(record.mcpServers)) {
525
- return { normalised: input, mcpHeaders };
526
- }
527
- const stripped = record.mcpServers.map((entry, i) => {
528
- if (!entry || typeof entry !== "object") return entry;
529
- const r = entry as Record<string, unknown>;
530
- if (r.headers && typeof r.headers === "object" && !Array.isArray(r.headers)) {
531
- const name = typeof r.name === "string" ? r.name : `__index_${i}__`;
532
- const headerObj: Record<string, string> = {};
533
- for (const [k, v] of Object.entries(r.headers as Record<string, unknown>)) {
534
- if (typeof v !== "string") {
535
- throw new Error(`mcpServers[${i}].headers["${k}"] must be a string`);
536
- }
537
- headerObj[k] = v;
538
- }
539
- mcpHeaders.set(name, headerObj);
540
- const { headers: _omit, ...rest } = r;
541
- void _omit;
542
- return rest;
543
- }
544
- return r;
545
- });
546
- return { normalised: { ...record, mcpServers: stripped }, mcpHeaders };
547
- }
548
-
549
- /**
550
- * Resolve a `--system` / `--prompt` argument that may be either a
551
- * literal string or a file reference. Conventions:
552
- *
553
- * plain text -> the text itself
554
- * "@path/to/file" -> contents of the file (relative to cwd)
555
- * "@@literal" -> the literal "@literal" (escape for content
556
- * that genuinely starts with `@`)
557
- */
558
- async function readMaybeFile(io: CliIO, value: string): Promise<string> {
559
- if (value.startsWith("@@")) {
560
- return value.slice(1);
561
- }
562
- if (value.startsWith("@")) {
563
- const path = resolvePath(io.cwd(), value.slice(1));
564
- return io.readFile(path);
565
- }
566
- return value;
567
- }
568
-
569
- function generateIdempotencyKey(): string {
570
- const c = (globalThis as { crypto?: { randomUUID?: () => string } }).crypto;
571
- if (c?.randomUUID) return c.randomUUID();
572
- return `idem-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
573
- }
574
-
575
- function sleep(ms: number): Promise<void> {
576
- return new Promise((resolve) => setTimeout(resolve, ms));
577
- }
578
-