@flatkey-ai/cli 0.1.18 → 0.1.19

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flatkey-ai/cli",
3
- "version": "0.1.18",
3
+ "version": "0.1.19",
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
@@ -14,6 +14,16 @@ export async function generateImage(options) {
14
14
  return requestJsonFromPlan(options, planImageRequest(options));
15
15
  }
16
16
 
17
+ export async function uploadTempMediaImage(options) {
18
+ const form = new FormData();
19
+ form.append("file", new Blob([options.file]), options.filename ?? "image");
20
+ return requestJsonFromPlan(options, planRequest(options, "/v1/temp-media/images", {
21
+ method: "POST",
22
+ headers: authHeaders(options.apiKey),
23
+ body: form,
24
+ }));
25
+ }
26
+
17
27
  export function planImageRequest(options) {
18
28
  const model = options.model ?? "nano-banana-pro-preview";
19
29
  if (model.startsWith("gpt")) {
@@ -223,7 +233,11 @@ async function requestJsonFromPlan(options, plan) {
223
233
  const response = await fetchImpl(plan.url, {
224
234
  method: plan.method,
225
235
  headers: plan.headers,
226
- body: plan.body === undefined ? undefined : JSON.stringify(plan.body),
236
+ body: plan.body === undefined
237
+ ? undefined
238
+ : isFormData(plan.body)
239
+ ? plan.body
240
+ : JSON.stringify(plan.body),
227
241
  });
228
242
  const body = await readJson(response);
229
243
  if (!response.ok) {
@@ -234,6 +248,10 @@ async function requestJsonFromPlan(options, plan) {
234
248
  return body;
235
249
  }
236
250
 
251
+ function isFormData(value) {
252
+ return typeof FormData !== "undefined" && value instanceof FormData;
253
+ }
254
+
237
255
  async function requestBinaryArtifactFromPlan(options, plan) {
238
256
  const fetchImpl = options.fetch ?? fetch;
239
257
  const response = await fetchImpl(plan.url, {
package/src/cli.js CHANGED
@@ -16,7 +16,7 @@ const COMMANDS = new Set([
16
16
 
17
17
  const GROUP_ACTIONS = new Set(["audio", "auth", "image", "text", "video"]);
18
18
  const GLOBAL_OPTIONS = new Set(["api_key", "base_url", "dry_run", "help", "json", "output", "out"]);
19
- const REPEATABLE_OPTIONS = new Set(["image_url", "video_url"]);
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"]),
22
22
  "audio music": new Set(["music_length_ms", "prompt"]),
@@ -27,6 +27,7 @@ const COMMAND_OPTIONS = {
27
27
  help: new Set(["ai", "command"]),
28
28
  image: new Set([]),
29
29
  "image generate": new Set(["model", "n", "prompt", "quality", "size"]),
30
+ "image upload": new Set(["file"]),
30
31
  login: new Set(["console_url", "no_open", "open"]),
31
32
  logout: new Set([]),
32
33
  models: new Set(["type"]),
@@ -36,7 +37,7 @@ const COMMAND_OPTIONS = {
36
37
  "text generate": new Set(["model", "prompt"]),
37
38
  version: new Set([]),
38
39
  video: new Set([]),
39
- "video generate": new Set(["aspect", "duration", "first_frame_url", "fps", "image_url", "last_frame_url", "model", "prompt", "ratio", "resolution", "video_url"]),
40
+ "video generate": new Set(["aspect", "duration", "first_frame", "first_frame_url", "fps", "image", "image_url", "last_frame", "last_frame_url", "model", "prompt", "ratio", "resolution", "video_url"]),
40
41
  };
41
42
 
42
43
  export function parseArgv(argv) {
@@ -214,10 +215,15 @@ export async function runCommand(command, deps = {}) {
214
215
  }
215
216
  const validAction = command.group === "audio"
216
217
  ? ["generate", "sfx", "music"].includes(command.action)
217
- : command.action === "generate";
218
+ : command.group === "image"
219
+ ? ["generate", "upload"].includes(command.action)
220
+ : command.action === "generate";
218
221
  if (!validAction) {
219
222
  throw new Error(`Unknown action for ${command.group}: ${command.action}`);
220
223
  }
224
+ if (command.group === "image" && command.action === "upload") {
225
+ return handleImageUpload(command, deps);
226
+ }
221
227
  return handleGenerate(command, { ...deps, stdout, stderr });
222
228
  }
223
229
 
@@ -394,6 +400,46 @@ async function handleAuthStatus(command, deps) {
394
400
  return command.options.json ? status : formatAuthStatus(status);
395
401
  }
396
402
 
403
+ async function handleImageUpload(command, deps) {
404
+ const { basename } = await import("node:path");
405
+ const { readFile } = await import("node:fs/promises");
406
+ const { resolveApiKey, resolveOrigins } = await import("./config.js");
407
+ const { uploadTempMediaImage } = await import("./api.js");
408
+ const file = command.options.file;
409
+ if (typeof file !== "string" || file.trim() === "") {
410
+ throw new Error("Missing --file value. Run `flatkey image upload --help` to see supported options.");
411
+ }
412
+ const apiKey = await resolveApiKey({
413
+ apiKey: command.options.api_key,
414
+ env: deps.env ?? process.env,
415
+ configDir: deps.configDir,
416
+ });
417
+ const { routerOrigin } = await resolveOrigins({
418
+ baseUrl: command.options.base_url,
419
+ env: deps.env ?? process.env,
420
+ configDir: deps.configDir,
421
+ });
422
+ const response = await uploadTempMediaImage({
423
+ apiKey,
424
+ baseUrl: routerOrigin,
425
+ file: await readFile(await expandHomePath(file)),
426
+ filename: basename(file),
427
+ fetch: deps.fetch,
428
+ });
429
+ const data = response?.data ?? response;
430
+ if (!data?.signed_url) {
431
+ throw new Error("Image upload failed: missing signed_url.");
432
+ }
433
+ return {
434
+ kind: "upload",
435
+ url: data?.signed_url,
436
+ objectKey: data?.object_key,
437
+ expiresAt: data?.expires_at,
438
+ expiresIn: data?.expires_in,
439
+ response,
440
+ };
441
+ }
442
+
397
443
  async function handleGenerate(command, deps) {
398
444
  const { resolveApiKey, resolveOrigins } = await import("./config.js");
399
445
  const {
@@ -410,6 +456,7 @@ async function handleGenerate(command, deps) {
410
456
  planImageRequest,
411
457
  planTextRequest,
412
458
  planVideoRequest,
459
+ uploadTempMediaImage,
413
460
  } = await import("./api.js");
414
461
  const { persistArtifacts } = await import("./artifacts.js");
415
462
  const { createAnimation } = await import("./animation.js");
@@ -432,6 +479,9 @@ async function handleGenerate(command, deps) {
432
479
  env: deps.env ?? process.env,
433
480
  fetch: deps.fetch,
434
481
  };
482
+ if (command.group === "video" && !command.options.dry_run) {
483
+ await uploadVideoLocalImages(options, { uploadTempMediaImage });
484
+ }
435
485
  if (command.options.dry_run) {
436
486
  const request = command.group === "image"
437
487
  ? planImageRequest(options)
@@ -484,6 +534,42 @@ async function handleGenerate(command, deps) {
484
534
  }
485
535
  }
486
536
 
537
+ async function uploadVideoLocalImages(options, deps) {
538
+ const localImages = asArray(options.image);
539
+ const firstFrame = options.first_frame;
540
+ const lastFrame = options.last_frame;
541
+ if (localImages.length === 0 && !firstFrame && !lastFrame) return;
542
+
543
+ const uploads = [];
544
+ for (const file of localImages) {
545
+ uploads.push(await uploadLocalImage(file, options, deps));
546
+ }
547
+ if (uploads.length > 0) {
548
+ options.image_url = [...asArray(options.image_url), ...uploads];
549
+ }
550
+ if (firstFrame) {
551
+ options.first_frame_url = await uploadLocalImage(firstFrame, options, deps);
552
+ }
553
+ if (lastFrame) {
554
+ options.last_frame_url = await uploadLocalImage(lastFrame, options, deps);
555
+ }
556
+ }
557
+
558
+ async function uploadLocalImage(file, options, deps) {
559
+ const { basename } = await import("node:path");
560
+ const { readFile } = await import("node:fs/promises");
561
+ const response = await deps.uploadTempMediaImage({
562
+ apiKey: options.apiKey,
563
+ baseUrl: options.baseUrl,
564
+ file: await readFile(await expandHomePath(file)),
565
+ filename: basename(file),
566
+ fetch: options.fetch,
567
+ });
568
+ const url = response?.data?.signed_url ?? response?.signed_url;
569
+ if (!url) throw new Error(`Image upload failed: missing signed_url for ${file}`);
570
+ return url;
571
+ }
572
+
487
573
  async function waitForVideoResult(response, options) {
488
574
  if (hasArtifact(response)) return response;
489
575
  const taskId = videoTaskId(response);
@@ -669,6 +755,7 @@ function formatHuman(result) {
669
755
  if (typeof result === "string") return result;
670
756
  if (!result || typeof result !== "object") return String(result);
671
757
  if (result.dryRun && result.request) return formatDryRun(result);
758
+ if (result.kind === "upload") return formatUploadResult(result);
672
759
  if (result.kind) return formatGenerationResult(result);
673
760
  if (Array.isArray(result.models)) return formatModels(result.models);
674
761
  if (Array.isArray(result.voices)) return formatVoices(result.voices);
@@ -688,6 +775,13 @@ function formatDryRun(result) {
688
775
  return lines.join("\n");
689
776
  }
690
777
 
778
+ function formatUploadResult(result) {
779
+ const lines = ["Image uploaded:"];
780
+ if (result.url) lines.push(`URL: ${result.url}`);
781
+ if (result.expiresIn) lines.push(`Expires in: ${result.expiresIn}s`);
782
+ return lines.join("\n");
783
+ }
784
+
691
785
  function formatGenerationResult(result) {
692
786
  if (result.kind === "text") {
693
787
  const lines = [result.text || "(empty text)"];
package/src/help.js CHANGED
@@ -52,6 +52,7 @@ Commands:
52
52
  auth status Show saved auth state
53
53
  onboard --api-key <key> Save Flatkey API key from https://console.flatkey.ai/keys
54
54
  image generate --prompt <txt> Generate image
55
+ image upload --file <path> Upload local image and return a temporary URL
55
56
  video generate --prompt <txt> Generate video
56
57
  audio generate --prompt <txt> Generate speech with ElevenLabs voices
57
58
  audio sfx --prompt <txt> Generate sound effects
@@ -141,14 +142,18 @@ Options:
141
142
 
142
143
  Options:
143
144
  --ai Print agent-focused usage guide`,
144
- image: `Usage: flatkey image generate --prompt <txt> [options]
145
+ image: `Usage: flatkey image <command> [options]
146
+
147
+ Commands:
148
+ generate --prompt <txt> Generate image
149
+ upload --file <path> Upload local image and return a temporary URL
145
150
 
146
151
  Options:
147
- --model <model> Model id
148
- --size <size> Image size for OpenAI-style image models
149
- --quality <quality> Image quality for supported models
150
- --n <count> Number of images for supported models
151
- --output, -o <file> Write image file
152
+ --json Print machine-readable JSON`,
153
+ "image upload": `Usage: flatkey image upload --file <path> [options]
154
+
155
+ Options:
156
+ --file <path> Local image file to upload
152
157
  --json Print machine-readable JSON`,
153
158
  "image generate": `Usage: flatkey image generate --prompt <txt> [options]
154
159
 
@@ -212,9 +217,12 @@ Options:
212
217
  --duration <seconds> Video duration
213
218
  --ratio <value> 16:9, 9:16, 4:3, 3:4, 21:9, or 1:1
214
219
  --resolution <value> 480p, 720p, or 1080p
220
+ --image <file> Upload local reference image, repeatable
215
221
  --image-url <url> Add reference image URL, repeatable
222
+ --first-frame <file> Upload local first frame image
216
223
  --video-url <url> Add reference video URL, repeatable
217
224
  --first-frame-url <url> Add first frame image URL
225
+ --last-frame <file> Upload local last frame image
218
226
  --last-frame-url <url> Add last frame image URL
219
227
  --fps <fps> Frames per second
220
228
  --output, -o <file> Write video file
@@ -226,9 +234,12 @@ Options:
226
234
  --duration <seconds> Video duration
227
235
  --ratio <value> 16:9, 9:16, 4:3, 3:4, 21:9, or 1:1
228
236
  --resolution <value> 480p, 720p, or 1080p
237
+ --image <file> Upload local reference image, repeatable
229
238
  --image-url <url> Add reference image URL, repeatable
239
+ --first-frame <file> Upload local first frame image
230
240
  --video-url <url> Add reference video URL, repeatable
231
241
  --first-frame-url <url> Add first frame image URL
242
+ --last-frame <file> Upload local last frame image
232
243
  --last-frame-url <url> Add last frame image URL
233
244
  --fps <fps> Frames per second
234
245
  --output, -o <file> Write video file