@assethub/cli 0.1.2 → 0.1.4

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 (3) hide show
  1. package/README.md +36 -5
  2. package/dist/index.js +705 -152
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -6,13 +6,13 @@ Command line interface for AssetHub.
6
6
  export ASSETHUB_API_KEY=ah_live_xxx
7
7
 
8
8
  assethub models list
9
- assethub models list --domain meshGen --ids-only
10
9
  assethub files import https://example.com/input.png --media-type image
11
10
  assethub source create --file ./input.png --media-type image
12
11
  assethub image generate --prompt "stylized prop concept" --wait
13
- assethub mesh generate --file ./input.png --model-id meshGen.hunyuan31 --wait
12
+ assethub mesh generate --file ./input.png --wait
14
13
  assethub jobs watch job_123 --download --out-dir ./out/job
15
14
  assethub parts split --file ./input.png --part-extractor "V1.5" --wait --all-ready --download --out-dir ./out/parts
15
+ assethub parts compare --file ./input.png --preprocess-prompt "clean white background, centered product photo" --part-extractor "V1.5" --wait --all-ready
16
16
  ```
17
17
 
18
18
  Configuration:
@@ -26,21 +26,41 @@ Agent-friendly login:
26
26
  ```sh
27
27
  printf '%s' "$ASSETHUB_API_KEY" | assethub auth login --api-key-stdin
28
28
  assethub image generate --prompt "small stylized game asset chest" --wait --download --out-dir ./out/image
29
- assethub mesh generate --source-url https://example.com/input.png --model-id meshGen.hunyuan31 --wait --download --out-dir ./out/mesh
29
+ assethub mesh generate --source-url https://example.com/input.png --wait --download --out-dir ./out/mesh
30
30
  cat ./character.png | assethub parts split --stdin --file-name character.png --part-extractor "V1.5" --wait --all-ready --download --out-dir ./out/parts
31
+ base64 -i ./character.png | assethub parts split --stdin-base64 --file-name character.png --content-type image/png --part-extractor "V1.5" --wait --all-ready
32
+ printf '{"image_url":{"url":"data:image/png;base64,..."}}' | assethub parts split --stdin-json --part-extractor "V1.5" --wait --all-ready
33
+ assethub parts split --clipboard --file-name character.png --part-extractor "V1.5" --wait --all-ready
34
+ cat ./character.png | assethub parts compare --stdin --file-name character.png --preprocess-prompt "clean white background" --part-extractor "V1.5" --wait --all-ready
31
35
  ```
32
36
 
33
37
  Agent-oriented input:
34
38
 
35
39
  - `--file <path>` uploads a local image or mesh through the public file API.
36
- - `--stdin --file-name <name>` accepts piped bytes. Give a file name with an
37
- extension when possible.
40
+ - `--stdin --file-name <name>` accepts raw piped bytes. Give a file name with
41
+ an extension when possible.
42
+ - `--stdin-base64 --file-name <name> --content-type <type>` accepts raw base64
43
+ from stdin. It also accepts a full `data:*;base64,...` value.
44
+ - `--stdin-data-uri` accepts a data URI from stdin.
45
+ - `--stdin-json` accepts common agent payload shapes from stdin, including
46
+ OpenAI-style `{"image_url":{"url":"..."}}`, Anthropic-style
47
+ `{"source":{"type":"base64","media_type":"image/png","data":"..."}}`,
48
+ `resourceId`, `fileRef`, `dataUri`, `base64`, `url`, and local `path`.
49
+ - `--source-json <json|@file|@->` parses the same JSON shapes from an argument,
50
+ a file, or stdin.
38
51
  - `--data-uri <uri>` accepts `data:image/png;base64,...` style inputs and uploads
39
52
  them before running the target command.
53
+ - `--clipboard` reads an image from the macOS clipboard for image inputs. It
54
+ uses `pngpaste` when available and falls back to the system clipboard bridge.
40
55
  - `--source-url`, `--source-id`/`--source-resource-id`, and `--file-ref-json`
41
56
  can be reused across image, mesh, and part extraction commands where the API
42
57
  supports them.
43
58
 
59
+ For Codex or other AI agents, the important contract is: if the runtime can
60
+ expose the attachment as bytes, base64, a data URI, a local path, an
61
+ OpenAI/Anthropic-style JSON object, or a URL, the CLI can consume it directly.
62
+ It does not require writing a temporary file first.
63
+
44
64
  Agent-oriented output:
45
65
 
46
66
  - stdout is one final JSON object.
@@ -48,6 +68,17 @@ Agent-oriented output:
48
68
  - `--download --out-dir <dir>` recursively downloads public artifact URLs from
49
69
  job or production status payloads and writes a `manifest.json`.
50
70
 
71
+ `assethub parts compare` runs the normal part split first. When
72
+ `--preprocess-prompt <text>` is provided, it then uses the Nano Banana image
73
+ model to generate a preprocessed image, waits for that image job to finish, and
74
+ runs a second split on the generated image. The final JSON contains `direct`
75
+ and `preprocessed` sections so agents can compare both outputs. If
76
+ preprocessing fails, the direct result is still printed with
77
+ `preprocessed.ok: false`; pass `--fail-on-preprocess-error` when a non-zero exit
78
+ is preferred. Use `--all-ready` rather than explicit `--task-id` values when
79
+ preprocessing is enabled, because the generated-image split creates a separate
80
+ production order.
81
+
51
82
  Image generation defaults to the deployed `v1` endpoint. Pass `--api-version v2`
52
83
  only when the `v2` image endpoint is available in the target environment.
53
84
 
package/dist/index.js CHANGED
@@ -1,8 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import { chmod, mkdir, readFile, rename, writeFile } from 'node:fs/promises';
3
- import { homedir } from 'node:os';
2
+ import { execFile } from 'node:child_process';
3
+ import { chmod, mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
4
+ import { homedir, tmpdir } from 'node:os';
4
5
  import { basename, dirname, extname, join } from 'node:path';
5
6
  import { argv, env, exit, stdin, stderr, stdout } from 'node:process';
7
+ import { promisify } from 'node:util';
6
8
  import { AssetHubApiError, createAssetHubClient, } from '@assethub/api-client';
7
9
  const defaultBaseUrl = 'https://app.assethub.io';
8
10
  const defaultImageModelId = 'imageGen.nanoBanana.pro.openrouter';
@@ -47,6 +49,7 @@ const partExtractorPublicNameByInput = new Map(partExtractorOptions.flatMap(opti
47
49
  ]),
48
50
  ]));
49
51
  const isInternalPartExtractorId = (value) => value.trim().includes('_');
52
+ const execFileAsync = promisify(execFile);
50
53
  const usage = `AssetHub CLI
51
54
 
52
55
  Usage:
