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.
- package/README.md +56 -27
- package/bin/commands/annotate.js +137 -0
- package/bin/commands/annotate.ts +118 -0
- package/bin/commands/cancel.js +37 -33
- package/bin/commands/cancel.ts +45 -0
- package/bin/commands/canvas-versions.js +91 -0
- package/bin/commands/canvas-versions.ts +80 -0
- package/bin/commands/cardnews.js +293 -0
- package/bin/commands/cardnews.ts +248 -0
- package/bin/commands/comfy.js +63 -0
- package/bin/commands/comfy.ts +54 -0
- package/bin/commands/config.js +270 -0
- package/bin/commands/config.ts +265 -0
- package/bin/commands/edit.js +97 -72
- package/bin/commands/edit.ts +116 -0
- package/bin/commands/gen.js +140 -118
- package/bin/commands/gen.ts +176 -0
- package/bin/commands/history.js +164 -0
- package/bin/commands/history.ts +145 -0
- package/bin/commands/ls.js +60 -42
- package/bin/commands/ls.ts +60 -0
- package/bin/commands/metadata.js +45 -0
- package/bin/commands/metadata.ts +36 -0
- package/bin/commands/multimode.js +159 -0
- package/bin/commands/multimode.ts +146 -0
- package/bin/commands/node.js +176 -0
- package/bin/commands/node.ts +157 -0
- package/bin/commands/observability.js +201 -0
- package/bin/commands/observability.ts +176 -0
- package/bin/commands/ping.js +26 -20
- package/bin/commands/ping.ts +29 -0
- package/bin/commands/prompt.js +506 -0
- package/bin/commands/prompt.ts +421 -0
- package/bin/commands/ps.js +78 -71
- package/bin/commands/ps.ts +78 -0
- package/bin/commands/session.js +308 -0
- package/bin/commands/session.ts +265 -0
- package/bin/commands/show.js +75 -40
- package/bin/commands/show.ts +69 -0
- package/bin/ima2.js +324 -310
- package/bin/ima2.ts +444 -0
- package/bin/lib/args.js +75 -66
- package/bin/lib/args.ts +73 -0
- package/bin/lib/browser-id.js +15 -0
- package/bin/lib/browser-id.ts +16 -0
- package/bin/lib/client.js +91 -83
- package/bin/lib/client.ts +109 -0
- package/bin/lib/error-hints.js +14 -17
- package/bin/lib/error-hints.ts +23 -0
- package/bin/lib/files.js +26 -28
- package/bin/lib/files.ts +39 -0
- package/bin/lib/output.js +44 -42
- package/bin/lib/output.ts +58 -0
- package/bin/lib/platform.js +60 -56
- package/bin/lib/platform.ts +97 -0
- package/bin/lib/sse.js +73 -0
- package/bin/lib/sse.ts +73 -0
- package/bin/lib/star-prompt.js +69 -76
- package/bin/lib/star-prompt.ts +97 -0
- package/bin/lib/storage-doctor.js +34 -35
- package/bin/lib/storage-doctor.ts +38 -0
- package/config.js +147 -190
- package/config.ts +331 -0
- package/docs/API.md +48 -8
- package/docs/CLI.md +190 -0
- package/docs/FAQ.ko.md +5 -5
- package/docs/FAQ.md +5 -5
- package/docs/README.ja.md +71 -25
- package/docs/README.ko.md +61 -24
- package/docs/README.zh-CN.md +73 -27
- package/lib/assetLifecycle.js +130 -130
- package/lib/assetLifecycle.ts +142 -0
- package/lib/canvasVersionStore.js +135 -153
- package/lib/canvasVersionStore.ts +181 -0
- package/lib/cardNewsGenerator.js +127 -142
- package/lib/cardNewsGenerator.ts +162 -0
- package/lib/cardNewsJobStore.js +78 -84
- package/lib/cardNewsJobStore.ts +107 -0
- package/lib/cardNewsManifestStore.js +88 -93
- package/lib/cardNewsManifestStore.ts +112 -0
- package/lib/cardNewsPlanner.js +157 -152
- package/lib/cardNewsPlanner.ts +180 -0
- package/lib/cardNewsPlannerClient.js +101 -98
- package/lib/cardNewsPlannerClient.ts +114 -0
- package/lib/cardNewsPlannerPrompt.js +56 -56
- package/lib/cardNewsPlannerPrompt.ts +60 -0
- package/lib/cardNewsPlannerSchema.js +231 -223
- package/lib/cardNewsPlannerSchema.ts +259 -0
- package/lib/cardNewsRoleTemplateStore.js +39 -41
- package/lib/cardNewsRoleTemplateStore.ts +47 -0
- package/lib/cardNewsTemplateStore.js +171 -175
- package/lib/cardNewsTemplateStore.ts +210 -0
- package/lib/codexDetect.js +44 -47
- package/lib/codexDetect.ts +69 -0
- package/lib/comfyBridge.js +164 -184
- package/lib/comfyBridge.ts +214 -0
- package/lib/db.js +41 -51
- package/lib/db.ts +166 -0
- package/lib/errorClassify.js +62 -78
- package/lib/errorClassify.ts +100 -0
- package/lib/generationErrors.js +140 -103
- package/lib/generationErrors.ts +125 -0
- package/lib/historyList.js +149 -147
- package/lib/historyList.ts +164 -0
- package/lib/imageMetadata.js +86 -89
- package/lib/imageMetadata.ts +111 -0
- package/lib/imageMetadataStore.js +46 -51
- package/lib/imageMetadataStore.ts +67 -0
- package/lib/imageModels.js +38 -45
- package/lib/imageModels.ts +52 -0
- package/lib/inflight.js +131 -150
- package/lib/inflight.ts +204 -0
- package/lib/localImportStore.js +105 -0
- package/lib/localImportStore.ts +111 -0
- package/lib/logger.js +105 -112
- package/lib/logger.ts +150 -0
- package/lib/nodeStore.js +65 -64
- package/lib/nodeStore.ts +81 -0
- package/lib/oauthLauncher.js +61 -59
- package/lib/oauthLauncher.ts +64 -0
- package/lib/oauthNormalize.js +15 -19
- package/lib/oauthNormalize.ts +30 -0
- package/lib/oauthProxy.js +834 -832
- package/lib/oauthProxy.ts +995 -0
- package/lib/openDirectory.js +41 -40
- package/lib/openDirectory.ts +45 -0
- package/lib/pngInfo.js +18 -20
- package/lib/pngInfo.ts +26 -0
- package/lib/promptImport/curatedSources.js +135 -0
- package/lib/promptImport/curatedSources.ts +139 -0
- package/lib/promptImport/discoveryRegistry.js +218 -0
- package/lib/promptImport/discoveryRegistry.ts +236 -0
- package/lib/promptImport/errors.js +10 -10
- package/lib/promptImport/errors.ts +18 -0
- package/lib/promptImport/githubDiscovery.js +238 -0
- package/lib/promptImport/githubDiscovery.ts +248 -0
- package/lib/promptImport/githubFolder.js +302 -0
- package/lib/promptImport/githubFolder.ts +308 -0
- package/lib/promptImport/githubSource.js +194 -171
- package/lib/promptImport/githubSource.ts +239 -0
- package/lib/promptImport/gptImageHints.js +61 -0
- package/lib/promptImport/gptImageHints.ts +68 -0
- package/lib/promptImport/parsePromptCandidates.js +110 -112
- package/lib/promptImport/parsePromptCandidates.ts +153 -0
- package/lib/promptImport/promptIndex.js +230 -0
- package/lib/promptImport/promptIndex.ts +248 -0
- package/lib/promptImport/rankPromptCandidates.js +52 -0
- package/lib/promptImport/rankPromptCandidates.ts +49 -0
- package/lib/providerOptions.js +31 -0
- package/lib/providerOptions.ts +41 -0
- package/lib/referenceImageCompress.js +51 -62
- package/lib/referenceImageCompress.ts +75 -0
- package/lib/refs.js +93 -81
- package/lib/refs.ts +117 -0
- package/lib/requestLogger.js +32 -38
- package/lib/requestLogger.ts +48 -0
- package/lib/responsesImageAdapter.js +351 -0
- package/lib/responsesImageAdapter.ts +352 -0
- package/lib/runtimePorts.js +71 -73
- package/lib/runtimePorts.ts +93 -0
- package/lib/sessionStore.js +179 -230
- package/lib/sessionStore.ts +272 -0
- package/lib/storageMigration.js +247 -245
- package/lib/storageMigration.ts +284 -0
- package/lib/styleSheet.js +86 -90
- package/lib/styleSheet.ts +128 -0
- package/lib/systemTrash.js +18 -0
- package/lib/systemTrash.ts +20 -0
- package/package.json +26 -10
- package/routes/annotations.js +76 -79
- package/routes/annotations.ts +95 -0
- package/routes/canvasVersions.js +50 -54
- package/routes/canvasVersions.ts +64 -0
- package/routes/cardNews.js +158 -171
- package/routes/cardNews.ts +183 -0
- package/routes/comfy.js +23 -31
- package/routes/comfy.ts +39 -0
- package/routes/edit.js +183 -214
- package/routes/edit.ts +230 -0
- package/routes/generate.js +269 -291
- package/routes/generate.ts +309 -0
- package/routes/health.js +102 -107
- package/routes/health.ts +114 -0
- package/routes/history.js +136 -144
- package/routes/history.ts +153 -0
- package/routes/imageImport.js +33 -0
- package/routes/imageImport.ts +33 -0
- package/routes/index.js +18 -16
- package/routes/index.ts +35 -0
- package/routes/metadata.js +60 -64
- package/routes/metadata.ts +71 -0
- package/routes/multimode.js +228 -263
- package/routes/multimode.ts +280 -0
- package/routes/nodes.js +378 -424
- package/routes/nodes.ts +455 -0
- package/routes/promptImport.js +291 -152
- package/routes/promptImport.ts +354 -0
- package/routes/prompts.js +333 -360
- package/routes/prompts.ts +379 -0
- package/routes/sessions.js +277 -285
- package/routes/sessions.ts +292 -0
- package/routes/storage.js +29 -31
- package/routes/storage.ts +39 -0
- package/server.js +189 -196
- package/server.ts +235 -0
- package/ui/dist/.vite/manifest.json +101 -0
- package/ui/dist/assets/CardNewsWorkspace-BJOCey7Z.js +2 -0
- package/ui/dist/assets/NodeCanvas-BZV40eAE.css +1 -0
- package/ui/dist/assets/NodeCanvas-C3dzYNsk.js +7 -0
- package/ui/dist/assets/PromptImportDialog-Dqu1VpUh.js +2 -0
- package/ui/dist/assets/PromptImportDiscoverySection-Dg8T9X0L.js +1 -0
- package/ui/dist/assets/PromptImportFolderSection-DBaqsFO4.js +1 -0
- package/ui/dist/assets/PromptLibraryPanel-p5QqR97M.js +2 -0
- package/ui/dist/assets/SettingsWorkspace-B5bSAZ6u.js +1 -0
- package/ui/dist/assets/index-C9cXwiWE.js +25 -0
- package/ui/dist/assets/index-CGMIkZXn.css +1 -0
- package/ui/dist/assets/index-Cvld7dUZ.js +1 -0
- package/ui/dist/index.html +6 -3
- package/assets/screenshot.png +0 -0
- package/assets/screenshots/classic-generate-light.png +0 -0
- package/assets/screenshots/node-graph-branching.png +0 -0
- package/assets/screenshots/settings-oauth-generation.png +0 -0
- package/assets/screenshots/settings-workspace.png +0 -0
- package/assets/screenshots/style-sheet-editor.png +0 -0
- package/integrations/comfyui/ima2_gen_bridge/__pycache__/__init__.cpython-313.pyc +0 -0
- package/integrations/comfyui/ima2_gen_bridge/__pycache__/nodes.cpython-313.pyc +0 -0
- package/ui/dist/assets/index-DARPdT4Q.css +0 -1
- package/ui/dist/assets/index-ht80GMq4.js +0 -31
- package/ui/dist/assets/index-ht80GMq4.js.map +0 -1
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { promptImportError } from "./errors.js";
|
|
3
|
+
import { upsertDiscoveryCandidates } from "./discoveryRegistry.js";
|
|
4
|
+
const GITHUB_API_HOST = "api.github.com";
|
|
5
|
+
const DEFAULT_DISCOVERY_SEEDS = [
|
|
6
|
+
"gpt-image-2 prompt",
|
|
7
|
+
"image generation prompts",
|
|
8
|
+
"nano banana prompts",
|
|
9
|
+
"product photography prompt",
|
|
10
|
+
"typography image prompt",
|
|
11
|
+
"reference image prompt",
|
|
12
|
+
];
|
|
13
|
+
function tokenize(value) {
|
|
14
|
+
return String(value || "")
|
|
15
|
+
.toLowerCase()
|
|
16
|
+
.split(/[^a-z0-9가-힣-]+/i)
|
|
17
|
+
.filter(Boolean);
|
|
18
|
+
}
|
|
19
|
+
function discoveryLimits(ctx) {
|
|
20
|
+
return {
|
|
21
|
+
limit: ctx.config.limits.promptImportDiscoverySearchLimit,
|
|
22
|
+
maxQueries: ctx.config.limits.promptImportDiscoveryMaxQueries,
|
|
23
|
+
fetchTimeoutMs: ctx.config.limits.promptImportFetchTimeoutMs,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function validateGitHubApiUrl(rawUrl) {
|
|
27
|
+
let url;
|
|
28
|
+
try {
|
|
29
|
+
url = new URL(rawUrl);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
throw promptImportError("GITHUB_DISCOVERY_FAILED", "GitHub discovery returned an invalid URL");
|
|
33
|
+
}
|
|
34
|
+
if (!["https:", "http:"].includes(url.protocol) || url.hostname !== GITHUB_API_HOST) {
|
|
35
|
+
throw promptImportError("GITHUB_DISCOVERY_FAILED", "GitHub discovery redirected to an unsupported host");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function rateLimitFromHeaders(headers) {
|
|
39
|
+
const remaining = Number(headers.get("x-ratelimit-remaining") || Number.NaN);
|
|
40
|
+
const reset = Number(headers.get("x-ratelimit-reset") || Number.NaN);
|
|
41
|
+
const limit = Number(headers.get("x-ratelimit-limit") || Number.NaN);
|
|
42
|
+
return {
|
|
43
|
+
limit: Number.isFinite(limit) ? limit : null,
|
|
44
|
+
remaining: Number.isFinite(remaining) ? remaining : null,
|
|
45
|
+
resetAt: Number.isFinite(reset) ? new Date(reset * 1000).toISOString() : null,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function searchHeaders(ctx) {
|
|
49
|
+
const headers = {
|
|
50
|
+
Accept: "application/vnd.github+json",
|
|
51
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
52
|
+
"User-Agent": "ima2-prompt-import-discovery",
|
|
53
|
+
};
|
|
54
|
+
const token = ctx.config.github?.token;
|
|
55
|
+
if (token)
|
|
56
|
+
headers.Authorization = `Bearer ${token}`;
|
|
57
|
+
return headers;
|
|
58
|
+
}
|
|
59
|
+
export function buildDiscoveryQueries({ q = "", seeds = [], limit = 5 } = {}) {
|
|
60
|
+
const raw = [q, ...seeds, ...DEFAULT_DISCOVERY_SEEDS]
|
|
61
|
+
.map((item) => String(item || "").trim())
|
|
62
|
+
.filter(Boolean);
|
|
63
|
+
const unique = [...new Set(raw)];
|
|
64
|
+
if (unique.length === 0) {
|
|
65
|
+
throw promptImportError("GITHUB_DISCOVERY_QUERY_EMPTY", "Discovery query is required", 422);
|
|
66
|
+
}
|
|
67
|
+
return unique.slice(0, Math.max(1, Number(limit) || 1));
|
|
68
|
+
}
|
|
69
|
+
export function scoreDiscoveryRepository(repo, context = {}) {
|
|
70
|
+
const terms = tokenize([context.query, ...(context.seeds || [])].join(" "));
|
|
71
|
+
const nameText = `${repo.full_name || ""} ${repo.description || ""} ${(repo.topics || []).join(" ")}`.toLowerCase();
|
|
72
|
+
const pushedAt = repo.pushed_at ? Date.parse(repo.pushed_at) : 0;
|
|
73
|
+
const daysSincePush = pushedAt ? (Date.now() - pushedAt) / 86_400_000 : Number.POSITIVE_INFINITY;
|
|
74
|
+
const scoreReasons = [];
|
|
75
|
+
const warnings = [];
|
|
76
|
+
let score = 0;
|
|
77
|
+
const stars = Number(repo.stargazers_count || 0);
|
|
78
|
+
if (stars > 1000) {
|
|
79
|
+
score += 20;
|
|
80
|
+
scoreReasons.push("popular-repo");
|
|
81
|
+
}
|
|
82
|
+
else if (stars > 100) {
|
|
83
|
+
score += 12;
|
|
84
|
+
scoreReasons.push("known-repo");
|
|
85
|
+
}
|
|
86
|
+
else if (stars > 10) {
|
|
87
|
+
score += 5;
|
|
88
|
+
scoreReasons.push("some-stars");
|
|
89
|
+
}
|
|
90
|
+
if (daysSincePush <= 365) {
|
|
91
|
+
score += 10;
|
|
92
|
+
scoreReasons.push("recently-updated");
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
score -= 6;
|
|
96
|
+
warnings.push("stale-repo");
|
|
97
|
+
}
|
|
98
|
+
if (repo.license?.spdx_id) {
|
|
99
|
+
score += 8;
|
|
100
|
+
scoreReasons.push("license-present");
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
score -= 8;
|
|
104
|
+
warnings.push("no-license");
|
|
105
|
+
}
|
|
106
|
+
if (repo.archived || repo.disabled) {
|
|
107
|
+
score -= 20;
|
|
108
|
+
warnings.push("archived-or-disabled");
|
|
109
|
+
}
|
|
110
|
+
if (repo.fork) {
|
|
111
|
+
score -= 4;
|
|
112
|
+
warnings.push("fork-source");
|
|
113
|
+
}
|
|
114
|
+
if (!repo.default_branch) {
|
|
115
|
+
score -= 10;
|
|
116
|
+
warnings.push("missing-default-branch");
|
|
117
|
+
}
|
|
118
|
+
else if (String(repo.default_branch).includes("/")) {
|
|
119
|
+
warnings.push("discovery-default-branch-unsupported");
|
|
120
|
+
}
|
|
121
|
+
const promptTerms = ["prompt", "image", "generation", "gpt-image", "nano", "typography", "reference"];
|
|
122
|
+
if (promptTerms.some((term) => nameText.includes(term))) {
|
|
123
|
+
score += 14;
|
|
124
|
+
scoreReasons.push("prompt-like");
|
|
125
|
+
}
|
|
126
|
+
for (const term of terms) {
|
|
127
|
+
if (term && nameText.includes(term))
|
|
128
|
+
score += 3;
|
|
129
|
+
}
|
|
130
|
+
if (!repo.html_url || !String(repo.html_url).startsWith("https://github.com/")) {
|
|
131
|
+
score -= 20;
|
|
132
|
+
warnings.push("non-github-url");
|
|
133
|
+
}
|
|
134
|
+
return { score, scoreReasons: [...new Set(scoreReasons)], warnings: [...new Set(warnings)] };
|
|
135
|
+
}
|
|
136
|
+
export function normalizeDiscoveryCandidate(repo, context = {}) {
|
|
137
|
+
const fullName = String(repo.full_name || "").trim();
|
|
138
|
+
const [owner, name] = fullName.split("/");
|
|
139
|
+
const scored = scoreDiscoveryRepository(repo, context);
|
|
140
|
+
return {
|
|
141
|
+
id: `github:${fullName}`,
|
|
142
|
+
repo: fullName,
|
|
143
|
+
owner,
|
|
144
|
+
name,
|
|
145
|
+
fullName,
|
|
146
|
+
htmlUrl: repo.html_url,
|
|
147
|
+
description: repo.description || "",
|
|
148
|
+
defaultBranch: repo.default_branch || "main",
|
|
149
|
+
stars: Number(repo.stargazers_count || 0),
|
|
150
|
+
forks: Number(repo.forks_count || 0),
|
|
151
|
+
openIssues: Number(repo.open_issues_count || 0),
|
|
152
|
+
updatedAt: repo.updated_at || null,
|
|
153
|
+
pushedAt: repo.pushed_at || null,
|
|
154
|
+
licenseSpdx: repo.license?.spdx_id || "NOASSERTION",
|
|
155
|
+
topics: Array.isArray(repo.topics) ? repo.topics : [],
|
|
156
|
+
language: repo.language || null,
|
|
157
|
+
score: scored.score,
|
|
158
|
+
scoreReasons: scored.scoreReasons,
|
|
159
|
+
warnings: scored.warnings,
|
|
160
|
+
status: "candidate",
|
|
161
|
+
query: context.query || "",
|
|
162
|
+
discoveredAt: new Date().toISOString(),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
export function discoveryCacheKey(input) {
|
|
166
|
+
return createHash("sha256").update(JSON.stringify(input)).digest("hex");
|
|
167
|
+
}
|
|
168
|
+
async function searchOneQuery(ctx, query, perPage) {
|
|
169
|
+
const controller = new AbortController();
|
|
170
|
+
const timer = setTimeout(() => controller.abort(), discoveryLimits(ctx).fetchTimeoutMs);
|
|
171
|
+
const url = new URL("https://api.github.com/search/repositories");
|
|
172
|
+
url.searchParams.set("q", `${query} in:name,description`);
|
|
173
|
+
url.searchParams.set("sort", "stars");
|
|
174
|
+
url.searchParams.set("order", "desc");
|
|
175
|
+
url.searchParams.set("per_page", String(perPage));
|
|
176
|
+
try {
|
|
177
|
+
const response = await fetch(url, {
|
|
178
|
+
headers: searchHeaders(ctx),
|
|
179
|
+
signal: controller.signal,
|
|
180
|
+
});
|
|
181
|
+
validateGitHubApiUrl(response.url);
|
|
182
|
+
const rateLimit = rateLimitFromHeaders(response.headers);
|
|
183
|
+
if (response.status === 403 || response.status === 429) {
|
|
184
|
+
throw promptImportError("GITHUB_RATE_LIMITED", "GitHub discovery rate limit reached", 429);
|
|
185
|
+
}
|
|
186
|
+
if (!response.ok) {
|
|
187
|
+
throw promptImportError("GITHUB_DISCOVERY_FAILED", `GitHub discovery failed with ${response.status}`, 422);
|
|
188
|
+
}
|
|
189
|
+
const data = await response.json();
|
|
190
|
+
const items = Array.isArray(data.items) ? data.items : [];
|
|
191
|
+
return { items, rateLimit };
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
if (error?.name === "AbortError") {
|
|
195
|
+
throw promptImportError("REMOTE_FETCH_TIMEOUT", "GitHub discovery timed out", 504);
|
|
196
|
+
}
|
|
197
|
+
throw error;
|
|
198
|
+
}
|
|
199
|
+
finally {
|
|
200
|
+
clearTimeout(timer);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
export async function searchGitHubDiscovery(ctx, options = {}) {
|
|
204
|
+
const limits = discoveryLimits(ctx);
|
|
205
|
+
const queryLimit = Math.min(Number(options.maxQueries) || limits.maxQueries, limits.maxQueries);
|
|
206
|
+
const queries = buildDiscoveryQueries({
|
|
207
|
+
q: options.q,
|
|
208
|
+
seeds: options.seeds,
|
|
209
|
+
limit: queryLimit,
|
|
210
|
+
});
|
|
211
|
+
const requestedLimit = Math.min(Number(options.limit) || limits.limit, limits.limit);
|
|
212
|
+
const perQuery = Math.max(1, Math.ceil(requestedLimit / queries.length));
|
|
213
|
+
const warnings = [];
|
|
214
|
+
let rateLimit = null;
|
|
215
|
+
const byRepo = new Map();
|
|
216
|
+
for (const query of queries) {
|
|
217
|
+
const result = await searchOneQuery(ctx, query, perQuery);
|
|
218
|
+
rateLimit = result.rateLimit;
|
|
219
|
+
for (const repo of result.items) {
|
|
220
|
+
const candidate = normalizeDiscoveryCandidate(repo, {
|
|
221
|
+
query,
|
|
222
|
+
seeds: options.seeds,
|
|
223
|
+
});
|
|
224
|
+
const existing = byRepo.get(candidate.fullName);
|
|
225
|
+
if (!existing || candidate.score > existing.score) {
|
|
226
|
+
byRepo.set(candidate.fullName, candidate);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
const candidates = [...byRepo.values()]
|
|
231
|
+
.filter((candidate) => candidate.fullName && candidate.htmlUrl?.startsWith("https://github.com/"))
|
|
232
|
+
.sort((a, b) => b.score - a.score || b.stars - a.stars)
|
|
233
|
+
.slice(0, requestedLimit);
|
|
234
|
+
await upsertDiscoveryCandidates(ctx, candidates);
|
|
235
|
+
if (rateLimit?.remaining === 0)
|
|
236
|
+
warnings.push("github-rate-limit-exhausted");
|
|
237
|
+
return { candidates, warnings, rateLimit, cacheKey: discoveryCacheKey({ queries, requestedLimit }) };
|
|
238
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { promptImportError } from "./errors.js";
|
|
3
|
+
import { upsertDiscoveryCandidates } from "./discoveryRegistry.js";
|
|
4
|
+
|
|
5
|
+
const GITHUB_API_HOST = "api.github.com";
|
|
6
|
+
const DEFAULT_DISCOVERY_SEEDS = [
|
|
7
|
+
"gpt-image-2 prompt",
|
|
8
|
+
"image generation prompts",
|
|
9
|
+
"nano banana prompts",
|
|
10
|
+
"product photography prompt",
|
|
11
|
+
"typography image prompt",
|
|
12
|
+
"reference image prompt",
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
function tokenize(value) {
|
|
16
|
+
return String(value || "")
|
|
17
|
+
.toLowerCase()
|
|
18
|
+
.split(/[^a-z0-9가-힣-]+/i)
|
|
19
|
+
.filter(Boolean);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function discoveryLimits(ctx) {
|
|
23
|
+
return {
|
|
24
|
+
limit: ctx.config.limits.promptImportDiscoverySearchLimit,
|
|
25
|
+
maxQueries: ctx.config.limits.promptImportDiscoveryMaxQueries,
|
|
26
|
+
fetchTimeoutMs: ctx.config.limits.promptImportFetchTimeoutMs,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function validateGitHubApiUrl(rawUrl) {
|
|
31
|
+
let url;
|
|
32
|
+
try {
|
|
33
|
+
url = new URL(rawUrl);
|
|
34
|
+
} catch {
|
|
35
|
+
throw promptImportError("GITHUB_DISCOVERY_FAILED", "GitHub discovery returned an invalid URL");
|
|
36
|
+
}
|
|
37
|
+
if (!["https:", "http:"].includes(url.protocol) || url.hostname !== GITHUB_API_HOST) {
|
|
38
|
+
throw promptImportError("GITHUB_DISCOVERY_FAILED", "GitHub discovery redirected to an unsupported host");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function rateLimitFromHeaders(headers) {
|
|
43
|
+
const remaining = Number(headers.get("x-ratelimit-remaining") || Number.NaN);
|
|
44
|
+
const reset = Number(headers.get("x-ratelimit-reset") || Number.NaN);
|
|
45
|
+
const limit = Number(headers.get("x-ratelimit-limit") || Number.NaN);
|
|
46
|
+
return {
|
|
47
|
+
limit: Number.isFinite(limit) ? limit : null,
|
|
48
|
+
remaining: Number.isFinite(remaining) ? remaining : null,
|
|
49
|
+
resetAt: Number.isFinite(reset) ? new Date(reset * 1000).toISOString() : null,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function searchHeaders(ctx) {
|
|
54
|
+
const headers: any = {
|
|
55
|
+
Accept: "application/vnd.github+json",
|
|
56
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
57
|
+
"User-Agent": "ima2-prompt-import-discovery",
|
|
58
|
+
};
|
|
59
|
+
const token = ctx.config.github?.token;
|
|
60
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
61
|
+
return headers;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function buildDiscoveryQueries({ q = "", seeds = [], limit = 5 }: any = {}) {
|
|
65
|
+
const raw = [q, ...seeds, ...DEFAULT_DISCOVERY_SEEDS]
|
|
66
|
+
.map((item) => String(item || "").trim())
|
|
67
|
+
.filter(Boolean);
|
|
68
|
+
const unique = [...new Set(raw)];
|
|
69
|
+
if (unique.length === 0) {
|
|
70
|
+
throw promptImportError("GITHUB_DISCOVERY_QUERY_EMPTY", "Discovery query is required", 422);
|
|
71
|
+
}
|
|
72
|
+
return unique.slice(0, Math.max(1, Number(limit) || 1));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function scoreDiscoveryRepository(repo, context: any = {}) {
|
|
76
|
+
const terms = tokenize([context.query, ...(context.seeds || [])].join(" "));
|
|
77
|
+
const nameText = `${repo.full_name || ""} ${repo.description || ""} ${(repo.topics || []).join(" ")}`.toLowerCase();
|
|
78
|
+
const pushedAt = repo.pushed_at ? Date.parse(repo.pushed_at) : 0;
|
|
79
|
+
const daysSincePush = pushedAt ? (Date.now() - pushedAt) / 86_400_000 : Number.POSITIVE_INFINITY;
|
|
80
|
+
const scoreReasons = [];
|
|
81
|
+
const warnings = [];
|
|
82
|
+
let score = 0;
|
|
83
|
+
|
|
84
|
+
const stars = Number(repo.stargazers_count || 0);
|
|
85
|
+
if (stars > 1000) {
|
|
86
|
+
score += 20;
|
|
87
|
+
scoreReasons.push("popular-repo");
|
|
88
|
+
} else if (stars > 100) {
|
|
89
|
+
score += 12;
|
|
90
|
+
scoreReasons.push("known-repo");
|
|
91
|
+
} else if (stars > 10) {
|
|
92
|
+
score += 5;
|
|
93
|
+
scoreReasons.push("some-stars");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (daysSincePush <= 365) {
|
|
97
|
+
score += 10;
|
|
98
|
+
scoreReasons.push("recently-updated");
|
|
99
|
+
} else {
|
|
100
|
+
score -= 6;
|
|
101
|
+
warnings.push("stale-repo");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (repo.license?.spdx_id) {
|
|
105
|
+
score += 8;
|
|
106
|
+
scoreReasons.push("license-present");
|
|
107
|
+
} else {
|
|
108
|
+
score -= 8;
|
|
109
|
+
warnings.push("no-license");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (repo.archived || repo.disabled) {
|
|
113
|
+
score -= 20;
|
|
114
|
+
warnings.push("archived-or-disabled");
|
|
115
|
+
}
|
|
116
|
+
if (repo.fork) {
|
|
117
|
+
score -= 4;
|
|
118
|
+
warnings.push("fork-source");
|
|
119
|
+
}
|
|
120
|
+
if (!repo.default_branch) {
|
|
121
|
+
score -= 10;
|
|
122
|
+
warnings.push("missing-default-branch");
|
|
123
|
+
} else if (String(repo.default_branch).includes("/")) {
|
|
124
|
+
warnings.push("discovery-default-branch-unsupported");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const promptTerms = ["prompt", "image", "generation", "gpt-image", "nano", "typography", "reference"];
|
|
128
|
+
if (promptTerms.some((term) => nameText.includes(term))) {
|
|
129
|
+
score += 14;
|
|
130
|
+
scoreReasons.push("prompt-like");
|
|
131
|
+
}
|
|
132
|
+
for (const term of terms) {
|
|
133
|
+
if (term && nameText.includes(term)) score += 3;
|
|
134
|
+
}
|
|
135
|
+
if (!repo.html_url || !String(repo.html_url).startsWith("https://github.com/")) {
|
|
136
|
+
score -= 20;
|
|
137
|
+
warnings.push("non-github-url");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return { score, scoreReasons: [...new Set(scoreReasons)], warnings: [...new Set(warnings)] };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function normalizeDiscoveryCandidate(repo, context: any = {}) {
|
|
144
|
+
const fullName = String(repo.full_name || "").trim();
|
|
145
|
+
const [owner, name] = fullName.split("/");
|
|
146
|
+
const scored = scoreDiscoveryRepository(repo, context);
|
|
147
|
+
return {
|
|
148
|
+
id: `github:${fullName}`,
|
|
149
|
+
repo: fullName,
|
|
150
|
+
owner,
|
|
151
|
+
name,
|
|
152
|
+
fullName,
|
|
153
|
+
htmlUrl: repo.html_url,
|
|
154
|
+
description: repo.description || "",
|
|
155
|
+
defaultBranch: repo.default_branch || "main",
|
|
156
|
+
stars: Number(repo.stargazers_count || 0),
|
|
157
|
+
forks: Number(repo.forks_count || 0),
|
|
158
|
+
openIssues: Number(repo.open_issues_count || 0),
|
|
159
|
+
updatedAt: repo.updated_at || null,
|
|
160
|
+
pushedAt: repo.pushed_at || null,
|
|
161
|
+
licenseSpdx: repo.license?.spdx_id || "NOASSERTION",
|
|
162
|
+
topics: Array.isArray(repo.topics) ? repo.topics : [],
|
|
163
|
+
language: repo.language || null,
|
|
164
|
+
score: scored.score,
|
|
165
|
+
scoreReasons: scored.scoreReasons,
|
|
166
|
+
warnings: scored.warnings,
|
|
167
|
+
status: "candidate",
|
|
168
|
+
query: context.query || "",
|
|
169
|
+
discoveredAt: new Date().toISOString(),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function discoveryCacheKey(input) {
|
|
174
|
+
return createHash("sha256").update(JSON.stringify(input)).digest("hex");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function searchOneQuery(ctx, query, perPage) {
|
|
178
|
+
const controller = new AbortController();
|
|
179
|
+
const timer = setTimeout(() => controller.abort(), discoveryLimits(ctx).fetchTimeoutMs);
|
|
180
|
+
const url = new URL("https://api.github.com/search/repositories");
|
|
181
|
+
url.searchParams.set("q", `${query} in:name,description`);
|
|
182
|
+
url.searchParams.set("sort", "stars");
|
|
183
|
+
url.searchParams.set("order", "desc");
|
|
184
|
+
url.searchParams.set("per_page", String(perPage));
|
|
185
|
+
try {
|
|
186
|
+
const response = await fetch(url, {
|
|
187
|
+
headers: searchHeaders(ctx),
|
|
188
|
+
signal: controller.signal,
|
|
189
|
+
});
|
|
190
|
+
validateGitHubApiUrl(response.url);
|
|
191
|
+
const rateLimit = rateLimitFromHeaders(response.headers);
|
|
192
|
+
if (response.status === 403 || response.status === 429) {
|
|
193
|
+
throw promptImportError("GITHUB_RATE_LIMITED", "GitHub discovery rate limit reached", 429);
|
|
194
|
+
}
|
|
195
|
+
if (!response.ok) {
|
|
196
|
+
throw promptImportError("GITHUB_DISCOVERY_FAILED", `GitHub discovery failed with ${response.status}`, 422);
|
|
197
|
+
}
|
|
198
|
+
const data: any = await response.json();
|
|
199
|
+
const items = Array.isArray(data.items) ? data.items : [];
|
|
200
|
+
return { items, rateLimit };
|
|
201
|
+
} catch (error) {
|
|
202
|
+
if (error?.name === "AbortError") {
|
|
203
|
+
throw promptImportError("REMOTE_FETCH_TIMEOUT", "GitHub discovery timed out", 504);
|
|
204
|
+
}
|
|
205
|
+
throw error;
|
|
206
|
+
} finally {
|
|
207
|
+
clearTimeout(timer);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export async function searchGitHubDiscovery(ctx, options: any = {}) {
|
|
212
|
+
const limits = discoveryLimits(ctx);
|
|
213
|
+
const queryLimit = Math.min(Number(options.maxQueries) || limits.maxQueries, limits.maxQueries);
|
|
214
|
+
const queries = buildDiscoveryQueries({
|
|
215
|
+
q: options.q,
|
|
216
|
+
seeds: options.seeds,
|
|
217
|
+
limit: queryLimit,
|
|
218
|
+
});
|
|
219
|
+
const requestedLimit = Math.min(Number(options.limit) || limits.limit, limits.limit);
|
|
220
|
+
const perQuery = Math.max(1, Math.ceil(requestedLimit / queries.length));
|
|
221
|
+
const warnings = [];
|
|
222
|
+
let rateLimit = null;
|
|
223
|
+
const byRepo = new Map();
|
|
224
|
+
|
|
225
|
+
for (const query of queries) {
|
|
226
|
+
const result = await searchOneQuery(ctx, query, perQuery);
|
|
227
|
+
rateLimit = result.rateLimit;
|
|
228
|
+
for (const repo of result.items) {
|
|
229
|
+
const candidate = normalizeDiscoveryCandidate(repo, {
|
|
230
|
+
query,
|
|
231
|
+
seeds: options.seeds,
|
|
232
|
+
});
|
|
233
|
+
const existing = byRepo.get(candidate.fullName);
|
|
234
|
+
if (!existing || candidate.score > existing.score) {
|
|
235
|
+
byRepo.set(candidate.fullName, candidate);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const candidates = [...byRepo.values()]
|
|
241
|
+
.filter((candidate) => candidate.fullName && candidate.htmlUrl?.startsWith("https://github.com/"))
|
|
242
|
+
.sort((a, b) => b.score - a.score || b.stars - a.stars)
|
|
243
|
+
.slice(0, requestedLimit);
|
|
244
|
+
|
|
245
|
+
await upsertDiscoveryCandidates(ctx, candidates);
|
|
246
|
+
if (rateLimit?.remaining === 0) warnings.push("github-rate-limit-exhausted");
|
|
247
|
+
return { candidates, warnings, rateLimit, cacheKey: discoveryCacheKey({ queries, requestedLimit }) };
|
|
248
|
+
}
|