ima2-gen 1.1.8 → 1.1.10

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 (255) hide show
  1. package/README.md +60 -27
  2. package/bin/commands/annotate.js +139 -0
  3. package/bin/commands/annotate.ts +119 -0
  4. package/bin/commands/cancel.js +40 -33
  5. package/bin/commands/cancel.ts +48 -0
  6. package/bin/commands/canvas-versions.js +91 -0
  7. package/bin/commands/canvas-versions.ts +80 -0
  8. package/bin/commands/cardnews.js +295 -0
  9. package/bin/commands/cardnews.ts +249 -0
  10. package/bin/commands/comfy.js +63 -0
  11. package/bin/commands/comfy.ts +54 -0
  12. package/bin/commands/config.js +270 -0
  13. package/bin/commands/config.ts +265 -0
  14. package/bin/commands/edit.js +100 -72
  15. package/bin/commands/edit.ts +119 -0
  16. package/bin/commands/gen.js +143 -118
  17. package/bin/commands/gen.ts +179 -0
  18. package/bin/commands/history.js +165 -0
  19. package/bin/commands/history.ts +146 -0
  20. package/bin/commands/ls.js +63 -42
  21. package/bin/commands/ls.ts +63 -0
  22. package/bin/commands/metadata.js +48 -0
  23. package/bin/commands/metadata.ts +39 -0
  24. package/bin/commands/multimode.js +159 -0
  25. package/bin/commands/multimode.ts +145 -0
  26. package/bin/commands/node.js +176 -0
  27. package/bin/commands/node.ts +157 -0
  28. package/bin/commands/observability.js +201 -0
  29. package/bin/commands/observability.ts +176 -0
  30. package/bin/commands/ping.js +28 -20
  31. package/bin/commands/ping.ts +31 -0
  32. package/bin/commands/prompt.js +509 -0
  33. package/bin/commands/prompt.ts +423 -0
  34. package/bin/commands/ps.js +81 -71
  35. package/bin/commands/ps.ts +81 -0
  36. package/bin/commands/session.js +309 -0
  37. package/bin/commands/session.ts +266 -0
  38. package/bin/commands/show.js +78 -40
  39. package/bin/commands/show.ts +72 -0
  40. package/bin/ima2.js +325 -311
  41. package/bin/ima2.ts +444 -0
  42. package/bin/lib/args.js +75 -66
  43. package/bin/lib/args.ts +92 -0
  44. package/bin/lib/browser-id.js +15 -0
  45. package/bin/lib/browser-id.ts +16 -0
  46. package/bin/lib/client.js +91 -83
  47. package/bin/lib/client.ts +122 -0
  48. package/bin/lib/error-hints.js +14 -17
  49. package/bin/lib/error-hints.ts +23 -0
  50. package/bin/lib/files.js +26 -28
  51. package/bin/lib/files.ts +39 -0
  52. package/bin/lib/output.js +46 -42
  53. package/bin/lib/output.ts +73 -0
  54. package/bin/lib/platform.js +62 -56
  55. package/bin/lib/platform.ts +99 -0
  56. package/bin/lib/sse.js +73 -0
  57. package/bin/lib/sse.ts +73 -0
  58. package/bin/lib/star-prompt.js +69 -76
  59. package/bin/lib/star-prompt.ts +97 -0
  60. package/bin/lib/storage-doctor.js +34 -35
  61. package/bin/lib/storage-doctor.ts +39 -0
  62. package/config.js +147 -243
  63. package/config.ts +333 -0
  64. package/docs/API.md +48 -8
  65. package/docs/CLI.md +190 -0
  66. package/docs/FAQ.ko.md +5 -5
  67. package/docs/FAQ.md +5 -5
  68. package/docs/README.ja.md +71 -25
  69. package/docs/README.ko.md +61 -24
  70. package/docs/README.zh-CN.md +73 -27
  71. package/docs/migration/runtime-test-inventory.md +135 -0
  72. package/lib/assetLifecycle.js +130 -130
  73. package/lib/assetLifecycle.ts +142 -0
  74. package/lib/canvasVersionStore.js +135 -153
  75. package/lib/canvasVersionStore.ts +221 -0
  76. package/lib/cardNewsGenerator.js +132 -143
  77. package/lib/cardNewsGenerator.ts +265 -0
  78. package/lib/cardNewsJobStore.js +78 -84
  79. package/lib/cardNewsJobStore.ts +142 -0
  80. package/lib/cardNewsManifestStore.js +98 -96
  81. package/lib/cardNewsManifestStore.ts +154 -0
  82. package/lib/cardNewsPlanner.js +159 -152
  83. package/lib/cardNewsPlanner.ts +236 -0
  84. package/lib/cardNewsPlannerClient.js +114 -98
  85. package/lib/cardNewsPlannerClient.ts +155 -0
  86. package/lib/cardNewsPlannerPrompt.js +56 -56
  87. package/lib/cardNewsPlannerPrompt.ts +60 -0
  88. package/lib/cardNewsPlannerSchema.js +238 -223
  89. package/lib/cardNewsPlannerSchema.ts +321 -0
  90. package/lib/cardNewsRoleTemplateStore.js +39 -41
  91. package/lib/cardNewsRoleTemplateStore.ts +47 -0
  92. package/lib/cardNewsTemplateStore.js +156 -175
  93. package/lib/cardNewsTemplateStore.ts +252 -0
  94. package/lib/codexDetect.js +46 -47
  95. package/lib/codexDetect.ts +71 -0
  96. package/lib/comfyBridge.js +166 -184
  97. package/lib/comfyBridge.ts +235 -0
  98. package/lib/db.js +41 -51
  99. package/lib/db.ts +166 -0
  100. package/lib/errInfo.js +32 -0
  101. package/lib/errInfo.ts +43 -0
  102. package/lib/errorClassify.js +62 -78
  103. package/lib/errorClassify.ts +100 -0
  104. package/lib/generationErrors.js +140 -103
  105. package/lib/generationErrors.ts +151 -0
  106. package/lib/historyList.js +153 -147
  107. package/lib/historyList.ts +168 -0
  108. package/lib/imageMetadata.js +86 -89
  109. package/lib/imageMetadata.ts +111 -0
  110. package/lib/imageMetadataStore.js +46 -51
  111. package/lib/imageMetadataStore.ts +67 -0
  112. package/lib/imageModels.js +38 -45
  113. package/lib/imageModels.ts +54 -0
  114. package/lib/inflight.js +131 -157
  115. package/lib/inflight.ts +247 -0
  116. package/lib/localImportStore.js +87 -93
  117. package/lib/localImportStore.ts +112 -0
  118. package/lib/logger.js +107 -112
  119. package/lib/logger.ts +161 -0
  120. package/lib/nodeStore.js +65 -64
  121. package/lib/nodeStore.ts +89 -0
  122. package/lib/oauthLauncher.js +61 -59
  123. package/lib/oauthLauncher.ts +65 -0
  124. package/lib/oauthNormalize.js +15 -19
  125. package/lib/oauthNormalize.ts +30 -0
  126. package/lib/oauthProxy/errors.js +93 -0
  127. package/lib/oauthProxy/errors.ts +128 -0
  128. package/lib/oauthProxy/generators.js +426 -0
  129. package/lib/oauthProxy/generators.ts +494 -0
  130. package/lib/oauthProxy/index.js +8 -0
  131. package/lib/oauthProxy/index.ts +28 -0
  132. package/lib/oauthProxy/prompts.js +84 -0
  133. package/lib/oauthProxy/prompts.ts +108 -0
  134. package/lib/oauthProxy/references.js +32 -0
  135. package/lib/oauthProxy/references.ts +45 -0
  136. package/lib/oauthProxy/runtime.js +101 -0
  137. package/lib/oauthProxy/runtime.ts +115 -0
  138. package/lib/oauthProxy/streams.js +211 -0
  139. package/lib/oauthProxy/streams.ts +232 -0
  140. package/lib/oauthProxy/types.js +6 -0
  141. package/lib/oauthProxy/types.ts +9 -0
  142. package/lib/oauthProxy.js +3 -909
  143. package/lib/oauthProxy.ts +3 -0
  144. package/lib/openDirectory.js +43 -40
  145. package/lib/openDirectory.ts +47 -0
  146. package/lib/pngInfo.js +18 -20
  147. package/lib/pngInfo.ts +26 -0
  148. package/lib/promptImport/curatedSources.js +125 -129
  149. package/lib/promptImport/curatedSources.ts +141 -0
  150. package/lib/promptImport/discoveryRegistry.js +198 -208
  151. package/lib/promptImport/discoveryRegistry.ts +329 -0
  152. package/lib/promptImport/errors.js +10 -10
  153. package/lib/promptImport/errors.ts +18 -0
  154. package/lib/promptImport/githubDiscovery.js +216 -220
  155. package/lib/promptImport/githubDiscovery.ts +309 -0
  156. package/lib/promptImport/githubFolder.js +261 -259
  157. package/lib/promptImport/githubFolder.ts +397 -0
  158. package/lib/promptImport/githubSource.js +191 -200
  159. package/lib/promptImport/githubSource.ts +257 -0
  160. package/lib/promptImport/gptImageHints.js +49 -56
  161. package/lib/promptImport/gptImageHints.ts +70 -0
  162. package/lib/promptImport/parsePromptCandidates.js +110 -123
  163. package/lib/promptImport/parsePromptCandidates.ts +179 -0
  164. package/lib/promptImport/promptIndex.js +203 -211
  165. package/lib/promptImport/promptIndex.ts +326 -0
  166. package/lib/promptImport/rankPromptCandidates.js +46 -43
  167. package/lib/promptImport/rankPromptCandidates.ts +65 -0
  168. package/lib/promptImport/types.js +1 -0
  169. package/lib/promptImport/types.ts +103 -0
  170. package/lib/promptSafetyPolicy.js +5 -0
  171. package/lib/promptSafetyPolicy.ts +5 -0
  172. package/lib/providerOptions.js +31 -0
  173. package/lib/providerOptions.ts +42 -0
  174. package/lib/referenceImageCompress.js +51 -62
  175. package/lib/referenceImageCompress.ts +84 -0
  176. package/lib/refs.js +95 -81
  177. package/lib/refs.ts +133 -0
  178. package/lib/requestLogger.js +32 -38
  179. package/lib/requestLogger.ts +49 -0
  180. package/lib/responsesImageAdapter.js +357 -0
  181. package/lib/responsesImageAdapter.ts +430 -0
  182. package/lib/runtimeContext.js +100 -0
  183. package/lib/runtimeContext.ts +131 -0
  184. package/lib/runtimePorts.js +72 -73
  185. package/lib/runtimePorts.ts +105 -0
  186. package/lib/sessionStore.js +183 -232
  187. package/lib/sessionStore.ts +308 -0
  188. package/lib/storageMigration.js +254 -245
  189. package/lib/storageMigration.ts +310 -0
  190. package/lib/styleSheet.js +87 -90
  191. package/lib/styleSheet.ts +139 -0
  192. package/lib/systemTrash.js +18 -0
  193. package/lib/systemTrash.ts +20 -0
  194. package/package.json +28 -10
  195. package/routes/annotations.js +81 -79
  196. package/routes/annotations.ts +118 -0
  197. package/routes/canvasVersions.js +56 -55
  198. package/routes/canvasVersions.ts +69 -0
  199. package/routes/cardNews.js +172 -172
  200. package/routes/cardNews.ts +211 -0
  201. package/routes/comfy.js +27 -32
  202. package/routes/comfy.ts +43 -0
  203. package/routes/edit.js +188 -215
  204. package/routes/edit.ts +250 -0
  205. package/routes/generate.js +282 -292
  206. package/routes/generate.ts +323 -0
  207. package/routes/health.js +107 -108
  208. package/routes/health.ts +119 -0
  209. package/routes/history.js +152 -145
  210. package/routes/history.ts +171 -0
  211. package/routes/imageImport.js +31 -27
  212. package/routes/imageImport.ts +37 -0
  213. package/routes/index.js +20 -18
  214. package/routes/index.ts +38 -0
  215. package/routes/metadata.js +66 -65
  216. package/routes/metadata.ts +77 -0
  217. package/routes/multimode.js +235 -264
  218. package/routes/multimode.ts +301 -0
  219. package/routes/nodes.js +389 -425
  220. package/routes/nodes.ts +494 -0
  221. package/routes/promptImport.js +291 -325
  222. package/routes/promptImport.ts +379 -0
  223. package/routes/prompts.js +357 -361
  224. package/routes/prompts.ts +428 -0
  225. package/routes/sessions.js +299 -285
  226. package/routes/sessions.ts +317 -0
  227. package/routes/storage.js +32 -32
  228. package/routes/storage.ts +47 -0
  229. package/server.js +193 -196
  230. package/server.ts +255 -0
  231. package/ui/dist/.vite/manifest.json +102 -0
  232. package/ui/dist/assets/CardNewsWorkspace-C9Cpxuxc.js +2 -0
  233. package/ui/dist/assets/NodeCanvas-BZV40eAE.css +1 -0
  234. package/ui/dist/assets/NodeCanvas-BllpfcQW.js +7 -0
  235. package/ui/dist/assets/PromptImportDialog-D8EMO--u.js +2 -0
  236. package/ui/dist/assets/PromptImportDiscoverySection-BB2FrKuq.js +1 -0
  237. package/ui/dist/assets/PromptImportFolderSection-aVteBUcb.js +1 -0
  238. package/ui/dist/assets/PromptLibraryPanel-Z-4B8RSs.js +2 -0
  239. package/ui/dist/assets/SettingsWorkspace-DBYdgpPI.js +1 -0
  240. package/ui/dist/assets/index-B2TDuGqy.css +1 -0
  241. package/ui/dist/assets/index-DFlbOIxI.js +25 -0
  242. package/ui/dist/assets/index-Deo5wBiA.js +1 -0
  243. package/ui/dist/index.html +2 -2
  244. package/assets/phase-a-bg-cleanup-test.png +0 -0
  245. package/assets/screenshot.png +0 -0
  246. package/assets/screenshots/classic-generate-light.png +0 -0
  247. package/assets/screenshots/node-graph-branching.png +0 -0
  248. package/assets/screenshots/settings-oauth-generation.png +0 -0
  249. package/assets/screenshots/settings-workspace.png +0 -0
  250. package/assets/screenshots/style-sheet-editor.png +0 -0
  251. package/integrations/comfyui/ima2_gen_bridge/__pycache__/__init__.cpython-313.pyc +0 -0
  252. package/integrations/comfyui/ima2_gen_bridge/__pycache__/nodes.cpython-313.pyc +0 -0
  253. package/ui/dist/assets/index-BDffwmLs.css +0 -1
  254. package/ui/dist/assets/index-D0fdHLkJ.js +0 -31
  255. package/ui/dist/assets/index-D0fdHLkJ.js.map +0 -1
