@flatkey-ai/cli 0.1.23 → 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.23",
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,12 +266,12 @@ 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") {
274
+ const account = accountFromAuthData(pollData);
270
275
  if (!pollData.api_key) {
271
276
  const saved = await readConfig(deps.configDir);
272
277
  if (typeof saved?.apiKey === "string" && saved.apiKey.trim() !== "") {
@@ -276,7 +281,7 @@ async function handleLogin(command, deps) {
276
281
  deviceId,
277
282
  userId: pollData.user_id ?? saved.auth?.userId,
278
283
  tokenId: pollData.token_id ?? saved.auth?.tokenId,
279
- account: accountFromAuthData(pollData) ?? saved.auth?.account,
284
+ account: account ?? saved.auth?.account,
280
285
  loginAt: Math.floor(Date.now() / 1000),
281
286
  },
282
287
  configDir: deps.configDir,
@@ -285,9 +290,7 @@ async function handleLogin(command, deps) {
285
290
  ? {
286
291
  success: true,
287
292
  configPath,
288
- tokenId: pollData.token_id ?? saved.auth?.tokenId,
289
- userId: pollData.user_id ?? saved.auth?.userId,
290
- account: accountFromAuthData(pollData) ?? saved.auth?.account,
293
+ account: stripDisplayOnlyIds(account ?? saved.auth?.account),
291
294
  reusedConfig: true,
292
295
  }
293
296
  : `Flatkey CLI already authorized. Saved config: ${configPath}`;
@@ -300,7 +303,7 @@ async function handleLogin(command, deps) {
300
303
  deviceId,
301
304
  userId: pollData.user_id,
302
305
  tokenId: pollData.token_id,
303
- account: accountFromAuthData(pollData),
306
+ account,
304
307
  loginAt: Math.floor(Date.now() / 1000),
305
308
  },
306
309
  configDir: deps.configDir,
@@ -309,9 +312,7 @@ async function handleLogin(command, deps) {
309
312
  ? {
310
313
  success: true,
311
314
  configPath,
312
- tokenId: pollData.token_id,
313
- userId: pollData.user_id,
314
- account: accountFromAuthData(pollData),
315
+ account: stripDisplayOnlyIds(account),
315
316
  }
316
317
  : `Flatkey CLI authorized. Saved config: ${configPath}`;
317
318
  }
@@ -435,6 +436,8 @@ async function handleImageUpload(command, deps) {
435
436
  filename: basename(file),
436
437
  contentType: imageContentTypeFromPath(file),
437
438
  fetch: deps.fetch,
439
+ verbose: command.options.verbose,
440
+ verboseLog: verboseLogger(deps.stderr, command.options.verbose),
438
441
  });
439
442
  const data = response?.data ?? response;
440
443
  const signedUrl = extractUploadedImageUrl(response);
@@ -497,6 +500,7 @@ async function handleGenerate(command, deps) {
497
500
  tempMediaBaseUrl: tempMediaBaseUrl ?? DEFAULT_CONSOLE_URL,
498
501
  env: deps.env ?? process.env,
499
502
  fetch: deps.fetch,
503
+ verboseLog: verboseLogger(deps.stderr, command.options.verbose),
500
504
  };
501
505
  if (command.group === "video" && !command.options.dry_run) {
502
506
  await uploadVideoLocalImages(options, { uploadTempMediaImage });
@@ -584,6 +588,8 @@ async function uploadLocalImage(file, options, deps) {
584
588
  filename: basename(file),
585
589
  contentType: imageContentTypeFromPath(file),
586
590
  fetch: options.fetch,
591
+ verbose: options.verbose,
592
+ verboseLog: options.verboseLog,
587
593
  });
588
594
  const url = extractUploadedImageUrl(response);
589
595
  if (!url) throw new Error(`Image upload failed: missing signed_url for ${file}`);
@@ -635,10 +641,12 @@ async function waitForVideoResult(response, options) {
635
641
  function hasArtifact(response) {
636
642
  return Boolean(
637
643
  response?.metadata?.url
644
+ || response?.metadata?.temp_url
638
645
  || response?.url
646
+ || response?.temp_url
639
647
  || response?.video_url?.url
640
- || response?.data?.some?.((item) => item?.url || item?.b64_json || item?.base64 || item?.data)
641
- || 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)
642
650
  || response?.content?.some?.((item) => item?.url || item?.video_url?.url || item?.data_url || item?.b64_json || item?.base64 || item?.data),
643
651
  );
644
652
  }
@@ -699,6 +707,8 @@ async function handleVoices(command, deps) {
699
707
  apiKey,
700
708
  baseUrl: routerOrigin,
701
709
  fetch: deps.fetch,
710
+ verbose: command.options.verbose,
711
+ verboseLog: verboseLogger(deps.stderr, command.options.verbose),
702
712
  });
703
713
  }
