ima2-gen 1.1.6 → 1.1.8

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 (51) hide show
  1. package/.env.example +5 -0
  2. package/README.md +3 -0
  3. package/assets/phase-a-bg-cleanup-test.png +0 -0
  4. package/config.js +111 -0
  5. package/docs/FAQ.ko.md +20 -0
  6. package/docs/FAQ.md +20 -0
  7. package/docs/README.ko.md +3 -0
  8. package/docs/README.zh-CN.md +3 -0
  9. package/integrations/comfyui/ima2_gen_bridge/README.md +88 -0
  10. package/integrations/comfyui/ima2_gen_bridge/__init__.py +3 -0
  11. package/integrations/comfyui/ima2_gen_bridge/__pycache__/__init__.cpython-313.pyc +0 -0
  12. package/integrations/comfyui/ima2_gen_bridge/__pycache__/nodes.cpython-313.pyc +0 -0
  13. package/integrations/comfyui/ima2_gen_bridge/nodes.py +238 -0
  14. package/lib/canvasVersionStore.js +181 -0
  15. package/lib/cardNewsPlannerClient.js +4 -2
  16. package/lib/comfyBridge.js +214 -0
  17. package/lib/db.js +14 -0
  18. package/lib/historyList.js +4 -0
  19. package/lib/imageMetadata.js +4 -0
  20. package/lib/imageModels.js +20 -0
  21. package/lib/localImportStore.js +111 -0
  22. package/lib/oauthProxy.js +88 -38
  23. package/lib/pngInfo.js +26 -0
  24. package/lib/promptImport/curatedSources.js +139 -0
  25. package/lib/promptImport/discoveryRegistry.js +236 -0
  26. package/lib/promptImport/errors.js +16 -0
  27. package/lib/promptImport/githubDiscovery.js +248 -0
  28. package/lib/promptImport/githubFolder.js +308 -0
  29. package/lib/promptImport/githubSource.js +239 -0
  30. package/lib/promptImport/gptImageHints.js +68 -0
  31. package/lib/promptImport/parsePromptCandidates.js +153 -0
  32. package/lib/promptImport/promptIndex.js +248 -0
  33. package/lib/promptImport/rankPromptCandidates.js +49 -0
  34. package/package.json +3 -2
  35. package/routes/annotations.js +95 -0
  36. package/routes/canvasVersions.js +64 -0
  37. package/routes/comfy.js +39 -0
  38. package/routes/edit.js +73 -4
  39. package/routes/generate.js +16 -2
  40. package/routes/imageImport.js +33 -0
  41. package/routes/index.js +10 -0
  42. package/routes/multimode.js +18 -1
  43. package/routes/nodes.js +25 -3
  44. package/routes/promptImport.js +354 -0
  45. package/ui/dist/assets/index-BDffwmLs.css +1 -0
  46. package/ui/dist/assets/index-D0fdHLkJ.js +31 -0
  47. package/ui/dist/assets/index-D0fdHLkJ.js.map +1 -0
  48. package/ui/dist/index.html +6 -3
  49. package/ui/dist/assets/index-3X-6VjbF.css +0 -1
  50. package/ui/dist/assets/index-DPSq9qEs.js +0 -31
  51. package/ui/dist/assets/index-DPSq9qEs.js.map +0 -1
@@ -4,7 +4,7 @@ import { randomBytes } from "crypto";
4
4
  import { validateAndNormalizeRefs } from "../lib/refs.js";
5
5
  import { classifyUpstreamError } from "../lib/errorClassify.js";
6
6
  import { normalizeOAuthParams } from "../lib/oauthNormalize.js";
7
- import { normalizeImageModel } from "../lib/imageModels.js";
7
+ import { normalizeImageModel, normalizeReasoningEffort } from "../lib/imageModels.js";
8
8
  import { generateViaOAuth } from "../lib/oauthProxy.js";
9
9
  import { isNonRetryableGenerationError, normalizeGenerationFailure } from "../lib/generationErrors.js";
10
10
  import { startJob, finishJob } from "../lib/inflight.js";
@@ -39,6 +39,8 @@ export function registerGenerateRoutes(app, ctx) {
39
39
  references = [],
40
40
  mode: promptMode = "auto",
41
41
  model: rawModel,
42
+ reasoningEffort: rawReasoningEffort,
43
+ webSearchEnabled: rawWebSearchEnabled = true,
42
44
  } = req.body;
