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.
- package/README.md +47 -7
- package/assets/card-news/templates/academy-lesson-square/base.png +0 -0
- package/assets/card-news/templates/academy-lesson-square/preview.png +0 -0
- package/assets/card-news/templates/academy-lesson-square/template.json +20 -0
- package/assets/card-news/templates/clean-report-square/base.png +0 -0
- package/assets/card-news/templates/clean-report-square/preview.png +0 -0
- package/assets/card-news/templates/clean-report-square/template.json +20 -0
- package/bin/commands/cancel.js +45 -0
- package/bin/commands/edit.js +33 -4
- package/bin/commands/gen.js +26 -3
- package/bin/commands/ps.js +48 -16
- package/bin/ima2.js +56 -12
- package/bin/lib/client.js +4 -1
- package/bin/lib/error-hints.js +23 -0
- package/bin/lib/output.js +10 -0
- package/config.js +19 -1
- package/docs/API.md +67 -0
- package/docs/FAQ.ko.md +248 -0
- package/docs/FAQ.md +256 -0
- package/docs/README.ja.md +4 -0
- package/docs/README.ko.md +14 -1
- package/docs/README.zh-CN.md +4 -0
- package/docs/RECOVER_OLD_IMAGES.md +2 -0
- package/lib/cardNewsGenerator.js +162 -0
- package/lib/cardNewsJobStore.js +107 -0
- package/lib/cardNewsManifestStore.js +112 -0
- package/lib/cardNewsPlanner.js +180 -0
- package/lib/cardNewsPlannerClient.js +112 -0
- package/lib/cardNewsPlannerPrompt.js +60 -0
- package/lib/cardNewsPlannerSchema.js +259 -0
- package/lib/cardNewsRoleTemplateStore.js +47 -0
- package/lib/cardNewsTemplateStore.js +210 -0
- package/lib/db.js +20 -3
- package/lib/errorClassify.js +2 -2
- package/lib/generationErrors.js +51 -0
- package/lib/historyList.js +82 -8
- package/lib/inflight.js +117 -34
- package/lib/logger.js +37 -3
- package/lib/oauthLauncher.js +52 -19
- package/lib/oauthProxy.js +81 -14
- package/lib/requestLogger.js +48 -0
- package/lib/runtimePorts.js +93 -0
- package/lib/sessionStore.js +48 -7
- package/package.json +3 -2
- package/routes/cardNews.js +183 -0
- package/routes/edit.js +1 -1
- package/routes/generate.js +10 -10
- package/routes/health.js +27 -3
- package/routes/index.js +2 -0
- package/routes/nodes.js +93 -26
- package/server.js +91 -18
- package/ui/dist/assets/index-BjX_nzuK.js +23 -0
- package/ui/dist/assets/index-BjX_nzuK.js.map +1 -0
- package/ui/dist/assets/index-DHyUax4_.css +1 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/assets/index-CqpVoXpZ.css +0 -1
- package/ui/dist/assets/index-IHSd1z1a.js +0 -22
- package/ui/dist/assets/index-IHSd1z1a.js.map +0 -1
package/README.md
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
[](https://nodejs.org/)
|
|
5
5
|
[](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
|
-
|
|
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
|
|
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
|
|
174
|
-
|
|
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
|
|
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
|
|
|
Binary file
|
|
Binary file
|
|
@@ -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
|
+
}
|
|
Binary file
|
|
Binary file
|
|
@@ -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
|
+
}
|
package/bin/commands/edit.js
CHANGED
|
@@ -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
|
|
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) {
|
|
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: {
|
|
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
|
-
|
|
85
|
+
dieWithError(e);
|
|
57
86
|
}
|
|
58
87
|
|
|
59
88
|
const image = resp.image;
|
package/bin/commands/gen.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
|
|
110
|
+
dieWithError(e);
|
|
88
111
|
}
|
|
89
112
|
|
|
90
113
|
const norm = normalizeGenerate(resp);
|
package/bin/commands/ps.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { parseArgs } from "../lib/args.js";
|
|
2
2
|
import { resolveServer, request } from "../lib/client.js";
|
|
3
|
-
import { out,
|
|
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) {
|
|
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) {
|
|
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
|
-
|
|
33
|
-
if (
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
//
|
|
220
|
-
|
|
221
|
-
|
|
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(
|
|
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
|
|
247
|
-
console.log(` ℹ
|
|
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
|
|
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
|
|
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);
|