ima2-gen 1.1.7 → 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.
@@ -29,6 +29,10 @@ function candidateId(text, ordinal) {
29
29
  return `candidate_${ordinal}_${createHash("sha256").update(text).digest("hex").slice(0, 10)}`;
30
30
  }
31
31
 
32
+ function promptHash(text) {
33
+ return createHash("sha256").update(normalizeWhitespace(text).toLowerCase()).digest("hex");
34
+ }
35
+
32
36
  function headingName(heading, fallback) {
33
37
  return heading?.replace(/^#+\s*/, "").trim() || fallback;
34
38
  }
@@ -52,13 +56,19 @@ function pushCandidate(candidates, rawText, options) {
52
56
  const text = normalizeWhitespace(rawText);
53
57
  if (!allowedCandidate(text, options.limits)) return;
54
58
  const ordinal = candidates.length + 1;
59
+ const hash = promptHash(text);
55
60
  candidates.push({
56
61
  id: candidateId(text, ordinal),
57
62
  name: options.name,
58
63
  text,
64
+ textPreview: text.slice(0, 220),
59
65
  tags: [...new Set(options.tags)],
60
66
  warnings: options.warnings ?? [],
61
67
  source: options.source,
68
+ headingPath: options.headingPath ?? null,
69
+ ordinal,
70
+ promptHash: hash,
71
+ scoreHints: options.scoreHints ?? {},
62
72
  });
63
73
  }
64
74
 
@@ -75,6 +85,7 @@ function parseMarkdown(text, options) {
75
85
  pushCandidate(options.candidates, match[2], {
76
86
  ...options,
77
87
  name: `${options.baseName} ${options.candidates.length + 1}`,
88
+ headingPath: options.headingPath ?? null,
78
89
  });
79
90
  if (options.candidates.length >= options.limits.maxPromptCandidatesPerFile) return;
80
91
  }
@@ -93,6 +104,7 @@ function parseMarkdown(text, options) {
93
104
  pushCandidate(options.candidates, body, {
94
105
  ...options,
95
106
  name: headingName(heading, `${options.baseName} ${options.candidates.length + 1}`),
107
+ headingPath: headingName(heading, options.baseName),
96
108
  });
97
109
  if (options.candidates.length >= options.limits.maxPromptCandidatesPerFile) return;
98
110
  }
@@ -116,6 +128,7 @@ function parsePlainText(text, options) {
116
128
  pushCandidate(options.candidates, clean, {
117
129
  ...options,
118
130
  name: `${options.baseName} ${options.candidates.length + 1}`,
131
+ headingPath: `${options.baseName} ${options.candidates.length + 1}`,
119
132
  });
120
133
  if (options.candidates.length >= options.limits.maxPromptCandidatesPerFile) return;
121
134
  }
@@ -0,0 +1,248 @@
1
+ import { createHash } from "node:crypto";
2
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
3
+ import { dirname } from "node:path";
4
+ import { getCuratedSource, getDefaultSearchSources, listCuratedSources } from "./curatedSources.js";
5
+ import { buildGitHubRawFileSource, fetchGitHubSource } from "./githubSource.js";
6
+ import { parsePromptCandidates } from "./parsePromptCandidates.js";
7
+ import { extractGptImageHints } from "./gptImageHints.js";
8
+ import { rankPromptCandidates } from "./rankPromptCandidates.js";
9
+ import {
10
+ getDefaultReviewedDiscoverySources,
11
+ getReviewedDiscoverySource,
12
+ listReviewedDiscoverySources,
13
+ } from "./discoveryRegistry.js";
14
+
15
+ const INDEX_VERSION = 1;
16
+ const EXTRACTOR_VERSION = 2;
17
+
18
+ function limitsFromCtx(ctx) {
19
+ return {
20
+ maxFileBytesForPreview: ctx.config.limits.promptImportMaxFileBytes,
21
+ maxPromptCandidatesPerFile: ctx.config.limits.promptImportMaxCandidatesPerFile,
22
+ maxPromptCandidatesPerImport: ctx.config.limits.promptImportMaxCandidatesPerImport,
23
+ fetchTimeoutMs: ctx.config.limits.promptImportFetchTimeoutMs,
24
+ maxCandidateChars: ctx.config.limits.promptImportMaxCandidateChars,
25
+ minCandidateChars: ctx.config.limits.promptImportMinCandidateChars,
26
+ maxSourceCharsScanned: ctx.config.limits.promptImportMaxSourceCharsScanned,
27
+ maxRepoIndexFiles: ctx.config.limits.promptImportMaxRepoIndexFiles,
28
+ searchLimit: ctx.config.limits.promptImportCuratedSearchLimit,
29
+ ttlMs: ctx.config.limits.promptImportIndexCacheTtlMs,
30
+ };
31
+ }
32
+
33
+ function cacheFile(ctx) {
34
+ return ctx.config.storage.promptImportIndexCacheFile;
35
+ }
36
+
37
+ function sourceFileId(source, path) {
38
+ return `github:${source.repo}@${source.defaultRef}:${path}`;
39
+ }
40
+
41
+ function hashId(...parts) {
42
+ return createHash("sha256").update(parts.join("\0")).digest("hex");
43
+ }
44
+
45
+ async function readCache(ctx) {
46
+ try {
47
+ const parsed = JSON.parse(await readFile(cacheFile(ctx), "utf8"));
48
+ if (parsed.version !== INDEX_VERSION) return { version: INDEX_VERSION, sources: {} };
49
+ return { version: INDEX_VERSION, sources: parsed.sources || {} };
50
+ } catch {
51
+ return { version: INDEX_VERSION, sources: {} };
52
+ }
53
+ }
54
+
55
+ async function writeCache(ctx, cache) {
56
+ const file = cacheFile(ctx);
57
+ await mkdir(dirname(file), { recursive: true });
58
+ const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
59
+ await writeFile(tmp, JSON.stringify(cache, null, 2));
60
+ await rename(tmp, file);
61
+ }
62
+
63
+ function sourceTags(source, fileSource) {
64
+ return [
65
+ ...fileSource.tags,
66
+ `source:${source.id}`,
67
+ `license:${source.licenseSpdx}`,
68
+ `trust:${source.trustTier}`,
69
+ source.requiresAttribution ? "attribution-required" : null,
70
+ ].filter(Boolean);
71
+ }
72
+
73
+ function indexedCandidate({ candidate, source, fileSource, fileIndex, index }) {
74
+ const scoreHints = extractGptImageHints(candidate.text);
75
+ const headingPath = candidate.headingPath || candidate.name || "";
76
+ const candidateId = hashId(fileIndex.sourceFileId, fileIndex.contentHash, headingPath, String(candidate.ordinal || index + 1));
77
+ const tags = [...new Set([...(candidate.tags || []), ...sourceTags(source, fileSource)])];
78
+ return {
79
+ ...candidate,
80
+ id: candidateId,
81
+ candidateId,
82
+ name: candidate.name,
83
+ text: candidate.text,
84
+ textPreview: candidate.textPreview || candidate.text.slice(0, 220),
85
+ tags,
86
+ warnings: [...new Set([...(candidate.warnings || []), ...scoreHints.warnings])],
87
+ source: {
88
+ kind: "github",
89
+ owner: source.owner,
90
+ repo: source.name,
91
+ ref: source.defaultRef,
92
+ path: fileSource.path,
93
+ htmlUrl: fileSource.htmlUrl,
94
+ sourceId: source.id,
95
+ },
96
+ sourceFileId: fileIndex.sourceFileId,
97
+ headingPath,
98
+ ordinal: candidate.ordinal || index + 1,
99
+ promptHash: candidate.promptHash || hashId(candidate.text.trim().toLowerCase()),
100
+ scoreHints,
101
+ };
102
+ }
103
+
104
+ async function indexSource(ctx, sourceId) {
105
+ const source = getCuratedSource(sourceId) || await getReviewedDiscoverySource(ctx, sourceId);
106
+ if (!source || source.trustTier === "manual-review") {
107
+ return { source, indexedFiles: 0, candidateCount: 0, warnings: ["curated-source-unavailable"] };
108
+ }
109
+ if (String(source.defaultRef || "").includes("/")) {
110
+ return { source, indexedFiles: 0, candidateCount: 0, warnings: ["discovery-default-branch-unsupported"] };
111
+ }
112
+ if (!Array.isArray(source.allowedPaths) || source.allowedPaths.length === 0) {
113
+ return { source, indexedFiles: 0, candidateCount: 0, warnings: ["discovery-requires-paths"] };
114
+ }
115
+
116
+ const limits = limitsFromCtx(ctx);
117
+ const warnings = [];
118
+ const files = [];
119
+ const candidates = [];
120
+ const allowedPaths = source.allowedPaths.slice(0, limits.maxRepoIndexFiles);
121
+
122
+ for (const path of allowedPaths) {
123
+ try {
124
+ const fileSource = buildGitHubRawFileSource({
125
+ owner: source.owner,
126
+ repo: source.name,
127
+ ref: source.defaultRef,
128
+ path,
129
+ });
130
+ const fetched = await fetchGitHubSource(fileSource, limits);
131
+ const fileIndex = {
132
+ sourceFileId: sourceFileId(source, path),
133
+ owner: source.owner,
134
+ repo: source.name,
135
+ ref: source.defaultRef,
136
+ path,
137
+ extension: fileSource.extension,
138
+ contentHash: fetched.contentHash,
139
+ etag: fetched.etag,
140
+ sizeBytes: fetched.sizeBytes,
141
+ licenseSpdx: source.licenseSpdx,
142
+ htmlUrl: fileSource.htmlUrl,
143
+ indexedAt: new Date().toISOString(),
144
+ lastFetchStatus: "ok",
145
+ promptCandidateCount: 0,
146
+ extractorVersion: EXTRACTOR_VERSION,
147
+ };
148
+ const parsed = parsePromptCandidates({
149
+ text: fetched.text,
150
+ filename: path,
151
+ source: { kind: "github", owner: source.owner, repo: source.name, ref: source.defaultRef, path, htmlUrl: fileSource.htmlUrl },
152
+ tags: sourceTags(source, fileSource),
153
+ limits,
154
+ });
155
+ const indexed = parsed.map((candidate, index) => indexedCandidate({ candidate, source, fileSource, fileIndex, index }));
156
+ fileIndex.promptCandidateCount = indexed.length;
157
+ files.push(fileIndex);
158
+ candidates.push(...indexed);
159
+ } catch (error) {
160
+ warnings.push(`${path}: ${error?.message || "index failed"}`);
161
+ }
162
+ }
163
+
164
+ return {
165
+ source,
166
+ indexedFiles: files.length,
167
+ candidateCount: candidates.length,
168
+ warnings,
169
+ entry: {
170
+ source,
171
+ files,
172
+ candidates,
173
+ refreshedAt: Date.now(),
174
+ },
175
+ };
176
+ }
177
+
178
+ function isFresh(entry, ttlMs) {
179
+ return entry?.refreshedAt && Date.now() - entry.refreshedAt < ttlMs;
180
+ }
181
+
182
+ async function ensureSearchCache(ctx) {
183
+ const cache = await readCache(ctx);
184
+ const limits = limitsFromCtx(ctx);
185
+ const sources = [
186
+ ...getDefaultSearchSources(),
187
+ ...await getDefaultReviewedDiscoverySources(ctx),
188
+ ];
189
+ let changed = false;
190
+ const warnings = [];
191
+
192
+ for (const source of sources) {
193
+ if (isFresh(cache.sources[source.id], limits.ttlMs)) continue;
194
+ const result = await indexSource(ctx, source.id);
195
+ if (result.entry) {
196
+ cache.sources[source.id] = result.entry;
197
+ changed = true;
198
+ }
199
+ warnings.push(...result.warnings);
200
+ }
201
+ if (changed) await writeCache(ctx, cache);
202
+ return { cache, warnings };
203
+ }
204
+
205
+ export async function refreshCuratedSource(ctx, sourceId) {
206
+ const cache = await readCache(ctx);
207
+ const result = await indexSource(ctx, sourceId);
208
+ if (result.entry) {
209
+ cache.sources[sourceId] = result.entry;
210
+ await writeCache(ctx, cache);
211
+ }
212
+ return {
213
+ source: result.source,
214
+ indexedFiles: result.indexedFiles,
215
+ candidateCount: result.candidateCount,
216
+ warnings: result.warnings,
217
+ };
218
+ }
219
+
220
+ export async function searchCuratedPrompts(ctx, { q = "", sourceIds, limit } = {}) {
221
+ const { cache, warnings } = await ensureSearchCache(ctx);
222
+ const limits = limitsFromCtx(ctx);
223
+ const defaultSources = [
224
+ ...getDefaultSearchSources(),
225
+ ...await getDefaultReviewedDiscoverySources(ctx),
226
+ ];
227
+ const allowedIds = Array.isArray(sourceIds) && sourceIds.length
228
+ ? new Set(sourceIds)
229
+ : new Set(defaultSources.map((source) => source.id));
230
+ const candidates = Object.values(cache.sources)
231
+ .filter((entry) => allowedIds.has(entry.source.id))
232
+ .flatMap((entry) => entry.candidates || []);
233
+ const results = rankPromptCandidates({
234
+ candidates,
235
+ query: q,
236
+ limit: Math.min(Number(limit) || limits.searchLimit, limits.searchLimit),
237
+ });
238
+ const sources = [
239
+ ...listCuratedSources(),
240
+ ...await listReviewedDiscoverySources(ctx),
241
+ ];
242
+ return { results, sources, warnings };
243
+ }
244
+
245
+ export async function getPromptImportSources(ctx) {
246
+ const reviewed = ctx ? await listReviewedDiscoverySources(ctx) : [];
247
+ return { sources: [...listCuratedSources(), ...reviewed] };
248
+ }
@@ -0,0 +1,49 @@
1
+ function tokenize(value) {
2
+ return String(value || "")
3
+ .toLowerCase()
4
+ .split(/[^a-z0-9가-힣-]+/i)
5
+ .map((token) => token.trim())
6
+ .filter(Boolean);
7
+ }
8
+
9
+ function includesAny(values, terms) {
10
+ const haystack = values.map((value) => String(value || "").toLowerCase());
11
+ return terms.some((term) => haystack.some((value) => value.includes(term)));
12
+ }
13
+
14
+ export function rankPromptCandidates({ candidates, query, limit }) {
15
+ const terms = tokenize(query);
16
+ const boundedLimit = Math.max(1, Math.min(Number(limit) || 50, 100));
17
+ const ranked = candidates.map((candidate) => {
18
+ const text = String(candidate.text || "").toLowerCase();
19
+ const name = String(candidate.name || "").toLowerCase();
20
+ const tags = Array.isArray(candidate.tags) ? candidate.tags : [];
21
+ const hints = candidate.scoreHints || {};
22
+ const hintValues = [
23
+ ...(hints.modelHints || []),
24
+ ...(hints.generationSurfaceHints || []),
25
+ ...(hints.taskHints || []),
26
+ ...(hints.sizeHints || []),
27
+ ...(hints.qualityHints || []),
28
+ ];
29
+ let score = 0;
30
+
31
+ if (terms.length === 0) score += 1;
32
+ for (const term of terms) {
33
+ if (name.includes(term)) score += 18;
34
+ if (includesAny(tags, [term])) score += 12;
35
+ if (includesAny(hintValues, [term])) score += 14;
36
+ if (text.includes(term)) score += 5;
37
+ if (String(candidate.sourceFileId || "").toLowerCase().includes(term)) score += 4;
38
+ }
39
+ if (tags.some((tag) => tag === "trust:curated")) score += 5;
40
+ if (candidate.warnings?.length) score -= candidate.warnings.length * 3;
41
+
42
+ return { ...candidate, score };
43
+ });
44
+
45
+ return ranked
46
+ .filter((candidate) => terms.length === 0 || candidate.score > 0)
47
+ .sort((a, b) => b.score - a.score || String(a.name).localeCompare(String(b.name)))
48
+ .slice(0, boundedLimit);
49
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ima2-gen",
3
- "version": "1.1.7",
3
+ "version": "1.1.8",
4
4
  "description": "Local OAuth image generation studio with classic and node workflows",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
@@ -13,6 +13,7 @@ import { registerPromptImportRoutes } from "./promptImport.js";
13
13
  import { registerAnnotationRoutes } from "./annotations.js";
14
14
  import { registerCanvasVersionRoutes } from "./canvasVersions.js";
15
15
  import { registerComfyRoutes } from "./comfy.js";
16
+ import { registerImageImportRoutes } from "./imageImport.js";
16
17
 
17
18
  export function configureRoutes(app, ctx) {
18
19
  registerHealthRoutes(app, ctx);
@@ -21,6 +22,7 @@ export function configureRoutes(app, ctx) {
21
22
  registerHistoryRoutes(app, ctx);
22
23
  registerAnnotationRoutes(app, ctx);
23
24
  registerCanvasVersionRoutes(app, ctx);
25
+ registerImageImportRoutes(app, ctx);
24
26
  registerComfyRoutes(app, ctx);
25
27
  registerSessionRoutes(app, ctx);
26
28
  registerEditRoutes(app, ctx);
@@ -6,7 +6,22 @@ import {
6
6
  isSupportedPromptFileName,
7
7
  normalizeGitHubSource,
8
8
  } from "../lib/promptImport/githubSource.js";
9
+ import {
10
+ fetchGitHubFolderFiles,
11
+ fetchSelectedGitHubFolderFiles,
12
+ normalizeGitHubFolderSource,
13
+ } from "../lib/promptImport/githubFolder.js";
9
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";
10
25
 
11
26
  function promptImportLimits(ctx) {
12
27
  return {
@@ -17,6 +32,13 @@ function promptImportLimits(ctx) {
17
32
  maxCandidateChars: ctx.config.limits.promptImportMaxCandidateChars,
18
33
  minCandidateChars: ctx.config.limits.promptImportMinCandidateChars,
19
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,
20
42
  };
21
43
  }
22
44
 
@@ -96,6 +118,58 @@ async function buildPreview(req, ctx) {
96
118
  return { source, candidates, warnings: [] };
97
119
  }
98
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
+
99
173
  function assertCommitCandidateText(text, limits) {
100
174
  if (text.length < limits.minCandidateChars) {
101
175
  throw promptImportError("PROMPT_IMPORT_EMPTY", "Prompt candidate is too short", 422);
@@ -144,6 +218,111 @@ function commitCandidates(candidates, folderId, limits) {
144
218
  }
145
219
 
146
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
+
147
326
  app.post("/api/prompts/import/preview", async (req, res) => {
148
327
  try {
149
328
  const preview = await buildPreview(req, ctx);