ima2-gen 1.1.0 → 1.1.2

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.
Files changed (58) hide show
  1. package/README.md +47 -7
  2. package/assets/card-news/templates/academy-lesson-square/base.png +0 -0
  3. package/assets/card-news/templates/academy-lesson-square/preview.png +0 -0
  4. package/assets/card-news/templates/academy-lesson-square/template.json +20 -0
  5. package/assets/card-news/templates/clean-report-square/base.png +0 -0
  6. package/assets/card-news/templates/clean-report-square/preview.png +0 -0
  7. package/assets/card-news/templates/clean-report-square/template.json +20 -0
  8. package/bin/commands/cancel.js +45 -0
  9. package/bin/commands/edit.js +33 -4
  10. package/bin/commands/gen.js +26 -3
  11. package/bin/commands/ps.js +48 -16
  12. package/bin/ima2.js +56 -12
  13. package/bin/lib/client.js +4 -1
  14. package/bin/lib/error-hints.js +23 -0
  15. package/bin/lib/output.js +10 -0
  16. package/config.js +19 -1
  17. package/docs/API.md +67 -0
  18. package/docs/FAQ.ko.md +248 -0
  19. package/docs/FAQ.md +256 -0
  20. package/docs/README.ja.md +4 -0
  21. package/docs/README.ko.md +14 -1
  22. package/docs/README.zh-CN.md +4 -0
  23. package/docs/RECOVER_OLD_IMAGES.md +2 -0
  24. package/lib/cardNewsGenerator.js +162 -0
  25. package/lib/cardNewsJobStore.js +107 -0
  26. package/lib/cardNewsManifestStore.js +112 -0
  27. package/lib/cardNewsPlanner.js +180 -0
  28. package/lib/cardNewsPlannerClient.js +112 -0
  29. package/lib/cardNewsPlannerPrompt.js +60 -0
  30. package/lib/cardNewsPlannerSchema.js +259 -0
  31. package/lib/cardNewsRoleTemplateStore.js +47 -0
  32. package/lib/cardNewsTemplateStore.js +210 -0
  33. package/lib/db.js +20 -3
  34. package/lib/errorClassify.js +2 -2
  35. package/lib/generationErrors.js +51 -0
  36. package/lib/historyList.js +82 -8
  37. package/lib/inflight.js +117 -34
  38. package/lib/logger.js +37 -3
  39. package/lib/oauthLauncher.js +52 -19
  40. package/lib/oauthProxy.js +81 -14
  41. package/lib/requestLogger.js +48 -0
  42. package/lib/runtimePorts.js +93 -0
  43. package/lib/sessionStore.js +48 -7
  44. package/package.json +3 -2
  45. package/routes/cardNews.js +183 -0
  46. package/routes/edit.js +1 -1
  47. package/routes/generate.js +10 -10
  48. package/routes/health.js +27 -3
  49. package/routes/index.js +2 -0
  50. package/routes/nodes.js +93 -26
  51. package/server.js +91 -18
  52. package/ui/dist/assets/index-BjX_nzuK.js +23 -0
  53. package/ui/dist/assets/index-BjX_nzuK.js.map +1 -0
  54. package/ui/dist/assets/index-DHyUax4_.css +1 -0
  55. package/ui/dist/index.html +2 -2
  56. package/ui/dist/assets/index-CqpVoXpZ.css +0 -1
  57. package/ui/dist/assets/index-IHSd1z1a.js +0 -22
  58. package/ui/dist/assets/index-IHSd1z1a.js.map +0 -1