704
714
 
@@ -758,15 +768,18 @@ async function handleUtility(command, deps) {
758
768
  apiKey,
759
769
  baseUrl: consoleOrigin,
760
770
  fetch: deps.fetch,
771
+ verbose: command.options.verbose,
772
+ verboseLog: verboseLogger(deps.stderr, command.options.verbose),
761
773
  };
762
774
  if (command.group === "credits") return getCredits(options);
763
775
 
764
- const status = await getStatus(options);
776
+ const status = sanitizeStatusResponse(await getStatus(options));
765
777
  const account = effectiveAccountForStatus({ command, saved, apiKey });
766
778
  if (!account || !status || typeof status !== "object" || Array.isArray(status) || status.account !== undefined) {
767
- return status;
779
+ return command.options.json ? status : extractStatusDisplayFields(status);
768
780
  }
769
- return { ...status, account };
781
+ const mergedStatus = sanitizeStatusResponse({ ...status, account });
782
+ return command.options.json ? mergedStatus : extractStatusDisplayFields(mergedStatus);
770
783
  }
771
784
 
772
785
  function effectiveAccountForStatus({ command, saved, apiKey }) {
@@ -778,16 +791,52 @@ function effectiveAccountForStatus({ command, saved, apiKey }) {
778
791
  function accountFromAuthData(data) {
779
792
  if (!data || typeof data !== "object") return null;
780
793
  const email = data.account?.email ?? data.email ?? data.userEmail ?? data.user_email;
794
+ const name = data.account?.name ?? data.name;
781
795
  const userId = data.account?.userId ?? data.account?.user_id ?? data.userId ?? data.user_id;
782
- if (email === undefined && userId === undefined) return null;
796
+ if (email === undefined && name === undefined && userId === undefined) return null;
783
797
  return Object.fromEntries(
784
798
  Object.entries({
785
799
  email,
800
+ name,
786
801
  userId,
787
802
  }).filter(([, value]) => value !== undefined && value !== null),
788
803
  );
789
804
  }
790
805
 
806
+ function sanitizeStatusResponse(status) {
807
+ if (!status || typeof status !== "object") return status;
808
+ if (Array.isArray(status)) {
809
+ return status.map((entry) => sanitizeStatusResponse(entry));
810
+ }
811
+ return Object.fromEntries(
812
+ Object.entries(status)
813
+ .filter(([key]) => !["userId", "user_id"].includes(key))
814
+ .map(([key, value]) => [key, sanitizeStatusResponse(value)]),
815
+ );
816
+ }
817
+
818
+ function stripDisplayOnlyIds(value) {
819
+ if (!value || typeof value !== "object") return value;
820
+ if (Array.isArray(value)) {
821
+ return value.map((entry) => stripDisplayOnlyIds(entry));
822
+ }
823
+ return Object.fromEntries(
824
+ Object.entries(value)
825
+ .filter(([key]) => !["userId", "user_id", "tokenId", "token_id"].includes(key))
826
+ .map(([key, nested]) => [key, stripDisplayOnlyIds(nested)]),
827
+ );
828
+ }
829
+
830
+ function extractStatusDisplayFields(status) {
831
+ if (!status || typeof status !== "object") return status;
832
+ const email = status.email ?? status.account?.email ?? status.user?.email;
833
+ const name = status.name ?? status.account?.name ?? status.user?.name;
834
+ const result = {};
835
+ if (email !== undefined) result.email = email;
836
+ if (name !== undefined) result.name = name;
837
+ return result;
838
+ }
839
+
791
840
  async function handleModels(command, deps) {
792
841
  const { resolveApiKey, resolveOrigins } = await import("./config.js");
793
842
  const { getModels } = await import("./api.js");
@@ -807,10 +856,21 @@ async function handleModels(command, deps) {
807
856
  apiKey,
808
857
  baseUrl: consoleOrigin,
809
858
  fetch: deps.fetch,
859
+ verbose: command.options.verbose,
860
+ verboseLog: verboseLogger(deps.stderr, command.options.verbose),
810
861
  });
811
862
  return { models: normalizeModels(response, command.options.type) };
812
863
  }
813
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
+
814
874
  function formatHuman(result) {
815
875
  if (typeof result === "string") return result;
816
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: