ima2-gen 1.1.7 → 1.1.9

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 (229) hide show
  1. package/README.md +56 -27
  2. package/bin/commands/annotate.js +137 -0
  3. package/bin/commands/annotate.ts +118 -0
  4. package/bin/commands/cancel.js +37 -33
  5. package/bin/commands/cancel.ts +45 -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 +293 -0
  9. package/bin/commands/cardnews.ts +248 -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 +97 -72
  15. package/bin/commands/edit.ts +116 -0
  16. package/bin/commands/gen.js +140 -118
  17. package/bin/commands/gen.ts +176 -0
  18. package/bin/commands/history.js +164 -0
  19. package/bin/commands/history.ts +145 -0
  20. package/bin/commands/ls.js +60 -42
  21. package/bin/commands/ls.ts +60 -0
  22. package/bin/commands/metadata.js +45 -0
  23. package/bin/commands/metadata.ts +36 -0
  24. package/bin/commands/multimode.js +159 -0
  25. package/bin/commands/multimode.ts +146 -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 +26 -20
  31. package/bin/commands/ping.ts +29 -0
  32. package/bin/commands/prompt.js +506 -0
  33. package/bin/commands/prompt.ts +421 -0
  34. package/bin/commands/ps.js +78 -71
  35. package/bin/commands/ps.ts +78 -0
  36. package/bin/commands/session.js +308 -0
  37. package/bin/commands/session.ts +265 -0
  38. package/bin/commands/show.js +75 -40
  39. package/bin/commands/show.ts +69 -0
  40. package/bin/ima2.js +324 -310
  41. package/bin/ima2.ts +444 -0
  42. package/bin/lib/args.js +75 -66
  43. package/bin/lib/args.ts +73 -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 +109 -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 +44 -42
  53. package/bin/lib/output.ts +58 -0
  54. package/bin/lib/platform.js +60 -56
  55. package/bin/lib/platform.ts +97 -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 +38 -0
  62. package/config.js +147 -190
  63. package/config.ts +331 -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/lib/assetLifecycle.js +130 -130
  72. package/lib/assetLifecycle.ts +142 -0
  73. package/lib/canvasVersionStore.js +135 -153
  74. package/lib/canvasVersionStore.ts +181 -0
  75. package/lib/cardNewsGenerator.js +127 -142
  76. package/lib/cardNewsGenerator.ts +162 -0
  77. package/lib/cardNewsJobStore.js +78 -84
  78. package/lib/cardNewsJobStore.ts +107 -0
  79. package/lib/cardNewsManifestStore.js +88 -93
  80. package/lib/cardNewsManifestStore.ts +112 -0
  81. package/lib/cardNewsPlanner.js +157 -152
  82. package/lib/cardNewsPlanner.ts +180 -0
  83. package/lib/cardNewsPlannerClient.js +101 -98
  84. package/lib/cardNewsPlannerClient.ts +114 -0
  85. package/lib/cardNewsPlannerPrompt.js +56 -56
  86. package/lib/cardNewsPlannerPrompt.ts +60 -0
  87. package/lib/cardNewsPlannerSchema.js +231 -223
  88. package/lib/cardNewsPlannerSchema.ts +259 -0
  89. package/lib/cardNewsRoleTemplateStore.js +39 -41
  90. package/lib/cardNewsRoleTemplateStore.ts +47 -0
  91. package/lib/cardNewsTemplateStore.js +171 -175
  92. package/lib/cardNewsTemplateStore.ts +210 -0
  93. package/lib/codexDetect.js +44 -47
  94. package/lib/codexDetect.ts +69 -0
  95. package/lib/comfyBridge.js +164 -184
  96. package/lib/comfyBridge.ts +214 -0
  97. package/lib/db.js +41 -51
  98. package/lib/db.ts +166 -0
  99. package/lib/errorClassify.js +62 -78
  100. package/lib/errorClassify.ts +100 -0
  101. package/lib/generationErrors.js +140 -103
  102. package/lib/generationErrors.ts +125 -0
  103. package/lib/historyList.js +149 -147
  104. package/lib/historyList.ts +164 -0
  105. package/lib/imageMetadata.js +86 -89
  106. package/lib/imageMetadata.ts +111 -0
  107. package/lib/imageMetadataStore.js +46 -51
  108. package/lib/imageMetadataStore.ts +67 -0
  109. package/lib/imageModels.js +38 -45
  110. package/lib/imageModels.ts +52 -0
  111. package/lib/inflight.js +131 -150
  112. package/lib/inflight.ts +204 -0
  113. package/lib/localImportStore.js +105 -0
  114. package/lib/localImportStore.ts +111 -0
  115. package/lib/logger.js +105 -112
  116. package/lib/logger.ts +150 -0
  117. package/lib/nodeStore.js +65 -64
  118. package/lib/nodeStore.ts +81 -0
  119. package/lib/oauthLauncher.js +61 -59
  120. package/lib/oauthLauncher.ts +64 -0
  121. package/lib/oauthNormalize.js +15 -19
  122. package/lib/oauthNormalize.ts +30 -0
  123. package/lib/oauthProxy.js +834 -832
  124. package/lib/oauthProxy.ts +995 -0
  125. package/lib/openDirectory.js +41 -40
  126. package/lib/openDirectory.ts +45 -0
  127. package/lib/pngInfo.js +18 -20
  128. package/lib/pngInfo.ts +26 -0
  129. package/lib/promptImport/curatedSources.js +135 -0
  130. package/lib/promptImport/curatedSources.ts +139 -0
  131. package/lib/promptImport/discoveryRegistry.js +218 -0
  132. package/lib/promptImport/discoveryRegistry.ts +236 -0
  133. package/lib/promptImport/errors.js +10 -10
  134. package/lib/promptImport/errors.ts +18 -0
  135. package/lib/promptImport/githubDiscovery.js +238 -0
  136. package/lib/promptImport/githubDiscovery.ts +248 -0
  137. package/lib/promptImport/githubFolder.js +302 -0
  138. package/lib/promptImport/githubFolder.ts +308 -0
  139. package/lib/promptImport/githubSource.js +194 -171
  140. package/lib/promptImport/githubSource.ts +239 -0
  141. package/lib/promptImport/gptImageHints.js +61 -0
  142. package/lib/promptImport/gptImageHints.ts +68 -0
  143. package/lib/promptImport/parsePromptCandidates.js +110 -112
  144. package/lib/promptImport/parsePromptCandidates.ts +153 -0
  145. package/lib/promptImport/promptIndex.js +230 -0
  146. package/lib/promptImport/promptIndex.ts +248 -0
  147. package/lib/promptImport/rankPromptCandidates.js +52 -0
  148. package/lib/promptImport/rankPromptCandidates.ts +49 -0
  149. package/lib/providerOptions.js +31 -0
  150. package/lib/providerOptions.ts +41 -0
  151. package/lib/referenceImageCompress.js +51 -62
  152. package/lib/referenceImageCompress.ts +75 -0
  153. package/lib/refs.js +93 -81
  154. package/lib/refs.ts +117 -0
  155. package/lib/requestLogger.js +32 -38
  156. package/lib/requestLogger.ts +48 -0
  157. package/lib/responsesImageAdapter.js +351 -0
  158. package/lib/responsesImageAdapter.ts +352 -0
  159. package/lib/runtimePorts.js +71 -73
  160. package/lib/runtimePorts.ts +93 -0
  161. package/lib/sessionStore.js +179 -230
  162. package/lib/sessionStore.ts +272 -0
  163. package/lib/storageMigration.js +247 -245
  164. package/lib/storageMigration.ts +284 -0
  165. package/lib/styleSheet.js +86 -90
  166. package/lib/styleSheet.ts +128 -0
  167. package/lib/systemTrash.js +18 -0
  168. package/lib/systemTrash.ts +20 -0
  169. package/package.json +26 -10
  170. package/routes/annotations.js +76 -79
  171. package/routes/annotations.ts +95 -0
  172. package/routes/canvasVersions.js +50 -54
  173. package/routes/canvasVersions.ts +64 -0
  174. package/routes/cardNews.js +158 -171
  175. package/routes/cardNews.ts +183 -0
  176. package/routes/comfy.js +23 -31
  177. package/routes/comfy.ts +39 -0
  178. package/routes/edit.js +183 -214
  179. package/routes/edit.ts +230 -0
  180. package/routes/generate.js +269 -291
  181. package/routes/generate.ts +309 -0
  182. package/routes/health.js +102 -107
  183. package/routes/health.ts +114 -0
  184. package/routes/history.js +136 -144
  185. package/routes/history.ts +153 -0
  186. package/routes/imageImport.js +33 -0
  187. package/routes/imageImport.ts +33 -0
  188. package/routes/index.js +18 -16
  189. package/routes/index.ts +35 -0
  190. package/routes/metadata.js +60 -64
  191. package/routes/metadata.ts +71 -0
  192. package/routes/multimode.js +228 -263
  193. package/routes/multimode.ts +280 -0
  194. package/routes/nodes.js +378 -424
  195. package/routes/nodes.ts +455 -0
  196. package/routes/promptImport.js +291 -152
  197. package/routes/promptImport.ts +354 -0
  198. package/routes/prompts.js +333 -360
  199. package/routes/prompts.ts +379 -0
  200. package/routes/sessions.js +277 -285
  201. package/routes/sessions.ts +292 -0
  202. package/routes/storage.js +29 -31
  203. package/routes/storage.ts +39 -0
  204. package/server.js +189 -196
  205. package/server.ts +235 -0
  206. package/ui/dist/.vite/manifest.json +101 -0
  207. package/ui/dist/assets/CardNewsWorkspace-BJOCey7Z.js +2 -0
  208. package/ui/dist/assets/NodeCanvas-BZV40eAE.css +1 -0
  209. package/ui/dist/assets/NodeCanvas-C3dzYNsk.js +7 -0
  210. package/ui/dist/assets/PromptImportDialog-Dqu1VpUh.js +2 -0
  211. package/ui/dist/assets/PromptImportDiscoverySection-Dg8T9X0L.js +1 -0
  212. package/ui/dist/assets/PromptImportFolderSection-DBaqsFO4.js +1 -0
  213. package/ui/dist/assets/PromptLibraryPanel-p5QqR97M.js +2 -0
  214. package/ui/dist/assets/SettingsWorkspace-B5bSAZ6u.js +1 -0
  215. package/ui/dist/assets/index-C9cXwiWE.js +25 -0
  216. package/ui/dist/assets/index-CGMIkZXn.css +1 -0
  217. package/ui/dist/assets/index-Cvld7dUZ.js +1 -0
  218. package/ui/dist/index.html +6 -3
  219. package/assets/screenshot.png +0 -0
  220. package/assets/screenshots/classic-generate-light.png +0 -0
  221. package/assets/screenshots/node-graph-branching.png +0 -0
  222. package/assets/screenshots/settings-oauth-generation.png +0 -0
  223. package/assets/screenshots/settings-workspace.png +0 -0
  224. package/assets/screenshots/style-sheet-editor.png +0 -0
  225. package/integrations/comfyui/ima2_gen_bridge/__pycache__/__init__.cpython-313.pyc +0 -0
  226. package/integrations/comfyui/ima2_gen_bridge/__pycache__/nodes.cpython-313.pyc +0 -0
  227. package/ui/dist/assets/index-DARPdT4Q.css +0 -1
  228. package/ui/dist/assets/index-ht80GMq4.js +0 -31
  229. package/ui/dist/assets/index-ht80GMq4.js.map +0 -1