@@ -58,17 +61,18 @@ Usage:
58
61
  assethub models get <model-id>
59
62
  assethub files import <url> --media-type image|mesh
60
63
  assethub files upload <file> --media-type image|mesh
61
- assethub source create (--file <path> | --source-url <url> | --source-id <id> | --stdin | --data-uri <uri>) --media-type image|mesh
62
- assethub image generate --prompt <text> [--file <path> | --source-url <url> | --stdin | --data-uri <uri>] [--model-id <id>] [--batch-size <n>] [--wait] [--download --out-dir <dir>] [--api-version v1|v2]
64
+ 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>]
65
+ 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]
63
66
  assethub image generate --prompt <text> --source-resource-id <id> --api-version v2 [--model-id <id>] [--batch-size <n>] [--wait]
64
- assethub mesh generate (--file <path> | --source-url <url> | --source-resource-id <id> | --file-ref-json <json> | --stdin | --data-uri <uri>) [--model-id <id>] [--name <name>] [--wait] [--download --out-dir <dir>]
65
- assethub mesh retopo (--source-resource-id <id> | --source-url <url> | --file-ref-json <json> | --stdin | --data-uri <uri>) --model-id <id> [--wait] [--download --out-dir <dir>]
67
+ 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>]
68
+ 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>]
66
69
  assethub jobs get <job-id> [--download --out-dir <dir>]
67
70
  assethub jobs watch <job-id> [--interval-ms <ms>] [--timeout-ms <ms>] [--download --out-dir <dir>]
68
- assethub production analyze (--file <path> | --source-url <url> | --source-id <id> | --image-url <url> | --file-ref-json <json> | --stdin | --data-uri <uri>) [--part-extractor <name>] [--name <name>] [--wait] [--download --out-dir <dir>]
71
+ 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>]
69
72
  assethub production status <order-id> [--wait] [--download --out-dir <dir>]
70
73
  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>]
71
- assethub parts split (--file <path> | --source-url <url> | --source-id <id> | --image-url <url> | --file-ref-json <json> | --stdin | --data-uri <uri> | --order-id <id>) [--part-extractor <name>] [--wait] [--task-id <id>...] [--mission-id <id>] [--all-ready] [--download --out-dir <dir>]
74
+ 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>]
75
+ 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>]
72
76
 
73
77
  Global options:
74
78
  --api-key <key> Overrides saved auth and ASSETHUB_API_KEY.
@@ -80,14 +84,19 @@ Global options:
80
84
  Examples:
81
85
  ASSETHUB_API_KEY=ah_live_xxx assethub models list
82
86
  assethub auth login --api-key-stdin
83
- assethub models list --domain meshGen --ids-only
87
+ assethub models list --ids-only
84
88
  assethub files upload ./input.png --media-type image
85
89
  assethub source create --file ./input.png --media-type image
90
+ pbpaste | assethub source create --stdin-data-uri --media-type image
91
+ assethub source create --clipboard --media-type image --file-name clipboard.png
92
+ printf '{"image_url":{"url":"data:image/png;base64,..."}}' | assethub parts split --stdin-json --part-extractor "V1.5" --wait
93
+ base64 -i ./input.png | assethub parts split --stdin-base64 --file-name input.png --content-type image/png --part-extractor "V1.5" --wait
86
94
  cat ./input.png | assethub parts split --stdin --file-name input.png --part-extractor "V1.5" --wait
87
95
  assethub image generate --prompt "stylized prop concept" --wait
88
- assethub mesh generate --file ./input.png --model-id meshGen.hunyuan31 --wait
96
+ assethub mesh generate --file ./input.png --wait
89
97
  assethub production analyze --file ./input.png --part-extractor "V1.5" --wait
90
98
  assethub parts split --file ./input.png --part-extractor "V1.5" --wait --all-ready
