@todoforai/image-api 2.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.
package/README.md ADDED
@@ -0,0 +1,18 @@
1
+ # @todoforai/image-api
2
+
3
+ Generate and edit images through the TODOforAI backend (gpt-image).
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g @todoforai/image-api
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```bash
14
+ image-api generate "An orange rocket on a white background" -o rocket.png
15
+ image-api edit "Change only the background to black" --image rocket.png -o edited.png
16
+ ```
17
+
18
+ Authenticate with `todoforai login`, `TODOFORAI_API_KEY`, or `--api-key`.
package/package.json ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "@todoforai/image-api",
3
+ "version": "2.1.0",
4
+ "description": "AI image generation and editing through TODOforAI (gpt-image)",
5
+ "type": "module",
6
+ "bin": {
7
+ "image-api": "src/index.ts"
8
+ },
9
+ "dependencies": {
10
+ "commander": "^14.0.3"
11
+ }
12
+ }
package/src/api.ts ADDED
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Backend API client — calls TODOFORAI's codex/imagegen endpoint.
3
+ *
4
+ * Mount routing: a bridge-injected `dst_` device-session token is only accepted
5
+ * on the `/dst/v1` mount; long-lived API keys go through `/api/v1`.
6
+ *
7
+ * Auth precedence (matches todoforai-subagent):
8
+ * 1. --api-key flag
9
+ * 2. TODOFORAI_API_TOKEN env (bridge-injected short-lived token)
10
+ * 3. stored creds for explicit --api-url
11
+ * 4. TODOFORAI_API_KEY env
12
+ * 5. stored creds at platform config dir
13
+ * (`~/.config/todoforai/credentials.json` on Linux,
14
+ * legacy `~/.todoforai/credentials.json` for back-compat).
15
+ */
16
+
17
+ import { readFileSync } from "fs";
18
+ import { homedir } from "os";
19
+ import { join } from "path";
20
+
21
+ // Mirrors @shared/fbe CodexImageGen{Input,Output,Content}; kept inline so this
22
+ // CLI stays a zero-dep package (like todoforai-subagent).
23
+ export type CodexImageGenContent =
24
+ | { type: "input_text"; text: string }
25
+ | { type: "input_image"; imageUrl: string };
26
+
27
+ export type CodexImageSize =
28
+ | "1024x1024"
29
+ | "1536x1024"
30
+ | "1024x1536"
31
+ | "2048x2048"
32
+ | "4096x4096";
33
+
34
+ export interface CodexImageGenInput {
35
+ content: CodexImageGenContent[];
36
+ model?: string;
37
+ size: CodexImageSize;
38
+ quality?: "low" | "medium" | "high";
39
+ background?: "auto" | "transparent" | "opaque";
40
+ outputFormat?: "png" | "webp" | "jpeg";
41
+ instructions?: string;
42
+ }
43
+
44
+ export interface CodexImageGenOutput {
45
+ image: string;
46
+ format: string;
47
+ cost?: number;
48
+ elapsed?: number;
49
+ revisedPrompt?: string;
50
+ }
51
+
52
+ const DEFAULT_API_URL = "https://api.todofor.ai";
53
+
54
+ // Credentials path matches todoforai-c-core / bridge (XDG on Linux,
55
+ // Library/Application Support on macOS, %APPDATA% on Windows).
56
+ function credentialsPaths(): string[] {
57
+ const sys = process.platform;
58
+ if (sys === "win32") {
59
+ // Match todoforai-c-core: %HOME%\AppData\Roaming (not %APPDATA%).
60
+ return [join(homedir(), "AppData", "Roaming", "todoforai", "credentials.json")];
61
+ }
62
+ if (sys === "darwin") {
63
+ return [join(homedir(), "Library", "Application Support", "todoforai", "credentials.json")];
64
+ }
65
+ const xdg = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
66
+ return [join(xdg, "todoforai", "credentials.json")];
67
+ }
68
+
69
+ export interface ApiOptions {
70
+ apiUrl?: string;
71
+ apiKey?: string;
72
+ }
73
+
74
+ function getEnv(name: string): string {
75
+ return process.env[`TODOFORAI_${name}`] || process.env[`TODO4AI_${name}`] || "";
76
+ }
77
+
78
+ function normalizeApiUrl(url: string): string {
79
+ if (url.startsWith("localhost")) return `http://${url}`;
80
+ if (!url.startsWith("http://") && !url.startsWith("https://")) return `https://${url}`;
81
+ return url.replace(/\/+$/, "");
82
+ }
83
+
84
+ function readStoredApiKey(apiUrl: string): string {
85
+ for (const p of credentialsPaths()) {
86
+ try {
87
+ const creds = JSON.parse(readFileSync(p, "utf8")) as Record<string, string>;
88
+ if (creds[apiUrl]) return creds[apiUrl];
89
+ } catch {}
90
+ }
91
+ return "";
92
+ }
93
+
94
+ function resolveAuth(opts: ApiOptions): { apiUrl: string; apiKey: string } {
95
+ const explicitUrl = opts.apiUrl || getEnv("API_URL");
96
+ const apiUrl = normalizeApiUrl(explicitUrl || DEFAULT_API_URL);
97
+ // Bridge-injected short-lived token (TODOFORAI_API_TOKEN) wins over
98
+ // long-lived API keys / stored creds, but loses to an explicit --api-key.
99
+ const apiKey =
100
+ opts.apiKey ||
101
+ getEnv("API_TOKEN") ||
102
+ (opts.apiUrl ? readStoredApiKey(apiUrl) : "") ||
103
+ getEnv("API_KEY") ||
104
+ process.env.API_KEY ||
105
+ readStoredApiKey(apiUrl);
106
+ if (!apiKey) {
107
+ console.error(
108
+ "Error: no API key. Set TODOFORAI_API_KEY, pass --api-key, or run `todoforai login` first."
109
+ );
110
+ process.exit(2);
111
+ }
112
+ return { apiUrl, apiKey };
113
+ }
114
+
115
+ export async function imageGen(
116
+ input: CodexImageGenInput,
117
+ opts: ApiOptions = {}
118
+ ): Promise<CodexImageGenOutput> {
119
+ const { apiUrl, apiKey } = resolveAuth(opts);
120
+ // `dst_` device-session tokens are only valid on the /dst/v1 mount; long-lived
121
+ // API keys go through /api/v1.
122
+ const mount = apiKey.startsWith("dst_") ? "dst" : "api";
123
+ const res = await fetch(`${apiUrl}/${mount}/v1/codex/imagegen`, {
124
+ method: "POST",
125
+ headers: { "content-type": "application/json", "x-api-key": apiKey },
126
+ body: JSON.stringify(input),
127
+ });
128
+ if (!res.ok) {
129
+ const text = await res.text().catch(() => "");
130
+ let msg = text.slice(0, 500);
131
+ try {
132
+ const j = JSON.parse(text);
133
+ msg = j?.error?.message || j?.message || msg;
134
+ } catch { /* keep raw */ }
135
+ console.error(`HTTP ${res.status}: ${msg}`);
136
+ process.exit(1);
137
+ }
138
+ return (await res.json()) as CodexImageGenOutput;
139
+ }
@@ -0,0 +1,37 @@
1
+ import { Command } from "commander";
2
+ import { runImageGen, imageToDataUrl, ContentItem } from "../imagegen";
3
+
4
+ export const editCommand = new Command("edit")
5
+ .description(
6
+ "Edit one or more images via TODOFORAI's image generation endpoint.\n\n" +
7
+ "Pass --image multiple times to provide reference / edit-target images.\n" +
8
+ "Tip: state invariants in the prompt (e.g. 'change only X; keep Y unchanged')."
9
+ )
10
+ .argument("<prompt>", "Edit instruction")
11
+ .requiredOption(
12
+ "-i, --image <path>",
13
+ "Input image; repeat for multiple images",
14
+ (v: string, prev: string[] = []) => [...prev, v],
15
+ [],
16
+ )
17
+ .option("-o, --output <file>", "Output PNG file", "edited.png")
18
+ .option("--model <model>", "Model (default: gpt-image-2)")
19
+ .option("--size <size>", "1024x1024 | 1536x1024 | 1024x1536 | 2048x2048 | 4096x4096", "1024x1024")
20
+ .option("--quality <quality>", "low | medium | high")
21
+ .option("--background <bg>", "auto | transparent | opaque")
22
+ .option("--output_format <fmt>", "png | webp | jpeg", "png")
23
+ .option("--instructions <text>", "System instructions")
24
+ .option("--api-url <url>", "TODOFORAI API URL")
25
+ .option("--api-key <key>", "TODOFORAI API key")
26
+ .action(async (prompt: string, opts) => {
27
+ const imagePaths: string[] = opts.image ?? [];
28
+ if (imagePaths.length === 0) {
29
+ console.error("Error: at least one --image is required.");
30
+ process.exit(1);
31
+ }
32
+ const content: ContentItem[] = [{ type: "input_text", text: prompt }];
33
+ for (const p of imagePaths) {
34
+ content.push({ type: "input_image", imageUrl: await imageToDataUrl(p) });
35
+ }
36
+ await runImageGen(content, opts);
37
+ });
@@ -0,0 +1,21 @@
1
+ import { Command } from "commander";
2
+ import { runImageGen } from "../imagegen";
3
+
4
+ export const generateCommand = new Command("generate")
5
+ .description(
6
+ "Generate an image via TODOFORAI's image generation endpoint.\n\n" +
7
+ "Auth: TODOFORAI_API_TOKEN (bridge-injected) or TODOFORAI_API_KEY env (or --api-key)."
8
+ )
9
+ .argument("<prompt>", "Image prompt")
10
+ .option("-o, --output <file>", "Output PNG file", "out.png")
11
+ .option("--model <model>", "Model (default: gpt-image-2)")
12
+ .option("--size <size>", "1024x1024 | 1536x1024 | 1024x1536 | 2048x2048 | 4096x4096", "1024x1024")
13
+ .option("--quality <quality>", "low | medium | high")
14
+ .option("--background <bg>", "auto | transparent | opaque")
15
+ .option("--output_format <fmt>", "png | webp | jpeg", "png")
16
+ .option("--instructions <text>", "System instructions")
17
+ .option("--api-url <url>", "TODOFORAI API URL (default: $TODOFORAI_API_URL or https://api.todofor.ai)")
18
+ .option("--api-key <key>", "TODOFORAI API key (default: $TODOFORAI_API_TOKEN or $TODOFORAI_API_KEY)")
19
+ .action(async (prompt: string, opts) => {
20
+ await runImageGen([{ type: "input_text", text: prompt }], opts);
21
+ });
@@ -0,0 +1,50 @@
1
+ import { writeFile, readFile } from "fs/promises";
2
+ import { extname } from "path";
3
+ import { imageGen, ApiOptions, CodexImageGenContent, CodexImageGenInput, CodexImageSize } from "./api";
4
+
5
+ const MIME_BY_EXT: Record<string, string> = {
6
+ ".png": "image/png",
7
+ ".jpg": "image/jpeg",
8
+ ".jpeg": "image/jpeg",
9
+ ".webp": "image/webp",
10
+ ".gif": "image/gif",
11
+ };
12
+
13
+ export async function imageToDataUrl(path: string): Promise<string> {
14
+ const buf = await readFile(path);
15
+ const mime = MIME_BY_EXT[extname(path).toLowerCase()] ?? "image/png";
16
+ return `data:${mime};base64,${buf.toString("base64")}`;
17
+ }
18
+
19
+ export type ContentItem = CodexImageGenContent;
20
+
21
+ export interface ImageGenOptions extends ApiOptions {
22
+ model?: string;
23
+ size?: CodexImageSize;
24
+ quality?: "low" | "medium" | "high";
25
+ background?: "auto" | "transparent" | "opaque";
26
+ output_format?: "png" | "webp" | "jpeg";
27
+ instructions?: string;
28
+ output: string;
29
+ }
30
+
31
+ export async function runImageGen(content: ContentItem[], opts: ImageGenOptions): Promise<void> {
32
+ const input: CodexImageGenInput = {
33
+ content,
34
+ size: opts.size ?? "1024x1024",
35
+ ...(opts.model && { model: opts.model }),
36
+ ...(opts.quality && { quality: opts.quality }),
37
+ ...(opts.background && { background: opts.background }),
38
+ ...(opts.output_format && { outputFormat: opts.output_format }),
39
+ ...(opts.instructions && { instructions: opts.instructions }),
40
+ };
41
+
42
+ const result = await imageGen(input, { apiUrl: opts.apiUrl, apiKey: opts.apiKey });
43
+
44
+ const buf = Buffer.from(result.image, "base64");
45
+ await writeFile(opts.output, buf);
46
+ console.error(`Saved ${opts.output} (${buf.length} bytes)`);
47
+
48
+ if (result.revisedPrompt) console.error(`Revised prompt: ${result.revisedPrompt}`);
49
+ if (result.cost !== undefined) console.error(`Cost: $${result.cost.toFixed(3)}`);
50
+ }
package/src/index.ts ADDED
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env bun
2
+ import { program } from "commander";
3
+ import { generateCommand } from "./commands/generate";
4
+ import { editCommand } from "./commands/edit";
5
+
6
+ program
7
+ .name("image-api")
8
+ .description(
9
+ "Image generation & editing CLI via TODOFORAI backend (gpt-image).\n\n" +
10
+ "Auth: TODOFORAI_API_TOKEN (bridge-injected) or TODOFORAI_API_KEY env (or --api-key).\n" +
11
+ "Run `todoforai login` to store credentials."
12
+ )
13
+ .version("2.1.0");
14
+
15
+ program.addCommand(generateCommand);
16
+ program.addCommand(editCommand);
17
+
18
+ program.parse();