@flatkey-ai/cli 0.1.25 → 0.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flatkey-ai/cli",
3
- "version": "0.1.25",
3
+ "version": "0.2.0",
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
@@ -29,12 +29,16 @@ export async function uploadTempMediaImage(options) {
29
29
  export function planImageRequest(options) {
30
30
  const model = options.model ?? "nano-banana-pro-preview";
31
31
  if (model.startsWith("gpt")) {
32
+ const responseFormat = options.response_format ?? options.responseFormat ?? "url";
33
+ const tempUrl = options.temp_url ?? options.tempUrl;
32
34
  return planJsonPost(options, "/v1/images/generations", cleanObject({
33
35
  model,
34
36
  prompt: options.prompt,
35
37
  size: options.size,
36
38
  n: parseOptionalInteger(options.n),
37
39
  quality: options.quality,
40
+ response_format: responseFormat,
41
+ temp_url: tempUrl ?? true,
38
42
  }));
39
43
  }
40
44
 
@@ -47,6 +51,7 @@ export function planImageRequest(options) {
47
51
  },
48
52
  body: {
49
53
  contents: [{ parts: [{ text: options.prompt }] }],
54
+ temp_url: true,
50
55
  },
51
56
  });
52
57
  }
@@ -88,6 +93,7 @@ export function planVideoRequest(options) {
88
93
  resolution,
89
94
  quality: resolution,
90
95
  fps: parseOptionalInteger(options.fps),
96
+ temp_url: true,
91
97
  images: !isSeedanceModel(model) && imageUrls.length > 0 ? imageUrls : undefined,
92
98
  });
93
99
  const seedanceContent = buildSeedanceContent(options);
