ima2-gen 3.12.3 → 3.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -3
- package/bin/commands/prompt-sub/build.js +5 -1
- package/config.js +14 -0
- package/docs/API.md +21 -0
- package/docs/CLI.md +7 -0
- package/docs/PROMPT_STUDIO.md +9 -0
- package/docs/migration/runtime-test-inventory.md +4 -1
- package/lib/configFileStore.js +44 -0
- package/lib/configKeys.js +4 -0
- package/lib/promptBuilder/client.js +106 -64
- package/lib/promptBuilder/constants.js +20 -2
- package/lib/promptBuilder/requestSchema.js +42 -7
- package/lib/promptBuilder/router.js +70 -0
- package/lib/promptBuilder/transport.js +5 -4
- package/package.json +2 -2
- package/routes/keys.js +6 -32
- package/routes/promptBuilder.js +68 -14
- package/skills/ima2/SKILL.md +40 -3
- package/skills/ima2-front/SKILL.md +1 -1
- package/skills/ima2-front/references/asset-requirements.md +16 -0
- package/ui/dist/.vite/manifest.json +44 -35
- package/ui/dist/assets/{AgentWorkspace-AJkVTaU5.js → AgentWorkspace-CBwn1WUC.js} +1 -1
- package/ui/dist/assets/AssetGenWorkspace-CleWSwPz.js +2 -0
- package/ui/dist/assets/AssetsWorkspace-DdjXb45L.js +1 -0
- package/ui/dist/assets/{CardNewsWorkspace-C_Eb2g7d.js → CardNewsWorkspace-BaNpBvJw.js} +1 -1
- package/ui/dist/assets/{GenerationRequestLogPanel-giH1e4hp.js → GenerationRequestLogPanel-Cd2ArsWr.js} +1 -1
- package/ui/dist/assets/HomeWorkspace-nKDhivwR.js +1 -0
- package/ui/dist/assets/KeyingPanel-DT8zsqpi.js +1 -0
- package/ui/dist/assets/{NodeCanvas-CF_NHudI.js → NodeCanvas-DcKs0w36.js} +1 -1
- package/ui/dist/assets/PromptBuilderPanel-e6BSRAdj.js +2 -0
- package/ui/dist/assets/{PromptImportDialog-B40X9UAa.js → PromptImportDialog-CLkjHz7L.js} +2 -2
- package/ui/dist/assets/{PromptImportDiscoverySection-BWoUDHj1.js → PromptImportDiscoverySection-DoQw4zcd.js} +1 -1
- package/ui/dist/assets/{PromptImportFolderSection-DBSEyJb-.js → PromptImportFolderSection-CcLiliAZ.js} +1 -1
- package/ui/dist/assets/{PromptLibraryPanel-DLywJBpt.js → PromptLibraryPanel-BjxzL7t5.js} +2 -2
- package/ui/dist/assets/SettingsWorkspace-Ctu_usKk.js +1 -0
- package/ui/dist/assets/SpriteRecipeWorkspace-DZjsabce.js +1 -0
- package/ui/dist/assets/index-0fpfY8vu.css +1 -0
- package/ui/dist/assets/index-DueH_AmZ.js +30 -0
- package/ui/dist/assets/index-ICf98ZcU.js +5 -0
- package/ui/dist/assets/{pptxgen.es-CSd_gCsx.js → pptxgen.es-C--aL3JM.js} +1 -1
- package/ui/dist/assets/promptBuilderStore-CkwnWMbR.js +1 -0
- package/ui/dist/assets/useAgentDialogFocus-DPB8JITN.js +1 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/assets/AssetGenWorkspace-Cguej69V.js +0 -2
- package/ui/dist/assets/AssetsWorkspace-DIQ9wBuD.js +0 -1
- package/ui/dist/assets/HomeWorkspace-DHmXGtqC.js +0 -1
- package/ui/dist/assets/PromptBuilderPanel-D7NJto-1.js +0 -2
- package/ui/dist/assets/SettingsWorkspace-BawWId1t.js +0 -1
- package/ui/dist/assets/SpriteRecipeWorkspace-Biy2yRAk.js +0 -1
- package/ui/dist/assets/VectorizePanel-DdlVvwV1.js +0 -1
- package/ui/dist/assets/index-D05Ong5g.js +0 -5
- package/ui/dist/assets/index-DGRIdnsL.css +0 -1
- package/ui/dist/assets/index-Sch70vBs.js +0 -30
- package/ui/dist/assets/useAgentDialogFocus-DcWwFfYd.js +0 -1
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
`ima2-gen` is a local-first visual generation runtime and studio for people and coding agents, with reproducible image and video workflows across multiple providers.
|
|
18
18
|
|
|
19
|
-
Install globally and generate images and videos
|
|
19
|
+
Install globally and generate images and videos through the core registry: OpenAI OAuth/API, Grok OAuth/API, Antigravity CLI, Gemini API, AtlasCloud, MiniMax, NovelAI, and registered ComfyUI workflows. Runway and Higgsfield remain separate MCP-backed integrations. Iterate with history, references, node branches, multimode batches, and Canvas Mode cleanup — now with one-click GPT background transparency (server-verified real alpha), annotation hover feedback, and a full light/dark/system theme.
|
|
20
20
|
|
|
21
21
|