package/README.md CHANGED
@@ -10,7 +10,7 @@
10
10
 
11
11
  `ima2-gen` is a local image generation studio for people who want the ChatGPT/Codex image workflow in a small desktop-like web app.
12
12
 
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.
13
+ 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
14
 
15
15
  ![ima2-gen classic generation screen with prompt composer, generated image, compact model label, and result metadata.](assets/screenshots/classic-generate-light.png)
16
16
 
@@ -47,20 +47,25 @@ persists, reboot and run the update before starting ima2 again.
47
47
 
48
48
  - **Classic mode**: generate, edit, reuse the current image, paste references, and continue from history.
49
49
  - **Node mode**: branch a good image into multiple directions without losing the original.
50
+ - **Multimode batches**: launch several Classic outputs from one prompt, watch slot-by-slot progress, and continue from the best result.
51
+ - **Canvas Mode**: zoom, pan, annotate, erase, clean backgrounds, keep transparent previews, and export either alpha or matte-backed versions.
50
52
  - **Local gallery**: keep generated assets on your machine with session-aware history.
51
53
  - **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.
54
+ - **Prompt library imports**: import local prompt packs, GitHub folders, and curated GPT-image prompt hints into the built-in prompt library.
55
+ - **Mobile shell**: use the app bar, compose sheet, and compact settings toggle on smaller screens.
53
56
  - **Observable jobs**: active and recent jobs are tracked with safe logs and request IDs.