@@ -232,7 +238,7 @@ function planRequest(options, path, init = {}) {
232
238
 
233
239
  async function requestJsonFromPlan(options, plan) {
234
240
  const fetchImpl = options.fetch ?? fetch;
235
- const response = await fetchImpl(plan.url, {
241
+ const request = {
236
242
  method: plan.method,
237
243
  headers: plan.headers,
238
244
  body: plan.body === undefined
@@ -240,8 +246,12 @@ async function requestJsonFromPlan(options, plan) {
240
246
  : isFormData(plan.body)
241
247
  ? plan.body
242
248
  : JSON.stringify(plan.body),
243
- });
249
+ };
250
+ logRequest(options, plan.url, request);
251
+ const response = await fetchImpl(plan.url, request);
252
+ logResponse(options, response);
244
253
  const body = await readJson(response);
254
+ logResponseBody(options, body);
245
255
  if (!response.ok || body?.success === false) {
246
256
  throw new FlatkeyError(extractErrorMessage(body, response.status), {
247
257
  status: response.status,
@@ -256,13 +266,17 @@ function isFormData(value) {
256
266
 
257
267
  async function requestBinaryArtifactFromPlan(options, plan) {
258
268
  const fetchImpl = options.fetch ?? fetch;
259
- const response = await fetchImpl(plan.url, {
269
+ const request = {
260
270
  method: plan.method,
261
271
  headers: plan.headers,
262
272
  body: plan.body === undefined ? undefined : JSON.stringify(plan.body),
263
- });
273
+ };
274
+ logRequest(options, plan.url, request);
275
+ const response = await fetchImpl(plan.url, request);
276
+ logResponse(options, response);
264
277
  if (!response.ok) {
265
278
  const body = await readJson(response);
279
+ logResponseBody(options, body);
266
280
  throw new FlatkeyError(extractErrorMessage(body, response.status), {
267
281
  status: response.status,
268
282
  });
@@ -272,6 +286,43 @@ async function requestBinaryArtifactFromPlan(options, plan) {
272
286
  };
273
287
  }
274
288
 
289
+ function logRequest(options, url, request) {
290
+ if (!options?.verbose) return;
291
+ const safeHeaders = redactHeaders(request.headers);
292
+ const safeBody = request.body === undefined
293
+ ? undefined
294
+ : isFormData(request.body)
295
+ ? "<form-data>"
296
+ : request.body;
297
+ options.verboseLog?.(`-> ${request.method} ${url}`);
298
+ options.verboseLog?.(`headers: ${JSON.stringify(safeHeaders)}`);
299
+ if (safeBody !== undefined) {
300
+ options.verboseLog?.(`body: ${safeBody}`);
301
+ }
302
+ }
303
+
304
+ function logResponse(options, response) {
305
+ if (!options?.verbose) return;
306
+ options.verboseLog?.(`<- ${response.status} ${response.statusText || ""}`.trim());
307
+ }
308
+
309
+ function logResponseBody(options, body) {
310
+ if (!options?.verbose) return;
311
+ options.verboseLog?.(`response: ${truncateLogValue(JSON.stringify(body))}`);
312
+ }
313
+
314
+ function redactHeaders(headers) {
315
+ return Object.fromEntries(Object.entries(headers ?? {}).map(([key, value]) => [
316
+ key,
317
+ key.toLowerCase() === "authorization" ? "Bearer <redacted>" : `${value}`,
318
+ ]));
319
+ }
320
+
321
+ function truncateLogValue(value) {
322
+ if (value === undefined) return "";
323
+ return value.length > 1200 ? `${value.slice(0, 1200)}…` : value;
324
+ }
325
+
275
326
  async function requestJson(options, path, init = {}) {
276
327
  return requestJsonFromPlan(options, planRequest(options, path, {
277
328
  method: init.method ?? "GET",
package/src/artifacts.js CHANGED
@@ -47,7 +47,7 @@ function expandHomePath(path) {
47
47
  }
48
48
 
49
49
  async function persistItem({ kind, item, outDir, output, index, fetchImpl }) {
50
- const dataUrl = getString(item, ["url", "video_url", "data_url", "dataUrl"]);
50
+ const dataUrl = getString(item, ["url", "video_url", "data_url", "dataUrl", "temp_url"]);
51
51
  if (dataUrl?.startsWith("data:")) {
52
52
  const parsed = parseDataUrl(dataUrl);
53
53
  const path = artifactPath({ kind, outDir, output, index, extension: parsed.extension });
@@ -94,6 +94,8 @@ function extractItems(response) {
94
94
  if (Array.isArray(response?.candidates)) {
95
95
  return response.candidates.flatMap((candidate) => candidate.content?.parts ?? []);
96
96
  }
97
+ if (typeof response?.temp_url === "string") return [{ url: response.temp_url }];
98
+ if (typeof response?.metadata?.temp_url === "string") return [{ url: response.metadata.temp_url }];
97
99
  if (typeof response?.metadata?.url === "string") return [{ url: response.metadata.url }];
98
100
  if (typeof response?.url === "string") return [response];
99
101
  if (typeof response?.video_url?.url === "string") return [{ url: response.video_url.url }];
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", "console_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", "verbose"]);
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"]),
@@ -187,7 +187,7 @@ export async function runCommand(command, deps = {}) {
187
187
  }
188
188
 
189
189
  if (command.group === "models") {
190
- return handleModels(command, deps);
190
+ return handleModels(command, { ...deps, stdout, stderr });
191
191
  }
192
192
 
193
193
  if (command.group === "login") {
@@ -206,12 +206,12 @@ export async function runCommand(command, deps = {}) {
206
206
  }
207
207
 
208
208
  if (command.group === "credits" || command.group === "status") {
209
- return handleUtility(command, deps);
209
+ return handleUtility(command, { ...deps, stdout, stderr });
210
210
  }
211
211
 
212
212
  if (["image", "video", "audio", "text"].includes(command.group)) {
213
213
  if (command.group === "audio" && command.action === "voices") {
214
- return handleVoices(command, deps);
214
+ return handleVoices(command, { ...deps, stdout, stderr });
215
215
  }
216
216
  const validAction = command.group === "audio"
217
217
  ? ["generate", "sfx", "music"].includes(command.action)
@@ -222,7 +222,7 @@ export async function runCommand(command, deps = {}) {
222
222
  throw new Error(`Unknown action for ${command.group}: ${command.action}`);
223
223
  }
224
224
  if (command.group === "image" && command.action === "upload") {
225
- return handleImageUpload(command, deps);
225
+ return handleImageUpload(command, { ...deps, stdout, stderr });
226
226
  }
227
227
  return handleGenerate(command, { ...deps, stdout, stderr });
228
228
  }
@@ -236,12 +236,17 @@ async function handleLogin(command, deps) {
236
236
  const deviceId = await ensureDeviceId({ configDir: deps.configDir });
237
237
  const version = await readPackageVersion();
238
238
  const consoleOrigin = firstNonEmpty(command.options.console_url, DEFAULT_CONSOLE_URL);
239
- const authorization = await createDeviceAuthorization({
239
+ const requestOptions = {
240
240
  consoleUrl: consoleOrigin,
241
+ fetch: deps.fetch,
242
+ verbose: command.options.verbose,
243
+ verboseLog: verboseLogger(deps.stderr, command.options.verbose),
244
+ };
245
+ const authorization = await createDeviceAuthorization({
241
246
  deviceId,
242
247
  clientName: "flatkey-cli",
243
248
  clientVersion: version,
244
- fetch: deps.fetch,
249
+ ...requestOptions,
245
250
  });
246
251
  const data = authorization?.data ?? authorization;
247
252
  if (!data?.device_code || !data?.verification_uri_complete) {
@@ -261,9 +266,8 @@ async function handleLogin(command, deps) {
261
266
  while (Date.now() < deadline) {
262
267
  await delay(nextLoginPollDelay(startedAt, initialIntervalMs), deps);
263
268
  const poll = await pollDeviceAuthorization({
264
- consoleUrl: consoleOrigin,
265
269
  deviceCode: data.device_code,
266
- fetch: deps.fetch,
270
+ ...requestOptions,
267
271
  });
268
272
  const pollData = poll?.data ?? poll;
269
273
  if (pollData?.status === "approved") {
@@ -432,6 +436,8 @@ async function handleImageUpload(command, deps) {
432
436
  filename: basename(file),
433
437
  contentType: imageContentTypeFromPath(file),
434
438
  fetch: deps.fetch,
439
+ verbose: command.options.verbose,
440
+ verboseLog: verboseLogger(deps.stderr, command.options.verbose),
435
441
  });
436
442
  const data = response?.data ?? response;
437
443
  const signedUrl = extractUploadedImageUrl(response);
@@ -494,6 +500,7 @@ async function handleGenerate(command, deps) {
494
500
  tempMediaBaseUrl: tempMediaBaseUrl ?? DEFAULT_CONSOLE_URL,
495
501
  env: deps.env ?? process.env,
496
502
  fetch: deps.fetch,
503
+ verboseLog: verboseLogger(deps.stderr, command.options.verbose),
497
504
  };
498
505
  if (command.group === "video" && !command.options.dry_run) {
499
506
  await uploadVideoLocalImages(options, { uploadTempMediaImage });
@@ -581,6 +588,8 @@ async function uploadLocalImage(file, options, deps) {
581
588
  filename: basename(file),
582
589
  contentType: imageContentTypeFromPath(file),
583
590
  fetch: options.fetch,
591
+ verbose: options.verbose,
592
+ verboseLog: options.verboseLog,
584
593
  });
585
594
  const url = extractUploadedImageUrl(response);
586
595
  if (!url) throw new Error(`Image upload failed: missing signed_url for ${file}`);
@@ -632,10 +641,12 @@ async function waitForVideoResult(response, options) {
632
641
  function hasArtifact(response) {
633
642
  return Boolean(
634
643
  response?.metadata?.url
644
+ || response?.metadata?.temp_url
635
645
  || response?.url
646
+ || response?.temp_url
636
647
  || response?.video_url?.url
637
- || response?.data?.some?.((item) => item?.url || item?.b64_json || item?.base64 || item?.data)
638
- || response?.artifacts?.some?.((item) => item?.url || item?.path)
648
+ || response?.data?.some?.((item) => item?.url || item?.temp_url || item?.b64_json || item?.base64 || item?.data)
649
+ || response?.artifacts?.some?.((item) => item?.url || item?.temp_url || item?.path)
639
650
  || response?.content?.some?.((item) => item?.url || item?.video_url?.url || item?.data_url || item?.b64_json || item?.base64 || item?.data),
640
651
  );
641
652
  }
@@ -696,6 +707,8 @@ async function handleVoices(command, deps) {
696
707
  apiKey,
697
708
  baseUrl: routerOrigin,
698
709
  fetch: deps.fetch,
710
+ verbose: command.options.verbose,
711
+ verboseLog: verboseLogger(deps.stderr, command.options.verbose),
699
712
  });
700
713
  }
701
714
 
@@ -755,6 +768,8 @@ async function handleUtility(command, deps) {
755
768
  apiKey,
756
769
  baseUrl: consoleOrigin,
757
770
  fetch: deps.fetch,
771
+ verbose: command.options.verbose,
772
+ verboseLog: verboseLogger(deps.stderr, command.options.verbose),
758
773
  };
759
774
  if (command.group === "credits") return getCredits(options);
760
775
 
@@ -841,10 +856,21 @@ async function handleModels(command, deps) {
841
856
  apiKey,
842
857
  baseUrl: consoleOrigin,
843
858
  fetch: deps.fetch,
859
+ verbose: command.options.verbose,
860
+ verboseLog: verboseLogger(deps.stderr, command.options.verbose),
844
861
  });
845
862
  return { models: normalizeModels(response, command.options.type) };
846
863
  }
847
864
 
865
+ function verboseLogger(stream, enabled) {
866
+ if (!enabled) return () => {};
867
+ return (line) => {
868
+ if (typeof line === "string") {
869
+ stream?.write?.(`${line}\n`);
870
+ }
871
+ };
872
+ }
873
+
848
874
  function formatHuman(result) {
849
875
  if (typeof result === "string") return result;
850
876
  if (!result || typeof result !== "object") return String(result);
package/src/help.js CHANGED
@@ -67,6 +67,7 @@ Commands:
67
67
  Global options:
68
68
  --json Print machine-readable JSON
69
69
  --output, -o <file> Write generated output to a local file
70
+ --verbose Print request/response logs to stderr
70
71
  --base-url <url> Override Flatkey router URL
71
72
 
72
73
  Environment: