ima2-gen 2.0.15 → 2.0.16

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 (125) hide show
  1. package/README.md +30 -0
  2. package/bin/commands/skill.js +336 -17
  3. package/bin/ima2.js +22 -2
  4. package/bin/lib/doctor-checks.js +6 -4
  5. package/config.js +1 -0
  6. package/docs/CLI.md +25 -3
  7. package/docs/PROMPT_STUDIO.ko.md +29 -0
  8. package/docs/PROMPT_STUDIO.md +31 -0
  9. package/docs/migration/runtime-test-inventory.md +7 -1
  10. package/lib/agentImageVideoGen.js +8 -0
  11. package/lib/agentQueueStore.js +36 -10
  12. package/lib/agentQueueWorker.js +79 -7
  13. package/lib/agentRuntime.js +2 -0
  14. package/lib/assetLifecycle.js +40 -8
  15. package/lib/canvasVersionStore.js +86 -0
  16. package/lib/cardNewsGenerator.js +37 -3
  17. package/lib/cardNewsJobStore.js +10 -0
  18. package/lib/cardNewsManifestStore.js +6 -12
  19. package/lib/cardNewsPath.js +25 -0
  20. package/lib/db.js +2 -0
  21. package/lib/generatePipeline.js +474 -0
  22. package/lib/generationInputValidation.js +32 -0
  23. package/lib/grokImageCore.js +18 -1
  24. package/lib/historyIndex.js +11 -4
  25. package/lib/historyList.js +3 -0
  26. package/lib/imageMetadata.js +3 -0
  27. package/lib/inflight.js +30 -3
  28. package/lib/multimodePipeline.js +462 -0
  29. package/lib/nodeGeneration.js +440 -0
  30. package/lib/nodeValidation.js +23 -0
  31. package/lib/oauthProxy/generators.js +3 -246
  32. package/lib/oauthProxy/multimodeGenerators.js +251 -0
  33. package/lib/routeHelpers.js +9 -2
  34. package/package.json +2 -2
  35. package/routes/agent.js +4 -4
  36. package/routes/canvasVersions.js +21 -1
  37. package/routes/cardNews.js +2 -1
  38. package/routes/edit.js +25 -9
  39. package/routes/events.js +5 -1
  40. package/routes/generate.js +2 -481
  41. package/routes/keys.js +31 -39
  42. package/routes/multimode.js +2 -502
  43. package/routes/nodes.js +3 -448
  44. package/routes/video.js +12 -3
  45. package/routes/videoExtended.js +19 -4
  46. package/server.js +53 -1
  47. package/skills/ima2/SKILL.md +204 -0
  48. package/skills/ima2-front/SKILL.md +611 -0
  49. package/skills/ima2-front/references/a11y-patterns.md +134 -0
  50. package/skills/ima2-front/references/aesthetics.md +327 -0
  51. package/skills/ima2-front/references/anti-slop.md +241 -0
  52. package/skills/ima2-front/references/asset-requirements.md +431 -0
  53. package/skills/ima2-front/references/brand-asset-sourcing.md +122 -0
  54. package/skills/ima2-front/references/color-system.md +132 -0
  55. package/skills/ima2-front/references/consistency-locks.md +20 -0
  56. package/skills/ima2-front/references/crud-ui.md +53 -0
  57. package/skills/ima2-front/references/dropdown-layer.md +110 -0
  58. package/skills/ima2-front/references/i18n-global.md +104 -0
  59. package/skills/ima2-front/references/iterative-design.md +90 -0
  60. package/skills/ima2-front/references/korea-2026.md +174 -0
  61. package/skills/ima2-front/references/layout-discipline.md +161 -0
  62. package/skills/ima2-front/references/liquid-glass.md +181 -0
  63. package/skills/ima2-front/references/logo-sections.md +194 -0
  64. package/skills/ima2-front/references/mobile-ux.md +144 -0
  65. package/skills/ima2-front/references/performance-budget.md +106 -0
  66. package/skills/ima2-front/references/preflight-full.md +58 -0
  67. package/skills/ima2-front/references/product-density.md +53 -0
  68. package/skills/ima2-front/references/prototype-variants.md +54 -0
  69. package/skills/ima2-front/references/reference-capture.md +120 -0
  70. package/skills/ima2-front/references/responsive-viewport.md +139 -0
  71. package/skills/ima2-front/references/seo-baseline.md +123 -0
  72. package/skills/ima2-front/references/soft-3d-asset-gates.md +70 -0
  73. package/skills/ima2-front/references/stacks/astro.md +204 -0
  74. package/skills/ima2-front/references/stacks/mobile-native.md +281 -0
  75. package/skills/ima2-front/references/stacks/nextjs.md +219 -0
  76. package/skills/ima2-front/references/stacks/react.md +264 -0
  77. package/skills/ima2-front/references/stacks/svelte.md +235 -0
  78. package/skills/ima2-front/references/stacks/vanilla.md +226 -0
  79. package/skills/ima2-front/references/theme-switching.md +82 -0
  80. package/skills/ima2-front/references/top-bar.md +126 -0
  81. package/skills/ima2-front/references/typography-wrapping.md +270 -0
  82. package/skills/ima2-front/references/ux-writing-ko.md +62 -0
  83. package/skills/ima2-front/references/visual-verification.md +102 -0
  84. package/skills/ima2-uiux/SKILL.md +477 -0
  85. package/skills/ima2-uiux/references/anti-slop-judgment.md +39 -0
  86. package/skills/ima2-uiux/references/color-system.md +80 -0
  87. package/skills/ima2-uiux/references/design-isms.md +194 -0
  88. package/skills/ima2-uiux/references/design-read-example.md +33 -0
  89. package/skills/ima2-uiux/references/design-system-bootstrap.md +133 -0
  90. package/skills/ima2-uiux/references/favicon-logo.md +355 -0
  91. package/skills/ima2-uiux/references/form-patterns.md +118 -0
  92. package/skills/ima2-uiux/references/intent-discovery-ladder.md +86 -0
  93. package/skills/ima2-uiux/references/korean-design-vocabulary.md +52 -0
  94. package/skills/ima2-uiux/references/layout-macrostructures.md +54 -0
  95. package/skills/ima2-uiux/references/logo-trust-sections.md +96 -0
  96. package/skills/ima2-uiux/references/mobile-native-ux.md +207 -0
  97. package/skills/ima2-uiux/references/product-personalities.md +118 -0
  98. package/skills/ima2-uiux/references/responsive-nav.md +66 -0
  99. package/skills/ima2-uiux/references/typography-line-breaks.md +193 -0
  100. package/skills/ima2-uiux/references/ux-preflight.md +65 -0
  101. package/skills/ima2-uiux/references/ux-states.md +197 -0
  102. package/skills/ima2-uiux/references/visual-hierarchy.md +115 -0
  103. package/ui/dist/.vite/manifest.json +13 -13
  104. package/ui/dist/assets/AgentWorkspace-CtYt4SF5.js +3 -0
  105. package/ui/dist/assets/{CardNewsWorkspace-Cdxn1ohU.js → CardNewsWorkspace-DN5cVqG5.js} +1 -1
  106. package/ui/dist/assets/GenerationRequestLogPanel-DnmUQdKR.js +1 -0
  107. package/ui/dist/assets/{NodeCanvas-C67sLboA.js → NodeCanvas-CxpfIBI0.js} +1 -1
  108. package/ui/dist/assets/PromptBuilderPanel-C2R3ObtR.js +2 -0
  109. package/ui/dist/assets/{PromptImportDialog-DeQXsEEv.js → PromptImportDialog-BIxz_yEx.js} +2 -2
  110. package/ui/dist/assets/PromptImportDiscoverySection-BQTlPMME.js +1 -0
  111. package/ui/dist/assets/PromptImportFolderSection-Ds_Th9O3.js +1 -0
  112. package/ui/dist/assets/{PromptLibraryPanel-Db3xEPM_.js → PromptLibraryPanel-Rs4l45pV.js} +2 -2
  113. package/ui/dist/assets/{SettingsWorkspace-BDk_1W-V.js → SettingsWorkspace-p1AZ6uT9.js} +1 -1
  114. package/ui/dist/assets/index-0-_vgFGs.js +4 -0
  115. package/ui/dist/assets/index-Dm3pFxV4.js +23 -0
  116. package/ui/dist/assets/index-J8yDF3Ch.css +1 -0
  117. package/ui/dist/index.html +2 -2
  118. package/ui/dist/assets/AgentWorkspace-DAg_7zpY.js +0 -3
  119. package/ui/dist/assets/GenerationRequestLogPanel-C_qvEUi2.js +0 -1
  120. package/ui/dist/assets/PromptBuilderPanel-DjvVdAie.js +0 -2
  121. package/ui/dist/assets/PromptImportDiscoverySection-CrwyUKYI.js +0 -1
  122. package/ui/dist/assets/PromptImportFolderSection-CToGMqYE.js +0 -1
  123. package/ui/dist/assets/index-2oG6in1i.css +0 -1
  124. package/ui/dist/assets/index-CoxMOXAU.js +0 -23
  125. package/ui/dist/assets/index-p8egnGHk.js +0 -4
