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
@@ -0,0 +1,16 @@
1
+ import { createHash } from "crypto";
2
+ import { config } from "../../config.js";
3
+
4
+ let cached: string | null = null;
5
+
6
+ /**
7
+ * Stable per-machine browser-id for CLI flows that share storage with the UI
8
+ * but do not have a real browser. Derived from the config dir so two CLIs on
9
+ * the same install share favorites/annotations, but different installs (e.g.
10
+ * different IMA2_CONFIG_DIR) stay isolated.
11
+ */
12
+ export function getCliBrowserId(): string {
13
+ if (cached) return cached;
14
+ cached = "cli-" + createHash("sha1").update(config.storage.configDir).digest("hex").slice(0, 16);
15
+ return cached;
16
+ }
package/bin/lib/client.js CHANGED
@@ -1,100 +1,108 @@
1
1
  import { readFileSync, existsSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { homedir } from "node:os";
4
-
5
4
  export const DEFAULT_PORT = 3333;
6
-
7
5
  function readAdvertise() {
8
- const p = process.env.IMA2_ADVERTISE_FILE ||
9
- join(process.env.IMA2_CONFIG_DIR || join(homedir(), ".ima2"), "server.json");
10
- if (!existsSync(p)) return null;
11
- try {
12
- return JSON.parse(readFileSync(p, "utf-8"));
13
- } catch {
14
- return null;
15
- }
6
+ const p = process.env.IMA2_ADVERTISE_FILE ||
7
+ join(process.env.IMA2_CONFIG_DIR || join(homedir(), ".ima2"), "server.json");
8
+ if (!existsSync(p))
9
+ return null;
10
+ try {
11
+ return JSON.parse(readFileSync(p, "utf-8"));
12
+ }
13
+ catch {
14
+ return null;
15
+ }
16
16
  }
17
-
18
17
  async function probe(base, timeoutMs = 600) {
19
- try {
20
- const r = await fetch(`${base}/api/health`, { signal: AbortSignal.timeout(timeoutMs) });
21
- if (!r.ok) return null;
22
- return await r.json();
23
- } catch {
24
- return null;
25
- }
18
+ try {
19
+ const r = await fetch(`${base}/api/health`, { signal: AbortSignal.timeout(timeoutMs) });
20
+ if (!r.ok)
21
+ return null;
22
+ return await r.json();
23
+ }
24
+ catch {
25
+ return null;
26
+ }
26
27
  }
27
-
28
28
  export async function resolveServer({ serverFlag } = {}) {
29
- if (serverFlag) {
30
- const base = serverFlag.replace(/\/$/, "");
31
- const health = await probe(base);
32
- if (health) return { base, health };
33
- const err = new Error(`server unreachable at ${base}`);
29
+ if (serverFlag) {
30
+ const base = serverFlag.replace(/\/$/, "");
31
+ const health = await probe(base);
32
+ if (health)
33
+ return { base, health };
34
+ const err = new Error(`server unreachable at ${base}`);
35
+ err.code = "SERVER_UNREACHABLE";
36
+ throw err;
37
+ }
38
+ const candidates = [];
39
+ if (process.env.IMA2_SERVER)
40
+ candidates.push(process.env.IMA2_SERVER.replace(/\/$/, ""));
41
+ const adv = readAdvertise();
42
+ if (adv?.backend?.url)
43
+ candidates.push(String(adv.backend.url).replace(/\/$/, ""));
44
+ if (adv?.url)
45
+ candidates.push(String(adv.url).replace(/\/$/, ""));
46
+ if (adv?.port)
47
+ candidates.push(`http://localhost:${adv.port}`);
48
+ candidates.push(`http://localhost:${DEFAULT_PORT}`);
49
+ const seen = new Set();
50
+ const uniq = candidates.filter((c) => !seen.has(c) && seen.add(c));
51
+ for (const base of uniq) {
52
+ const health = await probe(base);
53
+ if (health)
54
+ return { base, health };
55
+ }
56
+ const err = new Error("server unreachable — is 'ima2 serve' running?");
34
57
  err.code = "SERVER_UNREACHABLE";
35
58
  throw err;
36
- }
37
- const candidates = [];
38
- if (process.env.IMA2_SERVER) candidates.push(process.env.IMA2_SERVER.replace(/\/$/, ""));
39
- const adv = readAdvertise();
40
- if (adv?.backend?.url) candidates.push(String(adv.backend.url).replace(/\/$/, ""));
41
- if (adv?.url) candidates.push(String(adv.url).replace(/\/$/, ""));
42
- if (adv?.port) candidates.push(`http://localhost:${adv.port}`);
43
- candidates.push(`http://localhost:${DEFAULT_PORT}`);
44
-
45
- const seen = new Set();
46
- const uniq = candidates.filter((c) => !seen.has(c) && seen.add(c));
47
-
48
- for (const base of uniq) {
49
- const health = await probe(base);
50
- if (health) return { base, health };
51
- }
52
- const err = new Error("server unreachable — is 'ima2 serve' running?");
53
- err.code = "SERVER_UNREACHABLE";
54
- throw err;
55
59
  }
56
-
57
- export async function request(base, path, { method = "GET", body, timeoutMs = 180_000 } = {}) {
58
- const res = await fetch(base + path, {
59
- method,
60
- headers: {
61
- "Content-Type": "application/json",
62
- "X-ima2-client": `cli/${CLI_VERSION}`,
63
- },
64
- body: body !== undefined ? JSON.stringify(body) : undefined,
65
- signal: AbortSignal.timeout(timeoutMs),
66
- });
67
- const text = await res.text();
68
- let json = null;
69
- try { json = JSON.parse(text); } catch {}
70
- if (!res.ok) {
71
- const err = new Error(json?.error || `HTTP ${res.status}`);
72
- err.status = res.status;
73
- err.code = json?.code || null;
74
- err.body = json || text;
75
- throw err;
76
- }
77
- return json;
60
+ export async function request(base, path, { method = "GET", body, headers: extraHeaders, raw = false, timeoutMs = 180_000, } = {}) {
61
+ const baseHeaders = raw
62
+ ? { "X-ima2-client": `cli/${CLI_VERSION}` }
63
+ : { "Content-Type": "application/json", "X-ima2-client": `cli/${CLI_VERSION}` };
64
+ const finalHeaders = { ...baseHeaders, ...(extraHeaders || {}) };
65
+ const res = await fetch(base + path, {
66
+ method,
67
+ headers: finalHeaders,
68
+ body: body === undefined ? undefined
69
+ : raw ? body
70
+ : JSON.stringify(body),
71
+ signal: AbortSignal.timeout(timeoutMs),
72
+ });
73
+ const text = await res.text();
74
+ let json = null;
75
+ try {
76
+ json = JSON.parse(text);
77
+ }
78
+ catch { }
79
+ if (!res.ok) {
80
+ const err = new Error(json?.error?.message || json?.error || `HTTP ${res.status}`);
81
+ err.status = res.status;
82
+ err.code = json?.error?.code || json?.code || null;
83
+ err.body = json || text;
84
+ throw err;
85
+ }
86
+ return json;
78
87
  }
79
-
80
88
  export function normalizeGenerate(resp) {
81
- if (!resp) return { images: [], elapsed: null, requestId: null };
82
- if (Array.isArray(resp.images)) {
83
- return {
84
- images: resp.images.map((it) => ({ image: it.image, filename: it.filename })),
85
- elapsed: resp.elapsed ?? null,
86
- requestId: resp.requestId ?? null,
87
- };
88
- }
89
- if (resp.image) {
90
- return {
91
- images: [{ image: resp.image, filename: resp.filename || null }],
92
- elapsed: resp.elapsed ?? null,
93
- requestId: resp.requestId ?? null,
94
- };
95
- }
96
- return { images: [], elapsed: resp.elapsed ?? null, requestId: resp.requestId ?? null };
89
+ if (!resp)
90
+ return { images: [], elapsed: null, requestId: null };
91
+ if (Array.isArray(resp.images)) {
92
+ return {
93
+ images: resp.images.map((it) => ({ image: it.image, filename: it.filename })),
94
+ elapsed: resp.elapsed ?? null,
95
+ requestId: resp.requestId ?? null,
96
+ };
97
+ }
98
+ if (resp.image) {
99
+ return {
100
+ images: [{ image: resp.image, filename: resp.filename || null }],
101
+ elapsed: resp.elapsed ?? null,
102
+ requestId: resp.requestId ?? null,
103
+ };
104
+ }
105
+ return { images: [], elapsed: resp.elapsed ?? null, requestId: resp.requestId ?? null };
97
106
  }
98
-
99
107
  export let CLI_VERSION = "dev";
100
108
  export function setCliVersion(v) { CLI_VERSION = v; }
@@ -0,0 +1,109 @@
1
+ import { readFileSync, existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+
5
+ export const DEFAULT_PORT = 3333;
6
+
7
+ function readAdvertise() {
8
+ const p = process.env.IMA2_ADVERTISE_FILE ||
9
+ join(process.env.IMA2_CONFIG_DIR || join(homedir(), ".ima2"), "server.json");
10
+ if (!existsSync(p)) return null;
11
+ try {
12
+ return JSON.parse(readFileSync(p, "utf-8"));
13
+ } catch {
14
+ return null;
15
+ }
16
+ }
17
+
18
+ async function probe(base, timeoutMs = 600) {
19
+ try {
20
+ const r = await fetch(`${base}/api/health`, { signal: AbortSignal.timeout(timeoutMs) });
21
+ if (!r.ok) return null;
22
+ return await r.json();
23
+ } catch {
24
+ return null;
25
+ }
26
+ }
27
+
28
+ export async function resolveServer({ serverFlag }: any = {}) {
29
+ if (serverFlag) {
30
+ const base = serverFlag.replace(/\/$/, "");
31
+ const health = await probe(base);
32
+ if (health) return { base, health };
33
+ const err: any = new Error(`server unreachable at ${base}`);
34
+ err.code = "SERVER_UNREACHABLE";
35
+ throw err;
36
+ }
37
+ const candidates = [];
38
+ if (process.env.IMA2_SERVER) candidates.push(process.env.IMA2_SERVER.replace(/\/$/, ""));
39
+ const adv = readAdvertise();
40
+ if (adv?.backend?.url) candidates.push(String(adv.backend.url).replace(/\/$/, ""));
41
+ if (adv?.url) candidates.push(String(adv.url).replace(/\/$/, ""));
42
+ if (adv?.port) candidates.push(`http://localhost:${adv.port}`);
43
+ candidates.push(`http://localhost:${DEFAULT_PORT}`);
44
+
45
+ const seen = new Set();
46
+ const uniq = candidates.filter((c) => !seen.has(c) && seen.add(c));
47
+
48
+ for (const base of uniq) {
49
+ const health = await probe(base);
50
+ if (health) return { base, health };
51
+ }
52
+ const err: any = new Error("server unreachable — is 'ima2 serve' running?");
53
+ err.code = "SERVER_UNREACHABLE";
54
+ throw err;
55
+ }
56
+
57
+ export async function request(base, path, {
58
+ method = "GET",
59
+ body,
60
+ headers: extraHeaders,
61
+ raw = false,
62
+ timeoutMs = 180_000,
63
+ }: any = {}) {
64
+ const baseHeaders: Record<string, string> = raw
65
+ ? { "X-ima2-client": `cli/${CLI_VERSION}` }
66
+ : { "Content-Type": "application/json", "X-ima2-client": `cli/${CLI_VERSION}` };
67
+ const finalHeaders = { ...baseHeaders, ...(extraHeaders || {}) };
68
+ const res = await fetch(base + path, {
69
+ method,
70
+ headers: finalHeaders,
71
+ body: body === undefined ? undefined
72
+ : raw ? body
73
+ : JSON.stringify(body),
74
+ signal: AbortSignal.timeout(timeoutMs),
75
+ });
76
+ const text = await res.text();
77
+ let json: any = null;
78
+ try { json = JSON.parse(text); } catch {}
79
+ if (!res.ok) {
80
+ const err: any = new Error(json?.error?.message || json?.error || `HTTP ${res.status}`);
81
+ err.status = res.status;
82
+ err.code = json?.error?.code || json?.code || null;
83
+ err.body = json || text;
84
+ throw err;
85
+ }
86
+ return json;
87
+ }
88
+
89
+ export function normalizeGenerate(resp) {
90
+ if (!resp) return { images: [], elapsed: null, requestId: null };
91
+ if (Array.isArray(resp.images)) {
92
+ return {
93
+ images: resp.images.map((it) => ({ image: it.image, filename: it.filename })),
94
+ elapsed: resp.elapsed ?? null,
95
+ requestId: resp.requestId ?? null,
96
+ };
97
+ }
98
+ if (resp.image) {
99
+ return {
100
+ images: [{ image: resp.image, filename: resp.filename || null }],
101
+ elapsed: resp.elapsed ?? null,
102
+ requestId: resp.requestId ?? null,
103
+ };
104
+ }
105
+ return { images: [], elapsed: resp.elapsed ?? null, requestId: resp.requestId ?? null };
106
+ }
107
+
108
+ export let CLI_VERSION = "dev";
109
+ export function setCliVersion(v) { CLI_VERSION = v; }
@@ -1,23 +1,20 @@
1
1
  const HINTS = {
2
- SERVER_UNREACHABLE: "Start `ima2 serve`, or pass `--server <url>`.",
3
- APIKEY_DISABLED: "Image generation is OAuth-only. Use Codex login/OAuth.",
4
- IMAGE_MODEL_UNSUPPORTED:
5
- "This model is visible but cannot generate images here. Use gpt-5.4 or gpt-5.4-mini.",
6
- INVALID_IMAGE_MODEL: "Use one of: gpt-5.5, gpt-5.4, gpt-5.4-mini.",
7
- OAUTH_UNAVAILABLE: "OAuth proxy is unavailable. Check `ima2 doctor` and restart `ima2 serve`.",
8
- NETWORK_FAILED: "Network/proxy failed. This is not a moderation refusal.",
9
- SAFETY_REFUSAL: "The image backend refused this generation.",
10
- MODERATION_REFUSED: "The prompt or image was rejected by moderation.",
11
- AUTH_CHATGPT_EXPIRED: "Run `npx @openai/codex login`, then restart `ima2 serve`.",
12
- REF_TOO_LARGE: "Reference image is too large. Resize/compress it and retry.",
13
- REF_NOT_BASE64: "Reference payload is invalid. Use a normal PNG/JPEG/WebP file.",
2
+ SERVER_UNREACHABLE: "Start `ima2 serve`, or pass `--server <url>`.",
3
+ APIKEY_DISABLED: "API-key generation is supported in current builds; switch providers or update the configured API key.",
4
+ IMAGE_MODEL_UNSUPPORTED: "This model is visible but cannot generate images here. Use gpt-5.4 or gpt-5.4-mini.",
5
+ INVALID_IMAGE_MODEL: "Use one of: gpt-5.5, gpt-5.4, gpt-5.4-mini.",
6
+ OAUTH_UNAVAILABLE: "OAuth proxy is unavailable. Check `ima2 doctor` and restart `ima2 serve`.",
7
+ NETWORK_FAILED: "Network/proxy failed. This is not a moderation refusal.",
8
+ SAFETY_REFUSAL: "The image backend refused this generation.",
9
+ MODERATION_REFUSED: "The prompt or image was rejected by moderation.",
10
+ AUTH_CHATGPT_EXPIRED: "Run `npx @openai/codex login`, then restart `ima2 serve`.",
11
+ REF_TOO_LARGE: "Reference image is too large. Resize/compress it and retry.",
12
+ REF_NOT_BASE64: "Reference payload is invalid. Use a normal PNG/JPEG/WebP file.",
14
13
  };
15
-
16
14
  export function hintForErrorCode(code) {
17
- return code ? HINTS[code] || null : null;
15
+ return code ? HINTS[code] || null : null;
18
16
  }
19
-
20
17
  export function formatErrorWithHint(message, code) {
21
- const hint = hintForErrorCode(code);
22
- return hint ? `${message}\nHint: ${hint}` : message;
18
+ const hint = hintForErrorCode(code);
19
+ return hint ? `${message}\nHint: ${hint}` : message;
23
20
  }
@@ -0,0 +1,23 @@
1
+ const HINTS = {
2
+ SERVER_UNREACHABLE: "Start `ima2 serve`, or pass `--server <url>`.",
3
+ APIKEY_DISABLED: "API-key generation is supported in current builds; switch providers or update the configured API key.",
4
+ IMAGE_MODEL_UNSUPPORTED:
5
+ "This model is visible but cannot generate images here. Use gpt-5.4 or gpt-5.4-mini.",
6
+ INVALID_IMAGE_MODEL: "Use one of: gpt-5.5, gpt-5.4, gpt-5.4-mini.",
7
+ OAUTH_UNAVAILABLE: "OAuth proxy is unavailable. Check `ima2 doctor` and restart `ima2 serve`.",
8
+ NETWORK_FAILED: "Network/proxy failed. This is not a moderation refusal.",
9
+ SAFETY_REFUSAL: "The image backend refused this generation.",
10
+ MODERATION_REFUSED: "The prompt or image was rejected by moderation.",
11
+ AUTH_CHATGPT_EXPIRED: "Run `npx @openai/codex login`, then restart `ima2 serve`.",
12
+ REF_TOO_LARGE: "Reference image is too large. Resize/compress it and retry.",
13
+ REF_NOT_BASE64: "Reference payload is invalid. Use a normal PNG/JPEG/WebP file.",
14
+ };
15
+
16
+ export function hintForErrorCode(code) {
17
+ return code ? HINTS[code] || null : null;
18
+ }
19
+
20
+ export function formatErrorWithHint(message, code) {
21
+ const hint = hintForErrorCode(code);
22
+ return hint ? `${message}\nHint: ${hint}` : message;
23
+ }
package/bin/lib/files.js CHANGED
@@ -1,39 +1,37 @@
1
1
  import { readFile, writeFile, mkdir } from "node:fs/promises";
2
- import { dirname, extname, basename, join } from "node:path";
3
-
2
+ import { dirname, extname } from "node:path";
4
3
  const MIME = {
5
- png: "image/png",
6
- jpg: "image/jpeg",
7
- jpeg: "image/jpeg",
8
- webp: "image/webp",
4
+ png: "image/png",
5
+ jpg: "image/jpeg",
6
+ jpeg: "image/jpeg",
7
+ webp: "image/webp",
9
8
  };
10
-
11
9
  export async function fileToDataUri(path) {
12
- const b64 = (await readFile(path)).toString("base64");
13
- const ext = extname(path).slice(1).toLowerCase();
14
- const mime = MIME[ext] || "image/png";
15
- return `data:${mime};base64,${b64}`;
10
+ const b64 = (await readFile(path)).toString("base64");
11
+ const ext = extname(path).slice(1).toLowerCase();
12
+ const mime = MIME[ext] || "image/png";
13
+ return `data:${mime};base64,${b64}`;
16
14
  }
17
-
18
15
  export async function dataUriToFile(dataUri, outPath) {
19
- const m = dataUri.match(/^data:([^;]+);base64,(.+)$/);
20
- const raw = m ? m[2] : dataUri;
21
- await mkdir(dirname(outPath) || ".", { recursive: true });
22
- await writeFile(outPath, Buffer.from(raw, "base64"));
23
- return outPath;
16
+ const m = dataUri.match(/^data:([^;]+);base64,(.+)$/);
17
+ const raw = m ? m[2] : dataUri;
18
+ await mkdir(dirname(outPath) || ".", { recursive: true });
19
+ await writeFile(outPath, Buffer.from(raw, "base64"));
20
+ return outPath;
24
21
  }
25
-
26
22
  export function defaultOutName(index, total, ext = "png") {
27
- const now = new Date();
28
- const pad = (n) => String(n).padStart(2, "0");
29
- const stamp = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
30
- if (total <= 1) return `ima2-${stamp}.${ext}`;
31
- return `ima2-${stamp}-${index}.${ext}`;
23
+ const now = new Date();
24
+ const pad = (n) => String(n).padStart(2, "0");
25
+ const stamp = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
26
+ if (total <= 1)
27
+ return `ima2-${stamp}.${ext}`;
28
+ return `ima2-${stamp}-${index}.${ext}`;
32
29
  }
33
-
34
30
  export async function readStdin() {
35
- if (process.stdin.isTTY) return "";
36
- const chunks = [];
37
- for await (const c of process.stdin) chunks.push(c);
38
- return Buffer.concat(chunks).toString("utf-8").trim();
31
+ if (process.stdin.isTTY)
32
+ return "";
33
+ const chunks = [];
34
+ for await (const c of process.stdin)
35
+ chunks.push(c);
36
+ return Buffer.concat(chunks).toString("utf-8").trim();
39
37
  }
@@ -0,0 +1,39 @@
1
+ import { readFile, writeFile, mkdir } from "node:fs/promises";
2
+ import { dirname, extname, basename, join } from "node:path";
3
+
4
+ const MIME = {
5
+ png: "image/png",
6
+ jpg: "image/jpeg",
7
+ jpeg: "image/jpeg",
8
+ webp: "image/webp",
9
+ };
10
+
11
+ export async function fileToDataUri(path) {
12
+ const b64 = (await readFile(path)).toString("base64");
13
+ const ext = extname(path).slice(1).toLowerCase();
14
+ const mime = MIME[ext] || "image/png";
15
+ return `data:${mime};base64,${b64}`;
16
+ }
17
+
18
+ export async function dataUriToFile(dataUri, outPath) {
19
+ const m = dataUri.match(/^data:([^;]+);base64,(.+)$/);
20
+ const raw = m ? m[2] : dataUri;
21
+ await mkdir(dirname(outPath) || ".", { recursive: true });
22
+ await writeFile(outPath, Buffer.from(raw, "base64"));
23
+ return outPath;
24
+ }
25
+
26
+ export function defaultOutName(index, total, ext = "png") {
27
+ const now = new Date();
28
+ const pad = (n) => String(n).padStart(2, "0");
29
+ const stamp = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
30
+ if (total <= 1) return `ima2-${stamp}.${ext}`;
31
+ return `ima2-${stamp}-${index}.${ext}`;
32
+ }
33
+
34
+ export async function readStdin() {
35
+ if (process.stdin.isTTY) return "";
36
+ const chunks = [];
37
+ for await (const c of process.stdin) chunks.push(c);
38
+ return Buffer.concat(chunks).toString("utf-8").trim();
39
+ }
package/bin/lib/output.js CHANGED
@@ -1,58 +1,60 @@
1
1
  import { formatErrorWithHint } from "./error-hints.js";
2
-
3
2
  const isTty = process.stdout.isTTY && !process.env.NO_COLOR;
4
-
5
3
  export const color = {
6
- dim: (s) => (isTty ? `\x1b[2m${s}\x1b[0m` : s),
7
- bold: (s) => (isTty ? `\x1b[1m${s}\x1b[0m` : s),
8
- red: (s) => (isTty ? `\x1b[31m${s}\x1b[0m` : s),
9
- green: (s) => (isTty ? `\x1b[32m${s}\x1b[0m` : s),
10
- yellow: (s) => (isTty ? `\x1b[33m${s}\x1b[0m` : s),
11
- cyan: (s) => (isTty ? `\x1b[36m${s}\x1b[0m` : s),
4
+ dim: (s) => (isTty ? `\x1b[2m${s}\x1b[0m` : s),
5
+ bold: (s) => (isTty ? `\x1b[1m${s}\x1b[0m` : s),
6
+ red: (s) => (isTty ? `\x1b[31m${s}\x1b[0m` : s),
7
+ green: (s) => (isTty ? `\x1b[32m${s}\x1b[0m` : s),
8
+ yellow: (s) => (isTty ? `\x1b[33m${s}\x1b[0m` : s),
9
+ cyan: (s) => (isTty ? `\x1b[36m${s}\x1b[0m` : s),
12
10
  };
13
-
14
11
  export function out(msg = "") { process.stdout.write(msg + "\n"); }
15
12
  export function err(msg = "") { process.stderr.write(msg + "\n"); }
16
-
17
13
  export function die(code, msg) {
18
- if (msg) err(color.red("✗ ") + msg);
19
- process.exit(code);
14
+ if (msg)
15
+ err(color.red("✗ ") + msg);
16
+ process.exit(code);
20
17
  }
21
-
22
18
  export function dieWithError(e) {
23
- die(exitCodeForError(e), formatErrorWithHint(e?.message || String(e), e?.code));
19
+ die(exitCodeForError(e), formatErrorWithHint(e?.message || String(e), e?.code));
24
20
  }
25
-
26
21
  export function json(obj) {
27
- process.stdout.write(JSON.stringify(obj) + "\n");
22
+ process.stdout.write(JSON.stringify(obj) + "\n");
28
23
  }
29
-
30
24
  export function table(rows, columns) {
31
- if (rows.length === 0) return;
32
- const widths = columns.map((c) =>
33
- Math.max(c.label.length, ...rows.map((r) => {
34
- const v = c.format ? c.format(r[c.key], r) : r[c.key];
35
- return String(v ?? "").length;
36
- })),
37
- );
38
- const pad = (s, w) => String(s ?? "").padEnd(w);
39
- out(color.dim(columns.map((c, i) => pad(c.label, widths[i])).join(" ")));
40
- out(color.dim(widths.map((w) => "─".repeat(w)).join(" ")));
41
- for (const r of rows) {
42
- out(columns.map((c, i) => pad(c.format ? c.format(r[c.key], r) : r[c.key], widths[i])).join(" "));
43
- }
25
+ if (rows.length === 0)
26
+ return;
27
+ const widths = columns.map((c) => Math.max(c.label.length, ...rows.map((r) => {
28
+ const v = c.format ? c.format(r[c.key], r) : r[c.key];
29
+ return String(v ?? "").length;
30
+ })));
31
+ const pad = (s, w) => String(s ?? "").padEnd(w);
32
+ out(color.dim(columns.map((c, i) => pad(c.label, widths[i])).join(" ")));
33
+ out(color.dim(widths.map((w) => "─".repeat(w)).join(" ")));
34
+ for (const r of rows) {
35
+ out(columns.map((c, i) => pad(c.format ? c.format(r[c.key], r) : r[c.key], widths[i])).join(" "));
36
+ }
44
37
  }
45
-
46
38
  export function exitCodeForError(e) {
47
- if (e.code === "SERVER_UNREACHABLE") return 3;
48
- if (e.code === "APIKEY_DISABLED") return 4;
49
- if (e.code === "AUTH_CHATGPT_EXPIRED" || e.code === "OAUTH_UNAVAILABLE") return 4;
50
- if (e.code === "NETWORK_FAILED") return 6;
51
- if (e.code === "REF_TOO_LARGE" || e.code === "REF_NOT_BASE64") return 5;
52
- if (e.code === "SAFETY_REFUSAL") return 7;
53
- if (e.code === "MODERATION_REFUSED") return 7;
54
- if (e.name === "TimeoutError" || /abort/i.test(e.message || "")) return 8;
55
- if (e.status >= 500) return 6;
56
- if (e.status >= 400) return 5;
57
- return 1;
39
+ if (e.code === "SERVER_UNREACHABLE")
40
+ return 3;
41
+ if (e.code === "APIKEY_DISABLED")
42
+ return 4;
43
+ if (e.code === "AUTH_CHATGPT_EXPIRED" || e.code === "OAUTH_UNAVAILABLE")
44
+ return 4;
45
+ if (e.code === "NETWORK_FAILED")
46
+ return 6;
47
+ if (e.code === "REF_TOO_LARGE" || e.code === "REF_NOT_BASE64")
48
+ return 5;
49
+ if (e.code === "SAFETY_REFUSAL")
50
+ return 7;
51
+ if (e.code === "MODERATION_REFUSED")
52
+ return 7;
53
+ if (e.name === "TimeoutError" || /abort/i.test(e.message || ""))
54
+ return 8;
55
+ if (e.status >= 500)
56
+ return 6;
57
+ if (e.status >= 400)
58
+ return 5;
59
+ return 1;
58
60
  }
@@ -0,0 +1,58 @@
1
+ import { formatErrorWithHint } from "./error-hints.js";
2
+
3
+ const isTty = process.stdout.isTTY && !process.env.NO_COLOR;
4
+
5
+ export const color = {
6
+ dim: (s) => (isTty ? `\x1b[2m${s}\x1b[0m` : s),
7
+ bold: (s) => (isTty ? `\x1b[1m${s}\x1b[0m` : s),
8
+ red: (s) => (isTty ? `\x1b[31m${s}\x1b[0m` : s),
9
+ green: (s) => (isTty ? `\x1b[32m${s}\x1b[0m` : s),
10
+ yellow: (s) => (isTty ? `\x1b[33m${s}\x1b[0m` : s),
11
+ cyan: (s) => (isTty ? `\x1b[36m${s}\x1b[0m` : s),
12
+ };
13
+
14
+ export function out(msg = "") { process.stdout.write(msg + "\n"); }
15
+ export function err(msg = "") { process.stderr.write(msg + "\n"); }
16
+
17
+ export function die(code, msg) {
18
+ if (msg) err(color.red("✗ ") + msg);
19
+ process.exit(code);
20
+ }
21
+
22
+ export function dieWithError(e) {
23
+ die(exitCodeForError(e), formatErrorWithHint(e?.message || String(e), e?.code));
24
+ }
25
+
26
+ export function json(obj) {
27
+ process.stdout.write(JSON.stringify(obj) + "\n");
28
+ }
29
+
30
+ export function table(rows, columns) {
31
+ if (rows.length === 0) return;
32
+ const widths = columns.map((c) =>
33
+ Math.max(c.label.length, ...rows.map((r) => {
34
+ const v = c.format ? c.format(r[c.key], r) : r[c.key];
35
+ return String(v ?? "").length;
36
+ })),
37
+ );
38
+ const pad = (s, w) => String(s ?? "").padEnd(w);
39
+ out(color.dim(columns.map((c, i) => pad(c.label, widths[i])).join(" ")));
40
+ out(color.dim(widths.map((w) => "─".repeat(w)).join(" ")));
41
+ for (const r of rows) {
42
+ out(columns.map((c, i) => pad(c.format ? c.format(r[c.key], r) : r[c.key], widths[i])).join(" "));
43
+ }
44
+ }
45
+
46
+ export function exitCodeForError(e) {
47
+ if (e.code === "SERVER_UNREACHABLE") return 3;
48
+ if (e.code === "APIKEY_DISABLED") return 4;
49
+ if (e.code === "AUTH_CHATGPT_EXPIRED" || e.code === "OAUTH_UNAVAILABLE") return 4;
50
+ if (e.code === "NETWORK_FAILED") return 6;
51
+ if (e.code === "REF_TOO_LARGE" || e.code === "REF_NOT_BASE64") return 5;
52
+ if (e.code === "SAFETY_REFUSAL") return 7;
53
+ if (e.code === "MODERATION_REFUSED") return 7;
54
+ if (e.name === "TimeoutError" || /abort/i.test(e.message || "")) return 8;
55
+ if (e.status >= 500) return 6;
56
+ if (e.status >= 400) return 5;
57
+ return 1;
58
+ }