minecodex 0.1.0
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/LICENSE +21 -0
- package/README.md +56 -0
- package/apps/dashboard/app.js +387 -0
- package/apps/dashboard/favicon.svg +18 -0
- package/apps/dashboard/index.html +70 -0
- package/apps/dashboard/styles.css +297 -0
- package/features/images/README.md +106 -0
- package/features/images/codex-feature.json +24 -0
- package/features/images/src/http-server.mjs +302 -0
- package/features/images/src/image-library.mjs +582 -0
- package/features/images/src/main.mjs +52 -0
- package/features/images/src/prompt-index.mjs +182 -0
- package/features/images/src/save-as.mjs +40 -0
- package/features/images/src/thread-metadata.mjs +93 -0
- package/features/images/web/app.js +1045 -0
- package/features/images/web/index.html +162 -0
- package/features/images/web/styles.css +994 -0
- package/features/model-slider/README.md +30 -0
- package/features/model-slider/THIRD_PARTY_NOTICES.md +24 -0
- package/features/model-slider/assets/providers/anthropic.svg +1 -0
- package/features/model-slider/assets/providers/deepseek.svg +1 -0
- package/features/model-slider/assets/providers/gemini.svg +1 -0
- package/features/model-slider/assets/providers/grok.svg +1 -0
- package/features/model-slider/assets/providers/hunyuan.svg +1 -0
- package/features/model-slider/assets/providers/kimi.svg +1 -0
- package/features/model-slider/assets/providers/minimax.svg +1 -0
- package/features/model-slider/assets/providers/openai.svg +1 -0
- package/features/model-slider/assets/providers/qwen.svg +1 -0
- package/features/model-slider/assets/providers/xiaomimimo.svg +1 -0
- package/features/model-slider/assets/providers/zhipu.svg +1 -0
- package/features/model-slider/assets/ui/sparkles.svg +18 -0
- package/features/model-slider/assets/ui/star-off.svg +17 -0
- package/features/model-slider/assets/ui/star.svg +15 -0
- package/features/model-slider/codex-feature.json +56 -0
- package/features/model-slider/vendor/cc-switch-LICENSE +21 -0
- package/features/model-slider/vendor/lucide-LICENSE +35 -0
- package/features/notes/README.md +106 -0
- package/features/notes/assets/icons/circle.svg +15 -0
- package/features/notes/assets/icons/ellipsis.svg +17 -0
- package/features/notes/assets/icons/external-link.svg +17 -0
- package/features/notes/assets/icons/list-todo.svg +19 -0
- package/features/notes/assets/icons/message-square-quote.svg +17 -0
- package/features/notes/assets/icons/paperclip.svg +15 -0
- package/features/notes/assets/icons/star-off.svg +17 -0
- package/features/notes/assets/icons/star.svg +15 -0
- package/features/notes/assets/icons/sticky-note.svg +16 -0
- package/features/notes/assets/icons/trash-2.svg +19 -0
- package/features/notes/codex-feature.json +90 -0
- package/features/notes/src/http-server.mjs +541 -0
- package/features/notes/src/main.mjs +54 -0
- package/features/notes/src/product.mjs +771 -0
- package/features/notes/web/app.js +1495 -0
- package/features/notes/web/codex-artifact-document.svg +1 -0
- package/features/notes/web/codex-attachment.svg +1 -0
- package/features/notes/web/codex-build.svg +1 -0
- package/features/notes/web/codex-code.svg +1 -0
- package/features/notes/web/codex-cplusplus.svg +1 -0
- package/features/notes/web/codex-css.svg +1 -0
- package/features/notes/web/codex-document.svg +1 -0
- package/features/notes/web/codex-file.svg +1 -0
- package/features/notes/web/codex-folder.svg +1 -0
- package/features/notes/web/codex-hashes.svg +1 -0
- package/features/notes/web/codex-html.svg +1 -0
- package/features/notes/web/codex-image.svg +1 -0
- package/features/notes/web/codex-java.svg +1 -0
- package/features/notes/web/codex-javascript.svg +1 -0
- package/features/notes/web/codex-json.svg +1 -0
- package/features/notes/web/codex-notebook.svg +1 -0
- package/features/notes/web/codex-pdf.svg +1 -0
- package/features/notes/web/codex-pencil.svg +3 -0
- package/features/notes/web/codex-php.svg +1 -0
- package/features/notes/web/codex-plus.svg +3 -0
- package/features/notes/web/codex-presentation.svg +1 -0
- package/features/notes/web/codex-python.svg +1 -0
- package/features/notes/web/codex-react.svg +1 -0
- package/features/notes/web/codex-rust.svg +1 -0
- package/features/notes/web/codex-shell.svg +1 -0
- package/features/notes/web/codex-skill.svg +1 -0
- package/features/notes/web/codex-spreadsheet.svg +1 -0
- package/features/notes/web/codex-task.svg +3 -0
- package/features/notes/web/codex-terminal.svg +1 -0
- package/features/notes/web/codex-toml.svg +1 -0
- package/features/notes/web/codex-typescript.svg +1 -0
- package/features/notes/web/codex-view-all.svg +3 -0
- package/features/notes/web/index.html +15 -0
- package/features/notes/web/styles.css +1155 -0
- package/package.json +67 -0
- package/packages/cli/bin/mcx.mjs +7 -0
- package/packages/cli/src/browser-exchange.mjs +31 -0
- package/packages/cli/src/commands.mjs +389 -0
- package/packages/cli/src/config.mjs +101 -0
- package/packages/cli/src/control-server.mjs +371 -0
- package/packages/cli/src/npm-adapter.mjs +40 -0
- package/packages/cli/src/paths.mjs +33 -0
- package/packages/cli/src/platform.mjs +368 -0
- package/packages/cli/src/runtime-manager.mjs +338 -0
- package/packages/runtime-host/README.md +171 -0
- package/packages/runtime-host/src/codex-runtime.mjs +4311 -0
- package/packages/runtime-host/src/feature-registry.mjs +827 -0
- package/packages/runtime-host/src/main.mjs +98 -0
- package/packages/runtime-host/src/pid-file.mjs +23 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { createReadStream } from "node:fs";
|
|
2
|
+
import { readFile, stat } from "node:fs/promises";
|
|
3
|
+
import { createServer as createNodeServer } from "node:http";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { saveImageAs } from "./save-as.mjs";
|
|
6
|
+
|
|
7
|
+
const LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "::1", "[::1]"]);
|
|
8
|
+
const DEFAULT_PAGE_LIMIT = 36;
|
|
9
|
+
const MAX_PAGE_LIMIT = 72;
|
|
10
|
+
const MAX_PAGE_OFFSET = Number.MAX_SAFE_INTEGER;
|
|
11
|
+
|
|
12
|
+
const STATIC_FILES = new Map([
|
|
13
|
+
["/", ["index.html", "text/html; charset=utf-8"]],
|
|
14
|
+
["/app.js", ["app.js", "text/javascript; charset=utf-8"]],
|
|
15
|
+
["/styles.css", ["styles.css", "text/css; charset=utf-8"]],
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
function json(response, statusCode, body) {
|
|
19
|
+
response.writeHead(statusCode, {
|
|
20
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
21
|
+
"Cache-Control": "no-store",
|
|
22
|
+
});
|
|
23
|
+
response.end(JSON.stringify(body));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function publicImage(image, { includePrompt = false } = {}) {
|
|
27
|
+
const result = {
|
|
28
|
+
id: image.id,
|
|
29
|
+
threadId: image.threadId,
|
|
30
|
+
imageGenerationItemId: image.imageGenerationItemId,
|
|
31
|
+
width: image.width,
|
|
32
|
+
height: image.height,
|
|
33
|
+
byteLength: image.byteLength,
|
|
34
|
+
createdAt: image.createdAt,
|
|
35
|
+
importedAt: image.importedAt,
|
|
36
|
+
sourceKind: image.sourceKind,
|
|
37
|
+
projectId: image.projectId,
|
|
38
|
+
projectName: image.projectName,
|
|
39
|
+
threadTitle: image.threadTitle,
|
|
40
|
+
archived: image.archived,
|
|
41
|
+
fileUrl: `/api/images/${image.id}/file`,
|
|
42
|
+
downloadUrl: `/api/images/${image.id}/download`,
|
|
43
|
+
sourceUrl: `codex://threads/${image.threadId}`,
|
|
44
|
+
};
|
|
45
|
+
if (includePrompt) result.prompt = image.prompt;
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function publicGroup(group) {
|
|
50
|
+
return {
|
|
51
|
+
threadId: group.threadId,
|
|
52
|
+
sourceKind: group.sourceKind,
|
|
53
|
+
projectId: group.projectId,
|
|
54
|
+
projectName: group.projectName,
|
|
55
|
+
threadTitle: group.threadTitle,
|
|
56
|
+
archived: group.archived,
|
|
57
|
+
imageCount: group.imageCount,
|
|
58
|
+
createdAt: group.createdAt,
|
|
59
|
+
updatedAt: group.updatedAt,
|
|
60
|
+
previews: group.previews.map((image) => publicImage(image)),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function formatHost(host) {
|
|
65
|
+
return host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function mutationError(message, code) {
|
|
69
|
+
return Object.assign(new Error(message), { status: 403, code });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function parseIntegerQuery(searchParams, name, { defaultValue, min, max }) {
|
|
73
|
+
const values = searchParams.getAll(name);
|
|
74
|
+
if (values.length === 0) return defaultValue;
|
|
75
|
+
const [raw] = values;
|
|
76
|
+
if (values.length !== 1 || !/^(0|[1-9]\d*)$/.test(raw)) {
|
|
77
|
+
throw Object.assign(new Error(`${name} must be a single integer between ${min} and ${max}`), {
|
|
78
|
+
status: 400,
|
|
79
|
+
code: "INVALID_QUERY",
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
const value = Number(raw);
|
|
83
|
+
if (!Number.isSafeInteger(value) || value < min || value > max) {
|
|
84
|
+
throw Object.assign(new Error(`${name} must be between ${min} and ${max}`), {
|
|
85
|
+
status: 400,
|
|
86
|
+
code: "INVALID_QUERY",
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return value;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function paginationFor(url) {
|
|
93
|
+
return {
|
|
94
|
+
limit: parseIntegerQuery(url.searchParams, "limit", {
|
|
95
|
+
defaultValue: DEFAULT_PAGE_LIMIT,
|
|
96
|
+
min: 1,
|
|
97
|
+
max: MAX_PAGE_LIMIT,
|
|
98
|
+
}),
|
|
99
|
+
offset: parseIntegerQuery(url.searchParams, "offset", {
|
|
100
|
+
defaultValue: 0,
|
|
101
|
+
min: 0,
|
|
102
|
+
max: MAX_PAGE_OFFSET,
|
|
103
|
+
}),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function validateMutationOrigin(request, address, host) {
|
|
108
|
+
const expectedOrigin = `http://${formatHost(host)}:${address.port}`;
|
|
109
|
+
const expectedHost = new URL(expectedOrigin).host;
|
|
110
|
+
if (request.headers.host !== expectedHost) {
|
|
111
|
+
throw mutationError("Mutation requests are restricted to the exact bound loopback Host", "HOST_NOT_ALLOWED");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const origin = request.headers.origin;
|
|
115
|
+
if (!origin) {
|
|
116
|
+
throw mutationError("Mutation requests require the exact bound loopback Origin", "ORIGIN_NOT_ALLOWED");
|
|
117
|
+
}
|
|
118
|
+
let parsed;
|
|
119
|
+
try {
|
|
120
|
+
parsed = new URL(origin);
|
|
121
|
+
} catch {
|
|
122
|
+
throw mutationError("Request origin is invalid", "INVALID_ORIGIN");
|
|
123
|
+
}
|
|
124
|
+
if (parsed.origin !== expectedOrigin) {
|
|
125
|
+
throw mutationError("Mutation requests are restricted to this exact local Images origin", "ORIGIN_NOT_ALLOWED");
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export async function createHttpServer({
|
|
130
|
+
library,
|
|
131
|
+
webDir,
|
|
132
|
+
host = "127.0.0.1",
|
|
133
|
+
port = 47831,
|
|
134
|
+
instanceId = process.env.MINECODEX_FEATURE_INSTANCE_ID ?? "unconfigured",
|
|
135
|
+
}) {
|
|
136
|
+
if (!library) throw new Error("library is required");
|
|
137
|
+
if (!LOOPBACK_HOSTS.has(host)) throw new Error("Images only supports loopback hosts");
|
|
138
|
+
const server = createNodeServer(async (request, response) => {
|
|
139
|
+
try {
|
|
140
|
+
const address = server.address();
|
|
141
|
+
const boundOrigin = address && typeof address !== "string"
|
|
142
|
+
? `http://${formatHost(host)}:${address.port}`
|
|
143
|
+
: `http://${formatHost(host)}:${port}`;
|
|
144
|
+
const url = new URL(request.url ?? "/", boundOrigin);
|
|
145
|
+
|
|
146
|
+
if (!["GET", "HEAD", "OPTIONS"].includes(request.method)) {
|
|
147
|
+
validateMutationOrigin(request, address, host);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (request.method === "GET" && (url.pathname === "/api/health" || url.pathname === "/health")) {
|
|
151
|
+
json(response, 200, {
|
|
152
|
+
ok: true,
|
|
153
|
+
service: "images",
|
|
154
|
+
protocolVersion: 1,
|
|
155
|
+
instanceId,
|
|
156
|
+
images: library.count(),
|
|
157
|
+
});
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (request.method === "GET" && url.pathname === "/favicon.ico") {
|
|
162
|
+
response.writeHead(204);
|
|
163
|
+
response.end();
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (request.method === "GET" && url.pathname === "/api/images") {
|
|
168
|
+
const { limit, offset } = paginationFor(url);
|
|
169
|
+
const source = url.searchParams.get("source") ?? "all";
|
|
170
|
+
const threadId = url.searchParams.get("threadId") || null;
|
|
171
|
+
const sort = url.searchParams.get("sort") === "asc" ? "asc" : "desc";
|
|
172
|
+
const query = url.searchParams.get("q") ?? "";
|
|
173
|
+
const images = library.list({
|
|
174
|
+
query,
|
|
175
|
+
source,
|
|
176
|
+
threadId,
|
|
177
|
+
sort,
|
|
178
|
+
limit,
|
|
179
|
+
offset,
|
|
180
|
+
}).map(publicImage);
|
|
181
|
+
json(response, 200, {
|
|
182
|
+
images,
|
|
183
|
+
count: library.count({ query, source, threadId }),
|
|
184
|
+
limit,
|
|
185
|
+
offset,
|
|
186
|
+
});
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (request.method === "GET" && url.pathname === "/api/image-facets") {
|
|
191
|
+
json(response, 200, library.facets());
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (request.method === "GET" && url.pathname === "/api/image-groups") {
|
|
196
|
+
const { limit, offset } = paginationFor(url);
|
|
197
|
+
const source = url.searchParams.get("source") ?? "all";
|
|
198
|
+
const groups = library.groups({ source, limit, offset }).map(publicGroup);
|
|
199
|
+
json(response, 200, {
|
|
200
|
+
groups,
|
|
201
|
+
count: library.countGroups({ source }),
|
|
202
|
+
imageCount: library.count({ source }),
|
|
203
|
+
limit,
|
|
204
|
+
offset,
|
|
205
|
+
});
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (request.method === "POST" && url.pathname === "/api/rescan") {
|
|
210
|
+
if (library.rescanStatus().inProgress) {
|
|
211
|
+
json(response, 202, { imported: 0, total: library.count(), inProgress: true });
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const imported = await library.rescan();
|
|
215
|
+
json(response, 200, { imported, total: library.count(), inProgress: false });
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const saveAsMatch = url.pathname.match(/^\/api\/images\/([a-f0-9]{24})\/save-as$/);
|
|
220
|
+
if (request.method === "POST" && saveAsMatch) {
|
|
221
|
+
const image = library.get(saveAsMatch[1]);
|
|
222
|
+
if (!image) {
|
|
223
|
+
json(response, 404, { error: "Image not found" });
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
json(response, 200, await saveImageAs(image));
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const detailMatch = url.pathname.match(/^\/api\/images\/([a-f0-9]{24})$/);
|
|
231
|
+
if (request.method === "GET" && detailMatch) {
|
|
232
|
+
const image = await library.hydratePrompt(detailMatch[1]);
|
|
233
|
+
if (!image) {
|
|
234
|
+
json(response, 404, { error: "Image not found" });
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
json(response, 200, { image: publicImage(image, { includePrompt: true }) });
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const imageMatch = url.pathname.match(/^\/api\/images\/([a-f0-9]{24})\/(file|download)$/);
|
|
242
|
+
if (request.method === "GET" && imageMatch) {
|
|
243
|
+
const image = library.get(imageMatch[1]);
|
|
244
|
+
if (!image) {
|
|
245
|
+
json(response, 404, { error: "Image not found" });
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
const sourceStat = await stat(image.sourcePath);
|
|
249
|
+
const headers = {
|
|
250
|
+
"Content-Type": "image/png",
|
|
251
|
+
"Content-Length": sourceStat.size,
|
|
252
|
+
"Cache-Control": "public, max-age=31536000, immutable",
|
|
253
|
+
};
|
|
254
|
+
if (imageMatch[2] === "download") {
|
|
255
|
+
headers["Content-Disposition"] = `attachment; filename="${image.imageGenerationItemId}.png"`;
|
|
256
|
+
}
|
|
257
|
+
response.writeHead(200, headers);
|
|
258
|
+
createReadStream(image.sourcePath).pipe(response);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const staticFile = STATIC_FILES.get(url.pathname);
|
|
263
|
+
if (request.method === "GET" && staticFile) {
|
|
264
|
+
const [filename, contentType] = staticFile;
|
|
265
|
+
const body = await readFile(path.join(webDir, filename));
|
|
266
|
+
response.writeHead(200, {
|
|
267
|
+
"Content-Type": contentType,
|
|
268
|
+
"Cache-Control": "no-cache",
|
|
269
|
+
"Content-Security-Policy": "default-src 'self'; img-src 'self'; style-src 'self'; script-src 'self'; connect-src 'self'",
|
|
270
|
+
});
|
|
271
|
+
response.end(body);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
json(response, 404, { error: "Not found" });
|
|
276
|
+
} catch (error) {
|
|
277
|
+
const status = Number.isInteger(error.status) ? error.status : 500;
|
|
278
|
+
json(response, status, {
|
|
279
|
+
error: {
|
|
280
|
+
code: error.code ?? "INTERNAL_ERROR",
|
|
281
|
+
message: error.message,
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
await new Promise((resolve, reject) => {
|
|
288
|
+
server.once("error", reject);
|
|
289
|
+
server.listen(port, host, resolve);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
const address = server.address();
|
|
293
|
+
const boundPort = address && typeof address !== "string" ? address.port : port;
|
|
294
|
+
const origin = `http://${formatHost(host)}:${boundPort}`;
|
|
295
|
+
return {
|
|
296
|
+
host,
|
|
297
|
+
port: boundPort,
|
|
298
|
+
origin,
|
|
299
|
+
url: origin,
|
|
300
|
+
close: () => new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve()))),
|
|
301
|
+
};
|
|
302
|
+
}
|