minecodex 2.0.55 → 2.1.1

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 (65) hide show
  1. package/README.md +90 -26
  2. package/apps/dashboard/app.js +20 -14
  3. package/apps/dashboard/assets/chevron-left.svg +13 -0
  4. package/apps/dashboard/assets/chevron-right.svg +13 -0
  5. package/apps/dashboard/assets/feature-showcase.js +185 -0
  6. package/apps/dashboard/assets/pause.svg +14 -0
  7. package/apps/dashboard/assets/play.svg +13 -0
  8. package/apps/dashboard/assets/previews/composer-expand.jpg +0 -0
  9. package/apps/dashboard/assets/previews/composer-expand.mp4 +0 -0
  10. package/apps/dashboard/assets/previews/file-navigation.jpg +0 -0
  11. package/apps/dashboard/assets/previews/file-navigation.mp4 +0 -0
  12. package/apps/dashboard/assets/previews/files-auto-refresh.jpg +0 -0
  13. package/apps/dashboard/assets/previews/files-auto-refresh.mp4 +0 -0
  14. package/apps/dashboard/assets/previews/images.jpg +0 -0
  15. package/apps/dashboard/assets/previews/images.mp4 +0 -0
  16. package/apps/dashboard/assets/previews/manifest.json +67 -1
  17. package/apps/dashboard/assets/previews/markdown-reading.jpg +0 -0
  18. package/apps/dashboard/assets/previews/markdown-reading.mp4 +0 -0
  19. package/apps/dashboard/assets/previews/mermaid-diagrams.jpg +0 -0
  20. package/apps/dashboard/assets/previews/mermaid-diagrams.mp4 +0 -0
  21. package/apps/dashboard/assets/previews/model-slider.jpg +0 -0
  22. package/apps/dashboard/assets/previews/model-slider.mp4 +0 -0
  23. package/apps/dashboard/assets/previews/notes.jpg +0 -0
  24. package/apps/dashboard/assets/previews/notes.mp4 +0 -0
  25. package/apps/dashboard/assets/previews/quick-chat.jpg +0 -0
  26. package/apps/dashboard/assets/previews/quick-chat.mp4 +0 -0
  27. package/apps/dashboard/assets/previews/session-status.jpg +0 -0
  28. package/apps/dashboard/assets/previews/session-status.mp4 +0 -0
  29. package/apps/dashboard/assets/previews/sidebar-focus.jpg +0 -0
  30. package/apps/dashboard/assets/previews/sidebar-focus.mp4 +0 -0
  31. package/apps/dashboard/assets/previews/sites.jpg +0 -0
  32. package/apps/dashboard/assets/previews/sites.mp4 +0 -0
  33. package/apps/dashboard/assets/previews/usage-meter.jpg +0 -0
  34. package/apps/dashboard/assets/previews/usage-meter.mp4 +0 -0
  35. package/apps/dashboard/index.html +26 -2
  36. package/apps/dashboard/styles.css +58 -39
  37. package/assets/readme/hero-dark.svg +50 -0
  38. package/assets/readme/hero.svg +36 -79
  39. package/features/images/src/adapters/codex/rspack-modules.mjs +9 -3
  40. package/features/images/src/library-prompt-source.mjs +2 -2
  41. package/features/notes/codex-feature.json +1 -0
  42. package/features/quick-chat/src/adapters/codex/renderer.mjs +3 -1
  43. package/package.json +3 -3
  44. package/packages/cli/src/config.mjs +1 -2
  45. package/packages/cli/src/control-server.mjs +1 -1
  46. package/packages/plugin/assets/app.js +35 -35
  47. package/packages/runtime-host/src/codex-cdp.mjs +0 -7
  48. package/packages/runtime-host/src/codex-injection.mjs +50 -2114
  49. package/packages/runtime-host/src/codex-runtime.mjs +4 -203
  50. package/packages/runtime-host/src/feature-registry.mjs +10 -91
  51. package/packages/runtime-host/src/main.mjs +0 -1
  52. package/packages/ui/src/native/tokens.mjs +0 -28
  53. package/apps/dashboard/assets/feature-previews.js +0 -206
  54. package/features/images/src/adapters/codex/cloud-import.mjs +0 -52
  55. package/features/images/src/backend/cloud-library.mjs +0 -12
  56. package/features/images/src/chatgpt-image-source-page.mjs +0 -207
  57. package/features/images/src/chatgpt-image-source.mjs +0 -42
  58. package/features/images/src/http-server.mjs +0 -487
  59. package/features/images/src/main.mjs +0 -52
  60. package/features/images/src/save-as.mjs +0 -69
  61. package/features/images/web/app.js +0 -1780
  62. package/features/images/web/i18n.mjs +0 -132
  63. package/features/images/web/index.html +0 -187
  64. package/features/images/web/styles.css +0 -1114
  65. package/packages/runtime-host/src/host-actions.mjs +0 -221
