@flatkey-ai/cli 0.1.6 → 0.1.8
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 +1 -1
- package/src/api.js +46 -4
- package/src/artifacts.js +9 -0
- package/src/cli.js +237 -8
- package/src/config.js +57 -0
- package/src/help.js +30 -1
package/package.json
CHANGED
package/src/api.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export const DEFAULT_BASE_URL = "https://router.flatkey.ai";
|
|
2
2
|
export const DEFAULT_MODELS_BASE_URL = "https://console.flatkey.ai";
|
|
3
|
+
export const DEFAULT_CONSOLE_URL = "https://console.flatkey.ai";
|
|
3
4
|
|
|
4
5
|
export class FlatkeyError extends Error {
|
|
5
6
|
constructor(message, { status } = {}) {
|
|
@@ -124,11 +125,17 @@ export function planTextRequest(options) {
|
|
|
124
125
|
}
|
|
125
126
|
|
|
126
127
|
export function getCredits(options) {
|
|
127
|
-
return requestJson(
|
|
128
|
+
return requestJson({
|
|
129
|
+
...options,
|
|
130
|
+
baseUrl: options.baseUrl ?? DEFAULT_CONSOLE_URL,
|
|
131
|
+
}, "/v1/credits");
|
|
128
132
|
}
|
|
129
133
|
|
|
130
134
|
export function getStatus(options) {
|
|
131
|
-
return requestJson(
|
|
135
|
+
return requestJson({
|
|
136
|
+
...options,
|
|
137
|
+
baseUrl: options.baseUrl ?? DEFAULT_CONSOLE_URL,
|
|
138
|
+
}, "/v1/status");
|
|
132
139
|
}
|
|
133
140
|
|
|
134
141
|
export function getModels(options) {
|
|
@@ -138,6 +145,32 @@ export function getModels(options) {
|
|
|
138
145
|
}, "/v1/available_models");
|
|
139
146
|
}
|
|
140
147
|
|
|
148
|
+
export async function createDeviceAuthorization(options) {
|
|
149
|
+
return requestJson({
|
|
150
|
+
...options,
|
|
151
|
+
baseUrl: options.consoleUrl ?? DEFAULT_CONSOLE_URL,
|
|
152
|
+
}, "/api/cli/device_authorizations", {
|
|
153
|
+
method: "POST",
|
|
154
|
+
body: JSON.stringify({
|
|
155
|
+
client_name: options.clientName ?? "flatkey-cli",
|
|
156
|
+
client_version: options.clientVersion,
|
|
157
|
+
device_id: options.deviceId,
|
|
158
|
+
}),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export async function pollDeviceAuthorization(options) {
|
|
163
|
+
return requestJson({
|
|
164
|
+
...options,
|
|
165
|
+
baseUrl: options.consoleUrl ?? DEFAULT_CONSOLE_URL,
|
|
166
|
+
}, "/api/cli/device_authorizations/token", {
|
|
167
|
+
method: "POST",
|
|
168
|
+
body: JSON.stringify({
|
|
169
|
+
device_code: options.deviceCode,
|
|
170
|
+
}),
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
141
174
|
async function postJson(options, path, payload) {
|
|
142
175
|
return requestJsonFromPlan(options, planJsonPost(options, path, payload));
|
|
143
176
|
}
|
|
@@ -257,7 +290,16 @@ async function readJson(response) {
|
|
|
257
290
|
}
|
|
258
291
|
|
|
259
292
|
function extractErrorMessage(body, status) {
|
|
260
|
-
|
|
261
|
-
|
|
293
|
+
const message = typeof body?.error?.message === "string"
|
|
294
|
+
? body.error.message
|
|
295
|
+
: typeof body?.message === "string"
|
|
296
|
+
? body.message
|
|
297
|
+
: undefined;
|
|
298
|
+
if (message === "Token not provided") return missingApiKeyMessage();
|
|
299
|
+
if (message) return message;
|
|
262
300
|
return `Flatkey API request failed with HTTP ${status}`;
|
|
263
301
|
}
|
|
302
|
+
|
|
303
|
+
function missingApiKeyMessage() {
|
|
304
|
+
return "Missing Flatkey API key. Create one at https://console.flatkey.ai/keys, then run `flatkey onboard --api-key <key>` or set FLATKEY_API_KEY.";
|
|
305
|
+
}
|
package/src/artifacts.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
2
3
|
import { dirname, extname, join } from "node:path";
|
|
3
4
|
|
|
4
5
|
const DEFAULT_EXTENSIONS = {
|
|
@@ -25,6 +26,7 @@ export async function persistArtifacts({
|
|
|
25
26
|
output,
|
|
26
27
|
fetch: fetchImpl = fetch,
|
|
27
28
|
}) {
|
|
29
|
+
output = expandHomePath(output);
|
|
28
30
|
const items = extractItems(response);
|
|
29
31
|
const artifacts = [];
|
|
30
32
|
await mkdir(output ? dirname(output) : outDir, { recursive: true });
|
|
@@ -37,6 +39,13 @@ export async function persistArtifacts({
|
|
|
37
39
|
return artifacts;
|
|
38
40
|
}
|
|
39
41
|
|
|
42
|
+
function expandHomePath(path) {
|
|
43
|
+
if (typeof path !== "string") return path;
|
|
44
|
+
if (path === "~") return homedir();
|
|
45
|
+
if (path.startsWith("~/")) return join(homedir(), path.slice(2));
|
|
46
|
+
return path;
|
|
47
|
+
}
|
|
48
|
+
|
|
40
49
|
async function persistItem({ kind, item, outDir, output, index, fetchImpl }) {
|
|
41
50
|
const dataUrl = getString(item, ["url", "data_url", "dataUrl"]);
|
|
42
51
|
if (dataUrl?.startsWith("data:")) {
|
package/src/cli.js
CHANGED
|
@@ -3,15 +3,40 @@ const COMMANDS = new Set([
|
|
|
3
3
|
"credits",
|
|
4
4
|
"help",
|
|
5
5
|
"image",
|
|
6
|
+
"login",
|
|
6
7
|
"models",
|
|
7
8
|
"onboard",
|
|
9
|
+
"logout",
|
|
10
|
+
"auth",
|
|
8
11
|
"status",
|
|
9
12
|
"text",
|
|
10
13
|
"version",
|
|
11
14
|
"video",
|
|
12
15
|
]);
|
|
13
16
|
|
|
14
|
-
const GROUP_ACTIONS = new Set(["audio", "image", "text", "video"]);
|
|
17
|
+
const GROUP_ACTIONS = new Set(["audio", "auth", "image", "text", "video"]);
|
|
18
|
+
const GLOBAL_OPTIONS = new Set(["api_key", "base_url", "dry_run", "help", "json", "output", "out"]);
|
|
19
|
+
const COMMAND_OPTIONS = {
|
|
20
|
+
"audio generate": new Set(["model", "prompt", "similarity_boost", "stability", "style", "voice_id"]),
|
|
21
|
+
"audio music": new Set(["music_length_ms", "prompt"]),
|
|
22
|
+
"audio sfx": new Set(["duration", "prompt"]),
|
|
23
|
+
"audio voices": new Set([]),
|
|
24
|
+
"auth status": new Set([]),
|
|
25
|
+
credits: new Set([]),
|
|
26
|
+
help: new Set(["ai", "command"]),
|
|
27
|
+
image: new Set([]),
|
|
28
|
+
"image generate": new Set(["model", "n", "prompt", "quality", "size"]),
|
|
29
|
+
login: new Set(["console_url", "no_open", "open"]),
|
|
30
|
+
logout: new Set([]),
|
|
31
|
+
models: new Set(["type"]),
|
|
32
|
+
onboard: new Set(["api_key"]),
|
|
33
|
+
status: new Set([]),
|
|
34
|
+
text: new Set([]),
|
|
35
|
+
"text generate": new Set(["model", "prompt"]),
|
|
36
|
+
version: new Set([]),
|
|
37
|
+
video: new Set([]),
|
|
38
|
+
"video generate": new Set(["aspect", "duration", "fps", "model", "prompt", "ratio", "resolution"]),
|
|
39
|
+
};
|
|
15
40
|
|
|
16
41
|
export function parseArgv(argv) {
|
|
17
42
|
const [group, maybeAction, ...rest] = argv;
|
|
@@ -28,11 +53,11 @@ export function parseArgv(argv) {
|
|
|
28
53
|
throw new Error(`Unknown command: ${group}`);
|
|
29
54
|
}
|
|
30
55
|
if (group === "help" && maybeAction && !maybeAction.startsWith("--")) {
|
|
31
|
-
return {
|
|
56
|
+
return validateCommandOptions({
|
|
32
57
|
group: "help",
|
|
33
58
|
action: undefined,
|
|
34
59
|
options: { command: maybeAction, ...parseOptions(rest) },
|
|
35
|
-
};
|
|
60
|
+
});
|
|
36
61
|
}
|
|
37
62
|
|
|
38
63
|
const hasAction = GROUP_ACTIONS.has(group);
|
|
@@ -47,21 +72,21 @@ export function parseArgv(argv) {
|
|
|
47
72
|
const hasHelpOption = optionTokens.some((token) => token === "--help" || token === "-h")
|
|
48
73
|
|| (optionTokens.length === 1 && optionTokens[0] === "help");
|
|
49
74
|
if (hasHelpOption) {
|
|
50
|
-
return {
|
|
75
|
+
return validateCommandOptions({
|
|
51
76
|
group,
|
|
52
77
|
action,
|
|
53
78
|
options: {
|
|
54
79
|
...parseOptions(optionTokens.filter((token) => token !== "help" && token !== "--help" && token !== "-h")),
|
|
55
80
|
help: true,
|
|
56
81
|
},
|
|
57
|
-
};
|
|
82
|
+
});
|
|
58
83
|
}
|
|
59
84
|
|
|
60
|
-
return {
|
|
85
|
+
return validateCommandOptions({
|
|
61
86
|
group,
|
|
62
87
|
action,
|
|
63
88
|
options: parseOptions(optionTokens),
|
|
64
|
-
};
|
|
89
|
+
});
|
|
65
90
|
}
|
|
66
91
|
|
|
67
92
|
function isHelpToken(token) {
|
|
@@ -96,6 +121,20 @@ function parseOptions(tokens) {
|
|
|
96
121
|
return options;
|
|
97
122
|
}
|
|
98
123
|
|
|
124
|
+
function validateCommandOptions(command) {
|
|
125
|
+
const key = command.action ? `${command.group} ${command.action}` : command.group;
|
|
126
|
+
const allowed = COMMAND_OPTIONS[key] ?? new Set();
|
|
127
|
+
for (const option of Object.keys(command.options)) {
|
|
128
|
+
if (GLOBAL_OPTIONS.has(option) || allowed.has(option)) continue;
|
|
129
|
+
const flag = `--${option.replaceAll("_", "-")}`;
|
|
130
|
+
const helpCommand = command.action
|
|
131
|
+
? `flatkey ${command.group} ${command.action} --help`
|
|
132
|
+
: `flatkey ${command.group} --help`;
|
|
133
|
+
throw new Error(`Unknown option ${flag} for flatkey ${key}. Run \`${helpCommand}\` to see supported options.`);
|
|
134
|
+
}
|
|
135
|
+
return command;
|
|
136
|
+
}
|
|
137
|
+
|
|
99
138
|
export async function main(argv) {
|
|
100
139
|
const command = parseArgv(argv);
|
|
101
140
|
if (command.options.help) {
|
|
@@ -140,6 +179,21 @@ export async function runCommand(command, deps = {}) {
|
|
|
140
179
|
return handleModels(command, deps);
|
|
141
180
|
}
|
|
142
181
|
|
|
182
|
+
if (command.group === "login") {
|
|
183
|
+
return handleLogin(command, { ...deps, stdout, stderr });
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (command.group === "logout") {
|
|
187
|
+
return handleLogout(command, deps);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (command.group === "auth") {
|
|
191
|
+
if (command.action !== "status") {
|
|
192
|
+
throw new Error(`Unknown action for auth: ${command.action}`);
|
|
193
|
+
}
|
|
194
|
+
return handleAuthStatus(command, deps);
|
|
195
|
+
}
|
|
196
|
+
|
|
143
197
|
if (command.group === "credits" || command.group === "status") {
|
|
144
198
|
return handleUtility(command, deps);
|
|
145
199
|
}
|
|
@@ -160,6 +214,150 @@ export async function runCommand(command, deps = {}) {
|
|
|
160
214
|
throw new Error(`Unknown command: ${command.group}`);
|
|
161
215
|
}
|
|
162
216
|
|
|
217
|
+
async function handleLogin(command, deps) {
|
|
218
|
+
const { ensureDeviceId, writeAuthConfig } = await import("./config.js");
|
|
219
|
+
const { createDeviceAuthorization, pollDeviceAuthorization } = await import("./api.js");
|
|
220
|
+
const deviceId = await ensureDeviceId({ configDir: deps.configDir });
|
|
221
|
+
const version = await readPackageVersion();
|
|
222
|
+
const consoleUrl = command.options.console_url;
|
|
223
|
+
const authorization = await createDeviceAuthorization({
|
|
224
|
+
consoleUrl,
|
|
225
|
+
deviceId,
|
|
226
|
+
clientName: "flatkey-cli",
|
|
227
|
+
clientVersion: version,
|
|
228
|
+
fetch: deps.fetch,
|
|
229
|
+
});
|
|
230
|
+
const data = authorization?.data ?? authorization;
|
|
231
|
+
if (!data?.device_code || !data?.verification_uri_complete) {
|
|
232
|
+
throw new Error("Flatkey login failed: missing device authorization response.");
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (!command.options.json) {
|
|
236
|
+
deps.stdout?.write?.(`Open this URL to approve Flatkey CLI:\n${data.verification_uri_complete}\n\n`);
|
|
237
|
+
}
|
|
238
|
+
if (command.options.open !== false && command.options.no_open !== true) {
|
|
239
|
+
await openBrowser(data.verification_uri_complete, deps);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const initialIntervalMs = Math.max(Number(data.interval ?? 5), 5) * 1000;
|
|
243
|
+
const deadline = Date.now() + Math.max(Number(data.expires_in ?? 600), 1) * 1000;
|
|
244
|
+
const startedAt = Date.now();
|
|
245
|
+
while (Date.now() < deadline) {
|
|
246
|
+
await delay(nextLoginPollDelay(startedAt, initialIntervalMs), deps);
|
|
247
|
+
const poll = await pollDeviceAuthorization({
|
|
248
|
+
consoleUrl,
|
|
249
|
+
deviceCode: data.device_code,
|
|
250
|
+
fetch: deps.fetch,
|
|
251
|
+
});
|
|
252
|
+
const pollData = poll?.data ?? poll;
|
|
253
|
+
if (pollData?.status === "approved") {
|
|
254
|
+
if (!pollData.api_key) {
|
|
255
|
+
throw new Error("Flatkey login approved but no API key was returned.");
|
|
256
|
+
}
|
|
257
|
+
const configPath = await writeAuthConfig({
|
|
258
|
+
apiKey: pollData.api_key,
|
|
259
|
+
auth: {
|
|
260
|
+
deviceId,
|
|
261
|
+
userId: pollData.user_id,
|
|
262
|
+
tokenId: pollData.token_id,
|
|
263
|
+
loginAt: Math.floor(Date.now() / 1000),
|
|
264
|
+
},
|
|
265
|
+
configDir: deps.configDir,
|
|
266
|
+
});
|
|
267
|
+
return command.options.json
|
|
268
|
+
? { success: true, configPath, tokenId: pollData.token_id, userId: pollData.user_id }
|
|
269
|
+
: `Flatkey CLI authorized. Saved config: ${configPath}`;
|
|
270
|
+
}
|
|
271
|
+
if (pollData?.status === "denied") {
|
|
272
|
+
throw new Error("Flatkey login denied.");
|
|
273
|
+
}
|
|
274
|
+
if (pollData?.status === "expired") {
|
|
275
|
+
throw new Error("Flatkey login expired. Run `flatkey login` again.");
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
throw new Error("Flatkey login timed out. Run `flatkey login` again.");
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function nextLoginPollDelay(startedAt, initialIntervalMs) {
|
|
282
|
+
const elapsed = Date.now() - startedAt;
|
|
283
|
+
const base = elapsed < 30_000
|
|
284
|
+
? initialIntervalMs
|
|
285
|
+
: elapsed < 120_000
|
|
286
|
+
? Math.max(initialIntervalMs, 10_000)
|
|
287
|
+
: Math.max(initialIntervalMs, 15_000);
|
|
288
|
+
return base + Math.floor(Math.random() * 800);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async function delay(ms, deps = {}) {
|
|
292
|
+
const sleep = deps.sleep ?? ((duration) => new Promise((resolve) => setTimeout(resolve, duration)));
|
|
293
|
+
return sleep(ms);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async function openBrowser(url, deps = {}) {
|
|
297
|
+
if (deps.openBrowser) return deps.openBrowser(url);
|
|
298
|
+
const { spawn } = await import("node:child_process");
|
|
299
|
+
const platform = deps.platform ?? process.platform;
|
|
300
|
+
const command = platform === "darwin"
|
|
301
|
+
? "open"
|
|
302
|
+
: platform === "win32"
|
|
303
|
+
? "cmd"
|
|
304
|
+
: "xdg-open";
|
|
305
|
+
const args = platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
306
|
+
try {
|
|
307
|
+
const child = spawn(command, args, { stdio: "ignore", detached: true });
|
|
308
|
+
child.unref();
|
|
309
|
+
} catch {
|
|
310
|
+
// Printed URL is enough when open is unavailable.
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function maskKey(key) {
|
|
315
|
+
if (!key) return "";
|
|
316
|
+
if (key.length <= 8) return `${key.slice(0, 2)}****${key.slice(-2)}`;
|
|
317
|
+
return `${key.slice(0, 6)}...${key.slice(-4)}`;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function formatAuthStatus(status) {
|
|
321
|
+
if (!status.authenticated) return "Not authenticated";
|
|
322
|
+
return `Authenticated via ${status.source}: ${status.key}`;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async function handleLogout(command, deps = {}) {
|
|
326
|
+
const { clearSavedApiKey } = await import("./config.js");
|
|
327
|
+
const configPath = await clearSavedApiKey({ configDir: deps.configDir });
|
|
328
|
+
return command.options.json
|
|
329
|
+
? { success: true, configPath }
|
|
330
|
+
: `Removed saved Flatkey API key from ${configPath}`;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async function handleAuthStatus(command, deps) {
|
|
334
|
+
const { readConfig, resolveApiKey } = await import("./config.js");
|
|
335
|
+
const saved = await readConfig(deps.configDir);
|
|
336
|
+
let apiKey;
|
|
337
|
+
try {
|
|
338
|
+
apiKey = await resolveApiKey({
|
|
339
|
+
apiKey: command.options.api_key,
|
|
340
|
+
env: deps.env ?? process.env,
|
|
341
|
+
configDir: deps.configDir,
|
|
342
|
+
});
|
|
343
|
+
} catch {
|
|
344
|
+
apiKey = "";
|
|
345
|
+
}
|
|
346
|
+
const status = {
|
|
347
|
+
authenticated: Boolean(apiKey),
|
|
348
|
+
source: command.options.api_key
|
|
349
|
+
? "option"
|
|
350
|
+
: (deps.env ?? process.env).FLATKEY_API_KEY
|
|
351
|
+
? "env"
|
|
352
|
+
: saved?.apiKey
|
|
353
|
+
? "config"
|
|
354
|
+
: "none",
|
|
355
|
+
key: maskKey(apiKey),
|
|
356
|
+
auth: saved?.auth ?? null,
|
|
357
|
+
};
|
|
358
|
+
return command.options.json ? status : formatAuthStatus(status);
|
|
359
|
+
}
|
|
360
|
+
|
|
163
361
|
async function handleGenerate(command, deps) {
|
|
164
362
|
const { resolveApiKey } = await import("./config.js");
|
|
165
363
|
const {
|
|
@@ -233,12 +431,33 @@ async function handleGenerate(command, deps) {
|
|
|
233
431
|
output: command.options.output,
|
|
234
432
|
fetch: deps.fetch,
|
|
235
433
|
});
|
|
236
|
-
return { kind: command.group, artifacts, response };
|
|
434
|
+
return { kind: command.group, artifacts, response: scrubArtifactResponse(response) };
|
|
237
435
|
} finally {
|
|
238
436
|
animation.stop();
|
|
239
437
|
}
|
|
240
438
|
}
|
|
241
439
|
|
|
440
|
+
function scrubArtifactResponse(value) {
|
|
441
|
+
if (Array.isArray(value)) return value.map(scrubArtifactResponse);
|
|
442
|
+
if (!value || typeof value !== "object") {
|
|
443
|
+
if (typeof value === "string" && value.startsWith("data:")) return "<artifact omitted>";
|
|
444
|
+
return value;
|
|
445
|
+
}
|
|
446
|
+
return Object.fromEntries(
|
|
447
|
+
Object.entries(value).map(([key, entry]) => {
|
|
448
|
+
if (["b64_json", "base64", "data"].includes(key) && typeof entry === "string") {
|
|
449
|
+
return [key, "<artifact omitted>"];
|
|
450
|
+
}
|
|
451
|
+
if (["url", "data_url", "dataUrl"].includes(key)
|
|
452
|
+
&& typeof entry === "string"
|
|
453
|
+
&& entry.startsWith("data:")) {
|
|
454
|
+
return [key, "<artifact omitted>"];
|
|
455
|
+
}
|
|
456
|
+
return [key, scrubArtifactResponse(entry)];
|
|
457
|
+
}),
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
|
|
242
461
|
async function handleVoices(command, deps) {
|
|
243
462
|
const { resolveApiKey } = await import("./config.js");
|
|
244
463
|
const { getVoices } = await import("./api.js");
|
|
@@ -262,6 +481,7 @@ function extractText(response) {
|
|
|
262
481
|
|
|
263
482
|
async function writeTextOutput(text, output) {
|
|
264
483
|
if (!output) return undefined;
|
|
484
|
+
output = await expandHomePath(output);
|
|
265
485
|
const { mkdir, writeFile } = await import("node:fs/promises");
|
|
266
486
|
const { dirname } = await import("node:path");
|
|
267
487
|
await mkdir(dirname(output), { recursive: true });
|
|
@@ -269,6 +489,15 @@ async function writeTextOutput(text, output) {
|
|
|
269
489
|
return output;
|
|
270
490
|
}
|
|
271
491
|
|
|
492
|
+
async function expandHomePath(path) {
|
|
493
|
+
if (typeof path !== "string") return path;
|
|
494
|
+
if (path !== "~" && !path.startsWith("~/")) return path;
|
|
495
|
+
const { homedir } = await import("node:os");
|
|
496
|
+
const { join } = await import("node:path");
|
|
497
|
+
if (path === "~") return homedir();
|
|
498
|
+
return join(homedir(), path.slice(2));
|
|
499
|
+
}
|
|
500
|
+
|
|
272
501
|
function redactRequest(request) {
|
|
273
502
|
return {
|
|
274
503
|
...request,
|
package/src/config.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { mkdir, readFile, writeFile, chmod } from "node:fs/promises";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
2
3
|
import { homedir } from "node:os";
|
|
3
4
|
import { join } from "node:path";
|
|
4
5
|
|
|
@@ -44,6 +45,62 @@ export async function writeConfig({ apiKey, configDir = getDefaultConfigDir() })
|
|
|
44
45
|
return configPath;
|
|
45
46
|
}
|
|
46
47
|
|
|
48
|
+
export async function writeAuthConfig({
|
|
49
|
+
apiKey,
|
|
50
|
+
auth,
|
|
51
|
+
configDir = getDefaultConfigDir(),
|
|
52
|
+
}) {
|
|
53
|
+
if (typeof apiKey !== "string" || apiKey.trim() === "") {
|
|
54
|
+
throw new Error("Missing API key from Flatkey login response.");
|
|
55
|
+
}
|
|
56
|
+
const saved = await readSavedConfig(configDir) ?? {};
|
|
57
|
+
const next = {
|
|
58
|
+
...saved,
|
|
59
|
+
apiKey,
|
|
60
|
+
auth: {
|
|
61
|
+
...(saved.auth ?? {}),
|
|
62
|
+
...auth,
|
|
63
|
+
type: "device",
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
await mkdir(configDir, { recursive: true });
|
|
67
|
+
const configPath = getConfigPath(configDir);
|
|
68
|
+
await writeFile(configPath, `${JSON.stringify(next, null, 2)}\n`, {
|
|
69
|
+
mode: 0o600,
|
|
70
|
+
});
|
|
71
|
+
try {
|
|
72
|
+
await chmod(configPath, 0o600);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if (process.platform !== "win32") throw error;
|
|
75
|
+
}
|
|
76
|
+
return configPath;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function ensureDeviceId({ configDir = getDefaultConfigDir() } = {}) {
|
|
80
|
+
const saved = await readSavedConfig(configDir);
|
|
81
|
+
if (typeof saved?.auth?.deviceId === "string" && saved.auth.deviceId) {
|
|
82
|
+
return saved.auth.deviceId;
|
|
83
|
+
}
|
|
84
|
+
return randomUUID();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function clearSavedApiKey({ configDir = getDefaultConfigDir() } = {}) {
|
|
88
|
+
const saved = await readSavedConfig(configDir);
|
|
89
|
+
if (!saved) return getConfigPath(configDir);
|
|
90
|
+
const next = { ...saved };
|
|
91
|
+
delete next.apiKey;
|
|
92
|
+
await mkdir(configDir, { recursive: true });
|
|
93
|
+
const configPath = getConfigPath(configDir);
|
|
94
|
+
await writeFile(configPath, `${JSON.stringify(next, null, 2)}\n`, {
|
|
95
|
+
mode: 0o600,
|
|
96
|
+
});
|
|
97
|
+
return configPath;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function readConfig(configDir = getDefaultConfigDir()) {
|
|
101
|
+
return readSavedConfig(configDir);
|
|
102
|
+
}
|
|
103
|
+
|
|
47
104
|
async function readSavedConfig(configDir) {
|
|
48
105
|
try {
|
|
49
106
|
return JSON.parse(await readFile(getConfigPath(configDir), "utf8"));
|
package/src/help.js
CHANGED
|
@@ -2,7 +2,8 @@ export function getAiHelp() {
|
|
|
2
2
|
return `Flatkey CLI protocol for agents
|
|
3
3
|
|
|
4
4
|
Setup:
|
|
5
|
-
- Prefer
|
|
5
|
+
- Prefer browser auth: flatkey login
|
|
6
|
+
- Or env: FLATKEY_API_KEY=<key>
|
|
6
7
|
- Create a key at https://console.flatkey.ai/keys
|
|
7
8
|
- Or save key: flatkey onboard --api-key <key>
|
|
8
9
|
- Use --json for machine-readable output.
|
|
@@ -23,6 +24,9 @@ Commands:
|
|
|
23
24
|
- flatkey credits --json
|
|
24
25
|
- flatkey status --json
|
|
25
26
|
- flatkey models --json [--type image|video|audio|text]
|
|
27
|
+
- flatkey login [--no-open] [--console-url <url>]
|
|
28
|
+
- flatkey logout
|
|
29
|
+
- flatkey auth status --json
|
|
26
30
|
- flatkey help --ai
|
|
27
31
|
|
|
28
32
|
Environment:
|
|
@@ -41,6 +45,9 @@ export function getHumanHelp() {
|
|
|
41
45
|
return `Usage: flatkey <command> [options]
|
|
42
46
|
|
|
43
47
|
Commands:
|
|
48
|
+
login Authorize CLI in browser
|
|
49
|
+
logout Remove saved Flatkey API key
|
|
50
|
+
auth status Show saved auth state
|
|
44
51
|
onboard --api-key <key> Save Flatkey API key from https://console.flatkey.ai/keys
|
|
45
52
|
image generate --prompt <txt> Generate image
|
|
46
53
|
video generate --prompt <txt> Generate video
|
|
@@ -58,6 +65,7 @@ Global options:
|
|
|
58
65
|
--json Print machine-readable JSON
|
|
59
66
|
--output, -o <file> Write generated output to a local file
|
|
60
67
|
--base-url <url> Override Flatkey router URL
|
|
68
|
+
--console-url <url> Override Flatkey console URL for login
|
|
61
69
|
|
|
62
70
|
Video options:
|
|
63
71
|
--ratio <value> 16:9, 9:16, 4:3, 3:4, 21:9, or 1:1
|
|
@@ -110,6 +118,17 @@ Options:
|
|
|
110
118
|
--json Print machine-readable JSON`,
|
|
111
119
|
credits: `Usage: flatkey credits [options]
|
|
112
120
|
|
|
121
|
+
Options:
|
|
122
|
+
--json Print machine-readable JSON`,
|
|
123
|
+
auth: `Usage: flatkey auth <action> [options]
|
|
124
|
+
|
|
125
|
+
Actions:
|
|
126
|
+
status Show saved auth state
|
|
127
|
+
|
|
128
|
+
Options:
|
|
129
|
+
--json Print machine-readable JSON`,
|
|
130
|
+
"auth status": `Usage: flatkey auth status [options]
|
|
131
|
+
|
|
113
132
|
Options:
|
|
114
133
|
--json Print machine-readable JSON`,
|
|
115
134
|
help: `Usage: flatkey help [command] [options]
|
|
@@ -138,6 +157,16 @@ Options:
|
|
|
138
157
|
|
|
139
158
|
Options:
|
|
140
159
|
--type <type> Filter: image, video, audio, or text
|
|
160
|
+
--json Print machine-readable JSON`,
|
|
161
|
+
login: `Usage: flatkey login [options]
|
|
162
|
+
|
|
163
|
+
Options:
|
|
164
|
+
--no-open Print approval URL without opening a browser
|
|
165
|
+
--console-url <url> Override Flatkey console URL
|
|
166
|
+
--json Print machine-readable JSON`,
|
|
167
|
+
logout: `Usage: flatkey logout [options]
|
|
168
|
+
|
|
169
|
+
Options:
|
|
141
170
|
--json Print machine-readable JSON`,
|
|
142
171
|
onboard: `Usage: flatkey onboard --api-key <key>
|
|
143
172
|
|