@flatkey-ai/cli 0.1.21 → 0.1.23

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 CHANGED
@@ -13,7 +13,7 @@ Website: [flatkey.ai](https://flatkey.ai/?utm_source=github&utm_medium=readme&ut
13
13
 
14
14
  ```bash
15
15
  npm install -g @flatkey-ai/cli
16
- export FLATKEY_API_KEY=<your-flatkey-api-key>
16
+ flatkey login
17
17
 
18
18
  flatkey image generate --prompt "editorial cover, neon city at dawn" -o cover.png
19
19
  flatkey video generate --prompt "cinematic product launch clip" --model seedance2 -o launch.mp4
@@ -67,13 +67,44 @@ flatkey -v
67
67
 
68
68
  ## Auth
69
69
 
70
- Use an environment variable:
70
+ Recommended: sign in through the Flatkey console:
71
+
72
+ ```bash
73
+ flatkey login
74
+ ```
75
+
76
+ `flatkey login` opens a browser authorization page, waits for approval, then saves the CLI API key and account metadata in your local Flatkey config. After login, check the active auth state:
77
+
78
+ ```bash
79
+ flatkey auth status
80
+ flatkey status --json
81
+ ```
82
+
83
+ If your terminal cannot open a browser automatically, print the approval URL and open it yourself:
84
+
85
+ ```bash
86
+ flatkey login --no-open
87
+ ```
88
+
89
+ For staging or self-hosted console testing:
90
+
91
+ ```bash
92
+ flatkey login --console-url https://console.flatkey.ai
93
+ ```
94
+
95
+ To remove the saved API key:
96
+
97
+ ```bash
98
+ flatkey logout
99
+ ```
100
+
101
+ Alternative: use an environment variable:
71
102
 
72
103
  ```bash
73
104
  export FLATKEY_API_KEY=<key>
74
105
  ```
75
106
 
76
- Or save it locally:
107
+ Or save a manually created key locally:
77
108
 
78
109
  ```bash
79
110
  flatkey onboard --api-key <key>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flatkey-ai/cli",
3
- "version": "0.1.21",
3
+ "version": "0.1.23",
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
@@ -276,6 +276,7 @@ async function handleLogin(command, deps) {
276
276
  deviceId,
277
277
  userId: pollData.user_id ?? saved.auth?.userId,
278
278
  tokenId: pollData.token_id ?? saved.auth?.tokenId,
279
+ account: accountFromAuthData(pollData) ?? saved.auth?.account,
279
280
  loginAt: Math.floor(Date.now() / 1000),
280
281
  },
281
282
  configDir: deps.configDir,
@@ -286,6 +287,7 @@ async function handleLogin(command, deps) {
286
287
  configPath,
287
288
  tokenId: pollData.token_id ?? saved.auth?.tokenId,
288
289
  userId: pollData.user_id ?? saved.auth?.userId,
290
+ account: accountFromAuthData(pollData) ?? saved.auth?.account,
289
291
  reusedConfig: true,
290
292
  }
291
293
  : `Flatkey CLI already authorized. Saved config: ${configPath}`;
@@ -298,12 +300,19 @@ async function handleLogin(command, deps) {
298
300
  deviceId,
299
301
  userId: pollData.user_id,
300
302
  tokenId: pollData.token_id,
303
+ account: accountFromAuthData(pollData),
301
304
  loginAt: Math.floor(Date.now() / 1000),
302
305
  },
303
306
  configDir: deps.configDir,
304
307
  });
305
308
  return command.options.json
306
- ? { success: true, configPath, tokenId: pollData.token_id, userId: pollData.user_id }
309
+ ? {
310
+ success: true,
311
+ configPath,
312
+ tokenId: pollData.token_id,
313
+ userId: pollData.user_id,
314
+ account: accountFromAuthData(pollData),
315
+ }
307
316
  : `Flatkey CLI authorized. Saved config: ${configPath}`;
308
317
  }