43
45
  const { quality, warnings: qualityWarnings } = normalizeOAuthParams({ provider, quality: rawQuality });
44
46
  const modelCheck = normalizeImageModel(ctx, rawModel);
@@ -49,6 +51,15 @@ export function registerGenerateRoutes(app, ctx) {
49
51
  return res.status(modelCheck.status).json({ error: modelCheck.error, code: modelCheck.code });
50
52
  }
51
53
  const imageModel = modelCheck.model;
54
+ const reasoningCheck = normalizeReasoningEffort(ctx, rawReasoningEffort);
55
+ if (reasoningCheck.error) {
56
+ finishStatus = "error";
57
+ finishHttpStatus = reasoningCheck.status;
58
+ finishErrorCode = reasoningCheck.code;
59
+ return res.status(reasoningCheck.status).json({ error: reasoningCheck.error, code: reasoningCheck.code });
60
+ }
61
+ const reasoningEffort = reasoningCheck.effort;
62
+ const webSearchEnabled = rawWebSearchEnabled !== false;
52
63
  const normalizedPromptMode = promptMode === "direct" ? "direct" : "auto";
53
64
 
54
65
  if (!prompt) return res.status(400).json({ error: "Prompt is required" });
@@ -106,6 +117,7 @@ export function registerGenerateRoutes(app, ctx) {
106
117
  clientNodeId,
107
118
  promptChars: typeof prompt === "string" ? prompt.length : 0,
108
119
  promptMode: normalizedPromptMode,
120
+ webSearchEnabled,
109
121
  });
110
122
  const startTime = Date.now();
111
123
 
@@ -127,7 +139,7 @@ export function registerGenerateRoutes(app, ctx) {
127
139
  requestId,
128
140
  normalizedPromptMode,
129
141
  ctx,
130
- { model: imageModel },
142
+ { model: imageModel, reasoningEffort, webSearchEnabled },
131
143
  );
132
144
  if (r.b64) return r;
133
145
  lastErr = new Error("Empty response (safety refusal)");
@@ -170,6 +182,7 @@ export function registerGenerateRoutes(app, ctx) {
170
182
  createdAt: Date.now(),
171
183
  usage: r.value.usage || null,
172
184
  webSearchCalls: r.value.webSearchCalls || 0,
185
+ webSearchEnabled,
173
186
  refsCount: refCheck.refs.length,
174
187
  };
175
188
  const rawBuffer = Buffer.from(r.value.b64, "base64");
@@ -242,6 +255,7 @@ export function registerGenerateRoutes(app, ctx) {
242
255
  warnings: qualityWarnings,
243
256
  revisedPrompt: firstRevised,
244
257
  promptMode: normalizedPromptMode,
258
+ webSearchEnabled,
245
259
  };
246
260
 