54
57
 
55
- ## OAuth Only For Image Generation
58
+ ## Provider Paths
56
59
 
57
- Image generation currently runs through the local Codex/ChatGPT OAuth path.
60
+ Image generation can run through either the local Codex/ChatGPT OAuth path or a configured OpenAI API key.
58
61
 
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`.
62
+ - `provider: "oauth"` uses the local Codex OAuth proxy.
63
+ - `provider: "api"` calls the OpenAI Responses API with the hosted `image_generation` tool.
64
+ - API-key generation supports classic generate, edit, mask-guided edit, multimode, and node generation.
60
65
 
61
- If the settings page says **Configured but disabled**, that means an API key exists in env/config but image generation still uses OAuth.
66
+ 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
67
 
63
- ![Settings workspace showing OAuth active and API key configured but disabled.](assets/screenshots/settings-oauth-generation.png)
68
+ ![Settings workspace showing OAuth active and API key provider available.](assets/screenshots/settings-oauth-generation.png)
64
69
 
65
70
  ## Model Guidance
66
71
 
@@ -81,7 +86,10 @@ Use Classic when you want one strong result quickly.
81
86
  1. Write a prompt.
82
87
  2. Attach or paste references if needed.
83
88
  3. Pick model, quality, size, format, and moderation.
84
- 4. Generate, copy, download, or continue from the result.
89
+ 4. Generate one image, or enable multimode to fan out several candidate slots from the same prompt.
90
+ 5. Copy, download, continue from the result, or send it into Canvas Mode.
91
+
92
+ ![Multimode sequence with four candidate slots generating from one prompt and active job history in the sidebar.](assets/screenshots/multimode-sequence.png)
85
93
 
86
94
  ### Node Mode
87
95
 
@@ -91,22 +99,36 @@ Use Node mode when you want to explore branches.
91
99
 
92
100
  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
101
 
102
+ ### Canvas Mode
103
+
104
+ Use Canvas Mode when a generated image is close but needs targeted cleanup before the next prompt.
105
+
106
+ - Separate viewport panning from selection so you can move around a zoomed image without accidentally changing annotations.
107
+ - Use annotation, eraser, multiselect, grouping, undo/redo, and sticky notes while keeping the original gallery image available.
108
+ - Pick background-cleanup seeds, preview the mask, and save the cleanup as a canvas version.
109
+ - Detect transparent images and show a checkerboard preview; export with preserved alpha or with a chosen matte color.
110
+ - Saved canvas versions stay hidden from Gallery and HistoryStrip, but Canvas Mode can reuse them and attach a canvas version as the next reference.
111
+
112
+ ![Canvas Mode with zoom controls, annotation marks, a sticky note, and the canvas toolbar.](assets/screenshots/canvas-mode-cleanup.png)
113
+
114
+ ### Prompt Library And Imports
115
+
116
+ 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.
117
+
118
+ ![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)
119
+
94
120
  ### Experimental Card News Mode
95
121
 
96
122
  Card News is still dev-only and experimental. It is hidden in the default
97
123
  published runtime unless explicitly enabled for development, and it should not
98
124
  be treated as a stable public feature yet.
99
125
 
100
- ### Settings And Style Sheets
126
+ ### Settings
101
127
 
102
128
  The settings workspace keeps account, model, appearance, and language controls away from the generation sidebar.
103
129
 
104
130
  ![Settings workspace with account navigation and generation model controls.](assets/screenshots/settings-workspace.png)
105
131
 
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
132
  ## CLI Commands
111
133
 
112
134
  ### Server
@@ -122,27 +144,32 @@ Style sheets let you capture a reusable visual direction.
122
144
 
123
145
  ### Client
124
146
 
125
- These require a running `ima2 serve`.
147
+ 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
148
 
127
149
  | Command | Description |
128
150
  |---|---|
129
151
  | `ima2 gen <prompt>` | Generate from the CLI |
130
152
  | `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 |
