ima2-gen 3.11.0 → 3.12.1

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 (36) hide show
  1. package/README.md +3 -1
  2. package/bin/commands/gen.js +8 -2
  3. package/bin/commands/multimode.js +5 -0
  4. package/bin/commands/node.js +6 -1
  5. package/bin/lib/args.js +3 -1
  6. package/bin/lib/nai-options.js +195 -0
  7. package/config.js +2 -0
  8. package/docs/API.md +15 -1
  9. package/docs/CLI.md +41 -0
  10. package/docs/migration/runtime-test-inventory.md +4 -1
  11. package/lib/capabilities.js +2 -0
  12. package/lib/naiImageAdapter.js +2 -2
  13. package/lib/naiOptions.js +6 -0
  14. package/package.json +2 -2
  15. package/skills/ima2/SKILL.md +72 -0
  16. package/ui/dist/.vite/manifest.json +35 -35
  17. package/ui/dist/assets/{AgentWorkspace-B7Ry1zR0.js → AgentWorkspace-CKAWZD7U.js} +1 -1
  18. package/ui/dist/assets/{AssetGenWorkspace-BSl_K_-v.js → AssetGenWorkspace-wsNksO-Q.js} +2 -2
  19. package/ui/dist/assets/{AssetsWorkspace-Cqv_nI4Q.js → AssetsWorkspace-Bgk5sj1a.js} +1 -1
  20. package/ui/dist/assets/{CardNewsWorkspace-xSXavyEg.js → CardNewsWorkspace-DGOw6UH_.js} +1 -1
  21. package/ui/dist/assets/{GenerationRequestLogPanel-DuO9gtUk.js → GenerationRequestLogPanel-BGatd8qE.js} +1 -1
  22. package/ui/dist/assets/{HomeWorkspace-RCbYP5Rw.js → HomeWorkspace-CuJY4Jr6.js} +1 -1
  23. package/ui/dist/assets/{KeyingPanel-D3PLyaqp.js → KeyingPanel-DYJN_oNF.js} +1 -1
  24. package/ui/dist/assets/{NodeCanvas-CaJ4s-El.js → NodeCanvas-fil6cXN8.js} +1 -1
  25. package/ui/dist/assets/{PromptBuilderPanel-DreYEwPN.js → PromptBuilderPanel-Dt4gO_Rb.js} +1 -1
  26. package/ui/dist/assets/{PromptImportDialog-B1Rr1xX2.js → PromptImportDialog-uyJdrdTs.js} +2 -2
  27. package/ui/dist/assets/{PromptImportDiscoverySection-DYfAT0uk.js → PromptImportDiscoverySection-Bmyt_NVl.js} +1 -1
  28. package/ui/dist/assets/{PromptImportFolderSection-0eAllkEV.js → PromptImportFolderSection-DGqoTx-h.js} +1 -1
  29. package/ui/dist/assets/{PromptLibraryPanel-CPHkzEha.js → PromptLibraryPanel-BIi5_Rvq.js} +2 -2
  30. package/ui/dist/assets/{SettingsWorkspace-DM0I_1W7.js → SettingsWorkspace-CVying69.js} +1 -1
  31. package/ui/dist/assets/{SpriteRecipeWorkspace-2h020SW0.js → SpriteRecipeWorkspace-ztdiDwSf.js} +1 -1
  32. package/ui/dist/assets/{index-DhGOUKvx.js → index-CumGbxai.js} +3 -3
  33. package/ui/dist/assets/{index-6WyaBYVL.js → index-TU0_IXQH.js} +14 -14
  34. package/ui/dist/assets/{pptxgen.es-DqOBFty0.js → pptxgen.es-Cza43xx3.js} +1 -1
  35. package/ui/dist/assets/{useAgentDialogFocus-Czg5Oxus.js → useAgentDialogFocus-D741d8_6.js} +1 -1
  36. package/ui/dist/index.html +1 -1
package/README.md CHANGED
@@ -156,7 +156,7 @@ Image generation can run through the local Codex/ChatGPT OAuth path, a configure
156
156
  - `provider: "api"` calls the OpenAI Responses API with the hosted `image_generation` tool.
157
157
  - `provider: "grok"` starts bundled `progrok` on `127.0.0.1:18645`, runs mandatory xAI Web Search plus a planner pass (default: `grok-4.5`, configurable in settings or via `--planner-model`), then calls xAI Images API through the local proxy. `grok-4.3` remains available as an explicit compatibility override.
158
158
  - `provider: "grok-api"` calls the xAI Images API directly with `XAI_API_KEY` (no bundled progrok OAuth proxy).
159
- - `provider: "nai"` calls the NovelAI image API with a persistent API token (`pst-...`, saved in Settings > API Keys or `NOVELAI_API_KEY`). Four models: `nai-diffusion-5-full`, `nai-diffusion-5-curated`, `nai-diffusion-4-5-full`, `nai-diffusion-4-5-curated`. Responses arrive as a ZIP archive that ima2 decodes to PNG. Text-to-image only — reference images and edits are refused rather than silently dropped, and image generation requires an active NovelAI subscription.
159
+ - `provider: "nai"` calls the NovelAI image API with a persistent API token (saved in Settings > API Keys or `NOVELAI_API_KEY`; no fixed token prefix is required). Four models: `nai-diffusion-5-full`, `nai-diffusion-5-curated`, `nai-diffusion-4-5-full`, `nai-diffusion-4-5-curated`. Responses arrive as a ZIP archive that ima2 decodes to PNG. Text-to-image only — reference images, edits, and masks are refused rather than silently dropped. Browser and CLI surfaces expose negative prompt, sampler/schedule, steps/guidance/CFG rescale, seed, presets, Auto SMEA, Decrisper, Variety+, and V5 alpha.
160
160
  - `provider: "agy"` spawns the Antigravity CLI (`agy -p`) to generate images via Google Gemini's `default_api:generate_image` tool (model: `nano-banana-2`). Output is fixed at 1024×1024 JPEG, max 3 reference images. No web search, quality, or size controls.