|
|
22
22
|
|
|
@@ -107,6 +107,9 @@ Ctrl+C now performs a clean shutdown — closing the database, stopping child pr
|
|
|
107
107
|
- **Video generation**: create short videos from text, a single image, or multiple reference images via Grok video models. SSE streaming shows planning → submitted → progress % → done. Video frame copy buttons (First/Mid/Last) let you extract and copy keyframes from generated videos.
|
|
108
108
|
- **Storyboard mode**: toggle storyboard mode in the composer to maintain character and scene continuity across sequential frames. Works with both image and video generation — image keyframes are composed for video production, and video clips inherit character/environment lock rules.
|
|
109
109
|
- **Canvas Mode**: zoom, pan, annotate (with hover highlighting), erase, clean backgrounds, keep transparent previews, and export either alpha or matte-backed versions. A one-click **GPT transparency** button sends the current image through the i2i edit lane and reports honestly whether the result carries real pixel alpha — verified on the server, never trusted from provider metadata.
|
|
110
|
+
- **Raster-to-vector SVG**: trace flat raster art into real SVG paths with `ima2 vectorize`, from AssetGen/Assets, or from Canvas Export. Canvas labels its older self-contained wrapper as **SVG (embedded raster)** so it cannot be mistaken for a trace.
|
|
111
|
+
- **NovelAI dual prompt**: when NovelAI is selected, Classic, Home, and the mobile compose sheet show **Positive prompt** and **Undesired content** as peer panes; they stack below a 719px composer container. Other providers keep the normal single prompt.
|
|
112
|
+
- **Prompt Builder backend choice**: Settings > Providers can keep Builder routing on **Auto** or pin a supported text backend and model, and the **via <backend>** badge shows which backend actually answered.
|
|
110
113
|
- **Light & dark themes**: a token-based light mode with tinted neutrals and AA contrast, switchable between light / dark / system in Settings, with no flash on load.
|
|
111
114
|
- **Local gallery**: keep generated assets on your machine with session-aware history. By default the gallery shows the current session and an All Images toggle reveals the full history; the default scope is sticky across sessions. Each image records its generation time and reasoning effort in the result metadata, so they persist across reloads.
|
|
112
115
|
- **Reference images**: drag, drop, paste, and attach up to 5 references (images) or up to 7 references (video); large images are compressed before upload.
|
|
@@ -172,9 +175,9 @@ Grok video generation defaults to canonical `grok-imagine-video-1.5`; `grok-imag
|
|
|
172
175
|
|
|
173
176
|
## Model Guidance
|
|
174
177
|
|
|
175
|
-
|
|
178
|
+
Image generation defaults to **`gpt-5.6-luna`** on the GPT lane. Prompt Builder backend selection is separate: **Auto** chooses the first ready supported text backend, while Settings can pin one explicitly. The `via <backend>` badge reports the backend that actually answered.
|
|
176
179
|
|
|
177
|
-
- `gpt-5.6-luna` — current image and
|
|
180
|
+
- `gpt-5.6-luna` — current default image model on the GPT lane and the default GPT Builder model when that backend is selected.
|
|
178
181
|
- `gpt-5.6-terra` / `gpt-5.6-sol` — current GPT-5.6 alternatives when your account exposes them.
|
|
179
182
|
- `gpt-5.5`, `gpt-5.4`, `gpt-5.4-mini` — supported compatibility choices.
|
|
180
183
|
|
|
@@ -187,6 +190,7 @@ The app also exposes quality (`low`, `medium`, `high`) and moderation (`auto`, `
|
|
|
187
190
|
Use Classic when you want one strong result quickly.
|
|
188
191
|
|
|
189
192
|
1. Write a prompt.
|
|
193
|
+
With NovelAI, use **Positive prompt** for desired content and **Undesired content** for what the image should avoid.
|
|
190
194
|
2. Attach or paste references if needed.
|
|
191
195
|
3. Pick model, quality, size, format, and moderation.
|
|
192
196
|
4. Generate one image, or enable multimode to fan out several candidate slots from the same prompt.
|
|
@@ -214,6 +218,7 @@ Use Canvas Mode when a generated image is close but needs targeted cleanup befor
|
|
|
214
218
|
- Use annotation, eraser, multiselect, grouping, undo/redo, and sticky notes while keeping the original gallery image available.
|
|
215
219
|
- Pick background-cleanup seeds, preview the mask, and save the cleanup as a canvas version.
|
|
216
220
|
- Detect transparent images and show a checkerboard preview; export with preserved alpha or with a chosen matte color.
|
|
221
|
+
- Choose **SVG (embedded raster)** for a self-contained canvas document, or **Trace to SVG (vector)** to flatten the composition and open the shared real-vector tracing panel.
|
|
217
222
|
- Saved canvas versions stay hidden from Gallery and HistoryStrip, but Canvas Mode can reuse them and attach a canvas version as the next reference.
|
|
218
223
|
- Hover any annotation with the select tool to see a live outline and a move cursor before you click.
|
|
219
224
|
- Press the **GPT transparency** button in the canvas toolbar to remove the background through the OAuth i2i lane in one click. The server decodes the returned bytes and reports `alphaVerified` — the toast tells you whether real transparency actually landed, instead of assuming.
|
|
@@ -237,6 +242,7 @@ be treated as a stable public feature yet.
|
|
|
237
242
|
### Settings
|
|
238
243
|
|
|
239
244
|
The settings workspace keeps account, model, appearance, and language controls away from the generation sidebar.
|
|
245
|
+
Prompt Builder backend lives under Providers: Auto tries GPT OAuth, Grok, OpenAI API, then Grok API and uses the first ready lane. An explicit choice stays pinned and returns a typed error when unavailable; the Builder surface displays the backend that actually answered.
|
|
240
246
|
Appearance now includes a light / dark / system theme toggle; the light palette uses tinted neutrals with AA contrast and applies before first paint.
|
|
241
247
|
|
|
242
248
|