@@ -1,487 +0,0 @@
1
- import { createReadStream } from "node:fs";
2
- import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
3
- import { createServer as createNodeServer } from "node:http";
4
- import { randomUUID } from "node:crypto";
5
- import path from "node:path";
6
- import { MAX_IMPORTED_IMAGE_BYTES } from "./image-library.mjs";
7
- import { saveImageAs } from "./save-as.mjs";
8
-
9
- const LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "::1", "[::1]"]);
10
- const CODEX_EMBED_ORIGIN = "app://-";
11
- const DEFAULT_PAGE_LIMIT = 36;
12
- const MAX_PAGE_LIMIT = 72;
13
- const MAX_PAGE_OFFSET = Number.MAX_SAFE_INTEGER;
14
- const MAX_IMPORT_BODY_BYTES = Math.ceil(MAX_IMPORTED_IMAGE_BYTES * 4 / 3) + 64 * 1024;
15
- const MAX_EDIT_AUXILIARY_BYTES = 8 * 1024 * 1024;
16
-
17
- const STATIC_FILES = new Map([
18
- ["/", ["index.html", "text/html; charset=utf-8"]],
19
- ["/app.js", ["app.js", "text/javascript; charset=utf-8"]],
20
- ["/i18n.mjs", ["i18n.mjs", "text/javascript; charset=utf-8"]],
21
- ["/styles.css", ["styles.css", "text/css; charset=utf-8"]],
22
- ]);
23
-
24
- function json(response, statusCode, body) {
25
- response.writeHead(statusCode, {
26
- "Content-Type": "application/json; charset=utf-8",
27
- "Cache-Control": "no-store",
28
- });
29
- response.end(JSON.stringify(body));
30
- }
31
-
32
- function publicImage(image, { includePrompt = false } = {}) {
33
- const result = {
34
- id: image.id,
35
- threadId: image.threadId,
36
- imageGenerationItemId: image.imageGenerationItemId,
37
- width: image.width,
38
- height: image.height,
39
- byteLength: image.byteLength,
40
- createdAt: image.createdAt,
41
- importedAt: image.importedAt,
42
- sourceKind: image.sourceKind,
43
- projectId: image.projectId,
44
- projectName: image.projectName,
45
- threadTitle: image.threadTitle,
46
- archived: image.archived,
47
- fileUrl: `/api/images/${image.id}/file`,
48
- downloadUrl: `/api/images/${image.id}/download`,
49
- sourceUrl: image.threadId.startsWith("chatgpt:") ? null : `codex://threads/${image.threadId}`,
50
- };
51
- if (includePrompt) { result.prompt = image.prompt; result.promptKind = image.promptKind; }
52
- return result;
53
- }
54
-
55
- function publicGroup(group) {
56
- return {
57
- threadId: group.threadId,
58
- sourceKind: group.sourceKind,
59
- projectId: group.projectId,
60
- projectName: group.projectName,
61
- threadTitle: group.threadTitle,
62
- archived: group.archived,
63
- imageCount: group.imageCount,
64
- createdAt: group.createdAt,
65
- updatedAt: group.updatedAt,
66
- previews: group.previews.map((image) => publicImage(image)),
67
- };
68
- }
69
-
70
- function formatHost(host) {
71
- return host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
72
- }
73
-
74
- function mutationError(message, code) {
75
- return Object.assign(new Error(message), { status: 403, code });
76
- }
77
-
78
- async function readRequestBody(request, maxBytes) {
79
- const declaredLength = Number(request.headers["content-length"] ?? 0);
80
- if (Number.isSafeInteger(declaredLength) && declaredLength > maxBytes) {
81
- throw Object.assign(new Error("Request body exceeds the Images import limit"), {
82
- status: 413,
83
- code: "BODY_TOO_LARGE",
84
- });
85
- }
86
- const chunks = [];
87
- let total = 0;
88
- for await (const chunk of request) {
89
- total += chunk.length;
90
- if (total > maxBytes) {
91
- throw Object.assign(new Error("Request body exceeds the Images import limit"), {
92
- status: 413,
93
- code: "BODY_TOO_LARGE",
94
- });
95
- }
96
- chunks.push(chunk);
97
- }
98
- return Buffer.concat(chunks, total);
99
- }
100
-
101
- async function readImportPayload(request) {
102
- if (!String(request.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) {
103
- throw Object.assign(new Error("Images import requires application/json"), {
104
- status: 415,
105
- code: "UNSUPPORTED_CONTENT_TYPE",
106
- });
107
- }
108
- let payload;
109
- try {
110
- payload = JSON.parse((await readRequestBody(request, MAX_IMPORT_BODY_BYTES)).toString("utf8"));
111
- } catch (error) {
112
- if (error.code === "BODY_TOO_LARGE") throw error;
113
- throw Object.assign(new Error("Images import body must be valid JSON"), {
114
- status: 400,
115
- code: "INVALID_JSON",
116
- });
117
- }
118
- if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
119
- throw Object.assign(new Error("Images import body must be an object"), {
120
- status: 400,
121
- code: "INVALID_BODY",
122
- });
123
- }
124
- if (typeof payload.bytesBase64 !== "string" || payload.bytesBase64.length > MAX_IMPORT_BODY_BYTES) {
125
- throw Object.assign(new Error("Images import requires bounded base64 image bytes"), {
126
- status: 400,
127
- code: "INVALID_IMAGE_BYTES",
128
- });
129
- }
130
- const bytes = Buffer.from(payload.bytesBase64, "base64");
131
- if (!bytes.length || bytes.toString("base64") !== payload.bytesBase64.replace(/\s/g, "")) {
132
- throw Object.assign(new Error("Images import image bytes are not valid base64"), {
133
- status: 400,
134
- code: "INVALID_IMAGE_BYTES",
135
- });
136
- }
137
- return { ...payload, bytes };
138
- }
139
-
140
- async function readEditDraftPayload(request) {
141
- if (!String(request.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) {
142
- throw Object.assign(new Error("Image edit draft requires application/json"), { status: 415, code: "UNSUPPORTED_CONTENT_TYPE" });
143
- }
144
- let payload;
145
- try {
146
- payload = JSON.parse((await readRequestBody(request, Math.ceil(MAX_EDIT_AUXILIARY_BYTES * 4 / 3) + 8 * 1024)).toString("utf8"));
147
- } catch (error) {
148
- if (error.code === "BODY_TOO_LARGE") throw error;
149
- throw Object.assign(new Error("Image edit draft must be valid JSON"), { status: 400, code: "INVALID_JSON" });
150
- }
151
- if (!payload || typeof payload !== "object" || Array.isArray(payload)
152
- || !/^[a-f0-9]{24}$/.test(payload.imageId ?? "")) {
153
- throw Object.assign(new Error("Image edit draft requires an image id"), { status: 400, code: "INVALID_EDIT_DRAFT" });
154
- }
155
- if (payload.auxiliaryPngBase64 === undefined) return { imageId: payload.imageId, auxiliary: null };
156
- if (typeof payload.auxiliaryPngBase64 !== "string") {
157
- throw Object.assign(new Error("Image edit auxiliary image must be base64"), { status: 400, code: "INVALID_EDIT_AUXILIARY" });
158
- }
159
- const auxiliary = Buffer.from(payload.auxiliaryPngBase64, "base64");
160
- if (!auxiliary.length || auxiliary.length > MAX_EDIT_AUXILIARY_BYTES
161
- || auxiliary.toString("base64") !== payload.auxiliaryPngBase64.replace(/\s/g, "")
162
- || auxiliary.readUInt32BE(0) !== 0x89504e47) {
163
- throw Object.assign(new Error("Image edit auxiliary image must be a bounded PNG"), { status: 400, code: "INVALID_EDIT_AUXILIARY" });
164
- }
165
- return { imageId: payload.imageId, auxiliary };
166
- }
167
-
168
- function contentTypeForPath(sourcePath) {
169
- const extension = path.extname(sourcePath).toLowerCase();
170
- return extension === ".jpg" || extension === ".jpeg"
171
- ? "image/jpeg"
172
- : extension === ".webp" ? "image/webp" : "image/png";
173
- }
174
-
175
- function downloadFilename(image, sourcePath) {
176
- const stem = String(image.imageGenerationItemId ?? "generated-image")
177
- .replace(/[^a-zA-Z0-9._-]/g, "_")
178
- .slice(0, 128) || "generated-image";
179
- return `${stem}${path.extname(sourcePath)}`;
180
- }
181
-
182
- function applyCodexEmbedCors(request, response) {
183
- if (request.headers.origin !== CODEX_EMBED_ORIGIN) return false;
184
- response.setHeader("Access-Control-Allow-Origin", CODEX_EMBED_ORIGIN);
185
- response.setHeader("Access-Control-Allow-Methods", "GET, HEAD, POST, OPTIONS");
186
- response.setHeader("Access-Control-Allow-Headers", "Content-Type");
187
- response.setHeader("Access-Control-Allow-Private-Network", "true");
188
- response.setHeader("Vary", "Origin");
189
- return true;
190
- }
191
-
192
- function parseIntegerQuery(searchParams, name, { defaultValue, min, max }) {
193
- const values = searchParams.getAll(name);
194
- if (values.length === 0) return defaultValue;
195
- const [raw] = values;
196
- if (values.length !== 1 || !/^(0|[1-9]\d*)$/.test(raw)) {
197
- throw Object.assign(new Error(`${name} must be a single integer between ${min} and ${max}`), {
198
- status: 400,
199
- code: "INVALID_QUERY",
200
- });
201
- }
202
- const value = Number(raw);
203
- if (!Number.isSafeInteger(value) || value < min || value > max) {
204
- throw Object.assign(new Error(`${name} must be between ${min} and ${max}`), {
205
- status: 400,
206
- code: "INVALID_QUERY",
207
- });
208
- }
209
- return value;
210
- }
211
-
212
- function paginationFor(url) {
213
- return {
214
- limit: parseIntegerQuery(url.searchParams, "limit", {
215
- defaultValue: DEFAULT_PAGE_LIMIT,
216
- min: 1,
217
- max: MAX_PAGE_LIMIT,
218
- }),
219
- offset: parseIntegerQuery(url.searchParams, "offset", {
220
- defaultValue: 0,
221
- min: 0,
222
- max: MAX_PAGE_OFFSET,
223
- }),
224
- };
225
- }
226
-
227
- function validateMutationOrigin(request, address, host) {
228
- const expectedOrigin = `http://${formatHost(host)}:${address.port}`;
229
- const expectedHost = new URL(expectedOrigin).host;
230
- if (request.headers.host !== expectedHost) {
231
- throw mutationError("Mutation requests are restricted to the exact bound loopback Host", "HOST_NOT_ALLOWED");
232
- }
233
-
234
- const origin = request.headers.origin;
235
- if (!origin) {
236
- throw mutationError("Mutation requests require the exact bound loopback Origin", "ORIGIN_NOT_ALLOWED");
237
- }
238
- if (origin === CODEX_EMBED_ORIGIN) return;
239
- let parsed;
240
- try {
241
- parsed = new URL(origin);
242
- } catch {
243
- throw mutationError("Request origin is invalid", "INVALID_ORIGIN");
244
- }
245
- if (parsed.origin !== expectedOrigin) {
246
- throw mutationError("Mutation requests are restricted to this exact local Images origin", "ORIGIN_NOT_ALLOWED");
247
- }
248
- }
249
-
250
- export async function createHttpServer({
251
- library,
252
- webDir,
253
- host = "127.0.0.1",
254
- port = 47831,
255
- instanceId = process.env.MINECODEX_FEATURE_INSTANCE_ID ?? "unconfigured",
256
- }) {
257
- if (!library) throw new Error("library is required");
258
- if (!LOOPBACK_HOSTS.has(host)) throw new Error("Images only supports loopback hosts");
259
- const editDrafts = new Map();
260
- const server = createNodeServer(async (request, response) => {
261
- try {
262
- applyCodexEmbedCors(request, response);
263
- const address = server.address();
264
- const boundOrigin = address && typeof address !== "string"
265
- ? `http://${formatHost(host)}:${address.port}`
266
- : `http://${formatHost(host)}:${port}`;
267
- const url = new URL(request.url ?? "/", boundOrigin);
268
-
269
- if (request.method === "OPTIONS") {
270
- response.writeHead(204, { "Cache-Control": "no-store" });
271
- response.end();
272
- return;
273
- }
274
-
275
- if (!["GET", "HEAD"].includes(request.method)) {
276
- validateMutationOrigin(request, address, host);
277
- }
278
-
279
- if (request.method === "GET" && (url.pathname === "/api/health" || url.pathname === "/health")) {
280
- json(response, 200, {
281
- ok: true,
282
- service: "images",
283
- protocolVersion: 1,
284
- instanceId,
285
- images: library.count(),
286
- });
287
- return;
288
- }
289
-
290
- if (request.method === "GET" && url.pathname === "/favicon.ico") {
291
- response.writeHead(204);
292
- response.end();
293
- return;
294
- }
295
-
296
- if (request.method === "GET" && url.pathname === "/api/images") {
297
- const { limit, offset } = paginationFor(url);
298
- const snapshot = parseIntegerQuery(url.searchParams, "snapshot", { defaultValue: library.snapshot(), min: 0, max: Number.MAX_SAFE_INTEGER });
299
- const source = url.searchParams.get("source") ?? "all";
300
- const threadId = url.searchParams.get("threadId") || null;
301
- const sort = url.searchParams.get("sort") === "asc" ? "asc" : "desc";
302
- const query = url.searchParams.get("q") ?? "";
303
- const images = library.list({
304
- snapshot,
305
- query,
306
- source,
307
- threadId,
308
- sort,
309
- limit,
310
- offset,
311
- }).map(publicImage);
312
- json(response, 200, {
313
- images,
314
- count: library.count({ query, source, threadId, snapshot }),
315
- snapshot,
316
- limit,
317
- offset,
318
- });
319
- return;
320
- }
321
-
322
- if (request.method === "GET" && url.pathname === "/api/image-facets") {
323
- json(response, 200, library.facets());
324
- return;
325
- }
326
-
327
- if (request.method === "GET" && url.pathname === "/api/image-groups") {
328
- const { limit, offset } = paginationFor(url);
329
- const snapshot = parseIntegerQuery(url.searchParams, "snapshot", { defaultValue: library.snapshot(), min: 0, max: Number.MAX_SAFE_INTEGER });
330
- const source = url.searchParams.get("source") ?? "all";
331
- const groups = library.groups({ source, limit, offset, snapshot }).map(publicGroup);
332
- json(response, 200, {
333
- groups,
334
- count: library.countGroups({ source, snapshot }),
335
- imageCount: library.count({ source, snapshot }),
336
- snapshot,
337
- limit,
338
- offset,
339
- });
340
- return;
341
- }
342
-
343
- if (request.method === "GET" && url.pathname === "/api/cloud-image-cache") {
344
- json(response, 200, { items: library.cloudCache() });
345
- return;
346
- }
347
-
348
- if (request.method === "POST" && url.pathname === "/api/rescan") {
349
- if (library.rescanStatus().inProgress) {
350
- json(response, 202, { imported: 0, total: library.count(), inProgress: true });
351
- return;
352
- }
353
- const imported = await library.rescan();
354
- json(response, 200, { imported, total: library.count(), inProgress: false });
355
- return;
356
- }
357
-
358
- if (request.method === "POST" && url.pathname === "/api/import-generated-image") {
359
- const payload = await readImportPayload(request);
360
- const result = await library.importExternalImage({ ...payload, bytes: payload.bytes });
361
- if (!result.imported && result.reason !== "DUPLICATE") {
362
- json(response, 400, { ...result, imported: false });
363
- return;
364
- }
365
- json(response, result.reason === "DUPLICATE" ? 200 : 201, result);
366
- return;
367
- }
368
-
369
- if (request.method === "POST" && url.pathname === "/api/image-edit-draft") {
370
- const payload = await readEditDraftPayload(request);
371
- const image = library.get(payload.imageId);
372
- if (!image) {
373
- json(response, 404, { error: "Image not found" });
374
- return;
375
- }
376
- let auxiliaryPath = null;
377
- if (payload.auxiliary) {
378
- const editDir = path.join(library.dataDir, "edit-drafts");
379
- await mkdir(editDir, { recursive: true });
380
- auxiliaryPath = path.join(editDir, `${randomUUID()}.png`);
381
- await writeFile(auxiliaryPath, payload.auxiliary, { mode: 0o600 });
382
- }
383
- const draftId = randomUUID();
384
- editDrafts.set(draftId, { sourcePath: image.sourcePath, auxiliaryPath });
385
- json(response, 201, { draftId });
386
- return;
387
- }
388
-
389
- const editDraftMatch = url.pathname.match(/^\/api\/image-edit-draft\/([a-f0-9-]{36})$/);
390
- if (request.method === "GET" && editDraftMatch) {
391
- if (request.headers.origin !== CODEX_EMBED_ORIGIN) {
392
- throw mutationError("Image edit attachments are available only to RuntimeHost", "ORIGIN_NOT_ALLOWED");
393
- }
394
- const draft = editDrafts.get(editDraftMatch[1]);
395
- if (!draft) {
396
- json(response, 404, { error: "Image edit draft not found" });
397
- return;
398
- }
399
- editDrafts.delete(editDraftMatch[1]);
400
- json(response, 200, draft);
401
- return;
402
- }
403
-
404
- const saveAsMatch = url.pathname.match(/^\/api\/images\/([a-f0-9]{24})\/save-as$/);
405
- if (request.method === "POST" && saveAsMatch) {
406
- const image = library.get(saveAsMatch[1]);
407
- if (!image) {
408
- json(response, 404, { error: "Image not found" });
409
- return;
410
- }
411
- json(response, 200, await saveImageAs(image, { locale: url.searchParams.get("locale") ?? "en" }));
412
- return;
413
- }
414
-
415
- const detailMatch = url.pathname.match(/^\/api\/images\/([a-f0-9]{24})$/);
416
- if (request.method === "GET" && detailMatch) {
417
- const image = await library.hydratePrompt(detailMatch[1]);
418
- if (!image) {
419
- json(response, 404, { error: "Image not found" });
420
- return;
421
- }
422
- json(response, 200, { image: publicImage(image, { includePrompt: true }) });
423
- return;
424
- }
425
-
426
- const imageMatch = url.pathname.match(/^\/api\/images\/([a-f0-9]{24})\/(file|download)$/);
427
- if (request.method === "GET" && imageMatch) {
428
- const image = library.get(imageMatch[1]);
429
- if (!image) {
430
- json(response, 404, { error: "Image not found" });
431
- return;
432
- }
433
- const sourceStat = await stat(image.sourcePath);
434
- const headers = {
435
- "Content-Type": contentTypeForPath(image.sourcePath),
436
- "Content-Length": sourceStat.size,
437
- "Cache-Control": "public, max-age=31536000, immutable",
438
- };
439
- if (imageMatch[2] === "download") {
440
- headers["Content-Disposition"] = `attachment; filename="${downloadFilename(image, image.sourcePath)}"`;
441
- }
442
- response.writeHead(200, headers);
443
- createReadStream(image.sourcePath).pipe(response);
444
- return;
445
- }
446
-
447
- const staticFile = STATIC_FILES.get(url.pathname);
448
- if (request.method === "GET" && staticFile) {
449
- const [filename, contentType] = staticFile;
450
- const body = await readFile(path.join(webDir, filename));
451
- response.writeHead(200, {
452
- "Content-Type": contentType,
453
- "Cache-Control": "no-cache",
454
- "Content-Security-Policy": "default-src 'self'; img-src 'self'; style-src 'self'; script-src 'self'; connect-src 'self'",
455
- });
456
- response.end(body);
457
- return;
458
- }
459
-
460
- json(response, 404, { error: "Not found" });
461
- } catch (error) {
462
- const status = Number.isInteger(error.status) ? error.status : 500;
463
- json(response, status, {
464
- error: {
465
- code: error.code ?? "INTERNAL_ERROR",
466
- message: error.message,
467
- },
468
- });
469
- }
470
- });
471
-
472
- await new Promise((resolve, reject) => {
473
- server.once("error", reject);
474
- server.listen(port, host, resolve);
475
- });
476
-
477
- const address = server.address();
478
- const boundPort = address && typeof address !== "string" ? address.port : port;
479
- const origin = `http://${formatHost(host)}:${boundPort}`;
480
- return {
481
- host,
482
- port: boundPort,
483
- origin,
484
- url: origin,
485
- close: () => new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve()))),
486
- };
487
- }
@@ -1,52 +0,0 @@
1
- import os from "node:os";
2
- import path from "node:path";
3
- import { fileURLToPath } from "node:url";
4
- import { ImageLibrary } from "./image-library.mjs";
5
- import { createHttpServer } from "./http-server.mjs";
6
- import { ThreadMetadataIndex } from "./thread-metadata.mjs";
7
-
8
- const projectRoot = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
9
- const dataDir = process.env.CODEX_IMAGE_HOST_DATA_DIR ?? path.join(os.homedir(), ".codex-image-host");
10
- const sourceRoot = process.env.CODEX_IMAGEGEN_ROOT ?? path.join(os.homedir(), ".codex", "generated_images");
11
- const codexRoot = path.join(os.homedir(), ".codex");
12
- const sessionRoots = [
13
- process.env.CODEX_SESSION_ROOT ?? path.join(codexRoot, "sessions"),
14
- path.join(codexRoot, "archived_sessions"),
15
- ];
16
- const port = Number(process.env.CODEX_IMAGE_HOST_PORT ?? 47831);
17
- const threadMetadata = new ThreadMetadataIndex({
18
- stateDatabasePath: process.env.CODEX_STATE_DATABASE ?? path.join(codexRoot, "state_5.sqlite"),
19
- globalStatePath: process.env.CODEX_GLOBAL_STATE ?? path.join(codexRoot, ".codex-global-state.json"),
20
- sessionIndexPath: process.env.CODEX_SESSION_INDEX ?? path.join(codexRoot, "session_index.jsonl"),
21
- });
22
-
23
- const library = new ImageLibrary({ sourceRoot, dataDir, sessionRoots, threadMetadata });
24
- const startup = await library.start({ deferInitialScan: true });
25
- const server = await createHttpServer({
26
- library,
27
- webDir: path.join(projectRoot, "web"),
28
- port,
29
- });
30
-
31
- console.log(`Images: ${startup.total} indexed; initial scan running in background`);
32
- console.log(`Library: ${server.url}`);
33
-
34
- async function shutdown() {
35
- await server.close();
36
- library.close();
37
- }
38
-
39
- for (const signal of ["SIGINT", "SIGTERM"]) {
40
- process.once(signal, async () => {
41
- await shutdown();
42
- process.exit(0);
43
- });
44
- }
45
-
46
- void startup.initialScan()
47
- .then(({ total, imported }) => {
48
- console.log(`Images: ${total} total, ${imported} imported by initial scan`);
49
- })
50
- .catch((error) => {
51
- console.error("Images initial scan failed", error);
52
- });
@@ -1,69 +0,0 @@
1
- import { execFile } from "node:child_process";
2
- import { copyFile } from "node:fs/promises";
3
- import path from "node:path";
4
- import { promisify } from "node:util";
5
-
6
- const run = promisify(execFile);
7
- const SAVE_DIALOG_SCRIPT = `
8
- on run argv
9
- try
10
- set destinationFile to choose file name with prompt (item 2 of argv) default name (item 1 of argv)
11
- return POSIX path of destinationFile
12
- on error number -128
13
- return ""
14
- end try
15
- end run
16
- `;
17
-
18
- // PowerShell 单引号字面量:内部单引号翻倍转义。
19
- function psSingleQuoted(value) {
20
- return "'" + String(value).replaceAll("'", "''") + "'";
21
- }
22
-
23
- function buildWindowsSaveDialogScript(fileName, title, chinese) {
24
- return [
25
- "[Console]::OutputEncoding = [System.Text.Encoding]::UTF8",
26
- "Add-Type -AssemblyName System.Windows.Forms | Out-Null",
27
- "$dialog = New-Object System.Windows.Forms.SaveFileDialog",
28
- "$dialog.Title = " + psSingleQuoted(title),
29
- "$dialog.FileName = " + psSingleQuoted(fileName),
30
- "$dialog.Filter = " + psSingleQuoted(chinese ? "PNG 图片 (*.png)|*.png" : "PNG image (*.png)|*.png"),
31
- "if ($dialog.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) { [Console]::Out.Write($dialog.FileName) }",
32
- ].join("; ");
33
- }
34
-
35
- export function defaultImageFilename(threadTitle) {
36
- const rawTitle = String(threadTitle || "Generated image").replace(/\.png$/i, "");
37
- const safeTitle = rawTitle
38
- .replace(/[\\/:*?"<>|\u0000-\u001f]/g, "-")
39
- .replace(/\s+/g, " ")
40
- .replace(/^\.+|\.+$/g, "")
41
- .trim()
42
- .slice(0, 96);
43
- return `${safeTitle || "Generated image"}.png`;
44
- }
45
-
46
- export async function saveImageAs(image, { locale = "en" } = {}) {
47
- const chinese = String(locale).toLowerCase().startsWith("zh");
48
- const title = chinese ? "图片另存为" : "Save image as";
49
- let destinationPath;
50
- if (process.platform === "win32") {
51
- const { stdout } = await run(
52
- "powershell.exe",
53
- ["-NoProfile", "-STA", "-Command", buildWindowsSaveDialogScript(defaultImageFilename(image.threadTitle), title, chinese)],
54
- { encoding: "utf8" },
55
- );
56
- destinationPath = stdout.trim();
57
- } else {
58
- const { stdout } = await run(
59
- "/usr/bin/osascript",
60
- ["-e", SAVE_DIALOG_SCRIPT, defaultImageFilename(image.threadTitle), title],
61
- { encoding: "utf8" },
62
- );
63
- destinationPath = stdout.trim();
64
- }
65
- if (!destinationPath) return { saved: false, canceled: true };
66
-
67
- await copyFile(image.sourcePath, destinationPath);
68
- return { saved: true, fileName: path.basename(destinationPath) };
69
- }