@@ -1,14 +1,11 @@
1
1
  import { setJobPhase } from "../inflight.js";
2
2
  import { logEvent } from "../logger.js";
3
- import { compressReferenceB64ForOAuth } from "../referenceImageCompress.js";
4
3
  import { safeReferenceDiagnostics } from "../refs.js";
5
4
  import { throwOAuthHttpError, throwOAuthTimeoutError } from "./errors.js";
6
- import { AUTO_PROMPT_FIDELITY_SUFFIX, DIRECT_PROMPT_FIDELITY_SUFFIX, EDIT_DEVELOPER_PROMPT, EDIT_NO_SEARCH_DEVELOPER_PROMPT, GENERATE_DEVELOPER_PROMPT, GENERATE_NO_SEARCH_DEVELOPER_PROMPT, MULTIMODE_DEVELOPER_PROMPT, MULTIMODE_NO_SEARCH_DEVELOPER_PROMPT, buildEditTextPrompt, buildMultimodeSequencePrompt, buildUserTextPrompt, } from "./prompts.js";
5
+ import { GENERATE_DEVELOPER_PROMPT, GENERATE_NO_SEARCH_DEVELOPER_PROMPT, buildUserTextPrompt, } from "./prompts.js";
7
6
  import { normalizeReferenceForOAuth } from "./references.js";
8
7
  import { buildImageTools, createOAuthGenerationTimeout, fetchOAuth, getOAuthUrl, resolveReasoningEffort, resolveWebSearchEnabled, summarizeEventTypes, waitForOAuthReady, } from "./runtime.js";
