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,214 @@
1
+ import { constants as fsConstants } from "node:fs";
2
+ import { access, readFile, realpath, stat } from "node:fs/promises";
3
+ import { basename, extname, isAbsolute, join, relative } from "node:path";
4
+
5
+ export const COMFY_ERROR = {
6
+ URL_NOT_LOCAL: "COMFY_URL_NOT_LOCAL",
7
+ IMAGE_INVALID: "COMFY_IMAGE_INVALID",
8
+ IMAGE_NOT_FOUND: "COMFY_IMAGE_NOT_FOUND",
9
+ UPLOAD_FAILED: "COMFY_UPLOAD_FAILED",
10
+ };
11
+
12
+ const LOCAL_HOSTS = new Set(["127.0.0.1", "localhost", "[::1]"]);
13
+
14
+ class ComfyBridgeError extends Error {
15
+ constructor(code, message, status) {
16
+ super(message);
17
+ this.name = "ComfyBridgeError";
18
+ (this as any).code = code;
19
+ (this as any).status = status;
20
+ }
21
+ }
22
+
23
+ function bridgeError(code, message, status) {
24
+ return new ComfyBridgeError(code, message, status);
25
+ }
26
+
27
+ export function isComfyBridgeError(error) {
28
+ return error instanceof ComfyBridgeError;
29
+ }
30
+
31
+ export function normalizeComfyOrigin(rawUrl) {
32
+ if (typeof rawUrl !== "string" || !rawUrl.trim()) {
33
+ throw bridgeError(COMFY_ERROR.URL_NOT_LOCAL, "ComfyUI URL is not configured.", 400);
34
+ }
35
+ const trimmed = rawUrl.trim();
36
+ const rawHost = trimmed.match(/^http:\/\/(?:[^@/]+@)?(\[[^\]]+\]|[^/:?#]+)/i)?.[1] ?? "";
37
+ if (
38
+ rawHost === "localhost." ||
39
+ /^\d+$/.test(rawHost) ||
40
+ /^0x/i.test(rawHost) ||
41
+ /^0[0-9]+/.test(rawHost) ||
42
+ (/^[0-9.]+$/.test(rawHost) && rawHost.split(".").length !== 4) ||
43
+ rawHost.split(".").some((part) => part.length > 1 && part.startsWith("0"))
44
+ ) {
45
+ throw bridgeError(COMFY_ERROR.URL_NOT_LOCAL, "ComfyUI URL is not local.", 400);
46
+ }
47
+ let url;
48
+ try {
49
+ url = new URL(trimmed);
50
+ } catch {
51
+ throw bridgeError(COMFY_ERROR.URL_NOT_LOCAL, "ComfyUI URL is invalid.", 400);
52
+ }
53
+ if (url.protocol !== "http:") {
54
+ throw bridgeError(COMFY_ERROR.URL_NOT_LOCAL, "ComfyUI URL must use HTTP.", 400);
55
+ }
56
+ if (url.username || url.password || !url.port) {
57
+ throw bridgeError(COMFY_ERROR.URL_NOT_LOCAL, "ComfyUI URL is not local.", 400);
58
+ }
59
+ if (!LOCAL_HOSTS.has(url.hostname)) {
60
+ throw bridgeError(COMFY_ERROR.URL_NOT_LOCAL, "ComfyUI URL is not local.", 400);
61
+ }
62
+ if (url.pathname !== "/" || url.search || url.hash) {
63
+ throw bridgeError(COMFY_ERROR.URL_NOT_LOCAL, "ComfyUI URL must be an origin.", 400);
64
+ }
65
+ return url.origin;
66
+ }
67
+
68
+ function hasEncodedSeparator(filename) {
69
+ try {
70
+ const decoded = decodeURIComponent(filename);
71
+ return decoded.includes("/") || decoded.includes("\\");
72
+ } catch {
73
+ return true;
74
+ }
75
+ }
76
+
77
+ function validateFilename(filename) {
78
+ if (typeof filename !== "string" || !filename.trim()) {
79
+ throw bridgeError(COMFY_ERROR.IMAGE_INVALID, "A generated filename is required.", 400);
80
+ }
81
+ if (
82
+ isAbsolute(filename) ||
83
+ filename !== basename(filename) ||
84
+ filename.includes("/") ||
85
+ filename.includes("\\") ||
86
+ filename.includes("..") ||
87
+ /^[a-z][a-z0-9+.-]*:/i.test(filename) ||
88
+ hasEncodedSeparator(filename)
89
+ ) {
90
+ throw bridgeError(COMFY_ERROR.IMAGE_INVALID, "Generated filename is invalid.", 400);
91
+ }
92
+ return filename;
93
+ }
94
+
95
+ function isInsideDirectory(parent, candidate) {
96
+ const rel = relative(parent, candidate);
97
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
98
+ }
99
+
100
+ function sniffImage(buffer) {
101
+ if (
102
+ buffer.length >= 8 &&
103
+ buffer[0] === 0x89 &&
104
+ buffer[1] === 0x50 &&
105
+ buffer[2] === 0x4e &&
106
+ buffer[3] === 0x47 &&
107
+ buffer[4] === 0x0d &&
108
+ buffer[5] === 0x0a &&
109
+ buffer[6] === 0x1a &&
110
+ buffer[7] === 0x0a
111
+ ) {
112
+ return { ext: "png", mime: "image/png" };
113
+ }
114
+ if (buffer.length >= 3 && buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) {
115
+ return { ext: "jpg", mime: "image/jpeg" };
116
+ }
117
+ if (
118
+ buffer.length >= 12 &&
119
+ buffer.subarray(0, 4).toString("ascii") === "RIFF" &&
120
+ buffer.subarray(8, 12).toString("ascii") === "WEBP"
121
+ ) {
122
+ return { ext: "webp", mime: "image/webp" };
123
+ }
124
+ throw bridgeError(COMFY_ERROR.IMAGE_INVALID, "Generated file is not a supported image.", 400);
125
+ }
126
+
127
+ function sanitizeBaseName(filename) {
128
+ const raw = basename(filename, extname(filename));
129
+ const safe = raw.replace(/[^a-zA-Z0-9._-]+/g, "_").replace(/^_+|_+$/g, "");
130
+ return safe || "image";
131
+ }
132
+
133
+ async function readGeneratedImage(ctx, filename) {
134
+ const safeFilename = validateFilename(filename);
135
+ const generatedDir = await realpath(ctx.config.storage.generatedDir);
136
+ const candidatePath = join(ctx.config.storage.generatedDir, safeFilename);
137
+ try {
138
+ await access(candidatePath, fsConstants.F_OK);
139
+ } catch {
140
+ throw bridgeError(COMFY_ERROR.IMAGE_NOT_FOUND, "Generated image was not found.", 404);
141
+ }
142
+ let candidateReal;
143
+ try {
144
+ candidateReal = await realpath(candidatePath);
145
+ } catch {
146
+ throw bridgeError(COMFY_ERROR.IMAGE_NOT_FOUND, "Generated image was not found.", 404);
147
+ }
148
+ if (!isInsideDirectory(generatedDir, candidateReal)) {
149
+ throw bridgeError(COMFY_ERROR.IMAGE_INVALID, "Generated filename is invalid.", 400);
150
+ }
151
+ const info = await stat(candidateReal);
152
+ if (!info.isFile()) {
153
+ throw bridgeError(COMFY_ERROR.IMAGE_INVALID, "Generated filename is invalid.", 400);
154
+ }
155
+ if (info.size > ctx.config.comfy.maxUploadBytes) {
156
+ throw bridgeError(COMFY_ERROR.IMAGE_INVALID, "Generated image is too large.", 400);
157
+ }
158
+ const buffer = await readFile(candidateReal);
159
+ const imageType = sniffImage(buffer);
160
+ return {
161
+ buffer,
162
+ imageType,
163
+ sourceFilename: safeFilename,
164
+ uploadFilename: `ima2_${Date.now()}_${sanitizeBaseName(safeFilename)}.${imageType.ext}`,
165
+ };
166
+ }
167
+
168
+ async function postToComfy(origin, image, timeoutMs, fetchImpl = fetch) {
169
+ const controller = new AbortController();
170
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
171
+ try {
172
+ const form = new FormData();
173
+ form.append("image", new Blob([image.buffer], { type: image.imageType.mime }), image.uploadFilename);
174
+ form.append("type", "input");
175
+ const res = await fetchImpl(`${origin}/upload/image`, {
176
+ method: "POST",
177
+ body: form,
178
+ redirect: "manual",
179
+ signal: controller.signal,
180
+ });
181
+ if (res.status >= 300 && res.status < 400) {
182
+ throw bridgeError(COMFY_ERROR.UPLOAD_FAILED, "Could not upload image to ComfyUI.", 502);
183
+ }
184
+ if (!res.ok) {
185
+ throw bridgeError(COMFY_ERROR.UPLOAD_FAILED, "Could not upload image to ComfyUI.", 502);
186
+ }
187
+ const data: any = await res.json().catch(() => null);
188
+ if (!data || typeof data.name !== "string" || !data.name.trim()) {
189
+ throw bridgeError(COMFY_ERROR.UPLOAD_FAILED, "Could not upload image to ComfyUI.", 502);
190
+ }
191
+ return data.name;
192
+ } catch (error) {
193
+ if (isComfyBridgeError(error)) throw error;
194
+ throw bridgeError(COMFY_ERROR.UPLOAD_FAILED, "Could not upload image to ComfyUI.", 502);
195
+ } finally {
196
+ clearTimeout(timeout);
197
+ }
198
+ }
199
+
200
+ export async function exportImageToComfy(ctx, input, options: any = {}) {
201
+ const origin = normalizeComfyOrigin(options.comfyUrl ?? ctx.config.comfy.defaultUrl);
202
+ const image = await readGeneratedImage(ctx, input.filename);
203
+ const uploadedFilename = await postToComfy(
204
+ origin,
205
+ image,
206
+ ctx.config.comfy.uploadTimeoutMs,
207
+ options.fetchImpl,
208
+ );
209
+ return {
210
+ ok: true,
211
+ sourceFilename: image.sourceFilename,
212
+ uploadedFilename,
213
+ };
214
+ }
package/lib/db.js CHANGED
@@ -2,28 +2,25 @@ import Database from "better-sqlite3";
2
2
  import { mkdirSync, existsSync } from "fs";
3
3
  import { dirname } from "path";
4
4
  import { config } from "../config.js";
5
-
6
5
  let db = null;
7
-
8
6
  export function getDbPath() {
9
- return config.storage.dbPath;
7
+ return config.storage.dbPath;
10
8
  }
11
-
12
9
  export function getDb() {
13
- if (db) return db;
14
- const dbPath = getDbPath();
15
- const dir = dirname(dbPath);
16
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
17
-
18
- db = new Database(dbPath);
19
- db.pragma("journal_mode = WAL");
20
- db.pragma("foreign_keys = ON");
21
- migrate(db);
22
- return db;
10
+ if (db)
11
+ return db;
12
+ const dbPath = getDbPath();
13
+ const dir = dirname(dbPath);
14
+ if (!existsSync(dir))
15
+ mkdirSync(dir, { recursive: true });
16
+ db = new Database(dbPath);
17
+ db.pragma("journal_mode = WAL");
18
+ db.pragma("foreign_keys = ON");
19
+ migrate(db);
20
+ return db;
23
21
  }
24
-
25
22
  function migrate(database) {
26
- database.exec(`
23
+ database.exec(`
27
24
  CREATE TABLE IF NOT EXISTS _meta (
28
25
  key TEXT PRIMARY KEY,
29
26
  value TEXT NOT NULL
@@ -77,27 +74,21 @@ function migrate(database) {
77
74
  CREATE INDEX IF NOT EXISTS idx_inflight_kind ON inflight(kind);
78
75
  CREATE INDEX IF NOT EXISTS idx_inflight_session ON inflight(session_id);
79
76
  `);
80
-
81
- const sessionColumns = database
82
- .prepare("PRAGMA table_info(sessions)")
83
- .all()
84
- .map((row) => row.name);
85
- if (!sessionColumns.includes("graph_version")) {
86
- database.exec(
87
- "ALTER TABLE sessions ADD COLUMN graph_version INTEGER NOT NULL DEFAULT 0",
88
- );
89
- }
90
- if (!sessionColumns.includes("style_sheet")) {
91
- database.exec("ALTER TABLE sessions ADD COLUMN style_sheet TEXT");
92
- }
93
- if (!sessionColumns.includes("style_sheet_enabled")) {
94
- database.exec(
95
- "ALTER TABLE sessions ADD COLUMN style_sheet_enabled INTEGER NOT NULL DEFAULT 0",
96
- );
97
- }
98
-
99
- // ── Prompt Library (schema v4) ──
100
- database.exec(`
77
+ const sessionColumns = database
78
+ .prepare("PRAGMA table_info(sessions)")
79
+ .all()
80
+ .map((row) => row.name);
81
+ if (!sessionColumns.includes("graph_version")) {
82
+ database.exec("ALTER TABLE sessions ADD COLUMN graph_version INTEGER NOT NULL DEFAULT 0");
83
+ }
84
+ if (!sessionColumns.includes("style_sheet")) {
85
+ database.exec("ALTER TABLE sessions ADD COLUMN style_sheet TEXT");
86
+ }
87
+ if (!sessionColumns.includes("style_sheet_enabled")) {
88
+ database.exec("ALTER TABLE sessions ADD COLUMN style_sheet_enabled INTEGER NOT NULL DEFAULT 0");
89
+ }
90
+ // ── Prompt Library (schema v4) ──
91
+ database.exec(`
101
92
  CREATE TABLE IF NOT EXISTS prompt_folders (
102
93
  id TEXT PRIMARY KEY,
103
94
  parent_id TEXT NOT NULL,
@@ -147,20 +138,19 @@ function migrate(database) {
147
138
  ('__root__', '__root__', '__root__'),
148
139
  ('__trash__', '__root__', '__trash__');
149
140
  `);
150
-
151
- const row = database.prepare("SELECT value FROM _meta WHERE key = 'schema_version'").get();
152
- if (!row) {
153
- database.prepare("INSERT INTO _meta (key, value) VALUES ('schema_version', '4')").run();
154
- } else if (row.value !== "4") {
155
- database
156
- .prepare("UPDATE _meta SET value = '4' WHERE key = 'schema_version'")
157
- .run();
158
- }
141
+ const row = database.prepare("SELECT value FROM _meta WHERE key = 'schema_version'").get();
142
+ if (!row) {
143
+ database.prepare("INSERT INTO _meta (key, value) VALUES ('schema_version', '4')").run();
144
+ }
145
+ else if (row.value !== "4") {
146
+ database
147
+ .prepare("UPDATE _meta SET value = '4' WHERE key = 'schema_version'")
148
+ .run();
149
+ }
159
150
  }
160
-
161
151
  export function closeDb() {
162
- if (db) {
163
- db.close();
164
- db = null;
165
- }
152
+ if (db) {
153
+ db.close();
154
+ db = null;
155
+ }
166
156
  }
package/lib/db.ts ADDED
@@ -0,0 +1,166 @@
1
+ import Database from "better-sqlite3";
2
+ import { mkdirSync, existsSync } from "fs";
3
+ import { dirname } from "path";
4
+ import { config } from "../config.js";
5
+
6
+ let db = null;
7
+
8
+ export function getDbPath() {
9
+ return config.storage.dbPath;
10
+ }
11
+
12
+ export function getDb() {
13
+ if (db) return db;
14
+ const dbPath = getDbPath();
15
+ const dir = dirname(dbPath);
16
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
17
+
18
+ db = new Database(dbPath);
19
+ db.pragma("journal_mode = WAL");
20
+ db.pragma("foreign_keys = ON");
21
+ migrate(db);
22
+ return db;
23
+ }
24
+
25
+ function migrate(database) {
26
+ database.exec(`
27
+ CREATE TABLE IF NOT EXISTS _meta (
28
+ key TEXT PRIMARY KEY,
29
+ value TEXT NOT NULL
30
+ );
31
+
32
+ CREATE TABLE IF NOT EXISTS sessions (
33
+ id TEXT PRIMARY KEY,
34
+ title TEXT NOT NULL DEFAULT 'Untitled',
35
+ created_at INTEGER NOT NULL,
36
+ updated_at INTEGER NOT NULL,
37
+ graph_version INTEGER NOT NULL DEFAULT 0
38
+ );
39
+
40
+ CREATE TABLE IF NOT EXISTS nodes (
41
+ session_id TEXT NOT NULL,
42
+ id TEXT NOT NULL,
43
+ x REAL NOT NULL DEFAULT 0,
44
+ y REAL NOT NULL DEFAULT 0,
45
+ data TEXT NOT NULL DEFAULT '{}',
46
+ PRIMARY KEY (session_id, id),
47
+ FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
48
+ );
49
+
50
+ CREATE TABLE IF NOT EXISTS edges (
51
+ session_id TEXT NOT NULL,
52
+ id TEXT NOT NULL,
53
+ source TEXT NOT NULL,
54
+ target TEXT NOT NULL,
55
+ data TEXT NOT NULL DEFAULT '{}',
56
+ PRIMARY KEY (session_id, id),
57
+ FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
58
+ );
59
+
60
+ CREATE INDEX IF NOT EXISTS idx_nodes_session ON nodes(session_id);
61
+ CREATE INDEX IF NOT EXISTS idx_edges_session ON edges(session_id);
62
+
63
+ CREATE TABLE IF NOT EXISTS inflight (
64
+ request_id TEXT PRIMARY KEY,
65
+ kind TEXT NOT NULL,
66
+ prompt TEXT NOT NULL DEFAULT '',
67
+ meta TEXT NOT NULL DEFAULT '{}',
68
+ session_id TEXT,
69
+ parent_node_id TEXT,
70
+ client_node_id TEXT,
71
+ started_at INTEGER NOT NULL,
72
+ phase TEXT NOT NULL DEFAULT 'queued',
73
+ phase_at INTEGER NOT NULL
74
+ );
75
+
76
+ CREATE INDEX IF NOT EXISTS idx_inflight_started ON inflight(started_at);
77
+ CREATE INDEX IF NOT EXISTS idx_inflight_kind ON inflight(kind);
78
+ CREATE INDEX IF NOT EXISTS idx_inflight_session ON inflight(session_id);
79
+ `);
80
+
81
+ const sessionColumns = database
82
+ .prepare("PRAGMA table_info(sessions)")
83
+ .all()
84
+ .map((row) => row.name);
85
+ if (!sessionColumns.includes("graph_version")) {
86
+ database.exec(
87
+ "ALTER TABLE sessions ADD COLUMN graph_version INTEGER NOT NULL DEFAULT 0",
88
+ );
89
+ }
90
+ if (!sessionColumns.includes("style_sheet")) {
91
+ database.exec("ALTER TABLE sessions ADD COLUMN style_sheet TEXT");
92
+ }
93
+ if (!sessionColumns.includes("style_sheet_enabled")) {
94
+ database.exec(
95
+ "ALTER TABLE sessions ADD COLUMN style_sheet_enabled INTEGER NOT NULL DEFAULT 0",
96
+ );
97
+ }
98
+
99
+ // ── Prompt Library (schema v4) ──
100
+ database.exec(`
101
+ CREATE TABLE IF NOT EXISTS prompt_folders (
102
+ id TEXT PRIMARY KEY,
103
+ parent_id TEXT NOT NULL,
104
+ name TEXT NOT NULL COLLATE NOCASE,
105
+ created_at INTEGER NOT NULL DEFAULT (unixepoch()),
106
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch()),
107
+ UNIQUE(parent_id, name)
108
+ );
109
+
110
+ CREATE TABLE IF NOT EXISTS prompts (
111
+ id TEXT PRIMARY KEY,
112
+ folder_id TEXT NOT NULL DEFAULT '__root__',
113
+ name TEXT NOT NULL,
114
+ text TEXT NOT NULL,
115
+ tags TEXT,
116
+ mode TEXT,
117
+ is_favorite INTEGER NOT NULL DEFAULT 0,
118
+ favorited_at INTEGER,
119
+ created_at INTEGER NOT NULL DEFAULT (unixepoch()),
120
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch()),
121
+ FOREIGN KEY (folder_id) REFERENCES prompt_folders(id) ON DELETE SET DEFAULT
122
+ );
123
+
124
+ CREATE TABLE IF NOT EXISTS gallery_favorites (
125
+ id TEXT PRIMARY KEY,
126
+ browser_id TEXT NOT NULL,
127
+ filename TEXT NOT NULL,
128
+ favorited_at INTEGER NOT NULL DEFAULT (unixepoch()),
129
+ UNIQUE(browser_id, filename)
130
+ );
131
+
132
+ CREATE TABLE IF NOT EXISTS image_annotations (
133
+ id TEXT PRIMARY KEY,
134
+ browser_id TEXT NOT NULL,
135
+ filename TEXT NOT NULL,
136
+ payload TEXT NOT NULL,
137
+ schema_version INTEGER NOT NULL DEFAULT 1,
138
+ created_at INTEGER NOT NULL DEFAULT (unixepoch()),
139
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch()),
140
+ UNIQUE(browser_id, filename)
141
+ );
142
+
143
+ CREATE INDEX IF NOT EXISTS idx_image_annotations_filename
144
+ ON image_annotations(filename);
145
+
146
+ INSERT OR IGNORE INTO prompt_folders (id, parent_id, name) VALUES
147
+ ('__root__', '__root__', '__root__'),
148
+ ('__trash__', '__root__', '__trash__');
149
+ `);
150
+
151
+ const row = database.prepare("SELECT value FROM _meta WHERE key = 'schema_version'").get();
152
+ if (!row) {
153
+ database.prepare("INSERT INTO _meta (key, value) VALUES ('schema_version', '4')").run();
154
+ } else if (row.value !== "4") {
155
+ database
156
+ .prepare("UPDATE _meta SET value = '4' WHERE key = 'schema_version'")
157
+ .run();
158
+ }
159
+ }
160
+
161
+ export function closeDb() {
162
+ if (db) {
163
+ db.close();
164
+ db = null;
165
+ }
166
+ }
@@ -1,35 +1,34 @@
1
1
  // 0.09.8 — upstream error classifier.
2
2
  // Pattern-match upstream OpenAI / OAuth / network errors into stable ImaErrorCode
3
3
  // values so the UI can surface localized, actionable messages with CTAs.
4
-
5
4
  /** @typedef {"REF_TOO_LARGE"|"REF_NOT_BASE64"|"REF_EMPTY"|"REF_TOO_MANY"|"MODERATION_REFUSED"|"UPSTREAM_5XX"|"AUTH_CHATGPT_EXPIRED"|"AUTH_API_KEY_INVALID"|"NETWORK_FAILED"|"OAUTH_UNAVAILABLE"|"INVALID_REQUEST"|"INVALID_MODERATION"|"APIKEY_DISABLED"|"SAFETY_REFUSAL"|"EMPTY_RESPONSE"|"OAUTH_UPSTREAM_ERROR"|"DB_ERROR"|"UNKNOWN"} ImaErrorCode */
6
-
7
5
  const INVALID_REQUEST_CODES = new Set([
8
- "bad_request",
9
- "invalid_request",
10
- "invalid_request_error",
11
- "invalid_value",
12
- "invalid_size",
13
- "invalid_type",
14
- "invalid_parameter",
15
- "missing_required_parameter",
16
- "unsupported_parameter",
17
- "unsupported_value",
6
+ "bad_request",
7
+ "invalid_request",
8
+ "invalid_request_error",
9
+ "invalid_value",
10
+ "invalid_size",
11
+ "invalid_type",
12
+ "invalid_parameter",
13
+ "missing_required_parameter",
14
+ "unsupported_parameter",
15
+ "unsupported_value",
18
16
  ]);
19
-
20
17
  /**
21
18
  * Normalize provider-specific request/validation codes into app codes.
22
19
  * @param {string | undefined | null} code
23
20
  * @returns {ImaErrorCode}
24
21
  */
25
22
  export function classifyUpstreamErrorCode(code) {
26
- const s = String(code || "").toLowerCase();
27
- if (!s) return "UNKNOWN";
28
- if (INVALID_REQUEST_CODES.has(s)) return "INVALID_REQUEST";
29
- if (s.includes("moderation_blocked") || s.includes("moderation refused")) return "MODERATION_REFUSED";
30
- return "UNKNOWN";
23
+ const s = String(code || "").toLowerCase();
24
+ if (!s)
25
+ return "UNKNOWN";
26
+ if (INVALID_REQUEST_CODES.has(s))
27
+ return "INVALID_REQUEST";
28
+ if (s.includes("moderation_blocked") || s.includes("moderation refused"))
29
+ return "MODERATION_REFUSED";
30
+ return "UNKNOWN";
31
31
  }
32
-
33
32
  /**
34
33
  * Classify an upstream error message into an ImaErrorCode.
35
34
  * Order matters: auth session expiry must beat generic "token" matches,
@@ -38,63 +37,48 @@ export function classifyUpstreamErrorCode(code) {
38
37
  * @returns {ImaErrorCode}
39
38
  */
40
39
  export function classifyUpstreamError(msg) {
41
- const s = String(msg || "").toLowerCase();
42
- if (!s) return "UNKNOWN";
43
-
44
- if (s.includes("moderation_blocked") || s.includes("moderation refused")) {
45
- return "MODERATION_REFUSED";
46
- }
47
-
48
- // ChatGPT sign-in session expiry must precede the generic api-key checks
49
- // so it is not misclassified when messages contain both "token" and "api".
50
- if (
51
- s.includes("token is expired") ||
52
- s.includes("sign in again") ||
53
- (s.includes("access token") && s.includes("expired")) ||
54
- (s.includes("token") && s.includes("expired") && !s.includes("api key"))
55
- ) {
56
- return "AUTH_CHATGPT_EXPIRED";
57
- }
58
-
59
- if (
60
- s.includes("incorrect api key") ||
61
- s.includes("invalid authentication") ||
62
- s.includes("exceeded your current quota") ||
63
- s.includes("incorrect organization")
64
- ) {
65
- return "AUTH_API_KEY_INVALID";
66
- }
67
-
68
- if (
69
- s.includes("failed to fetch") ||
70
- s.includes("econnrefused") ||
71
- s.includes("econnreset") ||
72
- s.includes("enotfound") ||
73
- s.includes("etimedout") ||
74
- s.includes("network error")
75
- ) {
76
- return "NETWORK_FAILED";
77
- }
78
-
79
- if (s.includes("oauth") && (s.includes("not running") || s.includes("unavailable") || s.includes("not ready"))) {
80
- return "OAUTH_UNAVAILABLE";
81
- }
82
-
83
- if (
84
- s.includes("invalid_request_error") ||
85
- s.includes("invalid_value") ||
86
- s.includes("invalid size") ||
87
- s.includes("invalid request") ||
88
- s.includes("requested resolution") ||
89
- s.includes("minimum pixel budget") ||
90
- s.includes("unsupported value")
91
- ) {
92
- return "INVALID_REQUEST";
93
- }
94
-
95
- if (s.includes("an error occurred while processing") || /\b5\d\d\b/.test(s)) {
96
- return "UPSTREAM_5XX";
97
- }
98
-
99
- return "UNKNOWN";
40
+ const s = String(msg || "").toLowerCase();
41
+ if (!s)
42
+ return "UNKNOWN";
43
+ if (s.includes("moderation_blocked") || s.includes("moderation refused")) {
44
+ return "MODERATION_REFUSED";
45
+ }
46
+ // ChatGPT sign-in session expiry must precede the generic api-key checks
47
+ // so it is not misclassified when messages contain both "token" and "api".
48
+ if (s.includes("token is expired") ||
49
+ s.includes("sign in again") ||
50
+ (s.includes("access token") && s.includes("expired")) ||
51
+ (s.includes("token") && s.includes("expired") && !s.includes("api key"))) {
52
+ return "AUTH_CHATGPT_EXPIRED";
53
+ }
54
+ if (s.includes("incorrect api key") ||
55
+ s.includes("invalid authentication") ||
56
+ s.includes("exceeded your current quota") ||
57
+ s.includes("incorrect organization")) {
58
+ return "AUTH_API_KEY_INVALID";
59
+ }
60
+ if (s.includes("failed to fetch") ||
61
+ s.includes("econnrefused") ||
62
+ s.includes("econnreset") ||
63
+ s.includes("enotfound") ||
64
+ s.includes("etimedout") ||
65
+ s.includes("network error")) {
66
+ return "NETWORK_FAILED";
67
+ }
68
+ if (s.includes("oauth") && (s.includes("not running") || s.includes("unavailable") || s.includes("not ready"))) {
69
+ return "OAUTH_UNAVAILABLE";
70
+ }
71
+ if (s.includes("invalid_request_error") ||
72
+ s.includes("invalid_value") ||
73
+ s.includes("invalid size") ||
74
+ s.includes("invalid request") ||
75
+ s.includes("requested resolution") ||
76
+ s.includes("minimum pixel budget") ||
77
+ s.includes("unsupported value")) {
78
+ return "INVALID_REQUEST";
79
+ }
80
+ if (s.includes("an error occurred while processing") || /\b5\d\d\b/.test(s)) {
81
+ return "UPSTREAM_5XX";
82
+ }
83
+ return "UNKNOWN";
100
84
  }