247
261
  if (count === 1) {
@@ -0,0 +1,33 @@
1
+ import express from "express";
2
+ import { createLocalImport } from "../lib/localImportStore.js";
3
+
4
+ function decodeHeader(value) {
5
+ if (typeof value !== "string" || !value) return null;
6
+ try {
7
+ return decodeURIComponent(value);
8
+ } catch {
9
+ return value;
10
+ }
11
+ }
12
+
13
+ export function registerImageImportRoutes(app, ctx) {
14
+ const rawImage = express.raw({
15
+ type: ["image/png", "image/jpeg", "image/webp"],
16
+ limit: ctx.config.server.bodyLimit,
17
+ });
18
+
19
+ app.post("/api/history/import-local", rawImage, async (req, res) => {
20
+ try {
21
+ const item = await createLocalImport(ctx, {
22
+ buffer: Buffer.isBuffer(req.body) ? req.body : Buffer.alloc(0),
23
+ originalFilename: decodeHeader(req.headers["x-ima2-original-filename"]),
24
+ });
25
+ res.status(201).json({ item });
26
+ } catch (err) {
27
+ res.status(err.status || 500).json({
28
+ error: err.message,
29
+ code: err.code || "IMPORT_FAILED",
30
+ });
31
+ }
32
+ });
33
+ }
package/routes/index.js CHANGED
@@ -9,12 +9,21 @@ import { registerStorageRoutes } from "./storage.js";
9
9
  import { registerCardNewsRoutes } from "./cardNews.js";
10
10
  import { registerMetadataRoutes } from "./metadata.js";
11
11
  import { registerPromptRoutes } from "./prompts.js";
12
+ import { registerPromptImportRoutes } from "./promptImport.js";
13
+ import { registerAnnotationRoutes } from "./annotations.js";
14
+ import { registerCanvasVersionRoutes } from "./canvasVersions.js";
15
+ import { registerComfyRoutes } from "./comfy.js";
16
+ import { registerImageImportRoutes } from "./imageImport.js";
12
17
 
13
18
  export function configureRoutes(app, ctx) {
14
19
  registerHealthRoutes(app, ctx);
15
20
  registerStorageRoutes(app, ctx);
16
21
  registerMetadataRoutes(app, ctx);
17
22
  registerHistoryRoutes(app, ctx);
23
+ registerAnnotationRoutes(app, ctx);
24
+ registerCanvasVersionRoutes(app, ctx);
25
+ registerImageImportRoutes(app, ctx);
26
+ registerComfyRoutes(app, ctx);
18
27
  registerSessionRoutes(app, ctx);
19
28
  registerEditRoutes(app, ctx);
20
29
  registerNodeRoutes(app, ctx);
@@ -22,4 +31,5 @@ export function configureRoutes(app, ctx) {
22
31
  registerMultimodeRoutes(app, ctx);
23
32
  registerGenerateRoutes(app, ctx);
24
33
  registerPromptRoutes(app, ctx);
34
+ registerPromptImportRoutes(app, ctx);
25
35
  }
@@ -4,7 +4,7 @@ import { randomBytes } from "crypto";
4
4
  import { validateAndNormalizeRefs } from "../lib/refs.js";
5
5
  import { classifyUpstreamError } from "../lib/errorClassify.js";
6
6
  import { normalizeOAuthParams } from "../lib/oauthNormalize.js";
7
- import { normalizeImageModel } from "../lib/imageModels.js";
7
+ import { normalizeImageModel, normalizeReasoningEffort } from "../lib/imageModels.js";
8
8
  import { generateMultimodeViaOAuth } from "../lib/oauthProxy.js";
9
9
  import { startJob, finishJob } from "../lib/inflight.js";
10
10
  import { logEvent, logError } from "../lib/logger.js";
@@ -56,6 +56,8 @@ export function registerMultimodeRoutes(app, ctx) {
56
56
  references = [],
57
57
  mode: promptMode = "auto",
58
58
  model: rawModel,
59
+ reasoningEffort: rawReasoningEffort,
60
+ webSearchEnabled: rawWebSearchEnabled = true,
59
61
  } = req.body;
60
62
  const maxImages = normalizeMaxImages(req.body?.maxImages);
61
63
  const normalizedPromptMode = promptMode === "direct" ? "direct" : "auto";
@@ -69,6 +71,16 @@ export function registerMultimodeRoutes(app, ctx) {
69
71
  return;
70
72
  }
71
73
  const imageModel = modelCheck.model;
74
+ const reasoningCheck = normalizeReasoningEffort(ctx, rawReasoningEffort);
75
+ if (reasoningCheck.error) {
76
+ finishStatus = "error";
77
+ finishHttpStatus = reasoningCheck.status;
78
+ finishErrorCode = reasoningCheck.code;
79
+ sendSse(res, "error", { error: reasoningCheck.error, code: reasoningCheck.code, status: reasoningCheck.status, requestId });
80
+ return;
81
+ }
82
+ const reasoningEffort = reasoningCheck.effort;
83
+ const webSearchEnabled = rawWebSearchEnabled !== false;
72
84
  if (!prompt) {
73
85
  finishStatus = "error";
74
86
  finishHttpStatus = 400;
@@ -122,6 +134,7 @@ export function registerMultimodeRoutes(app, ctx) {
122
134
  maxImages,
123
135
  refs: refCheck.refs.length,
124
136
  promptChars: typeof prompt === "string" ? prompt.length : 0,
137
+ webSearchEnabled,
125
138
  });
126
139
 
127
140
  const startTime = Date.now();
@@ -143,6 +156,8 @@ export function registerMultimodeRoutes(app, ctx) {
143
156
  {
144
157
  model: imageModel,
145
158
  maxImages,
159
+ reasoningEffort,
160
+ webSearchEnabled,
146
161
  onPartialImage: (partial) =>
147
162
  sendSse(res, "partial", {
148
163
  image: `data:${mime};base64,${partial.b64}`,
@@ -184,6 +199,7 @@ export function registerMultimodeRoutes(app, ctx) {
184
199
  createdAt: Date.now(),
185
200
  usage: generated.usage || null,
186
201
  webSearchCalls: generated.webSearchCalls || 0,
202
+ webSearchEnabled,
187
203
  refsCount: refCheck.refs.length,
188
204
  };
189
205
  const rawBuffer = Buffer.from(image.b64, "base64");
@@ -224,6 +240,7 @@ export function registerMultimodeRoutes(app, ctx) {
224
240
  model: imageModel,
225
241
  usage: generated.usage || null,
226
242
  webSearchCalls: generated.webSearchCalls || 0,
243
+ webSearchEnabled,
227
244
  warnings: qualityWarnings,
228
245
  extraIgnored: generated.extraIgnored || 0,
229
246
  promptMode: normalizedPromptMode,
package/routes/nodes.js CHANGED
@@ -10,7 +10,7 @@ import { startJob, finishJob } from "../lib/inflight.js";
10
10
  import { validateAndNormalizeRefs } from "../lib/refs.js";
11
11
  import { classifyUpstreamError } from "../lib/errorClassify.js";
12
12
  import { normalizeOAuthParams } from "../lib/oauthNormalize.js";
13
- import { normalizeImageModel } from "../lib/imageModels.js";
13
+ import { normalizeImageModel, normalizeReasoningEffort } from "../lib/imageModels.js";
14
14
  import { generateViaOAuth, editViaOAuth } from "../lib/oauthProxy.js";
15
15
  import { isNonRetryableGenerationError, normalizeGenerationFailure } from "../lib/generationErrors.js";
16
16
  import { logEvent, logError } from "../lib/logger.js";
@@ -87,6 +87,7 @@ export function registerNodeRoutes(app, ctx) {
87
87
  contextMode: rawContextMode = "parent-plus-refs",
88
88
  searchMode: rawSearchMode = "on",
89
89
  model: rawModel,
90
+ reasoningEffort: rawReasoningEffort,
90
91
  } = body;
91
92
  const { provider = "oauth" } = body;
92
93
  const { quality, warnings: qualityWarnings } = normalizeOAuthParams({ provider, quality: rawQuality });
@@ -101,11 +102,23 @@ export function registerNodeRoutes(app, ctx) {
101
102
  });
102
103
  }
103
104
  const imageModel = modelCheck.model;
105
+ const reasoningCheck = normalizeReasoningEffort(ctx, rawReasoningEffort);
106
+ if (reasoningCheck.error) {
107
+ finishStatus = "error";
108
+ finishHttpStatus = reasoningCheck.status;
109
+ finishErrorCode = reasoningCheck.code;
110
+ return res.status(reasoningCheck.status).json({
111
+ error: { code: reasoningCheck.code, message: reasoningCheck.error },
112
+ parentNodeId,
113
+ });
114
+ }
115
+ const reasoningEffort = reasoningCheck.effort;
104
116
  const normalizedPromptMode = promptMode === "direct" ? "direct" : "auto";
105
117
  const contextMode = ["parent-plus-refs", "parent-only", "ancestry"].includes(rawContextMode)
106
118
  ? rawContextMode
107
119
  : "parent-plus-refs";
108
120
  const searchMode = ["off", "auto", "on"].includes(rawSearchMode) ? rawSearchMode : "on";
121
+ const webSearchEnabled = body.webSearchEnabled !== false && searchMode !== "off";
109
122
  if (contextMode === "ancestry") {
110
123
  finishStatus = "error";
111
124
  finishHttpStatus = 400;
@@ -168,7 +181,6 @@ export function registerNodeRoutes(app, ctx) {
168
181
  const generateReferenceDiagnostics = operation === "generate" ? referenceDiagnostics : [];
169
182
  const referenceMismatchCount = generateReferenceDiagnostics.filter((ref) => ref.warnings?.includes("mime_mismatch")).length;
170
183
  const refsForRequest = contextMode === "parent-only" ? [] : (refCheck.refDetails || refCheck.refs);
171
- const webSearchEnabled = true;
172
184
  const parentImagePresent = !!parentB64;
173
185
  const inputImageCount = (parentImagePresent ? 1 : 0) + refsForRequest.length;
174
186
  logEvent("node", "request", {
@@ -227,7 +239,13 @@ export function registerNodeRoutes(app, ctx) {
227
239
  webSearchEnabled,
228
240
  });
229
241
  const r = parentB64
230
- ? await editViaOAuth(prompt, parentB64, quality, size, moderation, normalizedPromptMode, ctx, requestId, { model: imageModel, references: refsForRequest, searchMode: "on" })
242
+ ? await editViaOAuth(prompt, parentB64, quality, size, moderation, normalizedPromptMode, ctx, requestId, {
243
+ model: imageModel,
244
+ references: refsForRequest,
245
+ searchMode,
246
+ reasoningEffort,
247
+ webSearchEnabled,
248
+ })
231
249
  : await generateViaOAuth(
232
250
  prompt,
233
251
  quality,
@@ -239,6 +257,8 @@ export function registerNodeRoutes(app, ctx) {
239
257
  ctx,
240
258
  {
241
259
  model: imageModel,
260
+ reasoningEffort,
261
+ webSearchEnabled,
242
262
  partialImages: streamResponse ? 2 : 0,
243
263
  onPartialImage: streamResponse
244
264
  ? (partial) =>
@@ -336,6 +356,7 @@ export function registerNodeRoutes(app, ctx) {
336
356
  elapsed,
337
357
  usage: usage || null,
338
358
  webSearchCalls,
359
+ webSearchEnabled,
339
360
  contextMode,
340
361
  searchMode,
341
362
  provider: "oauth",
@@ -375,6 +396,7 @@ export function registerNodeRoutes(app, ctx) {
375
396
  elapsed,
376
397
  usage,
377
398
  webSearchCalls,
399
+ webSearchEnabled,
378
400
  provider: "oauth",
379
401
  model: imageModel,
380
402
  size,
@@ -0,0 +1,354 @@
1
+ import { getDb } from "../lib/db.js";
2
+ import { logError, logEvent } from "../lib/logger.js";
3
+ import { isPromptImportError, promptImportError } from "../lib/promptImport/errors.js";
4
+ import {
5
+ fetchGitHubSourceText,
6
+ isSupportedPromptFileName,
7
+ normalizeGitHubSource,
8
+ } from "../lib/promptImport/githubSource.js";
9
+ import {
10
+ fetchGitHubFolderFiles,
11
+ fetchSelectedGitHubFolderFiles,
12
+ normalizeGitHubFolderSource,
13
+ } from "../lib/promptImport/githubFolder.js";
14
+ import { parsePromptCandidates } from "../lib/promptImport/parsePromptCandidates.js";
15
+ import {
16
+ getPromptImportSources,
17
+ refreshCuratedSource,
18
+ searchCuratedPrompts,
19
+ } from "../lib/promptImport/promptIndex.js";
20
+ import { searchGitHubDiscovery } from "../lib/promptImport/githubDiscovery.js";
21
+ import {
22
+ listDiscoveryCandidates,
23
+ reviewDiscoveryCandidate,
24
+ } from "../lib/promptImport/discoveryRegistry.js";
25
+
26
+ function promptImportLimits(ctx) {
27
+ return {
28
+ maxFileBytesForPreview: ctx.config.limits.promptImportMaxFileBytes,
29
+ maxPromptCandidatesPerFile: ctx.config.limits.promptImportMaxCandidatesPerFile,
30
+ maxPromptCandidatesPerImport: ctx.config.limits.promptImportMaxCandidatesPerImport,
31
+ fetchTimeoutMs: ctx.config.limits.promptImportFetchTimeoutMs,
32
+ maxCandidateChars: ctx.config.limits.promptImportMaxCandidateChars,
33
+ minCandidateChars: ctx.config.limits.promptImportMinCandidateChars,
34
+ maxSourceCharsScanned: ctx.config.limits.promptImportMaxSourceCharsScanned,
35
+ maxRepoIndexFiles: ctx.config.limits.promptImportMaxRepoIndexFiles,
36
+ curatedSearchLimit: ctx.config.limits.promptImportCuratedSearchLimit,
37
+ indexCacheTtlMs: ctx.config.limits.promptImportIndexCacheTtlMs,
38
+ maxFolderFiles: ctx.config.limits.promptImportMaxFolderFiles,
39
+ maxFolderPreviewFiles: ctx.config.limits.promptImportMaxFolderPreviewFiles,
40
+ discoverySearchLimit: ctx.config.limits.promptImportDiscoverySearchLimit,
41
+ discoveryMaxQueries: ctx.config.limits.promptImportDiscoveryMaxQueries,
42
+ };
43
+ }
44
+
45
+ function sendPromptImportError(res, error) {
46
+ const status = isPromptImportError(error) ? error.status : 500;
47
+ const code = isPromptImportError(error) ? error.code : "PROMPT_IMPORT_FAILED";
48
+ const message = error?.message || "Prompt import failed";
49
+ res.status(status).json({ error: { code, message } });
50
+ }
51
+
52
+ function generateId() {
53
+ return `${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
54
+ }
55
+
56
+ function sourceFilename(source) {
57
+ if (source.kind === "local") return source.filename;
58
+ return source.path.split("/").pop();
59
+ }
60
+
61
+ function normalizeLocalSource(source) {
62
+ const filename = typeof source?.filename === "string" ? source.filename.trim() : "";
63
+ const text = typeof source?.text === "string" ? source.text : "";
64
+ if (!filename || !isSupportedPromptFileName(filename)) {
65
+ throw promptImportError("UNSUPPORTED_EXTENSION", "Only .md, .markdown, and .txt files are supported");
66
+ }
67
+ if (!text.trim()) {
68
+ throw promptImportError("PROMPT_IMPORT_EMPTY", "Prompt source is empty", 422);
69
+ }
70
+ return {
71
+ kind: "local",
72
+ filename,
73
+ extension: filename.split(".").pop().toLowerCase(),
74
+ text,
75
+ tags: [`file:${filename}`, `ext:${filename.split(".").pop().toLowerCase()}`],
76
+ };
77
+ }
78
+
79
+ async function buildPreview(req, ctx) {
80
+ const body = req.body || {};
81
+ const rawSource = body.source || body;
82
+ const kind = rawSource.kind === "github" ? "github" : "local";
83
+ const limits = promptImportLimits(ctx);
84
+ let source;
85
+ let text;
86
+
87
+ if (kind === "github") {
88
+ source = normalizeGitHubSource(rawSource.input);
89
+ text = await fetchGitHubSourceText(source, limits);
90
+ } else {
91
+ source = normalizeLocalSource(rawSource);
92
+ text = source.text;
93
+ }
94
+
95
+ if (text.length > limits.maxSourceCharsScanned) {
96
+ text = text.slice(0, limits.maxSourceCharsScanned);
97
+ }
98
+
99
+ const candidates = parsePromptCandidates({
100
+ text,
101
+ filename: sourceFilename(source),
102
+ source: {
103
+ kind: source.kind,
104
+ owner: source.owner,
105
+ repo: source.repo,
106
+ ref: source.ref,
107
+ path: source.path,
108
+ htmlUrl: source.htmlUrl,
109
+ filename: source.filename,
110
+ },
111
+ tags: source.tags,
112
+ limits,
113
+ });
114
+
115
+ if (candidates.length === 0) {
116
+ throw promptImportError("PROMPT_IMPORT_EMPTY", "No prompt candidates were found", 422);
117
+ }
118
+ return { source, candidates, warnings: [] };
119
+ }
120
+
121
+ function normalizeFolderInput(body) {
122
+ const input = typeof body?.source?.input === "string" ? body.source.input : body?.input;
123
+ return normalizeGitHubFolderSource(input);
124
+ }
125
+
126
+ async function buildFolderFiles(req, ctx) {
127
+ const limits = promptImportLimits(ctx);
128
+ const source = normalizeFolderInput(req.body || {});
129
+ return fetchGitHubFolderFiles(source, limits);
130
+ }
131
+
132
+ async function buildFolderPreview(req, ctx) {
133
+ const limits = promptImportLimits(ctx);
134
+ const source = normalizeFolderInput(req.body || {});
135
+ const paths = Array.isArray(req.body?.paths) ? req.body.paths : [];
136
+ const selected = await fetchSelectedGitHubFolderFiles(source, paths, limits);
137
+ const candidates = [];
138
+ const warnings = [...selected.warnings];
139
+
140
+ for (const file of selected.files) {
141
+ const text = file.text.length > limits.maxSourceCharsScanned
142
+ ? file.text.slice(0, limits.maxSourceCharsScanned)
143
+ : file.text;
144
+ const parsed = parsePromptCandidates({
145
+ text,
146
+ filename: file.path,
147
+ source: {
148
+ kind: "github",
149
+ owner: source.owner,
150
+ repo: source.repo,
151
+ ref: source.ref,
152
+ path: file.path,
153
+ htmlUrl: file.htmlUrl,
154
+ },
155
+ tags: [...source.tags, `file:${file.name}`, `ext:${file.extension}`],
156
+ limits,
157
+ });
158
+ if (parsed.length === 0) warnings.push(`${file.path}: no prompt candidates`);
159
+ candidates.push(...parsed);
160
+ }
161
+
162
+ if (candidates.length === 0) {
163
+ throw promptImportError("PROMPT_IMPORT_EMPTY", "No prompt candidates were found", 422);
164
+ }
165
+ return {
166
+ source,
167
+ files: selected.files.map(({ text, contentHash, ...file }) => file),
168
+ candidates: candidates.slice(0, limits.maxPromptCandidatesPerImport),
169
+ warnings,
170
+ };
171
+ }
172
+
173
+ function assertCommitCandidateText(text, limits) {
174
+ if (text.length < limits.minCandidateChars) {
175
+ throw promptImportError("PROMPT_IMPORT_EMPTY", "Prompt candidate is too short", 422);
176
+ }
177
+ if (text.length > limits.maxCandidateChars) {
178
+ throw promptImportError("PROMPT_IMPORT_TOO_MANY_CANDIDATES", "Prompt candidate is too large", 413);
179
+ }
180
+ }
181
+
182
+ function commitCandidates(candidates, folderId, limits) {
183
+ const db = getDb();
184
+ const result = { foldersCreated: 0, promptsImported: 0, duplicatesSkipped: 0 };
185
+ const now = Math.floor(Date.now() / 1000);
186
+ const targetFolder = typeof folderId === "string" && folderId ? folderId : "__root__";
187
+ const folderExists = db.prepare("SELECT 1 FROM prompt_folders WHERE id = ? LIMIT 1").get(targetFolder);
188
+ const resolvedFolderId = folderExists ? targetFolder : "__root__";
189
+
190
+ for (const candidate of candidates) {
191
+ if (!candidate?.text || typeof candidate.text !== "string") continue;
192
+ const text = candidate.text.trim();
193
+ if (!text) continue;
194
+ assertCommitCandidateText(text, limits);
195
+ const dup = db.prepare("SELECT 1 FROM prompts WHERE text = ? AND folder_id = ? LIMIT 1").get(text, resolvedFolderId);
196
+ if (dup) {
197
+ result.duplicatesSkipped++;
198
+ continue;
199
+ }
200
+ const tagsJson = Array.isArray(candidate.tags) ? JSON.stringify([...new Set(candidate.tags)]) : null;
201
+ db.prepare(
202
+ `INSERT INTO prompts (id, folder_id, name, text, tags, mode, is_favorite, created_at, updated_at)
203
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
204
+ ).run(
205
+ generateId(),
206
+ resolvedFolderId,
207
+ typeof candidate.name === "string" && candidate.name.trim() ? candidate.name.trim() : text.slice(0, 30),
208
+ text,
209
+ tagsJson,
210
+ candidate.mode === "direct" || candidate.mode === "auto" ? candidate.mode : null,
211
+ 0,
212
+ now,
213
+ now,
214
+ );
215
+ result.promptsImported++;
216
+ }
217
+ return result;
218
+ }
219
+
220
+ export function registerPromptImportRoutes(app, ctx) {
221
+ app.get("/api/prompts/import/curated-sources", async (req, res) => {
222
+ try {
223
+ res.json(await getPromptImportSources(ctx));
224
+ } catch (error) {
225
+ logError("promptImport", "curated_sources_error", error);
226
+ sendPromptImportError(res, error);
227
+ }
228
+ });
229
+
230
+ app.get("/api/prompts/import/discovery", async (req, res) => {
231
+ try {
232
+ const candidates = await listDiscoveryCandidates(ctx, {
233
+ status: typeof req.query?.status === "string" ? req.query.status : undefined,
234
+ });
235
+ res.json({ candidates, warnings: [] });
236
+ } catch (error) {
237
+ logError("promptImport", "discovery_list_error", error);
238
+ sendPromptImportError(res, error);
239
+ }
240
+ });
241
+
242
+ app.post("/api/prompts/import/discovery-search", async (req, res) => {
243
+ try {
244
+ const limits = promptImportLimits(ctx);
245
+ const seeds = Array.isArray(req.body?.seeds) ? req.body.seeds : [];
246
+ if (seeds.length > limits.discoveryMaxQueries) {
247
+ throw promptImportError("GITHUB_DISCOVERY_TOO_MANY_QUERIES", "Too many discovery queries", 413);
248
+ }
249
+ const result = await searchGitHubDiscovery(ctx, {
250
+ q: req.body?.q,
251
+ seeds,
252
+ limit: req.body?.limit,
253
+ maxQueries: limits.discoveryMaxQueries,
254
+ });
255
+ res.json(result);
256
+ } catch (error) {
257
+ logError("promptImport", "discovery_search_error", error);
258
+ sendPromptImportError(res, error);
259
+ }
260
+ });
261
+
262
+ app.post("/api/prompts/import/discovery-review", async (req, res) => {
263
+ try {
264
+ const result = await reviewDiscoveryCandidate(ctx, {
265
+ repo: req.body?.repo,
266
+ status: req.body?.status,
267
+ reviewNotes: req.body?.reviewNotes,
268
+ allowedPaths: req.body?.allowedPaths,
269
+ defaultSearch: req.body?.defaultSearch,
270
+ });
271
+ res.json(result);
272
+ } catch (error) {
273
+ logError("promptImport", "discovery_review_error", error);
274
+ sendPromptImportError(res, error);
275
+ }
276
+ });
277
+
278
+ app.post("/api/prompts/import/curated-search", async (req, res) => {
279
+ try {
280
+ const result = await searchCuratedPrompts(ctx, {
281
+ q: req.body?.q,
282
+ sourceIds: req.body?.sourceIds,
283
+ limit: req.body?.limit,
284
+ });
285
+ res.json(result);
286
+ } catch (error) {
287
+ logError("promptImport", "curated_search_error", error);
288
+ sendPromptImportError(res, error);
289
+ }
290
+ });
291
+
292
+ app.post("/api/prompts/import/curated-refresh", async (req, res) => {
293
+ try {
294
+ const sourceId = typeof req.body?.sourceId === "string" ? req.body.sourceId : "";
295
+ if (!sourceId) {
296
+ throw promptImportError("INVALID_GITHUB_SOURCE", "Curated source is required", 400);
297
+ }
298
+ const result = await refreshCuratedSource(ctx, sourceId);
299
+ res.json(result);
300
+ } catch (error) {
301
+ logError("promptImport", "curated_refresh_error", error);
302
+ sendPromptImportError(res, error);
303
+ }
304
+ });
305
+
306
+ app.post("/api/prompts/import/folder-files", async (req, res) => {
307
+ try {
308
+ const result = await buildFolderFiles(req, ctx);
309
+ res.json(result);
310
+ } catch (error) {
311
+ logError("promptImport", "folder_files_error", error);
312
+ sendPromptImportError(res, error);
313
+ }
314
+ });
315
+
316
+ app.post("/api/prompts/import/folder-preview", async (req, res) => {
317
+ try {
318
+ const result = await buildFolderPreview(req, ctx);
319
+ res.json(result);
320
+ } catch (error) {
321
+ logError("promptImport", "folder_preview_error", error);
322
+ sendPromptImportError(res, error);
323
+ }
324
+ });
325
+
326
+ app.post("/api/prompts/import/preview", async (req, res) => {
327
+ try {
328
+ const preview = await buildPreview(req, ctx);
329
+ res.json(preview);
330
+ } catch (error) {
331
+ logError("promptImport", "preview_error", error);
332
+ sendPromptImportError(res, error);
333
+ }
334
+ });
335
+
336
+ app.post("/api/prompts/import/commit", async (req, res) => {
337
+ try {
338
+ const limits = promptImportLimits(ctx);
339
+ const candidates = Array.isArray(req.body?.candidates) ? req.body.candidates : [];
340
+ if (candidates.length === 0) {
341
+ throw promptImportError("PROMPT_IMPORT_EMPTY", "Select at least one prompt to import", 422);
342
+ }
343
+ if (candidates.length > limits.maxPromptCandidatesPerImport) {
344
+ throw promptImportError("PROMPT_IMPORT_TOO_MANY_CANDIDATES", "Too many prompt candidates", 413);
345
+ }
346
+ const result = commitCandidates(candidates, req.body?.folderId, limits);
347
+ logEvent("promptImport", "committed", result);
348
+ res.json(result);
349
+ } catch (error) {
350
+ logError("promptImport", "commit_error", error);
351
+ sendPromptImportError(res, error);
352
+ }
353
+ });
354
+ }