9
- import { readImageStream, readMultimodeImageStream } from "./streams.js";
10
- import { config } from "../../config.js";
11
- const RESEARCH_SUFFIX = config.oauth.researchSuffix;
8
+ import { readImageStream } from "./streams.js";
12
9
  export async function generateViaOAuth(prompt, quality, size, moderation = "low", references = [], requestId = null, mode = "auto", ctx = {}, options = {}) {
13
10
  await waitForOAuthReady(ctx);
14
11
  const oauthUrl = getOAuthUrl(ctx);
@@ -184,244 +181,4 @@ export async function generateViaOAuth(prompt, quality, size, moderation = "low"
184
181
  timeout.clear();
185
182
  }
186
183
  }
187
- export async function generateMultimodeViaOAuth(prompt, quality, size, moderation = "low", references = [], requestId = null, mode = "auto", ctx = {}, options = {}) {
188
- await waitForOAuthReady(ctx);
189
- const oauthUrl = getOAuthUrl(ctx);
190
- const model = options.model || ctx.config?.imageModels?.default || "gpt-5.4-mini";
191
- const maxGeneratedImages = Math.max(1, Math.trunc(Number(ctx.config?.limits?.maxGeneratedImages) || 24));
192
- const maxImages = Math.min(maxGeneratedImages, Math.max(1, Math.trunc(Number(options.maxImages) || 1)));
193
- const webSearchEnabled = resolveWebSearchEnabled(options);
194
- const tools = buildImageTools(webSearchEnabled, {
195
- quality,
196
- size,
197
- moderation,
198
- ...(options.partialImages ? { partial_images: options.partialImages } : {}),
199
- });
200
- const referenceInputs = references.map(normalizeReferenceForOAuth);
201
- const userText = buildMultimodeSequencePrompt(mode === "direct"
202
- ? `${prompt}${DIRECT_PROMPT_FIDELITY_SUFFIX}`
203
- : `${prompt}${webSearchEnabled ? RESEARCH_SUFFIX : ""}${AUTO_PROMPT_FIDELITY_SUFFIX}`, maxImages, { webSearchEnabled, size });
204
- const userContent = referenceInputs.length
205
- ? [
206
- ...referenceInputs.map(({ b64, requestMime }) => ({
207
- type: "input_image",
208
- image_url: `data:${requestMime};base64,${b64}`,
209
- })),
210
- { type: "input_text", text: userText },
211
- ]
212
- : userText;
213
- logEvent("oauth-multimode", "request", {
214
- requestId,
215
- model,
216
- refsCount: referenceInputs.length,
217
- maxImages,
218
- promptChars: typeof prompt === "string" ? prompt.length : 0,
219
- webSearchEnabled,
220
- });
221
- const reasoningEffort = resolveReasoningEffort(ctx, options);
222
- const developerPrompt = webSearchEnabled ? MULTIMODE_DEVELOPER_PROMPT : MULTIMODE_NO_SEARCH_DEVELOPER_PROMPT;
223
- const timeout = createOAuthGenerationTimeout(ctx, requestId, "oauth-multimode");
224
- try {
225
- const res = await fetchOAuth(`${oauthUrl}/v1/responses`, {
226
- method: "POST",
227
- headers: { "Content-Type": "application/json", Accept: "text/event-stream" },
228
- signal: options.signal || timeout.signal,
229
- body: JSON.stringify({
230
- model,
231
- input: [
232
- { role: "developer", content: `${developerPrompt}\n\nN = ${maxImages}.` },
233
- { role: "user", content: userContent },
234
- ],
235
- tools,
236
- tool_choice: "required",
237
- reasoning: { effort: reasoningEffort },
238
- stream: true,
239
- }),
240
- }, { requestId, scope: "oauth-multimode" });
241
- logEvent("oauth-multimode", "response", {
242
- requestId,
243
- model,
244
- status: res.status,
245
- contentType: res.headers.get("content-type"),
246
- });
247
- if (!res.ok) {
248
- const text = await res.text();
249
- logEvent("oauth-multimode", "error_response", { requestId, status: res.status, errorChars: text.length });
250
- throwOAuthHttpError(res, text, {
251
- requestId,
252
- scope: "oauth-multimode",
253
- fallbackMessage: `OAuth proxy returned ${res.status}`,
254
- });
255
- }
256
- if (requestId)
257
- setJobPhase(requestId, "streaming");
258
- const contentType = res.headers.get("content-type") || "";
259
- if (!contentType.includes("text/event-stream")) {
260
- const json = await res.json();
261
- const images = [];
262
- for (const item of json.output || []) {
263
- if (item.type === "image_generation_call" && item.result && images.length < maxImages) {
264
- images.push({
265
- b64: item.result,
266
- revisedPrompt: typeof item.revised_prompt === "string" ? item.revised_prompt : null,
267
- });
268
- }
269
- }
270
- return {
271
- images,
272
- usage: json.usage || null,
273
- webSearchCalls: 0,
274
- eventCount: 0,
275
- eventTypes: {},
276
- extraIgnored: 0,
277
- };
278
- }
279
- const result = await readMultimodeImageStream(res, {
280
- requestId,
281
- maxImages,
282
- scope: "oauth-multimode",
283
- onPartialImage: options.onPartialImage,
284
- });
285
- logEvent("oauth-multimode", "stream_end", {
286
- requestId,
287
- events: result.eventCount,
288
- imageCount: result.images.length,
289
- extraIgnored: result.extraIgnored,
290
- ...summarizeEventTypes(result.eventTypes),
291
- });
292
- return result;
293
- }
294
- catch (err) {
295
- if (timeout.isTimeoutError(err)) {
296
- throwOAuthTimeoutError(err, { timeoutMs: timeout.timeoutMs, requestId, scope: "oauth-multimode" });
297
- }
298
- throw err;
299
- }
300
- finally {
301
- timeout.clear();
302
- }
303
- }
304
- export async function editViaOAuth(prompt, imageB64, quality, size, moderation = "low", mode = "auto", ctx = {}, requestId = null, options = {}) {
305
- await waitForOAuthReady(ctx);
306
- const maskPresent = typeof options.mask === "string" && options.mask.length > 0;
307
- if (maskPresent && !ctx.config?.oauth?.maskedEditEnabled) {
308
- logEvent("oauth-edit", "mask_unsupported", { requestId, maskPresent: true });
309
- const err = new Error("Masked edit is not supported by the current OAuth image provider");
310
- err.status = 400;
311
- err.code = "EDIT_MASK_NOT_SUPPORTED";
312
- throw err;
313
- }
314
- if (maskPresent) {
315
- // TODO(#31): enable upstream mask payload after STEP-0 verification
316
- logEvent("oauth-edit", "mask_unsupported", { requestId, maskPresent: true });
317
- const err = new Error("Masked edit is not supported by the current OAuth image provider");
318
- err.status = 400;
319
- err.code = "EDIT_MASK_NOT_SUPPORTED";
320
- throw err;
321
- }
322
- const oauthUrl = getOAuthUrl(ctx);
323
- const model = options.model || ctx.config?.imageModels?.default || "gpt-5.4-mini";
324
- const webSearchEnabled = resolveWebSearchEnabled(options);
325
- const textPrompt = buildEditTextPrompt(prompt, mode, { webSearchEnabled, size });
326
- const imageForRequest = await compressReferenceB64ForOAuth(imageB64, {
327
- maxB64Bytes: ctx.config?.limits?.maxRefB64Bytes,
328
- force: true,
329
- });
330
- const references = Array.isArray(options.references) ? options.references : [];
331
- const referenceImagesForRequest = await Promise.all(references.map((ref) => compressReferenceB64ForOAuth(typeof ref === "string" ? ref : ref?.b64, {
332
- maxB64Bytes: ctx.config?.limits?.maxRefB64Bytes,
333
- force: true,
334
- })));
335
- const referenceContent = referenceImagesForRequest.map(({ b64 }) => ({
336
- type: "input_image",
337
- image_url: `data:image/jpeg;base64,${b64}`,
338
- }));
339
- const tools = buildImageTools(webSearchEnabled, { quality, size, moderation });
340
- logEvent("oauth-edit", "request", {
341
- requestId,
342
- model,
343
- refsCount: references.length,
344
- inputImageCount: 1 + references.length,
345
- parentImagePresent: true,
346
- webSearchEnabled,
347
- inputImageCompressed: imageForRequest.compressed,
348
- inputImageChars: imageForRequest.inputBytes,
349
- inputImageRequestChars: imageForRequest.outputBytes,
350
- });
351
- const reasoningEffort = resolveReasoningEffort(ctx, options);
352
- const developerPrompt = webSearchEnabled ? EDIT_DEVELOPER_PROMPT : EDIT_NO_SEARCH_DEVELOPER_PROMPT;
353
- const timeout = createOAuthGenerationTimeout(ctx, requestId, "oauth-edit");
354
- try {
355
- const res = await fetchOAuth(`${oauthUrl}/v1/responses`, {
356
- method: "POST",
357
- headers: { "Content-Type": "application/json", Accept: "text/event-stream" },
358
- signal: timeout.signal,
359
- body: JSON.stringify({
360
- model,
361
- input: [
362
- { role: "developer", content: developerPrompt },
363
- {
364
- role: "user",
365
- content: [
366
- { type: "input_image", image_url: `data:image/jpeg;base64,${imageForRequest.b64}` },
367
- ...referenceContent,
368
- { type: "input_text", text: textPrompt },
369
- ],
370
- },
371
- ],
372
- tools,
373
- tool_choice: "required",
374
- reasoning: { effort: reasoningEffort },
375
- stream: true,
376
- }),
377
- }, { requestId, scope: "oauth-edit" });
378
- logEvent("oauth-edit", "response", {
379
- requestId,
380
- model,
381
- status: res.status,
382
- contentType: res.headers.get("content-type"),
383
- });
384
- if (!res.ok) {
385
- const text = await res.text();
386
- logEvent("oauth-edit", "error_response", { requestId, status: res.status, errorChars: text.length });
387
- throwOAuthHttpError(res, text, {
388
- requestId,
389
- scope: "oauth-edit",
390
- fallbackMessage: `OAuth edit returned ${res.status}`,
391
- });
392
- }
393
- if (requestId)
394
- setJobPhase(requestId, "streaming");
395
- const { imageB64: resultB64, usage, revisedPrompt, webSearchCalls, eventCount, eventTypes } = await readImageStream(res, {
396
- scope: "oauth-edit",
397
- requestId,
398
- });
399
- logEvent("oauth-edit", "stream_end", {
400
- requestId,
401
- events: eventCount,
402
- hasImage: !!resultB64,
403
- ...summarizeEventTypes(eventTypes),
404
- });
405
- if (resultB64)
406
- return { b64: resultB64, usage, revisedPrompt, webSearchCalls };
407
- const emptyErr = new Error("No image data received from OAuth edit");
408
- emptyErr.eventCount = eventCount;
409
- emptyErr.eventTypes = eventTypes;
410
- emptyErr.size = size;
411
- emptyErr.quality = quality;
412
- emptyErr.model = model;
413
- emptyErr.refsCount = references.length;
414
- emptyErr.inputImageCount = 1 + references.length;
415
- emptyErr.parentImagePresent = true;
416
- throw emptyErr;
417
- }
418
- catch (err) {
419
- if (timeout.isTimeoutError(err)) {
420
- throwOAuthTimeoutError(err, { timeoutMs: timeout.timeoutMs, requestId, scope: "oauth-edit" });
421
- }
422
- throw err;
423
- }
424
- finally {
425
- timeout.clear();
426
- }
427
- }
184
+ export { generateMultimodeViaOAuth, editViaOAuth } from "./multimodeGenerators.js";
@@ -0,0 +1,251 @@
1
+ import { setJobPhase } from "../inflight.js";
2
+ import { logEvent } from "../logger.js";
3
+ import { compressReferenceB64ForOAuth } from "../referenceImageCompress.js";
4
+ import { throwOAuthHttpError, throwOAuthTimeoutError } from "./errors.js";
5
+ import { AUTO_PROMPT_FIDELITY_SUFFIX, DIRECT_PROMPT_FIDELITY_SUFFIX, EDIT_DEVELOPER_PROMPT, EDIT_NO_SEARCH_DEVELOPER_PROMPT, MULTIMODE_DEVELOPER_PROMPT, MULTIMODE_NO_SEARCH_DEVELOPER_PROMPT, buildEditTextPrompt, buildMultimodeSequencePrompt, } from "./prompts.js";
6
+ import { normalizeReferenceForOAuth } from "./references.js";
7
+ import { buildImageTools, createOAuthGenerationTimeout, fetchOAuth, getOAuthUrl, resolveReasoningEffort, resolveWebSearchEnabled, summarizeEventTypes, waitForOAuthReady, } from "./runtime.js";
8
+ import { readImageStream, readMultimodeImageStream } from "./streams.js";
9
+ import { config } from "../../config.js";
10
+ const RESEARCH_SUFFIX = config.oauth.researchSuffix;
11
+ export async function generateMultimodeViaOAuth(prompt, quality, size, moderation = "low", references = [], requestId = null, mode = "auto", ctx = {}, options = {}) {
12
+ await waitForOAuthReady(ctx);
13
+ const oauthUrl = getOAuthUrl(ctx);
14
+ const model = options.model || ctx.config?.imageModels?.default || "gpt-5.4-mini";
15
+ const maxGeneratedImages = Math.max(1, Math.trunc(Number(ctx.config?.limits?.maxGeneratedImages) || 24));
16
+ const maxImages = Math.min(maxGeneratedImages, Math.max(1, Math.trunc(Number(options.maxImages) || 1)));
17
+ const webSearchEnabled = resolveWebSearchEnabled(options);
18
+ const tools = buildImageTools(webSearchEnabled, {
19
+ quality,
20
+ size,
21
+ moderation,
22
+ ...(options.partialImages ? { partial_images: options.partialImages } : {}),
23
+ });
24
+ const referenceInputs = references.map(normalizeReferenceForOAuth);
25
+ const userText = buildMultimodeSequencePrompt(mode === "direct"
26
+ ? `${prompt}${DIRECT_PROMPT_FIDELITY_SUFFIX}`
27
+ : `${prompt}${webSearchEnabled ? RESEARCH_SUFFIX : ""}${AUTO_PROMPT_FIDELITY_SUFFIX}`, maxImages, { webSearchEnabled, size });
28
+ const userContent = referenceInputs.length
29
+ ? [
30
+ ...referenceInputs.map(({ b64, requestMime }) => ({
31
+ type: "input_image",
32
+ image_url: `data:${requestMime};base64,${b64}`,
33
+ })),
34
+ { type: "input_text", text: userText },
35
+ ]
36
+ : userText;
37
+ logEvent("oauth-multimode", "request", {
38
+ requestId,
39
+ model,
40
+ refsCount: referenceInputs.length,
41
+ maxImages,
42
+ promptChars: typeof prompt === "string" ? prompt.length : 0,
43
+ webSearchEnabled,
44
+ });
45
+ const reasoningEffort = resolveReasoningEffort(ctx, options);
46
+ const developerPrompt = webSearchEnabled ? MULTIMODE_DEVELOPER_PROMPT : MULTIMODE_NO_SEARCH_DEVELOPER_PROMPT;
47
+ const timeout = createOAuthGenerationTimeout(ctx, requestId, "oauth-multimode");
48
+ try {
49
+ const res = await fetchOAuth(`${oauthUrl}/v1/responses`, {
50
+ method: "POST",
51
+ headers: { "Content-Type": "application/json", Accept: "text/event-stream" },
52
+ signal: options.signal || timeout.signal,
53
+ body: JSON.stringify({
54
+ model,
55
+ input: [
56
+ { role: "developer", content: `${developerPrompt}\n\nN = ${maxImages}.` },
57
+ { role: "user", content: userContent },
58
+ ],
59
+ tools,
60
+ tool_choice: "required",
61
+ reasoning: { effort: reasoningEffort },
62
+ stream: true,
63
+ }),
64
+ }, { requestId, scope: "oauth-multimode" });
65
+ logEvent("oauth-multimode", "response", {
66
+ requestId,
67
+ model,
68
+ status: res.status,
69
+ contentType: res.headers.get("content-type"),
70
+ });
71
+ if (!res.ok) {
72
+ const text = await res.text();
73
+ logEvent("oauth-multimode", "error_response", { requestId, status: res.status, errorChars: text.length });
74
+ throwOAuthHttpError(res, text, {
75
+ requestId,
76
+ scope: "oauth-multimode",
77
+ fallbackMessage: `OAuth proxy returned ${res.status}`,
78
+ });
79
+ }
80
+ if (requestId)
81
+ setJobPhase(requestId, "streaming");
82
+ const contentType = res.headers.get("content-type") || "";
83
+ if (!contentType.includes("text/event-stream")) {
84
+ const json = await res.json();
85
+ const images = [];
86
+ for (const item of json.output || []) {
87
+ if (item.type === "image_generation_call" && item.result && images.length < maxImages) {
88
+ images.push({
89
+ b64: item.result,
90
+ revisedPrompt: typeof item.revised_prompt === "string" ? item.revised_prompt : null,
91
+ });
92
+ }
93
+ }
94
+ return {
95
+ images,
96
+ usage: json.usage || null,
97
+ webSearchCalls: 0,
98
+ eventCount: 0,
99
+ eventTypes: {},
100
+ extraIgnored: 0,
101
+ };
102
+ }
103
+ const result = await readMultimodeImageStream(res, {
104
+ requestId,
105
+ maxImages,
106
+ scope: "oauth-multimode",
107
+ onPartialImage: options.onPartialImage,
108
+ });
109
+ logEvent("oauth-multimode", "stream_end", {
110
+ requestId,
111
+ events: result.eventCount,
112
+ imageCount: result.images.length,
113
+ extraIgnored: result.extraIgnored,
114
+ ...summarizeEventTypes(result.eventTypes),
115
+ });
116
+ return result;
117
+ }
118
+ catch (err) {
119
+ if (timeout.isTimeoutError(err)) {
120
+ throwOAuthTimeoutError(err, { timeoutMs: timeout.timeoutMs, requestId, scope: "oauth-multimode" });
121
+ }
122
+ throw err;
123
+ }
124
+ finally {
125
+ timeout.clear();
126
+ }
127
+ }
128
+ export async function editViaOAuth(prompt, imageB64, quality, size, moderation = "low", mode = "auto", ctx = {}, requestId = null, options = {}) {
129
+ await waitForOAuthReady(ctx);
130
+ const maskPresent = typeof options.mask === "string" && options.mask.length > 0;
131
+ if (maskPresent && !ctx.config?.oauth?.maskedEditEnabled) {
132
+ logEvent("oauth-edit", "mask_unsupported", { requestId, maskPresent: true });
133
+ const err = new Error("Masked edit is not supported by the current OAuth image provider");
134
+ err.status = 400;
135
+ err.code = "EDIT_MASK_NOT_SUPPORTED";
136
+ throw err;
137
+ }
138
+ if (maskPresent) {
139
+ // TODO(#31): enable upstream mask payload after STEP-0 verification
140
+ logEvent("oauth-edit", "mask_unsupported", { requestId, maskPresent: true });
141
+ const err = new Error("Masked edit is not supported by the current OAuth image provider");
142
+ err.status = 400;
143
+ err.code = "EDIT_MASK_NOT_SUPPORTED";
144
+ throw err;
145
+ }
146
+ const oauthUrl = getOAuthUrl(ctx);
147
+ const model = options.model || ctx.config?.imageModels?.default || "gpt-5.4-mini";
148
+ const webSearchEnabled = resolveWebSearchEnabled(options);
149
+ const textPrompt = buildEditTextPrompt(prompt, mode, { webSearchEnabled, size });
150
+ const imageForRequest = await compressReferenceB64ForOAuth(imageB64, {
151
+ maxB64Bytes: ctx.config?.limits?.maxRefB64Bytes,
152
+ force: true,
153
+ });
154
+ const references = Array.isArray(options.references) ? options.references : [];
155
+ const referenceImagesForRequest = await Promise.all(references.map((ref) => compressReferenceB64ForOAuth(typeof ref === "string" ? ref : ref?.b64, {
156
+ maxB64Bytes: ctx.config?.limits?.maxRefB64Bytes,
157
+ force: true,
158
+ })));
159
+ const referenceContent = referenceImagesForRequest.map(({ b64 }) => ({
160
+ type: "input_image",
161
+ image_url: `data:image/jpeg;base64,${b64}`,
162
+ }));
163
+ const tools = buildImageTools(webSearchEnabled, { quality, size, moderation });
164
+ logEvent("oauth-edit", "request", {
165
+ requestId,
166
+ model,
167
+ refsCount: references.length,
168
+ inputImageCount: 1 + references.length,
169
+ parentImagePresent: true,
170
+ webSearchEnabled,
171
+ inputImageCompressed: imageForRequest.compressed,
172
+ inputImageChars: imageForRequest.inputBytes,
173
+ inputImageRequestChars: imageForRequest.outputBytes,
174
+ });
175
+ const reasoningEffort = resolveReasoningEffort(ctx, options);
176
+ const developerPrompt = webSearchEnabled ? EDIT_DEVELOPER_PROMPT : EDIT_NO_SEARCH_DEVELOPER_PROMPT;
177
+ const timeout = createOAuthGenerationTimeout(ctx, requestId, "oauth-edit");
178
+ try {
179
+ const res = await fetchOAuth(`${oauthUrl}/v1/responses`, {
180
+ method: "POST",
181
+ headers: { "Content-Type": "application/json", Accept: "text/event-stream" },
182
+ signal: timeout.signal,
183
+ body: JSON.stringify({
184
+ model,
185
+ input: [
186
+ { role: "developer", content: developerPrompt },
187
+ {
188
+ role: "user",
189
+ content: [
190
+ { type: "input_image", image_url: `data:image/jpeg;base64,${imageForRequest.b64}` },
191
+ ...referenceContent,
192
+ { type: "input_text", text: textPrompt },
193
+ ],
194
+ },
195
+ ],
196
+ tools,
197
+ tool_choice: "required",
198
+ reasoning: { effort: reasoningEffort },
199
+ stream: true,
200
+ }),
201
+ }, { requestId, scope: "oauth-edit" });
202
+ logEvent("oauth-edit", "response", {
203
+ requestId,
204
+ model,
205
+ status: res.status,
206
+ contentType: res.headers.get("content-type"),
207
+ });
208
+ if (!res.ok) {
209
+ const text = await res.text();
210
+ logEvent("oauth-edit", "error_response", { requestId, status: res.status, errorChars: text.length });
211
+ throwOAuthHttpError(res, text, {
212
+ requestId,
213
+ scope: "oauth-edit",
214
+ fallbackMessage: `OAuth edit returned ${res.status}`,
215
+ });
216
+ }
217
+ if (requestId)
218
+ setJobPhase(requestId, "streaming");
219
+ const { imageB64: resultB64, usage, revisedPrompt, webSearchCalls, eventCount, eventTypes } = await readImageStream(res, {
220
+ scope: "oauth-edit",
221
+ requestId,
222
+ });
223
+ logEvent("oauth-edit", "stream_end", {
224
+ requestId,
225
+ events: eventCount,
226
+ hasImage: !!resultB64,
227
+ ...summarizeEventTypes(eventTypes),
228
+ });
229
+ if (resultB64)
230
+ return { b64: resultB64, usage, revisedPrompt, webSearchCalls };
231
+ const emptyErr = new Error("No image data received from OAuth edit");
232
+ emptyErr.eventCount = eventCount;
233
+ emptyErr.eventTypes = eventTypes;
234
+ emptyErr.size = size;
235
+ emptyErr.quality = quality;
236
+ emptyErr.model = model;
237
+ emptyErr.refsCount = references.length;
238
+ emptyErr.inputImageCount = 1 + references.length;
239
+ emptyErr.parentImagePresent = true;
240
+ throw emptyErr;
241
+ }
242
+ catch (err) {
243
+ if (timeout.isTimeoutError(err)) {
244
+ throwOAuthTimeoutError(err, { timeoutMs: timeout.timeoutMs, requestId, scope: "oauth-edit" });
245
+ }
246
+ throw err;
247
+ }
248
+ finally {
249
+ timeout.clear();
250
+ }
251
+ }
@@ -12,8 +12,15 @@ export function imageFormatFromMime(mime) {
12
12
  return "png";
13
13
  }