153
+ | `ima2 multimode <prompt>` | Multi-image SSE generation |
154
+ | `ima2 ls [--session <id>] [--favorites]` | List recent history |
155
+ | `ima2 show <name> [--metadata]` | Reveal a generated asset |
156
+ | `ima2 prompt ls -q <search>` | Search the prompt library |
157
+ | `ima2 inflight ls [--terminal]` | List active and recent jobs (alias of `ps`) |
158
+ | `ima2 config set <key> <value>` | Write to `~/.ima2/config.json` |
135
159
  | `ima2 ping` | Health-check the running server |
136
160
 
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`.
161
+ 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
162
 
139
163
  ```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>
164
+ ima2 gen "poster" --model gpt-5.4 --reasoning-effort high
165
+ ima2 edit input.png --prompt "make it rainy" --web-search
166
+ ima2 multimode "two cats playing" -n 2
167
+ ima2 inflight ls --terminal
168
+ ima2 config set imageModels.reasoningEffort high
144
169
  ```
145
170
 
171
+ Full reference: [docs/CLI.md](docs/CLI.md).
172
+
146
173
  ## Configuration
147
174
 
148
175
  Config priority:
@@ -164,7 +191,7 @@ environment variables > ~/.ima2/config.json > built-in defaults
164
191
  | `IMA2_NO_OAUTH_PROXY` | — | Set `1` to disable the auto-started OAuth proxy |
165
192
  | `IMA2_LOG_LEVEL` | `warn` | Normal serve defaults to `warn`; dev mode defaults to `debug`; supports `debug`, `info`, `warn`, `error`, or `silent` |
166
193
  | `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 |