|
|
@@ -270,6 +276,8 @@ These require a running `ima2 serve`. The CLI covers every server route. The mos
|
|
|
270
276
|
| `ima2 defaults reset image\|video` | Remove a persisted CLI generation target |
|
|
271
277
|
| `ima2 gen <prompt> [--model <lane>/<model>]` | Generate from the CLI; requires an explicit target or saved image default |
|
|
272
278
|
| `ima2 edit <file> --prompt <text>` | Edit an existing image |
|
|
279
|
+
| `ima2 vectorize <input.png> [-o output.svg]` | Trace PNG/JPEG/WebP into a real SVG locally; no server or provider required |
|
|
280
|
+
| `ima2 prompt build --message <text> [--backend <backend>] [--model <model>]` | Refine prompt intent through the configured or explicitly selected Prompt Builder backend; requires the local server |
|
|
273
281
|
| `ima2 multimode <prompt>` | Multi-image SSE generation |
|
|
274
282
|
| `ima2 video <prompt> [--model <lane>/<model>]` | Generate video through a Grok or MCP lane; requires an explicit target or saved video default |
|
|
275
283
|
| `ima2 ls [--session <id>] [--favorites]` | List recent history |
|
|
@@ -284,6 +292,9 @@ The server advertises its actual port at `~/.ima2/server.json`. If `3333` is bus
|
|
|
284
292
|
```bash
|
|
285
293
|
ima2 models --kind image
|
|
286
294
|
ima2 gen "poster" --model oauth/gpt-5.6-luna --reasoning-effort high
|
|
295
|
+
ima2 gen "1girl, blue hair" --model nai/nai-diffusion-5-full --nai-negative-prompt "lowres, watermark"
|
|
296
|
+
ima2 vectorize logo.png -o logo.svg --json
|
|
297
|
+
ima2 prompt build --message "Make this prompt production-ready" --backend auto --model auto
|
|
287
298
|
ima2 edit input.png --prompt "make it rainy" --web-search
|
|
288
299
|
ima2 multimode "two cats playing" -n 2
|
|
289
300
|
ima2 video "a cat playing piano" --model grok/grok-imagine-video-1.5 --duration 5 --resolution 720p
|
|
@@ -312,6 +323,8 @@ environment variables > ~/.ima2/config.json > built-in defaults
|
|
|
312
323
|
| `IMA2_ADVERTISE_FILE` | `~/.ima2/server.json` | Runtime discovery file |
|
|
313
324
|
| `IMA2_GENERATED_DIR` | `~/.ima2/generated` | Generated image directory |
|
|
314
325
|
| `IMA2_IMAGE_MODEL_DEFAULT` | `gpt-5.6-luna` | Server fallback image model |
|
|
326
|
+
| `IMA2_PROMPT_BUILDER_BACKEND` | `auto` | Prompt Builder text backend (`auto`, `oauth`, `grok`, `api`, or `grok-api`); Settings persists the same value as `promptBuilder.backend` |
|
|
327
|
+
| `IMA2_PROMPT_BUILDER_MODEL` | `auto` with Auto backend | Backend-scoped Builder model; Settings persists the same value as `promptBuilder.model` |
|
|
315
328
|
| `IMA2_REASONING_EFFORT` | `medium` | Default reasoning effort for the default (GPT OAuth) path; one of `none`, `low`, `medium`, `high`, `xhigh` |
|
|
316
329
|
| `IMA2_NO_OAUTH_PROXY` | — | Set `1` to disable the auto-started OAuth proxy |
|
|
317
330
|
| `IMA2_LOG_LEVEL` | `info` | Normal serve defaults to `info`; dev mode defaults to `debug`; supports `debug`, `info`, `warn`, `error`, or `silent` |
|
|
@@ -351,6 +364,7 @@ Use `ima2 serve --dev`, `npm run dev`, or `IMA2_LOG_LEVEL=debug ima2 serve` when
|
|
|
351
364
|
## API Reference
|
|
352
365
|
|
|
353
366
|
The endpoint list moved to [docs/API.md](docs/API.md) so this README can stay focused on first-run use.
|
|
367
|
+
The relevant feature groups are `POST /api/assets/derived` with `kind=vector-svg`, NovelAI's `negativePrompt` generation field, `POST /api/prompt-builder/chat`, and `GET`/`PUT /api/prompt-builder/config`.
|
|
354
368
|
|
|
355
369
|
Useful references:
|
|
356
370
|
|
|
@@ -12,7 +12,8 @@ const BUILD_HELP = `
|
|
|
12
12
|
--message <text> User message (required unless --messages)
|
|
13
13
|
--messages <file|@file|-> Multi-turn conversation as JSON array
|
|
14
14
|
--ref <image> Image reference (repeatable)
|
|
15
|
-
--
|
|
15
|
+
--backend <backend> Backend override (auto, oauth, api, grok, grok-api)
|
|
16
|
+
--model <model> Model for the configured Prompt Builder backend
|
|
16
17
|
--language <ko|en|both> Preferred output language hint
|
|
17
18
|
--server <url> Override server URL
|
|
18
19
|
--json Output raw JSON
|
|
@@ -27,6 +28,7 @@ const FLAGS = {
|
|
|
27
28
|
message: { type: "string" },
|
|
28
29
|
messages: { type: "string" },
|
|
29
30
|
ref: { type: "string", repeatable: true },
|
|
31
|
+
backend: { type: "string" },
|
|
30
32
|
model: { type: "string" },
|
|
31
33
|
language: { type: "string" },
|
|
32
34
|
server: { type: "string" },
|
|
@@ -65,6 +67,8 @@ export default async function buildSub(argv) {
|
|
|
65
67
|
}
|
|
66
68
|
const messages = await resolveMessages(args);
|
|
67
69
|
const body = { messages };
|
|
70
|
+
if (args.backend)
|
|
71
|
+
body.backend = args.backend;
|
|
68
72
|
if (args.model)
|
|
69
73
|
body.model = args.model;
|
|
70
74
|
let server;
|
package/config.js
CHANGED
|
@@ -15,6 +15,7 @@ import { join, dirname } from "node:path";
|
|
|
15
15
|
import { fileURLToPath } from "node:url";
|
|
16
16
|
import { readFileSync, existsSync } from "node:fs";
|
|
17
17
|
import { deriveSupportedImageModels, deriveUnsupportedImageModels } from "./lib/providers/derive.js";
|
|
18
|
+
import { DEFAULT_PROMPT_BUILDER_MODELS, PROMPT_BUILDER_BACKENDS, PROMPT_BUILDER_MODELS, } from "./lib/promptBuilder/constants.js";
|
|
18
19
|
// 4.6 rewrites prompts in ways that read worse than 4.3 for this planner's job, which
|
|
19
20
|
// is judged by the result rather than a benchmark. 4.6 stays selectable below.
|
|
20
21
|
export const DEFAULT_GROK_PLANNER_MODEL = "grok-4.3";
|
|
@@ -80,6 +81,13 @@ function pickBool(envVal, fileVal, fallback) {
|
|
|
80
81
|
const s = String(v).toLowerCase();
|
|
81
82
|
return s === "1" || s === "true" || s === "yes";
|
|
82
83
|
}
|
|
84
|
+
function promptBuilderBackend(raw) {
|
|
85
|
+
return PROMPT_BUILDER_BACKENDS.includes(raw)
|
|
86
|
+
? raw
|
|
87
|
+
: "auto";
|
|
88
|
+
}
|
|
89
|
+
const selectedPromptBuilderBackend = promptBuilderBackend(pickStr(env.IMA2_PROMPT_BUILDER_BACKEND, fileCfg.promptBuilder?.backend, "auto"));
|
|
90
|
+
const selectedPromptBuilderModel = pickStr(env.IMA2_PROMPT_BUILDER_MODEL, fileCfg.promptBuilder?.model, DEFAULT_PROMPT_BUILDER_MODELS[selectedPromptBuilderBackend]);
|
|
83
91
|
export function defaultLogLevelForEnv(runtimeEnv = env) {
|
|
84
92
|
return runtimeEnv.IMA2_DEV === "1" ? "debug" : "info";
|
|
85
93
|
}
|
|
@@ -135,6 +143,12 @@ export const config = {
|
|
|
135
143
|
? fileCfg.oauth.validModeration
|
|
136
144
|
: ["auto", "low"]),
|
|
137
145
|
},
|
|
146
|
+
promptBuilder: {
|
|
147
|
+
backend: selectedPromptBuilderBackend,
|
|
148
|
+
model: PROMPT_BUILDER_MODELS[selectedPromptBuilderBackend].includes(selectedPromptBuilderModel)
|
|
149
|
+
? selectedPromptBuilderModel
|
|
150
|
+
: DEFAULT_PROMPT_BUILDER_MODELS[selectedPromptBuilderBackend],
|
|
151
|
+
},
|
|
138
152
|
github: {
|
|
139
153
|
token: pickStr(env.IMA2_GITHUB_TOKEN, fileCfg.github?.token, ""),
|
|
140
154
|
},
|
package/docs/API.md
CHANGED
|
@@ -810,6 +810,26 @@ Agent Mode is a conversational image workspace (web UI only — no CLI). All rou
|
|
|
810
810
|
| `POST` | `/api/agent/queue/:itemId/cancel` | Cancel queued item |
|
|
811
811
|
| `POST` | `/api/agent/queue/:itemId/retry` | Retry failed item |
|
|
812
812
|
|
|
813
|
+
## Prompt Builder
|
|
814
|
+
|
|
815
|
+
| Method | Path | Body | Response |
|
|
816
|
+
|---|---|---|---|
|
|
817
|
+
| `POST` | `/api/prompt-builder/chat` | `{ messages, backend?, model?, context? }` | `{ provider, backend, requestedBackend, model, message, usage }` |
|
|
818
|
+
| `GET` | `/api/prompt-builder/config` | none | `{ backend, model, options: { backends, models, autoOrder }, locked }` |
|
|
819
|
+
| `PUT` | `/api/prompt-builder/config` | `{ backend, model? }` | Same config payload after atomic persistence |
|
|
820
|
+
|
|
821
|
+
The persisted keys are `promptBuilder.backend` and `promptBuilder.model`; the corresponding
|
|
822
|
+
environment locks are `IMA2_PROMPT_BUILDER_BACKEND` and `IMA2_PROMPT_BUILDER_MODEL`.
|
|
823
|
+
`backend=auto` tries `oauth -> grok -> api -> grok-api` and selects the first ready lane.
|
|
824
|
+
An explicit backend stays pinned and never falls back. Chat responses separate
|
|
825
|
+
`requestedBackend` from the answering `backend`, which the UI renders as `via <backend>`.
|
|
826
|
+
|
|
827
|
+
Invalid backend/model pairs return typed 400 errors. An unavailable explicit lane returns its
|
|
828
|
+
typed 401/503 error, no ready Auto lane returns 503 `PROMPT_BUILDER_NO_BACKEND_READY`, and
|
|
829
|
+
environment-locked config writes return 409 `PROMPT_BUILDER_CONFIG_ENV_LOCKED`. Auto selection
|
|
830
|
+
happens before sending upstream; an accepted or failed upstream request is not retried on another
|
|
831
|
+
backend.
|
|
832
|
+
|
|
813
833
|
## Endpoint → CLI Mapping
|
|
814
834
|
|
|
815
835
|
Most server routes under `/api/*` have a CLI wrapper. The exception is **Agent Mode** (`/api/agent/*`), which is server + web-UI-only and has no `ima2` subcommand. The prompt builder HTTP route (`POST /api/prompt-builder/chat`) is wrapped by `ima2 prompt build`. Use this table to find the command that calls a given endpoint. (See README.md "Client" section for full flag lists.)
|
|
@@ -862,6 +882,7 @@ Most server routes under `/api/*` have a CLI wrapper. The exception is **Agent M
|
|
|
862
882
|
| `GET /api/keys/status`, `PUT/DELETE /api/keys/:provider`, `PUT/DELETE /api/keys/vertex` | Web UI only (Settings > API Keys) |
|
|
863
883
|
| `GET/POST/PATCH/DELETE /api/agent/*` (sessions, turns, queue) | — (Agent Mode; web UI only, no CLI) |
|
|
864
884
|
| `POST /api/prompt-builder/chat` | `ima2 prompt build` |
|
|
885
|
+
| `GET/PUT /api/prompt-builder/config` | Web UI only (Settings > Providers > Prompt Builder backend / Builder model) |
|
|
865
886
|
|
|
866
887
|
Notes:
|
|
867
888
|
- `ima2 history favorite` and `ima2 annotate …` send `X-Ima2-Browser-Id: cli-<sha1prefix>` derived from the config dir, so CLI activity does not collide with browser sessions.
|
package/docs/CLI.md
CHANGED
|
@@ -365,6 +365,13 @@ Windows DNS/fragmentation bypass tool such as SecretDNS is in use.
|
|
|
365
365
|
| `ima2 prompt build --message <text> [--ref <file>] [--model <id>] [--json]` | Build a structured image prompt through `/api/prompt-builder/chat` |
|
|
366
366
|
| `ima2 prompt build --messages <file\|@file\|-> [--json]` | Build from a message transcript file or stdin |
|
|
367
367
|
|
|
368
|
+
Prompt Builder uses the server's persisted `promptBuilder.backend` preference unless
|
|
369
|
+
`--backend <auto|oauth|api|grok|grok-api>` overrides it for one request. `auto`
|
|
370
|
+
chooses the first ready supported text backend (oauth, grok, api, grok-api); an explicit
|
|
371
|
+
value pins routing and fails with a typed error instead of falling back. The result
|
|
372
|
+
identifies `requestedBackend`, the answering `backend`, and `model`, so callers can
|
|
373
|
+
detect an Auto fallback.
|
|
374
|
+
|
|
368
375
|
## Card News (gated)
|
|
369
376
|
|
|
370
377
|
Card News requires the server to be started with `IMA2_CARD_NEWS=1` (or `features.cardNews: true` in `~/.ima2/config.json`). When disabled, the CLI exits 2 with a clear message instead of producing a 404.
|
package/docs/PROMPT_STUDIO.md
CHANGED
|
@@ -21,6 +21,15 @@ you want a reproducible way to report a workspace issue.
|
|
|
21
21
|
| Gallery | Browses saved local images, All/Favorites tabs, and folders. | Favorite toggles should preserve the gallery viewport you were browsing. |
|
|
22
22
|
| Prompt library | Imports saved prompt text into the composer intentionally. | Library insert/continue actions are explicit prompt imports; passive image selection is not. |
|
|
23
23
|
|
|
24
|
+
### Provider-specific prompt surfaces
|
|
25
|
+
|
|
26
|
+
NovelAI shows Positive prompt and Undesired content as separate panes in Classic, Home,
|
|
27
|
+
and the mobile compose sheet (side by side when the composer is wider than 719px,
|
|
28
|
+
stacked otherwise). Switching away from NovelAI restores the single prompt without
|
|
29
|
+
deleting the saved undesired-content text. Prompt Builder routing is configured in
|
|
30
|
+
Settings > Providers; the Builder badge shows the backend that actually answered,
|
|
31
|
+
including an Auto selection or fallback.
|
|
32
|
+
|
|
24
33
|
## Multimode Prompting
|
|
25
34
|
|
|
26
35
|
Multimode repeats one generation request shape across several candidate slots.
|
|
@@ -4,7 +4,7 @@ Generated by `npm run test:inventory` (script: `scripts/classify-tests.mjs`).
|
|
|
4
4
|
|
|
5
5
|
_Tests considered "runtime-importing" if they import from `../lib/`, `../routes/`, `../bin/`, `../server`, or `../config`._
|
|
6
6
|
|
|
7
|
-
Total:
|
|
7
|
+
Total: 415 (runtime: 191, contract: 224)
|
|
8
8
|
|
|
9
9
|
## Runtime-importing tests
|
|
10
10
|
- `tests/agent-image-reference-contract.test.ts`
|
|
@@ -251,6 +251,7 @@ Total: 412 (runtime: 191, contract: 221)
|
|
|
251
251
|
- `tests/canvas-selection-contract.test.js`
|
|
252
252
|
- `tests/canvas-style-contract.test.js`
|
|
253
253
|
- `tests/canvas-svg-export-contract.test.ts`
|
|
254
|
+
- `tests/canvas-vectorize-entry-contract.test.ts`
|
|
254
255
|
- `tests/canvas-version-api.test.js`
|
|
255
256
|
- `tests/canvas-version-contract.test.js`
|
|
256
257
|
- `tests/canvas-viewport-pan-contract.test.js`
|
|
@@ -337,6 +338,7 @@ Total: 412 (runtime: 191, contract: 221)
|
|
|
337
338
|
- `tests/multimode-concurrent-store-contract.test.js`
|
|
338
339
|
- `tests/multimode-ui-contract.test.js`
|
|
339
340
|
- `tests/nai-cli-built-smoke.test.ts`
|
|
341
|
+
- `tests/nai-dual-prompt-contract.test.ts`
|
|
340
342
|
- `tests/nai-ui-registration-contract.test.ts`
|
|
341
343
|
- `tests/navrail-hover-label-contract.test.ts`
|
|
342
344
|
- `tests/node-async-eventbus-contract.test.js`
|
|
@@ -397,6 +399,7 @@ Total: 412 (runtime: 191, contract: 221)
|
|
|
397
399
|
- `tests/star-surface-controls-contract.test.ts`
|
|
398
400
|
- `tests/storage-open-generated-dir.test.js`
|
|
399
401
|
- `tests/structure-line-counts-contract.test.js`
|
|
402
|
+
- `tests/studio-surface-docs-contract.test.ts`
|
|
400
403
|
- `tests/style-feature-removal-contract.test.js`
|
|
401
404
|
- `tests/subprocess-deadline-contract.test.ts`
|
|
402
405
|
- `tests/toast-stack-contract.test.js`
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
let configMutationQueue = Promise.resolve();
|
|
5
|
+
function serializeConfigMutation(mutation) {
|
|
6
|
+
const result = configMutationQueue.then(mutation, mutation);
|
|
7
|
+
configMutationQueue = result.then(() => undefined, () => undefined);
|
|
8
|
+
return result;
|
|
9
|
+
}
|
|
10
|
+
async function writeConfigAtomic(cfgPath, data) {
|
|
11
|
+
const tmp = `${cfgPath}.${process.pid}.${randomBytes(8).toString("hex")}.tmp`;
|
|
12
|
+
try {
|
|
13
|
+
await mkdir(dirname(cfgPath), { recursive: true, mode: 0o700 });
|
|
14
|
+
await writeFile(tmp, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
15
|
+
await rename(tmp, cfgPath);
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export async function updateConfigFileAtomic(cfgPath, mutate) {
|
|
22
|
+
try {
|
|
23
|
+
await serializeConfigMutation(async () => {
|
|
24
|
+
let existing = {};
|
|
25
|
+
try {
|
|
26
|
+
existing = JSON.parse(await readFile(cfgPath, "utf8"));
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
const code = error.code;
|
|
30
|
+
if (code !== "ENOENT") {
|
|
31
|
+
throw Object.assign(new Error(`config file unreadable: ${cfgPath}`), {
|
|
32
|
+
code: "CONFIG_UNREADABLE",
|
|
33
|
+
cause: error,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
mutate(existing);
|
|
38
|
+
await writeConfigAtomic(cfgPath, existing);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
}
|
package/lib/configKeys.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export const AUTH_CONFIG_KEYS = new Set(["provider", "apiKey"]);
|
|
2
2
|
export const WRITABLE_CONFIG_KEYS = new Set([
|
|
3
|
+
"promptBuilder.backend",
|
|
4
|
+
"promptBuilder.model",
|
|
3
5
|
"imageModels.default",
|
|
4
6
|
"imageModels.reasoningEffort",
|
|
5
7
|
"apiProvider.defaultImageModel",
|
|
@@ -37,6 +39,8 @@ export const WRITABLE_CONFIG_KEYS = new Set([
|
|
|
37
39
|
"history.maxPageCap",
|
|
38
40
|
]);
|
|
39
41
|
export const KEY_TO_ENV = {
|
|
42
|
+
"promptBuilder.backend": "IMA2_PROMPT_BUILDER_BACKEND",
|
|
43
|
+
"promptBuilder.model": "IMA2_PROMPT_BUILDER_MODEL",
|
|
40
44
|
"imageModels.default": "IMA2_IMAGE_MODEL_DEFAULT",
|
|
41
45
|
"imageModels.reasoningEffort": "IMA2_REASONING_EFFORT",
|
|
42
46
|
"apiProvider.defaultImageModel": "IMA2_API_IMAGE_MODEL_DEFAULT",
|
|
@@ -1,76 +1,118 @@
|
|
|
1
1
|
import { errInfo } from "../errInfo.js";
|
|
2
|
-
import { logWarn } from "../logger.js";
|
|
3
|
-
import { fetchOAuth
|
|
2
|
+
import { logEvent, logWarn } from "../logger.js";
|
|
3
|
+
import { fetchOAuth } from "../oauthProxy/runtime.js";
|
|
4
|
+
import { requireRuntimeContext, } from "../runtimeContext.js";
|
|
5
|
+
import { DEFAULT_PROMPT_BUILDER_MODELS, PROMPT_BUILDER_AUTO_ORDER, } from "./constants.js";
|
|
4
6
|
import { promptBuilderError } from "./errors.js";
|
|
5
|
-
import {
|
|
7
|
+
import { lanesForModel, normalizeMessages, normalizePromptBuilderBackend, normalizeRequestModel, } from "./requestSchema.js";
|
|
8
|
+
import { resolvePromptBuilderTransport, selectPromptBuilderBackend, } from "./router.js";
|
|
6
9
|
import { buildTransportPayload } from "./transport.js";
|
|
7
10
|
import { parseUpstreamError, responseSummary, extractChatText, readResponsesResult, } from "./responseParser.js";
|
|
8
|
-
|
|
9
|
-
const
|
|
11
|
+
async function prepareRequest(ctx, input, lanes) {
|
|
12
|
+
const persistedBackend = normalizePromptBuilderBackend(ctx.config.promptBuilder.backend);
|
|
13
|
+
const requestedBackend = normalizePromptBuilderBackend(input.backend, persistedBackend);
|
|
14
|
+
const backendOverridden = requestedBackend !== persistedBackend;
|
|
15
|
+
const requestedModel = normalizeRequestModel(requestedBackend, input.model ?? (backendOverridden
|
|
16
|
+
? DEFAULT_PROMPT_BUILDER_MODELS[requestedBackend]
|
|
17
|
+
: ctx.config.promptBuilder.model));
|
|
18
|
+
const allowedLanes = requestedBackend === "auto" && requestedModel !== "auto"
|
|
19
|
+
? lanesForModel(requestedModel)
|
|
20
|
+
: PROMPT_BUILDER_AUTO_ORDER;
|
|
21
|
+
const selection = selectPromptBuilderBackend(requestedBackend, lanes, allowedLanes);
|
|
22
|
+
const model = requestedBackend === "auto"
|
|
23
|
+
? (requestedModel === "auto" ? DEFAULT_PROMPT_BUILDER_MODELS[selection.backend] : requestedModel)
|
|
24
|
+
: requestedModel;
|
|
10
25
|
const messages = normalizeMessages(input.messages);
|
|
11
|
-
|
|
12
|
-
|
|
26
|
+
if (selection.fallbackFrom)
|
|
27
|
+
logFallback(selection);
|
|
28
|
+
const payload = buildTransportPayload(selection.backend, model, messages, input.context);
|
|
29
|
+
const target = await resolvePromptBuilderTransport(ctx, selection.backend, payload.endpoint);
|
|
30
|
+
return { selection, model, payload, target };
|
|
31
|
+
}
|
|
32
|
+
function logFallback(selection) {
|
|
33
|
+
logEvent("prompt-builder", "backend_fallback", {
|
|
34
|
+
requestedBackend: selection.requestedBackend,
|
|
35
|
+
from: selection.fallbackFrom,
|
|
36
|
+
to: selection.backend,
|
|
37
|
+
reason: selection.fallbackReason,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
function sendUpstream(prepared, signal) {
|
|
41
|
+
const init = {
|
|
42
|
+
method: "POST",
|
|
43
|
+
headers: prepared.target.headers,
|
|
44
|
+
signal,
|
|
45
|
+
body: JSON.stringify(prepared.payload.body),
|
|
46
|
+
};
|
|
47
|
+
return prepared.target.useOAuthFetch
|
|
48
|
+
? fetchOAuth(prepared.target.url, init, { scope: "prompt-builder" })
|
|
49
|
+
: fetch(prepared.target.url, init);
|
|
50
|
+
}
|
|
51
|
+
async function throwUpstreamFailure(res, prepared) {
|
|
52
|
+
const text = await res.text();
|
|
53
|
+
const upstream = parseUpstreamError(text);
|
|
54
|
+
logWarn("prompt-builder", "upstream_failed", {
|
|
55
|
+
endpoint: prepared.payload.endpoint,
|
|
56
|
+
model: prepared.model,
|
|
57
|
+
status: res.status,
|
|
58
|
+
hasImageAttachments: prepared.payload.endpoint === "responses",
|
|
59
|
+
upstreamBodyChars: text.length,
|
|
60
|
+
upstreamCode: upstream.upstreamCode,
|
|
61
|
+
upstreamType: upstream.upstreamType,
|
|
62
|
+
upstreamParam: upstream.upstreamParam,
|
|
63
|
+
});
|
|
64
|
+
const err = promptBuilderError("Prompt builder upstream failed", "PROMPT_BUILDER_UPSTREAM_FAILED", 502);
|
|
65
|
+
Object.assign(err, {
|
|
66
|
+
upstreamStatus: res.status,
|
|
67
|
+
upstreamBodyChars: text.length,
|
|
68
|
+
upstreamEndpoint: prepared.payload.endpoint,
|
|
69
|
+
...upstream,
|
|
70
|
+
});
|
|
71
|
+
throw err;
|
|
72
|
+
}
|
|
73
|
+
async function parseResponse(res, endpoint) {
|
|
74
|
+
if (endpoint === "responses") {
|
|
75
|
+
return readResponsesResult(res);
|
|
76
|
+
}
|
|
77
|
+
const body = (await res.json());
|
|
78
|
+
return {
|
|
79
|
+
content: extractChatText(body),
|
|
80
|
+
usage: body.usage ?? null,
|
|
81
|
+
summary: responseSummary(body),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function requireContent(parsed, prepared) {
|
|
85
|
+
const content = parsed.content.trim();
|
|
86
|
+
if (content)
|
|
87
|
+
return content;
|
|
88
|
+
logWarn("prompt-builder", "empty_response", {
|
|
89
|
+
endpoint: prepared.payload.endpoint,
|
|
90
|
+
model: prepared.model,
|
|
91
|
+
...parsed.summary,
|
|
92
|
+
});
|
|
93
|
+
const err = promptBuilderError("Prompt builder returned an empty response", "PROMPT_BUILDER_EMPTY_RESPONSE", 502);
|
|
94
|
+
err.upstreamEndpoint = prepared.payload.endpoint;
|
|
95
|
+
Object.assign(err, parsed.summary);
|
|
96
|
+
throw err;
|
|
97
|
+
}
|
|
98
|
+
export async function requestPromptBuilderChat(ctxRaw, input, lanes) {
|
|
99
|
+
const ctx = requireRuntimeContext(ctxRaw);
|
|
100
|
+
const prepared = await prepareRequest(ctx, input, lanes);
|
|
13
101
|
const controller = new AbortController();
|
|
14
|
-
const timer = setTimeout(() => controller.abort(),
|
|
102
|
+
const timer = setTimeout(() => controller.abort(), ctx.config.oauth.generationTimeoutMs);
|
|
15
103
|
try {
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
signal: controller.signal,
|
|
22
|
-
body: JSON.stringify(payload),
|
|
23
|
-
}, { scope: "prompt-builder" });
|
|
24
|
-
if (!res.ok) {
|
|
25
|
-
const text = await res.text();
|
|
26
|
-
const upstream = parseUpstreamError(text);
|
|
27
|
-
logWarn("prompt-builder", "upstream_failed", {
|
|
28
|
-
endpoint,
|
|
29
|
-
model,
|
|
30
|
-
status: res.status,
|
|
31
|
-
hasImageAttachments: endpoint === "responses",
|
|
32
|
-
upstreamBodyChars: text.length,
|
|
33
|
-
upstreamCode: upstream.upstreamCode,
|
|
34
|
-
upstreamType: upstream.upstreamType,
|
|
35
|
-
upstreamParam: upstream.upstreamParam,
|
|
36
|
-
});
|
|
37
|
-
const err = promptBuilderError("Prompt builder upstream failed", "PROMPT_BUILDER_UPSTREAM_FAILED", 502);
|
|
38
|
-
err.upstreamStatus = res.status;
|
|
39
|
-
err.upstreamBodyChars = text.length;
|
|
40
|
-
err.upstreamEndpoint = endpoint;
|
|
41
|
-
err.upstreamCode = upstream.upstreamCode;
|
|
42
|
-
err.upstreamType = upstream.upstreamType;
|
|
43
|
-
err.upstreamParam = upstream.upstreamParam;
|
|
44
|
-
throw err;
|
|
45
|
-
}
|
|
46
|
-
const useResponses = endpoint === "responses";
|
|
47
|
-
const responseBody = useResponses
|
|
48
|
-
? await readResponsesResult(res)
|
|
49
|
-
: (await res.json());
|
|
50
|
-
const content = (useResponses
|
|
51
|
-
? responseBody.content
|
|
52
|
-
: extractChatText(responseBody)).trim();
|
|
53
|
-
if (!content) {
|
|
54
|
-
const summary = useResponses
|
|
55
|
-
? responseBody.summary
|
|
56
|
-
: responseSummary(responseBody);
|
|
57
|
-
logWarn("prompt-builder", "empty_response", {
|
|
58
|
-
endpoint,
|
|
59
|
-
model,
|
|
60
|
-
...summary,
|
|
61
|
-
});
|
|
62
|
-
const err = promptBuilderError("Prompt builder returned an empty response", "PROMPT_BUILDER_EMPTY_RESPONSE", 502);
|
|
63
|
-
err.upstreamEndpoint = endpoint;
|
|
64
|
-
Object.assign(err, summary);
|
|
65
|
-
throw err;
|
|
66
|
-
}
|
|
104
|
+
const res = await sendUpstream(prepared, controller.signal);
|
|
105
|
+
if (!res.ok)
|
|
106
|
+
await throwUpstreamFailure(res, prepared);
|
|
107
|
+
const parsed = await parseResponse(res, prepared.payload.endpoint);
|
|
108
|
+
const content = requireContent(parsed, prepared);
|
|
67
109
|
return {
|
|
68
|
-
provider:
|
|
69
|
-
|
|
110
|
+
provider: prepared.selection.backend,
|
|
111
|
+
backend: prepared.selection.backend,
|
|
112
|
+
requestedBackend: prepared.selection.requestedBackend,
|
|
113
|
+
model: prepared.model,
|
|
70
114
|
message: { role: "assistant", content },
|
|
71
|
-
usage:
|
|
72
|
-
? responseBody.usage
|
|
73
|
-
: (responseBody.usage ?? null),
|
|
115
|
+
usage: parsed.usage,
|
|
74
116
|
};
|
|
75
117
|
}
|
|
76
118
|
catch (error) {
|
|
@@ -1,5 +1,23 @@
|
|
|
1
|
-
export const
|
|
2
|
-
|
|
1
|
+
export const PROMPT_BUILDER_BACKENDS = ["auto", "oauth", "grok", "api", "grok-api"];
|
|
2
|
+
const GPT_MODELS = ["gpt-5.6-luna", "gpt-5.6-terra", "gpt-5.6-sol", "gpt-5.5", "gpt-5.4", "gpt-5.4-mini"];
|
|
3
|
+
const grokModels = ["grok-4.3", "grok-4.6", "grok-4.5"];
|
|
4
|
+
export const PROMPT_BUILDER_MODELS = {
|
|
5
|
+
auto: ["auto"],
|
|
6
|
+
oauth: GPT_MODELS,
|
|
7
|
+
grok: grokModels,
|
|
8
|
+
api: GPT_MODELS,
|
|
9
|
+
"grok-api": grokModels,
|
|
10
|
+
};
|
|
11
|
+
export const DEFAULT_PROMPT_BUILDER_MODELS = {
|
|
12
|
+
auto: "auto",
|
|
13
|
+
oauth: "gpt-5.6-luna",
|
|
14
|
+
grok: "grok-4.3",
|
|
15
|
+
api: "gpt-5.6-luna",
|
|
16
|
+
"grok-api": "grok-4.3",
|
|
17
|
+
};
|
|
18
|
+
export const PROMPT_BUILDER_AUTO_ORDER = [
|
|
19
|
+
"oauth", "grok", "api", "grok-api",
|
|
20
|
+
];
|
|
3
21
|
export const MAX_MESSAGES = 24;
|
|
4
22
|
export const MAX_MESSAGE_CHARS = 16_000;
|
|
5
23
|
export const MAX_ATTACHMENTS = 6;
|
|
@@ -1,13 +1,48 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { DEFAULT_PROMPT_BUILDER_MODELS, MAX_MESSAGES, MAX_MESSAGE_CHARS, PROMPT_BUILDER_AUTO_ORDER, PROMPT_BUILDER_BACKENDS, PROMPT_BUILDER_MODELS, } from "./constants.js";
|
|
2
2
|
import { promptBuilderError } from "./errors.js";
|
|
3
3
|
import { normalizeAttachments } from "./attachments.js";
|
|
4
|
-
export function
|
|
5
|
-
if (
|
|
6
|
-
return
|
|
7
|
-
if (
|
|
8
|
-
|
|
4
|
+
export function normalizePromptBuilderBackend(raw, fallback = "auto") {
|
|
5
|
+
if (raw === undefined || raw === null)
|
|
6
|
+
return fallback;
|
|
7
|
+
if (typeof raw === "string") {
|
|
8
|
+
const candidate = raw.trim();
|
|
9
|
+
if (!candidate)
|
|
10
|
+
return fallback;
|
|
11
|
+
if (PROMPT_BUILDER_BACKENDS.includes(candidate)) {
|
|
12
|
+
return candidate;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
throw promptBuilderError(`backend must be one of: ${PROMPT_BUILDER_BACKENDS.join(", ")}`, "PROMPT_BUILDER_BAD_BACKEND");
|
|
16
|
+
}
|
|
17
|
+
export function normalizePromptBuilderModel(backend, raw) {
|
|
18
|
+
const candidate = typeof raw === "string" && raw.trim()
|
|
19
|
+
? raw.trim()
|
|
20
|
+
: DEFAULT_PROMPT_BUILDER_MODELS[backend];
|
|
21
|
+
if (!PROMPT_BUILDER_MODELS[backend].includes(candidate)) {
|
|
22
|
+
throw promptBuilderError(`model for ${backend} must be one of: ${PROMPT_BUILDER_MODELS[backend].join(", ")}`, "PROMPT_BUILDER_BAD_MODEL");
|
|
23
|
+
}
|
|
24
|
+
return candidate;
|
|
25
|
+
}
|
|
26
|
+
export function normalizeRequestModel(backend, raw) {
|
|
27
|
+
const candidate = typeof raw === "string" && raw.trim() ? raw.trim() : "";
|
|
28
|
+
if (backend === "auto" && candidate && candidate !== "auto") {
|
|
29
|
+
if (!lanesForModel(candidate).length) {
|
|
30
|
+
throw promptBuilderError(`model ${candidate} is not in any Prompt Builder catalog`, "PROMPT_BUILDER_BAD_MODEL");
|
|
31
|
+
}
|
|
32
|
+
return candidate;
|
|
9
33
|
}
|
|
10
|
-
return
|
|
34
|
+
return normalizePromptBuilderModel(backend, candidate || undefined);
|
|
35
|
+
}
|
|
36
|
+
export function lanesForModel(model) {
|
|
37
|
+
return PROMPT_BUILDER_AUTO_ORDER.filter((lane) => PROMPT_BUILDER_MODELS[lane].includes(model));
|
|
38
|
+
}
|
|
39
|
+
export function normalizePromptBuilderConfig(raw, current) {
|
|
40
|
+
const body = raw && typeof raw === "object" ? raw : {};
|
|
41
|
+
const backend = normalizePromptBuilderBackend(body.backend, current.backend);
|
|
42
|
+
const modelInput = body.model === undefined && backend !== current.backend
|
|
43
|
+
? DEFAULT_PROMPT_BUILDER_MODELS[backend]
|
|
44
|
+
: body.model ?? current.model;
|
|
45
|
+
return { backend, model: normalizePromptBuilderModel(backend, modelInput) };
|
|
11
46
|
}
|
|
12
47
|
export function normalizeMessages(raw) {
|
|
13
48
|
if (!Array.isArray(raw)) {
|