@assethub/cli 0.1.5 → 0.1.7

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 (50) hide show
  1. package/README.md +349 -5
  2. package/dist/autopilot.d.ts +13 -0
  3. package/dist/autopilot.d.ts.map +1 -0
  4. package/dist/autopilot.js +158 -0
  5. package/dist/canvas.d.ts +27 -0
  6. package/dist/canvas.d.ts.map +1 -0
  7. package/dist/canvas.js +82 -0
  8. package/dist/canvasAssetImport.d.ts +45 -0
  9. package/dist/canvasAssetImport.d.ts.map +1 -0
  10. package/dist/canvasAssetImport.js +154 -0
  11. package/dist/canvasComparison.d.ts +21 -0
  12. package/dist/canvasComparison.d.ts.map +1 -0
  13. package/dist/canvasComparison.js +58 -0
  14. package/dist/execution.d.ts +175 -0
  15. package/dist/execution.d.ts.map +1 -0
  16. package/dist/execution.js +234 -0
  17. package/dist/index.d.ts +94 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +1641 -192
  20. package/dist/projectSources.d.ts +65 -0
  21. package/dist/projectSources.d.ts.map +1 -0
  22. package/dist/projectSources.js +369 -0
  23. package/dist/projectSourcesWorkbook.d.ts +3 -0
  24. package/dist/projectSourcesWorkbook.d.ts.map +1 -0
  25. package/dist/projectSourcesWorkbook.js +143 -0
  26. package/dist/runsUpload/artifactGraphModel.d.ts +48 -0
  27. package/dist/runsUpload/artifactGraphModel.d.ts.map +1 -0
  28. package/dist/runsUpload/artifactGraphModel.js +127 -0
  29. package/dist/runsUpload/blobRefs.d.ts +13 -0
  30. package/dist/runsUpload/blobRefs.d.ts.map +1 -0
  31. package/dist/runsUpload/blobRefs.js +73 -0
  32. package/dist/runsUpload/buildRunUpload.d.ts +63 -0
  33. package/dist/runsUpload/buildRunUpload.d.ts.map +1 -0
  34. package/dist/runsUpload/buildRunUpload.js +428 -0
  35. package/dist/runsUpload/canonicalJson.d.ts +5 -0
  36. package/dist/runsUpload/canonicalJson.d.ts.map +1 -0
  37. package/dist/runsUpload/canonicalJson.js +108 -0
  38. package/dist/runsUpload/controlEndpoints.d.ts +63 -0
  39. package/dist/runsUpload/controlEndpoints.d.ts.map +1 -0
  40. package/dist/runsUpload/controlEndpoints.js +64 -0
  41. package/dist/runsUpload/graphFolder.d.ts +25 -0
  42. package/dist/runsUpload/graphFolder.d.ts.map +1 -0
  43. package/dist/runsUpload/graphFolder.js +147 -0
  44. package/dist/runsUpload/runUploadError.d.ts +23 -0
  45. package/dist/runsUpload/runUploadError.d.ts.map +1 -0
  46. package/dist/runsUpload/runUploadError.js +24 -0
  47. package/dist/runsUpload/uploadRun.d.ts +43 -0
  48. package/dist/runsUpload/uploadRun.d.ts.map +1 -0
  49. package/dist/runsUpload/uploadRun.js +217 -0
  50. package/package.json +11 -8
package/dist/index.js CHANGED
@@ -1,32 +1,34 @@
1
1
  #!/usr/bin/env node