194
+ | `OPENAI_API_KEY` | — | API key for the `provider: "api"` Responses API image path and auxiliary API-key features |
168
195
 
169
196
  ### Logging modes
170
197
 
@@ -178,6 +205,7 @@ The endpoint list moved to [docs/API.md](docs/API.md) so this README can stay fo
178
205
 
179
206
  Useful references:
180
207
 
208
+ - [CLI Reference](docs/CLI.md)
181
209
  - [API Reference](docs/API.md)
182
210
  - [FAQ](docs/FAQ.md)
183
211
  - [Recover old images](docs/RECOVER_OLD_IMAGES.md)
@@ -196,8 +224,8 @@ Run `npx @openai/codex login`, confirm `ima2 status`, then restart `ima2 serve`.
196
224
  **`fetch failed` repeats on a proxy/VPN network**
197
225
  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
226
 
199
- **Images fail with `APIKEY_DISABLED`**
200
- Use OAuth for generation. API-key image generation is intentionally disabled in this build.
227
+ **Images fail with `API_KEY_REQUIRED`**
228
+ Set `OPENAI_API_KEY` or configure an API key before using `provider: "api"`. The default OAuth path still works without an API key.
201
229
 
202
230
  **A large reference image fails**
203
231
  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 +251,12 @@ git clone https://github.com/lidge-jun/ima2-gen.git
223
251
  cd ima2-gen
224
252
  npm install
225
253
  npm run dev
254
+ npm run typecheck
226
255
  npm test
227
256
  npm run build
228
257
  ```
229
258
 
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.
259
+ `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
260
 
232
261
  ## License
233
262
 