14
14
  export function writeSse(res, event, data) {
15
- res.write(`event: ${event}\n`);
16
- res.write(`data: ${JSON.stringify(data)}\n\n`);
15
+ if (res.writableEnded || res.destroyed)
16
+ return false;
17
+ try {
18
+ res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
19
+ return true;
20
+ }
21
+ catch {
22
+ return false;
23
+ }
17
24
  }
18
25
  export function dataUrlFromB64(format, b64) {
19
26
  return `data:image/${format === "jpeg" ? "jpeg" : format};base64,${b64}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ima2-gen",
3
- "version": "2.0.15",
3
+ "version": "2.0.16",
4
4
  "packageManager": "npm@11.18.0",
5
5
  "description": "Local OAuth image generation studio with classic and node workflows",
6
6
  "type": "module",
@@ -105,5 +105,5 @@
105
105
  "tsx": "^4.21.0",
106
106
  "typescript": "^5.9.3"
107
107
  },
108
- "gitHead": "ab86116ee1d23040e7ab63e49d8a6d4e19eddabd"
108
+ "gitHead": "579055388e5959dcca3f6bcb3e2e61409ead0a7e"
109
109
  }
package/routes/agent.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { appendAgentTurn, compactAgentSession, createAgentSession, deleteAgentSession, getAgentGenerationSettings, getAgentSession, getAgentWorkspacePayload, importAgentImage, renameAgentSession, setAgentCurrentImage, setAgentGenerationSettings, setAgentLocks, setAgentWebSearch, } from "../lib/agentStore.js";
2
- import { cancelAgentQueueItem, createAgentQueueItem, getAgentGenerationErrors, getAgentQueueItem, listAgentQueueItems, retryAgentQueueItem, } from "../lib/agentQueueStore.js";
3
- import { ensureAgentQueueWorker, tickAgentQueueWorker } from "../lib/agentQueueWorker.js";
2
+ import { createAgentQueueItem, getAgentGenerationErrors, getAgentQueueItem, listAgentQueueItems, retryAgentQueueItem, } from "../lib/agentQueueStore.js";
3
+ import { cancelRunningAgentQueueItem, ensureAgentQueueWorker, tickAgentQueueWorker } from "../lib/agentQueueWorker.js";
4
4
  import { parseAgentSlashCommand, formatAgentQuestionReply, formatAgentSlashHelp } from "../lib/agentCommandParser.js";
5
5
  import { requestAgentQuestionAnswer } from "../lib/agentQuestionResponder.js";
6
6
  import { agentAllowedToolPayload, runAgentTurn } from "../lib/agentRuntime.js";
@@ -181,9 +181,9 @@ export function registerAgentRoutes(app, ctxRaw) {
181
181
  const item = getAgentQueueItem(req.params.itemId);
182
182
  if (!item)
183
183
  return sendError(res, queueItemNotFound(req.params.itemId));
184
- const ok = cancelAgentQueueItem(item.id);
184
+ const ok = cancelRunningAgentQueueItem(item.id);
185
185
  if (!ok)
186
- return sendError(res, queueActionError("AGENT_QUEUE_CANCEL_FAILED", "Only queued Agent work can be canceled."));
186
+ return sendError(res, queueActionError("AGENT_QUEUE_CANCEL_FAILED", "Only queued or running Agent work can be canceled."));
187
187
  res.json(getAgentWorkspacePayload(item.sessionId));
188
188
  });
189
189
  app.post("/api/agent/queue/:itemId/retry", (req, res) => {
@@ -1,5 +1,5 @@
1
1
  import express from "express";
2
- import { createCanvasVersion, updateCanvasVersion } from "../lib/canvasVersionStore.js";
2
+ import { createCanvasVersion, recordCanvasAnnotationBake, revertCanvasAnnotations, updateCanvasVersion, } from "../lib/canvasVersionStore.js";
3
3
  import { errInfo } from "../lib/errInfo.js";
4
4
  import { requireRuntimeContext } from "../lib/runtimeContext.js";
5
5
  function decodeHeader(value) {
@@ -51,6 +51,7 @@ export function registerCanvasVersionRoutes(app, ctxRaw) {
51
51
  sourceFilename,
52
52
  prompt: getPrompt(req),
53
53
  buffer: getRequestBuffer(req),
54
+ pixelEdited: req.headers["x-ima2-canvas-pixel-edited"] === "true",
54
55
  });
55
56
  res.json({ item });
56
57
  }
@@ -62,4 +63,23 @@ export function registerCanvasVersionRoutes(app, ctxRaw) {
62
63
  });
63
64
  }
64
65
  });
66
+ app.put("/api/canvas-versions/:filename/annotation-bake", express.json({ limit: ctx.config.server.bodyLimit }), async (req, res) => {
67
+ try {
68
+ const item = await recordCanvasAnnotationBake(ctx, decodeURIComponent(req.params.filename), req.body?.snapshot, req.body?.annotationOnly === true);
69
+ res.json({ item });
70
+ }
71
+ catch (e) {
72
+ const err = errInfo(e);
73
+ res.status(err.status || 500).json({ error: err.message, code: err.code || "CANVAS_ANNOTATION_BAKE_FAILED" });
74
+ }
75
+ });
76
+ app.post("/api/canvas-versions/:filename/revert-annotations", async (req, res) => {
77
+ try {
78
+ res.json(await revertCanvasAnnotations(ctx, decodeURIComponent(req.params.filename)));
79
+ }
80
+ catch (e) {
81
+ const err = errInfo(e);
82
+ res.status(err.status || 500).json({ error: err.message, code: err.code || "CANVAS_ANNOTATION_REVERT_FAILED" });
83
+ }
84
+ });
65
85
  }
@@ -1,7 +1,7 @@
1
1
  import { listImageTemplates, readTemplatePreview } from "../lib/cardNewsTemplateStore.js";
2
2
  import { listRoleTemplates } from "../lib/cardNewsRoleTemplateStore.js";
3
3
  import { createCardNewsDraft } from "../lib/cardNewsPlanner.js";
4
- import { generateCardNewsSet } from "../lib/cardNewsGenerator.js";
4
+ import { generateCardNewsSet, validateCardNewsInput } from "../lib/cardNewsGenerator.js";
5
5
  import { createCardNewsJob, finishCardNewsJob, getCardNewsJob, getCardNewsJobPlan, retryCardNewsJob, updateCardNewsJob, updateCardNewsJobCard, } from "../lib/cardNewsJobStore.js";
6
6
  import { listCardNewsSets, readCardNewsManifest, readCardNewsSetPlan } from "../lib/cardNewsManifestStore.js";
7
7
  import { errInfo } from "../lib/errInfo.js";
@@ -126,6 +126,7 @@ export function registerCardNewsRoutes(app, ctxRaw) {
126
126
  app.post("/api/cardnews/jobs", (req, res) => {
127
127
  try {
128
128
  const body = (req.body ?? {});
129
+ validateCardNewsInput(body);
129
130
  const summary = createCardNewsJob(body);
130
131
  runCardNewsJob(ctx, summary.jobId, body);
131
132
  res.status(202).json(summary);