@assethub/cli 0.1.3 → 0.1.5
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.
- package/README.md +23 -5
- package/dist/index.js +486 -114
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,11 +6,10 @@ 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 --
|
|
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
|
|
16
15
|
assethub parts compare --file ./input.png --preprocess-prompt "clean white background, centered product photo" --part-extractor "V1.5" --wait --all-ready
|
|
@@ -27,22 +26,41 @@ Agent-friendly login:
|
|
|
27
26
|
```sh
|
|
28
27
|
printf '%s' "$ASSETHUB_API_KEY" | assethub auth login --api-key-stdin
|
|
29
28
|
assethub image generate --prompt "small stylized game asset chest" --wait --download --out-dir ./out/image
|
|
30
|
-
assethub mesh generate --source-url https://example.com/input.png --
|
|
29
|
+
assethub mesh generate --source-url https://example.com/input.png --wait --download --out-dir ./out/mesh
|
|
31
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
|
|
32
34
|
cat ./character.png | assethub parts compare --stdin --file-name character.png --preprocess-prompt "clean white background" --part-extractor "V1.5" --wait --all-ready
|
|
33
35
|
```
|
|
34
36
|
|
|
35
37
|
Agent-oriented input:
|
|
36
38
|
|
|
37
39
|
- `--file <path>` uploads a local image or mesh through the public file API.
|
|
38
|
-
- `--stdin --file-name <name>` accepts piped bytes. Give a file name with
|
|
39
|
-
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.
|
|
40
51
|
- `--data-uri <uri>` accepts `data:image/png;base64,...` style inputs and uploads
|
|
41
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.
|
|
42
55
|
- `--source-url`, `--source-id`/`--source-resource-id`, and `--file-ref-json`
|
|
43
56
|
can be reused across image, mesh, and part extraction commands where the API
|
|
44
57
|
supports them.
|
|
45
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
|
+
|
|
46
64
|
Agent-oriented output:
|
|
47
65
|
|
|
48
66
|
- stdout is one final JSON object.
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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';
|
|
@@ -12,26 +14,32 @@ const defaultPartExtractorName = 'V1.5';
|
|
|
12
14
|
const partExtractorOptions = [
|
|
13
15
|
{
|
|
14
16
|
publicName: 'Part Extractor V3',
|
|
17
|
+
apiValue: 'ah_part_extractor_v3',
|
|
15
18
|
aliases: ['part extractor v3', 'v3'],
|
|
16
19
|
},
|
|
17
20
|
{
|
|
18
21
|
publicName: 'V1',
|
|
22
|
+
apiValue: 'ah_part_extractor_v5',
|
|
19
23
|
aliases: ['v1', '1'],
|
|
20
24
|
},
|
|
21
25
|
{
|
|
22
26
|
publicName: 'V1.5 alpha',
|
|
27
|
+
apiValue: 'ah_part_extractor_v55',
|
|
23
28
|
aliases: ['v1.5 alpha', 'v1.5-alpha', '1.5 alpha', '1.5-alpha'],
|
|
24
29
|
},
|
|
25
30
|
{
|
|
26
31
|
publicName: 'V1.5',
|
|
32
|
+
apiValue: 'ah_part_extractor_v65',
|
|
27
33
|
aliases: ['v1.5', '1.5', 'latest', 'default'],
|
|
28
34
|
},
|
|
29
35
|
{
|
|
30
36
|
publicName: 'V2.0 alpha',
|
|
37
|
+
apiValue: 'ah_part_extractor_v20_alpha',
|
|
31
38
|
aliases: ['v2.0 alpha', 'v2 alpha', 'v2.0-alpha', 'v2-alpha', '2.0 alpha'],
|
|
32
39
|
},
|
|
33
40
|
{
|
|
34
41
|
publicName: 'V2.1 alpha',
|
|
42
|
+
apiValue: 'ah_part_extractor_v21_alpha',
|
|
35
43
|
aliases: ['v2.1 alpha', 'v2.1-alpha', '2.1 alpha'],
|
|
36
44
|
},
|
|
37
45
|
];
|
|
@@ -39,14 +47,16 @@ const partExtractorPublicNames = partExtractorOptions
|
|
|
39
47
|
.map(option => option.publicName)
|
|
40
48
|
.join(', ');
|
|
41
49
|
const normalizePartExtractorName = (value) => value.trim().toLowerCase().replace(/\s+/g, ' ');
|
|
42
|
-
const
|
|
43
|
-
[normalizePartExtractorName(option.publicName), option
|
|
50
|
+
const partExtractorByInput = new Map(partExtractorOptions.flatMap(option => [
|
|
51
|
+
[normalizePartExtractorName(option.publicName), option],
|
|
44
52
|
...option.aliases.map(alias => [
|
|
45
53
|
normalizePartExtractorName(alias),
|
|
46
|
-
option
|
|
54
|
+
option,
|
|
47
55
|
]),
|
|
48
56
|
]));
|
|
57
|
+
const partExtractorPublicNameByApiValue = new Map(partExtractorOptions.map(option => [option.apiValue, option.publicName]));
|
|
49
58
|
const isInternalPartExtractorId = (value) => value.trim().includes('_');
|
|
59
|
+
const execFileAsync = promisify(execFile);
|
|
50
60
|
const usage = `AssetHub CLI
|
|
51
61
|
|
|
52
62
|
Usage:
|
|
@@ -58,18 +68,18 @@ Usage:
|
|
|
58
68
|
assethub models get <model-id>
|
|
59
69
|
assethub files import <url> --media-type image|mesh
|
|
60
70
|
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]
|
|
71
|
+
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]
|
|
63
73
|
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>]
|
|
74
|
+
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
|
+
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
76
|
assethub jobs get <job-id> [--download --out-dir <dir>]
|
|
67
77
|
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>]
|
|
78
|
+
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
79
|
assethub production status <order-id> [--wait] [--download --out-dir <dir>]
|
|
70
80
|
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>]
|
|
72
|
-
assethub parts compare (--file <path> | --source-url <url> | --source-id <id> | --image-url <url> | --file-ref-json <json> | --stdin | --data-uri <uri>) [--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>]
|
|
81
|
+
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
|
+
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>]
|
|
73
83
|
|
|
74
84
|
Global options:
|
|
75
85
|
--api-key <key> Overrides saved auth and ASSETHUB_API_KEY.
|
|
@@ -81,12 +91,16 @@ Global options:
|
|
|
81
91
|
Examples:
|
|
82
92
|
ASSETHUB_API_KEY=ah_live_xxx assethub models list
|
|
83
93
|
assethub auth login --api-key-stdin
|
|
84
|
-
assethub models list --
|
|
94
|
+
assethub models list --ids-only
|
|
85
95
|
assethub files upload ./input.png --media-type image
|
|
86
96
|
assethub source create --file ./input.png --media-type image
|
|
97
|
+
pbpaste | assethub source create --stdin-data-uri --media-type image
|
|
98
|
+
assethub source create --clipboard --media-type image --file-name clipboard.png
|
|
99
|
+
printf '{"image_url":{"url":"data:image/png;base64,..."}}' | assethub parts split --stdin-json --part-extractor "V1.5" --wait
|
|
100
|
+
base64 -i ./input.png | assethub parts split --stdin-base64 --file-name input.png --content-type image/png --part-extractor "V1.5" --wait
|
|
87
101
|
cat ./input.png | assethub parts split --stdin --file-name input.png --part-extractor "V1.5" --wait
|
|
88
102
|
assethub image generate --prompt "stylized prop concept" --wait
|
|
89
|
-
assethub mesh generate --file ./input.png --
|
|
103
|
+
assethub mesh generate --file ./input.png --wait
|
|
90
104
|
assethub production analyze --file ./input.png --part-extractor "V1.5" --wait
|
|
91
105
|
assethub parts split --file ./input.png --part-extractor "V1.5" --wait --all-ready
|
|
92
106
|
assethub parts compare --file ./input.png --preprocess-prompt "clean white background, centered product photo" --part-extractor "V1.5" --wait --all-ready
|
|
@@ -166,18 +180,29 @@ const resolvePartExtractorPublicName = (value) => {
|
|
|
166
180
|
if (isInternalPartExtractorId(value)) {
|
|
167
181
|
throw new Error(`Internal part extractor IDs are not accepted by the public CLI. Use one of: ${partExtractorPublicNames}.`);
|
|
168
182
|
}
|
|
169
|
-
const
|
|
170
|
-
if (
|
|
183
|
+
const option = partExtractorByInput.get(normalizePartExtractorName(value));
|
|
184
|
+
if (option == null) {
|
|
171
185
|
throw new Error(`Unknown part extractor: ${value}. Use one of: ${partExtractorPublicNames}.`);
|
|
172
186
|
}
|
|
173
|
-
return publicName;
|
|
187
|
+
return option.publicName;
|
|
174
188
|
};
|
|
175
|
-
const
|
|
189
|
+
const resolvePartExtractorApiValue = (value) => {
|
|
190
|
+
if (isInternalPartExtractorId(value)) {
|
|
191
|
+
throw new Error(`Internal part extractor IDs are not accepted by the public CLI. Use one of: ${partExtractorPublicNames}.`);
|
|
192
|
+
}
|
|
193
|
+
const option = partExtractorByInput.get(normalizePartExtractorName(value));
|
|
194
|
+
if (option == null) {
|
|
195
|
+
throw new Error(`Unknown part extractor: ${value}. Use one of: ${partExtractorPublicNames}.`);
|
|
196
|
+
}
|
|
197
|
+
return option.apiValue;
|
|
198
|
+
};
|
|
199
|
+
const resolvePartExtractorForAnalyze = (flags) => resolvePartExtractorApiValue(getPartExtractorFlag(flags) ?? defaultPartExtractorName);
|
|
176
200
|
const publicPartExtractorName = (value) => {
|
|
177
201
|
if (value == null) {
|
|
178
202
|
return null;
|
|
179
203
|
}
|
|
180
|
-
return (
|
|
204
|
+
return (partExtractorPublicNameByApiValue.get(value) ??
|
|
205
|
+
(isInternalPartExtractorId(value) ? 'Unknown' : value));
|
|
181
206
|
};
|
|
182
207
|
const sanitizeProductionOutput = (value) => {
|
|
183
208
|
if (Array.isArray(value)) {
|
|
@@ -327,31 +352,73 @@ const createContext = async (flags) => {
|
|
|
327
352
|
}),
|
|
328
353
|
};
|
|
329
354
|
};
|
|
355
|
+
const isRecord = (value) => value != null && typeof value === 'object' && !Array.isArray(value);
|
|
356
|
+
const isBlobLocation = (value) => {
|
|
357
|
+
if (!isRecord(value) || typeof value.type !== 'string') {
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
if (value.type === 'supabase' &&
|
|
361
|
+
typeof value.bucket === 'string' &&
|
|
362
|
+
typeof value.path === 'string') {
|
|
363
|
+
return true;
|
|
364
|
+
}
|
|
365
|
+
if (value.type === 'signedSupabase' &&
|
|
366
|
+
typeof value.signedUrl === 'string') {
|
|
367
|
+
return true;
|
|
368
|
+
}
|
|
369
|
+
if (value.type === 'asset' && typeof value.assetId === 'string') {
|
|
370
|
+
return true;
|
|
371
|
+
}
|
|
372
|
+
return false;
|
|
373
|
+
};
|
|
330
374
|
const parseFileRef = (value) => {
|
|
331
375
|
const parsed = JSON.parse(value);
|
|
332
|
-
if (
|
|
376
|
+
if (!isBlobLocation(parsed)) {
|
|
333
377
|
throw new Error('--file-ref-json must be a BlobLocation JSON object');
|
|
334
378
|
}
|
|
335
379
|
return parsed;
|
|
336
380
|
};
|
|
337
|
-
const
|
|
338
|
-
const
|
|
339
|
-
return new Blob([
|
|
381
|
+
const blobFromBuffer = (buffer, type) => {
|
|
382
|
+
const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
|
383
|
+
return new Blob([arrayBuffer], type == null ? undefined : { type });
|
|
340
384
|
};
|
|
341
|
-
const
|
|
342
|
-
|
|
385
|
+
const readBlob = async (filePath, type) => blobFromBuffer(await readFile(filePath), type);
|
|
386
|
+
const getContentTypeFlag = (flags) => getFlag(flags, 'content-type') ?? getFlag(flags, 'mime-type');
|
|
387
|
+
const blobFromDataUri = (value, flagName = '--data-uri') => {
|
|
388
|
+
const trimmed = value.trim();
|
|
389
|
+
const match = trimmed.match(/^data:([^;,]+)?(;base64)?,(.*)$/s);
|
|
343
390
|
if (match == null) {
|
|
344
|
-
throw new Error(
|
|
391
|
+
throw new Error(`${flagName} must be a valid data URI.`);
|
|
345
392
|
}
|
|
346
393
|
const contentType = match[1] === '' ? undefined : match[1];
|
|
347
394
|
const isBase64 = match[2] != null;
|
|
348
|
-
const payload = match[3] ?? '';
|
|
395
|
+
const payload = (match[3] ?? '').replace(/\s+/g, '');
|
|
349
396
|
const buffer = isBase64
|
|
350
397
|
? Buffer.from(payload, 'base64')
|
|
351
398
|
: Buffer.from(decodeURIComponent(payload), 'utf8');
|
|
352
|
-
const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
|
353
399
|
return {
|
|
354
|
-
blob:
|
|
400
|
+
blob: blobFromBuffer(buffer, contentType),
|
|
401
|
+
contentType,
|
|
402
|
+
};
|
|
403
|
+
};
|
|
404
|
+
const blobFromBase64 = (value, contentType, flagName) => {
|
|
405
|
+
const trimmed = value.trim();
|
|
406
|
+
if (trimmed.startsWith('data:')) {
|
|
407
|
+
return blobFromDataUri(trimmed, flagName);
|
|
408
|
+
}
|
|
409
|
+
const normalized = trimmed.replace(/\s+/g, '').replace(/-/g, '+').replace(/_/g, '/');
|
|
410
|
+
if (normalized === '') {
|
|
411
|
+
throw new Error(`${flagName} input is empty.`);
|
|
412
|
+
}
|
|
413
|
+
if (!/^[A-Za-z0-9+/]+={0,2}$/.test(normalized)) {
|
|
414
|
+
throw new Error(`${flagName} must contain base64 data or a data URI.`);
|
|
415
|
+
}
|
|
416
|
+
const buffer = Buffer.from(normalized, 'base64');
|
|
417
|
+
if (buffer.byteLength === 0) {
|
|
418
|
+
throw new Error(`${flagName} did not decode to any bytes.`);
|
|
419
|
+
}
|
|
420
|
+
return {
|
|
421
|
+
blob: blobFromBuffer(buffer, contentType),
|
|
355
422
|
contentType,
|
|
356
423
|
};
|
|
357
424
|
};
|
|
@@ -376,54 +443,392 @@ const ensureFileNameExtension = (fileName, mediaType, mimeType) => {
|
|
|
376
443
|
}
|
|
377
444
|
return `${fileName}.${extensionFromMimeType(mimeType, mediaType)}`;
|
|
378
445
|
};
|
|
446
|
+
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
|
+
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');
|
|
449
|
+
const assertFlagHasValue = (value, flagDescription) => {
|
|
450
|
+
if (value === 'true' || value.trim() === '') {
|
|
451
|
+
throw new Error(`Missing required flag value: ${flagDescription}`);
|
|
452
|
+
}
|
|
453
|
+
return value;
|
|
454
|
+
};
|
|
455
|
+
const sourceInputCount = (flags) => [
|
|
456
|
+
getSourceResourceIdFlag(flags) != null,
|
|
457
|
+
getSourceUrlFlag(flags) != null,
|
|
458
|
+
getFlag(flags, 'file-ref-json') != null,
|
|
459
|
+
getFlag(flags, 'file') != null,
|
|
460
|
+
getFlag(flags, 'data-uri') != null,
|
|
461
|
+
hasFlag(flags, 'stdin'),
|
|
462
|
+
hasFlag(flags, 'stdin-base64'),
|
|
463
|
+
hasFlag(flags, 'stdin-data-uri'),
|
|
464
|
+
hasFlag(flags, 'stdin-json'),
|
|
465
|
+
getFlag(flags, 'source-json') != null,
|
|
466
|
+
hasFlag(flags, 'clipboard'),
|
|
467
|
+
].filter(Boolean).length;
|
|
468
|
+
const sourceFromString = (value) => {
|
|
469
|
+
const trimmed = value.trim();
|
|
470
|
+
if (trimmed === '' || trimmed.startsWith('data:')) {
|
|
471
|
+
return undefined;
|
|
472
|
+
}
|
|
473
|
+
try {
|
|
474
|
+
const url = new URL(trimmed);
|
|
475
|
+
if (url.protocol === 'http:' || url.protocol === 'https:') {
|
|
476
|
+
return { url: trimmed };
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
catch {
|
|
480
|
+
// Ignore non-URL strings.
|
|
481
|
+
}
|
|
482
|
+
return undefined;
|
|
483
|
+
};
|
|
484
|
+
const getFirstString = (record, keys) => {
|
|
485
|
+
for (const key of keys) {
|
|
486
|
+
const value = record[key];
|
|
487
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
488
|
+
return value;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
return undefined;
|
|
492
|
+
};
|
|
493
|
+
const stringFromJsonImageUrl = (value) => {
|
|
494
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
495
|
+
return value;
|
|
496
|
+
}
|
|
497
|
+
if (isRecord(value)) {
|
|
498
|
+
return getFirstString(value, ['url']);
|
|
499
|
+
}
|
|
500
|
+
return undefined;
|
|
501
|
+
};
|
|
502
|
+
const descriptorFromDataUri = (value, mediaType, fileName, flagName) => {
|
|
503
|
+
const { blob, contentType } = blobFromDataUri(value, flagName);
|
|
504
|
+
return {
|
|
505
|
+
kind: 'blob',
|
|
506
|
+
blob,
|
|
507
|
+
fileName: ensureFileNameExtension(fileName, mediaType, contentType),
|
|
508
|
+
contentType,
|
|
509
|
+
};
|
|
510
|
+
};
|
|
511
|
+
const descriptorFromBase64 = ({ value, contentType, fileName, mediaType, flagName, }) => {
|
|
512
|
+
const decoded = blobFromBase64(value, contentType, flagName);
|
|
513
|
+
return {
|
|
514
|
+
kind: 'blob',
|
|
515
|
+
blob: decoded.blob,
|
|
516
|
+
fileName: ensureFileNameExtension(fileName, mediaType, decoded.contentType),
|
|
517
|
+
contentType: decoded.contentType,
|
|
518
|
+
};
|
|
519
|
+
};
|
|
520
|
+
const sourceDescriptorFromJson = (value, options) => {
|
|
521
|
+
if (typeof value === 'string') {
|
|
522
|
+
const trimmed = value.trim();
|
|
523
|
+
if (trimmed === '') {
|
|
524
|
+
return undefined;
|
|
525
|
+
}
|
|
526
|
+
if (trimmed.startsWith('data:')) {
|
|
527
|
+
return descriptorFromDataUri(trimmed, options.mediaType, options.fallbackFileName, '--source-json');
|
|
528
|
+
}
|
|
529
|
+
const source = sourceFromString(trimmed);
|
|
530
|
+
return source == null ? undefined : { kind: 'source', source };
|
|
531
|
+
}
|
|
532
|
+
if (Array.isArray(value)) {
|
|
533
|
+
for (const item of value) {
|
|
534
|
+
const found = sourceDescriptorFromJson(item, options);
|
|
535
|
+
if (found != null) {
|
|
536
|
+
return found;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return undefined;
|
|
540
|
+
}
|
|
541
|
+
if (!isRecord(value)) {
|
|
542
|
+
return undefined;
|
|
543
|
+
}
|
|
544
|
+
const nestedSource = value.source;
|
|
545
|
+
if (nestedSource != null) {
|
|
546
|
+
const found = sourceDescriptorFromJson(nestedSource, options);
|
|
547
|
+
if (found != null) {
|
|
548
|
+
return found;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
const imageUrl = stringFromJsonImageUrl(value.image_url ?? value.imageUrl);
|
|
552
|
+
if (imageUrl != null) {
|
|
553
|
+
if (imageUrl.trim().startsWith('data:')) {
|
|
554
|
+
return descriptorFromDataUri(imageUrl, options.mediaType, options.fallbackFileName, '--source-json');
|
|
555
|
+
}
|
|
556
|
+
return { kind: 'source', source: { url: imageUrl } };
|
|
557
|
+
}
|
|
558
|
+
const resourceId = getFirstString(value, [
|
|
559
|
+
'resourceId',
|
|
560
|
+
'resource_id',
|
|
561
|
+
'sourceResourceId',
|
|
562
|
+
'source_resource_id',
|
|
563
|
+
'sourceId',
|
|
564
|
+
'source_id',
|
|
565
|
+
'imageAssetId',
|
|
566
|
+
'image_asset_id',
|
|
567
|
+
'assetId',
|
|
568
|
+
'asset_id',
|
|
569
|
+
]);
|
|
570
|
+
if (resourceId != null) {
|
|
571
|
+
return { kind: 'source', source: { resourceId } };
|
|
572
|
+
}
|
|
573
|
+
const fileRefValue = value.fileRef ?? value.file_ref ?? value.imageBlobLocation ?? value.image_blob_location ?? value.blobLocation ?? value.blob_location;
|
|
574
|
+
if (isBlobLocation(fileRefValue)) {
|
|
575
|
+
return { kind: 'source', source: { fileRef: fileRefValue } };
|
|
576
|
+
}
|
|
577
|
+
const dataUri = getFirstString(value, ['dataUri', 'data_uri']);
|
|
578
|
+
if (dataUri != null) {
|
|
579
|
+
return descriptorFromDataUri(dataUri, options.mediaType, getFirstString(value, ['fileName', 'file_name', 'name']) ??
|
|
580
|
+
options.fallbackFileName, '--source-json');
|
|
581
|
+
}
|
|
582
|
+
const contentType = getFirstString(value, [
|
|
583
|
+
'contentType',
|
|
584
|
+
'content_type',
|
|
585
|
+
'mimeType',
|
|
586
|
+
'mime_type',
|
|
587
|
+
'media_type',
|
|
588
|
+
]);
|
|
589
|
+
const base64Value = getFirstString(value, ['base64', 'data', 'content']);
|
|
590
|
+
if (base64Value != null && (value.type === 'base64' || contentType != null)) {
|
|
591
|
+
return descriptorFromBase64({
|
|
592
|
+
value: base64Value,
|
|
593
|
+
contentType,
|
|
594
|
+
fileName: getFirstString(value, ['fileName', 'file_name', 'name']) ??
|
|
595
|
+
options.fallbackFileName,
|
|
596
|
+
mediaType: options.mediaType,
|
|
597
|
+
flagName: '--source-json',
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
const url = getFirstString(value, ['sourceUrl', 'source_url', 'url', 'publicUrl', 'public_url']);
|
|
601
|
+
if (url != null) {
|
|
602
|
+
const source = sourceFromString(url);
|
|
603
|
+
if (source != null) {
|
|
604
|
+
return { kind: 'source', source };
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
const path = getFirstString(value, ['path', 'file', 'filePath', 'file_path']);
|
|
608
|
+
if (path != null) {
|
|
609
|
+
return { kind: 'file', path };
|
|
610
|
+
}
|
|
611
|
+
const preferredKeys = [
|
|
612
|
+
'input',
|
|
613
|
+
'image',
|
|
614
|
+
'asset',
|
|
615
|
+
'file',
|
|
616
|
+
'message',
|
|
617
|
+
'messages',
|
|
618
|
+
'content',
|
|
619
|
+
'attachments',
|
|
620
|
+
];
|
|
621
|
+
for (const key of preferredKeys) {
|
|
622
|
+
const found = sourceDescriptorFromJson(value[key], options);
|
|
623
|
+
if (found != null) {
|
|
624
|
+
return found;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
for (const entry of Object.values(value)) {
|
|
628
|
+
const found = sourceDescriptorFromJson(entry, options);
|
|
629
|
+
if (found != null) {
|
|
630
|
+
return found;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
return undefined;
|
|
634
|
+
};
|
|
635
|
+
const parseJsonValue = (raw, sourceName) => {
|
|
636
|
+
try {
|
|
637
|
+
return JSON.parse(raw);
|
|
638
|
+
}
|
|
639
|
+
catch (error) {
|
|
640
|
+
throw new Error(`${sourceName} must be valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
641
|
+
}
|
|
642
|
+
};
|
|
643
|
+
const readSourceJsonFlag = async (flags) => {
|
|
644
|
+
const value = getFlag(flags, 'source-json');
|
|
645
|
+
if (value == null || value === 'true' || value.trim() === '') {
|
|
646
|
+
throw new Error('Missing required flag value: --source-json <json|@file|@->');
|
|
647
|
+
}
|
|
648
|
+
if (value === '@-' || value === '-') {
|
|
649
|
+
return await readStdin();
|
|
650
|
+
}
|
|
651
|
+
if (value.startsWith('@')) {
|
|
652
|
+
return await readFile(value.slice(1), 'utf8');
|
|
653
|
+
}
|
|
654
|
+
return value;
|
|
655
|
+
};
|
|
656
|
+
const readClipboardImage = async () => {
|
|
657
|
+
const filePath = join(tmpdir(), `assethub-clipboard-${process.pid}-${Date.now()}.png`);
|
|
658
|
+
const readTempFile = async () => await readBlob(filePath, 'image/png');
|
|
659
|
+
try {
|
|
660
|
+
await execFileAsync('pngpaste', [filePath], { timeout: 10000 });
|
|
661
|
+
return await readTempFile();
|
|
662
|
+
}
|
|
663
|
+
catch (pngpasteError) {
|
|
664
|
+
const quoteAppleScriptString = (value) => `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
665
|
+
const pngClass = `${String.fromCharCode(0x00ab)}class PNGf${String.fromCharCode(0x00bb)}`;
|
|
666
|
+
const script = [
|
|
667
|
+
`set outPath to ${quoteAppleScriptString(filePath)}`,
|
|
668
|
+
`set pngData to the clipboard as ${pngClass}`,
|
|
669
|
+
'set fileRef to open for access (POSIX file outPath) with write permission',
|
|
670
|
+
'try',
|
|
671
|
+
'set eof fileRef to 0',
|
|
672
|
+
'write pngData to fileRef',
|
|
673
|
+
'close access fileRef',
|
|
674
|
+
'on error errMsg',
|
|
675
|
+
'try',
|
|
676
|
+
'close access fileRef',
|
|
677
|
+
'end try',
|
|
678
|
+
'error errMsg',
|
|
679
|
+
'end try',
|
|
680
|
+
].join('\n');
|
|
681
|
+
try {
|
|
682
|
+
await execFileAsync('osascript', ['-e', script], { timeout: 10000 });
|
|
683
|
+
return await readTempFile();
|
|
684
|
+
}
|
|
685
|
+
catch (osascriptError) {
|
|
686
|
+
const details = osascriptError instanceof Error
|
|
687
|
+
? osascriptError.message
|
|
688
|
+
: pngpasteError instanceof Error
|
|
689
|
+
? pngpasteError.message
|
|
690
|
+
: 'unknown error';
|
|
691
|
+
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}`);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
finally {
|
|
695
|
+
await unlink(filePath).catch(() => undefined);
|
|
696
|
+
}
|
|
697
|
+
};
|
|
698
|
+
const uploadSourceDescriptor = async ({ descriptor, client, mediaType, }) => {
|
|
699
|
+
if (descriptor.kind === 'source') {
|
|
700
|
+
return descriptor.source;
|
|
701
|
+
}
|
|
702
|
+
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;
|
|
708
|
+
const upload = await client.v2.uploadFile({
|
|
709
|
+
file,
|
|
710
|
+
fileName: ensureFileNameExtension(fileName, mediaType, descriptor.contentType),
|
|
711
|
+
mediaType,
|
|
712
|
+
});
|
|
713
|
+
return { fileRef: upload.fileRef };
|
|
714
|
+
};
|
|
379
715
|
const resolveSource = async ({ flags, client, fileMediaType, }) => {
|
|
716
|
+
const providedCount = sourceInputCount(flags);
|
|
717
|
+
if (providedCount === 0) {
|
|
718
|
+
return undefined;
|
|
719
|
+
}
|
|
720
|
+
if (providedCount > 1) {
|
|
721
|
+
throw new Error(exactlyOneSourceMessage);
|
|
722
|
+
}
|
|
380
723
|
const sourceResourceId = getSourceResourceIdFlag(flags);
|
|
381
724
|
if (sourceResourceId != null) {
|
|
382
|
-
return {
|
|
725
|
+
return {
|
|
726
|
+
resourceId: assertFlagHasValue(sourceResourceId, '--source-id/--source-resource-id <id>'),
|
|
727
|
+
};
|
|
383
728
|
}
|
|
384
|
-
const sourceUrl =
|
|
729
|
+
const sourceUrl = getSourceUrlFlag(flags);
|
|
385
730
|
if (sourceUrl != null) {
|
|
386
|
-
return {
|
|
731
|
+
return {
|
|
732
|
+
url: assertFlagHasValue(sourceUrl, '--source-url/--image-url <url>'),
|
|
733
|
+
};
|
|
387
734
|
}
|
|
388
735
|
const fileRefJson = getFlag(flags, 'file-ref-json');
|
|
389
736
|
if (fileRefJson != null) {
|
|
390
|
-
return {
|
|
737
|
+
return {
|
|
738
|
+
fileRef: parseFileRef(assertFlagHasValue(fileRefJson, '--file-ref-json <json>')),
|
|
739
|
+
};
|
|
391
740
|
}
|
|
392
741
|
const filePath = getFlag(flags, 'file');
|
|
393
742
|
if (filePath != null) {
|
|
394
|
-
|
|
395
|
-
file:
|
|
396
|
-
|
|
743
|
+
return await uploadSourceDescriptor({
|
|
744
|
+
descriptor: { kind: 'file', path: assertFlagHasValue(filePath, '--file <path>') },
|
|
745
|
+
client,
|
|
397
746
|
mediaType: fileMediaType,
|
|
398
747
|
});
|
|
399
|
-
return { fileRef: fileRef.fileRef };
|
|
400
748
|
}
|
|
401
749
|
const dataUri = getFlag(flags, 'data-uri');
|
|
402
750
|
if (dataUri != null) {
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
751
|
+
return await uploadSourceDescriptor({
|
|
752
|
+
descriptor: descriptorFromDataUri(assertFlagHasValue(dataUri, '--data-uri <uri>'), fileMediaType, getFlag(flags, 'file-name') ?? 'data-uri', '--data-uri'),
|
|
753
|
+
client,
|
|
754
|
+
mediaType: fileMediaType,
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
if (getFlag(flags, 'source-json') != null) {
|
|
758
|
+
const parsed = parseJsonValue(await readSourceJsonFlag(flags), '--source-json');
|
|
759
|
+
const descriptor = sourceDescriptorFromJson(parsed, {
|
|
760
|
+
mediaType: fileMediaType,
|
|
761
|
+
fallbackFileName: getFlag(flags, 'file-name') ?? defaultFileNameForMediaType(fileMediaType),
|
|
762
|
+
});
|
|
763
|
+
if (descriptor == null) {
|
|
764
|
+
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
|
+
}
|
|
766
|
+
return await uploadSourceDescriptor({ descriptor, client, mediaType: fileMediaType });
|
|
767
|
+
}
|
|
768
|
+
if (hasFlag(flags, 'stdin-json')) {
|
|
769
|
+
const parsed = parseJsonValue(await readStdin(), '--stdin-json');
|
|
770
|
+
const descriptor = sourceDescriptorFromJson(parsed, {
|
|
771
|
+
mediaType: fileMediaType,
|
|
772
|
+
fallbackFileName: getFlag(flags, 'file-name') ?? defaultFileNameForMediaType(fileMediaType),
|
|
773
|
+
});
|
|
774
|
+
if (descriptor == null) {
|
|
775
|
+
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
|
+
}
|
|
777
|
+
return await uploadSourceDescriptor({ descriptor, client, mediaType: fileMediaType });
|
|
778
|
+
}
|
|
779
|
+
if (hasFlag(flags, 'stdin-data-uri')) {
|
|
780
|
+
return await uploadSourceDescriptor({
|
|
781
|
+
descriptor: descriptorFromDataUri(await readStdin(), fileMediaType, getFlag(flags, 'file-name') ?? 'stdin-data-uri', '--stdin-data-uri'),
|
|
782
|
+
client,
|
|
783
|
+
mediaType: fileMediaType,
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
if (hasFlag(flags, 'stdin-base64')) {
|
|
787
|
+
return await uploadSourceDescriptor({
|
|
788
|
+
descriptor: descriptorFromBase64({
|
|
789
|
+
value: await readStdin(),
|
|
790
|
+
contentType: getContentTypeFlag(flags),
|
|
791
|
+
fileName: getFlag(flags, 'file-name') ?? defaultFileNameForMediaType(fileMediaType),
|
|
792
|
+
mediaType: fileMediaType,
|
|
793
|
+
flagName: '--stdin-base64',
|
|
794
|
+
}),
|
|
795
|
+
client,
|
|
796
|
+
mediaType: fileMediaType,
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
if (hasFlag(flags, 'clipboard')) {
|
|
800
|
+
if (fileMediaType !== 'image') {
|
|
801
|
+
throw new Error('--clipboard is only supported for image inputs.');
|
|
802
|
+
}
|
|
803
|
+
return await uploadSourceDescriptor({
|
|
804
|
+
descriptor: {
|
|
805
|
+
kind: 'blob',
|
|
806
|
+
blob: await readClipboardImage(),
|
|
807
|
+
fileName: ensureFileNameExtension(getFlag(flags, 'file-name') ?? 'clipboard.png', fileMediaType, 'image/png'),
|
|
808
|
+
contentType: 'image/png',
|
|
809
|
+
},
|
|
810
|
+
client,
|
|
408
811
|
mediaType: fileMediaType,
|
|
409
812
|
});
|
|
410
|
-
return { fileRef: fileRef.fileRef };
|
|
411
813
|
}
|
|
412
814
|
if (hasFlag(flags, 'stdin')) {
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
815
|
+
return await uploadSourceDescriptor({
|
|
816
|
+
descriptor: {
|
|
817
|
+
kind: 'blob',
|
|
818
|
+
blob: await readStdinBlob(getContentTypeFlag(flags)),
|
|
819
|
+
fileName: ensureFileNameExtension(getFlag(flags, 'file-name') ?? defaultFileNameForMediaType(fileMediaType), fileMediaType, getContentTypeFlag(flags)),
|
|
820
|
+
contentType: getContentTypeFlag(flags),
|
|
821
|
+
},
|
|
822
|
+
client,
|
|
417
823
|
mediaType: fileMediaType,
|
|
418
824
|
});
|
|
419
|
-
return { fileRef: fileRef.fileRef };
|
|
420
825
|
}
|
|
421
826
|
return undefined;
|
|
422
827
|
};
|
|
423
828
|
const requireSource = async (options) => {
|
|
424
829
|
const source = await resolveSource(options);
|
|
425
830
|
if (source == null) {
|
|
426
|
-
throw new Error(
|
|
831
|
+
throw new Error(missingSourceMessage);
|
|
427
832
|
}
|
|
428
833
|
return source;
|
|
429
834
|
};
|
|
@@ -440,10 +845,6 @@ const sourceToV1ImageInput = (source) => {
|
|
|
440
845
|
throw new Error('v1 image generation does not accept source-resource-id. Use --source-url, --file, or --api-version v2.');
|
|
441
846
|
};
|
|
442
847
|
const resolveProductionAnalyzeSource = async (ctx) => {
|
|
443
|
-
const imageUrl = getFlag(ctx.flags, 'image-url');
|
|
444
|
-
if (imageUrl != null) {
|
|
445
|
-
return { imageUrl };
|
|
446
|
-
}
|
|
447
848
|
const source = await requireSource({
|
|
448
849
|
flags: ctx.flags,
|
|
449
850
|
client: ctx.client,
|
|
@@ -456,7 +857,7 @@ const resolveProductionAnalyzeSource = async (ctx) => {
|
|
|
456
857
|
return { imageUrl: source.url };
|
|
457
858
|
}
|
|
458
859
|
if (source.fileRef == null) {
|
|
459
|
-
throw new Error(
|
|
860
|
+
throw new Error(missingSourceMessage);
|
|
460
861
|
}
|
|
461
862
|
return { imageBlobLocation: source.fileRef };
|
|
462
863
|
};
|
|
@@ -522,54 +923,20 @@ const commandSource = async (subcommand, ctx) => {
|
|
|
522
923
|
throw new Error('Unknown source command. Use "source create".');
|
|
523
924
|
}
|
|
524
925
|
const mediaType = parseMediaType(requireFlag(ctx.flags, 'media-type'));
|
|
525
|
-
const
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
sourceResourceId != null,
|
|
533
|
-
filePath != null,
|
|
534
|
-
useStdin,
|
|
535
|
-
dataUri != null,
|
|
536
|
-
].filter(Boolean);
|
|
537
|
-
if (provided.length !== 1) {
|
|
538
|
-
throw new Error('Pass exactly one source input: --file, --source-url, --source-id, --source-resource-id, --stdin, or --data-uri.');
|
|
539
|
-
}
|
|
540
|
-
if (sourceUrl != null) {
|
|
541
|
-
print({ source: { url: sourceUrl }, url: sourceUrl, mediaType });
|
|
926
|
+
const source = await requireSource({
|
|
927
|
+
flags: ctx.flags,
|
|
928
|
+
client: ctx.client,
|
|
929
|
+
fileMediaType: mediaType,
|
|
930
|
+
});
|
|
931
|
+
if (source.url != null) {
|
|
932
|
+
print({ source, url: source.url, mediaType });
|
|
542
933
|
return;
|
|
543
934
|
}
|
|
544
|
-
if (
|
|
545
|
-
print({
|
|
546
|
-
source: { resourceId: sourceResourceId },
|
|
547
|
-
resourceId: sourceResourceId,
|
|
548
|
-
mediaType,
|
|
549
|
-
});
|
|
935
|
+
if (source.resourceId != null) {
|
|
936
|
+
print({ source, resourceId: source.resourceId, mediaType });
|
|
550
937
|
return;
|
|
551
938
|
}
|
|
552
|
-
|
|
553
|
-
? await ctx.client.v2.uploadFile({
|
|
554
|
-
file: await readBlob(filePath),
|
|
555
|
-
fileName: basename(filePath),
|
|
556
|
-
mediaType,
|
|
557
|
-
})
|
|
558
|
-
: dataUri != null
|
|
559
|
-
? await (async () => {
|
|
560
|
-
const { blob, contentType } = blobFromDataUri(dataUri);
|
|
561
|
-
return await ctx.client.v2.uploadFile({
|
|
562
|
-
file: blob,
|
|
563
|
-
fileName: ensureFileNameExtension(getFlag(ctx.flags, 'file-name') ?? 'data-uri', mediaType, contentType),
|
|
564
|
-
mediaType,
|
|
565
|
-
});
|
|
566
|
-
})()
|
|
567
|
-
: await ctx.client.v2.uploadFile({
|
|
568
|
-
file: await readStdinBlob(),
|
|
569
|
-
fileName: ensureFileNameExtension(getFlag(ctx.flags, 'file-name') ?? defaultFileNameForMediaType(mediaType), mediaType),
|
|
570
|
-
mediaType,
|
|
571
|
-
});
|
|
572
|
-
print({ source: { fileRef: upload.fileRef }, fileRef: upload.fileRef, mediaType });
|
|
939
|
+
print({ source, fileRef: source.fileRef, mediaType });
|
|
573
940
|
};
|
|
574
941
|
const commandModels = async (subcommand, positionals, ctx) => {
|
|
575
942
|
if (subcommand === 'list') {
|
|
@@ -782,11 +1149,12 @@ const resolvePartExtractorFromMission = async (ctx, orderId, missionId) => {
|
|
|
782
1149
|
throw new Error('Could not infer the part extractor from the mission. Pass --part-extractor with one of: ' +
|
|
783
1150
|
partExtractorPublicNames);
|
|
784
1151
|
}
|
|
785
|
-
|
|
1152
|
+
const publicName = publicPartExtractorName(mission.agentVersion);
|
|
1153
|
+
if (publicName == null || publicName === 'Unknown') {
|
|
786
1154
|
throw new Error('Could not infer the public part extractor from the mission. Pass --part-extractor with one of: ' +
|
|
787
1155
|
partExtractorPublicNames);
|
|
788
1156
|
}
|
|
789
|
-
return
|
|
1157
|
+
return resolvePartExtractorApiValue(publicName);
|
|
790
1158
|
};
|
|
791
1159
|
const redactUrl = (value) => {
|
|
792
1160
|
try {
|
|
@@ -1095,7 +1463,7 @@ const commandProduction = async (subcommand, positionals, ctx) => {
|
|
|
1095
1463
|
const partExtractorFlag = getPartExtractorFlag(ctx.flags);
|
|
1096
1464
|
const agentVersion = partExtractorFlag == null
|
|
1097
1465
|
? await resolvePartExtractorFromMission(ctx, orderId, missionId)
|
|
1098
|
-
:
|
|
1466
|
+
: resolvePartExtractorApiValue(partExtractorFlag);
|
|
1099
1467
|
const executed = await ctx.client.v1.executeProduction({
|
|
1100
1468
|
orderId,
|
|
1101
1469
|
missionId,
|
|
@@ -1118,6 +1486,9 @@ const commandProduction = async (subcommand, positionals, ctx) => {
|
|
|
1118
1486
|
const commandParts = async (subcommand, ctx) => {
|
|
1119
1487
|
const runPartSplit = async (source) => {
|
|
1120
1488
|
let orderId = source == null ? getFlag(ctx.flags, 'order-id') : undefined;
|
|
1489
|
+
if (orderId != null) {
|
|
1490
|
+
orderId = assertFlagHasValue(orderId, '--order-id <id>');
|
|
1491
|
+
}
|
|
1121
1492
|
let analyzed;
|
|
1122
1493
|
let status;
|
|
1123
1494
|
if (orderId == null) {
|
|
@@ -1163,7 +1534,7 @@ const commandParts = async (subcommand, ctx) => {
|
|
|
1163
1534
|
const partExtractorFlag = getPartExtractorFlag(ctx.flags);
|
|
1164
1535
|
const agentVersion = partExtractorFlag == null
|
|
1165
1536
|
? await resolvePartExtractorFromMission(ctx, orderId, missionId)
|
|
1166
|
-
:
|
|
1537
|
+
: resolvePartExtractorApiValue(partExtractorFlag);
|
|
1167
1538
|
const partExtractionMode = getFlag(ctx.flags, 'part-extraction-mode');
|
|
1168
1539
|
if (partExtractionMode != null &&
|
|
1169
1540
|
partExtractionMode !== 'fast' &&
|
|
@@ -1193,12 +1564,15 @@ const commandParts = async (subcommand, ctx) => {
|
|
|
1193
1564
|
});
|
|
1194
1565
|
};
|
|
1195
1566
|
if (subcommand === 'split') {
|
|
1567
|
+
if (getFlag(ctx.flags, 'order-id') != null && sourceInputCount(ctx.flags) > 0) {
|
|
1568
|
+
throw new Error('Pass either --order-id or one image source, not both.');
|
|
1569
|
+
}
|
|
1196
1570
|
print(await withOptionalDownloads(ctx.flags, await runPartSplit()));
|
|
1197
1571
|
return;
|
|
1198
1572
|
}
|
|
1199
1573
|
if (subcommand === 'compare') {
|
|
1200
1574
|
if (getFlag(ctx.flags, 'order-id') != null) {
|
|
1201
|
-
throw new Error('parts compare requires an image source. Use --file, --source-url, --source-id, --image-url, --file-ref-json, --stdin,
|
|
1575
|
+
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.');
|
|
1202
1576
|
}
|
|
1203
1577
|
const preprocessPrompt = getFlag(ctx.flags, 'preprocess-prompt');
|
|
1204
1578
|
if (preprocessPrompt === 'true') {
|
|
@@ -1210,17 +1584,15 @@ const commandParts = async (subcommand, ctx) => {
|
|
|
1210
1584
|
if (preprocessPrompt != null && getFlag(ctx.flags, 'mission-id') != null) {
|
|
1211
1585
|
throw new Error('parts compare preprocessing creates a second order. Omit --mission-id and let the CLI infer it.');
|
|
1212
1586
|
}
|
|
1213
|
-
const
|
|
1214
|
-
if (
|
|
1215
|
-
throw new Error('Missing required flag value: --
|
|
1587
|
+
const preprocessModelId = getFlag(ctx.flags, 'preprocess-model-id');
|
|
1588
|
+
if (preprocessModelId === 'true') {
|
|
1589
|
+
throw new Error('Missing required flag value: --preprocess-model-id <id>');
|
|
1216
1590
|
}
|
|
1217
|
-
const inputSource =
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
}))
|
|
1223
|
-
: { imageUrl: imageUrlFlag };
|
|
1591
|
+
const inputSource = productionAnalyzeSourceFromSource(await requireSource({
|
|
1592
|
+
flags: ctx.flags,
|
|
1593
|
+
client: ctx.client,
|
|
1594
|
+
fileMediaType: 'image',
|
|
1595
|
+
}));
|
|
1224
1596
|
const direct = await runPartSplit(inputSource);
|
|
1225
1597
|
let preprocessed = null;
|
|
1226
1598
|
if (preprocessPrompt != null && preprocessPrompt !== 'true') {
|
|
@@ -1228,7 +1600,7 @@ const commandParts = async (subcommand, ctx) => {
|
|
|
1228
1600
|
const queued = await ctx.client.v2.generateImage({
|
|
1229
1601
|
source: sourceFromProductionAnalyzeSource(inputSource),
|
|
1230
1602
|
prompt: preprocessPrompt,
|
|
1231
|
-
modelId:
|
|
1603
|
+
modelId: preprocessModelId ?? defaultImageModelId,
|
|
1232
1604
|
batchSize: 1,
|
|
1233
1605
|
negativePrompt: getFlag(ctx.flags, 'preprocess-negative-prompt'),
|
|
1234
1606
|
seed: getFlag(ctx.flags, 'preprocess-seed') == null
|