@@ -0,0 +1,137 @@
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
+ let text;
35
+ if (value === "-")
36
+ text = await readStdin();
37
+ else if (value.startsWith("@"))
38
+ text = await readFile(value.slice(1), "utf-8");
39
+ else
40
+ text = value;
41
+ try {
42
+ return JSON.parse(text);
43
+ }
44
+ catch {
45
+ die(2, "--body must be valid JSON");
46
+ throw 0;
47
+ }
48
+ }
49
+ async function readLine() {
50
+ return new Promise((resolve) => {
51
+ let buf = "";
52
+ process.stdin.setEncoding("utf-8");
53
+ const onData = (chunk) => {
54
+ buf += chunk;
55
+ const nl = buf.indexOf("\n");
56
+ if (nl !== -1) {
57
+ process.stdin.removeListener("data", onData);
58
+ process.stdin.pause();
59
+ resolve(buf.slice(0, nl));
60
+ }
61
+ };
62
+ process.stdin.resume();
63
+ process.stdin.on("data", onData);
64
+ });
65
+ }
66
+ async function getSub(argv) {
67
+ const args = parseArgs(argv, { flags: FLAGS });
68
+ const filename = args.positional[0];
69
+ if (!filename)
70
+ die(2, "filename required");
71
+ const server = await getServer(args);
72
+ const browserId = getCliBrowserId();
73
+ const resp = await request(server.base, `/api/annotations/${encodeURIComponent(filename)}`, {
74
+ headers: { "X-Ima2-Browser-Id": browserId },
75
+ }).catch((e) => die(exitCodeForError(e), `${e.message}${e.code ? ` (${e.code})` : ""}`));
76
+ json(resp);
77
+ }
78
+ async function setSub(argv) {
79
+ const args = parseArgs(argv, { flags: FLAGS });
80
+ const filename = args.positional[0];
81
+ if (!filename)
82
+ die(2, "filename required");
83
+ if (!args.body)
84
+ die(2, "--body <json|@file|-> required");
85
+ const body = await resolveBody(args.body);
86
+ const server = await getServer(args);
87
+ const browserId = getCliBrowserId();
88
+ const resp = await request(server.base, `/api/annotations/${encodeURIComponent(filename)}`, {
89
+ method: "PUT",
90
+ body,
91
+ headers: { "X-Ima2-Browser-Id": browserId },
92
+ }).catch((e) => die(exitCodeForError(e), `${e.message}${e.code ? ` (${e.code})` : ""}`));
93
+ if (args.json) {
94
+ json(resp);
95
+ return;
96
+ }
97
+ out(color.green("✓ annotation saved"));
98
+ }
99
+ async function rmSub(argv) {
100
+ const args = parseArgs(argv, { flags: FLAGS });
101
+ const filename = args.positional[0];
102
+ if (!filename)
103
+ die(2, "filename required");
104
+ if (!args.yes && !process.stdin.isTTY)
105
+ die(2, "destructive: pass --yes for non-TTY");
106
+ if (!args.yes) {
107
+ process.stdout.write(`Delete annotation for ${filename}? [y/N] `);
108
+ const ans = await readLine();
109
+ if (!/^y(es)?$/i.test(ans.trim())) {
110
+ out("(canceled)");
111
+ return;
112
+ }
113
+ }
114
+ const server = await getServer(args);
115
+ const browserId = getCliBrowserId();
116
+ await request(server.base, `/api/annotations/${encodeURIComponent(filename)}`, {
117
+ method: "DELETE",
118
+ headers: { "X-Ima2-Browser-Id": browserId },
119
+ }).catch((e) => die(exitCodeForError(e), `${e.message}${e.code ? ` (${e.code})` : ""}`));
120
+ out(color.green("✓ deleted"));
121
+ }
122
+ const SUB = {
123
+ get: getSub,
124
+ set: setSub,
125
+ rm: rmSub,
126
+ };
127
+ export default async function annotateCmd(argv) {
128
+ const sub = argv[0];
129
+ if (!sub || sub === "--help" || sub === "-h") {
130
+ out(HELP);
131
+ return;
132
+ }
133
+ const handler = SUB[sub];
134
+ if (!handler)
135
+ die(2, `unknown subcommand: ${sub}\n${HELP}`);
136
+ return handler(argv.slice(1));
137
+ }
@@ -0,0 +1,118 @@
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
+
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) {
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): Promise<any> {
31
+ if (!value) return null;
32
+ let text;
33
+ if (value === "-") text = await readStdin();
34
+ else if (value.startsWith("@")) text = await readFile(value.slice(1), "utf-8");
35
+ else text = value;
36
+ try { return JSON.parse(text); }
37
+ catch { die(2, "--body must be valid JSON"); throw 0; }
38
+ }
39
+
40
+ async function readLine(): Promise<string> {
41
+ return new Promise((resolve) => {
42
+ let buf = "";
43
+ process.stdin.setEncoding("utf-8");
44
+ const onData = (chunk) => {
45
+ buf += chunk;
46
+ const nl = buf.indexOf("\n");
47
+ if (nl !== -1) {
48
+ process.stdin.removeListener("data", onData);
49
+ process.stdin.pause();
50
+ resolve(buf.slice(0, nl));
51
+ }
52
+ };
53
+ process.stdin.resume();
54
+ process.stdin.on("data", onData);
55
+ });
56
+ }
57
+
58
+ async function getSub(argv) {
59
+ const args = parseArgs(argv, { flags: FLAGS });
60
+ const filename = args.positional[0];
61
+ if (!filename) die(2, "filename required");
62
+ const server = await getServer(args);
63
+ const browserId = getCliBrowserId();
64
+ const resp = await request(server.base, `/api/annotations/${encodeURIComponent(filename)}`, {
65
+ headers: { "X-Ima2-Browser-Id": browserId },
66
+ }).catch((e) => die(exitCodeForError(e), `${e.message}${e.code ? ` (${e.code})` : ""}`));
67
+ json(resp);
68
+ }
69
+
70
+ async function setSub(argv) {
71
+ const args = parseArgs(argv, { flags: FLAGS });
72
+ const filename = args.positional[0];
73
+ if (!filename) die(2, "filename required");
74
+ if (!args.body) die(2, "--body <json|@file|-> required");
75
+ const body = await resolveBody(args.body);
76
+ const server = await getServer(args);
77
+ const browserId = getCliBrowserId();
78
+ const resp = await request(server.base, `/api/annotations/${encodeURIComponent(filename)}`, {
79
+ method: "PUT",
80
+ body,
81
+ headers: { "X-Ima2-Browser-Id": browserId },
82
+ }).catch((e) => die(exitCodeForError(e), `${e.message}${e.code ? ` (${e.code})` : ""}`));
83
+ if (args.json) { json(resp); return; }
84
+ out(color.green("✓ annotation saved"));
85
+ }
86
+
87
+ async function rmSub(argv) {
88
+ const args = parseArgs(argv, { flags: FLAGS });
89
+ const filename = args.positional[0];
90
+ if (!filename) die(2, "filename required");
91
+ if (!args.yes && !process.stdin.isTTY) die(2, "destructive: pass --yes for non-TTY");
92
+ if (!args.yes) {
93
+ process.stdout.write(`Delete annotation for ${filename}? [y/N] `);
94
+ const ans = await readLine();
95
+ if (!/^y(es)?$/i.test(ans.trim())) { out("(canceled)"); return; }
96
+ }
97
+ const server = await getServer(args);
98
+ const browserId = getCliBrowserId();
99
+ await request(server.base, `/api/annotations/${encodeURIComponent(filename)}`, {
100
+ method: "DELETE",
101
+ headers: { "X-Ima2-Browser-Id": browserId },
102
+ }).catch((e) => die(exitCodeForError(e), `${e.message}${e.code ? ` (${e.code})` : ""}`));
103
+ out(color.green("✓ deleted"));
104
+ }
105
+
106
+ const SUB: Record<string, (argv: any[]) => Promise<void>> = {
107
+ get: getSub,
108
+ set: setSub,
109
+ rm: rmSub,
110
+ };
111
+
112
+ export default async function annotateCmd(argv) {
113
+ const sub = argv[0];
114
+ if (!sub || sub === "--help" || sub === "-h") { out(HELP); return; }
115
+ const handler = SUB[sub];
116
+ if (!handler) die(2, `unknown subcommand: ${sub}\n${HELP}`);
117
+ return handler(argv.slice(1));
118
+ }
@@ -1,45 +1,49 @@
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
-
5
4
  const SPEC = {
6
- flags: {
7
- json: { type: "boolean" },
8
- server: { type: "string" },
9
- help: { short: "h", type: "boolean" },
10
- },
5
+ flags: {
6
+ json: { type: "boolean" },
7
+ server: { type: "string" },
8
+ help: { short: "h", type: "boolean" },
9
+ },
11
10
  };
