@flatkey-ai/cli 0.1.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.
@@ -0,0 +1,36 @@
1
+ name: Publish npm
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ push:
6
+ tags:
7
+ - "v*"
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ publish:
14
+ name: Publish @flatkey-ai/cli
15
+ runs-on: ubuntu-latest
16
+
17
+ steps:
18
+ - name: Checkout
19
+ uses: actions/checkout@v4
20
+
21
+ - name: Setup Node
22
+ uses: actions/setup-node@v4
23
+ with:
24
+ node-version: 22
25
+ registry-url: "https://registry.npmjs.org"
26
+
27
+ - name: Install dependencies
28
+ run: npm install
29
+
30
+ - name: Test
31
+ run: npm test
32
+
33
+ - name: Publish
34
+ run: npm publish --access public
35
+ env:
36
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
package/README.md ADDED
@@ -0,0 +1,51 @@
1
+ # @flatkey-ai/cli
2
+
3
+ Unified Flatkey CLI for media workers. Generate images, videos, and audio through Flatkey credits.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g @flatkey-ai/cli
9
+ ```
10
+
11
+ Or run without installing:
12
+
13
+ ```bash
14
+ npx @flatkey-ai/cli help --ai
15
+ ```
16
+
17
+ ## Auth
18
+
19
+ ```bash
20
+ flatkey onboard --api-key <key>
21
+ ```
22
+
23
+ Or use:
24
+
25
+ ```bash
26
+ export FLATKEY_API_KEY=<key>
27
+ ```
28
+
29
+ ## Commands
30
+
31
+ ```bash
32
+ flatkey image generate --prompt "magazine cover" --json
33
+ flatkey video generate --prompt "product launch clip" --json
34
+ flatkey audio generate --prompt "30 second voiceover" --json
35
+ flatkey credits --json
36
+ flatkey status --json
37
+ flatkey models --json
38
+ flatkey help --ai
39
+ ```
40
+
41
+ Default router: `https://router.flatkey.ai`.
42
+
43
+ ## Release Channels
44
+
45
+ Primary release is npm package `@flatkey-ai/cli`.
46
+
47
+ Planned wrappers:
48
+
49
+ - Homebrew formula.
50
+ - Debian `.deb` package for apt-based install.
51
+ - Windows MSI package.
package/bin/flatkey.js ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ import { main } from "../src/cli.js";
3
+
4
+ main(process.argv.slice(2)).catch((error) => {
5
+ if (process.argv.includes("--json")) {
6
+ process.stderr.write(`${JSON.stringify({ error: { message: error.message } })}\n`);
7
+ } else {
8
+ process.stderr.write(`${error.message}\n`);
9
+ }
10
+ process.exitCode = 1;
11
+ });
@@ -0,0 +1,90 @@
1
+ # Flatkey CLI Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Build `@flatkey-ai/cli`, a Node.js CLI for Flatkey image, video, audio, credits, status, models, onboarding, and AI-first help.
6
+
7
+ **Architecture:** Use small ESM modules with dependency-injected `fetch`, filesystem paths, and streams so behavior can be tested without real Flatkey calls. The CLI parser converts argv into command objects, command handlers call provider functions, and output helpers keep human logs off stdout in JSON mode.
8
+
9
+ **Tech Stack:** Node.js ESM, npm package bin, built-in `node:test`, `assert/strict`, `fs/promises`, `child_process`, and global `fetch`.
10
+
11
+ ---
12
+
13
+ ## File Structure
14
+
15
+ - `package.json`: npm metadata, bin mapping, scripts, engines.
16
+ - `bin/flatkey.js`: executable entrypoint.
17
+ - `src/cli.js`: argv parsing, command dispatch, exit handling.
18
+ - `src/config.js`: API key/config resolution and onboarding writes.
19
+ - `src/api.js`: Flatkey HTTP requests and route selection.
20
+ - `src/artifacts.js`: save base64/data URL/url artifacts.
21
+ - `src/help.js`: human and AI help text.
22
+ - `src/models.js`: bundled fallback model list and remote normalization.
23
+ - `src/animation.js`: ASCII animation lifecycle.
24
+ - `test/*.test.js`: focused tests for each module and CLI subprocess behavior.
25
+
26
+ ## Task 1: Package and Parser
27
+
28
+ - [ ] Write failing tests in `test/cli.test.js` for `parseArgv`, including `flatkey image generate --prompt x --json`, `flatkey credits --json`, unknown command, and `flatkey help --ai`.
29
+ - [ ] Run `npm test -- test/cli.test.js`; expect module-not-found or function-not-found failure.
30
+ - [ ] Create `package.json`, `bin/flatkey.js`, and `src/cli.js` with minimal parser and dispatch stubs.
31
+ - [ ] Run `npm test -- test/cli.test.js`; expect parser tests pass.
32
+ - [ ] Commit: `feat: add cli parser`
33
+
34
+ ## Task 2: Config and Onboarding
35
+
36
+ - [ ] Write failing tests in `test/config.test.js` for API key precedence: option, `FLATKEY_API_KEY`, saved config, and missing key error.
37
+ - [ ] Write failing test for `writeConfig` creating config JSON with `apiKey`.
38
+ - [ ] Run `npm test -- test/config.test.js`; expect missing module failure.
39
+ - [ ] Create `src/config.js` and wire `onboard` command to write config.
40
+ - [ ] Run `npm test -- test/config.test.js test/cli.test.js`; expect pass.
41
+ - [ ] Commit: `feat: add flatkey config`
42
+
43
+ ## Task 3: API Requests
44
+
45
+ - [ ] Write failing tests in `test/api.test.js` for Nano image route, OpenAI image route, video route, audio route, credits route, status route, and models route using injected fetch.
46
+ - [ ] Run `npm test -- test/api.test.js`; expect missing module failure.
47
+ - [ ] Create `src/api.js` with `FlatkeyError`, `generateImage`, `generateVideo`, `generateAudio`, `getCredits`, `getStatus`, and `getModels`.
48
+ - [ ] Run `npm test -- test/api.test.js`; expect pass.
49
+ - [ ] Commit: `feat: add flatkey api client`
50
+
51
+ ## Task 4: Artifacts and Model Fallback
52
+
53
+ - [ ] Write failing tests in `test/artifacts.test.js` for saving base64, data URL, and preserving URL outputs.
54
+ - [ ] Write failing tests in `test/models.test.js` for bundled fallback shape and type filtering.
55
+ - [ ] Run targeted tests; expect missing module failures.
56
+ - [ ] Create `src/artifacts.js` and `src/models.js`.
57
+ - [ ] Run targeted tests; expect pass.
58
+ - [ ] Commit: `feat: add artifact and model helpers`
59
+
60
+ ## Task 5: Help and Animation
61
+
62
+ - [ ] Write failing tests in `test/help.test.js` checking `help --ai` includes setup, JSON contract, image/video/audio examples, credits, status, and models.
63
+ - [ ] Write failing tests in `test/animation.test.js` checking no animation when JSON mode or non-TTY.
64
+ - [ ] Run targeted tests; expect missing module failures.
65
+ - [ ] Create `src/help.js` and `src/animation.js`.
66
+ - [ ] Run targeted tests; expect pass.
67
+ - [ ] Commit: `feat: add help and animation`
68
+
69
+ ## Task 6: Command Integration
70
+
71
+ - [ ] Write failing subprocess tests in `test/integration.test.js` with a local HTTP server for `image generate --json`, `video generate --json`, `audio generate --json`, `credits --json`, `status --json`, and `models --json`.
72
+ - [ ] Run `npm test -- test/integration.test.js`; expect command behavior missing.
73
+ - [ ] Wire command handlers in `src/cli.js` to config, API, artifacts, models, help, and animation.
74
+ - [ ] Run `npm test`; expect pass.
75
+ - [ ] Commit: `feat: wire flatkey cli commands`
76
+
77
+ ## Task 7: Release Metadata
78
+
79
+ - [ ] Write failing tests or static checks for package name `@flatkey-ai/cli`, bin `flatkey`, and executable bin file.
80
+ - [ ] Add `README.md` with install, auth, commands, and release channel notes.
81
+ - [ ] Add `release/homebrew/flatkey.rb`, `release/deb/README.md`, and `release/msi/README.md` as packaging stubs that wrap npm package release.
82
+ - [ ] Run `npm test`.
83
+ - [ ] Commit: `docs: add release notes`
84
+
85
+ ## Final Verification
86
+
87
+ - [ ] Run `npm test`.
88
+ - [ ] Run `node bin/flatkey.js help --ai`.
89
+ - [ ] Run `node bin/flatkey.js models --json` without network key and confirm bundled fallback.
90
+ - [ ] Run `git status --short`.
@@ -0,0 +1,209 @@
1
+ # Flatkey CLI Design
2
+
3
+ ## Goal
4
+
5
+ Build `@flatkey-ai/cli`, a cross-platform npm CLI for media workers and AI agents to use Flatkey as the most cost-effective practical AI API entrypoint for image, video, and audio generation. The CLI should make cost, credits, model choice, upload inputs, generation jobs, and saved artifacts visible from the terminal.
6
+
7
+ ## Scope
8
+
9
+ The CLI provides:
10
+
11
+ - Image generation.
12
+ - Video generation.
13
+ - Audio generation.
14
+ - Cost estimation before generation.
15
+ - Generation job creation, listing, inspection, and waiting.
16
+ - Uploads for local image, video, and audio inputs.
17
+ - Credit balance and usage status.
18
+ - Recent credit transactions.
19
+ - AI-first help output.
20
+ - Available model listing.
21
+ - Model parameter inspection.
22
+ - Local onboarding for a Flatkey API key.
23
+ - A short ASCII animation during human-facing generation commands.
24
+
25
+ The first implementation is a Node.js npm package. It runs on Windows, Linux, and macOS through Node. Homebrew, apt/deb, and MSI packaging are release channels around the npm package, not separate native implementations.
26
+
27
+ ## Package and Binary
28
+
29
+ Package name: `@flatkey-ai/cli`.
30
+
31
+ Primary binary: `flatkey`.
32
+
33
+ The package uses ESM JavaScript and keeps runtime dependencies minimal. It should run with the current active Node LTS line and newer.
34
+
35
+ ## Command Surface
36
+
37
+ Top-level commands:
38
+
39
+ - `flatkey onboard [--api-key <key>]`
40
+ - `flatkey image generate --prompt "<prompt>" [options]`
41
+ - `flatkey video generate --prompt "<prompt>" [options]`
42
+ - `flatkey audio generate --prompt "<prompt>" [options]`
43
+ - `flatkey generate create <model> --prompt "<prompt>" [options]`
44
+ - `flatkey generate cost <model> --prompt "<prompt>" [options]`
45
+ - `flatkey generate get <job-id> [--json]`
46
+ - `flatkey generate list [--type image|video|audio] [--json]`
47
+ - `flatkey generate wait <job-id> [--json]`
48
+ - `flatkey upload create <file> [--json]`
49
+ - `flatkey upload list [--type image|video|audio] [--json]`
50
+ - `flatkey credits [--json]`
51
+ - `flatkey status [--json]`
52
+ - `flatkey transactions [--size <count>] [--json]`
53
+ - `flatkey models [--type image|video|audio] [--json]`
54
+ - `flatkey models get <model> [--json]`
55
+ - `flatkey help [--ai] [--json]`
56
+ - `flatkey version`
57
+
58
+ Shared generation options:
59
+
60
+ - `--model <model>`
61
+ - `--base-url <url>`
62
+ - `--api-key <key>`
63
+ - `--out <dir>`
64
+ - `--json`
65
+
66
+ Image-specific options:
67
+
68
+ - `--size <width>x<height>`
69
+ - `--aspect <ratio>`
70
+ - `--n <count>`
71
+ - `--quality <quality>`
72
+
73
+ Video-specific options:
74
+
75
+ - `--duration <seconds>`
76
+ - `--aspect <ratio>`
77
+ - `--fps <number>`
78
+
79
+ Audio-specific options:
80
+
81
+ - `--voice <voice>`
82
+ - `--format <mp3|wav|aac|flac>`
83
+
84
+ ## Authentication and Config
85
+
86
+ Credential resolution order:
87
+
88
+ 1. `--api-key`
89
+ 2. `FLATKEY_API_KEY`
90
+ 3. Saved config at `~/.config/flatkey/config.json`
91
+
92
+ `flatkey onboard` writes the config file with mode `0600` where supported. Windows should ignore chmod failures.
93
+
94
+ ## Flatkey API Integration
95
+
96
+ Default base URL: `https://router.flatkey.ai`.
97
+
98
+ Image generation follows the proven `awesome-images` behavior:
99
+
100
+ - OpenAI-compatible image endpoint: `POST /v1/images/generations`
101
+ - Gemini-style Nano endpoint: `POST /v1beta/models/{model}:generateContent?key={apiKey}`
102
+
103
+ The CLI should default image generation to the Nano route when `--model` is omitted or starts with `nano`, and use `/v1/images/generations` for `gpt` or `gpt-image-*`.
104
+
105
+ Video, audio, cost, jobs, uploads, credits, status, transactions, and models are implemented through small provider functions with configurable endpoint paths. The initial default paths are:
106
+
107
+ - `POST /v1/videos/generations`
108
+ - `POST /v1/audio/generations`
109
+ - `POST /v1/generations`
110
+ - `POST /v1/generations/cost`
111
+ - `GET /v1/generations`
112
+ - `GET /v1/generations/{jobId}`
113
+ - `POST /v1/uploads`
114
+ - `GET /v1/uploads`
115
+ - `GET /v1/credits`
116
+ - `GET /v1/credits/transactions`
117
+ - `GET /v1/status`
118
+ - `GET /v1/models`
119
+ - `GET /v1/models/{model}`
120
+
121
+ If Flatkey router returns a different shape, adapters normalize results before printing or saving artifacts.
122
+
123
+ ## Output and Artifacts
124
+
125
+ Default output directory: `flatkey-output`.
126
+
127
+ Generation commands save returned files when the API returns base64 or data URLs, and print remote URLs when the API returns URLs. JSON mode prints a machine-readable payload and suppresses animation/logging.
128
+
129
+ Artifact naming:
130
+
131
+ - `image-01.png`
132
+ - `video-01.mp4`
133
+ - `audio-01.mp3`
134
+
135
+ Extensions come from returned MIME types when available.
136
+
137
+ ## AI-First Help
138
+
139
+ `flatkey help --ai` prints a compact protocol-style guide:
140
+
141
+ - Required setup.
142
+ - Commands.
143
+ - Arguments.
144
+ - Environment variables.
145
+ - JSON mode contract.
146
+ - Example calls for image, video, audio, cost, upload, job wait, credits, transactions, status, and models.
147
+ - Error recovery instructions for missing key, unknown model, and API failure.
148
+
149
+ Human `--help` remains readable but secondary.
150
+
151
+ ## Models
152
+
153
+ `flatkey models` fetches remote model metadata when API key and network are available. If remote fetch fails, it falls back to bundled defaults:
154
+
155
+ - Image: `nano-banana-pro-preview`, `nano-banana-flash`, `gpt-image-2`
156
+ - Video: `veo-3`, `veo-3-fast`
157
+ - Audio: `tts-1`, `gpt-4o-mini-tts`
158
+
159
+ The fallback must mark `source: "bundled"` in JSON output.
160
+
161
+ ## ASCII Animation
162
+
163
+ Human generation mode shows a short stderr animation before and during network calls. Requirements:
164
+
165
+ - Uses plain ASCII only.
166
+ - No animation in `--json`.
167
+ - Does not corrupt stdout artifact paths.
168
+ - Works in Windows terminals.
169
+ - Automatically falls back to simple log lines when stderr is not a TTY.
170
+
171
+ ## Error Handling
172
+
173
+ Errors should be short and actionable:
174
+
175
+ - Missing key: explain `flatkey onboard` and `FLATKEY_API_KEY`.
176
+ - Unknown command: point to `flatkey help --ai`.
177
+ - HTTP failure: show API message when present, otherwise status code.
178
+ - Save failure: show output path and filesystem error.
179
+
180
+ JSON mode errors still exit non-zero and write error JSON to stderr.
181
+
182
+ ## Testing
183
+
184
+ Use Node's built-in `node:test` and `assert/strict`.
185
+
186
+ Test coverage:
187
+
188
+ - Argument parsing.
189
+ - Config read/write and env precedence.
190
+ - Request building for image OpenAI and Nano routes.
191
+ - Request building for video/audio/credits/status/models.
192
+ - Artifact persistence from base64, data URL, and URL payloads.
193
+ - JSON mode suppresses animation/logging.
194
+ - CLI subprocess tests against local HTTP servers.
195
+
196
+ ## Release Channels
197
+
198
+ Primary release is npm:
199
+
200
+ - `npm publish --access public`
201
+ - Users run `npx @flatkey-ai/cli` or install globally.
202
+
203
+ Secondary release assets:
204
+
205
+ - Homebrew formula that installs the npm package and exposes `flatkey`.
206
+ - Debian package wrapping the npm package and generated launcher.
207
+ - MSI package wrapping the npm package and generated Windows launcher.
208
+
209
+ Release automation should be added after the CLI is working and tested.
package/package.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "@flatkey-ai/cli",
3
+ "version": "0.1.0",
4
+ "description": "Flatkey media generation CLI for image, video, audio, credits, status, and models.",
5
+ "type": "module",
6
+ "bin": {
7
+ "flatkey": "bin/flatkey.js"
8
+ },
9
+ "scripts": {
10
+ "test": "node --test"
11
+ },
12
+ "engines": {
13
+ "node": ">=20"
14
+ },
15
+ "license": "MIT"
16
+ }
@@ -0,0 +1,16 @@
1
+ # Debian Package
2
+
3
+ Build `.deb` as a wrapper around the published npm package.
4
+
5
+ Expected install behavior:
6
+
7
+ ```bash
8
+ sudo apt-get install flatkey
9
+ flatkey help --ai
10
+ ```
11
+
12
+ Package contents:
13
+
14
+ - Node runtime dependency.
15
+ - Global install of `@flatkey-ai/cli`.
16
+ - `/usr/bin/flatkey` launcher.
@@ -0,0 +1,16 @@
1
+ class Flatkey < Formula
2
+ desc "Flatkey media generation CLI"
3
+ homepage "https://github.com/flatkey-ai/flatkey-cli"
4
+ license "MIT"
5
+
6
+ depends_on "node"
7
+
8
+ def install
9
+ system "npm", "install", "-g", "@flatkey-ai/cli", "--prefix", libexec
10
+ bin.install_symlink Dir["#{libexec}/bin/flatkey"].first
11
+ end
12
+
13
+ test do
14
+ system "#{bin}/flatkey", "help", "--ai"
15
+ end
16
+ end
@@ -0,0 +1,15 @@
1
+ # Windows MSI
2
+
3
+ Build MSI as a wrapper around the published npm package.
4
+
5
+ Expected install behavior:
6
+
7
+ ```powershell
8
+ flatkey help --ai
9
+ ```
10
+
11
+ Package contents:
12
+
13
+ - Node runtime prerequisite or bundled runtime.
14
+ - Install of `@flatkey-ai/cli`.
15
+ - `flatkey.cmd` on PATH.
@@ -0,0 +1,40 @@
1
+ const FRAMES = [
2
+ "[FLATKEY] . ",
3
+ "[FLATKEY] .. ",
4
+ "[FLATKEY] ... ",
5
+ "[FLATKEY] .... ",
6
+ "[FLATKEY] >>>> ",
7
+ ];
8
+
9
+ export function createAnimation({
10
+ json = false,
11
+ stream = process.stderr,
12
+ setIntervalFn = setInterval,
13
+ clearIntervalFn = clearInterval,
14
+ } = {}) {
15
+ let timer;
16
+ let active = false;
17
+ let frame = 0;
18
+
19
+ return {
20
+ start(kind = "media") {
21
+ if (json) return;
22
+ if (!stream.isTTY) {
23
+ stream.write(`flatkey ${kind} generation started\n`);
24
+ return;
25
+ }
26
+ active = true;
27
+ stream.write(`${FRAMES[0]}\r`);
28
+ timer = setIntervalFn(() => {
29
+ if (!active) return;
30
+ frame = (frame + 1) % FRAMES.length;
31
+ stream.write(`${FRAMES[frame]}\r`);
32
+ }, 120);
33
+ },
34
+ stop() {
35
+ active = false;
36
+ if (timer) clearIntervalFn(timer);
37
+ if (!json && stream.isTTY) stream.write(" \r");
38
+ },
39
+ };
40
+ }
package/src/api.js ADDED
@@ -0,0 +1,129 @@
1
+ export const DEFAULT_BASE_URL = "https://router.flatkey.ai";
2
+
3
+ export class FlatkeyError extends Error {
4
+ constructor(message, { status } = {}) {
5
+ super(message);
6
+ this.name = "FlatkeyError";
7
+ this.status = status;
8
+ }
9
+ }
10
+
11
+ export async function generateImage(options) {
12
+ const model = options.model ?? "nano-banana-pro-preview";
13
+ if (model.startsWith("gpt")) {
14
+ return postJson(options, "/v1/images/generations", cleanObject({
15
+ model,
16
+ prompt: options.prompt,
17
+ size: options.size,
18
+ n: parseOptionalInteger(options.n),
19
+ quality: options.quality,
20
+ }));
21
+ }
22
+
23
+ const path = `/v1beta/models/${encodeURIComponent(model)}:generateContent?key=${encodeURIComponent(options.apiKey)}`;
24
+ return requestJson(options, path, {
25
+ method: "POST",
26
+ headers: { "content-type": "application/json" },
27
+ body: JSON.stringify({
28
+ contents: [{ parts: [{ text: options.prompt }] }],
29
+ }),
30
+ });
31
+ }
32
+
33
+ export function generateVideo(options) {
34
+ return postJson(options, "/v1/videos/generations", cleanObject({
35
+ model: options.model ?? "veo-3",
36
+ prompt: options.prompt,
37
+ duration: parseOptionalInteger(options.duration),
38
+ aspect: options.aspect,
39
+ fps: parseOptionalInteger(options.fps),
40
+ }));
41
+ }
42
+
43
+ export function generateAudio(options) {
44
+ return postJson(options, "/v1/audio/generations", cleanObject({
45
+ model: options.model ?? "tts-1",
46
+ prompt: options.prompt,
47
+ voice: options.voice,
48
+ format: options.format,
49
+ }));
50
+ }
51
+
52
+ export function getCredits(options) {
53
+ return requestJson(options, "/v1/credits");
54
+ }
55
+
56
+ export function getStatus(options) {
57
+ return requestJson(options, "/v1/status");
58
+ }
59
+
60
+ export function getModels(options) {
61
+ return requestJson(options, "/v1/models");
62
+ }
63
+
64
+ async function postJson(options, path, payload) {
65
+ return requestJson(options, path, {
66
+ method: "POST",
67
+ headers: jsonHeaders(options.apiKey),
68
+ body: JSON.stringify(payload),
69
+ });
70
+ }
71
+
72
+ async function requestJson(options, path, init = {}) {
73
+ const fetchImpl = options.fetch ?? fetch;
74
+ const response = await fetchImpl(buildUrl(options.baseUrl, path), {
75
+ method: "GET",
76
+ ...init,
77
+ headers: {
78
+ ...authHeaders(options.apiKey),
79
+ ...init.headers,
80
+ },
81
+ });
82
+ const body = await readJson(response);
83
+ if (!response.ok) {
84
+ throw new FlatkeyError(extractErrorMessage(body, response.status), {
85
+ status: response.status,
86
+ });
87
+ }
88
+ return body;
89
+ }
90
+
91
+ function buildUrl(baseUrl = DEFAULT_BASE_URL, path) {
92
+ return `${baseUrl.replace(/\/$/, "")}${path}`;
93
+ }
94
+
95
+ function authHeaders(apiKey) {
96
+ return apiKey ? { Authorization: `Bearer ${apiKey}` } : {};
97
+ }
98
+
99
+ function jsonHeaders(apiKey) {
100
+ return {
101
+ ...authHeaders(apiKey),
102
+ "content-type": "application/json",
103
+ };
104
+ }
105
+
106
+ function parseOptionalInteger(value) {
107
+ if (value === undefined) return undefined;
108
+ return Number.parseInt(value, 10);
109
+ }
110
+
111
+ function cleanObject(value) {
112
+ return Object.fromEntries(
113
+ Object.entries(value).filter(([, entry]) => entry !== undefined),
114
+ );
115
+ }
116
+
117
+ async function readJson(response) {
118
+ try {
119
+ return await response.json();
120
+ } catch {
121
+ return {};
122
+ }
123
+ }
124
+
125
+ function extractErrorMessage(body, status) {
126
+ if (typeof body?.error?.message === "string") return body.error.message;
127
+ if (typeof body?.message === "string") return body.message;
128
+ return `Flatkey API request failed with HTTP ${status}`;
129
+ }