@clickraft/cli 0.13.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,16 +1,13 @@
1
- ## [0.13.0](https://github.com/clickraft/cli/compare/v0.12.0...v0.13.0) (2026-08-08)
1
+ ## [0.15.0](https://github.com/clickraft/cli/compare/v0.14.0...v0.15.0) (2026-09-25)
2
2
 
3
3
  ### Features
4
4
 
5
- * **cli:** add product create ([fe46251](https://github.com/clickraft/cli/commit/fe4625100ae9af7571e0b97156d58eba59e9aea1))
6
- * **cli:** request products:write at login and degrade when the server refuses it ([a8b6db2](https://github.com/clickraft/cli/commit/a8b6db24d2789df834bffdf6290cb1bebd41116c))
7
- * **errors:** recognize the five product-create error codes ([9e0aef4](https://github.com/clickraft/cli/commit/9e0aef4a3eb0cf188b1016f70b7c326b14b7cd8b))
8
- * **mcp:** add the product_create tool ([f725172](https://github.com/clickraft/cli/commit/f725172c9df13af80c244c93506036c178008284))
9
- * **sdk:** add the product create request contract ([75ae3c7](https://github.com/clickraft/cli/commit/75ae3c78b011b5278a21da75d247e7452e2c630c))
10
-
11
- ### Bug Fixes
12
-
13
- * **cli:** point the insufficient-scope hint at login --force-reauth ([398abd3](https://github.com/clickraft/cli/commit/398abd3ba7928706faf7f505038c4b52361b4a68))
5
+ * **cli:** add generate list and generate estimate ([938df07](https://github.com/clickraft/cli/commit/938df073db7d234c6516f0bfe45dab9c5ea8db41))
6
+ * **cli:** save a completed result with --output ([8265d7a](https://github.com/clickraft/cli/commit/8265d7ac4fa7057264123b6856a425a193a79c9b))
7
+ * **mcp:** add generate_history and generate_estimate ([3288baa](https://github.com/clickraft/cli/commit/3288baadc4ddfccbd04114a2a3434515b78c2b64))
8
+ * **mcp:** show generations inline with MCP Apps widgets ([ca318a5](https://github.com/clickraft/cli/commit/ca318a53cd5ca29ffbb77bbef57773793487a619))
9
+ * **sdk:** accept a caller idempotency key in createGeneration ([a532ac3](https://github.com/clickraft/cli/commit/a532ac338277a699f9504abb43cda95cf97365b8))
10
+ * **sdk:** add the generation history and estimate contracts ([da4d161](https://github.com/clickraft/cli/commit/da4d16196d3ef4e2d017143c9ab800ea09f10ba5))
14
11
 
15
12
  # Changelog
16
13
 
package/README.md CHANGED
@@ -45,6 +45,35 @@ clickraft generate create --model-slug nano-banana-2 \
45
45
 
46
46
  > **Note:** `--brand-model` only works with reference-supporting models (nano-banana-2 recommended).
47
47
 
48
+ ### Quality
49
+
50
+ Use `--quality` to pick a model's quality tier. Lower tiers cost fewer credits. Valid values depend on the model (GPT Image 2.5 offers `low`, `medium`, `high`, `xhigh` and `max`). Omit it to use the model default. An unsupported value is rejected before any charge, and the error lists the values the model accepts:
51
+
52
+ ```bash
53
+ clickraft generate create --model-slug gpt-image-2.5-flare \
54
+ --prompt "product on marble" \
55
+ --quality low
56
+ ```
57
+
58
+ ### Saving the result
59
+
60
+ Use `--output` on `generate create`, `generate wait` or `generate get` to save a completed result to disk. Pass a directory (or a path ending in `/`) to get `<jobId>.<ext>` inside it, or a file path to write exactly there. JSON mode reports the absolute path as `data.savedPath`; a job that has not completed is not saved, and the CLI says so on stderr:
61
+
62
+ ```bash
63
+ clickraft generate create --model-slug nano-banana-2 \
64
+ --prompt "product on marble" \
65
+ --output ./out/
66
+ ```
67
+
68
+ ### History and cost
69
+
70
+ `generate list` shows your recent generations, newest first (`--limit`, `--status`, `--content-type`, `--cursor` for the next page). `generate estimate` takes the same flags as `generate create` and prints what it would cost and whether your account can run it now — nothing is created or charged:
71
+
72
+ ```bash
73
+ clickraft generate list --limit 10
74
+ clickraft generate estimate --model-slug nano-banana-pro --prompt "product on marble" --quality high
75
+ ```
76
+
48
77
  ### Products
49
78
 
50
79
  Create a product from images you have already uploaded, then generate against it:
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { readFileSync } from "fs";
5
- import { dirname as dirname2, join as join2 } from "path";
5
+ import { dirname as dirname3, join as join3 } from "path";
6
6
  import { fileURLToPath } from "url";
7
7
 
8
8
  // ../sdk/dist/index.js
@@ -151,10 +151,10 @@ function parseProductCreateRequest(input) {
151
151
  { retryable: false, details: parsed.error.issues }
152
152
  );
153
153
  }
154
- const request2 = parsed.data;
154
+ const request3 = parsed.data;
155
155
  const seen = /* @__PURE__ */ new Set();
156
156
  const duplicates = /* @__PURE__ */ new Set();
157
- for (const img of request2.images) {
157
+ for (const img of request3.images) {
158
158
  if (seen.has(img.assetId)) duplicates.add(img.assetId);
159
159
  seen.add(img.assetId);
160
160
  }
@@ -165,7 +165,7 @@ function parseProductCreateRequest(input) {
165
165
  { retryable: false }
166
166
  );
167
167
  }
168
- const bytes = Buffer.byteLength(JSON.stringify(request2), "utf8");
168
+ const bytes = Buffer.byteLength(JSON.stringify(request3), "utf8");
169
169
  if (bytes > MAX_PRODUCT_CREATE_BODY_BYTES) {
170
170
  throw new ApiError(
171
171
  "INPUT_INVALID_FORMAT",
@@ -173,7 +173,7 @@ function parseProductCreateRequest(input) {
173
173
  { retryable: false }
174
174
  );
175
175
  }
176
- return request2;
176
+ return request3;
177
177
  }
178
178
  var TokenRowSchema = z4.object({
179
179
  id: z4.string().uuid(),
@@ -235,6 +235,10 @@ var GenerateCreateRequestSchema = z6.object({
235
235
  "resolution must match WxH (e.g. 1280x720), p-token (e.g. 720p), or K-token (e.g. 1K)"
236
236
  ).optional(),
237
237
  durationSeconds: z6.number().int().min(1).max(60).optional(),
238
+ // The model's quality tier (e.g. 'low' | 'medium' | 'high'). The valid set is
239
+ // per model, so it is checked server-side: an unoffered value returns 400
240
+ // INPUT_INVALID_FORMAT with the supported options in `details.allowed`.
241
+ quality: z6.string().min(1).max(40).optional(),
238
242
  width: z6.number().int().min(64).max(8192).optional(),
239
243
  height: z6.number().int().min(64).max(8192).optional(),
240
244
  providerParams: z6.record(z6.string(), z6.unknown()).optional()
@@ -280,6 +284,34 @@ var GenerationResultSchema = z6.object({
280
284
  startedAt: z6.string().nullable(),
281
285
  completedAt: z6.string().nullable()
282
286
  }).strict();
287
+ var GenerationListItemSchema = z6.object({
288
+ jobId: z6.string().uuid(),
289
+ status: GenerationStatusSchema,
290
+ modelSlug: z6.string().nullable(),
291
+ contentType: z6.enum(["image", "video", "audio"]),
292
+ prompt: z6.string().nullable(),
293
+ resultUrl: z6.string().nullable(),
294
+ thumbnailUrl: z6.string().nullable(),
295
+ errorCode: z6.string().nullable(),
296
+ errorMessage: z6.string().nullable(),
297
+ creditsCharged: z6.number().int().nonnegative(),
298
+ creditsRefunded: z6.boolean(),
299
+ createdAt: z6.string(),
300
+ completedAt: z6.string().nullable()
301
+ }).strict();
302
+ var GenerationListResponseSchema = z6.object({
303
+ jobs: z6.array(GenerationListItemSchema),
304
+ cursor: z6.string().nullable(),
305
+ hasMore: z6.boolean()
306
+ }).strict();
307
+ var GenerateEstimateResponseSchema = z6.object({
308
+ modelSlug: z6.string(),
309
+ creditCost: z6.number().int().nonnegative(),
310
+ estimatedSeconds: z6.number().int().nonnegative().nullable(),
311
+ balance: z6.number().nullable(),
312
+ affordable: z6.boolean(),
313
+ blockedReason: z6.enum(["insufficient_credits", "feature_disabled", "subscription_required", "past_due"]).nullable()
314
+ }).strict();
283
315
  var MAX_UPLOAD_SIZE_BYTES = 15 * 1024 * 1024;
284
316
  var UploadRequestSchema = z7.object({
285
317
  filename: z7.string().min(1).max(255),
@@ -532,10 +564,10 @@ async function sleep(delayMs, signal) {
532
564
  if (signal?.aborted) {
533
565
  throw signalToAbortError(signal);
534
566
  }
535
- await new Promise((resolve, reject) => {
567
+ await new Promise((resolve2, reject) => {
536
568
  const timer = setTimeout(() => {
537
569
  signal?.removeEventListener("abort", onAbort);
538
- resolve();
570
+ resolve2();
539
571
  }, delayMs);
540
572
  const onAbort = () => {
541
573
  clearTimeout(timer);
@@ -1083,7 +1115,8 @@ async function createGeneration(params) {
1083
1115
  path: "/jobs",
1084
1116
  body: params.body,
1085
1117
  responseSchema: params.createSchema,
1086
- signal: params.signal
1118
+ signal: params.signal,
1119
+ idempotencyKey: params.idempotencyKey
1087
1120
  });
1088
1121
  if (!params.wait) {
1089
1122
  return params.projectCreated(created, params.modelSlug);
@@ -2184,8 +2217,7 @@ async function generateWait(opts) {
2184
2217
 
2185
2218
  // src/commands/generate/create.ts
2186
2219
  var PREFLIGHT_PLACEHOLDER_URL = "https://placeholder.invalid/pending-upload";
2187
- async function generateCreate(opts) {
2188
- const client = opts.client ?? await buildGenerateClient(opts);
2220
+ async function prepareCreateBody(opts, client) {
2189
2221
  const preflightRefs = (opts.referenceImages ?? []).map(
2190
2222
  (v) => isUrlLike(v) ? v : PREFLIGHT_PLACEHOLDER_URL
2191
2223
  );
@@ -2201,7 +2233,11 @@ async function generateCreate(opts) {
2201
2233
  client,
2202
2234
  signal: opts.signal
2203
2235
  }))[0];
2204
- const body = buildRequestBody({ ...opts, referenceImages, startFrame });
2236
+ return buildRequestBody({ ...opts, referenceImages, startFrame });
2237
+ }
2238
+ async function generateCreate(opts) {
2239
+ const client = opts.client ?? await buildGenerateClient(opts);
2240
+ const body = await prepareCreateBody(opts, client);
2205
2241
  return createGeneration({
2206
2242
  client,
2207
2243
  body,
@@ -2245,6 +2281,7 @@ function buildRequestBody(args) {
2245
2281
  if (args.aspectRatio !== void 0) input.aspectRatio = args.aspectRatio;
2246
2282
  if (args.resolution !== void 0) input.resolution = args.resolution;
2247
2283
  if (args.durationSeconds !== void 0) input.durationSeconds = args.durationSeconds;
2284
+ if (args.quality !== void 0) input.quality = args.quality;
2248
2285
  if (args.brandModels !== void 0 && args.brandModels.length > 0) {
2249
2286
  input.providerParams = {
2250
2287
  brandModels: args.brandModels.map((spec) => {
@@ -2297,6 +2334,87 @@ function projectCreateToResult(response, modelSlug) {
2297
2334
  };
2298
2335
  }
2299
2336
 
2337
+ // src/commands/generate/download.ts
2338
+ import { createWriteStream } from "fs";
2339
+ import { mkdir as mkdir2, rename as rename2, rm as rm2, stat as stat3 } from "fs/promises";
2340
+ import { basename as basename2, dirname as dirname2, extname, join as join2, resolve, sep } from "path";
2341
+ import { Readable } from "stream";
2342
+ import { pipeline } from "stream/promises";
2343
+ import { request } from "undici";
2344
+ var DOWNLOAD_TIMEOUT_MS = 12e4;
2345
+ async function isDirectory(path) {
2346
+ try {
2347
+ return (await stat3(path)).isDirectory();
2348
+ } catch {
2349
+ return false;
2350
+ }
2351
+ }
2352
+ function extensionOf(url) {
2353
+ const ext = extname(new URL(url).pathname);
2354
+ return /^\.[a-z0-9]{1,5}$/i.test(ext) ? ext.toLowerCase() : "";
2355
+ }
2356
+ async function resolveOutputPath(output, result) {
2357
+ if (!result.resultUrl) {
2358
+ throw new ApiError("INPUT_INVALID_FORMAT", "The job has no result to save.", {
2359
+ retryable: false
2360
+ });
2361
+ }
2362
+ const asDir = output.endsWith("/") || output.endsWith(sep) || await isDirectory(output);
2363
+ return asDir ? resolve(output, `${result.jobId}${extensionOf(result.resultUrl)}`) : resolve(output);
2364
+ }
2365
+ async function downloadResult(result, output, options = {}) {
2366
+ if (result.status !== "completed" || !result.resultUrl) {
2367
+ throw new ApiError(
2368
+ "INPUT_INVALID_FORMAT",
2369
+ `Job ${result.jobId} is ${result.status}; only a completed job can be saved.`,
2370
+ { retryable: false }
2371
+ );
2372
+ }
2373
+ const url = result.resultUrl;
2374
+ if (!url.startsWith("https://") && !url.startsWith("http://")) {
2375
+ throw new ApiError("INPUT_INVALID_FORMAT", `Refusing to download a non-HTTP URL: ${url}`, {
2376
+ retryable: false
2377
+ });
2378
+ }
2379
+ const target = await resolveOutputPath(output, result);
2380
+ await mkdir2(dirname2(target), { recursive: true });
2381
+ const partial = join2(dirname2(target), `.${basename2(target)}.part`);
2382
+ const signals = [AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS)];
2383
+ if (options.signal) signals.push(options.signal);
2384
+ const res = await request(url, {
2385
+ method: "GET",
2386
+ signal: AbortSignal.any(signals),
2387
+ dispatcher: options.dispatcher
2388
+ });
2389
+ if (res.statusCode !== 200) {
2390
+ await res.body.dump();
2391
+ throw new ApiError("NETWORK_ERROR", `Could not download the result (HTTP ${res.statusCode}).`, {
2392
+ retryable: res.statusCode >= 500
2393
+ });
2394
+ }
2395
+ try {
2396
+ await pipeline(Readable.from(res.body), createWriteStream(partial));
2397
+ await rename2(partial, target);
2398
+ } catch (err) {
2399
+ await rm2(partial, { force: true });
2400
+ throw err;
2401
+ }
2402
+ return target;
2403
+ }
2404
+
2405
+ // src/commands/generate/estimate.ts
2406
+ async function generateEstimate(opts) {
2407
+ const client = opts.client ?? await buildGenerateClient(opts);
2408
+ const body = await prepareCreateBody(opts, client);
2409
+ return client.request({
2410
+ method: "POST",
2411
+ path: "/jobs/estimate",
2412
+ body,
2413
+ responseSchema: GenerateEstimateResponseSchema,
2414
+ signal: opts.signal
2415
+ });
2416
+ }
2417
+
2300
2418
  // src/commands/generate/get.ts
2301
2419
  async function generateGet(opts) {
2302
2420
  const client = opts.client ?? await buildGenerateClient(opts);
@@ -2309,6 +2427,23 @@ async function generateGet(opts) {
2309
2427
  });
2310
2428
  }
2311
2429
 
2430
+ // src/commands/generate/list.ts
2431
+ async function generateList(opts) {
2432
+ const client = opts.client ?? await buildGenerateClient(opts);
2433
+ return client.request({
2434
+ method: "GET",
2435
+ path: "/jobs",
2436
+ query: {
2437
+ limit: opts.limit,
2438
+ status: opts.status,
2439
+ contentType: opts.contentType,
2440
+ cursor: opts.cursor
2441
+ },
2442
+ responseSchema: GenerationListResponseSchema,
2443
+ signal: opts.signal
2444
+ });
2445
+ }
2446
+
2312
2447
  // src/cli/args.ts
2313
2448
  var ArgError = class extends Error {
2314
2449
  constructor(message) {
@@ -2495,7 +2630,7 @@ function validateProductSpecs(specs) {
2495
2630
 
2496
2631
  // src/auth/device-flow.ts
2497
2632
  import { setTimeout as delay } from "timers/promises";
2498
- import { request } from "undici";
2633
+ import { request as request2 } from "undici";
2499
2634
  var GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
2500
2635
  var FORM_CONTENT_TYPE = "application/x-www-form-urlencoded";
2501
2636
  async function runDeviceFlow(opts) {
@@ -2610,7 +2745,7 @@ async function attemptDeviceAuthorization(opts, scope) {
2610
2745
  }
2611
2746
  let resp;
2612
2747
  try {
2613
- resp = await request(`${opts.apiBaseUrl}/oauth/device-authorization`, {
2748
+ resp = await request2(`${opts.apiBaseUrl}/oauth/device-authorization`, {
2614
2749
  method: "POST",
2615
2750
  headers,
2616
2751
  body,
@@ -2692,7 +2827,7 @@ async function pollToken(args) {
2692
2827
  }
2693
2828
  let resp;
2694
2829
  try {
2695
- resp = await request(`${args.apiBaseUrl}/oauth/token`, {
2830
+ resp = await request2(`${args.apiBaseUrl}/oauth/token`, {
2696
2831
  method: "POST",
2697
2832
  headers,
2698
2833
  body,
@@ -4333,6 +4468,8 @@ function renderRootHelp(version) {
4333
4468
  " generate create Submit a generation job and wait for completion",
4334
4469
  " generate wait Wait for an existing job to reach a terminal state",
4335
4470
  " generate get Fetch the current state of a job",
4471
+ " generate list List your recent generations, newest first",
4472
+ " generate estimate Price a generation without running it",
4336
4473
  " jobs wait Alias of `generate wait`",
4337
4474
  " workflow create Create a new empty workflow",
4338
4475
  " workflow list List workflows in your organization",
@@ -4604,6 +4741,8 @@ function renderCommandHelp(command) {
4604
4741
  " create Submit a generation job (waits for completion by default)",
4605
4742
  " wait Long-poll an existing job until terminal",
4606
4743
  " get Fetch the current state of a job (no polling)",
4744
+ " list List your recent generations, newest first",
4745
+ " estimate Price a generation without running it (same flags as create)",
4607
4746
  ""
4608
4747
  ].join("\n");
4609
4748
  case "generate create":
@@ -4627,10 +4766,13 @@ function renderCommandHelp(command) {
4627
4766
  " --aspect-ratio <a:b> e.g. 16:9",
4628
4767
  " --resolution <size> e.g. 1024x768, 720p, 1K",
4629
4768
  " --duration-seconds <n> For video/audio models (1-60)",
4769
+ " --quality <tier> e.g. low, medium, high (valid values depend on the model)",
4630
4770
  " --workflow-id <uuid> Attach to an existing workflow",
4631
4771
  " --node-id <id> Attach to a specific node",
4632
4772
  " --no-wait, --async Return immediately with the jobId",
4633
4773
  " --timeout <seconds> Override the 120s default wait",
4774
+ " --output <path> Save the completed result to a file, or into a",
4775
+ " directory as <jobId>.<ext>",
4634
4776
  " --profile <name> Credentials profile name",
4635
4777
  " --api-base-url <url> Override the API base URL",
4636
4778
  " --token <token> Override the access token",
@@ -4647,6 +4789,42 @@ function renderCommandHelp(command) {
4647
4789
  "",
4648
4790
  "Options:",
4649
4791
  " --timeout <seconds> Overall wait deadline (default: 120)",
4792
+ " --output <path> Save the completed result (file, or directory \u2192 <jobId>.<ext>)",
4793
+ " --profile <name>",
4794
+ " --api-base-url <url>",
4795
+ " --token <token>",
4796
+ " --json",
4797
+ ""
4798
+ ].join("\n");
4799
+ case "generate list":
4800
+ return [
4801
+ "Usage: clickraft generate list [options]",
4802
+ "",
4803
+ "Lists your recent generations in the organization, newest first.",
4804
+ "",
4805
+ "Options:",
4806
+ " --limit <n> Results per page (1-50, default: 20)",
4807
+ " --status <status> queued, processing, uploading, completed, failed, cancelled",
4808
+ " --content-type <type> image, video, or audio",
4809
+ " --cursor <token> Pagination cursor from a previous response",
4810
+ " --profile <name>",
4811
+ " --api-base-url <url>",
4812
+ " --token <token>",
4813
+ " --json",
4814
+ ""
4815
+ ].join("\n");
4816
+ case "generate estimate":
4817
+ return [
4818
+ "Usage: clickraft generate estimate [prompt] --model-slug <slug> [options]",
4819
+ "",
4820
+ "Prices exactly what `generate create` with the same flags would charge, and",
4821
+ "says whether the account can run it now. Nothing is created or charged.",
4822
+ "Accepts the same generation flags as `generate create` (--prompt,",
4823
+ "--model-slug, --reference-image, --start-frame, --aspect-ratio, --resolution,",
4824
+ "--duration-seconds, --quality, --brand-model, --product, --workflow-id,",
4825
+ "--node-id). Local reference paths are uploaded first, as with create.",
4826
+ "",
4827
+ "Options:",
4650
4828
  " --profile <name>",
4651
4829
  " --api-base-url <url>",
4652
4830
  " --token <token>",
@@ -4660,6 +4838,7 @@ function renderCommandHelp(command) {
4660
4838
  "Fetches the current state of a generation job without polling.",
4661
4839
  "",
4662
4840
  "Options:",
4841
+ " --output <path> Save the completed result (file, or directory \u2192 <jobId>.<ext>)",
4663
4842
  " --profile <name>",
4664
4843
  " --api-base-url <url>",
4665
4844
  " --token <token>",
@@ -4791,6 +4970,8 @@ function matchNewCommand(argv) {
4791
4970
  if (sub === "create") return { kind: "cmd", path: "generate.create", verbTokenCount: 2 };
4792
4971
  if (sub === "wait") return { kind: "cmd", path: "generate.wait", verbTokenCount: 2 };
4793
4972
  if (sub === "get") return { kind: "cmd", path: "generate.get", verbTokenCount: 2 };
4973
+ if (sub === "list") return { kind: "cmd", path: "generate.list", verbTokenCount: 2 };
4974
+ if (sub === "estimate") return { kind: "cmd", path: "generate.estimate", verbTokenCount: 2 };
4794
4975
  if (sub === void 0 || sub.startsWith("-")) return { kind: "sub-help", verb: "generate" };
4795
4976
  return { kind: "unknown-sub", verb: "generate", sub };
4796
4977
  }
@@ -4883,6 +5064,10 @@ async function runNewCommand(newCmd, options, stdout, stderr) {
4883
5064
  return await runGenerateWait(argvAfterVerb, ctx, rc);
4884
5065
  case "generate.get":
4885
5066
  return await runGenerateGet(argvAfterVerb, ctx, rc);
5067
+ case "generate.list":
5068
+ return await runGenerateList(argvAfterVerb, ctx, rc);
5069
+ case "generate.estimate":
5070
+ return await runGenerateEstimate(argvAfterVerb, ctx, rc);
4886
5071
  case "jobs.wait":
4887
5072
  return await runJobsWait(argvAfterVerb, ctx, rc);
4888
5073
  case "upload":
@@ -4945,9 +5130,11 @@ async function runGenerateCreate(argv, ctx, rc) {
4945
5130
  "aspect-ratio",
4946
5131
  "resolution",
4947
5132
  "duration-seconds",
5133
+ "quality",
4948
5134
  "workflow-id",
4949
5135
  "node-id",
4950
- "timeout"
5136
+ "timeout",
5137
+ "output"
4951
5138
  ],
4952
5139
  boolean: [...COMMON_RICH_BOOL_FLAGS, "wait", "async"],
4953
5140
  array: ["reference-image", "brand-model", "product"]
@@ -4968,6 +5155,7 @@ async function runGenerateCreate(argv, ctx, rc) {
4968
5155
  aspectRatio: parsed.string["aspect-ratio"],
4969
5156
  resolution: parsed.string.resolution,
4970
5157
  durationSeconds: optionalInt(parsed.string["duration-seconds"], "duration-seconds"),
5158
+ quality: parsed.string.quality,
4971
5159
  brandModels,
4972
5160
  products,
4973
5161
  workflowId: parsed.string["workflow-id"],
@@ -4979,12 +5167,12 @@ async function runGenerateCreate(argv, ctx, rc) {
4979
5167
  token: parsed.string.token,
4980
5168
  signal: rc.signal
4981
5169
  });
4982
- emitJob(result, ctx, rc);
5170
+ await emitJob(result, ctx, rc, parsed.string.output);
4983
5171
  return exitCodeForResult(result);
4984
5172
  }
4985
5173
  async function runGenerateWait(argv, ctx, rc) {
4986
5174
  const parsed = parseArgs(argv, {
4987
- string: [...COMMON_RICH_STRING_FLAGS, "timeout"],
5175
+ string: [...COMMON_RICH_STRING_FLAGS, "timeout", "output"],
4988
5176
  boolean: [...COMMON_RICH_BOOL_FLAGS]
4989
5177
  });
4990
5178
  const jobId = parsed.positional[0];
@@ -4997,12 +5185,12 @@ async function runGenerateWait(argv, ctx, rc) {
4997
5185
  token: parsed.string.token,
4998
5186
  signal: rc.signal
4999
5187
  });
5000
- emitJob(result, ctx, rc);
5188
+ await emitJob(result, ctx, rc, parsed.string.output);
5001
5189
  return exitCodeForResult(result);
5002
5190
  }
5003
5191
  async function runGenerateGet(argv, ctx, rc) {
5004
5192
  const parsed = parseArgs(argv, {
5005
- string: [...COMMON_RICH_STRING_FLAGS],
5193
+ string: [...COMMON_RICH_STRING_FLAGS, "output"],
5006
5194
  boolean: [...COMMON_RICH_BOOL_FLAGS]
5007
5195
  });
5008
5196
  const jobId = parsed.positional[0];
@@ -5014,12 +5202,84 @@ async function runGenerateGet(argv, ctx, rc) {
5014
5202
  token: parsed.string.token,
5015
5203
  signal: rc.signal
5016
5204
  });
5017
- emitJob(result, ctx, rc);
5205
+ await emitJob(result, ctx, rc, parsed.string.output);
5018
5206
  return exitCodeForResult(result);
5019
5207
  }
5208
+ async function runGenerateList(argv, ctx, rc) {
5209
+ const parsed = parseArgs(argv, {
5210
+ string: [...COMMON_RICH_STRING_FLAGS, "limit", "status", "content-type", "cursor"],
5211
+ boolean: [...COMMON_RICH_BOOL_FLAGS]
5212
+ });
5213
+ const page = await generateList({
5214
+ limit: optionalInt(parsed.string.limit, "limit"),
5215
+ status: parsed.string.status,
5216
+ contentType: parsed.string["content-type"],
5217
+ cursor: parsed.string.cursor,
5218
+ profile: parsed.string.profile,
5219
+ apiBaseUrl: parsed.string["api-base-url"],
5220
+ token: parsed.string.token,
5221
+ signal: rc.signal
5222
+ });
5223
+ if (rc.jsonMode) {
5224
+ rc.stdout.write(JSON.stringify(wrapSuccess(page, ctx)) + "\n");
5225
+ return 0;
5226
+ }
5227
+ rc.stdout.write(renderGenerationList(page) + "\n");
5228
+ return 0;
5229
+ }
5230
+ async function runGenerateEstimate(argv, ctx, rc) {
5231
+ const parsed = parseArgs(argv, {
5232
+ string: [
5233
+ ...COMMON_RICH_STRING_FLAGS,
5234
+ "prompt",
5235
+ "model-slug",
5236
+ "start-frame",
5237
+ "aspect-ratio",
5238
+ "resolution",
5239
+ "duration-seconds",
5240
+ "quality",
5241
+ "workflow-id",
5242
+ "node-id"
5243
+ ],
5244
+ boolean: [...COMMON_RICH_BOOL_FLAGS],
5245
+ array: ["reference-image", "brand-model", "product"]
5246
+ });
5247
+ const modelSlug = parsed.string["model-slug"];
5248
+ if (!modelSlug) throw new ArgError("--model-slug is required.");
5249
+ const rawBrandModels = parsed.array["brand-model"] ?? [];
5250
+ const brandModels = rawBrandModels.length > 0 ? rawBrandModels.map(parseBrandModelSpec) : void 0;
5251
+ if (brandModels) validateBrandModelSpecs(brandModels);
5252
+ const rawProducts = parsed.array["product"] ?? [];
5253
+ const products = rawProducts.length > 0 ? rawProducts.map(parseProductSpec) : void 0;
5254
+ if (products) validateProductSpecs(products);
5255
+ const estimate = await generateEstimate({
5256
+ prompt: parsed.string.prompt ?? parsed.positional[0],
5257
+ modelSlug,
5258
+ referenceImages: parsed.array["reference-image"],
5259
+ startFrame: parsed.string["start-frame"],
5260
+ aspectRatio: parsed.string["aspect-ratio"],
5261
+ resolution: parsed.string.resolution,
5262
+ durationSeconds: optionalInt(parsed.string["duration-seconds"], "duration-seconds"),
5263
+ quality: parsed.string.quality,
5264
+ brandModels,
5265
+ products,
5266
+ workflowId: parsed.string["workflow-id"],
5267
+ nodeId: parsed.string["node-id"],
5268
+ profile: parsed.string.profile,
5269
+ apiBaseUrl: parsed.string["api-base-url"],
5270
+ token: parsed.string.token,
5271
+ signal: rc.signal
5272
+ });
5273
+ if (rc.jsonMode) {
5274
+ rc.stdout.write(JSON.stringify(wrapSuccess(estimate, ctx)) + "\n");
5275
+ return 0;
5276
+ }
5277
+ rc.stdout.write(renderEstimate(estimate) + "\n");
5278
+ return 0;
5279
+ }
5020
5280
  async function runJobsWait(argv, ctx, rc) {
5021
5281
  const parsed = parseArgs(argv, {
5022
- string: [...COMMON_RICH_STRING_FLAGS, "timeout"],
5282
+ string: [...COMMON_RICH_STRING_FLAGS, "timeout", "output"],
5023
5283
  boolean: [...COMMON_RICH_BOOL_FLAGS]
5024
5284
  });
5025
5285
  const jobId = parsed.positional[0];
@@ -5032,7 +5292,7 @@ async function runJobsWait(argv, ctx, rc) {
5032
5292
  token: parsed.string.token,
5033
5293
  signal: rc.signal
5034
5294
  });
5035
- emitJob(result, ctx, rc);
5295
+ await emitJob(result, ctx, rc, parsed.string.output);
5036
5296
  return exitCodeForResult(result);
5037
5297
  }
5038
5298
  async function runUploadCmd(argv, ctx, rc) {
@@ -5053,15 +5313,27 @@ async function runUploadCmd(argv, ctx, rc) {
5053
5313
  emitUpload(result, ctx, rc);
5054
5314
  return 0;
5055
5315
  }
5056
- function emitJob(result, ctx, rc) {
5316
+ async function emitJob(result, ctx, rc, output) {
5057
5317
  if (rc.signal?.aborted) {
5058
5318
  throw new AuthError("AUTH_FLOW_CANCELLED", "Cancelled.");
5059
5319
  }
5320
+ let savedPath;
5321
+ if (output !== void 0) {
5322
+ if (result.status === "completed" && result.resultUrl) {
5323
+ savedPath = await downloadResult(result, output, { signal: rc.signal });
5324
+ } else {
5325
+ rc.stderr.write(`Not saved: job ${result.jobId} is ${result.status}.
5326
+ `);
5327
+ }
5328
+ }
5060
5329
  if (rc.jsonMode) {
5061
- rc.stdout.write(JSON.stringify(wrapSuccess(result, ctx)) + "\n");
5330
+ const data = savedPath ? { ...result, savedPath } : result;
5331
+ rc.stdout.write(JSON.stringify(wrapSuccess(data, ctx)) + "\n");
5062
5332
  return;
5063
5333
  }
5064
5334
  rc.stdout.write(renderGenerationResult(result) + "\n");
5335
+ if (savedPath) rc.stdout.write(`Saved: ${savedPath}
5336
+ `);
5065
5337
  }
5066
5338
  function emitUpload(result, ctx, rc) {
5067
5339
  if (rc.signal?.aborted) {
@@ -5084,6 +5356,38 @@ function renderGenerationResult(r) {
5084
5356
  }
5085
5357
  return lines.join("\n");
5086
5358
  }
5359
+ function renderGenerationList(page) {
5360
+ if (page.jobs.length === 0) return "No generations.";
5361
+ const headers = ["JOB ID", "STATUS", "MODEL", "CREATED", "CREDITS", "PROMPT"];
5362
+ const PROMPT_WIDTH = 48;
5363
+ const data = page.jobs.map((job) => {
5364
+ const prompt = (job.prompt ?? "\u2014").replace(/\s+/g, " ");
5365
+ return [
5366
+ job.jobId,
5367
+ job.status,
5368
+ job.modelSlug ?? "\u2014",
5369
+ job.createdAt.slice(0, 16),
5370
+ String(job.creditsCharged),
5371
+ prompt.length > PROMPT_WIDTH ? `${prompt.slice(0, PROMPT_WIDTH - 1)}\u2026` : prompt
5372
+ ];
5373
+ });
5374
+ const widths = headers.map(
5375
+ (header, i) => Math.max(header.length, ...data.map((cells) => cells[i].length))
5376
+ );
5377
+ const padRow = (cells) => cells.map((cell, i) => cell.padEnd(widths[i])).join(" ").trimEnd();
5378
+ const lines = [padRow(headers), ...data.map(padRow)];
5379
+ if (page.hasMore && page.cursor) lines.push("", `More: --cursor ${page.cursor}`);
5380
+ return lines.join("\n");
5381
+ }
5382
+ function renderEstimate(e) {
5383
+ const lines = [`Model: ${e.modelSlug}`, `Estimated cost: ${e.creditCost} credits`];
5384
+ if (e.estimatedSeconds !== null) lines.push(`Typical duration: ~${e.estimatedSeconds}s`);
5385
+ if (e.balance !== null) lines.push(`Balance: ${e.balance} credits`);
5386
+ lines.push(
5387
+ e.affordable ? "Can run now: yes" : `Can run now: no (${e.blockedReason?.replace(/_/g, " ") ?? "blocked"})`
5388
+ );
5389
+ return lines.join("\n");
5390
+ }
5087
5391
  function renderUploadResult(r) {
5088
5392
  return [
5089
5393
  `Uploaded: ${r.publicUrl}`,
@@ -5614,8 +5918,8 @@ async function readJsonFile(filePath) {
5614
5918
 
5615
5919
  // src/index.ts
5616
5920
  var __filename2 = fileURLToPath(import.meta.url);
5617
- var __dirname2 = dirname2(__filename2);
5618
- var pkg = JSON.parse(readFileSync(join2(__dirname2, "..", "package.json"), "utf8"));
5921
+ var __dirname2 = dirname3(__filename2);
5922
+ var pkg = JSON.parse(readFileSync(join3(__dirname2, "..", "package.json"), "utf8"));
5619
5923
  async function run(args, signal) {
5620
5924
  return dispatch({
5621
5925
  argv: args,