12
-
13
11
  const HELP = `
14
12
  ima2 cancel <requestId> [--json]
15
13
 
16
14
  Mark an in-flight job as canceled in the local ima2 server registry.
17
15
  `;
18
-
19
16
  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}`);
17
+ const args = parseArgs(argv, SPEC);
18
+ if (args.help) {
19
+ out(HELP);
20
+ return;
21
+ }
22
+ const requestId = args.positional[0];
23
+ if (!requestId)
24
+ die(2, "requestId required");
25
+ let server;
26
+ try {
27
+ server = await resolveServer({ serverFlag: args.server });
28
+ }
29
+ catch (e) {
30
+ if (args.json)
31
+ json({ ok: false, requestId, error: e.message, code: e.code, status: e.status });
32
+ dieWithError(e);
33
+ }
34
+ try {
35
+ await request(server.base, `/api/inflight/${encodeURIComponent(requestId)}`, {
36
+ method: "DELETE",
37
+ timeoutMs: 30_000,
38
+ });
39
+ }
40
+ catch (e) {
41
+ if (args.json)
42
+ json({ ok: false, requestId, error: e.message, code: e.code, status: e.status });
43
+ dieWithError(e);
44
+ }
45
+ if (args.json)
46
+ json({ ok: true, requestId });
47
+ else
48
+ out(color.green("✓ ") + `canceled ${requestId}`);
45
49
  }
@@ -0,0 +1,45 @@
1
+ import { parseArgs } from "../lib/args.js";
2
+ import { resolveServer, request } from "../lib/client.js";
3
+ import { out, die, dieWithError, color, json } from "../lib/output.js";
4
+
5
+ const SPEC = {
6
+ flags: {
7
+ json: { type: "boolean" },
8
+ server: { type: "string" },
9
+ help: { short: "h", type: "boolean" },
10
+ },
11
+ };
12
+
13
+ const HELP = `
14
+ ima2 cancel <requestId> [--json]
15
+
16
+ Mark an in-flight job as canceled in the local ima2 server registry.
17
+ `;
18
+
19
+ export default async function cancelCmd(argv) {
20
+ const args = parseArgs(argv, SPEC);
21
+ if (args.help) { out(HELP); return; }
22
+
23
+ const requestId = args.positional[0];
24
+ if (!requestId) die(2, "requestId required");
25
+
26
+ let server;
27
+ try { server = await resolveServer({ serverFlag: args.server }); }
28
+ catch (e) {
29
+ if (args.json) json({ ok: false, requestId, error: e.message, code: e.code, status: e.status });
30
+ dieWithError(e);
31
+ }
32
+
33
+ try {
34
+ await request(server.base, `/api/inflight/${encodeURIComponent(requestId)}`, {
35
+ method: "DELETE",
36
+ timeoutMs: 30_000,
37
+ });
38
+ } catch (e) {
39
+ if (args.json) json({ ok: false, requestId, error: e.message, code: e.code, status: e.status });
40
+ dieWithError(e);
41
+ }
42
+
43
+ if (args.json) json({ ok: true, requestId });
44
+ else out(color.green("✓ ") + `canceled ${requestId}`);
45
+ }
@@ -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"] = args.source;
35
+ if (args.prompt)
36
+ h["X-Ima2-Canvas-Prompt"] = 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) => die(exitCodeForError(e), `${e.message}${e.code ? ` (${e.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) => die(exitCodeForError(e), `${e.message}${e.code ? ` (${e.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
+ }