package/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # ima2-gen
2
2
 
3
+ <p align="center">
4
+ <img src="assets/logo.png" alt="ima2-gen logo" width="240">
5
+ </p>
6
+
3
7
  [![npm version](https://img.shields.io/npm/v/ima2-gen)](https://www.npmjs.com/package/ima2-gen)
4
8
  [![Node.js](https://img.shields.io/badge/node-%3E%3D20-brightgreen)](https://nodejs.org/)
5
9
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
@@ -10,7 +14,7 @@
10
14
 
11
15
  `ima2-gen` is a local image generation studio for people who want the ChatGPT/Codex image workflow in a small desktop-like web app.
12
16
 
13
- Run it with `npx`, sign in with Codex OAuth, type a prompt, and keep iterating with history, references, style sheets, and node branches. No OpenAI API key is required for image generation in the default path.
17
+ Run it with `npx`, sign in with Codex OAuth, type a prompt, and keep iterating with history, references, node branches, multimode batches, and Canvas Mode cleanup. No OpenAI API key is required for the default path, but API-key generation is also supported when configured.
14
18
 
15
19
  ![ima2-gen classic generation screen with prompt composer, generated image, compact model label, and result metadata.](assets/screenshots/classic-generate-light.png)
16
20
 
@@ -47,20 +51,25 @@ persists, reboot and run the update before starting ima2 again.
47
51
 
48
52
  - **Classic mode**: generate, edit, reuse the current image, paste references, and continue from history.
49
53
  - **Node mode**: branch a good image into multiple directions without losing the original.
54
+ - **Multimode batches**: launch several Classic outputs from one prompt, watch slot-by-slot progress, and continue from the best result.
55
+ - **Canvas Mode**: zoom, pan, annotate, erase, clean backgrounds, keep transparent previews, and export either alpha or matte-backed versions.
50
56
  - **Local gallery**: keep generated assets on your machine with session-aware history.
51
57
  - **Reference images**: drag, drop, paste, and attach up to 5 references; large images are compressed before upload.
52
- - **Style sheets**: extract and reuse a visual direction across classic and node prompts.
58
+ - **Prompt library imports**: import local prompt packs, GitHub folders, and curated GPT-image prompt hints into the built-in prompt library.
59
+ - **Mobile shell**: use the app bar, compose sheet, and compact settings toggle on smaller screens.
53
60
  - **Observable jobs**: active and recent jobs are tracked with safe logs and request IDs.
54
61
 
55
- ## OAuth Only For Image Generation
62
+ ## Provider Paths
56
63
 
57
- Image generation currently runs through the local Codex/ChatGPT OAuth path.
64
+ Image generation can run through either the local Codex/ChatGPT OAuth path or a configured OpenAI API key.
58
65
 
59
- API keys may still be detected for auxiliary developer features such as billing checks or style-sheet extraction, but generation routes reject `provider: "api"` with `APIKEY_DISABLED`.
66
+ - `provider: "oauth"` uses the local Codex OAuth proxy.
67
+ - `provider: "api"` calls the OpenAI Responses API with the hosted `image_generation` tool.
68
+ - API-key generation supports classic generate, edit, mask-guided edit, multimode, and node generation.
60
69
 
61
- If the settings page says **Configured but disabled**, that means an API key exists in env/config but image generation still uses OAuth.
70
+ If no provider is specified, the app keeps the current OAuth/default behavior. API-key generation defaults to `gpt-5.4-mini`, `low` reasoning, and `1024x1024` unless the request passes validated model, reasoning, size, or web-search options.
62
71
 
63
- ![Settings workspace showing OAuth active and API key configured but disabled.](assets/screenshots/settings-oauth-generation.png)
72
+ ![Settings workspace showing OAuth active and API key provider available.](assets/screenshots/settings-oauth-generation.png)
64
73
 
65
74
  ## Model Guidance
66
75
 
@@ -81,7 +90,10 @@ Use Classic when you want one strong result quickly.
81
90
  1. Write a prompt.
82
91
  2. Attach or paste references if needed.
83
92
  3. Pick model, quality, size, format, and moderation.
84
- 4. Generate, copy, download, or continue from the result.
93
+ 4. Generate one image, or enable multimode to fan out several candidate slots from the same prompt.
94
+ 5. Copy, download, continue from the result, or send it into Canvas Mode.
95
+
96
+ ![Multimode sequence with four candidate slots generating from one prompt and active job history in the sidebar.](assets/screenshots/multimode-sequence.png)
85
97
 
86
98
  ### Node Mode
87
99
 
@@ -91,22 +103,36 @@ Use Node mode when you want to explore branches.
91
103
 
92
104
  Each node keeps its own prompt and result. Root nodes can attach local references; child nodes use the parent image as their source. Completed jobs are matched back to nodes by request ID, so reloads and graph version conflicts can recover finished results.
93
105
 
106
+ ### Canvas Mode
107
+
108
+ Use Canvas Mode when a generated image is close but needs targeted cleanup before the next prompt.
109
+
110
+ - Separate viewport panning from selection so you can move around a zoomed image without accidentally changing annotations.
111
+ - Use annotation, eraser, multiselect, grouping, undo/redo, and sticky notes while keeping the original gallery image available.
112
+ - Pick background-cleanup seeds, preview the mask, and save the cleanup as a canvas version.
113
+ - Detect transparent images and show a checkerboard preview; export with preserved alpha or with a chosen matte color.
114
+ - Saved canvas versions stay hidden from Gallery and HistoryStrip, but Canvas Mode can reuse them and attach a canvas version as the next reference.
115
+
116
+ ![Canvas Mode with zoom controls, annotation marks, a sticky note, and the canvas toolbar.](assets/screenshots/canvas-mode-cleanup.png)
117
+
118
+ ### Prompt Library And Imports
119
+
120
+ The prompt library can now be filled from local files, GitHub folders, curated sources, and GPT-image hint packs. Imported prompts are indexed locally so search and ranking work without re-importing the same source every session.
121
+
122
+ ![Prompt import dialog for bringing prompts into the library, showing GitHub folder controls, curated sources, and searched prompt candidates before import.](assets/screenshots/prompt-import-dialog.png)
123
+
94
124
  ### Experimental Card News Mode
95
125
 
96
126
  Card News is still dev-only and experimental. It is hidden in the default
97
127
  published runtime unless explicitly enabled for development, and it should not
98
128
  be treated as a stable public feature yet.
99
129
 
100
- ### Settings And Style Sheets
130
+ ### Settings
101
131
 
102
132
  The settings workspace keeps account, model, appearance, and language controls away from the generation sidebar.
103
133
 
104
134
  ![Settings workspace with account navigation and generation model controls.](assets/screenshots/settings-workspace.png)
105
135
 
106
- Style sheets let you capture a reusable visual direction.
107
-
108
- ![Style sheet editor with medium, composition, mood, subject, palette, and negative fields.](assets/screenshots/style-sheet-editor.png)
109
-
110
136
  ## CLI Commands
111
137
 
112
138
  ### Server
@@ -122,27 +148,32 @@ Style sheets let you capture a reusable visual direction.
122
148
 
123
149
  ### Client
124
150
 
125
- These require a running `ima2 serve`.
151
+ These require a running `ima2 serve`. The CLI covers every server route. The most common ones are below — the [full CLI reference](docs/CLI.md) lists everything (generation, history, sessions, prompt library, annotations, Card News, observability, config).
126
152
 
127
153
  | Command | Description |
128
154
  |---|---|
129
155
  | `ima2 gen <prompt>` | Generate from the CLI |
130
156
  | `ima2 edit <file> --prompt <text>` | Edit an existing image |
131
- | `ima2 ls` | List local history |
132
- | `ima2 show <name>` | Reveal a generated asset |
133
- | `ima2 ps` | List active jobs |
134
- | `ima2 cancel <requestId>` | Mark an in-flight job canceled |
157
+ | `ima2 multimode <prompt>` | Multi-image SSE generation |
158
+ | `ima2 ls [--session <id>] [--favorites]` | List recent history |
159
+ | `ima2 show <name> [--metadata]` | Reveal a generated asset |
160
+ | `ima2 prompt ls -q <search>` | Search the prompt library |
161
+ | `ima2 inflight ls [--terminal]` | List active and recent jobs (alias of `ps`) |
162
+ | `ima2 config set <key> <value>` | Write to `~/.ima2/config.json` |
135
163
  | `ima2 ping` | Health-check the running server |
136
164
 
137
- The server advertises its actual port at `~/.ima2/server.json`. If `3333` is busy, the backend can fall back to `3334+` and CLI commands follow the advertised URL. Override discovery with `--server <url>` or `IMA2_SERVER=http://localhost:3333`.
165
+ The server advertises its actual port at `~/.ima2/server.json`. If `3333` is busy, the backend falls back to `3334+` and CLI commands follow the advertised URL. Override discovery with `--server <url>` or `IMA2_SERVER=http://localhost:3333`.
138
166
 
139
167
  ```bash
140
- ima2 gen "poster" --model gpt-5.4 --mode direct --moderation low
141
- ima2 edit input.png --prompt "make it rainy" --model gpt-5.4
142
- ima2 ps --terminal
143
- ima2 cancel <requestId>
168
+ ima2 gen "poster" --model gpt-5.4 --reasoning-effort high
169
+ ima2 edit input.png --prompt "make it rainy" --web-search
170
+ ima2 multimode "two cats playing" -n 2
171
+ ima2 inflight ls --terminal
172
+ ima2 config set imageModels.reasoningEffort high
144
173
  ```
145
174
 
175
+ Full reference: [docs/CLI.md](docs/CLI.md).
176
+
146
177
  ## Configuration
147
178
 
148
179
  Config priority:
@@ -164,7 +195,7 @@ environment variables > ~/.ima2/config.json > built-in defaults
164
195
  | `IMA2_NO_OAUTH_PROXY` | — | Set `1` to disable the auto-started OAuth proxy |
165
196
  | `IMA2_LOG_LEVEL` | `warn` | Normal serve defaults to `warn`; dev mode defaults to `debug`; supports `debug`, `info`, `warn`, `error`, or `silent` |
166
197
  | `IMA2_INFLIGHT_TERMINAL_TTL_MS` | `30000` | Recent terminal job retention for debug views |
167
- | `OPENAI_API_KEY` | — | API key for supported auxiliary paths, not image generation |
198
+ | `OPENAI_API_KEY` | — | API key for the `provider: "api"` Responses API image path and auxiliary API-key features |
168
199
 
169
200
  ### Logging modes
170
201
 
@@ -178,6 +209,7 @@ The endpoint list moved to [docs/API.md](docs/API.md) so this README can stay fo
178
209
 
179
210
  Useful references:
180
211
 
212
+ - [CLI Reference](docs/CLI.md)
181
213
  - [API Reference](docs/API.md)
182
214
  - [FAQ](docs/FAQ.md)
183
215
  - [Recover old images](docs/RECOVER_OLD_IMAGES.md)
@@ -196,8 +228,8 @@ Run `npx @openai/codex login`, confirm `ima2 status`, then restart `ima2 serve`.
196
228
  **`fetch failed` repeats on a proxy/VPN network**
197
229
  Check that the local OAuth proxy is reachable. On networks that require a proxy, enable your proxy client's TUN/TURN-style mode, then retry `npx openai-oauth --port 10531`. If it still fails, set `HTTP_PROXY` and `HTTPS_PROXY` in the same terminal that runs `ima2 serve` or `openai-oauth`.
198
230
 
199
- **Images fail with `APIKEY_DISABLED`**
200
- Use OAuth for generation. API-key image generation is intentionally disabled in this build.
231
+ **Images fail with `API_KEY_REQUIRED`**
232
+ Set `OPENAI_API_KEY` or configure an API key before using `provider: "api"`. The default OAuth path still works without an API key.
201
233
 
202
234
  **A large reference image fails**
203
235
  The app compresses large JPEG/PNG references before upload. If a file still fails, convert it to JPEG or PNG at a lower resolution and try again. HEIC/HEIF files are not supported by the browser path.
@@ -223,11 +255,12 @@ git clone https://github.com/lidge-jun/ima2-gen.git
223
255
  cd ima2-gen
224
256
  npm install
225
257
  npm run dev
258
+ npm run typecheck
226
259
  npm test
227
260
  npm run build
228
261
  ```
229
262
 
230
- `npm run dev` builds the UI and starts `server.js` with `--watch` and verbose server diagnostics. Node mode is part of the packaged UI by default.
263
+ `npm run dev` builds the UI and starts the TypeScript server entry with `--watch` and verbose server diagnostics. `npm run typecheck`, `npm run build:server`, and `npm run build:cli` verify the TypeScript migration and package emit path. Node mode and Canvas Mode are part of the packaged UI by default.
231
264
 
232
265
  ## License
233
266
 
@@ -0,0 +1,139 @@
1
+ import { readFile } from "fs/promises";
2
+ import { parseArgs } from "../lib/args.js";
3
+ import { resolveServer, request } from "../lib/client.js";
4
+ import { readStdin } from "../lib/files.js";
5
+ import { out, die, color, json, exitCodeForError } from "../lib/output.js";
6
+ import { getCliBrowserId } from "../lib/browser-id.js";
7
+ const HELP = `
8
+ ima2 annotate <subcommand> <filename> [options]
9
+
10
+ Subcommands:
11
+ get <filename> [--json]
12
+ set <filename> --body <json|@file|->
13
+ rm <filename> [--yes]
14
+ `;
15
+ const FLAGS = {
16
+ json: { type: "boolean" },
17
+ server: { type: "string" },
18
+ yes: { type: "boolean" },
19
+ body: { type: "string" },
20
+ help: { short: "h", type: "boolean" },
21
+ };
22
+ async function getServer(args) {
23
+ try {
24
+ return await resolveServer({ serverFlag: args.server });
25
+ }
26
+ catch (e) {
27
+ die(exitCodeForError(e), e.message);
28
+ throw e;
29
+ }
30
+ }
31
+ async function resolveBody(value) {
32
+ if (!value)
33
+ return null;
34
+ if (typeof value !== "string")
35
+ return null;
36
+ let text;
37
+ if (value === "-")
38
+ text = await readStdin();
39
+ else if (value.startsWith("@"))
40
+ text = await readFile(value.slice(1), "utf-8");
41
+ else
42
+ text = value;
43
+ try {
44
+ return JSON.parse(text);
45
+ }
46
+ catch {
47
+ die(2, "--body must be valid JSON");
48
+ throw 0;
49
+ }
50
+ }
51
+ async function readLine() {
52
+ return new Promise((resolve) => {
53
+ let buf = "";
54
+ process.stdin.setEncoding("utf-8");
55
+ const onData = (chunk) => {
56
+ buf += chunk;
57
+ const nl = buf.indexOf("\n");
58
+ if (nl !== -1) {
59
+ process.stdin.removeListener("data", onData);
60
+ process.stdin.pause();
61
+ resolve(buf.slice(0, nl));
62
+ }
63
+ };
64
+ process.stdin.resume();
65
+ process.stdin.on("data", onData);
66
+ });
67
+ }
68
+ async function getSub(argv) {
69
+ const args = parseArgs(argv, { flags: FLAGS });
70
+ const filename = args.positional[0];
71
+ if (!filename)
72
+ die(2, "filename required");
73
+ const server = await getServer(args);
74
+ const browserId = getCliBrowserId();
75
+ const resp = await request(server.base, `/api/annotations/${encodeURIComponent(filename)}`, {
76
+ headers: { "X-Ima2-Browser-Id": browserId },
77
+ }).catch((e) => { const err = e; die(exitCodeForError(e), `${err.message}${err.code ? ` (${err.code})` : ""}`); });
78
+ json(resp);
79
+ }
80
+ async function setSub(argv) {
81
+ const args = parseArgs(argv, { flags: FLAGS });
82
+ const filename = args.positional[0];
83
+ if (!filename)
84
+ die(2, "filename required");
85
+ if (!args.body)
86
+ die(2, "--body <json|@file|-> required");
87
+ const body = await resolveBody(args.body);
88
+ const server = await getServer(args);
89
+ const browserId = getCliBrowserId();
90
+ const resp = await request(server.base, `/api/annotations/${encodeURIComponent(filename)}`, {
91
+ method: "PUT",
92
+ body,
93
+ headers: { "X-Ima2-Browser-Id": browserId },
94
+ }).catch((e) => { const err = e; die(exitCodeForError(e), `${err.message}${err.code ? ` (${err.code})` : ""}`); });
95
+ if (args.json) {
96
+ json(resp);
97
+ return;
98
+ }
99
+ out(color.green("✓ annotation saved"));
100
+ }
101
+ async function rmSub(argv) {
102
+ const args = parseArgs(argv, { flags: FLAGS });
103
+ const filename = args.positional[0];
104
+ if (!filename)
105
+ die(2, "filename required");
106
+ if (!args.yes && !process.stdin.isTTY)
107
+ die(2, "destructive: pass --yes for non-TTY");
108
+ if (!args.yes) {
109
+ process.stdout.write(`Delete annotation for ${filename}? [y/N] `);
110
+ const ans = await readLine();
111
+ if (!/^y(es)?$/i.test(ans.trim())) {
112
+ out("(canceled)");
113
+ return;
114
+ }
115
+ }
116
+ const server = await getServer(args);
117
+ const browserId = getCliBrowserId();
118
+ await request(server.base, `/api/annotations/${encodeURIComponent(filename)}`, {
119
+ method: "DELETE",
120
+ headers: { "X-Ima2-Browser-Id": browserId },
121
+ }).catch((e) => { const err = e; die(exitCodeForError(e), `${err.message}${err.code ? ` (${err.code})` : ""}`); });
122
+ out(color.green("✓ deleted"));
123
+ }
124
+ const SUB = {
125
+ get: getSub,
126
+ set: setSub,
127
+ rm: rmSub,
128
+ };
129
+ export default async function annotateCmd(argv) {
130
+ const sub = argv[0];
131
+ if (!sub || sub === "--help" || sub === "-h") {
132
+ out(HELP);
133
+ return;
134
+ }
135
+ const handler = SUB[sub];
136
+ if (!handler)
137
+ die(2, `unknown subcommand: ${sub}\n${HELP}`);
138
+ return handler(argv.slice(1));
139
+ }
@@ -0,0 +1,119 @@
1
+ import { readFile } from "fs/promises";
2
+ import { parseArgs, type ParsedArgs } from "../lib/args.js";
3
+ import { resolveServer, request } from "../lib/client.js";
4
+ import { readStdin } from "../lib/files.js";
5
+ import { out, die, color, json, exitCodeForError } from "../lib/output.js";
6
+ import { getCliBrowserId } from "../lib/browser-id.js";
7
+
8
+ const HELP = `
9
+ ima2 annotate <subcommand> <filename> [options]
10
+
11
+ Subcommands:
12
+ get <filename> [--json]
13
+ set <filename> --body <json|@file|->
14
+ rm <filename> [--yes]
15
+ `;
16
+
17
+ const FLAGS = {
18
+ json: { type: "boolean" },
19
+ server: { type: "string" },
20
+ yes: { type: "boolean" },
21
+ body: { type: "string" },
22
+ help: { short: "h", type: "boolean" },
23
+ };
24
+
25
+ async function getServer(args: ParsedArgs) {
26
+ try { return await resolveServer({ serverFlag: args.server }); }
27
+ catch (e: any) { die(exitCodeForError(e), e.message); throw e; }
28
+ }
29
+
30
+ async function resolveBody(value: unknown): Promise<any> {
31
+ if (!value) return null;
32
+ if (typeof value !== "string") return null;
33
+ let text: string;
34
+ if (value === "-") text = await readStdin();
35
+ else if (value.startsWith("@")) text = await readFile(value.slice(1), "utf-8");
36
+ else text = value;
37
+ try { return JSON.parse(text); }
38
+ catch { die(2, "--body must be valid JSON"); throw 0; }
39
+ }
40
+
41
+ async function readLine(): Promise<string> {
42
+ return new Promise((resolve) => {
43
+ let buf = "";
44
+ process.stdin.setEncoding("utf-8");
45
+ const onData = (chunk: Buffer | string) => {
46
+ buf += chunk;
47
+ const nl = buf.indexOf("\n");
48
+ if (nl !== -1) {
49
+ process.stdin.removeListener("data", onData);
50
+ process.stdin.pause();
51
+ resolve(buf.slice(0, nl));
52
+ }
53
+ };
54
+ process.stdin.resume();
55
+ process.stdin.on("data", onData);
56
+ });
57
+ }
58
+
59
+ async function getSub(argv: string[]) {
60
+ const args = parseArgs(argv, { flags: FLAGS });
61
+ const filename = args.positional[0];
62
+ if (!filename) die(2, "filename required");
63
+ const server = await getServer(args);
64
+ const browserId = getCliBrowserId();
65
+ const resp = await request(server.base, `/api/annotations/${encodeURIComponent(filename)}`, {
66
+ headers: { "X-Ima2-Browser-Id": browserId },
67
+ }).catch((e: unknown) => { const err = e as { message?: string; code?: string }; die(exitCodeForError(e), `${err.message}${err.code ? ` (${err.code})` : ""}`); });
68
+ json(resp);
69
+ }
70
+
71
+ async function setSub(argv: string[]) {
72
+ const args = parseArgs(argv, { flags: FLAGS });
73
+ const filename = args.positional[0];
74
+ if (!filename) die(2, "filename required");
75
+ if (!args.body) die(2, "--body <json|@file|-> required");
76
+ const body = await resolveBody(args.body);
77
+ const server = await getServer(args);
78
+ const browserId = getCliBrowserId();
79
+ const resp = await request(server.base, `/api/annotations/${encodeURIComponent(filename)}`, {
80
+ method: "PUT",
81
+ body,
82
+ headers: { "X-Ima2-Browser-Id": browserId },
83
+ }).catch((e: unknown) => { const err = e as { message?: string; code?: string }; die(exitCodeForError(e), `${err.message}${err.code ? ` (${err.code})` : ""}`); });
84
+ if (args.json) { json(resp); return; }
85
+ out(color.green("✓ annotation saved"));
86
+ }
87
+
88
+ async function rmSub(argv: string[]) {
89
+ const args = parseArgs(argv, { flags: FLAGS });
90
+ const filename = args.positional[0];
91
+ if (!filename) die(2, "filename required");
92
+ if (!args.yes && !process.stdin.isTTY) die(2, "destructive: pass --yes for non-TTY");
93
+ if (!args.yes) {
94
+ process.stdout.write(`Delete annotation for ${filename}? [y/N] `);
95
+ const ans = await readLine();
96
+ if (!/^y(es)?$/i.test(ans.trim())) { out("(canceled)"); return; }
97
+ }
98
+ const server = await getServer(args);
99
+ const browserId = getCliBrowserId();
100
+ await request(server.base, `/api/annotations/${encodeURIComponent(filename)}`, {
101
+ method: "DELETE",
102
+ headers: { "X-Ima2-Browser-Id": browserId },
103
+ }).catch((e: unknown) => { const err = e as { message?: string; code?: string }; die(exitCodeForError(e), `${err.message}${err.code ? ` (${err.code})` : ""}`); });
104
+ out(color.green("✓ deleted"));
105
+ }
106
+
107
+ const SUB: Record<string, (argv: any[]) => Promise<void>> = {
108
+ get: getSub,
109
+ set: setSub,
110
+ rm: rmSub,
111
+ };
112
+
113
+ export default async function annotateCmd(argv: string[]) {
114
+ const sub = argv[0];
115
+ if (!sub || sub === "--help" || sub === "-h") { out(HELP); return; }
116
+ const handler = SUB[sub];
117
+ if (!handler) die(2, `unknown subcommand: ${sub}\n${HELP}`);
118
+ return handler(argv.slice(1));
119
+ }
@@ -1,45 +1,52 @@
1
1
  import { parseArgs } from "../lib/args.js";
2
2
  import { resolveServer, request } from "../lib/client.js";
3
3
  import { out, die, dieWithError, color, json } from "../lib/output.js";
4
-
4
+ import { errInfo } from "../../lib/errInfo.js";
5
5
  const SPEC = {
6
- flags: {
7
- json: { type: "boolean" },
8
- server: { type: "string" },
9
- help: { short: "h", type: "boolean" },
10
- },
6
+ flags: {
7
+ json: { type: "boolean" },
8
+ server: { type: "string" },
9
+ help: { short: "h", type: "boolean" },
10
+ },
11
11
  };
12
-
13
12
  const HELP = `
14
13
  ima2 cancel <requestId> [--json]
15
14
 
16
15
  Mark an in-flight job as canceled in the local ima2 server registry.
17
16
  `;
18
-
19
17
  export default async function cancelCmd(argv) {
20
- const args = parseArgs(argv, SPEC);
21
- if (args.help) { out(HELP); return; }
22
-
23
- const requestId = args.positional[0];
24
- if (!requestId) die(2, "requestId required");
25
-
26
- let server;
27
- try { server = await resolveServer({ serverFlag: args.server }); }
28
- catch (e) {
29
- if (args.json) json({ ok: false, requestId, error: e.message, code: e.code, status: e.status });
30
- dieWithError(e);
31
- }
32
-
33
- try {
34
- await request(server.base, `/api/inflight/${encodeURIComponent(requestId)}`, {
35
- method: "DELETE",
36
- timeoutMs: 30_000,
37
- });
38
- } catch (e) {
39
- if (args.json) json({ ok: false, requestId, error: e.message, code: e.code, status: e.status });
40
- dieWithError(e);
41
- }
42
-
43
- if (args.json) json({ ok: true, requestId });
44
- else out(color.green("✓ ") + `canceled ${requestId}`);
18
+ const args = parseArgs(argv, SPEC);
19
+ if (args.help) {
20
+ out(HELP);
21
+ return;
22
+ }
23
+ const requestId = args.positional[0];
24
+ if (!requestId)
25
+ die(2, "requestId required");
26
+ let server;
27
+ try {
28
+ server = await resolveServer({ serverFlag: args.server });
29
+ }
30
+ catch (e) {
31
+ const err = errInfo(e);
32
+ if (args.json)
33
+ json({ ok: false, requestId, error: err.message, code: err.code, status: err.status });
34
+ dieWithError(e);
35
+ }
36
+ try {
37
+ await request(server.base, `/api/inflight/${encodeURIComponent(requestId)}`, {
38
+ method: "DELETE",
39
+ timeoutMs: 30_000,
40
+ });
41
+ }
42
+ catch (e) {
43
+ const err = errInfo(e);
44
+ if (args.json)
45
+ json({ ok: false, requestId, error: err.message, code: err.code, status: err.status });
46
+ dieWithError(e);
47
+ }
48
+ if (args.json)
49
+ json({ ok: true, requestId });
50
+ else
51
+ out(color.green("✓ ") + `canceled ${requestId}`);
45
52
  }
@@ -0,0 +1,48 @@
1
+ import { parseArgs } from "../lib/args.js";
2
+ import { resolveServer, request } from "../lib/client.js";
3
+ import { out, die, dieWithError, color, json } from "../lib/output.js";
4
+
5
+ import { errInfo } from "../../lib/errInfo.js";
6
+ const SPEC = {
7
+ flags: {
8
+ json: { type: "boolean" },
9
+ server: { type: "string" },
10
+ help: { short: "h", type: "boolean" },
11
+ },
12
+ };
13
+
14
+ const HELP = `
15
+ ima2 cancel <requestId> [--json]
16
+
17
+ Mark an in-flight job as canceled in the local ima2 server registry.
18
+ `;
19
+
20
+ export default async function cancelCmd(argv: string[]) {
21
+ const args = parseArgs(argv, SPEC);
22
+ if (args.help) { out(HELP); return; }
23
+
24
+ const requestId = args.positional[0];
25
+ if (!requestId) die(2, "requestId required");
26
+
27
+ let server;
28
+ try { server = await resolveServer({ serverFlag: args.server }); }
29
+ catch (e) {
30
+ const err = errInfo(e);
31
+ if (args.json) json({ ok: false, requestId, error: err.message, code: err.code, status: err.status });
32
+ dieWithError(e);
33
+ }
34
+
35
+ try {
36
+ await request(server.base, `/api/inflight/${encodeURIComponent(requestId)}`, {
37
+ method: "DELETE",
38
+ timeoutMs: 30_000,
39
+ });
40
+ } catch (e) {
41
+ const err = errInfo(e);
42
+ if (args.json) json({ ok: false, requestId, error: err.message, code: err.code, status: err.status });
43
+ dieWithError(e);
44
+ }
45
+
46
+ if (args.json) json({ ok: true, requestId });
47
+ else out(color.green("✓ ") + `canceled ${requestId}`);
48
+ }
@@ -0,0 +1,91 @@
1
+ import { readFile } from "fs/promises";
2
+ import { parseArgs } from "../lib/args.js";
3
+ import { resolveServer, request } from "../lib/client.js";
4
+ import { out, die, color, json, exitCodeForError } from "../lib/output.js";
5
+ const HELP = `
6
+ ima2 canvas-versions <subcommand> [options]
7
+
8
+ Subcommands:
9
+ save <imagefile> [--source <filename>] [--prompt <text>]
10
+ update <filename> <imagefile> [--source <filename>] [--prompt <text>]
11
+
12
+ Notes: server only exposes POST /api/canvas-versions (collection) and
13
+ PUT /api/canvas-versions/:filename. No GET, no DELETE.
14
+ `;
15
+ const FLAGS = {
16
+ json: { type: "boolean" },
17
+ server: { type: "string" },
18
+ source: { type: "string" },
19
+ prompt: { type: "string" },
20
+ help: { short: "h", type: "boolean" },
21
+ };
22
+ async function getServer(args) {
23
+ try {
24
+ return await resolveServer({ serverFlag: args.server });
25
+ }
26
+ catch (e) {
27
+ die(exitCodeForError(e), e.message);
28
+ throw e;
29
+ }
30
+ }
31
+ function buildHeaders(args) {
32
+ const h = { "Content-Type": "image/png" };
33
+ if (args.source)
34
+ h["X-Ima2-Canvas-Source-Filename"] = String(args.source);
35
+ if (args.prompt)
36
+ h["X-Ima2-Canvas-Prompt"] = String(args.prompt);
37
+ return h;
38
+ }
39
+ async function saveSub(argv) {
40
+ const args = parseArgs(argv, { flags: FLAGS });
41
+ const file = args.positional[0];
42
+ if (!file)
43
+ die(2, "image file required");
44
+ const buf = await readFile(file);
45
+ const server = await getServer(args);
46
+ const resp = await request(server.base, "/api/canvas-versions", {
47
+ method: "POST",
48
+ body: buf,
49
+ raw: true,
50
+ headers: buildHeaders(args),
51
+ }).catch((e) => { const err = e; die(exitCodeForError(e), `${err.message}${err.code ? ` (${err.code})` : ""}`); });
52
+ if (args.json) {
53
+ json(resp);
54
+ return;
55
+ }
56
+ out(color.green("✓ saved canvas version"));
57
+ }
58
+ async function updateSub(argv) {
59
+ const args = parseArgs(argv, { flags: FLAGS });
60
+ const [filename, file] = args.positional;
61
+ if (!filename || !file)
62
+ die(2, "usage: canvas-versions update <filename> <imagefile>");
63
+ const buf = await readFile(file);
64
+ const server = await getServer(args);
65
+ const resp = await request(server.base, `/api/canvas-versions/${encodeURIComponent(filename)}`, {
66
+ method: "PUT",
67
+ body: buf,
68
+ raw: true,
69
+ headers: buildHeaders(args),
70
+ }).catch((e) => { const err = e; die(exitCodeForError(e), `${err.message}${err.code ? ` (${err.code})` : ""}`); });
71
+ if (args.json) {
72
+ json(resp);
73
+ return;
74
+ }
75
+ out(color.green("✓ updated"));
76
+ }
77
+ const SUB = {
78
+ save: saveSub,
79
+ update: updateSub,
80
+ };
81
+ export default async function canvasVersionsCmd(argv) {
82
+ const sub = argv[0];
83
+ if (!sub || sub === "--help" || sub === "-h") {
84
+ out(HELP);
85
+ return;
86
+ }
87
+ const handler = SUB[sub];
88
+ if (!handler)
89
+ die(2, `unknown subcommand: ${sub}\n${HELP}`);
90
+ return handler(argv.slice(1));
91
+ }