161
161
  - `provider: "gemini-api"` calls the Google Generative Language API directly. Supports two models: `nano-banana-2` (Gemini 3.1 Flash Image) and `nano-banana-pro` (Gemini 3 Pro Image). Auth is via `GEMINI_API_KEY` env var, web UI key management, or a Vertex AI service account JSON (`VERTEX_SERVICE_ACCOUNT_JSON`). When both an API key and Vertex credentials are configured, Vertex takes priority. Supports variable aspect ratios (1:1 through 21:9) and four resolution tiers (512px, 1K, 2K, 4K); these controls are only honored on the direct API path — the Vertex AI endpoint ignores aspect/size because it does not accept the `response_format` field. Per-model cost differs: `nano-banana-2` (Flash): 512=$0.001, 1K=$0.003, 2K=$0.004, 4K=$0.006; `nano-banana-pro`: 1K=$0.007, 2K=$0.007, 4K=$0.013. No web search or mask controls.
162
162
  - API-key generation supports classic generate, edit, mask-guided edit, multimode, and node generation.
@@ -320,6 +320,8 @@ environment variables > ~/.ima2/config.json > built-in defaults
320
320
  | `XAI_API_KEY` | — | API key for `provider: "grok-api"` direct xAI Images API path |
321
321
  | `NOVELAI_API_KEY` | — | NovelAI persistent API token for `provider: "nai"` |
322
322
  | `IMA2_NAI_IMAGE_MODEL_DEFAULT` | `nai-diffusion-5-full` | Default NovelAI image model |
323
+ | `IMA2_NAI_DEFAULT_AUTO_SMEA` | `false` | Default NovelAI Auto SMEA state |
324
+ | `IMA2_NAI_DEFAULT_DECRISPER` | `false` | Default NovelAI Decrisper (`dynamic_thresholding`) state |
323
325
  | `IMA2_API_IMAGE_MODEL_DEFAULT` | `gpt-5.6-luna` | Default image model for `provider: "api"` |
324
326
  | `IMA2_API_REASONING_EFFORT` | `low` | Default reasoning effort for `provider: "api"` |
325
327
  | `IMA2_API_IMAGE_SIZE` | `1024x1024` | Default size for `provider: "api"` |
@@ -17,6 +17,7 @@ import { createCliRequestId, recoverGeneratedOutputs, formatRecoveryHint } from
17
17
  import { deriveProviderIds } from "../../lib/providers/derive.js";
18
18
  import { listProviders } from "../../lib/mcp/providerRegistry.js";
19
19
  import { BACKGROUND_PRESETS } from "../../lib/backgroundPresets.js";
20
+ import { NAI_CLI_FLAGS, NAI_CLI_HELP, finalizeNaiCliTarget, parseNaiCliOptions, unwrapNaiCliResult } from "../lib/nai-options.js";
20
21
  const VALID_MODES = new Set(["auto", "direct"]);
21
22
  const VALID_MODERATION = new Set(["auto", "low"]);
22
23
  const MAX_GENERATION_COUNT = Math.max(1, Math.trunc(Number(config.limits.maxGeneratedImages) || 24));
@@ -43,6 +44,7 @@ const SPEC = {
43
44
  character: { type: "string" },
44
45
  "reasoning-effort": { type: "string" }, "web-search": { type: "boolean" },
45
46
  "no-web-search": { type: "boolean" }, help: { short: "h", type: "boolean" },
47
+ ...NAI_CLI_FLAGS,
46
48
  },
47
49
  };
48
50
  const HELP = `
@@ -83,6 +85,7 @@ const HELP = `
83
85
  --reasoning-effort <none|low|medium|high|xhigh|max>
84
86
  Core lanes only
85
87
  --web-search / --no-web-search Core lanes only
88
+ ${NAI_CLI_HELP}
86
89
 
87
90
  Examples:
88
91
  ima2 defaults set image oauth/gpt-5.6-luna
@@ -90,6 +93,7 @@ const HELP = `
90
93
  ima2 gen "poster" --model oauth/luna --mode direct
91
94
  ima2 gen "fox logo mark" --bg transparent -o logo.png
92
95
  ima2 gen "campaign still" --model runway/gen-4 --ref 1780000000000_abcd.png
96
+ ima2 gen "transparent character sprite" --provider nai --model nai-diffusion-5-full --nai-straight-alpha --nai-negative-prompt "watermark"
93
97
  `;