package/README.md CHANGED
@@ -4,6 +4,8 @@
4
4
  [![Node.js](https://img.shields.io/badge/node-%3E%3D20-brightgreen)](https://nodejs.org/)
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
6
6
 
7
+ > 🌐 **Live site**: [lidge-jun.github.io/ima2-gen](https://lidge-jun.github.io/ima2-gen/) · [한국어](https://lidge-jun.github.io/ima2-gen/ko/)
8
+ >
7
9
  > **Read in other languages**: [한국어](docs/README.ko.md) · [日本語](docs/README.ja.md) · [简体中文](docs/README.zh-CN.md)
8
10
 
9
11
  `ima2-gen` is a local image generation studio for people who want the ChatGPT/Codex image workflow in a small desktop-like web app.
@@ -27,6 +29,8 @@ npx @openai/codex login
27
29
  npx ima2-gen serve
28
30
  ```
29
31
 
32
+ If `3333` is already occupied, `ima2-gen` binds the next available port and writes the actual URL to `~/.ima2/server.json`. Use `ima2 open` or the URL printed in the terminal instead of assuming the port.
33
+
30
34
  You can also install it globally:
31
35
 
32
36
  ```bash
@@ -34,6 +38,11 @@ npm install -g ima2-gen
34
38
  ima2 serve
35
39
  ```
36
40
 
41
+ Before updating a global install on Windows, stop any running `ima2 serve`
42
+ process. If npm reports `EBUSY` or `resource busy or locked`, close ima2
43
+ terminals, end stale `node.exe` processes if needed, and retry. If the lock
44
+ persists, reboot and run the update before starting ima2 again.
45
+
37
46
  ## What It Does
38
47
 
39
48
  - **Classic mode**: generate, edit, reuse the current image, paste references, and continue from history.
@@ -55,10 +64,10 @@ If the settings page says **Configured but disabled**, that means an API key exi
55
64
 
56
65
  ## Model Guidance
57
66
 
58
- Start with **`gpt-5.4`** when you want the safest balanced image workflow.
67
+ The app defaults to **`gpt-5.4-mini`** for fast local iteration. Switch to **`gpt-5.4`** when you want the safest balanced image workflow.
59
68
 
60
69
  - `gpt-5.4` — recommended balanced choice.
61
- - `gpt-5.4-mini` — current app default and faster draft model.
70
+ - `gpt-5.4-mini` — current default and faster draft model.
62
71
  - `gpt-5.5` — strongest quality option when your Codex CLI/OAuth backend supports it. It may use more quota, expose different tool capabilities, or require updating Codex CLI before it works reliably.
63
72
 
64
73
  The app also exposes quality (`low`, `medium`, `high`) and moderation (`auto`, `low`) controls.
@@ -82,6 +91,12 @@ Use Node mode when you want to explore branches.
82
91
 
83
92
  Each node keeps its own prompt and result. Root nodes can attach local references; child nodes use the parent image as their source. Completed jobs are matched back to nodes by request ID, so reloads and graph version conflicts can recover finished results.
84
93
 
94
+ ### Experimental Card News Mode
95
+
96
+ Card News is still dev-only and experimental. It is hidden in the default
97
+ published runtime unless explicitly enabled for development, and it should not
98
+ be treated as a stable public feature yet.
99
+
85
100
  ### Settings And Style Sheets
86
101
 
87
102
  The settings workspace keeps account, model, appearance, and language controls away from the generation sidebar.
@@ -98,7 +113,7 @@ Style sheets let you capture a reusable visual direction.
98
113
 
99
114
  | Command | Description |
100
115
  |---|---|
101
- | `ima2 serve` | Start the local web server |
116
+ | `ima2 serve [--dev]` | Start the local web server; `--dev` enables verbose server diagnostics |
102
117
  | `ima2 setup` | Reconfigure saved auth |
103
118
  | `ima2 status` | Show config and OAuth status |
104
119
  | `ima2 doctor` | Diagnose Node, package, config, and auth |
@@ -116,9 +131,17 @@ These require a running `ima2 serve`.
116
131
  | `ima2 ls` | List local history |
117
132
  | `ima2 show <name>` | Reveal a generated asset |
118
133
  | `ima2 ps` | List active jobs |
134
+ | `ima2 cancel <requestId>` | Mark an in-flight job canceled |
119
135
  | `ima2 ping` | Health-check the running server |
120
136
 
121
- The server advertises its port at `~/.ima2/server.json`. Override discovery with `--server <url>` or `IMA2_SERVER=http://localhost:3333`.
137
+ The server advertises its actual port at `~/.ima2/server.json`. If `3333` is busy, the backend can fall back to `3334+` and CLI commands follow the advertised URL. Override discovery with `--server <url>` or `IMA2_SERVER=http://localhost:3333`.
138
+
139
+ ```bash
140
+ ima2 gen "poster" --model gpt-5.4 --mode direct --moderation low
141
+ ima2 edit input.png --prompt "make it rainy" --model gpt-5.4
142
+ ima2 ps --terminal
143
+ ima2 cancel <requestId>
144
+ ```
122
145
 
123
146
  ## Configuration
124
147
 
@@ -131,14 +154,24 @@ environment variables > ~/.ima2/config.json > built-in defaults
131
154
  | Variable | Default | Description |
132
155
  |---|---:|---|
133
156
  | `IMA2_PORT` / `PORT` | `3333` | Web server port |
157
+ | `IMA2_HOST` | `127.0.0.1` | Web server bind host |
134
158
  | `IMA2_OAUTH_PROXY_PORT` / `OAUTH_PORT` | `10531` | OAuth proxy port |
135
159
  | `IMA2_SERVER` | — | CLI target override |
136
160
  | `IMA2_CONFIG_DIR` | `~/.ima2` | Config and SQLite location |
161
+ | `IMA2_ADVERTISE_FILE` | `~/.ima2/server.json` | Runtime discovery file |
137
162
  | `IMA2_GENERATED_DIR` | `~/.ima2/generated` | Generated image directory |
163
+ | `IMA2_IMAGE_MODEL_DEFAULT` | `gpt-5.4-mini` | Server fallback image model |
138
164
  | `IMA2_NO_OAUTH_PROXY` | — | Set `1` to disable the auto-started OAuth proxy |
165
+ | `IMA2_LOG_LEVEL` | `warn` | Normal serve defaults to `warn`; dev mode defaults to `debug`; supports `debug`, `info`, `warn`, `error`, or `silent` |
139
166
  | `IMA2_INFLIGHT_TERMINAL_TTL_MS` | `30000` | Recent terminal job retention for debug views |
140
167
  | `OPENAI_API_KEY` | — | API key for supported auxiliary paths, not image generation |
141
168
 
169
+ ### Logging modes
170
+
171
+ `ima2 serve` keeps terminal output intentionally quiet: startup URLs, warnings, and errors stay visible, while request/node/OAuth structured logs are hidden by default.
172
+
173
+ Use `ima2 serve --dev`, `npm run dev`, or `IMA2_LOG_LEVEL=debug ima2 serve` when you need request IDs, node generation phases, OAuth stream diagnostics, or inflight state transitions. Explicit `IMA2_LOG_LEVEL` and `~/.ima2/config.json` values still override the built-in defaults.
174
+
142
175
  ## API Reference
143
176
 
144
177
  The endpoint list moved to [docs/API.md](docs/API.md) so this README can stay focused on first-run use.
@@ -146,6 +179,8 @@ The endpoint list moved to [docs/API.md](docs/API.md) so this README can stay fo
146
179
  Useful references:
147
180
 
148
181
  - [API Reference](docs/API.md)
182
+ - [FAQ](docs/FAQ.md)
183
+ - [Recover old images](docs/RECOVER_OLD_IMAGES.md)
149
184
  - [Korean README](docs/README.ko.md)
150
185
  - [Japanese README](docs/README.ja.md)
151
186
  - [Chinese README](docs/README.zh-CN.md)
@@ -170,8 +205,13 @@ Recent versions moved generated images from the installed package folder to `~/.
170
205
  **`gpt-5.5` fails but other models work**
171
206
  Update Codex CLI first, then retry. If it still fails, your account or backend route may not expose the same image capability or quota for `gpt-5.5` yet; use `gpt-5.4` as the stable fallback.
172
207
 
173
- **The port is unexpectedly `3457`**
174
- Your shell may have inherited `PORT=3457` from another local tool. Run `unset PORT` or start with `IMA2_PORT=3333 ima2 serve`.
208
+ **The app opened on a different port**
209
+ If the requested server port is busy, `ima2-gen` falls back to the next available port and records it in `~/.ima2/server.json`. If the port is unexpectedly `3457`, your shell may also have inherited `PORT=3457` from another local tool. Run `unset PORT` or start with `IMA2_PORT=3333 ima2 serve`.
210
+
211
+ **Port `10531` is already used on Windows**
212
+ Some Windows security tools, including `AnySign4PC.exe`, may occupy the default OAuth proxy port. Current builds track the actual fallback OAuth port. If you still need a manual override, start with `IMA2_OAUTH_PROXY_PORT=11531 ima2 serve` and check `ima2 doctor`.
213
+
214
+ For more beginner-friendly answers, see the [FAQ](docs/FAQ.md).
175
215
 
176
216
  ## Development
177
217
 
@@ -184,7 +224,7 @@ npm test
184
224
  npm run build
185
225
  ```
186
226
 
187
- `npm run dev` builds the UI and starts `server.js` with `--watch`. Node mode is part of the packaged UI by default.
227
+ `npm run dev` builds the UI and starts `server.js` with `--watch` and verbose server diagnostics. Node mode is part of the packaged UI by default.
188
228
 
189
229
  ## License
190
230
 
@@ -0,0 +1,20 @@
1
+ {
2
+ "id": "academy-lesson-square",
3
+ "name": "Academy Lesson Square",
4
+ "description": "Education-focused square template with headline, visual, and caption safe areas.",
5
+ "authoringLabel": "Academy lesson carousel",
6
+ "size": "2048x2048",
7
+ "recommendedOutputSizes": ["1024x1024", "2048x2048"],
8
+ "previewFilename": "preview.png",
9
+ "baseFilename": "base.png",
10
+ "stylePrompt": "Modern Korean academy lesson card, confident educational tone, bright study-focused composition, blue and white palette, safe headline area, reusable card-news frame.",
11
+ "negativePrompt": "messy notes, unreadable tiny text, low contrast, watermark, signature, chaotic classroom",
12
+ "slots": [
13
+ { "id": "title", "kind": "text", "textKind": "headline", "label": "Top headline", "placement": "top-center", "x": 140, "y": 140, "w": 1768, "h": 320, "required": true, "maxChars": 28, "safeArea": true },
14
+ { "id": "visual", "kind": "image", "label": "Main visual area", "placement": "center", "x": 200, "y": 540, "w": 1648, "h": 1000, "required": true, "safeArea": false },
15
+ { "id": "caption", "kind": "text", "textKind": "body", "label": "Bottom caption", "placement": "bottom-center", "x": 200, "y": 1610, "w": 1648, "h": 260, "required": false, "maxChars": 48, "safeArea": true }
16
+ ],
17
+ "palette": ["white", "blue", "slate"],
18
+ "typography": { "headline": "strong sans", "body": "rounded sans" },
19
+ "recommendedRoleNodeIds": ["mid-5", "long-8"]
20
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "id": "clean-report-square",
3
+ "name": "Clean Report Square",
4
+ "description": "Editorial report-style square template for clear title, visual, and CTA hierarchy.",
5
+ "authoringLabel": "Clean editorial report",
6
+ "size": "2048x2048",
7
+ "recommendedOutputSizes": ["1024x1024", "2048x2048"],
8
+ "previewFilename": "preview.png",
9
+ "baseFilename": "base.png",
10
+ "stylePrompt": "Clean Korean card news report layout, white editorial surface, subtle grid, restrained navy accent, large safe title area, clear central visual slot, minimal decoration.",
11
+ "negativePrompt": "busy layout, tiny text, watermark, signature, excessive decoration, cluttered typography",
12
+ "slots": [
13
+ { "id": "title", "kind": "text", "textKind": "headline", "label": "Top title", "placement": "top-center", "x": 160, "y": 160, "w": 1728, "h": 260, "required": true, "maxChars": 26, "safeArea": true },
14
+ { "id": "visual", "kind": "image", "label": "Main visual area", "placement": "center", "x": 160, "y": 500, "w": 1728, "h": 980, "required": true, "safeArea": false },
15
+ { "id": "cta", "kind": "text", "textKind": "cta", "label": "Bottom CTA", "placement": "bottom-center", "x": 160, "y": 1620, "w": 1728, "h": 260, "required": false, "maxChars": 32, "safeArea": true }
16
+ ],
17
+ "palette": ["white", "navy", "blue"],
18
+ "typography": { "headline": "bold sans", "body": "clean sans" },
19
+ "recommendedRoleNodeIds": ["mid-5", "short-3"]
20
+ }
@@ -0,0 +1,45 @@
1
+ import { parseArgs } from "../lib/args.js";
2
+ import { resolveServer, request } from "../lib/client.js";
3
+ import { out, die, dieWithError, color, json } 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
+ const HELP = `
14
+ ima2 cancel <requestId> [--json]
15
+
16
+ Mark an in-flight job as canceled in the local ima2 server registry.
17
+ `;
18
+
19
+ export default async function cancelCmd(argv) {
20
+ const args = parseArgs(argv, SPEC);
21
+ if (args.help) { out(HELP); return; }
22
+
23
+ const requestId = args.positional[0];
24
+ if (!requestId) die(2, "requestId required");
25
+
26
+ let server;
27
+ try { server = await resolveServer({ serverFlag: args.server }); }
28
+ catch (e) {
29
+ if (args.json) json({ ok: false, requestId, error: e.message, code: e.code, status: e.status });
30
+ dieWithError(e);
31
+ }
32
+
33
+ try {
34
+ await request(server.base, `/api/inflight/${encodeURIComponent(requestId)}`, {
35
+ method: "DELETE",
36
+ timeoutMs: 30_000,
37
+ });
38
+ } catch (e) {
39
+ if (args.json) json({ ok: false, requestId, error: e.message, code: e.code, status: e.status });
40
+ dieWithError(e);
41
+ }
42
+
43
+ if (args.json) json({ ok: true, requestId });
44
+ else out(color.green("✓ ") + `canceled ${requestId}`);
45
+ }
@@ -1,7 +1,11 @@
1
1
  import { parseArgs } from "../lib/args.js";
2
2
  import { resolveServer, request } from "../lib/client.js";
3
3
  import { fileToDataUri, dataUriToFile, defaultOutName } from "../lib/files.js";
4
- import { out, die, color, json, exitCodeForError } from "../lib/output.js";
4
+ import { out, die, dieWithError, color, json } from "../lib/output.js";
5
+
6
+ const VALID_MODES = new Set(["auto", "direct"]);
7
+ const VALID_MODERATION = new Set(["auto", "low"]);
8
+ const KNOWN_IMAGE_MODELS = new Set(["gpt-5.5", "gpt-5.4", "gpt-5.4-mini", "gpt-5.3-codex-spark"]);
5
9
 
6
10
  const SPEC = {
7
11
  flags: {
@@ -12,6 +16,10 @@ const SPEC = {
12
16
  json: { type: "boolean" },
13
17
  timeout: { type: "string", default: "180" },
14
18
  server: { type: "string" },
19
+ model: { type: "string" },
20
+ mode: { type: "string", default: "auto" },
21
+ moderation: { type: "string", default: "low" },
22
+ session: { type: "string" },
15
23
  help: { short: "h", type: "boolean" },
16
24
  },
17
25
  };
@@ -27,6 +35,10 @@ const HELP = `
27
35
  -s, --size <WxH>
28
36
  -o, --out <file>
29
37
  --json
38
+ --model <gpt-5.5|gpt-5.4|gpt-5.4-mini>
39
+ --mode <auto|direct> Prompt handling mode. Default: auto
40
+ --moderation <auto|low> Default: low
41
+ --session <id> Apply session style sheet if enabled
30
42
  `;
31
43
 
32
44
  export default async function editCmd(argv) {
@@ -35,10 +47,18 @@ export default async function editCmd(argv) {
35
47
  const input = args.positional[0];
36
48
  if (!input) die(2, "input image path required");
37
49
  if (!args.prompt) die(2, "--prompt is required");
50
+ if (!VALID_MODES.has(args.mode)) die(2, "--mode must be one of: auto, direct");
51
+ if (!VALID_MODERATION.has(args.moderation)) die(2, "--moderation must be one of: auto, low");
52
+ if (args.model && !KNOWN_IMAGE_MODELS.has(args.model)) {
53
+ die(2, "--model must be one of: gpt-5.5, gpt-5.4, gpt-5.4-mini, gpt-5.3-codex-spark");
54
+ }
38
55
 
39
56
  let server;
40
57
  try { server = await resolveServer({ serverFlag: args.server }); }
41
- catch (e) { die(exitCodeForError(e), e.message); }
58
+ catch (e) {
59
+ if (args.json) json({ ok: false, error: e.message, code: e.code, status: e.status });
60
+ dieWithError(e);
61
+ }
42
62
 
43
63
  const imageDataUri = await fileToDataUri(input);
44
64
  const imageB64 = imageDataUri.split(",")[1];
@@ -48,12 +68,21 @@ export default async function editCmd(argv) {
48
68
  try {
49
69
  resp = await request(server.base, "/api/edit", {
50
70
  method: "POST",
51
- body: { prompt: args.prompt, image: imageB64, quality: args.quality, size: args.size },
71
+ body: {
72
+ prompt: args.prompt,
73
+ image: imageB64,
74
+ quality: args.quality,
75
+ size: args.size,
76
+ model: args.model,
77
+ mode: args.mode,
78
+ moderation: args.moderation,
79
+ sessionId: args.session,
80
+ },
52
81
  timeoutMs,
53
82
  });
54
83
  } catch (e) {
55
84
  if (args.json) json({ ok: false, error: e.message, code: e.code });
56
- die(exitCodeForError(e), e.message);
85
+ dieWithError(e);
57
86
  }
58
87
 
59
88
  const image = resp.image;
@@ -1,7 +1,11 @@
1
1
  import { parseArgs } from "../lib/args.js";
2
2
  import { resolveServer, request, normalizeGenerate } from "../lib/client.js";
3
3
  import { fileToDataUri, dataUriToFile, defaultOutName, readStdin } from "../lib/files.js";
4
- import { out, err, die, color, json, exitCodeForError } from "../lib/output.js";
4
+ import { out, die, dieWithError, color, json } from "../lib/output.js";
5
+
6
+ const VALID_MODES = new Set(["auto", "direct"]);
7
+ const VALID_MODERATION = new Set(["auto", "low"]);
8
+ const KNOWN_IMAGE_MODELS = new Set(["gpt-5.5", "gpt-5.4", "gpt-5.4-mini", "gpt-5.3-codex-spark"]);
5
9
 
6
10
  const SPEC = {
7
11
  flags: {
@@ -17,6 +21,10 @@ const SPEC = {
17
21
  stdin: { type: "boolean" },
18
22
  timeout: { type: "string", default: "180" },
19
23
  server: { type: "string" },
24
+ model: { type: "string" },
25
+ mode: { type: "string", default: "auto" },
26
+ moderation: { type: "string", default: "low" },
27
+ session: { type: "string" },
20
28
  help: { short: "h", type: "boolean" },
21
29
  },
22
30
  };
@@ -38,9 +46,14 @@ const HELP = `
38
46
  --stdin Read prompt from stdin
39
47
  --timeout <sec> Default: 180
40
48
  --server <url> Override server URL
49
+ --model <gpt-5.5|gpt-5.4|gpt-5.4-mini>
50
+ --mode <auto|direct> Prompt handling mode. Default: auto
51
+ --moderation <auto|low> Default: low
52
+ --session <id> Apply session style sheet if enabled
41
53
 
42
54
  Examples:
43
55
  ima2 gen "a shiba in space"
56
+ ima2 gen "poster" --model gpt-5.4 --mode direct --moderation low
44
57
  ima2 gen "merge" --ref a.png --ref b.png -q high -o out.png
45
58
  cat prompt.txt | ima2 gen --stdin -n 2 -d ./out
46
59
  `;
@@ -58,6 +71,11 @@ export default async function genCmd(argv) {
58
71
 
59
72
  const refs = args.ref || [];
60
73
  if (refs.length > 5) die(2, "max 5 --ref attachments");
74
+ if (!VALID_MODES.has(args.mode)) die(2, "--mode must be one of: auto, direct");
75
+ if (!VALID_MODERATION.has(args.moderation)) die(2, "--moderation must be one of: auto, low");
76
+ if (args.model && !KNOWN_IMAGE_MODELS.has(args.model)) {
77
+ die(2, "--model must be one of: gpt-5.5, gpt-5.4, gpt-5.4-mini, gpt-5.3-codex-spark");
78
+ }
61
79
 
62
80
  const n = Math.max(1, Math.min(8, parseInt(args.count) || 1));
63
81
  const timeoutMs = (parseInt(args.timeout) || 180) * 1000;
@@ -66,7 +84,8 @@ export default async function genCmd(argv) {
66
84
  try {
67
85
  server = await resolveServer({ serverFlag: args.server });
68
86
  } catch (e) {
69
- die(exitCodeForError(e), e.message);
87
+ if (args.json) json({ ok: false, error: e.message, code: e.code, status: e.status });
88
+ dieWithError(e);
70
89
  }
71
90
 
72
91
  const references = await Promise.all(refs.map((p) => fileToDataUri(p)));
@@ -77,6 +96,10 @@ export default async function genCmd(argv) {
77
96
  size: args.size,
78
97
  n,
79
98
  references,
99
+ model: args.model,
100
+ mode: args.mode,
101
+ moderation: args.moderation,
102
+ sessionId: args.session,
80
103
  };
81
104
 
82
105
  let resp;
@@ -84,7 +107,7 @@ export default async function genCmd(argv) {
84
107
  resp = await request(server.base, "/api/generate", { method: "POST", body, timeoutMs });
85
108
  } catch (e) {
86
109
  if (args.json) json({ ok: false, error: e.message, code: e.code, status: e.status });
87
- die(exitCodeForError(e), e.message);
110
+ dieWithError(e);
88
111
  }
89
112
 
90
113
  const norm = normalizeGenerate(resp);
@@ -1,11 +1,12 @@
1
1
  import { parseArgs } from "../lib/args.js";
2
2
  import { resolveServer, request } from "../lib/client.js";
3
- import { out, die, color, json, table, exitCodeForError } from "../lib/output.js";
3
+ import { out, dieWithError, color, json, table } from "../lib/output.js";
4
4
 
5
5
  const SPEC = {
6
6
  flags: {
7
7
  kind: { type: "string" },
8
8
  session: { type: "string" },
9
+ terminal: { type: "boolean" },
9
10
  json: { type: "boolean" },
10
11
  server: { type: "string" },
11
12
  help: { short: "h", type: "boolean" },
@@ -14,33 +15,64 @@ const SPEC = {
14
15
 
15
16
  export default async function psCmd(argv) {
16
17
  const args = parseArgs(argv, SPEC);
17
- if (args.help) { out("ima2 ps [--kind classic|node] [--session id] [--json]"); return; }
18
+ if (args.help) { out("ima2 ps [--kind classic|node] [--session id] [--terminal] [--json]"); return; }
18
19
 
19
20
  let server;
20
21
  try { server = await resolveServer({ serverFlag: args.server }); }
21
- catch (e) { die(exitCodeForError(e), e.message); }
22
+ catch (e) {
23
+ if (args.json) json({ ok: false, error: e.message, code: e.code, status: e.status });
24
+ dieWithError(e);
25
+ }
22
26
 
23
27
  const qs = new URLSearchParams();
24
28
  if (args.kind) qs.set("kind", args.kind);
25
29
  if (args.session) qs.set("sessionId", args.session);
30
+ if (args.terminal) qs.set("includeTerminal", "1");
26
31
  const path = `/api/inflight${qs.toString() ? `?${qs}` : ""}`;
27
32
  let resp;
28
33
  try { resp = await request(server.base, path); }
29
- catch (e) { die(exitCodeForError(e), e.message); }
34
+ catch (e) {
35
+ if (args.json) json({ ok: false, error: e.message, code: e.code, status: e.status });
36
+ dieWithError(e);
37
+ }
30
38
 
31
39
  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; }
40
+ const terminalJobs = resp.terminalJobs || [];
41
+ if (args.json) {
42
+ json(args.terminal ? { jobs, terminalJobs } : { jobs });
43
+ return;
44
+ }
45
+ if (jobs.length === 0 && terminalJobs.length === 0) {
46
+ out(color.dim(args.terminal ? "(no active or terminal jobs)" : "(no active jobs)"));
47
+ return;
48
+ }
34
49
 
35
50
  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
- ]);
51
+ if (jobs.length > 0) {
52
+ out(color.bold("Active jobs"));
53
+ table(jobs, [
54
+ { key: "requestId", label: "ID", format: (v) => String(v || "").slice(0, 10) },
55
+ { key: "kind", label: "KIND" },
56
+ { key: "phase", label: "PHASE" },
57
+ { key: "startedAt", label: "AGE", format: (v) => v ? `${Math.round((now - v) / 1000)}s` : "" },
58
+ { key: "prompt", label: "PROMPT", format: (v) => {
59
+ const s = String(v || "").replace(/\s+/g, " ");
60
+ return s.length > 40 ? s.slice(0, 37) + "…" : s;
61
+ } },
62
+ ]);
63
+ }
64
+ if (terminalJobs.length > 0) {
65
+ if (jobs.length > 0) out("");
66
+ out(color.bold("Terminal jobs"));
67
+ table(terminalJobs, [
68
+ { key: "requestId", label: "ID", format: (v) => String(v || "").slice(0, 10) },
69
+ { key: "kind", label: "KIND" },
70
+ { key: "status", label: "STATUS" },
71
+ { key: "finishedAt", label: "AGE", format: (v) => v ? `${Math.round((now - v) / 1000)}s` : "" },
72
+ { key: "prompt", label: "PROMPT", format: (v) => {
73
+ const s = String(v || "").replace(/\s+/g, " ");
74
+ return s.length > 40 ? s.slice(0, 37) + "…" : s;
75
+ } },
76
+ ]);
77
+ }
46
78
  }
package/bin/ima2.js CHANGED
@@ -3,6 +3,7 @@ import { createInterface } from "readline/promises";
3
3
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
4
4
  import { join, dirname } from "path";
5
5
  import { fileURLToPath } from "url";
6
+ import { createRequire } from "module";
6
7
  import { spawn, execSync } from "child_process";
7
8
  import { networkInterfaces, homedir } from "os";
8
9
  import { openUrl, resolveBin } from "./lib/platform.js";
@@ -14,6 +15,7 @@ import { config as runtimeConfig } from "../config.js";
14
15
  const __dirname = dirname(fileURLToPath(import.meta.url));
15
16
  const ROOT = join(__dirname, "..");
16
17
  const HOME = homedir();
18
+ const requireFromRoot = createRequire(join(ROOT, "package.json"));
17
19
  // Config lives under runtimeConfig.storage.configDir (honors IMA2_CONFIG_DIR).
18
20
  // Legacy installs that stored config at <packageRoot>/.ima2/config.json will be
19
21
  // migrated on first write.
@@ -43,6 +45,33 @@ function saveConfig(config) {
43
45
  writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
44
46
  }
45
47
 
48
+ function loadAdvertisement() {
49
+ const p = runtimeConfig.storage.advertiseFile;
50
+ if (!existsSync(p)) return null;
51
+ try {
52
+ return JSON.parse(readFileSync(p, "utf-8"));
53
+ } catch {
54
+ return null;
55
+ }
56
+ }
57
+
58
+ function advertisedServerUrl() {
59
+ const adv = loadAdvertisement();
60
+ return adv?.backend?.url || adv?.url || (adv?.port ? `http://localhost:${adv.port}` : null);
61
+ }
62
+
63
+ function missingRuntimeDeps() {
64
+ const deps = ["express", "better-sqlite3", "openai", "openai-oauth"];
65
+ return deps.filter((dep) => {
66
+ try {
67
+ requireFromRoot.resolve(dep);
68
+ return false;
69
+ } catch {
70
+ return true;
71
+ }
72
+ });
73
+ }
74
+
46
75
  async function setup() {
47
76
  const rl = createInterface({ input: process.stdin, output: process.stdout });
48
77
 
@@ -102,7 +131,7 @@ async function setup() {
102
131
  return config;
103
132
  }
104
133
 
105
- async function serve() {
134
+ async function serve(serveArgs = []) {
106
135
  try {
107
136
  await maybePromptGithubStar();
108
137
  } catch (err) {
@@ -135,6 +164,11 @@ async function serve() {
135
164
  }
136
165
 
137
166
  const env = { ...process.env };
167
+ const serveDev = serveArgs.includes("--dev");
168
+ if (serveDev) {
169
+ env.IMA2_DEV = "1";
170
+ env.IMA2_LOG_LEVEL = env.IMA2_LOG_LEVEL || "debug";
171
+ }
138
172
 
139
173
  if (config.provider === "api" && config.apiKey) {
140
174
  env.OPENAI_API_KEY = config.apiKey;
@@ -216,12 +250,14 @@ async function doctor() {
216
250
  fail++;
217
251
  }
218
252
 
219
- // node_modules
220
- if (existsSync(join(ROOT, "node_modules"))) {
221
- console.log(" node_modules installed");
253
+ // Runtime dependencies may be hoisted by npm, pnpm, or Yarn. Resolve the
254
+ // packages instead of requiring a package-local node_modules folder.
255
+ const missingDeps = missingRuntimeDeps();
256
+ if (missingDeps.length === 0) {
257
+ console.log(" ✓ runtime dependencies resolvable");
222
258
  ok++;
223
259
  } else {
224
- console.log("node_modules missing run 'npm install'");
260
+ console.log(` ✗ missing runtime dependencies: ${missingDeps.join(", ")}`);
225
261
  fail++;
226
262
  }
227
263
 
@@ -243,8 +279,14 @@ async function doctor() {
243
279
  }
244
280
 
245
281
  // Port availability (simple check)
246
- const port = runtimeConfig.server.port;
247
- console.log(` ℹ Default port: ${port}`);
282
+ const adv = loadAdvertisement();
283
+ console.log(` ℹ Preferred backend port: ${runtimeConfig.server.port}`);
284
+ if (adv?.backend || adv?.port) {
285
+ console.log(` ℹ Backend actual URL: ${adv?.backend?.url || adv?.url || `http://localhost:${adv.port}`}`);
286
+ if (adv?.oauth) {
287
+ console.log(` ℹ OAuth actual URL: ${adv.oauth.url} (${adv.oauth.status || "unknown"})`);
288
+ }
289
+ }
248
290
 
249
291
  const storageLines = await buildStorageDoctorLines({
250
292
  rootDir: ROOT,
@@ -258,8 +300,7 @@ async function doctor() {
258
300
  }
259
301
 
260
302
  function openBrowser() {
261
- const port = runtimeConfig.server.port;
262
- const url = `http://localhost:${port}`;
303
+ const url = advertisedServerUrl() || `http://localhost:${runtimeConfig.server.port}`;
263
304
  const res = openUrl(url);
264
305
  if (res.ok) {
265
306
  console.log(`\n Opening ${url} ...\n`);
@@ -275,7 +316,7 @@ function showHelp() {
275
316
  Usage: ima2 <command> [options]
276
317
 
277
318
  Server commands:
278
- serve Start the image generation server
319
+ serve [--dev] Start the image generation server
279
320
  setup, login Configure API key or OAuth (interactive)
280
321
  status Show current configuration status
281
322
  doctor Diagnose environment and setup
@@ -288,6 +329,7 @@ function showHelp() {
288
329
  ls List recent history (ima2 ls --help)
289
330
  show <name> Show one history item (ima2 show --help)
290
331
  ps List active jobs (ima2 ps --help)
332
+ cancel <id> Mark an in-flight job canceled (ima2 cancel --help)
291
333
  ping Ping running server / check health
292
334
 
293
335
  Options:
@@ -296,6 +338,7 @@ function showHelp() {
296
338
 
297
339
  Examples:
298
340
  ima2 serve Start server
341
+ ima2 serve --dev Start with verbose server diagnostics
299
342
  ima2 gen "a shiba in space" Generate from CLI
300
343
  ima2 gen "merge" --ref a.png --ref b.png -q high -o out.png
301
344
  ima2 ls -n 10 Last 10 generations
@@ -313,14 +356,14 @@ if (args.includes("-v") || args.includes("--version")) {
313
356
  }
314
357
 
315
358
  if ((!command || args.includes("-h") || args.includes("--help"))
316
- && !["gen", "edit", "ls", "show", "ps", "ping"].includes(command)) {
359
+ && !["gen", "edit", "ls", "show", "ps", "cancel", "ping"].includes(command)) {
317
360
  showHelp();
318
361
  process.exit(command ? 0 : 1);
319
362
  }
320
363
 
321
364
  switch (command) {
322
365
  case "serve":
323
- serve();
366
+ serve(args.slice(1));
324
367
  break;
325
368
  case "setup":
326
369
  case "login":
@@ -348,6 +391,7 @@ switch (command) {
348
391
  case "ls":
349
392
  case "show":
350
393
  case "ps":
394
+ case "cancel":
351
395
  case "ping": {
352
396
  const { setCliVersion } = await import("./lib/client.js");
353
397
  setCliVersion(pkg.version);