309
318
  if (pollData?.status === "denied") {
@@ -404,7 +413,7 @@ async function handleImageUpload(command, deps) {
404
413
  const { basename } = await import("node:path");
405
414
  const { readFile } = await import("node:fs/promises");
406
415
  const { resolveApiKey, resolveOrigins } = await import("./config.js");
407
- const { uploadTempMediaImage } = await import("./api.js");
416
+ const { DEFAULT_CONSOLE_URL, uploadTempMediaImage } = await import("./api.js");
408
417
  const file = command.options.file;
409
418
  if (typeof file !== "string" || file.trim() === "") {
410
419
  throw new Error("Missing --file value. Run `flatkey image upload --help` to see supported options.");
@@ -421,18 +430,20 @@ async function handleImageUpload(command, deps) {
421
430
  });
422
431
  const response = await uploadTempMediaImage({
423
432
  apiKey,
424
- baseUrl: consoleOrigin,
433
+ baseUrl: consoleOrigin ?? DEFAULT_CONSOLE_URL,
425
434
  file: await readFile(await expandHomePath(file)),
426
435
  filename: basename(file),
436
+ contentType: imageContentTypeFromPath(file),
427
437
  fetch: deps.fetch,
428
438
  });
429
439
  const data = response?.data ?? response;
430
- if (!data?.signed_url) {
440
+ const signedUrl = extractUploadedImageUrl(response);
441
+ if (!signedUrl) {
431
442
  throw new Error("Image upload failed: missing signed_url.");
432
443
  }
433
444
  return {
434
445
  kind: "upload",
435
- url: data?.signed_url,
446
+ url: signedUrl,
436
447
  objectKey: data?.object_key,
437
448
  expiresAt: data?.expires_at,
438
449
  expiresIn: data?.expires_in,
@@ -449,6 +460,7 @@ async function handleGenerate(command, deps) {
449
460
  generateImage,
450
461
  generateText,
451
462
  generateVideo,
463
+ DEFAULT_CONSOLE_URL,
452
464
  getVideo,
453
465
  planAudioMusicRequest,
454
466
  planAudioRequest,
@@ -467,7 +479,7 @@ async function handleGenerate(command, deps) {
467
479
  env: deps.env ?? process.env,
468
480
  configDir: deps.configDir,
469
481
  });
470
- const { routerOrigin, consoleOrigin } = await resolveOrigins({
482
+ const { routerOrigin } = await resolveOrigins({
471
483
  baseUrl: command.options.base_url,
472
484
  consoleUrl: command.options.console_url,
473
485
  env: deps.env ?? process.env,
@@ -482,7 +494,7 @@ async function handleGenerate(command, deps) {
482
494
  ...command.options,
483
495
  apiKey,
484
496
  baseUrl: routerOrigin,
485
- tempMediaBaseUrl: tempMediaBaseUrl ?? consoleOrigin,
497
+ tempMediaBaseUrl: tempMediaBaseUrl ?? DEFAULT_CONSOLE_URL,
486
498
  env: deps.env ?? process.env,
487
499
  fetch: deps.fetch,
488
500
  };
@@ -570,13 +582,27 @@ async function uploadLocalImage(file, options, deps) {
570
582
  baseUrl: options.tempMediaBaseUrl ?? options.baseUrl,
571
583
  file: await readFile(await expandHomePath(file)),
572
584
  filename: basename(file),
585
+ contentType: imageContentTypeFromPath(file),
573
586
  fetch: options.fetch,
574
587
  });
575
- const url = response?.data?.signed_url ?? response?.signed_url;
588
+ const url = extractUploadedImageUrl(response);
576
589
  if (!url) throw new Error(`Image upload failed: missing signed_url for ${file}`);
577
590
  return url;
578
591
  }
579
592
 
593
+ function extractUploadedImageUrl(response) {
594
+ const data = response?.data ?? response;
595
+ return data?.signed_url ?? data?.signedUrl ?? data?.url ?? response?.signed_url ?? response?.signedUrl ?? response?.url;
596
+ }
597
+
598
+ function imageContentTypeFromPath(file) {
599
+ const lower = String(file).toLowerCase();
600
+ if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
601
+ if (lower.endsWith(".png")) return "image/png";
602
+ if (lower.endsWith(".webp")) return "image/webp";
603
+ return undefined;
604
+ }
605
+
580
606
  async function waitForVideoResult(response, options) {
581
607
  if (hasArtifact(response)) return response;
582
608
  const taskId = videoTaskId(response);
@@ -715,7 +741,8 @@ function redactRequest(request) {
715
741
  }
716
742
 
717
743
  async function handleUtility(command, deps) {
718
- const { resolveApiKey, resolveOrigins } = await import("./config.js");
744
+ const { readConfig, resolveApiKey, resolveOrigins } = await import("./config.js");
745
+ const saved = await readConfig(deps.configDir);
719
746
  const { getCredits, getStatus } = await import("./api.js");
720
747
  const apiKey = await resolveApiKey({
721
748
  apiKey: command.options.api_key,
@@ -732,7 +759,33 @@ async function handleUtility(command, deps) {
732
759
  baseUrl: consoleOrigin,
733
760
  fetch: deps.fetch,
734
761
  };
735
- return command.group === "credits" ? getCredits(options) : getStatus(options);
762
+ if (command.group === "credits") return getCredits(options);
763
+
764
+ const status = await getStatus(options);
765
+ const account = effectiveAccountForStatus({ command, saved, apiKey });
766
+ if (!account || !status || typeof status !== "object" || Array.isArray(status) || status.account !== undefined) {
767
+ return status;
768
+ }
769
+ return { ...status, account };
770
+ }
771
+
772
+ function effectiveAccountForStatus({ command, saved, apiKey }) {
773
+ if (command.options.api_key || !saved?.apiKey || saved.apiKey !== apiKey) return null;
774
+ if (saved.auth?.account !== undefined) return saved.auth.account;
775
+ return accountFromAuthData(saved.auth);
776
+ }
777
+
778
+ function accountFromAuthData(data) {
779
+ if (!data || typeof data !== "object") return null;
780
+ const email = data.account?.email ?? data.email ?? data.userEmail ?? data.user_email;
781
+ const userId = data.account?.userId ?? data.account?.user_id ?? data.userId ?? data.user_id;
782
+ if (email === undefined && userId === undefined) return null;
783
+ return Object.fromEntries(
784
+ Object.entries({
785
+ email,
786
+ userId,
787
+ }).filter(([, value]) => value !== undefined && value !== null),
788
+ );
736
789
  }
737
790
 
738
791
  async function handleModels(command, deps) {
@@ -835,9 +888,45 @@ function formatVoices(voices) {
835
888
  function formatKeyValues(result) {
836
889
  const entries = Object.entries(result);
837
890
  if (entries.length === 0) return "OK";
838
- return entries
891
+ const lines = entries
839
892
  .map(([key, value]) => `${humanLabel(key)}: ${formatScalar(value)}`)
840
- .join("\n");
893
+ const warning = lowCreditWarning(result);
894
+ if (warning) lines.push(warning);
895
+ return lines.join("\n");
896
+ }
897
+
898
+ function lowCreditWarning(result) {
899
+ const credits = extractCreditAmount(result);
900
+ if (credits === undefined || credits >= 10) return "";
901
+ return `Low credit warning: remaining credits are ${credits}. Top up soon.`;
902
+ }
903
+
904
+ function extractCreditAmount(result) {
905
+ const creditKeys = new Set([
906
+ "credit",
907
+ "credits",
908
+ "remaining",
909
+ "balance",
910
+ "total_available",
911
+ "totalAvailable",
912
+ "available_balance",
913
+ "availableBalance",
914
+ ]);
915
+ const stack = [result];
916
+ const seen = new Set();
917
+ while (stack.length > 0) {
918
+ const value = stack.shift();
919
+ if (!value || typeof value !== "object" || seen.has(value)) continue;
920
+ seen.add(value);
921
+ for (const [key, entry] of Object.entries(value)) {
922
+ if (creditKeys.has(key)) {
923
+ const number = Number(entry);
924
+ if (Number.isFinite(number)) return number;
925
+ }
926
+ if (entry && typeof entry === "object") stack.push(entry);
927
+ }
928
+ }
929
+ return undefined;
841
930
  }
842
931
 
843
932
  function formatScalar(value) {