2
- import { execFile } from 'node:child_process';
3
- import { chmod, mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
2
+ import { setTimeout as delay } from 'node:timers/promises';
3
+ import { ingestProjectSources } from './projectSources.js';
4
+ import { writeCanvasComparison } from './canvasComparison.js';
5
+ import { importCanvasAssetWithState } from './canvasAssetImport.js';
6
+ import { execFile, spawn } from 'node:child_process';
7
+ import { createHash, randomUUID } from 'node:crypto';
8
+ import { realpathSync } from 'node:fs';
9
+ import { chmod, mkdir, readFile, rename, unlink, writeFile, } from 'node:fs/promises';
4
10
  import { homedir, tmpdir } from 'node:os';
5
- import { basename, dirname, extname, join } from 'node:path';
11
+ import { basename, dirname, extname, join, resolve } from 'node:path';
6
12
  import { argv, env, exit, stdin, stderr, stdout } from 'node:process';
13
+ import { fileURLToPath } from 'node:url';
7
14
  import { promisify } from 'node:util';
8
15
  import { AssetHubApiError, createAssetHubClient, } from '@assethub/api-client';
16
+ import { resolveCanvasSelection, saveCanvasSelection } from './canvas.js';
17
+ import { activeExecution, CliExecutionError, executeRecorded, hasRecordedOperation, openExecutionSession, resumeRecorded, waitForExecution, } from './execution.js';
18
+ import { buildRunUploadPlan, formatBytes } from './runsUpload/buildRunUpload.js';
19
+ import { readGraphFolder } from './runsUpload/graphFolder.js';
20
+ import { uploadRun } from './runsUpload/uploadRun.js';
9
21
  const defaultBaseUrl = 'https://app.assethub.io';
10
- const defaultImageModelId = 'imageGen.nanoBanana.pro.openrouter';
22
+ // Mirrors PUBLIC_IMAGE_GENERATE_DEFAULT_MODEL_ID in apps/frontend
23
+ // (src/feature/imageGen/util/publicImageGenDefaultModel.ts). This package does
24
+ // not depend on the app, so the value is duplicated: keep the two in step so
25
+ // `assethub image generate` without --model matches the API's own default.
26
+ const defaultImageModelId = 'imageGen.nanoBanana2.openrouter';
11
27
  const defaultMeshModelId = 'meshGen.hunyuan31';
12
28
  const defaultProfileName = 'default';
13
29
  const defaultPartExtractorName = 'V1.5';
30
+ const artifactGraphPartExtractorApiValue = 'ah_agent_graph_v3_6_1';
14
31
  const partExtractorOptions = [
15
- {
16
- publicName: 'Part Extractor V3',
17
- apiValue: 'ah_part_extractor_v3',
18
- aliases: ['part extractor v3', 'v3'],
19
- },
20
- {
21
- publicName: 'V1',
22
- apiValue: 'ah_part_extractor_v5',
23
- aliases: ['v1', '1'],
24
- },
25
- {
26
- publicName: 'V1.5 alpha',
27
- apiValue: 'ah_part_extractor_v55',
28
- aliases: ['v1.5 alpha', 'v1.5-alpha', '1.5 alpha', '1.5-alpha'],
29
- },
30
32
  {
31
33
  publicName: 'V1.5',
32
34
  apiValue: 'ah_part_extractor_v65',
@@ -42,6 +44,11 @@ const partExtractorOptions = [
42
44
  apiValue: 'ah_part_extractor_v21_alpha',
43
45
  aliases: ['v2.1 alpha', 'v2.1-alpha', '2.1 alpha'],
44
46
  },
47
+ {
48
+ publicName: 'V3.6.1',
49
+ apiValue: artifactGraphPartExtractorApiValue,
50
+ aliases: ['v3.6.1', '3.6.1', 'v3.6.1 primary images first'],
51
+ },
45
52
  ];
46
53
  const partExtractorPublicNames = partExtractorOptions
47
54
  .map(option => option.publicName)
@@ -49,37 +56,78 @@ const partExtractorPublicNames = partExtractorOptions
49
56
  const normalizePartExtractorName = (value) => value.trim().toLowerCase().replace(/\s+/g, ' ');
50
57
  const partExtractorByInput = new Map(partExtractorOptions.flatMap(option => [
51
58
  [normalizePartExtractorName(option.publicName), option],
52
- ...option.aliases.map(alias => [
53
- normalizePartExtractorName(alias),
54
- option,
55
- ]),
59
+ ...option.aliases.map(alias => [normalizePartExtractorName(alias), option]),
56
60
  ]));
57
61
  const partExtractorPublicNameByApiValue = new Map(partExtractorOptions.map(option => [option.apiValue, option.publicName]));
58
62
  const isInternalPartExtractorId = (value) => value.trim().includes('_');
59
63
  const execFileAsync = promisify(execFile);
60
- const usage = `AssetHub CLI
64
+ export const usage = `AssetHub CLI
65
+
66
+ Internal preview: canvas/context/layout and moodboards require workspace feature access.
61
67
 
62
68
  Usage:
63
69
  assethub auth login --api-key <key> [--base-url <url>] [--profile <name>]
64
70
  assethub auth login --api-key-stdin [--base-url <url>] [--profile <name>]
65
71
  assethub auth status [--profile <name>]
66
72
  assethub auth logout [--profile <name>]
73
+ assethub workspace prepare|sync|analyze|verify --internal --manifest <json> --state <json>
74
+ assethub workspace self-check --internal
75
+ assethub workspace context put --internal --org <uuid> --actor <uuid> --canvas <id> --file <json> [--if-version <n>] [--order <uuid>]
76
+ assethub workspace context get --internal --org <uuid> --actor <uuid> --canvas <id> [--out <json>] [--order <uuid>]
77
+ assethub project ingest <source-dir> --out-dir <directory> [--exclude-dir <relative-path>...]
78
+ assethub language text|vision --input-json @<json> --operation-id <uuid> [--out <json>]
79
+ assethub canvas import --canvas <id> --file <image> [--name <name>]
80
+ assethub canvas context get --canvas <id> [--out <json>]
81
+ assethub canvas context put --canvas <id> --file <json> [--if-version <n>]
82
+ assethub canvas layout --canvas <id> --input-json @<json>
83
+ assethub canvas compare --canvas <id> --source-id <asset-id> --asset-id <asset-id> --out <html>
84
+ assethub moodboard create --input-json @<json> [--operation-id <uuid>]
85
+ assethub moodboard list|get|update|archive [<board-id>] [--input-json @<json>]
86
+ assethub moodboard analyze <board-id> [--revision <uuid>] [--wait]
87
+ assethub image generate --canvas <id> --context <id> [--context-version <n>] [--context-source <key>] [--moodboard-revision <uuid>] --prompt <text> --wait
67
88
  assethub models list [--domain <domain>] [--ids-only] [--limit <n>]
68
89
  assethub models get <model-id>
69
90
  assethub files import <url> --media-type image|mesh
70
91
  assethub files upload <file> --media-type image|mesh
71
92
  assethub source create (--file <path> | --source-url <url> | --source-id <id> | --stdin | --stdin-base64 | --stdin-data-uri | --stdin-json | --source-json <json|@file|@-> | --data-uri <uri> | --clipboard) --media-type image|mesh [--file-name <name>] [--content-type <type>]
72
- assethub image generate --prompt <text> [--file <path> | --source-url <url> | --stdin | --stdin-base64 | --stdin-data-uri | --stdin-json | --source-json <json|@file|@-> | --data-uri <uri> | --clipboard] [--file-name <name>] [--content-type <type>] [--model-id <id>] [--batch-size <n>] [--wait] [--download --out-dir <dir>] [--api-version v1|v2]
93
+ assethub capabilities
94
+ assethub canvas create [--name <name>] [--operation-id <uuid>]
95
+ assethub canvas list [--cursor <cursor>] [--limit <n>]
96
+ assethub canvas get|use|open [<id>]
97
+ assethub runs list [--canvas <id>] [--cursor <cursor>] [--limit <n>]
98
+ assethub runs get|watch <run-id> [--timeout-ms <n>]
99
+ assethub runs resume <operation-id> [--wait]
100
+ assethub graph show|export [--canvas <id>] [--artifact <node-id>] [--out <path>] [--allow-truncated]
101
+ assethub graph lineage --artifact <node-id> [--canvas <id>] [--direction ancestors|descendants|both] [--depth <n>]
102
+ assethub evaluate list
103
+ assethub evaluations submit --artifact <asset-id> --report <json|file|@file|@-> [--agent <name>] [--canvas <id>] [--reference <asset-id>...] [--run <run-id>] [--operation-id <uuid>] [--require-pass]
104
+ assethub evaluations list [--canvas <id>] [--artifact <asset-id>]
105
+ assethub evaluations get <evaluation-id>
106
+ assethub image generate --input-json <json|@file|@-> [--canvas <id>] [--operation-id <uuid>] [--agent <name>] [--derived-from-run <run-id>] [--from-node <graphId/nodeId> --graph-revision <rev>] [--wait]
107
+ assethub mesh generate --input-json <json|@file|@-> [--canvas <id>] [--operation-id <uuid>] [--agent <name>] [--derived-from-run <run-id>] [--from-node <graphId/nodeId> --graph-revision <rev>] [--wait]
108
+ assethub image generate --prompt <text> [--file <path> | --source-url <url> | --stdin | --stdin-base64 | --stdin-data-uri | --stdin-json | --source-json <json|@file|@-> | --data-uri <uri> | --clipboard] [--file-name <name>] [--content-type <type>] [--model-id <id>] [--batch-size <n>] [--wait] [--download --out-dir <dir>] [--canvas <id>] [--operation-id <uuid>] [--api-version v2]
73
109
  assethub image generate --prompt <text> --source-resource-id <id> --api-version v2 [--model-id <id>] [--batch-size <n>] [--wait]
74
110
  assethub mesh generate (--file <path> | --source-url <url> | --source-resource-id <id> | --file-ref-json <json> | --stdin | --stdin-base64 | --stdin-data-uri | --stdin-json | --source-json <json|@file|@-> | --data-uri <uri>) [--file-name <name>] [--content-type <type>] [--model-id <id>] [--name <name>] [--wait] [--download --out-dir <dir>]
75
111
  assethub mesh retopo (--source-resource-id <id> | --source-url <url> | --file-ref-json <json> | --stdin | --stdin-base64 | --stdin-data-uri | --stdin-json | --source-json <json|@file|@-> | --data-uri <uri>) --model-id <id> [--file-name <name>] [--content-type <type>] [--wait] [--download --out-dir <dir>]
112
+ assethub rig check (--file <path> | --source-url <url> | --source-resource-id <id> | --file-ref-json <json> | --stdin | --stdin-base64 | --stdin-data-uri | --stdin-json | --source-json <json|@file|@-> | --data-uri <uri>) [--name <name>] [--wait] [--download --out-dir <dir>]
113
+ assethub rig create (--file <path> | --source-url <url> | --source-resource-id <id> | --file-ref-json <json> | --stdin | --stdin-base64 | --stdin-data-uri | --stdin-json | --source-json <json|@file|@-> | --data-uri <uri>) [--model v1.0-20240301|v2.5-20260210] [--rig-type biped|quadruped|hexapod|octopod|avian|serpentine|aquatic] [--spec tripo|mixamo] [--out-format glb|fbx] [--name <name>] [--wait] [--download --out-dir <dir>]
114
+ assethub animate presets [--model v1.0-20240301|v2.5-20260210] [--rig-type <type>]
115
+ assethub animate retarget --resource-id <id> --animation <preset-id> [--animation <preset-id>... up to 5 total] [--out-format glb|fbx] [--bake-animation true|false] [--export-with-geometry true|false] [--animate-in-place true|false] [--name <name>] [--wait] [--download --out-dir <dir>]
76
116
  assethub jobs get <job-id> [--download --out-dir <dir>]
77
117
  assethub jobs watch <job-id> [--interval-ms <ms>] [--timeout-ms <ms>] [--download --out-dir <dir>]
78
118
  assethub production analyze (--file <path> | --source-url <url> | --source-id <id> | --image-url <url> | --file-ref-json <json> | --stdin | --stdin-base64 | --stdin-data-uri | --stdin-json | --source-json <json|@file|@-> | --data-uri <uri> | --clipboard) [--file-name <name>] [--content-type <type>] [--part-extractor <name>] [--name <name>] [--wait] [--download --out-dir <dir>]
119
+ assethub production agents
120
+ assethub production automation --input-json <json|@file|@-> [--canvas <id>] [--operation-id <uuid>] [--wait]
79
121
  assethub production status <order-id> [--wait] [--download --out-dir <dir>]
80
122
  assethub production execute --order-id <id> --mission-id <id> --task-id <id> [--task-id <id>...] [--part-extractor <name>] [--part-extraction-mode fast|high_quality] [--wait] [--download --out-dir <dir>]
123
+ assethub production run --order-id <id> --mission-id <id> [--run-mode full_auto|approval] [--task-id <id>...] [--part-extractor <name>] [--part-extraction-mode fast|high_quality] [--allowed-model-id <id>...] [--max-iterations <n>] [--max-cost-credits <n>] [--wait] [--download --out-dir <dir>]
124
+ assethub production watch <order-id> [--interval-ms <ms>] [--timeout-ms <ms>] [--download --out-dir <dir>]
125
+ assethub production intervene <order-id> (--exclude <target-id> | --include <target-id> | --add-part <name> | --rename <target-id>=<name> | --reject <target-id>[=<reason>] | --regenerate <target-id>=<mode> | --set-param <key>=<value> | --ops-json <json|@file|@->)... [--idempotency-key <key>]
126
+ assethub production interventions <order-id>
127
+ assethub runs upload <path> [--graph-id <id>] [--stream-id <id>] [--rev <n>] [--description <text>] [--tag <tag>...] [--skip-register] [--dry-run]
81
128
  assethub parts split (--file <path> | --source-url <url> | --source-id <id> | --image-url <url> | --file-ref-json <json> | --stdin | --stdin-base64 | --stdin-data-uri | --stdin-json | --source-json <json|@file|@-> | --data-uri <uri> | --clipboard | --order-id <id>) [--file-name <name>] [--content-type <type>] [--part-extractor <name>] [--wait] [--task-id <id>...] [--mission-id <id>] [--all-ready] [--download --out-dir <dir>]
82
129
  assethub parts compare (--file <path> | --source-url <url> | --source-id <id> | --image-url <url> | --file-ref-json <json> | --stdin | --stdin-base64 | --stdin-data-uri | --stdin-json | --source-json <json|@file|@-> | --data-uri <uri> | --clipboard) [--file-name <name>] [--content-type <type>] [--preprocess-prompt <text>] [--preprocess-model-id <id>] [--fail-on-preprocess-error] [--part-extractor <name>] [--wait] [--task-id <id>...] [--mission-id <id>] [--all-ready] [--download --out-dir <dir>]
130
+ assethub autopilot [<image>] --demo [--yolo] [--json] [--max-regen <n>] [--stall-rounds <n>] [--tie-epsilon <x>] [--multiview 2-view|4-view|6-view] [--ab-mode serial|cross] [--credit-budget <n>]
83
131
 
84
132
  Global options:
85
133
  --api-key <key> Overrides saved auth and ASSETHUB_API_KEY.
@@ -101,7 +149,16 @@ Examples:
101
149
  cat ./input.png | assethub parts split --stdin --file-name input.png --part-extractor "V1.5" --wait
102
150
  assethub image generate --prompt "stylized prop concept" --wait
103
151
  assethub mesh generate --file ./input.png --wait
152
+ assethub animate presets --rig-type biped
153
+ assethub rig create --source-resource-id mesh_x --rig-type biped --wait
154
+ assethub animate retarget --resource-id rig_x --animation preset:idle --wait
104
155
  assethub production analyze --file ./input.png --part-extractor "V1.5" --wait
156
+ assethub production run --order-id ord_x --mission-id ms_x --run-mode full_auto --max-cost-credits 200 --wait
157
+ assethub production watch ord_x
158
+ assethub production intervene ord_x --exclude 7 --rename 3="left wing" --idempotency-key review-2026-07-29
159
+ assethub production interventions ord_x
160
+ assethub runs upload ./out/pluffy-run-2026-07-30 --dry-run
161
+ assethub runs upload ./out/pluffy-run-2026-07-30 --tag pluffy --description "V3 humanoid, 18 parts"
105
162
  assethub parts split --file ./input.png --part-extractor "V1.5" --wait --all-ready
106
163
  assethub parts compare --file ./input.png --preprocess-prompt "clean white background, centered product photo" --part-extractor "V1.5" --wait --all-ready
107
164
  `;
@@ -127,7 +184,7 @@ const parseArgs = (args) => {
127
184
  const value = inlineValue != null
128
185
  ? inlineValue
129
186
  : next != null && !next.startsWith('--')
130
- ? (index += 1, next)
187
+ ? ((index += 1), next)
131
188
  : true;
132
189
  const previous = flags[key];
133
190
  if (previous == null) {
@@ -211,11 +268,15 @@ const sanitizeProductionOutput = (value) => {
211
268
  if (value == null || typeof value !== 'object') {
212
269
  return value;
213
270
  }
214
- return Object.fromEntries(Object.entries(value).map(([key, entry]) => [
271
+ return Object.fromEntries(Object.entries(value)
272
+ .filter(([key]) => key !== 'publicAccessToken')
273
+ .map(([key, entry]) => [
215
274
  key,
216
- key === 'agentVersion' && typeof entry === 'string'
217
- ? publicPartExtractorName(entry)
218
- : sanitizeProductionOutput(entry),
275
+ key === 'execution'
276
+ ? entry
277
+ : key === 'agentVersion' && typeof entry === 'string'
278
+ ? publicPartExtractorName(entry)
279
+ : sanitizeProductionOutput(entry),
219
280
  ]));
220
281
  };
221
282
  const parsePositiveIntegerFlag = (flags, name, fallback) => {
@@ -242,6 +303,24 @@ const parseBooleanFlag = (flags, name) => {
242
303
  }
243
304
  throw new Error(`--${name} must be true or false`);
244
305
  };
306
+ /**
307
+ * An optional flag whose value must come from a fixed set.
308
+ *
309
+ * The server stays the authority on what it accepts; this only turns a typo
310
+ * into an immediate local error that names the valid values, instead of a
311
+ * round trip that comes back as an opaque 400. Same contract as
312
+ * `parseBooleanFlag`: absent means `undefined`, present-but-wrong throws.
313
+ */
314
+ const parseEnumFlag = (flags, name, allowed) => {
315
+ const raw = getFlag(flags, name);
316
+ if (raw == null) {
317
+ return undefined;
318
+ }
319
+ if (allowed.includes(raw)) {
320
+ return raw;
321
+ }
322
+ throw new Error(`--${name} must be one of: ${allowed.join(', ')}`);
323
+ };
245
324
  const parseMediaType = (value) => {
246
325
  if (value === 'image' || value === 'mesh') {
247
326
  return value;
@@ -335,15 +414,18 @@ const resolveAuth = async (flags) => {
335
414
  return {
336
415
  apiKey: storedProfile.apiKey,
337
416
  baseUrl: baseUrlFromFlags ?? env.ASSETHUB_API_BASE_URL ?? storedProfile.baseUrl,
338
- profile: config.profiles?.[profile] != null ? profile : (fallbackProfileName ?? profile),
417
+ profile: config.profiles?.[profile] != null
418
+ ? profile
419
+ : (fallbackProfileName ?? profile),
339
420
  source: 'profile',
340
421
  };
341
422
  }
342
423
  throw new Error('Missing API key. Run "assethub auth login --api-key-stdin", set ASSETHUB_API_KEY, or pass --api-key.');
343
424
  };
344
- const createContext = async (flags) => {
425
+ const createContext = async (flags, args) => {
345
426
  const auth = await resolveAuth(flags);
346
427
  return {
428
+ args,
347
429
  auth,
348
430
  flags,
349
431
  client: createAssetHubClient({
@@ -362,8 +444,7 @@ const isBlobLocation = (value) => {
362
444
  typeof value.path === 'string') {
363
445
  return true;
364
446
  }
365
- if (value.type === 'signedSupabase' &&
366
- typeof value.signedUrl === 'string') {
447
+ if (value.type === 'signedSupabase' && typeof value.signedUrl === 'string') {
367
448
  return true;
368
449
  }
369
450
  if (value.type === 'asset' && typeof value.assetId === 'string') {
@@ -406,7 +487,10 @@ const blobFromBase64 = (value, contentType, flagName) => {
406
487
  if (trimmed.startsWith('data:')) {
407
488
  return blobFromDataUri(trimmed, flagName);
408
489
  }
409
- const normalized = trimmed.replace(/\s+/g, '').replace(/-/g, '+').replace(/_/g, '/');
490
+ const normalized = trimmed
491
+ .replace(/\s+/g, '')
492
+ .replace(/-/g, '+')
493
+ .replace(/_/g, '/');
410
494
  if (normalized === '') {
411
495
  throw new Error(`${flagName} input is empty.`);
412
496
  }
@@ -437,6 +521,61 @@ const extensionFromMimeType = (mimeType, mediaType) => {
437
521
  return 'gltf';
438
522
  return mediaType === 'image' ? 'png' : 'glb';
439
523
  };
524
+ const contentTypeFromExtension = (extension) => {
525
+ switch (extension.toLowerCase()) {
526
+ case 'png':
527
+ return 'image/png';
528
+ case 'jpg':
529
+ case 'jpeg':
530
+ return 'image/jpeg';
531
+ case 'webp':
532
+ return 'image/webp';
533
+ case 'gif':
534
+ return 'image/gif';
535
+ case 'bmp':
536
+ return 'image/bmp';
537
+ case 'tif':
538
+ case 'tiff':
539
+ return 'image/tiff';
540
+ case 'glb':
541
+ return 'model/gltf-binary';
542
+ case 'gltf':
543
+ return 'model/gltf+json';
544
+ case 'obj':
545
+ return 'model/obj';
546
+ case 'fbx':
547
+ return 'application/fbx';
548
+ case 'stl':
549
+ return 'model/stl';
550
+ case 'ply':
551
+ return 'text/plain';
552
+ case 'usdz':
553
+ return 'model/vnd.usdz+zip';
554
+ case 'usd':
555
+ case 'usdc':
556
+ return 'model/vnd.usd';
557
+ default:
558
+ return undefined;
559
+ }
560
+ };
561
+ const isNonspecificContentType = (contentType) => {
562
+ const clean = contentType?.split(';')[0]?.trim().toLowerCase() ?? '';
563
+ return (clean === '' ||
564
+ clean === 'application/octet-stream' ||
565
+ clean === 'binary/octet-stream');
566
+ };
567
+ const inferContentTypeFromFileName = (fileName) => {
568
+ const extension = extname(fileName).replace(/^\./, '');
569
+ if (extension === '')
570
+ return undefined;
571
+ return contentTypeFromExtension(extension);
572
+ };
573
+ const resolveUploadContentType = (fileName, explicitContentType) => {
574
+ if (!isNonspecificContentType(explicitContentType)) {
575
+ return explicitContentType;
576
+ }
577
+ return inferContentTypeFromFileName(fileName);
578
+ };
440
579
  const ensureFileNameExtension = (fileName, mediaType, mimeType) => {
441
580
  if (extname(fileName) !== '') {
442
581
  return fileName;
@@ -445,7 +584,9 @@ const ensureFileNameExtension = (fileName, mediaType, mimeType) => {
445
584
  };
446
585
  const missingSourceMessage = 'Missing source. Pass --file, --source-url, --source-resource-id, --source-id, --file-ref-json, --stdin, --stdin-base64, --stdin-data-uri, --stdin-json, --source-json, --data-uri, or --clipboard.';
447
586
  const exactlyOneSourceMessage = 'Pass exactly one source input: --file, --source-url/--image-url, --source-id/--source-resource-id, --file-ref-json, --stdin, --stdin-base64, --stdin-data-uri, --stdin-json, --source-json, --data-uri, or --clipboard.';
448
- const getSourceUrlFlag = (flags) => getFlag(flags, 'source-url') ?? getFlag(flags, 'image-url') ?? getFlag(flags, 'url');
587
+ const getSourceUrlFlag = (flags) => getFlag(flags, 'source-url') ??
588
+ getFlag(flags, 'image-url') ??
589
+ getFlag(flags, 'url');
449
590
  const assertFlagHasValue = (value, flagDescription) => {
450
591
  if (value === 'true' || value.trim() === '') {
451
592
  throw new Error(`Missing required flag value: ${flagDescription}`);
@@ -570,7 +711,15 @@ const sourceDescriptorFromJson = (value, options) => {
570
711
  if (resourceId != null) {
571
712
  return { kind: 'source', source: { resourceId } };
572
713
  }
573
- const fileRefValue = value.fileRef ?? value.file_ref ?? value.imageBlobLocation ?? value.image_blob_location ?? value.blobLocation ?? value.blob_location;
714
+ const uploadId = getFirstString(value, ['uploadId', 'upload_id']);
715
+ if (uploadId != null)
716
+ return { kind: 'source', source: { uploadId } };
717
+ const fileRefValue = value.fileRef ??
718
+ value.file_ref ??
719
+ value.imageBlobLocation ??
720
+ value.image_blob_location ??
721
+ value.blobLocation ??
722
+ value.blob_location;
574
723
  if (isBlobLocation(fileRefValue)) {
575
724
  return { kind: 'source', source: { fileRef: fileRefValue } };
576
725
  }
@@ -597,7 +746,13 @@ const sourceDescriptorFromJson = (value, options) => {
597
746
  flagName: '--source-json',
598
747
  });
599
748
  }
600
- const url = getFirstString(value, ['sourceUrl', 'source_url', 'url', 'publicUrl', 'public_url']);
749
+ const url = getFirstString(value, [
750
+ 'sourceUrl',
751
+ 'source_url',
752
+ 'url',
753
+ 'publicUrl',
754
+ 'public_url',
755
+ ]);
601
756
  if (url != null) {
602
757
  const source = sourceFromString(url);
603
758
  if (source != null) {
@@ -695,24 +850,31 @@ const readClipboardImage = async () => {
695
850
  await unlink(filePath).catch(() => undefined);
696
851
  }
697
852
  };
698
- const uploadSourceDescriptor = async ({ descriptor, client, mediaType, }) => {
853
+ const uploadSourceDescriptor = async ({ descriptor, client, mediaType, preferUploadId, }) => {
699
854
  if (descriptor.kind === 'source') {
700
855
  return descriptor.source;
701
856
  }
857
+ const fileName = descriptor.kind === 'file' ? basename(descriptor.path) : descriptor.fileName;
858
+ const fileNameWithExtension = ensureFileNameExtension(fileName, mediaType, descriptor.contentType);
859
+ const contentType = resolveUploadContentType(fileNameWithExtension, descriptor.contentType);
702
860
  const file = descriptor.kind === 'file'
703
- ? await readBlob(descriptor.path, descriptor.contentType)
704
- : descriptor.blob;
705
- const fileName = descriptor.kind === 'file'
706
- ? basename(descriptor.path)
707
- : descriptor.fileName;
861
+ ? await readBlob(descriptor.path, contentType)
862
+ : isNonspecificContentType(descriptor.blob.type) && contentType != null
863
+ ? new Blob([descriptor.blob], { type: contentType })
864
+ : descriptor.blob;
708
865
  const upload = await client.v2.uploadFile({
709
866
  file,
710
- fileName: ensureFileNameExtension(fileName, mediaType, descriptor.contentType),
867
+ fileName: fileNameWithExtension,
711
868
  mediaType,
712
869
  });
870
+ if (preferUploadId) {
871
+ if (!upload.uploadId)
872
+ throw new Error('Server did not return a durable upload ID');
873
+ return { uploadId: upload.uploadId };
874
+ }
713
875
  return { fileRef: upload.fileRef };
714
876
  };
715
- const resolveSource = async ({ flags, client, fileMediaType, }) => {
877
+ const resolveSource = async ({ flags, client, fileMediaType, preferUploadId, }) => {
716
878
  const providedCount = sourceInputCount(flags);
717
879
  if (providedCount === 0) {
718
880
  return undefined;
@@ -741,9 +903,13 @@ const resolveSource = async ({ flags, client, fileMediaType, }) => {
741
903
  const filePath = getFlag(flags, 'file');
742
904
  if (filePath != null) {
743
905
  return await uploadSourceDescriptor({
744
- descriptor: { kind: 'file', path: assertFlagHasValue(filePath, '--file <path>') },
906
+ descriptor: {
907
+ kind: 'file',
908
+ path: assertFlagHasValue(filePath, '--file <path>'),
909
+ },
745
910
  client,
746
911
  mediaType: fileMediaType,
912
+ preferUploadId,
747
913
  });
748
914
  }
749
915
  const dataUri = getFlag(flags, 'data-uri');
@@ -752,35 +918,49 @@ const resolveSource = async ({ flags, client, fileMediaType, }) => {
752
918
  descriptor: descriptorFromDataUri(assertFlagHasValue(dataUri, '--data-uri <uri>'), fileMediaType, getFlag(flags, 'file-name') ?? 'data-uri', '--data-uri'),
753
919
  client,
754
920
  mediaType: fileMediaType,
921
+ preferUploadId,
755
922
  });
756
923
  }
757
924
  if (getFlag(flags, 'source-json') != null) {
758
925
  const parsed = parseJsonValue(await readSourceJsonFlag(flags), '--source-json');
759
926
  const descriptor = sourceDescriptorFromJson(parsed, {
760
927
  mediaType: fileMediaType,
761
- fallbackFileName: getFlag(flags, 'file-name') ?? defaultFileNameForMediaType(fileMediaType),
928
+ fallbackFileName: getFlag(flags, 'file-name') ??
929
+ defaultFileNameForMediaType(fileMediaType),
762
930
  });
763
931
  if (descriptor == null) {
764
932
  throw new Error('--source-json did not contain a usable source. Expected url, image_url.url, resourceId, fileRef, data URI, base64 data, or path.');
765
933
  }
766
- return await uploadSourceDescriptor({ descriptor, client, mediaType: fileMediaType });
934
+ return await uploadSourceDescriptor({
935
+ descriptor,
936
+ client,
937
+ mediaType: fileMediaType,
938
+ preferUploadId,
939
+ });
767
940
  }
768
941
  if (hasFlag(flags, 'stdin-json')) {
769
942
  const parsed = parseJsonValue(await readStdin(), '--stdin-json');
770
943
  const descriptor = sourceDescriptorFromJson(parsed, {
771
944
  mediaType: fileMediaType,
772
- fallbackFileName: getFlag(flags, 'file-name') ?? defaultFileNameForMediaType(fileMediaType),
945
+ fallbackFileName: getFlag(flags, 'file-name') ??
946
+ defaultFileNameForMediaType(fileMediaType),
773
947
  });
774
948
  if (descriptor == null) {
775
949
  throw new Error('--stdin-json did not contain a usable source. Expected url, image_url.url, resourceId, fileRef, data URI, base64 data, or path.');
776
950
  }
777
- return await uploadSourceDescriptor({ descriptor, client, mediaType: fileMediaType });
951
+ return await uploadSourceDescriptor({
952
+ descriptor,
953
+ client,
954
+ mediaType: fileMediaType,
955
+ preferUploadId,
956
+ });
778
957
  }
779
958
  if (hasFlag(flags, 'stdin-data-uri')) {
780
959
  return await uploadSourceDescriptor({
781
960
  descriptor: descriptorFromDataUri(await readStdin(), fileMediaType, getFlag(flags, 'file-name') ?? 'stdin-data-uri', '--stdin-data-uri'),
782
961
  client,
783
962
  mediaType: fileMediaType,
963
+ preferUploadId,
784
964
  });
785
965
  }
786
966
  if (hasFlag(flags, 'stdin-base64')) {
@@ -788,12 +968,14 @@ const resolveSource = async ({ flags, client, fileMediaType, }) => {
788
968
  descriptor: descriptorFromBase64({
789
969
  value: await readStdin(),
790
970
  contentType: getContentTypeFlag(flags),
791
- fileName: getFlag(flags, 'file-name') ?? defaultFileNameForMediaType(fileMediaType),
971
+ fileName: getFlag(flags, 'file-name') ??
972
+ defaultFileNameForMediaType(fileMediaType),
792
973
  mediaType: fileMediaType,
793
974
  flagName: '--stdin-base64',
794
975
  }),
795
976
  client,
796
977
  mediaType: fileMediaType,
978
+ preferUploadId,
797
979
  });
798
980
  }
799
981
  if (hasFlag(flags, 'clipboard')) {
@@ -809,6 +991,7 @@ const resolveSource = async ({ flags, client, fileMediaType, }) => {
809
991
  },
810
992
  client,
811
993
  mediaType: fileMediaType,
994
+ preferUploadId,
812
995
  });
813
996
  }
814
997
  if (hasFlag(flags, 'stdin')) {
@@ -816,11 +999,13 @@ const resolveSource = async ({ flags, client, fileMediaType, }) => {
816
999
  descriptor: {
817
1000
  kind: 'blob',
818
1001
  blob: await readStdinBlob(getContentTypeFlag(flags)),
819
- fileName: ensureFileNameExtension(getFlag(flags, 'file-name') ?? defaultFileNameForMediaType(fileMediaType), fileMediaType, getContentTypeFlag(flags)),
1002
+ fileName: ensureFileNameExtension(getFlag(flags, 'file-name') ??
1003
+ defaultFileNameForMediaType(fileMediaType), fileMediaType, getContentTypeFlag(flags)),
820
1004
  contentType: getContentTypeFlag(flags),
821
1005
  },
822
1006
  client,
823
1007
  mediaType: fileMediaType,
1008
+ preferUploadId,
824
1009
  });
825
1010
  }
826
1011
  return undefined;
@@ -832,69 +1017,126 @@ const requireSource = async (options) => {
832
1017
  }
833
1018
  return source;
834
1019
  };
835
- const sourceToV1ImageInput = (source) => {
836
- if (source == null) {
837
- return {};
838
- }
839
- if ('url' in source) {
840
- return { imageUrl: source.url };
841
- }
842
- if ('fileRef' in source) {
843
- return { imageBlobLocation: source.fileRef };
844
- }
845
- throw new Error('v1 image generation does not accept source-resource-id. Use --source-url, --file, or --api-version v2.');
846
- };
847
1020
  const resolveProductionAnalyzeSource = async (ctx) => {
848
- const source = await requireSource({
1021
+ const source = await recordedSource(ctx, await requireSource({
849
1022
  flags: ctx.flags,
850
1023
  client: ctx.client,
851
1024
  fileMediaType: 'image',
852
- });
853
- if (source.resourceId != null) {
854
- return { imageAssetId: source.resourceId };
855
- }
856
- if (source.url != null) {
857
- return { imageUrl: source.url };
858
- }
859
- if (source.fileRef == null) {
860
- throw new Error(missingSourceMessage);
861
- }
862
- return { imageBlobLocation: source.fileRef };
1025
+ preferUploadId: true,
1026
+ }));
1027
+ return productionAnalyzeSourceFromSource(source);
863
1028
  };
864
1029
  const productionAnalyzeSourceFromSource = (source) => {
865
- if (source.resourceId != null) {
1030
+ if (source.uploadId)
1031
+ return { uploadId: source.uploadId };
1032
+ if (source.resourceId)
866
1033
  return { imageAssetId: source.resourceId };
867
- }
868
- if (source.url != null) {
1034
+ if (source.url)
869
1035
  return { imageUrl: source.url };
870
- }
871
- return { imageBlobLocation: source.fileRef };
1036
+ if (source.fileRef)
1037
+ return { imageBlobLocation: source.fileRef };
1038
+ throw new Error(missingSourceMessage);
872
1039
  };
873
1040
  const sourceFromProductionAnalyzeSource = (source) => {
1041
+ if (source.uploadId)
1042
+ return { uploadId: source.uploadId };
874
1043
  if (source.imageAssetId != null) {
875
1044
  return { resourceId: source.imageAssetId };
876
1045
  }
877
1046
  if (source.imageUrl != null) {
878
1047
  return { url: source.imageUrl };
879
1048
  }
880
- return { fileRef: source.imageBlobLocation };
1049
+ if (source.imageBlobLocation)
1050
+ return { fileRef: source.imageBlobLocation };
1051
+ throw new Error(missingSourceMessage);
881
1052
  };
882
- const productionAnalyzeTerminalStatuses = new Set(['ready', 'failed']);
1053
+ const productionAnalyzeTerminalStatuses = new Set([
1054
+ 'ready',
1055
+ 'completed',
1056
+ 'failed',
1057
+ ]);
883
1058
  const productionExecuteTerminalStatuses = new Set(['completed', 'failed']);
884
- const pollProductionStatus = async ({ ctx, orderId, terminalStatuses, }) => {
1059
+ const parseRunModeFlag = (flags) => {
1060
+ const raw = getFlag(flags, 'run-mode') ?? 'full_auto';
1061
+ if (raw === 'approval' || raw === 'full_auto') {
1062
+ return raw;
1063
+ }
1064
+ throw new Error('--run-mode must be "approval" or "full_auto"');
1065
+ };
1066
+ /**
1067
+ * Derive a human-facing run phase from a production status payload. Beyond the
1068
+ * raw order status, this surfaces `waiting_approval` — the state an approval
1069
+ * run parks in once parts are generated but not yet reviewed.
1070
+ */
1071
+ const deriveRunPhase = (status) => {
1072
+ const order = status != null && typeof status === 'object' && 'order' in status
1073
+ ? status.order
1074
+ : undefined;
1075
+ const orderStatus = order != null && typeof order.status === 'string' ? order.status : 'unknown';
1076
+ if (orderStatus !== 'in_progress') {
1077
+ return orderStatus;
1078
+ }
1079
+ const summary = status != null && typeof status === 'object' && 'summary' in status
1080
+ ? status.summary
1081
+ : undefined;
1082
+ const pending = summary != null && typeof summary.pendingTasks === 'number'
1083
+ ? summary.pendingTasks
1084
+ : undefined;
1085
+ const completed = summary != null && typeof summary.completedTasks === 'number'
1086
+ ? summary.completedTasks
1087
+ : undefined;
1088
+ if (pending === 0 && completed != null && completed > 0) {
1089
+ return 'waiting_approval';
1090
+ }
1091
+ return 'in_progress';
1092
+ };
1093
+ const productionWatchTerminalPhases = new Set([
1094
+ 'completed',
1095
+ 'failed',
1096
+ 'waiting_approval',
1097
+ ]);
1098
+ const watchProductionRun = async ({ ctx, orderId, }) => {
885
1099
  const intervalMs = parsePositiveIntegerFlag(ctx.flags, 'interval-ms', 5000);
886
- const timeoutMs = parsePositiveIntegerFlag(ctx.flags, 'timeout-ms', 900000);
1100
+ const timeoutMs = parsePositiveIntegerFlag(ctx.flags, 'timeout-ms', 1800000);
887
1101
  const startedAt = Date.now();
1102
+ let lastPhase;
888
1103
  while (Date.now() - startedAt <= timeoutMs) {
889
1104
  const status = await ctx.client.v1.getProductionStatus(orderId);
890
- const orderStatus = status.order.status;
891
- stderr.write(`[poll] order=${orderId} status=${orderStatus}\n`);
892
- if (terminalStatuses.has(orderStatus)) {
1105
+ const phase = deriveRunPhase(status);
1106
+ if (phase !== lastPhase) {
1107
+ stderr.write(`[watch] order=${orderId} phase=${phase}\n`);
1108
+ lastPhase = phase;
1109
+ }
1110
+ if (productionWatchTerminalPhases.has(phase)) {
893
1111
  return status;
894
1112
  }
895
1113
  await new Promise(resolve => setTimeout(resolve, intervalMs));
896
1114
  }
897
- throw new Error(`Polling timeout: order=${orderId} did not reach terminal state within ${timeoutMs}ms`);
1115
+ throw new Error(`Watch timeout: order=${orderId} did not reach a terminal phase within ${timeoutMs}ms`);
1116
+ };
1117
+ const pollProductionStatus = async ({ ctx, orderId, terminalStatuses, }) => {
1118
+ const intervalMs = parsePositiveIntegerFlag(ctx.flags, 'interval-ms', 5000);
1119
+ const timeoutMs = parsePositiveIntegerFlag(ctx.flags, 'timeout-ms', 900000);
1120
+ const deadline = Date.now() + timeoutMs;
1121
+ const timeout = () => new CliExecutionError(`Wait timed out for order ${orderId}; server execution continues.`, 3, activeExecution.operationId, activeExecution.execution, activeExecution.runId);
1122
+ while (Date.now() < deadline) {
1123
+ const signal = AbortSignal.timeout(Math.max(1, deadline - Date.now()));
1124
+ let status;
1125
+ try {
1126
+ status = await ctx.client.v1.getProductionStatus(orderId, { signal });
1127
+ }
1128
+ catch (error) {
1129
+ if (signal.aborted)
1130
+ throw timeout();
1131
+ throw error;
1132
+ }
1133
+ const orderStatus = status.order.status;
1134
+ stderr.write(`[poll] order=${orderId} status=${orderStatus}\n`);
1135
+ if (terminalStatuses.has(orderStatus))
1136
+ return status;
1137
+ await new Promise(resolve => setTimeout(resolve, Math.min(intervalMs, Math.max(0, deadline - Date.now()))));
1138
+ }
1139
+ throw timeout();
898
1140
  };
899
1141
  const pollIfRequested = async ({ ctx, jobId, apiVersion = 'v2', }) => {
900
1142
  if (!hasFlag(ctx.flags, 'wait')) {
@@ -923,11 +1165,18 @@ const commandSource = async (subcommand, ctx) => {
923
1165
  throw new Error('Unknown source command. Use "source create".');
924
1166
  }
925
1167
  const mediaType = parseMediaType(requireFlag(ctx.flags, 'media-type'));
926
- const source = await requireSource({
1168
+ const source = await resolveSource({
927
1169
  flags: ctx.flags,
928
1170
  client: ctx.client,
929
1171
  fileMediaType: mediaType,
1172
+ preferUploadId: true,
930
1173
  });
1174
+ if (!source)
1175
+ throw new Error(missingSourceMessage);
1176
+ if (source.uploadId != null) {
1177
+ print({ source, uploadId: source.uploadId, mediaType });
1178
+ return;
1179
+ }
931
1180
  if (source.url != null) {
932
1181
  print({ source, url: source.url, mediaType });
933
1182
  return;
@@ -1045,82 +1294,349 @@ const commandFiles = async (subcommand, positionals, ctx) => {
1045
1294
  }
1046
1295
  throw new Error('Unknown files command. Use "files import" or "files upload".');
1047
1296
  };
1297
+ const stateOptions = (ctx) => ({
1298
+ client: ctx.client,
1299
+ cwd: process.cwd(),
1300
+ stateDir: env.ASSETHUB_CLI_STATE_DIR ??
1301
+ join(dirname(getConfigPath(ctx.flags)), 'state'),
1302
+ });
1303
+ const selectedCanvasId = (flags) => getFlag(flags, 'canvas') == null
1304
+ ? undefined
1305
+ : parsePositiveIntegerFlag(flags, 'canvas', 1);
1306
+ const executionOptions = (flags) => ({
1307
+ operationId: getFlag(flags, 'operation-id'),
1308
+ parentRunId: getFlag(flags, 'derived-from-run') ?? getFlag(flags, 'parent-run'),
1309
+ agent: getFlag(flags, 'agent')
1310
+ ? {
1311
+ name: getFlag(flags, 'agent'),
1312
+ sessionId: getFlag(flags, 'agent-session'),
1313
+ }
1314
+ : undefined,
1315
+ });
1316
+ const watchOptions = (flags) => ({
1317
+ intervalMs: parsePositiveIntegerFlag(flags, 'interval-ms', 5000),
1318
+ timeoutMs: parsePositiveIntegerFlag(flags, 'timeout-ms', 900000),
1319
+ onProgress: (run) => stderr.write(`[run] ${run.runId} ${run.status}\n`),
1320
+ });
1321
+ const readJsonArgument = async (value, flag) => {
1322
+ const text = value === '@-'
1323
+ ? await readStdin()
1324
+ : value.startsWith('@')
1325
+ ? await readFile(resolve(value.slice(1)), 'utf8')
1326
+ : value;
1327
+ const parsed = parseJsonValue(text, flag);
1328
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
1329
+ throw new Error(`${flag} must contain a JSON object`);
1330
+ return parsed;
1331
+ };
1332
+ const recordedSource = async (ctx, source) => {
1333
+ if (!source)
1334
+ return undefined;
1335
+ if (source.url) {
1336
+ const imported = await ctx.client.v2.importFile({
1337
+ url: source.url,
1338
+ mediaType: 'image',
1339
+ });
1340
+ if (!imported.uploadId)
1341
+ throw new Error('Server did not return a durable upload ID');
1342
+ return { uploadId: imported.uploadId };
1343
+ }
1344
+ if (source.fileRef)
1345
+ throw new Error('Canvas history requires --file, --source-id, --source-url or an uploadId; raw fileRef is unsupported');
1346
+ return source;
1347
+ };
1348
+ const graphSourceInput = async (ctx, canvasId) => {
1349
+ const fromNode = getFlag(ctx.flags, 'from-node');
1350
+ if (!fromNode) {
1351
+ if (getFlag(ctx.flags, 'graph-revision'))
1352
+ throw new Error('--graph-revision requires --from-node');
1353
+ return undefined;
1354
+ }
1355
+ if (sourceInputCount(ctx.flags) || getFlag(ctx.flags, 'input-json'))
1356
+ throw new Error('--from-node cannot be combined with another source or --input-json');
1357
+ const separator = fromNode.indexOf('/');
1358
+ if (separator < 1)
1359
+ throw new Error('--from-node must be <graphId>/<nodeId>');
1360
+ const graphId = fromNode.slice(0, separator), nodeId = fromNode.slice(separator + 1);
1361
+ const revision = requireFlag(ctx.flags, 'graph-revision');
1362
+ const graph = await ctx.client.v2.getCanvasGraph(canvasId);
1363
+ if (graph.graphId !== graphId || graph.revision !== revision)
1364
+ throw new Error('Graph revision changed or belongs to another canvas; read graph show again');
1365
+ const node = graph.nodes.find(item => item.id === nodeId);
1366
+ const assetId = node?.metadata.assetId;
1367
+ if (typeof assetId !== 'string')
1368
+ throw new Error('Graph node has no source asset in this graph projection');
1369
+ return {
1370
+ source: { resourceId: assetId },
1371
+ graphSource: { graphId, nodeId, revision },
1372
+ };
1373
+ };
1374
+ const executionExitCode = (execution) => ['failed', 'partial', 'cancelled'].includes(execution.status)
1375
+ ? 1
1376
+ : execution.status === 'needs_review' ||
1377
+ execution.history.status !== 'recorded'
1378
+ ? 3
1379
+ : 0;
1380
+ const printRecorded = async (ctx, queued) => {
1381
+ const execution = hasFlag(ctx.flags, 'wait')
1382
+ ? await waitForExecution(ctx.client, queued.execution.runId, watchOptions(ctx.flags))
1383
+ : queued.execution;
1384
+ const job = hasFlag(ctx.flags, 'wait') && execution.jobIds[0]
1385
+ ? await ctx.client.v2.getJob(execution.jobIds[0])
1386
+ : undefined;
1387
+ const downloads = await maybeDownloadUrls(ctx.flags, execution.outputs);
1388
+ print({
1389
+ queued: execution.operation.startsWith('production.')
1390
+ ? sanitizeProductionOutput(queued)
1391
+ : queued,
1392
+ job,
1393
+ execution,
1394
+ apiVersion: execution.operation.startsWith('production.') ? 'v1' : 'v2',
1395
+ ...(downloads ? { downloads } : {}),
1396
+ });
1397
+ process.exitCode = executionExitCode(execution);
1398
+ };
1048
1399
  const commandImage = async (subcommand, ctx) => {
1049
- if (subcommand !== 'generate') {
1400
+ if (subcommand !== 'generate')
1050
1401
  throw new Error('Unknown image command. Use "image generate".');
1051
- }
1052
- const source = await resolveSource({
1053
- flags: ctx.flags,
1054
- client: ctx.client,
1055
- fileMediaType: 'image',
1402
+ if (getFlag(ctx.flags, 'api-version') &&
1403
+ getFlag(ctx.flags, 'api-version') !== 'v2')
1404
+ throw new Error('Canvas generation requires --api-version v2');
1405
+ const inputJson = getFlag(ctx.flags, 'input-json');
1406
+ const input = inputJson
1407
+ ? await readJsonArgument(inputJson, '--input-json')
1408
+ : {
1409
+ prompt: requireFlag(ctx.flags, 'prompt'),
1410
+ modelId: getFlag(ctx.flags, 'model-id') ?? defaultImageModelId,
1411
+ batchSize: parsePositiveIntegerFlag(ctx.flags, 'batch-size', 1),
1412
+ negativePrompt: getFlag(ctx.flags, 'negative-prompt'),
1413
+ seed: getFlag(ctx.flags, 'seed') == null
1414
+ ? undefined
1415
+ : parsePositiveIntegerFlag(ctx.flags, 'seed', 1),
1416
+ steps: getFlag(ctx.flags, 'steps') == null
1417
+ ? undefined
1418
+ : parsePositiveIntegerFlag(ctx.flags, 'steps', 1),
1419
+ cfgScale: getFlag(ctx.flags, 'cfg-scale') == null
1420
+ ? undefined
1421
+ : Number(getFlag(ctx.flags, 'cfg-scale')),
1422
+ };
1423
+ if (typeof input.prompt !== 'string' || !input.prompt.trim())
1424
+ throw new Error('Image prompt is required');
1425
+ const contextCanvas = getFlag(ctx.flags, 'context') == null
1426
+ ? undefined
1427
+ : parsePositiveIntegerFlag(ctx.flags, 'context', 1);
1428
+ const explicitCanvas = selectedCanvasId(ctx.flags);
1429
+ if (contextCanvas != null &&
1430
+ explicitCanvas != null &&
1431
+ contextCanvas !== explicitCanvas)
1432
+ throw new Error('--context must match --canvas');
1433
+ const session = await openExecutionSession({
1434
+ ...stateOptions(ctx),
1435
+ canvasId: explicitCanvas ?? contextCanvas,
1436
+ operation: 'image.generate',
1056
1437
  });
1057
- const apiVersion = getFlag(ctx.flags, 'api-version') ?? 'v1';
1058
- if (apiVersion !== 'v1' && apiVersion !== 'v2') {
1059
- throw new Error('--api-version must be "v1" or "v2"');
1060
- }
1061
- const commonInput = {
1062
- prompt: requireFlag(ctx.flags, 'prompt'),
1063
- modelId: getFlag(ctx.flags, 'model-id') ?? defaultImageModelId,
1064
- batchSize: parsePositiveIntegerFlag(ctx.flags, 'batch-size', 1),
1065
- negativePrompt: getFlag(ctx.flags, 'negative-prompt'),
1066
- seed: getFlag(ctx.flags, 'seed') == null
1067
- ? undefined
1068
- : parsePositiveIntegerFlag(ctx.flags, 'seed', 1),
1069
- steps: getFlag(ctx.flags, 'steps') == null
1070
- ? undefined
1071
- : parsePositiveIntegerFlag(ctx.flags, 'steps', 1),
1072
- cfgScale: getFlag(ctx.flags, 'cfg-scale') == null
1073
- ? undefined
1074
- : Number(getFlag(ctx.flags, 'cfg-scale')),
1075
- };
1076
- const queued = apiVersion === 'v1'
1077
- ? await ctx.client.v1.generateImage({
1078
- ...commonInput,
1079
- ...sourceToV1ImageInput(source),
1080
- })
1081
- : await ctx.client.v2.generateImage({
1082
- ...commonInput,
1083
- source,
1084
- });
1085
- const job = await pollIfRequested({ ctx, jobId: queued.jobId, apiVersion });
1086
- print(await withOptionalDownloads(ctx.flags, { queued, job, apiVersion }));
1438
+ if (contextCanvas != null) {
1439
+ const saved = await ctx.client.v2.getCanvasContext(contextCanvas);
1440
+ input.projectContext = {
1441
+ canvasId: contextCanvas,
1442
+ version: getFlag(ctx.flags, 'context-version') == null
1443
+ ? saved.projectContext.version
1444
+ : parsePositiveIntegerFlag(ctx.flags, 'context-version', 1),
1445
+ ...(getFlagValues(ctx.flags, 'context-source').length
1446
+ ? { sourceKeys: getFlagValues(ctx.flags, 'context-source') }
1447
+ : {}),
1448
+ };
1449
+ }
1450
+ const moodboardRevisionId = getFlag(ctx.flags, 'moodboard-revision');
1451
+ if (moodboardRevisionId)
1452
+ input.moodboardRevisionId = moodboardRevisionId;
1453
+ const graphInput = await graphSourceInput(ctx, session.canvas.id);
1454
+ const source = graphInput?.source ??
1455
+ (await recordedSource(ctx, inputJson
1456
+ ? input.source
1457
+ : await resolveSource({
1458
+ flags: ctx.flags,
1459
+ client: ctx.client,
1460
+ fileMediaType: 'image',
1461
+ preferUploadId: true,
1462
+ })));
1463
+ if (source)
1464
+ input.source = source;
1465
+ if (Array.isArray(input.sources))
1466
+ input.sources = await Promise.all(input.sources.map(source => recordedSource(ctx, source)));
1467
+ await printRecorded(ctx, await executeRecorded(session, 'image.generate', input, { ...executionOptions(ctx.flags), graphSource: graphInput?.graphSource }));
1087
1468
  };
1088
1469
  const commandMesh = async (subcommand, ctx) => {
1089
1470
  if (subcommand === 'generate') {
1090
- const queued = await ctx.client.v2.generateMesh({
1471
+ const inputJson = getFlag(ctx.flags, 'input-json');
1472
+ const input = inputJson
1473
+ ? await readJsonArgument(inputJson, '--input-json')
1474
+ : {
1475
+ modelId: getFlag(ctx.flags, 'model-id') ?? defaultMeshModelId,
1476
+ name: getFlag(ctx.flags, 'name'),
1477
+ faceLimit: getFlag(ctx.flags, 'face-limit') == null
1478
+ ? undefined
1479
+ : parsePositiveIntegerFlag(ctx.flags, 'face-limit', 1),
1480
+ isLowPoly: parseBooleanFlag(ctx.flags, 'low-poly'),
1481
+ };
1482
+ const session = await openExecutionSession({
1483
+ ...stateOptions(ctx),
1484
+ canvasId: selectedCanvasId(ctx.flags),
1485
+ operation: 'mesh.generate',
1486
+ });
1487
+ const graphInput = await graphSourceInput(ctx, session.canvas.id);
1488
+ const source = graphInput?.source ??
1489
+ (await recordedSource(ctx, inputJson
1490
+ ? input.source
1491
+ : await resolveSource({
1492
+ flags: ctx.flags,
1493
+ client: ctx.client,
1494
+ fileMediaType: 'image',
1495
+ preferUploadId: true,
1496
+ })));
1497
+ if (source)
1498
+ input.source = source;
1499
+ if (Array.isArray(input.sources))
1500
+ input.sources = await Promise.all(input.sources.map(source => recordedSource(ctx, source)));
1501
+ if (!input.source && !input.sources)
1502
+ throw new Error(missingSourceMessage);
1503
+ await printRecorded(ctx, await executeRecorded(session, 'mesh.generate', input, { ...executionOptions(ctx.flags), graphSource: graphInput?.graphSource }));
1504
+ return;
1505
+ }
1506
+ if (subcommand === 'retopo') {
1507
+ const queued = await ctx.client.v2.retopo({
1091
1508
  source: await requireSource({
1092
1509
  flags: ctx.flags,
1093
1510
  client: ctx.client,
1094
- fileMediaType: 'image',
1511
+ fileMediaType: 'mesh',
1095
1512
  }),
1096
- modelId: getFlag(ctx.flags, 'model-id') ?? defaultMeshModelId,
1513
+ modelId: requireFlag(ctx.flags, 'model-id'),
1097
1514
  name: getFlag(ctx.flags, 'name'),
1098
- faceLimit: getFlag(ctx.flags, 'face-limit') == null
1099
- ? undefined
1100
- : parsePositiveIntegerFlag(ctx.flags, 'face-limit', 1),
1101
- isLowPoly: parseBooleanFlag(ctx.flags, 'low-poly'),
1515
+ faceLevel: getFlag(ctx.flags, 'face-level'),
1516
+ polygonType: getFlag(ctx.flags, 'polygon-type'),
1102
1517
  });
1103
1518
  const job = await pollIfRequested({ ctx, jobId: queued.jobId });
1104
1519
  print(await withOptionalDownloads(ctx.flags, { queued, job }));
1105
1520
  return;
1106
1521
  }
1107
- if (subcommand === 'retopo') {
1108
- const queued = await ctx.client.v2.retopo({
1522
+ throw new Error('Unknown mesh command. Use "mesh generate" or "mesh retopo".');
1523
+ };
1524
+ /**
1525
+ * Accepted values for the rig/animate enum flags, kept next to the commands
1526
+ * that read them. `satisfies` ties each list to the client's own type, so a
1527
+ * new rig type or output format added there fails to compile here rather than
1528
+ * silently going unvalidated.
1529
+ */
1530
+ const RIG_MODEL_VERSIONS = [
1531
+ 'v1.0-20240301',
1532
+ 'v2.5-20260210',
1533
+ ];
1534
+ const RIG_TYPES = [
1535
+ 'biped',
1536
+ 'quadruped',
1537
+ 'hexapod',
1538
+ 'octopod',
1539
+ 'avian',
1540
+ 'serpentine',
1541
+ 'aquatic',
1542
+ ];
1543
+ const RIG_SPECS = ['tripo', 'mixamo'];
1544
+ const RIG_OUT_FORMATS = ['glb', 'fbx'];
1545
+ /**
1546
+ * `/animations/retarget` caps `animations` at five server-side. Rejecting the
1547
+ * sixth here means the user sees the limit before a request is built, rather
1548
+ * than a 400 after typing them all out.
1549
+ */
1550
+ const MAX_RETARGET_ANIMATIONS = 5;
1551
+ /**
1552
+ * The `animate retarget` request, built from flags alone.
1553
+ *
1554
+ * Split out of the command — like `parseRunsUploadArgs` — so the parts worth
1555
+ * testing (the required/at-most-five guards and the single-vs-plural field
1556
+ * choice) need no client or network.
1557
+ */
1558
+ export const buildAnimationRetargetRequest = (flags) => {
1559
+ const resourceId = requireFlag(flags, 'resource-id');
1560
+ const animations = getFlagValues(flags, 'animation');
1561
+ const [firstAnimation] = animations;
1562
+ if (firstAnimation == null) {
1563
+ throw new Error('Missing required flag: --animation');
1564
+ }
1565
+ if (animations.length > MAX_RETARGET_ANIMATIONS) {
1566
+ throw new Error(`--animation accepts at most ${MAX_RETARGET_ANIMATIONS} values, got ${animations.length}`);
1567
+ }
1568
+ const base = {
1569
+ resourceId,
1570
+ outFormat: parseEnumFlag(flags, 'out-format', RIG_OUT_FORMATS),
1571
+ bakeAnimation: parseBooleanFlag(flags, 'bake-animation'),
1572
+ exportWithGeometry: parseBooleanFlag(flags, 'export-with-geometry'),
1573
+ animateInPlace: parseBooleanFlag(flags, 'animate-in-place'),
1574
+ name: getFlag(flags, 'name'),
1575
+ };
1576
+ // Built per branch rather than spread-then-cast: `animation` and
1577
+ // `animations` are mutually exclusive in the request type, and a cast over
1578
+ // the whole object would stop checking the other six fields too.
1579
+ return animations.length === 1
1580
+ ? { ...base, animation: firstAnimation }
1581
+ : { ...base, animations };
1582
+ };
1583
+ /**
1584
+ * Everything `rig create` sends apart from the source, which needs a client to
1585
+ * resolve. Separated for the same reason as `buildAnimationRetargetRequest`:
1586
+ * the flag validation is the part worth testing.
1587
+ */
1588
+ export const buildRigCreateOptions = (flags) => ({
1589
+ model: parseEnumFlag(flags, 'model', RIG_MODEL_VERSIONS),
1590
+ rigType: parseEnumFlag(flags, 'rig-type', RIG_TYPES),
1591
+ spec: parseEnumFlag(flags, 'spec', RIG_SPECS),
1592
+ outFormat: parseEnumFlag(flags, 'out-format', RIG_OUT_FORMATS),
1593
+ name: getFlag(flags, 'name'),
1594
+ });
1595
+ const commandRig = async (subcommand, ctx) => {
1596
+ if (subcommand === 'check') {
1597
+ const queued = await ctx.client.v2.checkRig({
1109
1598
  source: await requireSource({
1110
1599
  flags: ctx.flags,
1111
1600
  client: ctx.client,
1112
1601
  fileMediaType: 'mesh',
1113
1602
  }),
1114
- modelId: requireFlag(ctx.flags, 'model-id'),
1115
1603
  name: getFlag(ctx.flags, 'name'),
1116
- faceLevel: getFlag(ctx.flags, 'face-level'),
1117
- polygonType: getFlag(ctx.flags, 'polygon-type'),
1118
1604
  });
1119
1605
  const job = await pollIfRequested({ ctx, jobId: queued.jobId });
1120
1606
  print(await withOptionalDownloads(ctx.flags, { queued, job }));
1121
1607
  return;
1122
1608
  }
1123
- throw new Error('Unknown mesh command. Use "mesh generate" or "mesh retopo".');
1609
+ if (subcommand === 'create') {
1610
+ const queued = await ctx.client.v2.rigMesh({
1611
+ source: await requireSource({
1612
+ flags: ctx.flags,
1613
+ client: ctx.client,
1614
+ fileMediaType: 'mesh',
1615
+ }),
1616
+ ...buildRigCreateOptions(ctx.flags),
1617
+ });
1618
+ const job = await pollIfRequested({ ctx, jobId: queued.jobId });
1619
+ print(await withOptionalDownloads(ctx.flags, { queued, job }));
1620
+ return;
1621
+ }
1622
+ throw new Error('Unknown rig command. Use "rig check" or "rig create".');
1623
+ };
1624
+ const commandAnimate = async (subcommand, ctx) => {
1625
+ if (subcommand === 'presets') {
1626
+ const result = await ctx.client.v2.listAnimationPresets({
1627
+ model: parseEnumFlag(ctx.flags, 'model', RIG_MODEL_VERSIONS),
1628
+ rigType: parseEnumFlag(ctx.flags, 'rig-type', RIG_TYPES),
1629
+ });
1630
+ print(result);
1631
+ return;
1632
+ }
1633
+ if (subcommand === 'retarget') {
1634
+ const queued = await ctx.client.v2.retargetAnimation(buildAnimationRetargetRequest(ctx.flags));
1635
+ const job = await pollIfRequested({ ctx, jobId: queued.jobId });
1636
+ print(await withOptionalDownloads(ctx.flags, { queued, job }));
1637
+ return;
1638
+ }
1639
+ throw new Error('Unknown animate command. Use "animate presets" or "animate retarget".');
1124
1640
  };
1125
1641
  const commandJobs = async (subcommand, positionals, ctx) => {
1126
1642
  const jobId = requirePositional(positionals, 2, 'job-id');
@@ -1311,14 +1827,17 @@ const firstBlobLocation = (value) => {
1311
1827
  return undefined;
1312
1828
  }
1313
1829
  const record = value;
1314
- if (record.type === 'supabase' && typeof record.bucket === 'string' && typeof record.path === 'string') {
1830
+ if (record.type === 'supabase' &&
1831
+ typeof record.bucket === 'string' &&
1832
+ typeof record.path === 'string') {
1315
1833
  return {
1316
1834
  type: 'supabase',
1317
1835
  bucket: record.bucket,
1318
1836
  path: record.path,
1319
1837
  };
1320
1838
  }
1321
- if (record.type === 'signedSupabase' && typeof record.signedUrl === 'string') {
1839
+ if (record.type === 'signedSupabase' &&
1840
+ typeof record.signedUrl === 'string') {
1322
1841
  return {
1323
1842
  type: 'signedSupabase',
1324
1843
  signedUrl: record.signedUrl,
@@ -1342,7 +1861,13 @@ const imageSourceFromCompletedJob = (job) => {
1342
1861
  if (job.resourceId != null && job.resourceId.trim() !== '') {
1343
1862
  return { imageAssetId: job.resourceId };
1344
1863
  }
1345
- const resourceId = firstStringValueForKeys(job, new Set(['resourceId', 'resource_id', 'assetId', 'asset_id', 'imageAssetId']));
1864
+ const resourceId = firstStringValueForKeys(job, new Set([
1865
+ 'resourceId',
1866
+ 'resource_id',
1867
+ 'assetId',
1868
+ 'asset_id',
1869
+ 'imageAssetId',
1870
+ ]));
1346
1871
  if (resourceId != null) {
1347
1872
  return { imageAssetId: resourceId };
1348
1873
  }
@@ -1357,6 +1882,14 @@ const imageSourceFromCompletedJob = (job) => {
1357
1882
  throw new Error('Preprocessing completed, but the image job did not include an image source for part splitting.');
1358
1883
  };
1359
1884
  const errorToOutput = (error) => {
1885
+ if (error instanceof CliExecutionError)
1886
+ return {
1887
+ message: error.message,
1888
+ exitCode: error.exitCode,
1889
+ operationId: error.operationId,
1890
+ runId: error.runId,
1891
+ execution: error.execution,
1892
+ };
1360
1893
  if (error instanceof AssetHubApiError) {
1361
1894
  return {
1362
1895
  name: error.name,
@@ -1381,7 +1914,9 @@ const readyTaskIdsFromStatus = (status) => {
1381
1914
  return [];
1382
1915
  }
1383
1916
  return missions.flatMap(mission => {
1384
- if (mission == null || typeof mission !== 'object' || !('tasks' in mission)) {
1917
+ if (mission == null ||
1918
+ typeof mission !== 'object' ||
1919
+ !('tasks' in mission)) {
1385
1920
  return [];
1386
1921
  }
1387
1922
  const tasks = mission.tasks;
@@ -1415,21 +1950,203 @@ const singleMissionIdFromStatus = (status) => {
1415
1950
  }
1416
1951
  return missionId;
1417
1952
  };
1953
+ /**
1954
+ * Flag forms for the intervention ops, one per contract op.
1955
+ *
1956
+ * `params.set` has no `--params-set`: parameters arrive as repeated
1957
+ * `--set-param key=value` and merge into a single op (see below).
1958
+ */
1959
+ const interventionOpFlags = [
1960
+ 'add-part',
1961
+ 'exclude',
1962
+ 'include',
1963
+ 'regenerate',
1964
+ 'reject',
1965
+ 'rename',
1966
+ 'set-param',
1967
+ ];
1968
+ const isInterventionOpFlag = (name) => interventionOpFlags.includes(name);
1969
+ /** Splits on the FIRST `=` so a value may itself contain one. */
1970
+ const splitOnFirstEquals = (value) => {
1971
+ const index = value.indexOf('=');
1972
+ return index === -1
1973
+ ? [value, undefined]
1974
+ : [value.slice(0, index), value.slice(index + 1)];
1975
+ };
1976
+ const requireOpValue = (flag, value, shape) => {
1977
+ if (value == null || value.trim().length === 0) {
1978
+ throw new Error(`--${flag} requires a value: --${flag} ${shape}`);
1979
+ }
1980
+ return value;
1981
+ };
1982
+ /**
1983
+ * Builds an intervention batch from the raw argument slice.
1984
+ *
1985
+ * Raw argv rather than the parsed `Flags` record on purpose: that record is
1986
+ * keyed by flag name, so it can report every `--exclude` but not whether one of
1987
+ * them preceded an `--add-part`. The batch's order becomes the log's `seq`
1988
+ * order, which is the order the operator made the decisions in, so it has to
1989
+ * survive parsing.
1990
+ *
1991
+ * Repeated `--set-param` flags merge into ONE `params.set` op, emitted at the
1992
+ * position of the first of them. Two parameters set in the same invocation are
1993
+ * one decision; writing them as two log rows would make the log claim the
1994
+ * operator changed their mind halfway through.
1995
+ *
1996
+ * Values are not validated against the contract's key set. The CLI carries no
1997
+ * zod and must not carry a second copy of the contract either -- an unknown
1998
+ * param key is the server's 400 to give, from the one place the contract is
1999
+ * enforced. Only the shape this function itself has to parse is checked here.
2000
+ */
2001
+ export const interventionOpsFromArgs = (args) => {
2002
+ const ops = [];
2003
+ let paramsOp = null;
2004
+ for (let index = 0; index < args.length; index += 1) {
2005
+ const arg = args[index];
2006
+ if (!arg.startsWith('--'))
2007
+ continue;
2008
+ const [rawName, inlineValue] = splitOnFirstEquals(arg.slice(2));
2009
+ const flag = rawName.trim();
2010
+ if (!isInterventionOpFlag(flag))
2011
+ continue;
2012
+ const next = args[index + 1];
2013
+ const value = inlineValue != null
2014
+ ? inlineValue
2015
+ : next != null && !next.startsWith('--')
2016
+ ? ((index += 1), next)
2017
+ : undefined;
2018
+ if (flag === 'add-part') {
2019
+ ops.push({ op: 'part.add', name: requireOpValue(flag, value, '<name>') });
2020
+ continue;
2021
+ }
2022
+ if (flag === 'exclude' || flag === 'include') {
2023
+ ops.push({
2024
+ op: flag === 'exclude' ? 'part.exclude' : 'part.include',
2025
+ targetId: requireOpValue(flag, value, '<target-id>'),
2026
+ });
2027
+ continue;
2028
+ }
2029
+ if (flag === 'rename') {
2030
+ const [targetId, name] = splitOnFirstEquals(requireOpValue(flag, value, '<target-id>=<name>'));
2031
+ if (targetId.trim().length === 0 ||
2032
+ name == null ||
2033
+ name.trim().length === 0) {
2034
+ throw new Error('--rename requires --rename <target-id>=<name>');
2035
+ }
2036
+ ops.push({ op: 'part.rename', targetId, name });
2037
+ continue;
2038
+ }
2039
+ if (flag === 'regenerate') {
2040
+ const [targetId, mode] = splitOnFirstEquals(requireOpValue(flag, value, '<target-id>=<mode>'));
2041
+ if (targetId.trim().length === 0 ||
2042
+ mode == null ||
2043
+ mode.trim().length === 0) {
2044
+ throw new Error('--regenerate requires --regenerate <target-id>=<mode>');
2045
+ }
2046
+ ops.push({ op: 'part.regenerate', targetId, mode });
2047
+ continue;
2048
+ }
2049
+ if (flag === 'reject') {
2050
+ const [targetId, reason] = splitOnFirstEquals(requireOpValue(flag, value, '<target-id>[=<reason>]'));
2051
+ if (targetId.trim().length === 0) {
2052
+ throw new Error('--reject requires --reject <target-id>[=<reason>]');
2053
+ }
2054
+ // `--reject 7=` is a typo, not "no reason": the contract rejects a blank
2055
+ // one, so it is refused here rather than round-tripped for a 400.
2056
+ if (reason != null && reason.trim().length === 0) {
2057
+ throw new Error('--reject reason must not be blank');
2058
+ }
2059
+ ops.push(reason == null
2060
+ ? { op: 'part.reject', targetId }
2061
+ : { op: 'part.reject', targetId, reason });
2062
+ continue;
2063
+ }
2064
+ const [key, paramValue] = splitOnFirstEquals(requireOpValue(flag, value, '<key>=<value>'));
2065
+ if (key.trim().length === 0 ||
2066
+ paramValue == null ||
2067
+ paramValue.trim().length === 0) {
2068
+ throw new Error('--set-param requires --set-param <key>=<value>');
2069
+ }
2070
+ if (paramsOp == null) {
2071
+ paramsOp = { op: 'params.set', patch: {} };
2072
+ // Cast because the patch is intentionally open here: the contract's key
2073
+ // set is enforced server-side, in one place, not duplicated in the CLI.
2074
+ ops.push(paramsOp);
2075
+ }
2076
+ paramsOp.patch[key] = paramValue;
2077
+ }
2078
+ return ops;
2079
+ };
2080
+ const readOpsJsonFlag = async (flags) => {
2081
+ const value = getFlag(flags, 'ops-json');
2082
+ if (value == null || value === 'true' || value.trim() === '') {
2083
+ throw new Error('Missing required flag value: --ops-json <json|@file|@->');
2084
+ }
2085
+ const raw = value === '@-' || value === '-'
2086
+ ? await readStdin()
2087
+ : value.startsWith('@')
2088
+ ? await readFile(value.slice(1), 'utf8')
2089
+ : value;
2090
+ const parsed = parseJsonValue(raw, '--ops-json');
2091
+ if (!Array.isArray(parsed)) {
2092
+ throw new Error('--ops-json must be a JSON array of intervention ops');
2093
+ }
2094
+ return parsed;
2095
+ };
2096
+ const productionSession = (ctx, operation) => openExecutionSession({
2097
+ ...stateOptions(ctx),
2098
+ canvasId: selectedCanvasId(ctx.flags),
2099
+ operation,
2100
+ });
2101
+ const childOperationId = (parent, step) => {
2102
+ const hash = createHash('sha256')
2103
+ .update(JSON.stringify([parent, step]))
2104
+ .digest('hex');
2105
+ return `${hash.slice(0, 8)}-${hash.slice(8, 12)}-4${hash.slice(13, 16)}-8${hash.slice(17, 20)}-${hash.slice(20, 32)}`;
2106
+ };
1418
2107
  const commandProduction = async (subcommand, positionals, ctx) => {
2108
+ if (subcommand === 'agents') {
2109
+ print(sanitizeProductionOutput(await ctx.client.v1.getProductionAgents()));
2110
+ return;
2111
+ }
1419
2112
  if (subcommand === 'analyze') {
1420
- const analyzed = await ctx.client.v1.analyzeProduction({
2113
+ const session = await productionSession(ctx, 'production.analyze');
2114
+ const analyzed = await executeRecorded(session, 'production.analyze', {
1421
2115
  ...(await resolveProductionAnalyzeSource(ctx)),
1422
2116
  agentVersion: resolvePartExtractorForAnalyze(ctx.flags),
1423
2117
  name: getFlag(ctx.flags, 'name'),
1424
- });
1425
- const status = hasFlag(ctx.flags, 'wait')
1426
- ? await pollProductionStatus({
1427
- ctx,
1428
- orderId: analyzed.orderId,
1429
- terminalStatuses: productionAnalyzeTerminalStatuses,
1430
- })
1431
- : undefined;
1432
- print(await withOptionalDownloads(ctx.flags, sanitizeProductionOutput({ analyzed, status })));
2118
+ }, executionOptions(ctx.flags));
2119
+ await printRecorded(ctx, analyzed);
2120
+ return;
2121
+ }
2122
+ if (subcommand === 'automation') {
2123
+ const session = await productionSession(ctx, 'production.automation');
2124
+ const input = getFlag(ctx.flags, 'input-json');
2125
+ const body = input
2126
+ ? await readJsonArgument(input, '--input-json')
2127
+ : {
2128
+ images: [await resolveProductionAnalyzeSource(ctx)],
2129
+ agentVersion: resolvePartExtractorForAnalyze(ctx.flags),
2130
+ name: getFlag(ctx.flags, 'name'),
2131
+ };
2132
+ if (!Array.isArray(body.images) || !body.images.length)
2133
+ throw new Error('production automation requires non-empty images[]');
2134
+ const images = await Promise.all(body.images.map(async (image) => {
2135
+ if (!image || typeof image !== 'object')
2136
+ throw new Error('Invalid automation image source');
2137
+ return productionAnalyzeSourceFromSource((await recordedSource(ctx, sourceFromProductionAnalyzeSource(image))));
2138
+ }));
2139
+ const queued = await executeRecorded(session, 'production.automation', {
2140
+ ...body,
2141
+ images,
2142
+ agentVersion: body.agentVersion ?? resolvePartExtractorForAnalyze(ctx.flags),
2143
+ ...(getFlag(ctx.flags, 'max-cost-credits')
2144
+ ? {
2145
+ maxCostCredits: parsePositiveIntegerFlag(ctx.flags, 'max-cost-credits', 1),
2146
+ }
2147
+ : {}),
2148
+ }, executionOptions(ctx.flags));
2149
+ await printRecorded(ctx, queued);
1433
2150
  return;
1434
2151
  }
1435
2152
  if (subcommand === 'status') {
@@ -1464,41 +2181,182 @@ const commandProduction = async (subcommand, positionals, ctx) => {
1464
2181
  const agentVersion = partExtractorFlag == null
1465
2182
  ? await resolvePartExtractorFromMission(ctx, orderId, missionId)
1466
2183
  : resolvePartExtractorApiValue(partExtractorFlag);
1467
- const executed = await ctx.client.v1.executeProduction({
2184
+ const session = await productionSession(ctx, 'production.execute');
2185
+ const executed = await executeRecorded(session, 'production.execute', {
1468
2186
  orderId,
1469
2187
  missionId,
1470
2188
  confirmedTaskIds,
1471
2189
  agentVersion,
1472
2190
  partExtractionMode,
2191
+ }, executionOptions(ctx.flags));
2192
+ await printRecorded(ctx, executed);
2193
+ return;
2194
+ }
2195
+ if (subcommand === 'run') {
2196
+ const orderId = requireFlag(ctx.flags, 'order-id');
2197
+ const missionId = requireFlag(ctx.flags, 'mission-id');
2198
+ const runMode = parseRunModeFlag(ctx.flags);
2199
+ const partExtractionMode = getFlag(ctx.flags, 'part-extraction-mode');
2200
+ if (partExtractionMode != null &&
2201
+ partExtractionMode !== 'fast' &&
2202
+ partExtractionMode !== 'high_quality') {
2203
+ throw new Error('--part-extraction-mode must be "fast" or "high_quality"');
2204
+ }
2205
+ const partExtractorFlag = getPartExtractorFlag(ctx.flags);
2206
+ const agentVersion = partExtractorFlag == null
2207
+ ? await resolvePartExtractorFromMission(ctx, orderId, missionId)
2208
+ : resolvePartExtractorApiValue(partExtractorFlag);
2209
+ const confirmedTaskIds = getFlagValues(ctx.flags, 'task-id');
2210
+ const allowedModelIds = getFlagValues(ctx.flags, 'allowed-model-id');
2211
+ const maxIterations = getFlag(ctx.flags, 'max-iterations') == null
2212
+ ? undefined
2213
+ : parsePositiveIntegerFlag(ctx.flags, 'max-iterations', 1);
2214
+ const maxCostCredits = getFlag(ctx.flags, 'max-cost-credits') == null
2215
+ ? undefined
2216
+ : parsePositiveIntegerFlag(ctx.flags, 'max-cost-credits', 1);
2217
+ const started = await ctx.client.v1.runProduction({
2218
+ orderId,
2219
+ missionId,
2220
+ agentVersion,
2221
+ runMode,
2222
+ ...(confirmedTaskIds.length > 0 ? { confirmedTaskIds } : {}),
2223
+ ...(allowedModelIds.length > 0 ? { allowedModelIds } : {}),
2224
+ ...(partExtractionMode != null
2225
+ ? { partExtractionMode: partExtractionMode }
2226
+ : {}),
2227
+ ...(maxIterations != null ? { maxIterations } : {}),
2228
+ ...(maxCostCredits != null ? { maxCostCredits } : {}),
1473
2229
  });
1474
2230
  const status = hasFlag(ctx.flags, 'wait')
1475
- ? await pollProductionStatus({
1476
- ctx,
1477
- orderId,
1478
- terminalStatuses: productionExecuteTerminalStatuses,
1479
- })
2231
+ ? await watchProductionRun({ ctx, orderId })
1480
2232
  : undefined;
1481
- print(await withOptionalDownloads(ctx.flags, sanitizeProductionOutput({ executed, status })));
2233
+ print(await withOptionalDownloads(ctx.flags, sanitizeProductionOutput({ started, status })));
2234
+ return;
2235
+ }
2236
+ if (subcommand === 'interventions') {
2237
+ const orderId = requirePositional(positionals, 2, 'order-id');
2238
+ print(await ctx.client.v2.listInterventions(orderId));
1482
2239
  return;
1483
2240
  }
1484
- throw new Error('Unknown production command. Use "production analyze", "production status", or "production execute".');
2241
+ if (subcommand === 'intervene') {
2242
+ const orderId = requirePositional(positionals, 2, 'order-id');
2243
+ const usesOpsJson = hasFlag(ctx.flags, 'ops-json');
2244
+ const flagOps = interventionOpsFromArgs(ctx.args);
2245
+ // Refused rather than concatenated: with both present the batch's order
2246
+ // would depend on which source the CLI happened to read first, and that
2247
+ // order becomes the log's seq order.
2248
+ if (usesOpsJson && flagOps.length > 0) {
2249
+ throw new Error('Use either --ops-json or the individual op flags, not both.');
2250
+ }
2251
+ const interventions = usesOpsJson
2252
+ ? await readOpsJsonFlag(ctx.flags)
2253
+ : flagOps;
2254
+ if (interventions.length === 0) {
2255
+ throw new Error('No interventions given. Use --exclude/--include/--add-part/--rename/--reject/--regenerate/--set-param, or --ops-json.');
2256
+ }
2257
+ const idempotencyKey = getFlag(ctx.flags, 'idempotency-key');
2258
+ const appended = await ctx.client.v2.appendInterventions(orderId, interventions,
2259
+ // Omitted rather than passed as undefined so the request carries no header
2260
+ // at all; the server then reports `idempotent: false` back.
2261
+ idempotencyKey == null ? {} : { idempotencyKey });
2262
+ print({ appended, interventions });
2263
+ return;
2264
+ }
2265
+ if (subcommand === 'watch') {
2266
+ const orderId = requirePositional(positionals, 2, 'order-id');
2267
+ const status = await watchProductionRun({ ctx, orderId });
2268
+ print(await withOptionalDownloads(ctx.flags, sanitizeProductionOutput({ status })));
2269
+ return;
2270
+ }
2271
+ throw new Error('Unknown production command. Use "production agents", "production analyze", "production automation", "production run", "production watch", "production status", "production execute", "production intervene", or "production interventions".');
2272
+ };
2273
+ // Multi-phase commands may contain several receipts; only their durable outputs
2274
+ // are downloadable, never canvas links, source URLs or evaluation evidence URLs.
2275
+ const recordedOutputs = (value) => {
2276
+ if (!value || typeof value !== 'object')
2277
+ return [];
2278
+ if (Array.isArray(value))
2279
+ return value.flatMap(recordedOutputs);
2280
+ return Object.entries(value).flatMap(([key, item]) => {
2281
+ if (key === 'execution' &&
2282
+ item &&
2283
+ typeof item === 'object' &&
2284
+ 'outputs' in item &&
2285
+ Array.isArray(item.outputs))
2286
+ return item.outputs;
2287
+ return recordedOutputs(item);
2288
+ });
1485
2289
  };
2290
+ const isArtifactGraphPartSplit = (value, execution) => (value != null &&
2291
+ typeof value === 'object' &&
2292
+ 'engine' in value &&
2293
+ value.engine === 'artifact-graph') ||
2294
+ execution.resolvedInput?.agentVersion === artifactGraphPartExtractorApiValue;
1486
2295
  const commandParts = async (subcommand, ctx) => {
1487
- const runPartSplit = async (source) => {
2296
+ const runPartSplit = async (source, operationId = getFlag(ctx.flags, 'operation-id') ?? randomUUID()) => {
1488
2297
  let orderId = source == null ? getFlag(ctx.flags, 'order-id') : undefined;
1489
2298
  if (orderId != null) {
1490
2299
  orderId = assertFlagHasValue(orderId, '--order-id <id>');
1491
2300
  }
2301
+ const partExtractorFlag = getPartExtractorFlag(ctx.flags);
2302
+ const requestedAgentVersion = orderId == null || partExtractorFlag != null
2303
+ ? resolvePartExtractorApiValue(partExtractorFlag ?? defaultPartExtractorName)
2304
+ : undefined;
2305
+ if (requestedAgentVersion === artifactGraphPartExtractorApiValue) {
2306
+ for (const flag of ['task-id', 'mission-id', 'part-extraction-mode']) {
2307
+ if (hasFlag(ctx.flags, flag))
2308
+ throw new Error(`--${flag} is not supported with V3.6.1`);
2309
+ }
2310
+ if (orderId != null)
2311
+ throw new Error('--order-id is not supported with V3.6.1; use runs resume <operation-id> to resume a graph split');
2312
+ }
2313
+ const session = await productionSession(ctx, orderId == null ? 'production.analyze' : 'production.execute');
2314
+ let execution;
1492
2315
  let analyzed;
1493
2316
  let status;
1494
2317
  if (orderId == null) {
1495
- const result = await ctx.client.v1.analyzeProduction({
2318
+ const result = await executeRecorded(session, 'production.analyze', {
1496
2319
  ...(source ?? (await resolveProductionAnalyzeSource(ctx))),
1497
2320
  agentVersion: resolvePartExtractorForAnalyze(ctx.flags),
1498
2321
  name: getFlag(ctx.flags, 'name'),
1499
- });
2322
+ }, { ...executionOptions(ctx.flags), operationId });
1500
2323
  analyzed = result;
1501
- orderId = result.orderId;
2324
+ execution = result.execution;
2325
+ if (isArtifactGraphPartSplit(result, execution)) {
2326
+ stderr.write(`[run] ${execution.runId} graph=${execution.orderIds[0] ?? execution.runId}\n`);
2327
+ if (hasFlag(ctx.flags, 'wait') || hasFlag(ctx.flags, 'all-ready'))
2328
+ execution = await waitForExecution(ctx.client, execution.runId, watchOptions(ctx.flags));
2329
+ process.exitCode = executionExitCode(execution);
2330
+ return sanitizeProductionOutput({ execution, analyzed });
2331
+ }
2332
+ orderId = execution.orderIds[0];
2333
+ if (!orderId)
2334
+ throw new CliExecutionError('Analysis receipt omitted its order', 3, operationId, execution);
2335
+ stderr.write(`[run] ${execution.runId} order=${orderId}\n`);
2336
+ const executeOperationId = childOperationId(operationId, 'execute');
2337
+ if ((hasFlag(ctx.flags, 'all-ready') ||
2338
+ getFlagValues(ctx.flags, 'task-id').length > 0) &&
2339
+ (await hasRecordedOperation(session.stateDir, executeOperationId))) {
2340
+ const resumed = await resumeRecorded({
2341
+ ...stateOptions(ctx),
2342
+ operationId: executeOperationId,
2343
+ expectedCanvasId: session.canvas.id,
2344
+ expectedBody: {
2345
+ partExtractionMode: getFlag(ctx.flags, 'part-extraction-mode'),
2346
+ ...(getFlagValues(ctx.flags, 'task-id').length
2347
+ ? { confirmedTaskIds: getFlagValues(ctx.flags, 'task-id') }
2348
+ : {}),
2349
+ ...(getFlag(ctx.flags, 'mission-id')
2350
+ ? { missionId: getFlag(ctx.flags, 'mission-id') }
2351
+ : {}),
2352
+ },
2353
+ });
2354
+ const execution = hasFlag(ctx.flags, 'wait') || hasFlag(ctx.flags, 'all-ready')
2355
+ ? await waitForExecution(ctx.client, resumed.execution.runId, watchOptions(ctx.flags))
2356
+ : resumed.execution;
2357
+ process.exitCode = executionExitCode(execution);
2358
+ return { execution, executed: sanitizeProductionOutput(resumed) };
2359
+ }
1502
2360
  if (hasFlag(ctx.flags, 'wait') || hasFlag(ctx.flags, 'all-ready')) {
1503
2361
  status = await pollProductionStatus({
1504
2362
  ctx,
@@ -1541,13 +2399,21 @@ const commandParts = async (subcommand, ctx) => {
1541
2399
  partExtractionMode !== 'high_quality') {
1542
2400
  throw new Error('--part-extraction-mode must be "fast" or "high_quality"');
1543
2401
  }
1544
- executed = await ctx.client.v1.executeProduction({
2402
+ const result = await executeRecorded(session, 'production.execute', {
1545
2403
  orderId,
1546
2404
  missionId,
1547
2405
  confirmedTaskIds: taskIds,
1548
2406
  agentVersion,
1549
2407
  partExtractionMode,
2408
+ }, {
2409
+ ...executionOptions(ctx.flags),
2410
+ operationId: execution
2411
+ ? childOperationId(operationId, 'execute')
2412
+ : operationId,
2413
+ parentRunId: execution?.runId ?? executionOptions(ctx.flags).parentRunId,
1550
2414
  });
2415
+ executed = result;
2416
+ execution = result.execution;
1551
2417
  if (hasFlag(ctx.flags, 'wait')) {
1552
2418
  executionStatus = await pollProductionStatus({
1553
2419
  ctx,
@@ -1556,7 +2422,14 @@ const commandParts = async (subcommand, ctx) => {
1556
2422
  });
1557
2423
  }
1558
2424
  }
2425
+ if (execution &&
2426
+ (hasFlag(ctx.flags, 'wait') || hasFlag(ctx.flags, 'all-ready'))) {
2427
+ execution = await waitForExecution(ctx.client, execution.runId, watchOptions(ctx.flags));
2428
+ }
2429
+ if (execution)
2430
+ process.exitCode = executionExitCode(execution);
1559
2431
  return sanitizeProductionOutput({
2432
+ execution,
1560
2433
  analyzed,
1561
2434
  status,
1562
2435
  executed,
@@ -1564,10 +2437,13 @@ const commandParts = async (subcommand, ctx) => {
1564
2437
  });
1565
2438
  };
1566
2439
  if (subcommand === 'split') {
1567
- if (getFlag(ctx.flags, 'order-id') != null && sourceInputCount(ctx.flags) > 0) {
2440
+ if (getFlag(ctx.flags, 'order-id') != null &&
2441
+ sourceInputCount(ctx.flags) > 0) {
1568
2442
  throw new Error('Pass either --order-id or one image source, not both.');
1569
2443
  }
1570
- print(await withOptionalDownloads(ctx.flags, await runPartSplit()));
2444
+ const result = await runPartSplit();
2445
+ const downloads = await maybeDownloadUrls(ctx.flags, recordedOutputs(result));
2446
+ print({ ...result, ...(downloads ? { downloads } : {}) });
1571
2447
  return;
1572
2448
  }
1573
2449
  if (subcommand === 'compare') {
@@ -1578,7 +2454,8 @@ const commandParts = async (subcommand, ctx) => {
1578
2454
  if (preprocessPrompt === 'true') {
1579
2455
  throw new Error('Missing required flag value: --preprocess-prompt <text>');
1580
2456
  }
1581
- if (preprocessPrompt != null && getFlagValues(ctx.flags, 'task-id').length > 0) {
2457
+ if (preprocessPrompt != null &&
2458
+ getFlagValues(ctx.flags, 'task-id').length > 0) {
1582
2459
  throw new Error('parts compare preprocessing creates a second order. Use --all-ready instead of --task-id.');
1583
2460
  }
1584
2461
  if (preprocessPrompt != null && getFlag(ctx.flags, 'mission-id') != null) {
@@ -1588,17 +2465,22 @@ const commandParts = async (subcommand, ctx) => {
1588
2465
  if (preprocessModelId === 'true') {
1589
2466
  throw new Error('Missing required flag value: --preprocess-model-id <id>');
1590
2467
  }
2468
+ const compareSession = await productionSession(ctx, 'production.analyze');
2469
+ const compareOperationId = getFlag(ctx.flags, 'operation-id') ?? randomUUID();
1591
2470
  const inputSource = productionAnalyzeSourceFromSource(await requireSource({
1592
2471
  flags: ctx.flags,
1593
2472
  client: ctx.client,
1594
2473
  fileMediaType: 'image',
2474
+ preferUploadId: true,
1595
2475
  }));
1596
- const direct = await runPartSplit(inputSource);
2476
+ const durableInput = productionAnalyzeSourceFromSource((await recordedSource(ctx, sourceFromProductionAnalyzeSource(inputSource))));
2477
+ const direct = await runPartSplit(durableInput, compareOperationId);
1597
2478
  let preprocessed = null;
1598
2479
  if (preprocessPrompt != null && preprocessPrompt !== 'true') {
2480
+ const preprocessOperationId = childOperationId(compareOperationId, 'preprocess');
1599
2481
  try {
1600
- const queued = await ctx.client.v2.generateImage({
1601
- source: sourceFromProductionAnalyzeSource(inputSource),
2482
+ const queued = await executeRecorded(compareSession, 'image.generate', {
2483
+ source: sourceFromProductionAnalyzeSource(durableInput),
1602
2484
  prompt: preprocessPrompt,
1603
2485
  modelId: preprocessModelId ?? defaultImageModelId,
1604
2486
  batchSize: 1,
@@ -1606,8 +2488,15 @@ const commandParts = async (subcommand, ctx) => {
1606
2488
  seed: getFlag(ctx.flags, 'preprocess-seed') == null
1607
2489
  ? undefined
1608
2490
  : parsePositiveIntegerFlag(ctx.flags, 'preprocess-seed', 1),
2491
+ }, {
2492
+ ...executionOptions(ctx.flags),
2493
+ operationId: preprocessOperationId,
1609
2494
  });
1610
- const job = await ctx.client.v2.pollJob(queued.jobId, {
2495
+ const completed = await waitForExecution(ctx.client, queued.execution.runId, watchOptions(ctx.flags));
2496
+ const jobId = completed.jobIds[0];
2497
+ if (!jobId)
2498
+ throw new CliExecutionError('Preprocessing omitted its job', 3, undefined, completed);
2499
+ const job = await ctx.client.v2.pollJob(jobId, {
1611
2500
  intervalMs: parsePositiveIntegerFlag(ctx.flags, 'interval-ms', 5000),
1612
2501
  timeoutMs: parsePositiveIntegerFlag(ctx.flags, 'timeout-ms', 900000),
1613
2502
  onPoll: current => {
@@ -1625,27 +2514,445 @@ const commandParts = async (subcommand, ctx) => {
1625
2514
  job,
1626
2515
  source: generatedSource,
1627
2516
  },
1628
- split: await runPartSplit(generatedSource),
2517
+ split: await runPartSplit(generatedSource, childOperationId(compareOperationId, 'preprocessed-split')),
1629
2518
  };
1630
2519
  }
1631
2520
  catch (error) {
2521
+ const failure = error instanceof CliExecutionError ||
2522
+ activeExecution.operationId !== preprocessOperationId
2523
+ ? error
2524
+ : new CliExecutionError(error instanceof Error ? error.message : String(error), 1, preprocessOperationId, activeExecution.execution, activeExecution.runId);
1632
2525
  if (hasFlag(ctx.flags, 'fail-on-preprocess-error')) {
1633
- throw error;
2526
+ throw failure;
1634
2527
  }
2528
+ process.exitCode =
2529
+ failure instanceof CliExecutionError ? failure.exitCode : 1;
1635
2530
  preprocessed = {
1636
2531
  ok: false,
1637
- error: errorToOutput(error),
2532
+ error: errorToOutput(failure),
1638
2533
  };
1639
2534
  }
1640
2535
  }
1641
- print(await withOptionalDownloads(ctx.flags, {
1642
- direct,
1643
- preprocessed,
1644
- }));
2536
+ const result = { direct, preprocessed };
2537
+ const downloads = await maybeDownloadUrls(ctx.flags, recordedOutputs(result));
2538
+ print({ ...result, ...(downloads ? { downloads } : {}) });
1645
2539
  return;
1646
2540
  }
1647
2541
  throw new Error('Unknown parts command. Use "parts split" or "parts compare".');
1648
2542
  };
2543
+ const pageOptions = (flags) => ({
2544
+ cursor: getFlag(flags, 'cursor'),
2545
+ limit: getFlag(flags, 'limit') == null
2546
+ ? undefined
2547
+ : parsePositiveIntegerFlag(flags, 'limit', 50),
2548
+ });
2549
+ const canvasForRead = (ctx) => openExecutionSession({
2550
+ ...stateOptions(ctx),
2551
+ canvasId: selectedCanvasId(ctx.flags),
2552
+ create: false,
2553
+ });
2554
+ const commandCanvas = async (subcommand, positionals, ctx) => {
2555
+ if (subcommand === 'context' ||
2556
+ subcommand === 'import' ||
2557
+ subcommand === 'compare' ||
2558
+ subcommand === 'layout') {
2559
+ const { canvas } = await canvasForRead(ctx);
2560
+ if (subcommand === 'context') {
2561
+ const action = positionals[2];
2562
+ if (action !== 'get' && action !== 'put')
2563
+ throw new Error('Use canvas context get|put');
2564
+ const result = action === 'get'
2565
+ ? await ctx.client.v2.getCanvasContext(canvas.id)
2566
+ : await ctx.client.v2.putCanvasContext(canvas.id, {
2567
+ document: await readJsonArgument(`@${requireFlag(ctx.flags, 'file')}`, '--file'),
2568
+ expectedVersion: parseNonNegativeIntegerFlag(ctx.flags, 'if-version'),
2569
+ });
2570
+ const out = getFlag(ctx.flags, 'out');
2571
+ if (out) {
2572
+ await mkdir(dirname(resolve(out)), { recursive: true });
2573
+ await writeFile(resolve(out), JSON.stringify(result.projectContext.document, null, 2) + '\n', { mode: 0o600 });
2574
+ }
2575
+ print(result);
2576
+ }
2577
+ else if (subcommand === 'layout') {
2578
+ const input = await readJsonArgument(requireFlag(ctx.flags, 'input-json'), '--input-json');
2579
+ print(await ctx.client.v2.appendCanvasLayout(canvas.id, input));
2580
+ }
2581
+ else if (subcommand === 'import') {
2582
+ const filePath = getFlag(ctx.flags, 'file');
2583
+ const uploadId = getFlag(ctx.flags, 'upload-id');
2584
+ const resourceId = getSourceResourceIdFlag(ctx.flags);
2585
+ if ([filePath, uploadId, resourceId].filter(value => value != null)
2586
+ .length !== 1 ||
2587
+ sourceInputCount(ctx.flags) > (uploadId == null ? 1 : 0))
2588
+ throw new Error('Provide exactly one of --file, --resource-id, or --upload-id');
2589
+ print(await importCanvasAssetWithState({
2590
+ client: ctx.client,
2591
+ baseUrl: ctx.client.baseUrl,
2592
+ ownerId: canvas.ownerId,
2593
+ canvasId: canvas.id,
2594
+ stateDir: stateOptions(ctx).stateDir,
2595
+ name: getFlag(ctx.flags, 'name'),
2596
+ ...(filePath != null
2597
+ ? {
2598
+ filePath: assertFlagHasValue(filePath, '--file <image>'),
2599
+ contentType: getContentTypeFlag(ctx.flags),
2600
+ }
2601
+ : {
2602
+ source: uploadId != null
2603
+ ? {
2604
+ uploadId: assertFlagHasValue(uploadId, '--upload-id <id>'),
2605
+ }
2606
+ : {
2607
+ resourceId: assertFlagHasValue(resourceId, '--resource-id <id>'),
2608
+ },
2609
+ }),
2610
+ }));
2611
+ }
2612
+ else {
2613
+ print(await writeCanvasComparison({
2614
+ client: ctx.client,
2615
+ canvasId: canvas.id,
2616
+ sourceId: requireFlag(ctx.flags, 'source-id'),
2617
+ assetId: requireFlag(ctx.flags, 'asset-id'),
2618
+ out: requireFlag(ctx.flags, 'out'),
2619
+ title: getFlag(ctx.flags, 'title'),
2620
+ }));
2621
+ }
2622
+ return;
2623
+ }
2624
+ if (subcommand === 'list') {
2625
+ print(await ctx.client.v2.listCanvases(pageOptions(ctx.flags)));
2626
+ return;
2627
+ }
2628
+ const capabilities = await ctx.client.v2.getCapabilities();
2629
+ if (capabilities.executionContext.status !== 'available')
2630
+ throw new Error('Canvas execution history unavailable');
2631
+ const scope = {
2632
+ ...stateOptions(ctx),
2633
+ baseUrl: ctx.client.baseUrl,
2634
+ ownerId: capabilities.ownerId,
2635
+ };
2636
+ if (subcommand === 'create') {
2637
+ const canvas = await ctx.client.v2.createCanvas({ name: getFlag(ctx.flags, 'name') ?? 'Agent canvas' }, { idempotencyKey: getFlag(ctx.flags, 'operation-id') ?? randomUUID() });
2638
+ await saveCanvasSelection(scope, canvas.id);
2639
+ print(canvas);
2640
+ return;
2641
+ }
2642
+ if (!['get', 'use', 'open'].includes(subcommand ?? ''))
2643
+ throw new Error('Use canvas create|list|get|use|open');
2644
+ const id = positionals[2] == null
2645
+ ? selectedCanvasId(ctx.flags)
2646
+ : Number(positionals[2]);
2647
+ const canvas = await resolveCanvasSelection({
2648
+ ...scope,
2649
+ canvasId: id,
2650
+ create: false,
2651
+ });
2652
+ if (subcommand === 'use')
2653
+ await saveCanvasSelection(scope, canvas.id);
2654
+ if (subcommand === 'open' && stdout.isTTY) {
2655
+ const url = canvas.url;
2656
+ await execFileAsync(process.platform === 'darwin'
2657
+ ? 'open'
2658
+ : process.platform === 'win32'
2659
+ ? 'rundll32'
2660
+ : 'xdg-open', process.platform === 'win32'
2661
+ ? ['url.dll,FileProtocolHandler', url]
2662
+ : [url]);
2663
+ }
2664
+ print(canvas);
2665
+ };
2666
+ const commandLanguage = async (subcommand, ctx) => {
2667
+ if (subcommand !== 'text' && subcommand !== 'vision')
2668
+ throw new Error('Use language text|vision --input-json @<file> --operation-id <uuid>');
2669
+ const input = await readJsonArgument(requireFlag(ctx.flags, 'input-json'), '--input-json');
2670
+ const operationId = requireFlag(ctx.flags, 'operation-id');
2671
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(operationId))
2672
+ throw new Error('--operation-id must be a UUID; reuse the same ID and input after an uncertain response');
2673
+ activeExecution.operationId = operationId;
2674
+ const result = subcommand === 'text'
2675
+ ? await ctx.client.v2.languageText(input, {
2676
+ idempotencyKey: operationId,
2677
+ })
2678
+ : await ctx.client.v2.languageVision(input, {
2679
+ idempotencyKey: operationId,
2680
+ });
2681
+ const out = getFlag(ctx.flags, 'out');
2682
+ if (out) {
2683
+ await mkdir(dirname(resolve(out)), { recursive: true });
2684
+ await writeFile(resolve(out), JSON.stringify(result, null, 2) + '\n', {
2685
+ mode: 0o600,
2686
+ });
2687
+ }
2688
+ print(result);
2689
+ };
2690
+ const commandMoodboard = async (subcommand, positionals, ctx) => {
2691
+ if (subcommand === 'list') {
2692
+ print(await ctx.client.v2.listMoodboards(pageOptions(ctx.flags)));
2693
+ return;
2694
+ }
2695
+ if (subcommand === 'create' || subcommand === 'update') {
2696
+ const input = (await readJsonArgument(requireFlag(ctx.flags, 'input-json'), '--input-json'));
2697
+ if (subcommand === 'update') {
2698
+ print(await ctx.client.v2.updateMoodboard(requirePositional(positionals, 2, 'board-id'), input));
2699
+ return;
2700
+ }
2701
+ const { ownerId } = await ctx.client.v2.getCapabilities();
2702
+ const fingerprint = createHash('sha256')
2703
+ .update(JSON.stringify({ baseUrl: ctx.client.baseUrl, ownerId, input }))
2704
+ .digest('hex');
2705
+ const clientOperationId = getFlag(ctx.flags, 'operation-id') ??
2706
+ `${fingerprint.slice(0, 8)}-${fingerprint.slice(8, 12)}-4${fingerprint.slice(13, 16)}-8${fingerprint.slice(17, 20)}-${fingerprint.slice(20, 32)}`;
2707
+ activeExecution.operationId = clientOperationId;
2708
+ print(await ctx.client.v2.createMoodboard({ ...input, clientOperationId }));
2709
+ return;
2710
+ }
2711
+ const boardId = requirePositional(positionals, 2, 'board-id');
2712
+ if (subcommand === 'get') {
2713
+ print(await ctx.client.v2.getMoodboard(boardId));
2714
+ return;
2715
+ }
2716
+ if (subcommand === 'archive') {
2717
+ print(await ctx.client.v2.archiveMoodboard(boardId));
2718
+ return;
2719
+ }
2720
+ if (subcommand !== 'analyze')
2721
+ throw new Error('Use moodboard create|list|get|update|archive|analyze');
2722
+ const timeoutMs = parsePositiveIntegerFlag(ctx.flags, 'timeout-ms', 900000);
2723
+ const intervalMs = parsePositiveIntegerFlag(ctx.flags, 'interval-ms', 5000);
2724
+ const deadline = Date.now() + timeoutMs;
2725
+ let revision = await ctx.client.v2.analyzeMoodboard(boardId, { revisionId: getFlag(ctx.flags, 'revision') }, { signal: AbortSignal.timeout(timeoutMs) });
2726
+ while (hasFlag(ctx.flags, 'wait') && revision.status === 'analyzing') {
2727
+ stderr.write(`[moodboard] ${boardId} revision=${revision.id} analyzing job=${revision.analysisJobId ?? 'pending'}\n`);
2728
+ if (Date.now() >= deadline)
2729
+ throw new Error(`Moodboard analysis timeout; resume with moodboard get ${boardId}`);
2730
+ await delay(Math.min(intervalMs, Math.max(1, deadline - Date.now())));
2731
+ if (Date.now() >= deadline)
2732
+ throw new Error(`Moodboard analysis timeout; resume with moodboard get ${boardId}`);
2733
+ const board = await ctx.client.v2.getMoodboard(boardId, {
2734
+ signal: AbortSignal.timeout(Math.max(1, deadline - Date.now())),
2735
+ });
2736
+ if (board.currentRevision?.id !== revision.id)
2737
+ throw new Error('Moodboard revision changed while waiting; inspect the board before generating');
2738
+ revision = board.currentRevision;
2739
+ }
2740
+ print(revision);
2741
+ if (revision.status === 'failed')
2742
+ process.exitCode = 1;
2743
+ };
2744
+ const commandGraph = async (subcommand, ctx) => {
2745
+ if (!['show', 'lineage', 'export'].includes(subcommand ?? ''))
2746
+ throw new Error('Use graph show|lineage|export');
2747
+ const { canvas } = await canvasForRead(ctx);
2748
+ const direction = getFlag(ctx.flags, 'direction') ?? 'both';
2749
+ if (!['ancestors', 'descendants', 'both'].includes(direction))
2750
+ throw new Error('--direction must be ancestors, descendants, or both');
2751
+ const graph = await ctx.client.v2.getCanvasGraph(canvas.id, {
2752
+ artifactId: subcommand === 'lineage'
2753
+ ? requireFlag(ctx.flags, 'artifact')
2754
+ : getFlag(ctx.flags, 'artifact'),
2755
+ direction: direction,
2756
+ depth: parsePositiveIntegerFlag(ctx.flags, 'depth', 5),
2757
+ });
2758
+ if (subcommand === 'export' &&
2759
+ graph.truncated &&
2760
+ !hasFlag(ctx.flags, 'allow-truncated'))
2761
+ throw new Error('Graph exceeds export bounds; narrow with --artifact or explicitly use --allow-truncated');
2762
+ const out = getFlag(ctx.flags, 'out');
2763
+ if (subcommand === 'export' && out) {
2764
+ await writeFile(resolve(out), JSON.stringify(graph, null, 2) + '\n', {
2765
+ mode: 0o600,
2766
+ });
2767
+ print({
2768
+ path: resolve(out),
2769
+ revision: graph.revision,
2770
+ truncated: graph.truncated,
2771
+ });
2772
+ }
2773
+ else
2774
+ print(graph);
2775
+ };
2776
+ const commandEvaluations = async (subcommand, positionals, ctx) => {
2777
+ if (subcommand === 'get') {
2778
+ print(await ctx.client.v2.getEvaluation(requirePositional(positionals, 2, 'evaluation-id')));
2779
+ return;
2780
+ }
2781
+ const { canvas } = await canvasForRead(ctx);
2782
+ if (subcommand === 'list') {
2783
+ print(await ctx.client.v2.listEvaluations({
2784
+ ...pageOptions(ctx.flags),
2785
+ canvasId: canvas.id,
2786
+ artifactId: getFlag(ctx.flags, 'artifact'),
2787
+ }));
2788
+ return;
2789
+ }
2790
+ if (subcommand !== 'submit')
2791
+ throw new Error('Use evaluations submit|list|get');
2792
+ const reportArg = requireFlag(ctx.flags, 'report');
2793
+ const report = await readJsonArgument(reportArg.startsWith('{') || reportArg.startsWith('@')
2794
+ ? reportArg
2795
+ : `@${reportArg}`, '--report');
2796
+ const clientOperationId = getFlag(ctx.flags, 'operation-id') ?? randomUUID();
2797
+ activeExecution.operationId = clientOperationId;
2798
+ const evaluation = await ctx.client.v2.submitEvaluation({
2799
+ canvasId: canvas.id,
2800
+ clientOperationId,
2801
+ artifactId: requireFlag(ctx.flags, 'artifact'),
2802
+ runId: getFlag(ctx.flags, 'run'),
2803
+ referenceAssetIds: [
2804
+ ...new Set([
2805
+ ...getFlagValues(ctx.flags, 'reference'),
2806
+ ...(Array.isArray(report.referenceAssetIds)
2807
+ ? report.referenceAssetIds.filter((id) => typeof id === 'string')
2808
+ : []),
2809
+ ]),
2810
+ ],
2811
+ evaluator: {
2812
+ name: getFlag(ctx.flags, 'agent') ?? 'external-agent',
2813
+ version: getFlag(ctx.flags, 'evaluator-version'),
2814
+ },
2815
+ report: report,
2816
+ }, { idempotencyKey: clientOperationId });
2817
+ print(evaluation);
2818
+ if (hasFlag(ctx.flags, 'require-pass'))
2819
+ process.exitCode =
2820
+ evaluation.report.verdict === 'pass'
2821
+ ? 0
2822
+ : evaluation.report.verdict === 'fail'
2823
+ ? 1
2824
+ : 3;
2825
+ };
2826
+ /**
2827
+ * `assethub runs upload <path>` — push a local artifact-graph run folder to
2828
+ * Production Control.
2829
+ *
2830
+ * The wire format is `ag.registry.v1`, identical to what the Cloudflare
2831
+ * ag-registry accepts, so the same folder can go to either destination without
2832
+ * conversion. See src/runsUpload/ for the plan/transport split, and
2833
+ * src/runsUpload/controlEndpoints.ts for every path and size limit.
2834
+ */
2835
+ const commandRuns = async (subcommand, positionals, ctx) => {
2836
+ if (subcommand === 'list') {
2837
+ const { canvas } = await canvasForRead(ctx);
2838
+ print(await ctx.client.v2.listCanvasRuns(canvas.id, pageOptions(ctx.flags)));
2839
+ return;
2840
+ }
2841
+ if (subcommand === 'get' || subcommand === 'watch') {
2842
+ const id = requirePositional(positionals, 2, 'run-id');
2843
+ const execution = subcommand === 'get'
2844
+ ? await ctx.client.v2.getRun(id)
2845
+ : await waitForExecution(ctx.client, id, watchOptions(ctx.flags));
2846
+ print({ execution });
2847
+ process.exitCode = executionExitCode(execution);
2848
+ return;
2849
+ }
2850
+ if (subcommand === 'resume') {
2851
+ const result = await resumeRecorded({
2852
+ ...stateOptions(ctx),
2853
+ operationId: requirePositional(positionals, 2, 'operation-id'),
2854
+ });
2855
+ await printRecorded(ctx, result);
2856
+ return;
2857
+ }
2858
+ const args = parseRunsUploadArgs(subcommand, positionals, ctx.flags);
2859
+ const folder = await readGraphFolder(resolve(args.folderPath));
2860
+ const plan = await buildRunUploadPlan({
2861
+ folder,
2862
+ graphId: args.graphId,
2863
+ streamId: args.streamId,
2864
+ rev: args.rev,
2865
+ description: args.description,
2866
+ tags: args.tags,
2867
+ });
2868
+ for (const warning of plan.warnings) {
2869
+ stderr.write(`[warn] ${warning}\n`);
2870
+ }
2871
+ const summary = runsUploadSummary(folder.path, plan);
2872
+ // --dry-run stops here on purpose: everything above is the validation a
2873
+ // corrupt run folder fails, so this is how you find out before production
2874
+ // sees it.
2875
+ if (args.dryRun) {
2876
+ print(runsUploadDryRunReport(summary, plan, ctx.auth.baseUrl));
2877
+ return;
2878
+ }
2879
+ const result = await uploadRun({
2880
+ plan,
2881
+ baseUrl: ctx.auth.baseUrl,
2882
+ apiKey: ctx.auth.apiKey,
2883
+ skipRegister: args.skipRegister,
2884
+ onProgress: event => {
2885
+ if (event.kind === 'register') {
2886
+ stderr.write(`[upload] register graph=${event.graphId}\n`);
2887
+ return;
2888
+ }
2889
+ if (event.kind === 'blob') {
2890
+ stderr.write(`[upload] blob ${event.index + 1}/${event.total} ${event.status} ` +
2891
+ `${event.blob.name} (${formatBytes(event.blob.size)})\n`);
2892
+ return;
2893
+ }
2894
+ stderr.write(`[upload] snapshot graph=${event.graphId} rev=${event.rev}\n`);
2895
+ },
2896
+ });
2897
+ print({ ...summary, ...result });
2898
+ };
2899
+ /**
2900
+ * Everything `runs upload` decides from argv, split out from the command so the
2901
+ * subcommand routing and flag handling are testable without a filesystem, an
2902
+ * API key, or a network.
2903
+ */
2904
+ export const parseRunsUploadArgs = (subcommand, positionals, flags) => {
2905
+ if (subcommand !== 'upload') {
2906
+ throw new Error('Unknown runs command. Use "runs upload <path>".');
2907
+ }
2908
+ const folderPath = positionals[2];
2909
+ if (folderPath == null || folderPath.trim().length === 0) {
2910
+ throw new Error('assethub runs upload needs the path to a run folder (the directory holding manifest.json, nodes.jsonl and edges.jsonl).');
2911
+ }
2912
+ return {
2913
+ folderPath,
2914
+ graphId: getFlag(flags, 'graph-id'),
2915
+ streamId: getFlag(flags, 'stream-id'),
2916
+ rev: parseNonNegativeIntegerFlag(flags, 'rev'),
2917
+ description: getFlag(flags, 'description') ?? '',
2918
+ tags: getFlagValues(flags, 'tag'),
2919
+ dryRun: hasFlag(flags, 'dry-run'),
2920
+ skipRegister: hasFlag(flags, 'skip-register'),
2921
+ };
2922
+ };
2923
+ export const runsUploadSummary = (runFolder, plan) => ({
2924
+ runFolder,
2925
+ graphId: plan.graphId,
2926
+ streamId: plan.streamId,
2927
+ rev: plan.rev,
2928
+ graphHash: plan.graphHash,
2929
+ counts: plan.counts,
2930
+ snapshotSize: formatBytes(plan.sizes.pushBytes),
2931
+ blobBytes: formatBytes(plan.counts.blobBytes),
2932
+ });
2933
+ export const runsUploadDryRunReport = (summary, plan, baseUrl) => ({
2934
+ dryRun: true,
2935
+ ...summary,
2936
+ warnings: plan.warnings,
2937
+ blobs: plan.blobs.map(blob => ({
2938
+ blobKey: blob.blobKey,
2939
+ name: blob.name,
2940
+ mime: blob.mime,
2941
+ size: blob.size,
2942
+ })),
2943
+ wouldUpload: `${plan.blobs.length} blob(s) then 1 snapshot to ${baseUrl}`,
2944
+ });
2945
+ const parseNonNegativeIntegerFlag = (flags, name) => {
2946
+ const raw = getFlag(flags, name);
2947
+ if (raw == null || raw === 'true') {
2948
+ return undefined;
2949
+ }
2950
+ const parsed = Number(raw);
2951
+ if (!Number.isInteger(parsed) || parsed < 0) {
2952
+ throw new Error(`--${name} must be a non-negative integer`);
2953
+ }
2954
+ return parsed;
2955
+ };
1649
2956
  const run = async () => {
1650
2957
  const parsed = parseArgs(argv.slice(2));
1651
2958
  if (hasFlag(parsed.flags, 'help') || parsed.positionals.length === 0) {
@@ -1653,12 +2960,96 @@ const run = async () => {
1653
2960
  return;
1654
2961
  }
1655
2962
  const [command, subcommand] = parsed.positionals;
2963
+ if (command === 'project') {
2964
+ if (subcommand !== 'ingest')
2965
+ throw new Error('Use project ingest <source-dir> --out-dir <directory>');
2966
+ const result = await ingestProjectSources({
2967
+ sourceDir: requirePositional(parsed.positionals, 2, 'source-dir'),
2968
+ outDir: requireFlag(parsed.flags, 'out-dir'),
2969
+ excludedDirectories: getFlagValues(parsed.flags, 'exclude-dir').map(value => assertFlagHasValue(value, '--exclude-dir <relative-path>')),
2970
+ onProgress: message => {
2971
+ stderr.write(`[ingest] ${message}\n`);
2972
+ },
2973
+ });
2974
+ print(result);
2975
+ if (result.status !== 'complete')
2976
+ process.exitCode = 3;
2977
+ return;
2978
+ }
2979
+ if (command === 'workspace') {
2980
+ if (!hasFlag(parsed.flags, 'internal')) {
2981
+ throw new Error('Workspace operations currently require --internal and the AssetHub monorepo with its existing Infisical environment.');
2982
+ }
2983
+ const root = fileURLToPath(new URL('../../../', import.meta.url));
2984
+ const script = join(root, 'apps/frontend/scripts/assethub-workspace.ts');
2985
+ await readFile(script).catch(() => {
2986
+ throw new Error('Internal workspace, moodboard and context operations require the AssetHub monorepo checkout and its native application dependencies.');
2987
+ });
2988
+ await new Promise((done, reject) => {
2989
+ const child = spawn(process.execPath, [
2990
+ '--import',
2991
+ 'tsx',
2992
+ script,
2993
+ ...argv.slice(3).filter(arg => arg !== '--internal'),
2994
+ ], {
2995
+ cwd: root,
2996
+ env: {
2997
+ ...env,
2998
+ TSX_TSCONFIG_PATH: join(root, 'apps/frontend/tsconfig.json'),
2999
+ },
3000
+ stdio: 'inherit',
3001
+ });
3002
+ child.once('error', reject);
3003
+ child.once('exit', code => code === 0
3004
+ ? done()
3005
+ : reject(new Error(`Internal workspace operation exited with ${code ?? 'a signal'}`)));
3006
+ });
3007
+ return;
3008
+ }
1656
3009
  if (command === 'auth') {
1657
3010
  await commandAuth(subcommand, parsed.flags);
1658
3011
  return;
1659
3012
  }
1660
- const ctx = await createContext(parsed.flags);
3013
+ // The experimental demo uses an unpublished workspace development dependency.
3014
+ // Resolve it only for this command so standalone project commands can load.
3015
+ if (command === 'autopilot') {
3016
+ const { commandAutopilot } = await import('./autopilot.js').catch((error) => {
3017
+ if (error instanceof Error &&
3018
+ 'code' in error &&
3019
+ error.code === 'ERR_MODULE_NOT_FOUND' &&
3020
+ error.message.includes('@assethub/autopilot-core')) {
3021
+ throw new Error('The experimental autopilot demo requires the AssetHub monorepo with @assethub/autopilot-core installed. Standalone project workflow commands are available without it.');
3022
+ }
3023
+ throw error;
3024
+ });
3025
+ await commandAutopilot(parsed.positionals, parsed.flags);
3026
+ return;
3027
+ }
3028
+ const ctx = await createContext(parsed.flags, argv.slice(2));
1661
3029
  switch (command) {
3030
+ case 'language':
3031
+ await commandLanguage(subcommand, ctx);
3032
+ return;
3033
+ case 'moodboard':
3034
+ await commandMoodboard(subcommand, parsed.positionals, ctx);
3035
+ return;
3036
+ case 'canvas':
3037
+ await commandCanvas(subcommand, parsed.positionals, ctx);
3038
+ return;
3039
+ case 'graph':
3040
+ await commandGraph(subcommand, ctx);
3041
+ return;
3042
+ case 'evaluations':
3043
+ await commandEvaluations(subcommand, parsed.positionals, ctx);
3044
+ return;
3045
+ case 'evaluate':
3046
+ if (subcommand !== 'list')
3047
+ throw new Error('Server evaluators are capability-gated. Use evaluate list, or evaluations submit for external agent reports.');
3048
+ print((await ctx.client.v2.getCapabilities()).evaluators);
3049
+ return;
3050
+ case 'capabilities':
3051
+ print(await ctx.client.v2.getCapabilities());
3052
+ return;
1662
3053
  case 'models':
1663
3054
  await commandModels(subcommand, parsed.positionals, ctx);
1664
3055
  return;
@@ -1674,12 +3065,21 @@ const run = async () => {
1674
3065
  case 'mesh':
1675
3066
  await commandMesh(subcommand, ctx);
1676
3067
  return;
3068
+ case 'rig':
3069
+ await commandRig(subcommand, ctx);
3070
+ return;
3071
+ case 'animate':
3072
+ await commandAnimate(subcommand, ctx);
3073
+ return;
1677
3074
  case 'jobs':
1678
3075
  await commandJobs(subcommand, parsed.positionals, ctx);
1679
3076
  return;
1680
3077
  case 'production':
1681
3078
  await commandProduction(subcommand, parsed.positionals, ctx);
1682
3079
  return;
3080
+ case 'runs':
3081
+ await commandRuns(subcommand, parsed.positionals, ctx);
3082
+ return;
1683
3083
  case 'parts':
1684
3084
  await commandParts(subcommand, ctx);
1685
3085
  return;
@@ -1687,15 +3087,64 @@ const run = async () => {
1687
3087
  throw new Error(`Unknown command: ${command}`);
1688
3088
  }
1689
3089
  };
1690
- run().catch((error) => {
1691
- if (error instanceof AssetHubApiError) {
1692
- stderr.write(`AssetHub API error [${error.status}] ${error.code}: ${error.message}\n`);
1693
- }
1694
- else if (error instanceof Error) {
1695
- stderr.write(`${error.message}\n`);
3090
+ const isMainModule = (() => {
3091
+ const entry = argv[1];
3092
+ if (entry == null)
3093
+ return false;
3094
+ try {
3095
+ return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(entry);
1696
3096
  }
1697
- else {
1698
- stderr.write(`${String(error)}\n`);
3097
+ catch {
3098
+ return false;
1699
3099
  }
1700
- exit(1);
1701
- });
3100
+ })();
3101
+ if (isMainModule) {
3102
+ process.once('SIGINT', () => {
3103
+ print({
3104
+ error: {
3105
+ code: 'INTERRUPTED',
3106
+ message: 'Client interrupted; server execution continues',
3107
+ },
3108
+ ...activeExecution,
3109
+ });
3110
+ exit(130);
3111
+ });
3112
+ run().catch((error) => {
3113
+ if (error instanceof AssetHubApiError) {
3114
+ stderr.write(`AssetHub API error [${error.status}] ${error.code}: ${error.message}\n`);
3115
+ }
3116
+ else if (error instanceof Error) {
3117
+ stderr.write(`${error.message}\n`);
3118
+ }
3119
+ else {
3120
+ stderr.write(`${String(error)}\n`);
3121
+ }
3122
+ print({
3123
+ error: {
3124
+ code: error instanceof AssetHubApiError
3125
+ ? error.code
3126
+ : error instanceof CliExecutionError && error.exitCode === 3
3127
+ ? 'WAIT_TIMEOUT'
3128
+ : 'CLI_ERROR',
3129
+ message: error instanceof Error ? error.message : String(error),
3130
+ },
3131
+ runId: error instanceof CliExecutionError
3132
+ ? error.runId
3133
+ : activeExecution.runId,
3134
+ operationId: error instanceof CliExecutionError
3135
+ ? error.operationId
3136
+ : activeExecution.operationId,
3137
+ execution: error instanceof CliExecutionError
3138
+ ? error.execution
3139
+ : activeExecution.execution,
3140
+ });
3141
+ exit(error instanceof CliExecutionError
3142
+ ? error.exitCode
3143
+ : error instanceof AssetHubApiError &&
3144
+ error.status >= 500 &&
3145
+ error.code !== 'CANVAS_EXECUTION_UNAVAILABLE' &&
3146
+ error.code !== 'EVALUATOR_UNAVAILABLE'
3147
+ ? 1
3148
+ : 2);
3149
+ });
3150
+ }