94
98
  /**
95
99
  * Resolves --out against --out-dir.
@@ -287,7 +291,7 @@ async function requestCoreImage(args, context, n, requestId) {
287
291
  const body = { prompt: context.prompt, quality: args.quality, size: args.size, n, references,
288
292
  ...(args["no-size-nudge"] ? { sizeNudge: false } : {}),
289
293
  model: context.target.model, mode: args.mode, moderation: args.moderation, sessionId: args.session,
290
- provider: context.target.lane };
294
+ provider: context.target.lane, ...context.naiOptions };
291
295
  body.requestId = requestId;
292
296
  if (args.bg)
293
297
  body.backgroundPreset = String(args.bg);
@@ -385,6 +389,7 @@ export default async function genCmd(argv) {
385
389
  out(HELP);
386
390
  return;
387
391
  }
392
+ const naiPreflight = unwrapNaiCliResult(parseNaiCliOptions(args, "allow-unknown"), Boolean(args.json));
388
393
  let prompt = args.positional.join(" ");
389
394
  if (!prompt && !args.stdin)
390
395
  die(2, "prompt is required (positional or via --stdin)");
@@ -393,13 +398,14 @@ export default async function genCmd(argv) {
393
398
  die(2, `max ${MAX_REFERENCE_COUNT} --ref attachments`);
394
399
  const { server, catalog } = await fetchCatalog(args.server, Boolean(args.json));
395
400
  const target = resolveImageTarget(args, catalog);
401
+ const naiFinal = unwrapNaiCliResult(finalizeNaiCliTarget(naiPreflight, target), Boolean(args.json));
396
402
  if (args.character && target.transport !== "mcp") {
397
403
  fail({ json: Boolean(args.json), code: "CAPABILITY_MISMATCH",
398
404
  message: "--character is only supported on MCP lanes (runway/higgsfield); core lanes use element mentions",
399
405
  exitCode: 2 });
400
406
  }
401
407
  let context = { server, catalog, target, prompt, refs, explicitOut: args.out ? String(args.out) : null,
402
- outDir: args["out-dir"] ? String(args["out-dir"]) : null };
408
+ outDir: args["out-dir"] ? String(args["out-dir"]) : null, naiOptions: naiFinal.payload };
403
409
  if (target.transport === "mcp")
404
410
  return runMcpImage(argv, args, context);
405
411
  if (args.stdin) {
@@ -7,6 +7,7 @@ import { config } from "../../config.js";
7
7
  import { createCliRequestId, recoverGeneratedOutputs, formatRecoveryHint } from "../lib/recover-output.js";
8
8
  import { canonicalizeImageModel } from "../lib/model-aliases.js";
9
9
  import { deriveCliImageModelSet, deriveProviderIds } from "../../lib/providers/derive.js";
10
+ import { NAI_CLI_FLAGS, NAI_CLI_HELP, parseNaiCliOptions, unwrapNaiCliResult } from "../lib/nai-options.js";
10
11
  const MAX_GENERATION_COUNT = Math.max(1, Math.trunc(Number(config.limits.maxGeneratedImages) || 24));
11
12
  const MAX_REFERENCE_COUNT = Math.max(1, Math.trunc(Number(config.limits.maxRefCount) || 5));
12
13
  const PROVIDER_VALUES = ["auto", ...deriveProviderIds()];
@@ -34,6 +35,7 @@ const SPEC = {
34
35
  session: { type: "string" },
35
36
  "show-partial": { type: "boolean" },
36
37
  help: { short: "h", type: "boolean" },
38
+ ...NAI_CLI_FLAGS,
37
39
  },
38
40
  };
39
41
  const HELP = `
@@ -62,6 +64,7 @@ const HELP = `
62
64
  --show-partial Print [partial #N received] notices
63
65
  --timeout <sec> Default: 600
64
66
  --server <url> Override server URL
67
+ ${NAI_CLI_HELP}
65
68
  `;
66
69
  export default async function multimodeCmd(argv) {
67
70
  const hasCount = argv.some((value) => value === "-n" || value === "--count" || value.startsWith("--count="));
@@ -73,6 +76,7 @@ export default async function multimodeCmd(argv) {
73
76
  out(HELP);
74
77
  return;
75
78
  }
79
+ const naiPreflight = unwrapNaiCliResult(parseNaiCliOptions(args, "require-explicit"), Boolean(args.json));
76
80
  const prompt = args.positional.join(" ");
77
81
  if (!prompt)
78
82
  die(2, "prompt required");
@@ -117,6 +121,7 @@ export default async function multimodeCmd(argv) {
117
121
  moderation: args.moderation,
118
122
  sessionId: args.session,
119
123
  requestId,
124
+ ...naiPreflight.payload,
120
125
  };
121
126
  const model = canonicalizeImageModel(args.model);
122
127
  if (model)
@@ -6,6 +6,7 @@ import { out, die, color, json, exitCodeForError } from "../lib/output.js";
6
6
  import { canonicalizeImageModel } from "../lib/model-aliases.js";
7
7
  import { config } from "../../config.js";
8
8
  import { deriveProviderIds } from "../../lib/providers/derive.js";
9
+ import { NAI_CLI_FLAGS, NAI_CLI_HELP, parseNaiCliOptions, unwrapNaiCliResult } from "../lib/nai-options.js";
9
10
  const MAX_GENERATION_COUNT = Math.max(1, Math.trunc(Number(config.limits.maxGeneratedImages) || 24));
10
11
  const PROVIDER_VALUES = ["auto", ...deriveProviderIds()];
11
12
  const HELP = `
@@ -16,7 +17,8 @@ const HELP = `
16
17
  show <nodeId> [--json]
17
18
 
18
19
  Generate options:
19
- --provider <${PROVIDER_VALUES.join("|")}> Provider for this request
20
+ --provider <${PROVIDER_VALUES.join("|")}> Provider for this request
21
+ ${NAI_CLI_HELP}
20
22
  `;
21
23
  const GEN_FLAGS = {
22
24
  quality: { short: "q", type: "string", default: "low" },
@@ -37,6 +39,7 @@ const GEN_FLAGS = {
37
39
  session: { type: "string" },
38
40
  "no-stream": { type: "boolean" },
39
41
  help: { short: "h", type: "boolean" },
42
+ ...NAI_CLI_FLAGS,
40
43
  };
41
44
  const SHOW_FLAGS = {
42
45
  json: { type: "boolean" },
@@ -58,6 +61,7 @@ async function generateSub(argv) {
58
61
  out(HELP);
59
62
  return;
60
63
  }
64
+ const naiPreflight = unwrapNaiCliResult(parseNaiCliOptions(args, "require-explicit"), Boolean(args.json));
61
65
  const prompt = args.positional.join(" ");
62
66
  if (!prompt)
63
67
  die(2, "prompt required");
@@ -83,6 +87,7 @@ async function generateSub(argv) {
83
87
  references,
84
88
  moderation: args.moderation,
85
89
  sessionId: args.session,
90
+ ...naiPreflight.payload,
86
91
  };
87
92
  const model = canonicalizeImageModel(args.model);
88
93
  if (model)
package/bin/lib/args.js CHANGED
@@ -6,7 +6,7 @@ export function parseArgs(argv, spec = {}) {
6
6
  if (def.short)
7
7
  shortMap[def.short] = name;
8
8
  }
9
- const out = { positional: [], _unknown: [] };
9
+ const out = { positional: [], _unknown: [], _present: [] };
10
10
  for (const [name, def] of Object.entries(spec.flags || {})) {
11
11
  if (def.repeatable)
12
12
  out[name] = [];
@@ -38,6 +38,7 @@ export function parseArgs(argv, spec = {}) {
38
38
  i++;
39
39
  continue;
40
40
  }
41
+ out._present.push(name);
41
42
  if (def.type === "boolean") {
42
43
  out[name] = true;
43
44
  i++;
@@ -61,6 +62,7 @@ export function parseArgs(argv, spec = {}) {
61
62
  i++;
62
63
  continue;
63
64
  }
65
+ out._present.push(name);
64
66
  const def = spec.flags[name];
65
67
  if (def.type === "boolean") {
66
68
  out[name] = true;
@@ -0,0 +1,195 @@
1
+ import { NAI_NOISE_SCHEDULES, NAI_QUALITY_PRESET_IDS, NAI_SAMPLERS, NAI_UC_PRESET_IDS, } from "../../lib/naiImageAdapter.js";
2
+ import { getProvider } from "../../lib/providers/registry.js";
3
+ import { fail } from "./output.js";
4
+ export const NAI_CLI_FLAGS = {
5
+ "nai-negative-prompt": { type: "string" },
6
+ "nai-sampler": { type: "string" },
7
+ "nai-noise-schedule": { type: "string" },
8
+ "nai-steps": { type: "string" },
9
+ "nai-scale": { type: "string" },
10
+ "nai-cfg-rescale": { type: "string" },
11
+ "nai-seed": { type: "string" },
12
+ "nai-uc-preset": { type: "string" },
13
+ "nai-quality-preset": { type: "string" },
14
+ "nai-auto-smea": { type: "boolean" },
15
+ "no-nai-auto-smea": { type: "boolean" },
16
+ "nai-decrisper": { type: "boolean" },
17
+ "no-nai-decrisper": { type: "boolean" },
18
+ "nai-variety-plus": { type: "boolean" },
19
+ "no-nai-variety-plus": { type: "boolean" },
20
+ "nai-straight-alpha": { type: "boolean" },
21
+ "no-nai-straight-alpha": { type: "boolean" },
22
+ };
23
+ export const NAI_CLI_HELP = `
24
+ NovelAI options (nai lane only):
25
+ --nai-negative-prompt <text> Undesired content (max 10,000 chars)
26
+ --nai-sampler <sampler> ${NAI_SAMPLERS.filter((v) => v !== "ddim_v3").join("|")}
27
+ --nai-noise-schedule <schedule> ${NAI_NOISE_SCHEDULES.join("|")}
28
+ --nai-steps <1..50> Diffusion steps
29
+ --nai-scale <1..10> Prompt guidance
30
+ --nai-cfg-rescale <0..1> Guidance rescale
31
+ --nai-seed <0..4294967295> Reproducible seed
32
+ --nai-uc-preset <preset> ${NAI_UC_PRESET_IDS.join("|")}
33
+ --nai-quality-preset <preset> ${NAI_QUALITY_PRESET_IDS.join("|")} (V5 only)
34
+ --nai-auto-smea / --no-nai-auto-smea
35
+ --nai-decrisper / --no-nai-decrisper
36
+ --nai-variety-plus / --no-nai-variety-plus
37
+ --nai-straight-alpha / --no-nai-straight-alpha V5 only
38
+ `;
39
+ export function unwrapNaiCliResult(result, jsonMode) {
40
+ if ("message" in result) {
41
+ fail({ json: jsonMode, code: result.code, message: result.message,
42
+ ...(result.flag ? { extra: { flag: result.flag } } : {}) });
43
+ }
44
+ return result.value;
45
+ }
46
+ const SELECTABLE_SAMPLERS = NAI_SAMPLERS.filter((value) => value !== "ddim_v3");
47
+ const NAI_MODEL_IDS = new Set(getProvider("nai").models.filter((model) => model.kind === "image").map((model) => model.id));
48
+ const V5_MODEL_IDS = new Set(["nai-diffusion-5-full", "nai-diffusion-5-curated"]);
49
+ const OPTION_KEYS = Object.keys(NAI_CLI_FLAGS);
50
+ const VALUE_OPTION_KEYS = Object.entries(NAI_CLI_FLAGS)
51
+ .filter(([, definition]) => definition.type !== "boolean")
52
+ .map(([key]) => key);
53
+ function failure(code, message, flag) {
54
+ return { ok: false, code, message, ...(flag ? { flag } : {}) };
55
+ }
56
+ function isFailure(value) {
57
+ return Boolean(value && typeof value === "object" && "ok" in value && value.ok === false);
58
+ }
59
+ function explicitModel(raw) {
60
+ if (typeof raw !== "string" || !raw)
61
+ return null;
62
+ const slash = raw.indexOf("/");
63
+ if (slash < 0)
64
+ return NAI_MODEL_IDS.has(raw) ? raw : null;
65
+ const lane = raw.slice(0, slash);
66
+ const model = raw.slice(slash + 1);
67
+ return lane === "nai" && NAI_MODEL_IDS.has(model) ? model : null;
68
+ }
69
+ function classifyTarget(args) {
70
+ const provider = typeof args.provider === "string" ? args.provider : undefined;
71
+ const providerClass = !provider || provider === "auto"
72
+ ? "unknown"
73
+ : provider === "nai" ? "nai" : "non-nai";
74
+ const rawModel = typeof args.model === "string" ? args.model : undefined;
75
+ const modelClass = !rawModel
76
+ ? "unknown"
77
+ : explicitModel(rawModel) ? "nai" : "non-nai";
78
+ if ((providerClass === "nai" && modelClass === "non-nai") ||
79
+ (providerClass === "non-nai" && modelClass === "nai"))
80
+ return "conflict";
81
+ if (providerClass === "nai" || modelClass === "nai")
82
+ return "nai";
83
+ if (providerClass === "non-nai" || modelClass === "non-nai")
84
+ return "non-nai";
85
+ return "unknown";
86
+ }
87
+ function enumValue(args, key, allowed) {
88
+ const raw = args[key];
89
+ if (raw === undefined)
90
+ return undefined;
91
+ if (typeof raw !== "string" || !allowed.includes(raw)) {
92
+ return failure("NAI_FLAG_INVALID", `--${key} must be one of: ${allowed.join(", ")}`, `--${key}`);
93
+ }
94
+ return raw;
95
+ }
96
+ function numberValue(args, key, min, max, integer = false) {
97
+ const raw = args[key];
98
+ if (raw === undefined)
99
+ return undefined;
100
+ const value = typeof raw === "string" && raw.trim() !== "" ? Number(raw) : Number.NaN;
101
+ if (!Number.isFinite(value) || value < min || value > max || (integer && !Number.isInteger(value))) {
102
+ return failure("NAI_FLAG_INVALID", `--${key} must be ${integer ? "an integer " : ""}between ${min} and ${max}`, `--${key}`);
103
+ }
104
+ return value;
105
+ }
106
+ function booleanPair(args, positive, negative) {
107
+ if (args[positive] && args[negative]) {
108
+ return failure("NAI_FLAG_CONFLICT", `--${positive} and --${negative} are mutually exclusive`, `--${positive}`);
109
+ }
110
+ if (args[positive])
111
+ return true;
112
+ if (args[negative])
113
+ return false;
114
+ return undefined;
115
+ }
116
+ function assignValue(payload, key, value) {
117
+ if (isFailure(value))
118
+ return value;
119
+ if (value !== undefined)
120
+ payload[key] = value;
121
+ return null;
122
+ }
123
+ function buildPayload(args) {
124
+ const payload = {};
125
+ const negative = args["nai-negative-prompt"];
126
+ if (negative !== undefined) {
127
+ if (typeof negative !== "string" || negative.length > 10_000) {
128
+ return failure("NAI_FLAG_INVALID", "--nai-negative-prompt must be at most 10,000 characters", "--nai-negative-prompt");
129
+ }
130
+ payload.negativePrompt = negative;
131
+ }
132
+ const values = [
133
+ ["sampler", enumValue(args, "nai-sampler", SELECTABLE_SAMPLERS)],
134
+ ["noiseSchedule", enumValue(args, "nai-noise-schedule", NAI_NOISE_SCHEDULES)],
135
+ ["steps", numberValue(args, "nai-steps", 1, 50, true)],
136
+ ["scale", numberValue(args, "nai-scale", 1, 10)],
137
+ ["cfgRescale", numberValue(args, "nai-cfg-rescale", 0, 1)],
138
+ ["seed", numberValue(args, "nai-seed", 0, 2 ** 32 - 1, true)],
139
+ ["ucPresetId", enumValue(args, "nai-uc-preset", NAI_UC_PRESET_IDS)],
140
+ ["qualityPresetId", enumValue(args, "nai-quality-preset", NAI_QUALITY_PRESET_IDS)],
141
+ ["autoSmea", booleanPair(args, "nai-auto-smea", "no-nai-auto-smea")],
142
+ ["decrisper", booleanPair(args, "nai-decrisper", "no-nai-decrisper")],
143
+ ["varietyPlus", booleanPair(args, "nai-variety-plus", "no-nai-variety-plus")],
144
+ ["straightAlpha", booleanPair(args, "nai-straight-alpha", "no-nai-straight-alpha")],
145
+ ];
146
+ for (const [key, value] of values) {
147
+ const error = assignValue(payload, key, value);
148
+ if (error)
149
+ return error;
150
+ }
151
+ return payload;
152
+ }
153
+ function needsV5(payload) {
154
+ return payload.straightAlpha === true || payload.qualityPresetId !== undefined;
155
+ }
156
+ export function parseNaiCliOptions(args, policy) {
157
+ const present = new Set(args._present ?? []);
158
+ const hasOptions = OPTION_KEYS.some((key) => present.has(key) || args[key] !== undefined);
159
+ if (!hasOptions)
160
+ return { ok: true, value: { hasOptions: false, payload: {}, target: "unknown" } };
161
+ const missingValue = VALUE_OPTION_KEYS.find((key) => present.has(key) && args[key] === undefined);
162
+ if (missingValue) {
163
+ return failure("NAI_FLAG_INVALID", `--${missingValue} requires a value`, `--${missingValue}`);
164
+ }
165
+ const built = buildPayload(args);
166
+ if (isFailure(built))
167
+ return built;
168
+ const target = classifyTarget(args);
169
+ if (target === "conflict")
170
+ return failure("NAI_TARGET_CONFLICT", "NovelAI flags conflict with the explicit provider/model target");
171
+ if (target === "non-nai")
172
+ return failure("NAI_FLAG_TARGET_MISMATCH", "NovelAI flags require a NovelAI provider/model target");
173
+ if (target === "unknown" && policy === "require-explicit") {
174
+ return failure("NAI_EXPLICIT_TARGET_REQUIRED", "NovelAI flags require --provider nai or --model nai-diffusion-*");
175
+ }
176
+ const model = explicitModel(args.model);
177
+ if (needsV5(built) && policy === "require-explicit" && !model) {
178
+ return failure("NAI_V5_MODEL_REQUIRED", "V5-only NovelAI flags require an explicit V5 model");
179
+ }
180
+ if (needsV5(built) && model && !V5_MODEL_IDS.has(model)) {
181
+ return failure("NAI_V5_MODEL_REQUIRED", "V5-only NovelAI flags are not supported by V4.5");
182
+ }
183
+ return { ok: true, value: { hasOptions, payload: built, target } };
184
+ }
185
+ export function finalizeNaiCliTarget(preflight, target) {
186
+ if (!preflight.hasOptions)
187
+ return { ok: true, value: preflight };
188
+ if (target.lane !== "nai" || !NAI_MODEL_IDS.has(target.model)) {
189
+ return failure("NAI_FLAG_TARGET_MISMATCH", "NovelAI flags require a resolved NovelAI target");
190
+ }
191
+ if (needsV5(preflight.payload) && !V5_MODEL_IDS.has(target.model)) {
192
+ return failure("NAI_V5_MODEL_REQUIRED", "V5-only NovelAI flags are not supported by V4.5");
193
+ }
194
+ return { ok: true, value: { ...preflight, target: "nai" } };
195
+ }
package/config.js CHANGED
@@ -260,6 +260,8 @@ export const config = {
260
260
  defaultScale: pickInt(env.IMA2_NAI_DEFAULT_SCALE, fileCfg.naiProvider?.defaultScale, 5),
261
261
  defaultSampler: pickStr(env.IMA2_NAI_DEFAULT_SAMPLER, fileCfg.naiProvider?.defaultSampler, "k_euler_ancestral"),
262
262
  defaultNoiseSchedule: pickStr(env.IMA2_NAI_DEFAULT_NOISE_SCHEDULE, fileCfg.naiProvider?.defaultNoiseSchedule, "karras"),
263
+ defaultAutoSmea: pickBool(env.IMA2_NAI_DEFAULT_AUTO_SMEA, fileCfg.naiProvider?.defaultAutoSmea, false),
264
+ defaultDecrisper: pickBool(env.IMA2_NAI_DEFAULT_DECRISPER, fileCfg.naiProvider?.defaultDecrisper, false),
263
265
  },
264
266
  log: {
265
267
  level: pickStr(env.IMA2_LOG_LEVEL, fileCfg.log?.level, defaultLogLevelForEnv(env)),
package/docs/API.md CHANGED
@@ -18,6 +18,7 @@ Image generation supports OAuth, API-key, Grok, and Gemini (`agy` and `gemini-ap
18
18
  - `provider: "agy"` spawns the Antigravity CLI (`agy -p`) to generate images via Google Gemini's `default_api:generate_image` tool. Model is `nano-banana-2`. Output is fixed at 1024×1024 JPEG. Max 3 reference images (i2i). No web search, quality, size, or mask controls. Multimode returns a single image. Video is unsupported (`AGY_VIDEO_UNSUPPORTED`).
19
19
  - `provider: "grok-api"` uses a direct xAI API key instead of the bundled progrok OAuth proxy. Same pipeline as `grok` (Web Search → planner → `/v1/images/generations`), same aspect ratio and resolution options. Requires an xAI API key configured via the web UI key management or `XAI_API_KEY` env var. Also supports video generation.
20
20
  - `provider: "gemini-api"` calls the Google Generative Language API directly (or Vertex AI with a service account JSON). Supports models `nano-banana-2` (Gemini 3.1 Flash Image) and `nano-banana-pro` (Gemini 3 Pro Image). Supports variable aspect ratios (1:1 through 21:9) and four resolution tiers (512px, 1K, 2K, 4K) on both auth paths — the direct API path sends `generation_config.response_format.image` (snake_case) while the Vertex AI endpoint (`aiplatform.googleapis.com`) sends `generationConfig.imageConfig` (camelCase). With `size: "auto"` the image config is omitted entirely and the model decides ratio/size. Auth: `GEMINI_API_KEY` env var, web UI key management (`/api/keys/gemini`), or a Vertex AI service account JSON (`VERTEX_SERVICE_ACCOUNT_JSON` or `/api/keys/vertex`). When both Vertex credentials and an API key are configured, Vertex takes priority. The chosen auth mode (`apikey` or `vertex`) persists to `~/.ima2/config.json` as `geminiAuthMode` and is restored on server startup. Per-model cost: `nano-banana-2` (Flash): 512=$0.001, 1K=$0.003, 2K=$0.004, 4K=$0.006; `nano-banana-pro`: 1K=$0.007, 2K=$0.007, 4K=$0.013. No web search or mask controls.
21
+ - `provider: "nai"` calls NovelAI text-to-image generation with one of `nai-diffusion-5-full`, `nai-diffusion-5-curated`, `nai-diffusion-4-5-full`, or `nai-diffusion-4-5-curated`. It accepts the provider-native request fields documented below and returns a ZIP that ima2 decodes to PNG. References, edits, and masks are explicitly refused.
21
22
  - API-key generation covers classic generate, edit, mask-guided edit, multimode, and node generation.
22
23
  - If `provider: "api"` is requested without an API key, routes fail before upstream with `401` and `API_KEY_REQUIRED`.
23
24
  - Grok generation maps `size` to xAI `aspect_ratio` and `resolution`; it does not send an OpenAI-style `size` field upstream. Grok edit uses xAI `/v1/images/edits`; Grok mask edit remains unsupported and returns `GROK_MASK_UNSUPPORTED`.
@@ -216,6 +217,19 @@ generations maintain character and scene continuity for multi-shot video product
216
217
 
217
218
  Current app default: `gpt-5.6-luna`. `gpt-5.5` and the other supported GPT image models remain available when callers explicitly select them.
218
219
 
220
+ When `provider` is `"nai"`, classic, multimode, and node generation accept the
221
+ same 13 provider-native fields: `negativePrompt`, `sampler`, `noiseSchedule`,
222
+ `steps`, `scale`, `cfgRescale`, `seed`, `ucPresetId`, `qualityPresetId`,
223
+ `autoSmea`, `decrisper`, `varietyPlus`, and `straightAlpha`. Missing values stay
224
+ sparse and resolve from `config.naiProvider`; operator defaults `defaultAutoSmea`
225
+ and `defaultDecrisper` are `false` unless overridden by config or
226
+ `IMA2_NAI_DEFAULT_AUTO_SMEA` / `IMA2_NAI_DEFAULT_DECRISPER`. Quality preset and
227
+ enabled alpha are V5-only. The four exact image models are
228
+ `nai-diffusion-5-full`, `nai-diffusion-5-curated`,
229
+ `nai-diffusion-4-5-full`, and `nai-diffusion-4-5-curated`. The lane is
230
+ text-to-image only: `NAI_REF_UNSUPPORTED`, `NAI_EDIT_UNSUPPORTED`, and
231
+ `NAI_MASK_UNSUPPORTED` fail closed rather than discarding input.
232
+
219
233
  When `provider` is `"grok"`, supported models are `grok-imagine-image` and
220
234
  `grok-imagine-image-quality`. The server uses `grok-4.5` as the search/planner
221
235
  model by default (`IMA2_GROK_PLANNER_MODEL`) and times the mandatory search and
@@ -758,7 +772,7 @@ API key management endpoints for configuring provider credentials at runtime thr
758
772
  | Endpoint | Method | Description |
759
773
  |---|---|---|
760
774
  | `/api/keys/status` | GET | Returns configured/valid/maskedKey status for all providers (openai, xai, gemini, atlascloud, minimax, nai, vertex) plus `geminiAuthMode` (`"apikey"` or `"vertex"`) |
761
- | `/api/keys/:provider` | PUT | Save an API key. Body: `{ "apiKey": "..." }`. Validates key format and upstream before saving to config.json. Provider: `openai`, `xai`, `gemini`, `atlascloud`, `minimax`, or `nai`. `minimax` and `nai` have no fixed key prefix, so only the upstream validation call gates them; a NovelAI persistent token looks like `pst-...`. |
775
+ | `/api/keys/:provider` | PUT | Save an API key. Body: `{ "apiKey": "..." }`. Validates key format and upstream before saving to config.json. Provider: `openai`, `xai`, `gemini`, `atlascloud`, `minimax`, or `nai`. `minimax` and `nai` have no fixed key prefix, so only the upstream validation call gates them. |
762
776
  | `/api/keys/:provider` | DELETE | Remove a config-sourced API key. Env-sourced keys cannot be removed (`ENV_KEY_IMMUTABLE`). |
763
777
  | `/api/keys/vertex` | PUT | Save a Vertex AI service account JSON. Body: `{ "serviceAccountJson": "..." }`. Validates JSON structure (`type: "service_account"`, `project_id` required). |
764
778
  | `/api/keys/vertex` | DELETE | Remove a config-sourced Vertex AI service account. |
package/docs/CLI.md CHANGED
@@ -90,6 +90,47 @@ Provider override semantics:
90
90
  - `runway` / `higgsfield` (gen/video only) route through the MCP async pipeline (`POST /api/mcp/generate` + SSE wait). Runway requires an MCP connection; Higgsfield stays catalog-only (`locked`) until a paid plan. MCP lanes accept `-n 1` only, gallery filenames for `--ref`, and reject core-only flags with `FLAG_NOT_SUPPORTED`.
91
91
  - `auto` preserves route default behavior and currently resolves to GPT OAuth unless server routing changes (edit/multimode/node only; removed from gen/video in 3.0.0).
92
92
 
93
+ ### NovelAI native generation options
94
+
95
+ `ima2 gen`, `ima2 multimode`, and `ima2 node generate` share these request-only
96
+ flags. They do not change saved defaults.
97
+
98
+ | Flag | Request field | Rule |
99
+ |---|---|---|
100
+ | `--nai-negative-prompt <text>` | `negativePrompt` | Undesired content, max 10,000 characters |
101
+ | `--nai-sampler <id>` | `sampler` | Modern NovelAI sampler enum |
102
+ | `--nai-noise-schedule <id>` | `noiseSchedule` | `native`, `karras`, `exponential`, or `polyexponential` |
103
+ | `--nai-steps <1..50>` | `steps` | Integer |
104
+ | `--nai-scale <1..10>` | `scale` | Prompt guidance |
105
+ | `--nai-cfg-rescale <0..1>` | `cfgRescale` | Guidance rescale |
106
+ | `--nai-seed <0..4294967295>` | `seed` | Integer; zero is valid |
107
+ | `--nai-uc-preset <id>` | `ucPresetId` | `heavy`, `light`, `furryFocus`, `humanFocus`, or `none` |
108
+ | `--nai-quality-preset <id>` | `qualityPresetId` | `standard`, `light`, or `none`; V5 only |
109
+ | `--nai-auto-smea` / `--no-nai-auto-smea` | `autoSmea` | Explicit true/false |
110
+ | `--nai-decrisper` / `--no-nai-decrisper` | `decrisper` | Explicit true/false |
111
+ | `--nai-variety-plus` / `--no-nai-variety-plus` | `varietyPlus` | Explicit true/false |
112
+ | `--nai-straight-alpha` / `--no-nai-straight-alpha` | `straightAlpha` | Enabled alpha is V5 only |
113
+
114
+ `gen` may resolve a persisted NovelAI default set with
115
+ `ima2 defaults set image nai/nai-diffusion-5-full`. `multimode` requires an
116
+ explicit NovelAI target when any `--nai-*` flag is used, and `node generate`
117
+ likewise requires an explicit NovelAI target. Provider/model conflicts fail before
118
+ network I/O. V5-only values on V4.5 fail with `NAI_V5_MODEL_REQUIRED`; malformed
119
+ values and non-NAI targets also exit 2. The ima2 NovelAI lane is text-to-image only:
120
+ references, edits, and masks remain explicitly unsupported.
121
+
122
+ ```bash
123
+ ima2 gen "character sprite, transparent background, has alpha" \
124
+ --model nai/nai-diffusion-5-full --nai-straight-alpha \
125
+ --nai-negative-prompt "watermark" -o sprite.png
126
+
127
+ ima2 multimode "city at night" --provider nai --model nai-diffusion-5-full \
128
+ --nai-auto-smea --nai-decrisper --max-images 2
129
+
130
+ ima2 node generate "portrait" --provider nai --model nai-diffusion-5-curated \
131
+ --nai-uc-preset light --nai-quality-preset light
132
+ ```
133
+
93
134
  `ima2 serve` starts the bundled Grok proxy automatically. No separate `progrok`
94
135
  install is required. Use `ima2 grok login` once to authorize xAI OAuth. Login
95
136
  defaults to `--manual-paste` so PowerShell, Terminal, and remote shells all use
@@ -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: 396 (runtime: 186, contract: 210)
7
+ Total: 399 (runtime: 188, contract: 211)
8
8
 
9
9
  ## Runtime-importing tests
10
10
  - `tests/agent-mode-auto-planner-contract.test.ts`
@@ -120,6 +120,8 @@ Total: 396 (runtime: 186, contract: 210)
120
120
  - `tests/minimax-provider-contract.test.ts`
121
121
  - `tests/model-default-projection-contract.test.ts`
122
122
  - `tests/models-endpoint-contract.test.ts`
123
+ - `tests/nai-built-runtime-contract.test.ts`
124
+ - `tests/nai-cli-options-contract.test.ts`
123
125
  - `tests/nai-client-options-contract.test.ts`
124
126
  - `tests/nai-options-contract.test.ts`
125
127
  - `tests/nai-provider-contract.test.ts`
@@ -328,6 +330,7 @@ Total: 396 (runtime: 186, contract: 210)
328
330
  - `tests/multimode-backend-contract.test.js`
329
331
  - `tests/multimode-concurrent-store-contract.test.js`
330
332
  - `tests/multimode-ui-contract.test.js`
333
+ - `tests/nai-cli-built-smoke.test.ts`
331
334
  - `tests/nai-ui-registration-contract.test.ts`
332
335
  - `tests/navrail-hover-label-contract.test.ts`
333
336
  - `tests/node-async-eventbus-contract.test.js`
@@ -64,6 +64,8 @@ export function buildIma2Capabilities({ appConfig = runtimeConfigDefault, packag
64
64
  noiseSchedule: appConfig.naiProvider.defaultNoiseSchedule,
65
65
  steps: appConfig.naiProvider.defaultSteps,
66
66
  scale: appConfig.naiProvider.defaultScale,
67
+ autoSmea: appConfig.naiProvider.defaultAutoSmea,
68
+ decrisper: appConfig.naiProvider.defaultDecrisper,
67
69
  },
68
70
  },
69
71
  valid: {
@@ -79,8 +79,8 @@ export async function generateViaNai(prompt, ctx, options = {}) {
79
79
  n_samples: 1,
80
80
  ucPresetId: options.ucPresetId ?? "heavy",
81
81
  qualityPresetId: isV5 ? (options.qualityPresetId ?? "standard") : "standard",
82
- autoSmea: false,
83
- dynamic_thresholding: false,
82
+ autoSmea: options.autoSmea ?? cfg.defaultAutoSmea,
83
+ dynamic_thresholding: options.decrisper ?? cfg.defaultDecrisper,
84
84
  controlnet_strength: 1,
85
85
  legacy: false,
86
86
  legacy_v3_extend: false,
package/lib/naiOptions.js CHANGED
@@ -86,6 +86,12 @@ export function readNaiOptions(body) {
86
86
  const varietyPlus = pickBoolean(raw.varietyPlus);
87
87
  if (varietyPlus !== undefined)
88
88
  out.varietyPlus = varietyPlus;
89
+ const autoSmea = pickBoolean(raw.autoSmea);
90
+ if (autoSmea !== undefined)
91
+ out.autoSmea = autoSmea;
92
+ const decrisper = pickBoolean(raw.decrisper);
93
+ if (decrisper !== undefined)
94
+ out.decrisper = decrisper;
89
95
  const ucPresetId = pickEnum(raw.ucPresetId, NAI_UC_PRESET_IDS);
90
96
  if (ucPresetId !== undefined)
91
97
  out.ucPresetId = ucPresetId;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ima2-gen",
3
- "version": "3.11.0",
3
+ "version": "3.12.1",
4
4
  "packageManager": "npm@11.18.0",
5
5
  "description": "Local-first visual generation runtime and studio for people and coding agents, with reproducible image and video workflows across multiple providers.",
6
6
  "type": "module",
@@ -119,5 +119,5 @@
119
119
  "tsx": "^4.23.12",
120
120
  "typescript": "^5.9.3"
121
121
  },
122
- "gitHead": "d18e56caabd03d5019dbfffa8c9686c9be225e4f"
122
+ "gitHead": "ae45711d42383755697b66ff0b4a7e49beab8076"
123
123
  }
@@ -100,6 +100,78 @@ Grok requests with reference images use the edit/image-to-image path so the
100
100
  references remain attached after planning. Keep Grok references to three total
101
101
  input images.
102
102
 
103
+ ## NovelAI Image Generation
104
+
105
+ Discover the live NovelAI lane before choosing a model:
106
+
107
+ ```bash
108
+ ima2 models --kind image --lane nai --json
109
+ ima2 defaults set image nai/nai-diffusion-5-full
110
+ ```
111
+
112
+ The four exact model IDs are:
113
+
114
+ - `nai-diffusion-5-full`
115
+ - `nai-diffusion-5-curated`
116
+ - `nai-diffusion-4-5-full`
117
+ - `nai-diffusion-4-5-curated`
118
+
119
+ Use a persistent NovelAI token from Settings > API Keys or `NOVELAI_API_KEY`.
120
+ NovelAI does not publish one mandatory token prefix, so never reject a token based
121
+ on a guessed prefix. Check the lane state through `ima2 models` instead.
122
+
123
+ The same NovelAI options work on `ima2 gen`, `ima2 multimode`, and
124
+ `ima2 node generate`. Run `ima2 gen --help` for the live enum/range list. Example:
125
+
126
+ ```bash
127
+ ima2 gen "1girl, blue hair, city at night" \
128
+ --provider nai --model nai-diffusion-5-full \
129
+ --nai-negative-prompt "lowres, watermark" \
130
+ --nai-steps 28 --nai-scale 5 \
131
+ --nai-auto-smea --nai-decrisper --nai-variety-plus
132
+ ```
133
+
134
+ For V5 native alpha, pair the request flag with an alpha-aware prompt:
135
+
136
+ ```bash
137
+ ima2 gen "character sprite, transparent background, has alpha" \
138
+ --model nai/nai-diffusion-5-full \
139
+ --nai-straight-alpha -o sprite.png
140
+ ```
141
+
142
+ Supported request controls include sampler, noise schedule, steps, guidance,
143
+ CFG rescale, seed, undesired-content/UC preset, quality preset, Auto SMEA,
144
+ Decrisper, Variety+, and V5 native alpha. `--nai-quality-preset` and enabled
145
+ `--nai-straight-alpha` require an explicit V5 model on CLI surfaces that cannot
146
+ resolve a saved catalog default. `multimode` and `node generate` require an
147
+ explicit NovelAI provider or model whenever NAI-native flags are used; `gen`
148
+ can use a persisted NovelAI CLI default.
149
+
150
+ NovelAI V5 officially supports English and Japanese prompts. Natural language
151
+ and tags both work; quote text that should appear in the image, and use tags such
152
+ as `transparent background`, `has alpha`, or `alpha transparency` with the alpha
153
+ flag. Other languages may work but are not the officially supported prompt pair.
154
+
155
+ Do not assume a generation costs no Anlas. Check the current account and usage
156
+ limit. The no-Anlas Opus conditions include one image, no base or source image,
157
+ a normal resolution up to 1024x1024, and at most 28 steps; V5 usage limits can
158
+ still apply.
159
+
160
+ The ima2 NovelAI lane is text-to-image only. NovelAI itself has additional product
161
+ features, but ima2 does not currently expose reference/img2img, masks/inpainting,
162
+ Character Positioning, Director Reference, Vibe Transfer, or Max Enhance.
163
+ References, edits, and masks fail closed with `NAI_REF_UNSUPPORTED`,
164
+ `NAI_EDIT_UNSUPPORTED`, or `NAI_MASK_UNSUPPORTED` rather than being dropped.
165
+
166
+ Primary product references, checked 2026-08-27: the official
167
+ [NovelAI V5 release](https://journal.novelai.net/image-generation-novelai-diffusion-v5-is-here-c2df7c6b8d2d/)
168
+ and [NovelAI subscription documentation](https://docs.novelai.net/en/subscription/).
169
+ Native control details come from the official
170
+ [sampling](https://docs.novelai.net/en/image/sampling/),
171
+ [steps and guidance](https://docs.novelai.net/en/image/stepsguidance/),
172
+ [quality tags](https://docs.novelai.net/en/image/qualitytags/), and
173
+ [seed](https://docs.novelai.net/en/image/seed/) pages, checked the same date.
174
+
103
175
  ## Prompting Guidance
104
176
 
105
177
  GPT Image 2 can follow detailed visual instructions and can render visible text