@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 +34 -3
- package/package.json +1 -1
- package/src/api.js +4 -2
- package/src/cli.js +101 -12
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
|
-
|
|
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
|
-
|
|
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
|
|
107
|
+
Or save a manually created key locally:
|
|
77
108
|
|
|
78
109
|
```bash
|
|
79
110
|
flatkey onboard --api-key <key>
|
package/package.json
CHANGED
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]
|
|
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
|
-
? {
|
|
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
|
-
|
|
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:
|
|
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
|
|
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 ??
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
891
|
+
const lines = entries
|
|
839
892
|
.map(([key, value]) => `${humanLabel(key)}: ${formatScalar(value)}`)
|
|
840
|
-
|
|
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) {
|