ima2-gen 1.0.2 → 1.0.4

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 CHANGED
@@ -1,82 +1,253 @@
1
1
  # ima2-gen
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/ima2-gen)](https://www.npmjs.com/package/ima2-gen)
4
+ [![Node.js](https://img.shields.io/badge/node-%3E%3D18-brightgreen)](https://nodejs.org/)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
4
6
 
5
- Minimal CLI + web UI for OpenAI `gpt-image-2` image generation.
7
+ > **Read in other languages**: [한국어](docs/README.ko.md) · [日本語](docs/README.ja.md) · [简体中文](docs/README.zh-CN.md)
8
+
9
+ Minimal CLI + web UI for OpenAI **GPT Image 2** (`gpt-image-2`) image generation. OAuth (free via ChatGPT Plus/Pro) or API key. Parallel generation, multi-image references, CLI automation, persistent history.
6
10
 
7
11
  ![ima2-gen screenshot](assets/screenshot.png)
8
12
 
9
- ## Install & Run
13
+ ---
14
+
15
+ ## Quick Start
10
16
 
11
17
  ```bash
18
+ # Run instantly with npx (no install)
12
19
  npx ima2-gen serve
13
- ```
14
-
15
- Or install globally:
16
20
 
17
- ```bash
21
+ # Or install globally
18
22
  npm install -g ima2-gen
19
23
  ima2 serve
20
24
  ```
21
25
 
22
- First run prompts you to choose:
26
+ First run prompts you to pick an auth method:
23
27
 
24
28
  ```
25
- 1) API Key — paste your OpenAI API key (paid)
26
- 2) OAuth login with ChatGPT account (free)
29
+ Choose authentication method:
30
+ 1) API Key paste your OpenAI API key (paid)
31
+ 2) OAuth — login with ChatGPT account (free)
27
32
  ```
28
33
 
29
- Then opens `http://localhost:3333`.
34
+ Web UI opens at `http://localhost:3333`.
35
+
36
+ ---
37
+
38
+ ## Features
39
+
40
+ Everything in the screenshot above, shipping today:
41
+
42
+ ### Authentication
43
+ - **OAuth** — log in with your ChatGPT Plus/Pro account, $0 per image
44
+ - **API Key** — paste your `sk-...` key, pay per call
45
+
46
+ Both indicators shown live in the left panel (green dot = ready, red dot = disabled). API key path is hard-disabled by default; OAuth is the primary route.
47
+
48
+ ### Generation controls
49
+ | Control | Options |
50
+ |---------|---------|
51
+ | **Quality** | Low (fast) · Medium (balanced) · High (best) |
52
+ | **Size** | `1024²` `1536×1024` `1024×1536` `1360×1024` `1024×1360` `1824×1024` `1024×1824` `2048²` `2048×1152` `1152×2048` `3824×2160` `2160×3824` · `auto` · custom |
53
+ | **Format** | PNG · JPEG · WebP |
54
+ | **Moderation** | Low (less restrictive) · Auto (standard) |
55
+ | **Count** | 1 · 2 · 4 parallel |
56
+
57
+ All sizes respect gpt-image-2 constraints: every side is a multiple of 16, long:short ratio ≤ 3:1, 655,360–8,294,400 total pixels.
30
58
 
31
- ## CLI
59
+ ### Workflow
60
+ - **Multi-reference** — attach up to 5 reference images, drag & drop anywhere on the left panel
61
+ - **Prompt-with-context** — mixes text + reference images in one request
62
+ - **Use current** — one-click re-use of the selected image as a new reference
63
+ - **Download** · **Copy to clipboard** · **Copy prompt** directly from the canvas
64
+ - **Sticky gallery strip** at the bottom, fixed-position so it never scrolls away
65
+ - **Gallery modal (+)** — grid view of everything in history
66
+ - **Session persistence** — refresh mid-generation and your pending jobs reconcile automatically
32
67
 
68
+ ### CLI (headless automation)
33
69
  ```bash
34
- ima2 serve # start server (auto-setup on first run)
35
- ima2 setup # reconfigure auth
36
- ima2 reset # clear saved config
70
+ ima2 gen "a shiba in space" -q high -o shiba.png
71
+ ima2 gen "merge these" --ref a.png --ref b.png -n 4 -d out/
72
+ ima2 ls -n 10
73
+ ima2 ps
74
+ ima2 ping
37
75
  ```
38
76
 
39
- ## Features
77
+ Full command matrix below ↓
78
+
79
+ ---
80
+
81
+ ## CLI Commands
82
+
83
+ ### Server commands
84
+ | Command | Alias | Description |
85
+ |---------|-------|-------------|
86
+ | `ima2 serve` | — | Start the web server (auto-setup on first run) |
87
+ | `ima2 setup` | `login` | Reconfigure authentication method |
88
+ | `ima2 status` | — | Show current config & auth status |
89
+ | `ima2 doctor` | — | Diagnose environment & dependencies |
90
+ | `ima2 open` | — | Open web UI in browser |
91
+ | `ima2 reset` | — | Clear saved configuration |
92
+ | `ima2 --version` | `-v` | Show version |
93
+ | `ima2 --help` | `-h` | Show help |
94
+
95
+ ### Client commands (require a running `ima2 serve`)
96
+ | Command | Description |
97
+ |---------|-------------|
98
+ | `ima2 gen <prompt>` | Generate image(s) from the CLI |
99
+ | `ima2 edit <file>` | Edit an existing image (requires `--prompt`) |
100
+ | `ima2 ls` | List recent history (table or `--json`) |
101
+ | `ima2 show <name>` | Reveal one history item (`--reveal`) |
102
+ | `ima2 ps` | List active jobs (`--kind`, `--session`) |
103
+ | `ima2 ping` | Health-check the running server |
104
+
105
+ The running server advertises itself at `~/.ima2/server.json`. Client commands auto-discover it; override with `--server <url>` or `IMA2_SERVER=...`.
106
+
107
+ ### Exit codes
108
+ `0` ok · `2` bad args · `3` server unreachable · `4` APIKEY_DISABLED · `5` 4xx · `6` 5xx · `7` safety refusal · `8` timeout.
109
+
110
+ ---
111
+
112
+ ## Roadmap
113
+
114
+ Public roadmap — subject to change. Version numbers reflect the actual ship cycle, not time estimates.
115
+
116
+ ### ✅ Shipped
117
+ - **0.06** Session DB — SQLite-backed history with sidecar JSON
118
+ - **0.07** Multi-reference — up to 5 attachments, i2i merged into unified flow
119
+ - **0.08** Inflight tracking — refresh-safe pending state, phase tracking
120
+ - **0.09** Node mode (dev-only) — graph-based canvas for branching generations
121
+ - **0.09.1** CLI integration — `gen / edit / ls / show / ps / ping` + `/api/health` + port advertisement
122
+
123
+ ### 🚧 0.10 — Compare & Reuse (current cycle)
124
+ - **F3 Prompt presets** — save/apply `{prompt, refs, quality, size}` bundles
125
+ - **F3 Gallery groupBy** — `preset / date / compareRun` grouping
126
+ - **F2 Batch A/B compare** — spawn 2–6 parallel variants from one prompt, keyboard-driven judging (`1-6`, `Space`=winner, `V`=variation, `P`=save preset)
127
+ - **F4 Export bundle** — zip selected images with `manifest.json` + per-image prompt `.txt`
128
+ - Every server verb ships with its CLI mirror (`ima2 preset / compare / export`)
129
+
130
+ ### 🔭 0.11 — Card-news mode
131
+ - Instagram carousel generation (4 / 6 / 10 cards)
132
+ - Style consistency via `file_id` fan-out (not `previous_response_id`, not seed)
133
+ - Parallel card regeneration without breaking the style chain
40
134
 
41
- - **Dual provider**OAuth (free, ChatGPT account) or API Key (paid)
42
- - **Text-to-Image** generate images from text prompts
43
- - **Image-to-Image** edit/inpaint with drag-and-drop
44
- - **Quality** — low / medium / high
45
- - **Size** — presets (1024 ~ 4K) + custom (any 16px-aligned ratio)
46
- - **Format** PNG / JPEG / WebP
47
- - **Moderation** auto (standard) / low (less restrictive)
48
- - **Prompt display** shown under image, click to copy
49
- - **History** persisted across page refreshes (localStorage)
50
- - **Download / Copy** — save or clipboard
135
+ ### 🔭 0.12Style kit
136
+ - Codified house-style presets with style-reference uploads
137
+ - Optional `input_fidelity: "high"` for identity-critical edits
138
+
139
+ ### 🗂 Backlog
140
+ - Web UI dark/light toggle
141
+ - Keyboard shortcuts cheat-sheet overlay
142
+ - Collaborative sessions (shared SQLite over WebSocket)
143
+ - Plugin system for custom post-processing
144
+
145
+ ---
51
146
 
52
147
  ## Architecture
53
148
 
54
149
  ```
55
150
  ima2 serve
56
- ├── Express (:3333) ← web UI + API
57
- └── openai-oauth (:10531) ← embedded OAuth proxy
151
+ ├── Express server (:3333)
152
+ │ ├── GET /api/health — version, uptime, activeJobs, pid
153
+ │ ├── GET /api/providers — available auth methods
154
+ │ ├── GET /api/oauth/status — OAuth proxy health check
155
+ │ ├── POST /api/generate — text+ref → image (parallel via n)
156
+ │ ├── POST /api/edit — ref-heavy edit path
157
+ │ ├── GET /api/history — paginated sidecar listing
158
+ │ ├── GET /api/inflight — in-progress jobs (kind/session filters)
159
+ │ ├── GET /api/sessions/* — node-graph sessions (dev-only)
160
+ │ ├── GET /api/billing — API credit / cost info
161
+ │ └── Static files (public/) — web UI
162
+
163
+ ├── openai-oauth proxy (:10531) — embedded OAuth relay
164
+ └── ~/.ima2/server.json — port advertisement for CLI auto-discovery
58
165
  ```
59
166
 
60
- ## Config
167
+ **Node mode** is dev-only (`npm run dev`) and gated from npm publishes until the session DB + multi-user story lands.
168
+
169
+ ---
170
+
171
+ ## Configuration
61
172
 
62
- Stored in `.ima2/config.json` (auto-created, gitignored).
173
+ Config lives in `.ima2/config.json` (auto-created, gitignored).
63
174
 
64
- Optional `.env`:
175
+ ### Environment Variables
176
+ | Variable | Default | Description |
177
+ |----------|---------|-------------|
178
+ | `OPENAI_API_KEY` | — | OpenAI API key (skips OAuth) |
179
+ | `PORT` | `3333` | Web server port |
180
+ | `OAUTH_PORT` | `10531` | OAuth proxy port |
181
+ | `IMA2_SERVER` | — | Client: override target server URL |
182
+
183
+ ```bash
184
+ cp .env.example .env
65
185
  ```
66
- OPENAI_API_KEY=sk-proj-...
67
- PORT=3333
68
- OAUTH_PORT=10531
186
+
187
+ ---
188
+
189
+ ## API Pricing (API Key mode only)
190
+
191
+ | Quality | 1024×1024 | 1024×1536 | 1536×1024 | 2048×2048 | 3840×2160 |
192
+ |---------|-----------|-----------|-----------|-----------|-----------|
193
+ | Low | $0.006 | $0.005 | $0.005 | $0.012 | $0.023 |
194
+ | Medium | $0.053 | $0.041 | $0.041 | $0.106 | $0.200 |
195
+ | High | $0.211 | $0.165 | $0.165 | $0.422 | $0.800 |
196
+
197
+ **OAuth mode is free** — billed against your existing ChatGPT Plus/Pro plan.
198
+
199
+ ---
200
+
201
+ ## Development
202
+
203
+ ```bash
204
+ git clone https://github.com/lidge-jun/ima2-gen.git
205
+ cd ima2-gen
206
+ npm install
207
+ npm run dev # server with --watch + Node mode enabled
208
+ npm test # 51+ tests (health, CLI lib, commands, server)
69
209
  ```
70
210
 
71
- ## Pricing (API Key mode)
211
+ Frontend stack:
212
+ - Vanilla HTML/CSS/JS (no framework in the published build)
213
+ - Vite + React for the Node-mode canvas (dev-only, gated)
214
+ - Fonts: Outfit + Geist Mono
215
+
216
+ ## Tech Stack
217
+ - **Runtime**: Node.js ≥18
218
+ - **Server**: Express 5, SQLite (better-sqlite3)
219
+ - **API**: OpenAI SDK v5
220
+ - **OAuth**: `openai-oauth` proxy
221
+ - **Tests**: Node built-in test runner
222
+
223
+ ---
224
+
225
+ ## Troubleshooting
226
+
227
+ **Port already in use / "why is it on 3457?"**
228
+ → The default is `3333`. If `PORT` is set in your shell (e.g. inherited from another server like `cli-jaw`), ima2 uses that instead. Unset it or run `PORT=3333 ima2 serve`.
229
+
230
+ **`ima2 ping` says server unreachable**
231
+ → Is `ima2 serve` running? Check `~/.ima2/server.json`. Override with `ima2 ping --server http://localhost:3333`.
72
232
 
73
- | Quality | 1024x1024 | 1024x1536 | 1536x1024 |
74
- |---------|-----------|-----------|-----------|
75
- | Low | $0.006 | $0.005 | $0.005 |
76
- | Medium | $0.053 | $0.041 | $0.041 |
77
- | High | $0.211 | $0.165 | $0.165 |
233
+ **OAuth login not working**
234
+ → Run `npx @openai/codex login` manually, then `ima2 serve`.
78
235
 
79
- OAuth mode is free (uses your ChatGPT Plus/Pro subscription).
236
+ **`ima2 doctor` fails on node_modules**
237
+ → `npm install`.
238
+
239
+ **Images not generating**
240
+ → `ima2 status` to verify config. API key must start with `sk-`.
241
+
242
+ ---
243
+
244
+ ## Release
245
+
246
+ ```bash
247
+ npm run release:patch # 1.0.2 → 1.0.3
248
+ npm run release:minor # 1.0.x → 1.1.0
249
+ npm run release:major # 1.x.x → 2.0.0
250
+ ```
80
251
 
81
252
  ## License
82
253
 
Binary file
@@ -0,0 +1,70 @@
1
+ import { parseArgs } from "../lib/args.js";
2
+ import { resolveServer, request } from "../lib/client.js";
3
+ import { fileToDataUri, dataUriToFile, defaultOutName } from "../lib/files.js";
4
+ import { out, die, color, json, exitCodeForError } from "../lib/output.js";
5
+
6
+ const SPEC = {
7
+ flags: {
8
+ prompt: { short: "p", type: "string" },
9
+ quality: { short: "q", type: "string", default: "low" },
10
+ size: { short: "s", type: "string", default: "1024x1024" },
11
+ out: { short: "o", type: "string" },
12
+ json: { type: "boolean" },
13
+ timeout: { type: "string", default: "180" },
14
+ server: { type: "string" },
15
+ help: { short: "h", type: "boolean" },
16
+ },
17
+ };
18
+
19
+ const HELP = `
20
+ ima2 edit <file> --prompt "<text>" [options]
21
+
22
+ Edit an existing image (inpainting-style).
23
+
24
+ Options:
25
+ -p, --prompt <text> Edit instruction (required)
26
+ -q, --quality <low|medium|high|auto>
27
+ -s, --size <WxH>
28
+ -o, --out <file>
29
+ --json
30
+ `;
31
+
32
+ export default async function editCmd(argv) {
33
+ const args = parseArgs(argv, SPEC);
34
+ if (args.help) { out(HELP); return; }
35
+ const input = args.positional[0];
36
+ if (!input) die(2, "input image path required");
37
+ if (!args.prompt) die(2, "--prompt is required");
38
+
39
+ let server;
40
+ try { server = await resolveServer({ serverFlag: args.server }); }
41
+ catch (e) { die(exitCodeForError(e), e.message); }
42
+
43
+ const imageDataUri = await fileToDataUri(input);
44
+ const imageB64 = imageDataUri.split(",")[1];
45
+
46
+ const timeoutMs = (parseInt(args.timeout) || 180) * 1000;
47
+ let resp;
48
+ try {
49
+ resp = await request(server.base, "/api/edit", {
50
+ method: "POST",
51
+ body: { prompt: args.prompt, image: imageB64, quality: args.quality, size: args.size },
52
+ timeoutMs,
53
+ });
54
+ } catch (e) {
55
+ if (args.json) json({ ok: false, error: e.message, code: e.code });
56
+ die(exitCodeForError(e), e.message);
57
+ }
58
+
59
+ const image = resp.image;
60
+ if (!image) die(1, "server returned no image");
61
+ const target = args.out || defaultOutName(0, 1);
62
+ await dataUriToFile(image, target);
63
+
64
+ if (args.json) {
65
+ json({ ok: true, path: target, requestId: resp.requestId, elapsed: resp.elapsed });
66
+ } else {
67
+ out(color.green("✓ ") + target);
68
+ if (resp.elapsed) out(color.dim(`elapsed ${resp.elapsed}s`));
69
+ }
70
+ }
@@ -0,0 +1,136 @@
1
+ import { parseArgs } from "../lib/args.js";
2
+ import { resolveServer, request, normalizeGenerate } from "../lib/client.js";
3
+ import { fileToDataUri, dataUriToFile, defaultOutName, readStdin } from "../lib/files.js";
4
+ import { out, err, die, color, json, exitCodeForError } from "../lib/output.js";
5
+
6
+ const SPEC = {
7
+ flags: {
8
+ quality: { short: "q", type: "string", default: "low" },
9
+ size: { short: "s", type: "string", default: "1024x1024" },
10
+ count: { short: "n", type: "string", default: "1" },
11
+ ref: { type: "string", repeatable: true },
12
+ out: { short: "o", type: "string" },
13
+ "out-dir": { short: "d", type: "string" },
14
+ json: { type: "boolean" },
15
+ "no-save": { type: "boolean" },
16
+ force: { type: "boolean" },
17
+ stdin: { type: "boolean" },
18
+ timeout: { type: "string", default: "180" },
19
+ server: { type: "string" },
20
+ help: { short: "h", type: "boolean" },
21
+ },
22
+ };
23
+
24
+ const HELP = `
25
+ ima2 gen <prompt...> [options]
26
+
27
+ Generate image(s) via the running ima2 server.
28
+
29
+ Options:
30
+ -q, --quality <low|medium|high|auto> Default: low
31
+ -s, --size <WxH | auto> Default: 1024x1024
32
+ -n, --count <1..8> Default: 1
33
+ --ref <file> Attach reference image (repeatable, max 5)
34
+ -o, --out <file> Single-image output path (implies -n 1)
35
+ -d, --out-dir <dir> Output dir for multiple images
36
+ --json Print JSON result to stdout
37
+ --no-save Skip save; print b64 to stdout (use --force for TTY)
38
+ --stdin Read prompt from stdin
39
+ --timeout <sec> Default: 180
40
+ --server <url> Override server URL
41
+
42
+ Examples:
43
+ ima2 gen "a shiba in space"
44
+ ima2 gen "merge" --ref a.png --ref b.png -q high -o out.png
45
+ cat prompt.txt | ima2 gen --stdin -n 2 -d ./out
46
+ `;
47
+
48
+ export default async function genCmd(argv) {
49
+ const args = parseArgs(argv, SPEC);
50
+ if (args.help) { out(HELP); return; }
51
+
52
+ let prompt = args.positional.join(" ");
53
+ if (args.stdin) {
54
+ const piped = await readStdin();
55
+ if (piped) prompt = prompt ? `${prompt} ${piped}` : piped;
56
+ }
57
+ if (!prompt) die(2, "prompt is required (positional or via --stdin)");
58
+
59
+ const refs = args.ref || [];
60
+ if (refs.length > 5) die(2, "max 5 --ref attachments");
61
+
62
+ const n = Math.max(1, Math.min(8, parseInt(args.count) || 1));
63
+ const timeoutMs = (parseInt(args.timeout) || 180) * 1000;
64
+
65
+ let server;
66
+ try {
67
+ server = await resolveServer({ serverFlag: args.server });
68
+ } catch (e) {
69
+ die(exitCodeForError(e), e.message);
70
+ }
71
+
72
+ const references = await Promise.all(refs.map((p) => fileToDataUri(p)));
73
+
74
+ const body = {
75
+ prompt,
76
+ quality: args.quality,
77
+ size: args.size,
78
+ n,
79
+ references,
80
+ };
81
+
82
+ let resp;
83
+ try {
84
+ resp = await request(server.base, "/api/generate", { method: "POST", body, timeoutMs });
85
+ } catch (e) {
86
+ if (args.json) json({ ok: false, error: e.message, code: e.code, status: e.status });
87
+ die(exitCodeForError(e), e.message);
88
+ }
89
+
90
+ const norm = normalizeGenerate(resp);
91
+ if (norm.images.length === 0) die(1, "server returned no images");
92
+
93
+ // --no-save path
94
+ if (args["no-save"]) {
95
+ const totalBytes = norm.images.reduce((s, im) => s + im.image.length, 0);
96
+ if (process.stdout.isTTY && totalBytes > 2 * 1024 * 1024 && !args.force) {
97
+ die(2, "refusing to print >2MB of b64 to TTY; use --force or drop --no-save");
98
+ }
99
+ for (const im of norm.images) out(im.image);
100
+ return;
101
+ }
102
+
103
+ // Save path
104
+ const outDir = args["out-dir"] || null;
105
+ const explicitOut = args.out || null;
106
+ if (explicitOut && norm.images.length > 1) {
107
+ die(2, "--out only supports a single image; use --out-dir for n>1");
108
+ }
109
+
110
+ const savedPaths = [];
111
+ for (let i = 0; i < norm.images.length; i++) {
112
+ const im = norm.images[i];
113
+ let target;
114
+ if (explicitOut) {
115
+ target = explicitOut;
116
+ } else if (outDir) {
117
+ target = `${outDir}/${defaultOutName(i, norm.images.length)}`;
118
+ } else {
119
+ target = defaultOutName(i, norm.images.length);
120
+ }
121
+ await dataUriToFile(im.image, target);
122
+ savedPaths.push(target);
123
+ }
124
+
125
+ if (args.json) {
126
+ json({
127
+ ok: true,
128
+ requestId: norm.requestId,
129
+ elapsed: norm.elapsed,
130
+ images: savedPaths.map((p, i) => ({ path: p, filename: norm.images[i].filename })),
131
+ });
132
+ } else {
133
+ for (const p of savedPaths) out(color.green("✓ ") + p);
134
+ if (norm.elapsed) out(color.dim(`elapsed ${norm.elapsed}s`));
135
+ }
136
+ }
@@ -0,0 +1,49 @@
1
+ import { parseArgs } from "../lib/args.js";
2
+ import { resolveServer, request } from "../lib/client.js";
3
+ import { out, die, color, json, table, exitCodeForError } from "../lib/output.js";
4
+
5
+ const SPEC = {
6
+ flags: {
7
+ count: { short: "n", type: "string", default: "20" },
8
+ json: { type: "boolean" },
9
+ server: { type: "string" },
10
+ help: { short: "h", type: "boolean" },
11
+ },
12
+ };
13
+
14
+ export default async function lsCmd(argv) {
15
+ const args = parseArgs(argv, SPEC);
16
+ if (args.help) { out("ima2 ls [-n count] [--json]"); return; }
17
+
18
+ let server;
19
+ try { server = await resolveServer({ serverFlag: args.server }); }
20
+ catch (e) { die(exitCodeForError(e), e.message); }
21
+
22
+ const limit = parseInt(args.count) || 20;
23
+ let resp;
24
+ try { resp = await request(server.base, "/api/history"); }
25
+ catch (e) { die(exitCodeForError(e), e.message); }
26
+
27
+ const items = (resp.items || resp.history || []).slice(0, limit);
28
+
29
+ if (args.json) { json({ items }); return; }
30
+
31
+ if (items.length === 0) {
32
+ out(color.dim("(no history)"));
33
+ return;
34
+ }
35
+ table(items, [
36
+ { key: "filename", label: "FILENAME" },
37
+ { key: "quality", label: "Q" },
38
+ { key: "size", label: "SIZE" },
39
+ { key: "createdAt", label: "WHEN", format: (v) => {
40
+ if (!v) return "";
41
+ const d = new Date(v);
42
+ return d.toISOString().replace("T", " ").slice(0, 19);
43
+ } },
44
+ { key: "prompt", label: "PROMPT", format: (v) => {
45
+ const s = String(v || "").replace(/\s+/g, " ");
46
+ return s.length > 48 ? s.slice(0, 45) + "…" : s;
47
+ } },
48
+ ]);
49
+ }
@@ -0,0 +1,28 @@
1
+ import { parseArgs } from "../lib/args.js";
2
+ import { resolveServer } from "../lib/client.js";
3
+ import { out, die, color, json, exitCodeForError } from "../lib/output.js";
4
+
5
+ const SPEC = {
6
+ flags: {
7
+ json: { type: "boolean" },
8
+ server: { type: "string" },
9
+ help: { short: "h", type: "boolean" },
10
+ },
11
+ };
12
+
13
+ export default async function pingCmd(argv) {
14
+ const args = parseArgs(argv, SPEC);
15
+ if (args.help) { out("ima2 ping [--json]"); return; }
16
+
17
+ try {
18
+ const { base, health } = await resolveServer({ serverFlag: args.server });
19
+ if (args.json) {
20
+ json({ ok: true, base, ...health });
21
+ } else {
22
+ out(color.green("✓ ") + `${base} v${health.version} uptime ${health.uptimeSec}s activeJobs ${health.activeJobs}`);
23
+ }
24
+ } catch (e) {
25
+ if (args.json) { json({ ok: false, error: e.message }); process.exit(exitCodeForError(e)); }
26
+ die(exitCodeForError(e), e.message);
27
+ }
28
+ }
@@ -0,0 +1,46 @@
1
+ import { parseArgs } from "../lib/args.js";
2
+ import { resolveServer, request } from "../lib/client.js";
3
+ import { out, die, color, json, table, exitCodeForError } from "../lib/output.js";
4
+
5
+ const SPEC = {
6
+ flags: {
7
+ kind: { type: "string" },
8
+ session: { type: "string" },
9
+ json: { type: "boolean" },
10
+ server: { type: "string" },
11
+ help: { short: "h", type: "boolean" },
12
+ },
13
+ };
14
+
15
+ export default async function psCmd(argv) {
16
+ const args = parseArgs(argv, SPEC);
17
+ if (args.help) { out("ima2 ps [--kind classic|node] [--session id] [--json]"); return; }
18
+
19
+ let server;
20
+ try { server = await resolveServer({ serverFlag: args.server }); }
21
+ catch (e) { die(exitCodeForError(e), e.message); }
22
+
23
+ const qs = new URLSearchParams();
24
+ if (args.kind) qs.set("kind", args.kind);
25
+ if (args.session) qs.set("sessionId", args.session);
26
+ const path = `/api/inflight${qs.toString() ? `?${qs}` : ""}`;
27
+ let resp;
28
+ try { resp = await request(server.base, path); }
29
+ catch (e) { die(exitCodeForError(e), e.message); }
30
+
31
+ const jobs = resp.jobs || resp.items || [];
32
+ if (args.json) { json({ jobs }); return; }
33
+ if (jobs.length === 0) { out(color.dim("(no active jobs)")); return; }
34
+
35
+ const now = Date.now();
36
+ table(jobs, [
37
+ { key: "requestId", label: "ID", format: (v) => String(v || "").slice(0, 10) },
38
+ { key: "kind", label: "KIND" },
39
+ { key: "phase", label: "PHASE" },
40
+ { key: "startedAt", label: "AGE", format: (v) => v ? `${Math.round((now - v) / 1000)}s` : "" },
41
+ { key: "prompt", label: "PROMPT", format: (v) => {
42
+ const s = String(v || "").replace(/\s+/g, " ");
43
+ return s.length > 40 ? s.slice(0, 37) + "…" : s;
44
+ } },
45
+ ]);
46
+ }
@@ -0,0 +1,48 @@
1
+ import { parseArgs } from "../lib/args.js";
2
+ import { resolveServer, request } from "../lib/client.js";
3
+ import { openUrl } from "../lib/platform.js";
4
+ import { out, die, color, json, exitCodeForError } from "../lib/output.js";
5
+
6
+ const SPEC = {
7
+ flags: {
8
+ json: { type: "boolean" },
9
+ reveal: { type: "boolean" },
10
+ server: { type: "string" },
11
+ help: { short: "h", type: "boolean" },
12
+ },
13
+ };
14
+
15
+ export default async function showCmd(argv) {
16
+ const args = parseArgs(argv, SPEC);
17
+ if (args.help) { out("ima2 show <filename> [--json] [--reveal]"); return; }
18
+ const name = args.positional[0];
19
+ if (!name) die(2, "filename required");
20
+
21
+ let server;
22
+ try { server = await resolveServer({ serverFlag: args.server }); }
23
+ catch (e) { die(exitCodeForError(e), e.message); }
24
+
25
+ let resp;
26
+ try { resp = await request(server.base, "/api/history"); }
27
+ catch (e) { die(exitCodeForError(e), e.message); }
28
+
29
+ const items = resp.items || resp.history || [];
30
+ const item = items.find((it) => it.filename === name || (it.filename && it.filename.endsWith(name)));
31
+ if (!item) die(1, `not found: ${name}`);
32
+
33
+ if (args.json) { json(item); }
34
+ else {
35
+ out(color.bold(item.filename));
36
+ out(color.dim(` prompt:`) + ` ${item.prompt || ""}`);
37
+ out(color.dim(` size:`) + ` ${item.size || ""} quality: ${item.quality || ""}`);
38
+ if (item.createdAt) out(color.dim(` when:`) + ` ${new Date(item.createdAt).toISOString()}`);
39
+ if (item.url) out(color.dim(` url:`) + ` ${server.base}${item.url}`);
40
+ }
41
+
42
+ if (args.reveal) {
43
+ const url = item.url ? `${server.base}${item.url}` : null;
44
+ if (!url) { out(color.yellow("(no url)")); return; }
45
+ const res = openUrl(url);
46
+ if (!res.ok) out(color.yellow("(could not reveal)"));
47
+ }
48
+ }