@flatkey-ai/cli 0.1.20 → 0.1.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/package.json +1 -1
  2. package/src/api.js +4 -2
  3. package/src/cli.js +71 -11
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flatkey-ai/cli",
3
- "version": "0.1.20",
3
+ "version": "0.1.22",
4
4
  "description": "Flatkey media generation CLI for image, video, audio, credits, status, and models.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/api.js CHANGED
@@ -16,7 +16,9 @@ export async function generateImage(options) {
16
16
 
17
17
  export async function uploadTempMediaImage(options) {
18
18
  const form = new FormData();
19
- form.append("file", new Blob([options.file]), options.filename ?? "image");
19
+ form.append("file", new Blob([options.file], cleanObject({
20
+ type: options.contentType,
21
+ })), options.filename ?? "image");
20
22
  return requestJsonFromPlan(options, planRequest(options, "/v1/temp-media/images", {
21
23
  method: "POST",
22
24
  headers: authHeaders(options.apiKey),
@@ -240,7 +242,7 @@ async function requestJsonFromPlan(options, plan) {
240
242
  : JSON.stringify(plan.body),
241
243
  });
242
244
  const body = await readJson(response);
243
- if (!response.ok) {
245
+ if (!response.ok || body?.success === false) {
244
246
  throw new FlatkeyError(extractErrorMessage(body, response.status), {
245
247
  status: response.status,
246
248
  });
package/src/cli.js CHANGED
@@ -15,7 +15,7 @@ const COMMANDS = new Set([
15
15
  ]);
16
16
 
17
17
  const GROUP_ACTIONS = new Set(["audio", "auth", "image", "text", "video"]);
18
- const GLOBAL_OPTIONS = new Set(["api_key", "base_url", "dry_run", "help", "json", "output", "out"]);
18
+ const GLOBAL_OPTIONS = new Set(["api_key", "base_url", "console_url", "dry_run", "help", "json", "output", "out"]);
19
19
  const REPEATABLE_OPTIONS = new Set(["image", "image_url", "video_url"]);
20
20
  const COMMAND_OPTIONS = {
21
21
  "audio generate": new Set(["model", "prompt", "similarity_boost", "stability", "style", "voice_id"]),
@@ -404,7 +404,7 @@ async function handleImageUpload(command, deps) {
404
404
  const { basename } = await import("node:path");
405
405
  const { readFile } = await import("node:fs/promises");
406
406
  const { resolveApiKey, resolveOrigins } = await import("./config.js");
407
- const { uploadTempMediaImage } = await import("./api.js");
407
+ const { DEFAULT_CONSOLE_URL, uploadTempMediaImage } = await import("./api.js");
408
408
  const file = command.options.file;
409
409
  if (typeof file !== "string" || file.trim() === "") {
410
410
  throw new Error("Missing --file value. Run `flatkey image upload --help` to see supported options.");
@@ -414,25 +414,27 @@ async function handleImageUpload(command, deps) {
414
414
  env: deps.env ?? process.env,
415
415
  configDir: deps.configDir,
416
416
  });
417
- const { routerOrigin } = await resolveOrigins({
418
- baseUrl: command.options.base_url,
417
+ const { consoleOrigin } = await resolveOrigins({
418
+ consoleUrl: command.options.console_url,
419
419
  env: deps.env ?? process.env,
420
420
  configDir: deps.configDir,
421
421
  });
422
422
  const response = await uploadTempMediaImage({
423
423
  apiKey,
424
- baseUrl: routerOrigin,
424
+ baseUrl: consoleOrigin ?? DEFAULT_CONSOLE_URL,
425
425
  file: await readFile(await expandHomePath(file)),
426
426
  filename: basename(file),
427
+ contentType: imageContentTypeFromPath(file),
427
428
  fetch: deps.fetch,
428
429
  });
429
430
  const data = response?.data ?? response;
430
- if (!data?.signed_url) {
431
+ const signedUrl = extractUploadedImageUrl(response);
432
+ if (!signedUrl) {
431
433
  throw new Error("Image upload failed: missing signed_url.");
432
434
  }
433
435
  return {
434
436
  kind: "upload",
435
- url: data?.signed_url,
437
+ url: signedUrl,
436
438
  objectKey: data?.object_key,
437
439
  expiresAt: data?.expires_at,
438
440
  expiresIn: data?.expires_in,
@@ -449,6 +451,7 @@ async function handleGenerate(command, deps) {
449
451
  generateImage,
450
452
  generateText,
451
453
  generateVideo,
454
+ DEFAULT_CONSOLE_URL,
452
455
  getVideo,
453
456
  planAudioMusicRequest,
454
457
  planAudioRequest,
@@ -469,6 +472,12 @@ async function handleGenerate(command, deps) {
469
472
  });
470
473
  const { routerOrigin } = await resolveOrigins({
471
474
  baseUrl: command.options.base_url,
475
+ consoleUrl: command.options.console_url,
476
+ env: deps.env ?? process.env,
477
+ configDir: deps.configDir,
478
+ });
479
+ const { consoleOrigin: tempMediaBaseUrl } = await resolveOrigins({
480
+ consoleUrl: command.options.console_url,
472
481
  env: deps.env ?? process.env,
473
482
  configDir: deps.configDir,
474
483
  });
@@ -476,6 +485,7 @@ async function handleGenerate(command, deps) {
476
485
  ...command.options,
477
486
  apiKey,
478
487
  baseUrl: routerOrigin,
488
+ tempMediaBaseUrl: tempMediaBaseUrl ?? DEFAULT_CONSOLE_URL,
479
489
  env: deps.env ?? process.env,
480
490
  fetch: deps.fetch,
481
491
  };
@@ -560,16 +570,30 @@ async function uploadLocalImage(file, options, deps) {
560
570
  const { readFile } = await import("node:fs/promises");
561
571
  const response = await deps.uploadTempMediaImage({
562
572
  apiKey: options.apiKey,
563
- baseUrl: options.baseUrl,
573
+ baseUrl: options.tempMediaBaseUrl ?? options.baseUrl,
564
574
  file: await readFile(await expandHomePath(file)),
565
575
  filename: basename(file),
576
+ contentType: imageContentTypeFromPath(file),
566
577
  fetch: options.fetch,
567
578
  });
568
- const url = response?.data?.signed_url ?? response?.signed_url;
579
+ const url = extractUploadedImageUrl(response);
569
580
  if (!url) throw new Error(`Image upload failed: missing signed_url for ${file}`);
570
581
  return url;
571
582
  }
572
583
 
584
+ function extractUploadedImageUrl(response) {
585
+ const data = response?.data ?? response;
586
+ return data?.signed_url ?? data?.signedUrl ?? data?.url ?? response?.signed_url ?? response?.signedUrl ?? response?.url;
587
+ }
588
+
589
+ function imageContentTypeFromPath(file) {
590
+ const lower = String(file).toLowerCase();
591
+ if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
592
+ if (lower.endsWith(".png")) return "image/png";
593
+ if (lower.endsWith(".webp")) return "image/webp";
594
+ return undefined;
595
+ }
596
+
573
597
  async function waitForVideoResult(response, options) {
574
598
  if (hasArtifact(response)) return response;
575
599
  const taskId = videoTaskId(response);
@@ -828,9 +852,45 @@ function formatVoices(voices) {
828
852
  function formatKeyValues(result) {
829
853
  const entries = Object.entries(result);
830
854
  if (entries.length === 0) return "OK";
831
- return entries
855
+ const lines = entries
832
856
  .map(([key, value]) => `${humanLabel(key)}: ${formatScalar(value)}`)
833
- .join("\n");
857
+ const warning = lowCreditWarning(result);
858
+ if (warning) lines.push(warning);
859
+ return lines.join("\n");
860
+ }
861
+
862
+ function lowCreditWarning(result) {
863
+ const credits = extractCreditAmount(result);
864
+ if (credits === undefined || credits >= 10) return "";
865
+ return `Low credit warning: remaining credits are ${credits}. Top up soon.`;
866
+ }
867
+
868
+ function extractCreditAmount(result) {
869
+ const creditKeys = new Set([
870
+ "credit",
871
+ "credits",
872
+ "remaining",
873
+ "balance",
874
+ "total_available",
875
+ "totalAvailable",
876
+ "available_balance",
877
+ "availableBalance",
878
+ ]);
879
+ const stack = [result];
880
+ const seen = new Set();
881
+ while (stack.length > 0) {
882
+ const value = stack.shift();
883
+ if (!value || typeof value !== "object" || seen.has(value)) continue;
884
+ seen.add(value);
885
+ for (const [key, entry] of Object.entries(value)) {
886
+ if (creditKeys.has(key)) {
887
+ const number = Number(entry);
888
+ if (Number.isFinite(number)) return number;
889
+ }
890
+ if (entry && typeof entry === "object") stack.push(entry);
891
+ }
892
+ }
893
+ return undefined;
834
894
  }
835
895
 
836
896
  function formatScalar(value) {