99
+ assethub parts compare --file ./input.png --preprocess-prompt "clean white background, centered product photo" --part-extractor "V1.5" --wait --all-ready
91
100
  `;
92
101
  const print = (value) => {
93
102
  stdout.write(`${JSON.stringify(value, null, 2)}\n`);
@@ -325,31 +334,73 @@ const createContext = async (flags) => {
325
334
  }),
326
335
  };
327
336
  };
337
+ const isRecord = (value) => value != null && typeof value === 'object' && !Array.isArray(value);
338
+ const isBlobLocation = (value) => {
339
+ if (!isRecord(value) || typeof value.type !== 'string') {
340
+ return false;
341
+ }
342
+ if (value.type === 'supabase' &&
343
+ typeof value.bucket === 'string' &&
344
+ typeof value.path === 'string') {
345
+ return true;
346
+ }
347
+ if (value.type === 'signedSupabase' &&
348
+ typeof value.signedUrl === 'string') {
349
+ return true;
350
+ }
351
+ if (value.type === 'asset' && typeof value.assetId === 'string') {
352
+ return true;
353
+ }
354
+ return false;
355
+ };
328
356
  const parseFileRef = (value) => {
329
357
  const parsed = JSON.parse(value);
330
- if (parsed == null || typeof parsed !== 'object' || !('type' in parsed)) {
358
+ if (!isBlobLocation(parsed)) {
331
359
  throw new Error('--file-ref-json must be a BlobLocation JSON object');
332
360
  }
333
361
  return parsed;
334
362
  };
335
- const readBlob = async (filePath) => {
336
- const buffer = await readFile(filePath);
337
- return new Blob([new Uint8Array(buffer)]);
363
+ const blobFromBuffer = (buffer, type) => {
364
+ const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
365
+ return new Blob([arrayBuffer], type == null ? undefined : { type });
338
366
  };
339
- const blobFromDataUri = (value) => {
340
- const match = value.match(/^data:([^;,]+)?(;base64)?,(.*)$/s);
367
+ const readBlob = async (filePath, type) => blobFromBuffer(await readFile(filePath), type);
368
+ const getContentTypeFlag = (flags) => getFlag(flags, 'content-type') ?? getFlag(flags, 'mime-type');
369
+ const blobFromDataUri = (value, flagName = '--data-uri') => {
370
+ const trimmed = value.trim();
371
+ const match = trimmed.match(/^data:([^;,]+)?(;base64)?,(.*)$/s);
341
372
  if (match == null) {
342
- throw new Error('--data-uri must be a valid data URI.');
373
+ throw new Error(`${flagName} must be a valid data URI.`);
343
374
  }
344
375
  const contentType = match[1] === '' ? undefined : match[1];
345
376
  const isBase64 = match[2] != null;
346
- const payload = match[3] ?? '';
377
+ const payload = (match[3] ?? '').replace(/\s+/g, '');
347
378
  const buffer = isBase64
348
379
  ? Buffer.from(payload, 'base64')
349
380
  : Buffer.from(decodeURIComponent(payload), 'utf8');
350
- const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
351
381
  return {
352
- blob: new Blob([arrayBuffer], contentType == null ? undefined : { type: contentType }),
382
+ blob: blobFromBuffer(buffer, contentType),
383
+ contentType,
384
+ };
385
+ };
386
+ const blobFromBase64 = (value, contentType, flagName) => {
387
+ const trimmed = value.trim();
388
+ if (trimmed.startsWith('data:')) {
389
+ return blobFromDataUri(trimmed, flagName);
390
+ }
391
+ const normalized = trimmed.replace(/\s+/g, '').replace(/-/g, '+').replace(/_/g, '/');
392
+ if (normalized === '') {
393
+ throw new Error(`${flagName} input is empty.`);
394
+ }
395
+ if (!/^[A-Za-z0-9+/]+={0,2}$/.test(normalized)) {
396
+ throw new Error(`${flagName} must contain base64 data or a data URI.`);
397
+ }
398
+ const buffer = Buffer.from(normalized, 'base64');
399
+ if (buffer.byteLength === 0) {
400
+ throw new Error(`${flagName} did not decode to any bytes.`);
401
+ }
402
+ return {
403
+ blob: blobFromBuffer(buffer, contentType),
353
404
  contentType,
354
405
  };
355
406
  };
@@ -374,54 +425,392 @@ const ensureFileNameExtension = (fileName, mediaType, mimeType) => {
374
425
  }
375
426
  return `${fileName}.${extensionFromMimeType(mimeType, mediaType)}`;
376
427
  };
428
+ 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.';
429
+ 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.';
430
+ const getSourceUrlFlag = (flags) => getFlag(flags, 'source-url') ?? getFlag(flags, 'image-url') ?? getFlag(flags, 'url');
431
+ const assertFlagHasValue = (value, flagDescription) => {
432
+ if (value === 'true' || value.trim() === '') {
433
+ throw new Error(`Missing required flag value: ${flagDescription}`);
434
+ }
435
+ return value;
436
+ };
437
+ const sourceInputCount = (flags) => [
438
+ getSourceResourceIdFlag(flags) != null,
439
+ getSourceUrlFlag(flags) != null,
440
+ getFlag(flags, 'file-ref-json') != null,
441
+ getFlag(flags, 'file') != null,
442
+ getFlag(flags, 'data-uri') != null,
443
+ hasFlag(flags, 'stdin'),
444
+ hasFlag(flags, 'stdin-base64'),
445
+ hasFlag(flags, 'stdin-data-uri'),
446
+ hasFlag(flags, 'stdin-json'),
447
+ getFlag(flags, 'source-json') != null,
448
+ hasFlag(flags, 'clipboard'),
449
+ ].filter(Boolean).length;
450
+ const sourceFromString = (value) => {
451
+ const trimmed = value.trim();
452
+ if (trimmed === '' || trimmed.startsWith('data:')) {
453
+ return undefined;
454
+ }
455
+ try {
456
+ const url = new URL(trimmed);
457
+ if (url.protocol === 'http:' || url.protocol === 'https:') {
458
+ return { url: trimmed };
459
+ }
460
+ }
461
+ catch {
462
+ // Ignore non-URL strings.
463
+ }
464
+ return undefined;
465
+ };
466
+ const getFirstString = (record, keys) => {
467
+ for (const key of keys) {
468
+ const value = record[key];
469
+ if (typeof value === 'string' && value.trim() !== '') {
470
+ return value;
471
+ }
472
+ }
473
+ return undefined;
474
+ };
475
+ const stringFromJsonImageUrl = (value) => {
476
+ if (typeof value === 'string' && value.trim() !== '') {
477
+ return value;
478
+ }
479
+ if (isRecord(value)) {
480
+ return getFirstString(value, ['url']);
481
+ }
482
+ return undefined;
483
+ };
484
+ const descriptorFromDataUri = (value, mediaType, fileName, flagName) => {
485
+ const { blob, contentType } = blobFromDataUri(value, flagName);
486
+ return {
487
+ kind: 'blob',
488
+ blob,
489
+ fileName: ensureFileNameExtension(fileName, mediaType, contentType),
490
+ contentType,
491
+ };
492
+ };
493
+ const descriptorFromBase64 = ({ value, contentType, fileName, mediaType, flagName, }) => {
494
+ const decoded = blobFromBase64(value, contentType, flagName);
495
+ return {
496
+ kind: 'blob',
497
+ blob: decoded.blob,
498
+ fileName: ensureFileNameExtension(fileName, mediaType, decoded.contentType),
499
+ contentType: decoded.contentType,
500
+ };
501
+ };
502
+ const sourceDescriptorFromJson = (value, options) => {
503
+ if (typeof value === 'string') {
504
+ const trimmed = value.trim();
505
+ if (trimmed === '') {
506
+ return undefined;
507
+ }
508
+ if (trimmed.startsWith('data:')) {
509
+ return descriptorFromDataUri(trimmed, options.mediaType, options.fallbackFileName, '--source-json');
510
+ }
511
+ const source = sourceFromString(trimmed);
512
+ return source == null ? undefined : { kind: 'source', source };
513
+ }
514
+ if (Array.isArray(value)) {
515
+ for (const item of value) {
516
+ const found = sourceDescriptorFromJson(item, options);
517
+ if (found != null) {
518
+ return found;
519
+ }
520
+ }
521
+ return undefined;
522
+ }
523
+ if (!isRecord(value)) {
524
+ return undefined;
525
+ }
526
+ const nestedSource = value.source;
527
+ if (nestedSource != null) {
528
+ const found = sourceDescriptorFromJson(nestedSource, options);
529
+ if (found != null) {
530
+ return found;
531
+ }
532
+ }
533
+ const imageUrl = stringFromJsonImageUrl(value.image_url ?? value.imageUrl);
534
+ if (imageUrl != null) {
535
+ if (imageUrl.trim().startsWith('data:')) {
536
+ return descriptorFromDataUri(imageUrl, options.mediaType, options.fallbackFileName, '--source-json');
537
+ }
538
+ return { kind: 'source', source: { url: imageUrl } };
539
+ }
540
+ const resourceId = getFirstString(value, [
541
+ 'resourceId',
542
+ 'resource_id',
543
+ 'sourceResourceId',
544
+ 'source_resource_id',
545
+ 'sourceId',
546
+ 'source_id',
547
+ 'imageAssetId',
548
+ 'image_asset_id',
549
+ 'assetId',
550
+ 'asset_id',
551
+ ]);
552
+ if (resourceId != null) {
553
+ return { kind: 'source', source: { resourceId } };
554
+ }
555
+ const fileRefValue = value.fileRef ?? value.file_ref ?? value.imageBlobLocation ?? value.image_blob_location ?? value.blobLocation ?? value.blob_location;
556
+ if (isBlobLocation(fileRefValue)) {
557
+ return { kind: 'source', source: { fileRef: fileRefValue } };
558
+ }
559
+ const dataUri = getFirstString(value, ['dataUri', 'data_uri']);
560
+ if (dataUri != null) {
561
+ return descriptorFromDataUri(dataUri, options.mediaType, getFirstString(value, ['fileName', 'file_name', 'name']) ??
562
+ options.fallbackFileName, '--source-json');
563
+ }
564
+ const contentType = getFirstString(value, [
565
+ 'contentType',
566
+ 'content_type',
567
+ 'mimeType',
568
+ 'mime_type',
569
+ 'media_type',
570
+ ]);
571
+ const base64Value = getFirstString(value, ['base64', 'data', 'content']);
572
+ if (base64Value != null && (value.type === 'base64' || contentType != null)) {
573
+ return descriptorFromBase64({
574
+ value: base64Value,
575
+ contentType,
576
+ fileName: getFirstString(value, ['fileName', 'file_name', 'name']) ??
577
+ options.fallbackFileName,
578
+ mediaType: options.mediaType,
579
+ flagName: '--source-json',
580
+ });
581
+ }
582
+ const url = getFirstString(value, ['sourceUrl', 'source_url', 'url', 'publicUrl', 'public_url']);
583
+ if (url != null) {
584
+ const source = sourceFromString(url);
585
+ if (source != null) {
586
+ return { kind: 'source', source };
587
+ }
588
+ }
589
+ const path = getFirstString(value, ['path', 'file', 'filePath', 'file_path']);
590
+ if (path != null) {
591
+ return { kind: 'file', path };
592
+ }
593
+ const preferredKeys = [
594
+ 'input',
595
+ 'image',
596
+ 'asset',
597
+ 'file',
598
+ 'message',
599
+ 'messages',
600
+ 'content',
601
+ 'attachments',
602
+ ];
603
+ for (const key of preferredKeys) {
604
+ const found = sourceDescriptorFromJson(value[key], options);
605
+ if (found != null) {
606
+ return found;
607
+ }
608
+ }
609
+ for (const entry of Object.values(value)) {
610
+ const found = sourceDescriptorFromJson(entry, options);
611
+ if (found != null) {
612
+ return found;
613
+ }
614
+ }
615
+ return undefined;
616
+ };
617
+ const parseJsonValue = (raw, sourceName) => {
618
+ try {
619
+ return JSON.parse(raw);
620
+ }
621
+ catch (error) {
622
+ throw new Error(`${sourceName} must be valid JSON: ${error instanceof Error ? error.message : String(error)}`);
623
+ }
624
+ };
625
+ const readSourceJsonFlag = async (flags) => {
626
+ const value = getFlag(flags, 'source-json');
627
+ if (value == null || value === 'true' || value.trim() === '') {
628
+ throw new Error('Missing required flag value: --source-json <json|@file|@->');
629
+ }
630
+ if (value === '@-' || value === '-') {
631
+ return await readStdin();
632
+ }
633
+ if (value.startsWith('@')) {
634
+ return await readFile(value.slice(1), 'utf8');
635
+ }
636
+ return value;
637
+ };
638
+ const readClipboardImage = async () => {
639
+ const filePath = join(tmpdir(), `assethub-clipboard-${process.pid}-${Date.now()}.png`);
640
+ const readTempFile = async () => await readBlob(filePath, 'image/png');
641
+ try {
642
+ await execFileAsync('pngpaste', [filePath], { timeout: 10000 });
643
+ return await readTempFile();
644
+ }
645
+ catch (pngpasteError) {
646
+ const quoteAppleScriptString = (value) => `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
647
+ const pngClass = `${String.fromCharCode(0x00ab)}class PNGf${String.fromCharCode(0x00bb)}`;
648
+ const script = [
649
+ `set outPath to ${quoteAppleScriptString(filePath)}`,
650
+ `set pngData to the clipboard as ${pngClass}`,
651
+ 'set fileRef to open for access (POSIX file outPath) with write permission',
652
+ 'try',
653
+ 'set eof fileRef to 0',
654
+ 'write pngData to fileRef',
655
+ 'close access fileRef',
656
+ 'on error errMsg',
657
+ 'try',
658
+ 'close access fileRef',
659
+ 'end try',
660
+ 'error errMsg',
661
+ 'end try',
662
+ ].join('\n');
663
+ try {
664
+ await execFileAsync('osascript', ['-e', script], { timeout: 10000 });
665
+ return await readTempFile();
666
+ }
667
+ catch (osascriptError) {
668
+ const details = osascriptError instanceof Error
669
+ ? osascriptError.message
670
+ : pngpasteError instanceof Error
671
+ ? pngpasteError.message
672
+ : 'unknown error';
673
+ throw new Error(`Could not read an image from the clipboard. Copy an image, install pngpaste, or pass bytes through --stdin/--stdin-base64/--stdin-json. Details: ${details}`);
674
+ }
675
+ }
676
+ finally {
677
+ await unlink(filePath).catch(() => undefined);
678
+ }
679
+ };
680
+ const uploadSourceDescriptor = async ({ descriptor, client, mediaType, }) => {
681
+ if (descriptor.kind === 'source') {
682
+ return descriptor.source;
683
+ }
684
+ const file = descriptor.kind === 'file'
685
+ ? await readBlob(descriptor.path, descriptor.contentType)
686
+ : descriptor.blob;
687
+ const fileName = descriptor.kind === 'file'
688
+ ? basename(descriptor.path)
689
+ : descriptor.fileName;
690
+ const upload = await client.v2.uploadFile({
691
+ file,
692
+ fileName: ensureFileNameExtension(fileName, mediaType, descriptor.contentType),
693
+ mediaType,
694
+ });
695
+ return { fileRef: upload.fileRef };
696
+ };
377
697
  const resolveSource = async ({ flags, client, fileMediaType, }) => {
698
+ const providedCount = sourceInputCount(flags);
699
+ if (providedCount === 0) {
700
+ return undefined;
701
+ }
702
+ if (providedCount > 1) {
703
+ throw new Error(exactlyOneSourceMessage);
704
+ }
378
705
  const sourceResourceId = getSourceResourceIdFlag(flags);
379
706
  if (sourceResourceId != null) {
380
- return { resourceId: sourceResourceId };
707
+ return {
708
+ resourceId: assertFlagHasValue(sourceResourceId, '--source-id/--source-resource-id <id>'),
709
+ };
381
710
  }
382
- const sourceUrl = getFlag(flags, 'source-url');
711
+ const sourceUrl = getSourceUrlFlag(flags);
383
712
  if (sourceUrl != null) {
384
- return { url: sourceUrl };
713
+ return {
714
+ url: assertFlagHasValue(sourceUrl, '--source-url/--image-url <url>'),
715
+ };
385
716
  }
386
717
  const fileRefJson = getFlag(flags, 'file-ref-json');
387
718
  if (fileRefJson != null) {
388
- return { fileRef: parseFileRef(fileRefJson) };
719
+ return {
720
+ fileRef: parseFileRef(assertFlagHasValue(fileRefJson, '--file-ref-json <json>')),
721
+ };
389
722
  }
390
723
  const filePath = getFlag(flags, 'file');
391
724
  if (filePath != null) {
392
- const fileRef = await client.v2.uploadFile({
393
- file: await readBlob(filePath),
394
- fileName: basename(filePath),
725
+ return await uploadSourceDescriptor({
726
+ descriptor: { kind: 'file', path: assertFlagHasValue(filePath, '--file <path>') },
727
+ client,
395
728
  mediaType: fileMediaType,
396
729
  });
397
- return { fileRef: fileRef.fileRef };
398
730
  }
399
731
  const dataUri = getFlag(flags, 'data-uri');
400
732
  if (dataUri != null) {
401
- const { blob, contentType } = blobFromDataUri(dataUri);
402
- const fileName = ensureFileNameExtension(getFlag(flags, 'file-name') ?? 'data-uri', fileMediaType, contentType);
403
- const fileRef = await client.v2.uploadFile({
404
- file: blob,
405
- fileName,
733
+ return await uploadSourceDescriptor({
734
+ descriptor: descriptorFromDataUri(assertFlagHasValue(dataUri, '--data-uri <uri>'), fileMediaType, getFlag(flags, 'file-name') ?? 'data-uri', '--data-uri'),
735
+ client,
736
+ mediaType: fileMediaType,
737
+ });
738
+ }
739
+ if (getFlag(flags, 'source-json') != null) {
740
+ const parsed = parseJsonValue(await readSourceJsonFlag(flags), '--source-json');
741
+ const descriptor = sourceDescriptorFromJson(parsed, {
742
+ mediaType: fileMediaType,
743
+ fallbackFileName: getFlag(flags, 'file-name') ?? defaultFileNameForMediaType(fileMediaType),
744
+ });
745
+ if (descriptor == null) {
746
+ throw new Error('--source-json did not contain a usable source. Expected url, image_url.url, resourceId, fileRef, data URI, base64 data, or path.');
747
+ }
748
+ return await uploadSourceDescriptor({ descriptor, client, mediaType: fileMediaType });
749
+ }
750
+ if (hasFlag(flags, 'stdin-json')) {
751
+ const parsed = parseJsonValue(await readStdin(), '--stdin-json');
752
+ const descriptor = sourceDescriptorFromJson(parsed, {
753
+ mediaType: fileMediaType,
754
+ fallbackFileName: getFlag(flags, 'file-name') ?? defaultFileNameForMediaType(fileMediaType),
755
+ });
756
+ if (descriptor == null) {
757
+ throw new Error('--stdin-json did not contain a usable source. Expected url, image_url.url, resourceId, fileRef, data URI, base64 data, or path.');
758
+ }
759
+ return await uploadSourceDescriptor({ descriptor, client, mediaType: fileMediaType });
760
+ }
761
+ if (hasFlag(flags, 'stdin-data-uri')) {
762
+ return await uploadSourceDescriptor({
763
+ descriptor: descriptorFromDataUri(await readStdin(), fileMediaType, getFlag(flags, 'file-name') ?? 'stdin-data-uri', '--stdin-data-uri'),
764
+ client,
765
+ mediaType: fileMediaType,
766
+ });
767
+ }
768
+ if (hasFlag(flags, 'stdin-base64')) {
769
+ return await uploadSourceDescriptor({
770
+ descriptor: descriptorFromBase64({
771
+ value: await readStdin(),
772
+ contentType: getContentTypeFlag(flags),
773
+ fileName: getFlag(flags, 'file-name') ?? defaultFileNameForMediaType(fileMediaType),
774
+ mediaType: fileMediaType,
775
+ flagName: '--stdin-base64',
776
+ }),
777
+ client,
778
+ mediaType: fileMediaType,
779
+ });
780
+ }
781
+ if (hasFlag(flags, 'clipboard')) {
782
+ if (fileMediaType !== 'image') {
783
+ throw new Error('--clipboard is only supported for image inputs.');
784
+ }
785
+ return await uploadSourceDescriptor({
786
+ descriptor: {
787
+ kind: 'blob',
788
+ blob: await readClipboardImage(),
789
+ fileName: ensureFileNameExtension(getFlag(flags, 'file-name') ?? 'clipboard.png', fileMediaType, 'image/png'),
790
+ contentType: 'image/png',
791
+ },
792
+ client,
406
793
  mediaType: fileMediaType,
407
794
  });
408
- return { fileRef: fileRef.fileRef };
409
795
  }
410
796
  if (hasFlag(flags, 'stdin')) {
411
- const fileName = ensureFileNameExtension(getFlag(flags, 'file-name') ?? defaultFileNameForMediaType(fileMediaType), fileMediaType);
412
- const fileRef = await client.v2.uploadFile({
413
- file: await readStdinBlob(),
414
- fileName,
797
+ return await uploadSourceDescriptor({
798
+ descriptor: {
799
+ kind: 'blob',
800
+ blob: await readStdinBlob(getContentTypeFlag(flags)),
801
+ fileName: ensureFileNameExtension(getFlag(flags, 'file-name') ?? defaultFileNameForMediaType(fileMediaType), fileMediaType, getContentTypeFlag(flags)),
802
+ contentType: getContentTypeFlag(flags),
803
+ },
804
+ client,
415
805
  mediaType: fileMediaType,
416
806
  });
417
- return { fileRef: fileRef.fileRef };
418
807
  }
419
808
  return undefined;
420
809
  };
421
810
  const requireSource = async (options) => {
422
811
  const source = await resolveSource(options);
423
812
  if (source == null) {
424
- throw new Error('Missing source. Pass --file, --source-url, --source-resource-id, --source-id, --file-ref-json, --stdin, or --data-uri.');
813
+ throw new Error(missingSourceMessage);
425
814
  }
426
815
  return source;
427
816
  };
@@ -438,10 +827,6 @@ const sourceToV1ImageInput = (source) => {
438
827
  throw new Error('v1 image generation does not accept source-resource-id. Use --source-url, --file, or --api-version v2.');
439
828
  };
440
829
  const resolveProductionAnalyzeSource = async (ctx) => {
441
- const imageUrl = getFlag(ctx.flags, 'image-url');
442
- if (imageUrl != null) {
443
- return { imageUrl };
444
- }
445
830
  const source = await requireSource({
446
831
  flags: ctx.flags,
447
832
  client: ctx.client,
@@ -454,10 +839,28 @@ const resolveProductionAnalyzeSource = async (ctx) => {
454
839
  return { imageUrl: source.url };
455
840
  }
456
841
  if (source.fileRef == null) {
457
- throw new Error('Missing source. Pass --file, --source-url, --source-id, --file-ref-json, --stdin, or --data-uri.');
842
+ throw new Error(missingSourceMessage);
843
+ }
844
+ return { imageBlobLocation: source.fileRef };
845
+ };
846
+ const productionAnalyzeSourceFromSource = (source) => {
847
+ if (source.resourceId != null) {
848
+ return { imageAssetId: source.resourceId };
849
+ }
850
+ if (source.url != null) {
851
+ return { imageUrl: source.url };
458
852
  }
459
853
  return { imageBlobLocation: source.fileRef };
460
854
  };
855
+ const sourceFromProductionAnalyzeSource = (source) => {
856
+ if (source.imageAssetId != null) {
857
+ return { resourceId: source.imageAssetId };
858
+ }
859
+ if (source.imageUrl != null) {
860
+ return { url: source.imageUrl };
861
+ }
862
+ return { fileRef: source.imageBlobLocation };
863
+ };
461
864
  const productionAnalyzeTerminalStatuses = new Set(['ready', 'failed']);
462
865
  const productionExecuteTerminalStatuses = new Set(['completed', 'failed']);
463
866
  const pollProductionStatus = async ({ ctx, orderId, terminalStatuses, }) => {
@@ -502,54 +905,20 @@ const commandSource = async (subcommand, ctx) => {
502
905
  throw new Error('Unknown source command. Use "source create".');
503
906
  }
504
907
  const mediaType = parseMediaType(requireFlag(ctx.flags, 'media-type'));
505
- const sourceUrl = getFlag(ctx.flags, 'source-url') ?? getFlag(ctx.flags, 'image-url');
506
- const sourceResourceId = getSourceResourceIdFlag(ctx.flags);
507
- const filePath = getFlag(ctx.flags, 'file');
508
- const useStdin = hasFlag(ctx.flags, 'stdin');
509
- const dataUri = getFlag(ctx.flags, 'data-uri');
510
- const provided = [
511
- sourceUrl != null,
512
- sourceResourceId != null,
513
- filePath != null,
514
- useStdin,
515
- dataUri != null,
516
- ].filter(Boolean);
517
- if (provided.length !== 1) {
518
- throw new Error('Pass exactly one source input: --file, --source-url, --source-id, --source-resource-id, --stdin, or --data-uri.');
519
- }
520
- if (sourceUrl != null) {
521
- print({ source: { url: sourceUrl }, url: sourceUrl, mediaType });
908
+ const source = await requireSource({
909
+ flags: ctx.flags,
910
+ client: ctx.client,
911
+ fileMediaType: mediaType,
912
+ });
913
+ if (source.url != null) {
914
+ print({ source, url: source.url, mediaType });
522
915
  return;
523
916
  }
524
- if (sourceResourceId != null) {
525
- print({
526
- source: { resourceId: sourceResourceId },
527
- resourceId: sourceResourceId,
528
- mediaType,
529
- });
917
+ if (source.resourceId != null) {
918
+ print({ source, resourceId: source.resourceId, mediaType });
530
919
  return;
531
920
  }
532
- const upload = filePath != null
533
- ? await ctx.client.v2.uploadFile({
534
- file: await readBlob(filePath),
535
- fileName: basename(filePath),
536
- mediaType,
537
- })
538
- : dataUri != null
539
- ? await (async () => {
540
- const { blob, contentType } = blobFromDataUri(dataUri);
541
- return await ctx.client.v2.uploadFile({
542
- file: blob,
543
- fileName: ensureFileNameExtension(getFlag(ctx.flags, 'file-name') ?? 'data-uri', mediaType, contentType),
544
- mediaType,
545
- });
546
- })()
547
- : await ctx.client.v2.uploadFile({
548
- file: await readStdinBlob(),
549
- fileName: ensureFileNameExtension(getFlag(ctx.flags, 'file-name') ?? defaultFileNameForMediaType(mediaType), mediaType),
550
- mediaType,
551
- });
552
- print({ source: { fileRef: upload.fileRef }, fileRef: upload.fileRef, mediaType });
921
+ print({ source, fileRef: source.fileRef, mediaType });
553
922
  };
554
923
  const commandModels = async (subcommand, positionals, ctx) => {
555
924
  if (subcommand === 'list') {
@@ -885,6 +1254,106 @@ const withOptionalDownloads = async (flags, value) => {
885
1254
  const downloads = await maybeDownloadUrls(flags, value);
886
1255
  return downloads == null ? value : { ...value, downloads };
887
1256
  };
1257
+ const firstStringValueForKeys = (value, keys) => {
1258
+ if (value == null || typeof value !== 'object') {
1259
+ return undefined;
1260
+ }
1261
+ if (Array.isArray(value)) {
1262
+ for (const item of value) {
1263
+ const found = firstStringValueForKeys(item, keys);
1264
+ if (found != null) {
1265
+ return found;
1266
+ }
1267
+ }
1268
+ return undefined;
1269
+ }
1270
+ for (const [key, entry] of Object.entries(value)) {
1271
+ if (keys.has(key) && typeof entry === 'string' && entry.trim() !== '') {
1272
+ return entry;
1273
+ }
1274
+ const found = firstStringValueForKeys(entry, keys);
1275
+ if (found != null) {
1276
+ return found;
1277
+ }
1278
+ }
1279
+ return undefined;
1280
+ };
1281
+ const firstBlobLocation = (value) => {
1282
+ if (value == null || typeof value !== 'object') {
1283
+ return undefined;
1284
+ }
1285
+ if (Array.isArray(value)) {
1286
+ for (const item of value) {
1287
+ const found = firstBlobLocation(item);
1288
+ if (found != null) {
1289
+ return found;
1290
+ }
1291
+ }
1292
+ return undefined;
1293
+ }
1294
+ const record = value;
1295
+ if (record.type === 'supabase' && typeof record.bucket === 'string' && typeof record.path === 'string') {
1296
+ return {
1297
+ type: 'supabase',
1298
+ bucket: record.bucket,
1299
+ path: record.path,
1300
+ };
1301
+ }
1302
+ if (record.type === 'signedSupabase' && typeof record.signedUrl === 'string') {
1303
+ return {
1304
+ type: 'signedSupabase',
1305
+ signedUrl: record.signedUrl,
1306
+ };
1307
+ }
1308
+ if (record.type === 'asset' && typeof record.assetId === 'string') {
1309
+ return {
1310
+ type: 'asset',
1311
+ assetId: record.assetId,
1312
+ };
1313
+ }
1314
+ for (const entry of Object.values(record)) {
1315
+ const found = firstBlobLocation(entry);
1316
+ if (found != null) {
1317
+ return found;
1318
+ }
1319
+ }
1320
+ return undefined;
1321
+ };
1322
+ const imageSourceFromCompletedJob = (job) => {
1323
+ if (job.resourceId != null && job.resourceId.trim() !== '') {
1324
+ return { imageAssetId: job.resourceId };
1325
+ }
1326
+ const resourceId = firstStringValueForKeys(job, new Set(['resourceId', 'resource_id', 'assetId', 'asset_id', 'imageAssetId']));
1327
+ if (resourceId != null) {
1328
+ return { imageAssetId: resourceId };
1329
+ }
1330
+ const imageUrl = firstStringValueForKeys(job, new Set(['imageUrl', 'image_url', 'url', 'publicUrl', 'public_url']));
1331
+ if (imageUrl != null) {
1332
+ return { imageUrl };
1333
+ }
1334
+ const blobLocation = firstBlobLocation(job);
1335
+ if (blobLocation != null) {
1336
+ return { imageBlobLocation: blobLocation };
1337
+ }
1338
+ throw new Error('Preprocessing completed, but the image job did not include an image source for part splitting.');
1339
+ };
1340
+ const errorToOutput = (error) => {
1341
+ if (error instanceof AssetHubApiError) {
1342
+ return {
1343
+ name: error.name,
1344
+ message: error.message,
1345
+ status: error.status,
1346
+ code: error.code,
1347
+ };
1348
+ }
1349
+ if (error instanceof Error) {
1350
+ return {
1351
+ name: error.name,
1352
+ message: error.message,
1353
+ };
1354
+ }
1355
+ return { message: String(error) };
1356
+ };
888
1357
  const readyTaskIdsFromStatus = (status) => {
889
1358
  const missions = status != null && typeof status === 'object' && 'missions' in status
890
1359
  ? status.missions
@@ -996,83 +1465,167 @@ const commandProduction = async (subcommand, positionals, ctx) => {
996
1465
  throw new Error('Unknown production command. Use "production analyze", "production status", or "production execute".');
997
1466
  };
998
1467
  const commandParts = async (subcommand, ctx) => {
999
- if (subcommand !== 'split') {
1000
- throw new Error('Unknown parts command. Use "parts split".');
1001
- }
1002
- let orderId = getFlag(ctx.flags, 'order-id');
1003
- let analyzed;
1004
- let status;
1005
- if (orderId == null) {
1006
- const result = await ctx.client.v1.analyzeProduction({
1007
- ...(await resolveProductionAnalyzeSource(ctx)),
1008
- agentVersion: resolvePartExtractorForAnalyze(ctx.flags),
1009
- name: getFlag(ctx.flags, 'name'),
1010
- });
1011
- analyzed = result;
1012
- orderId = result.orderId;
1013
- if (hasFlag(ctx.flags, 'wait') || hasFlag(ctx.flags, 'all-ready')) {
1468
+ const runPartSplit = async (source) => {
1469
+ let orderId = source == null ? getFlag(ctx.flags, 'order-id') : undefined;
1470
+ if (orderId != null) {
1471
+ orderId = assertFlagHasValue(orderId, '--order-id <id>');
1472
+ }
1473
+ let analyzed;
1474
+ let status;
1475
+ if (orderId == null) {
1476
+ const result = await ctx.client.v1.analyzeProduction({
1477
+ ...(source ?? (await resolveProductionAnalyzeSource(ctx))),
1478
+ agentVersion: resolvePartExtractorForAnalyze(ctx.flags),
1479
+ name: getFlag(ctx.flags, 'name'),
1480
+ });
1481
+ analyzed = result;
1482
+ orderId = result.orderId;
1483
+ if (hasFlag(ctx.flags, 'wait') || hasFlag(ctx.flags, 'all-ready')) {
1484
+ status = await pollProductionStatus({
1485
+ ctx,
1486
+ orderId,
1487
+ terminalStatuses: productionAnalyzeTerminalStatuses,
1488
+ });
1489
+ }
1490
+ }
1491
+ else if (hasFlag(ctx.flags, 'wait') || hasFlag(ctx.flags, 'all-ready')) {
1014
1492
  status = await pollProductionStatus({
1015
1493
  ctx,
1016
1494
  orderId,
1017
1495
  terminalStatuses: productionAnalyzeTerminalStatuses,
1018
1496
  });
1019
1497
  }
1020
- }
1021
- else if (hasFlag(ctx.flags, 'wait') || hasFlag(ctx.flags, 'all-ready')) {
1022
- status = await pollProductionStatus({
1023
- ctx,
1024
- orderId,
1025
- terminalStatuses: productionAnalyzeTerminalStatuses,
1498
+ let taskIds = getFlagValues(ctx.flags, 'task-id');
1499
+ if (hasFlag(ctx.flags, 'all-ready')) {
1500
+ if (status == null) {
1501
+ status = await ctx.client.v1.getProductionStatus(orderId);
1502
+ }
1503
+ taskIds = readyTaskIdsFromStatus(status);
1504
+ if (taskIds.length === 0) {
1505
+ throw new Error('No ready tasks were found for --all-ready.');
1506
+ }
1507
+ }
1508
+ let executed;
1509
+ let executionStatus;
1510
+ if (taskIds.length > 0) {
1511
+ if (status == null) {
1512
+ status = await ctx.client.v1.getProductionStatus(orderId);
1513
+ }
1514
+ const missionId = getFlag(ctx.flags, 'mission-id') ?? singleMissionIdFromStatus(status);
1515
+ const partExtractorFlag = getPartExtractorFlag(ctx.flags);
1516
+ const agentVersion = partExtractorFlag == null
1517
+ ? await resolvePartExtractorFromMission(ctx, orderId, missionId)
1518
+ : resolvePartExtractorPublicName(partExtractorFlag);
1519
+ const partExtractionMode = getFlag(ctx.flags, 'part-extraction-mode');
1520
+ if (partExtractionMode != null &&
1521
+ partExtractionMode !== 'fast' &&
1522
+ partExtractionMode !== 'high_quality') {
1523
+ throw new Error('--part-extraction-mode must be "fast" or "high_quality"');
1524
+ }
1525
+ executed = await ctx.client.v1.executeProduction({
1526
+ orderId,
1527
+ missionId,
1528
+ confirmedTaskIds: taskIds,
1529
+ agentVersion,
1530
+ partExtractionMode,
1531
+ });
1532
+ if (hasFlag(ctx.flags, 'wait')) {
1533
+ executionStatus = await pollProductionStatus({
1534
+ ctx,
1535
+ orderId,
1536
+ terminalStatuses: productionExecuteTerminalStatuses,
1537
+ });
1538
+ }
1539
+ }
1540
+ return sanitizeProductionOutput({
1541
+ analyzed,
1542
+ status,
1543
+ executed,
1544
+ executionStatus,
1026
1545
  });
1546
+ };
1547
+ if (subcommand === 'split') {
1548
+ if (getFlag(ctx.flags, 'order-id') != null && sourceInputCount(ctx.flags) > 0) {
1549
+ throw new Error('Pass either --order-id or one image source, not both.');
1550
+ }
1551
+ print(await withOptionalDownloads(ctx.flags, await runPartSplit()));
1552
+ return;
1027
1553
  }
1028
- let taskIds = getFlagValues(ctx.flags, 'task-id');
1029
- if (hasFlag(ctx.flags, 'all-ready')) {
1030
- if (status == null) {
1031
- status = await ctx.client.v1.getProductionStatus(orderId);
1554
+ if (subcommand === 'compare') {
1555
+ if (getFlag(ctx.flags, 'order-id') != null) {
1556
+ throw new Error('parts compare requires an image source. Use --file, --source-url, --source-id, --image-url, --file-ref-json, --stdin, --stdin-base64, --stdin-data-uri, --stdin-json, --source-json, --data-uri, or --clipboard.');
1032
1557
  }
1033
- taskIds = readyTaskIdsFromStatus(status);
1034
- if (taskIds.length === 0) {
1035
- throw new Error('No ready tasks were found for --all-ready.');
1558
+ const preprocessPrompt = getFlag(ctx.flags, 'preprocess-prompt');
1559
+ if (preprocessPrompt === 'true') {
1560
+ throw new Error('Missing required flag value: --preprocess-prompt <text>');
1036
1561
  }
1037
- }
1038
- let executed;
1039
- let executionStatus;
1040
- if (taskIds.length > 0) {
1041
- if (status == null) {
1042
- status = await ctx.client.v1.getProductionStatus(orderId);
1562
+ if (preprocessPrompt != null && getFlagValues(ctx.flags, 'task-id').length > 0) {
1563
+ throw new Error('parts compare preprocessing creates a second order. Use --all-ready instead of --task-id.');
1043
1564
  }
1044
- const missionId = getFlag(ctx.flags, 'mission-id') ?? singleMissionIdFromStatus(status);
1045
- const partExtractorFlag = getPartExtractorFlag(ctx.flags);
1046
- const agentVersion = partExtractorFlag == null
1047
- ? await resolvePartExtractorFromMission(ctx, orderId, missionId)
1048
- : resolvePartExtractorPublicName(partExtractorFlag);
1049
- const partExtractionMode = getFlag(ctx.flags, 'part-extraction-mode');
1050
- if (partExtractionMode != null &&
1051
- partExtractionMode !== 'fast' &&
1052
- partExtractionMode !== 'high_quality') {
1053
- throw new Error('--part-extraction-mode must be "fast" or "high_quality"');
1565
+ if (preprocessPrompt != null && getFlag(ctx.flags, 'mission-id') != null) {
1566
+ throw new Error('parts compare preprocessing creates a second order. Omit --mission-id and let the CLI infer it.');
1054
1567
  }
1055
- executed = await ctx.client.v1.executeProduction({
1056
- orderId,
1057
- missionId,
1058
- confirmedTaskIds: taskIds,
1059
- agentVersion,
1060
- partExtractionMode,
1061
- });
1062
- if (hasFlag(ctx.flags, 'wait')) {
1063
- executionStatus = await pollProductionStatus({
1064
- ctx,
1065
- orderId,
1066
- terminalStatuses: productionExecuteTerminalStatuses,
1067
- });
1568
+ const preprocessModelId = getFlag(ctx.flags, 'preprocess-model-id');
1569
+ if (preprocessModelId === 'true') {
1570
+ throw new Error('Missing required flag value: --preprocess-model-id <id>');
1068
1571
  }
1572
+ const inputSource = productionAnalyzeSourceFromSource(await requireSource({
1573
+ flags: ctx.flags,
1574
+ client: ctx.client,
1575
+ fileMediaType: 'image',
1576
+ }));
1577
+ const direct = await runPartSplit(inputSource);
1578
+ let preprocessed = null;
1579
+ if (preprocessPrompt != null && preprocessPrompt !== 'true') {
1580
+ try {
1581
+ const queued = await ctx.client.v2.generateImage({
1582
+ source: sourceFromProductionAnalyzeSource(inputSource),
1583
+ prompt: preprocessPrompt,
1584
+ modelId: preprocessModelId ?? defaultImageModelId,
1585
+ batchSize: 1,
1586
+ negativePrompt: getFlag(ctx.flags, 'preprocess-negative-prompt'),
1587
+ seed: getFlag(ctx.flags, 'preprocess-seed') == null
1588
+ ? undefined
1589
+ : parsePositiveIntegerFlag(ctx.flags, 'preprocess-seed', 1),
1590
+ });
1591
+ const job = await ctx.client.v2.pollJob(queued.jobId, {
1592
+ intervalMs: parsePositiveIntegerFlag(ctx.flags, 'interval-ms', 5000),
1593
+ timeoutMs: parsePositiveIntegerFlag(ctx.flags, 'timeout-ms', 900000),
1594
+ onPoll: current => {
1595
+ stderr.write(`[poll] preprocess job=${current.id} status=${current.status}\n`);
1596
+ },
1597
+ });
1598
+ if (job.status !== 'completed') {
1599
+ throw new Error(`Preprocessing failed: job=${job.id} status=${job.status}`);
1600
+ }
1601
+ const generatedSource = imageSourceFromCompletedJob(job);
1602
+ preprocessed = {
1603
+ ok: true,
1604
+ image: {
1605
+ queued,
1606
+ job,
1607
+ source: generatedSource,
1608
+ },
1609
+ split: await runPartSplit(generatedSource),
1610
+ };
1611
+ }
1612
+ catch (error) {
1613
+ if (hasFlag(ctx.flags, 'fail-on-preprocess-error')) {
1614
+ throw error;
1615
+ }
1616
+ preprocessed = {
1617
+ ok: false,
1618
+ error: errorToOutput(error),
1619
+ };
1620
+ }
1621
+ }
1622
+ print(await withOptionalDownloads(ctx.flags, {
1623
+ direct,
1624
+ preprocessed,
1625
+ }));
1626
+ return;
1069
1627
  }
1070
- print(await withOptionalDownloads(ctx.flags, sanitizeProductionOutput({
1071
- analyzed,
1072
- status,
1073
- executed,
1074
- executionStatus,
1075
- })));
1628
+ throw new Error('Unknown parts command. Use "parts split" or "parts compare".');
1076
1629
  };
1077
1630
  const run = async () => {
1078
1631
  const parsed = parseArgs(argv.slice(2));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assethub/cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Command line interface for AssetHub.",
5
5
  "type": "module",
6
6
  "bin": {