@clickraft/cli 0.14.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,10 +1,13 @@
1
- ## [0.14.0](https://github.com/clickraft/cli/compare/v0.13.0...v0.14.0) (2026-09-25)
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 --quality to generate create ([1497837](https://github.com/clickraft/cli/commit/149783789b272eeeb879a0b83e18522c6d7ced37))
6
- * **mcp:** add quality to generate_create ([72e8232](https://github.com/clickraft/cli/commit/72e82323005efc7f535d6c7708d43c107e721229))
7
- * **sdk:** accept a quality tier in the generate create contract ([3ef37ec](https://github.com/clickraft/cli/commit/3ef37ec4bfba289ee46d08cdac88beeff769798d))
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))
8
11
 
9
12
  # Changelog
10
13
 
package/README.md CHANGED
@@ -55,6 +55,25 @@ clickraft generate create --model-slug gpt-image-2.5-flare \
55
55
  --quality low
56
56
  ```
57
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
+
58
77
  ### Products
59
78
 
60
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(),
@@ -284,6 +284,34 @@ var GenerationResultSchema = z6.object({
284
284
  startedAt: z6.string().nullable(),
285
285
  completedAt: z6.string().nullable()
286
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();
287
315
  var MAX_UPLOAD_SIZE_BYTES = 15 * 1024 * 1024;
288
316
  var UploadRequestSchema = z7.object({
289
317
  filename: z7.string().min(1).max(255),
@@ -536,10 +564,10 @@ async function sleep(delayMs, signal) {
536
564
  if (signal?.aborted) {
537
565
  throw signalToAbortError(signal);
538
566
  }
539
- await new Promise((resolve, reject) => {
567
+ await new Promise((resolve2, reject) => {
540
568
  const timer = setTimeout(() => {
541
569
  signal?.removeEventListener("abort", onAbort);
542
- resolve();
570
+ resolve2();
543
571
  }, delayMs);
544
572
  const onAbort = () => {
545
573
  clearTimeout(timer);
@@ -1087,7 +1115,8 @@ async function createGeneration(params) {
1087
1115
  path: "/jobs",
1088
1116
  body: params.body,
1089
1117
  responseSchema: params.createSchema,
1090
- signal: params.signal
1118
+ signal: params.signal,
1119
+ idempotencyKey: params.idempotencyKey
1091
1120
  });
1092
1121
  if (!params.wait) {
1093
1122
  return params.projectCreated(created, params.modelSlug);
@@ -2188,8 +2217,7 @@ async function generateWait(opts) {
2188
2217
 
2189
2218
  // src/commands/generate/create.ts
2190
2219
  var PREFLIGHT_PLACEHOLDER_URL = "https://placeholder.invalid/pending-upload";
2191
- async function generateCreate(opts) {
2192
- const client = opts.client ?? await buildGenerateClient(opts);
2220
+ async function prepareCreateBody(opts, client) {
2193
2221
  const preflightRefs = (opts.referenceImages ?? []).map(
2194
2222
  (v) => isUrlLike(v) ? v : PREFLIGHT_PLACEHOLDER_URL
2195
2223
  );
@@ -2205,7 +2233,11 @@ async function generateCreate(opts) {
2205
2233
  client,
2206
2234
  signal: opts.signal
2207
2235
  }))[0];
2208
- 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);
2209
2241
  return createGeneration({
2210
2242
  client,
2211
2243
  body,
@@ -2302,6 +2334,87 @@ function projectCreateToResult(response, modelSlug) {
2302
2334
  };
2303
2335
  }
2304
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
+
2305
2418
  // src/commands/generate/get.ts
2306
2419
  async function generateGet(opts) {
2307
2420
  const client = opts.client ?? await buildGenerateClient(opts);
@@ -2314,6 +2427,23 @@ async function generateGet(opts) {
2314
2427
  });
2315
2428
  }
2316
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
+
2317
2447
  // src/cli/args.ts
2318
2448
  var ArgError = class extends Error {
2319
2449
  constructor(message) {
@@ -2500,7 +2630,7 @@ function validateProductSpecs(specs) {
2500
2630
 
2501
2631
  // src/auth/device-flow.ts
2502
2632
  import { setTimeout as delay } from "timers/promises";
2503
- import { request } from "undici";
2633
+ import { request as request2 } from "undici";
2504
2634
  var GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
2505
2635
  var FORM_CONTENT_TYPE = "application/x-www-form-urlencoded";
2506
2636
  async function runDeviceFlow(opts) {
@@ -2615,7 +2745,7 @@ async function attemptDeviceAuthorization(opts, scope) {
2615
2745
  }
2616
2746
  let resp;
2617
2747
  try {
2618
- resp = await request(`${opts.apiBaseUrl}/oauth/device-authorization`, {
2748
+ resp = await request2(`${opts.apiBaseUrl}/oauth/device-authorization`, {
2619
2749
  method: "POST",
2620
2750
  headers,
2621
2751
  body,
@@ -2697,7 +2827,7 @@ async function pollToken(args) {
2697
2827
  }
2698
2828
  let resp;
2699
2829
  try {
2700
- resp = await request(`${args.apiBaseUrl}/oauth/token`, {
2830
+ resp = await request2(`${args.apiBaseUrl}/oauth/token`, {
2701
2831
  method: "POST",
2702
2832
  headers,
2703
2833
  body,
@@ -4338,6 +4468,8 @@ function renderRootHelp(version) {
4338
4468
  " generate create Submit a generation job and wait for completion",
4339
4469
  " generate wait Wait for an existing job to reach a terminal state",
4340
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",
4341
4473
  " jobs wait Alias of `generate wait`",
4342
4474
  " workflow create Create a new empty workflow",
4343
4475
  " workflow list List workflows in your organization",
@@ -4609,6 +4741,8 @@ function renderCommandHelp(command) {
4609
4741
  " create Submit a generation job (waits for completion by default)",
4610
4742
  " wait Long-poll an existing job until terminal",
4611
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)",
4612
4746
  ""
4613
4747
  ].join("\n");
4614
4748
  case "generate create":
@@ -4637,6 +4771,8 @@ function renderCommandHelp(command) {
4637
4771
  " --node-id <id> Attach to a specific node",
4638
4772
  " --no-wait, --async Return immediately with the jobId",
4639
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>",
4640
4776
  " --profile <name> Credentials profile name",
4641
4777
  " --api-base-url <url> Override the API base URL",
4642
4778
  " --token <token> Override the access token",
@@ -4653,6 +4789,42 @@ function renderCommandHelp(command) {
4653
4789
  "",
4654
4790
  "Options:",
4655
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:",
4656
4828
  " --profile <name>",
4657
4829
  " --api-base-url <url>",
4658
4830
  " --token <token>",
@@ -4666,6 +4838,7 @@ function renderCommandHelp(command) {
4666
4838
  "Fetches the current state of a generation job without polling.",
4667
4839
  "",
4668
4840
  "Options:",
4841
+ " --output <path> Save the completed result (file, or directory \u2192 <jobId>.<ext>)",
4669
4842
  " --profile <name>",
4670
4843
  " --api-base-url <url>",
4671
4844
  " --token <token>",
@@ -4797,6 +4970,8 @@ function matchNewCommand(argv) {
4797
4970
  if (sub === "create") return { kind: "cmd", path: "generate.create", verbTokenCount: 2 };
4798
4971
  if (sub === "wait") return { kind: "cmd", path: "generate.wait", verbTokenCount: 2 };
4799
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 };
4800
4975
  if (sub === void 0 || sub.startsWith("-")) return { kind: "sub-help", verb: "generate" };
4801
4976
  return { kind: "unknown-sub", verb: "generate", sub };
4802
4977
  }
@@ -4889,6 +5064,10 @@ async function runNewCommand(newCmd, options, stdout, stderr) {
4889
5064
  return await runGenerateWait(argvAfterVerb, ctx, rc);
4890
5065
  case "generate.get":
4891
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);
4892
5071
  case "jobs.wait":
4893
5072
  return await runJobsWait(argvAfterVerb, ctx, rc);
4894
5073
  case "upload":
@@ -4954,7 +5133,8 @@ async function runGenerateCreate(argv, ctx, rc) {
4954
5133
  "quality",
4955
5134
  "workflow-id",
4956
5135
  "node-id",
4957
- "timeout"
5136
+ "timeout",
5137
+ "output"
4958
5138
  ],
4959
5139
  boolean: [...COMMON_RICH_BOOL_FLAGS, "wait", "async"],
4960
5140
  array: ["reference-image", "brand-model", "product"]
@@ -4987,12 +5167,12 @@ async function runGenerateCreate(argv, ctx, rc) {
4987
5167
  token: parsed.string.token,
4988
5168
  signal: rc.signal
4989
5169
  });
4990
- emitJob(result, ctx, rc);
5170
+ await emitJob(result, ctx, rc, parsed.string.output);
4991
5171
  return exitCodeForResult(result);
4992
5172
  }
4993
5173
  async function runGenerateWait(argv, ctx, rc) {
4994
5174
  const parsed = parseArgs(argv, {
4995
- string: [...COMMON_RICH_STRING_FLAGS, "timeout"],
5175
+ string: [...COMMON_RICH_STRING_FLAGS, "timeout", "output"],
4996
5176
  boolean: [...COMMON_RICH_BOOL_FLAGS]
4997
5177
  });
4998
5178
  const jobId = parsed.positional[0];
@@ -5005,12 +5185,12 @@ async function runGenerateWait(argv, ctx, rc) {
5005
5185
  token: parsed.string.token,
5006
5186
  signal: rc.signal
5007
5187
  });
5008
- emitJob(result, ctx, rc);
5188
+ await emitJob(result, ctx, rc, parsed.string.output);
5009
5189
  return exitCodeForResult(result);
5010
5190
  }
5011
5191
  async function runGenerateGet(argv, ctx, rc) {
5012
5192
  const parsed = parseArgs(argv, {
5013
- string: [...COMMON_RICH_STRING_FLAGS],
5193
+ string: [...COMMON_RICH_STRING_FLAGS, "output"],
5014
5194
  boolean: [...COMMON_RICH_BOOL_FLAGS]
5015
5195
  });
5016
5196
  const jobId = parsed.positional[0];
@@ -5022,12 +5202,84 @@ async function runGenerateGet(argv, ctx, rc) {
5022
5202
  token: parsed.string.token,
5023
5203
  signal: rc.signal
5024
5204
  });
5025
- emitJob(result, ctx, rc);
5205
+ await emitJob(result, ctx, rc, parsed.string.output);
5026
5206
  return exitCodeForResult(result);
5027
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
+ }
5028
5280
  async function runJobsWait(argv, ctx, rc) {
5029
5281
  const parsed = parseArgs(argv, {
5030
- string: [...COMMON_RICH_STRING_FLAGS, "timeout"],
5282
+ string: [...COMMON_RICH_STRING_FLAGS, "timeout", "output"],
5031
5283
  boolean: [...COMMON_RICH_BOOL_FLAGS]
5032
5284
  });
5033
5285
  const jobId = parsed.positional[0];
@@ -5040,7 +5292,7 @@ async function runJobsWait(argv, ctx, rc) {
5040
5292
  token: parsed.string.token,
5041
5293
  signal: rc.signal
5042
5294
  });
5043
- emitJob(result, ctx, rc);
5295
+ await emitJob(result, ctx, rc, parsed.string.output);
5044
5296
  return exitCodeForResult(result);
5045
5297
  }
5046
5298
  async function runUploadCmd(argv, ctx, rc) {
@@ -5061,15 +5313,27 @@ async function runUploadCmd(argv, ctx, rc) {
5061
5313
  emitUpload(result, ctx, rc);
5062
5314
  return 0;
5063
5315
  }
5064
- function emitJob(result, ctx, rc) {
5316
+ async function emitJob(result, ctx, rc, output) {
5065
5317
  if (rc.signal?.aborted) {
5066
5318
  throw new AuthError("AUTH_FLOW_CANCELLED", "Cancelled.");
5067
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
+ }
5068
5329
  if (rc.jsonMode) {
5069
- 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");
5070
5332
  return;
5071
5333
  }
5072
5334
  rc.stdout.write(renderGenerationResult(result) + "\n");
5335
+ if (savedPath) rc.stdout.write(`Saved: ${savedPath}
5336
+ `);
5073
5337
  }
5074
5338
  function emitUpload(result, ctx, rc) {
5075
5339
  if (rc.signal?.aborted) {
@@ -5092,6 +5356,38 @@ function renderGenerationResult(r) {
5092
5356
  }
5093
5357
  return lines.join("\n");
5094
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
+ }
5095
5391
  function renderUploadResult(r) {
5096
5392
  return [
5097
5393
  `Uploaded: ${r.publicUrl}`,
@@ -5622,8 +5918,8 @@ async function readJsonFile(filePath) {
5622
5918
 
5623
5919
  // src/index.ts
5624
5920
  var __filename2 = fileURLToPath(import.meta.url);
5625
- var __dirname2 = dirname2(__filename2);
5626
- 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"));
5627
5923
  async function run(args, signal) {
5628
5924
  return dispatch({
5629
5925
  argv: args,