minecodex 1.2.2 → 2.0.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.
- package/README.md +9 -3
- package/features/images/README.md +16 -98
- package/features/images/codex-feature.json +6 -23
- package/features/images/src/library-prompt-page.mjs +239 -0
- package/features/model-slider/THIRD_PARTY_NOTICES.md +1 -1
- package/features/notes/DESIGN.md +66 -0
- package/features/notes/README.md +18 -99
- package/features/notes/assets/icons/book-open-nav.svg +15 -0
- package/features/notes/assets/icons/book-open.svg +16 -0
- package/features/notes/assets/native-emojis.json +1 -0
- package/features/notes/assets/native-prompt-icons.json +28 -0
- package/features/notes/codex-feature.json +12 -70
- package/features/notes/src/main.mjs +2 -2
- package/features/notes/src/product.mjs +287 -613
- package/features/notes/src/prompt-schema.mjs +117 -0
- package/features/notes/src/quick-prompts-page.mjs +1559 -0
- package/features/notes/src/quick-prompts-server.mjs +167 -0
- package/features/session-status/DESIGN.md +11 -3
- package/features/session-status/README.md +8 -4
- package/features/session-status/src/session-status.mjs +272 -25
- package/features/sidebar-focus/DESIGN.md +16 -0
- package/features/sidebar-focus/codex-feature.json +20 -0
- package/features/sidebar-focus/src/sidebar-focus.mjs +134 -0
- package/features/sidebar-focus/vendor/lucide-LICENSE +43 -0
- package/features/unread-inbox/DESIGN.md +36 -0
- package/features/unread-inbox/codex-feature.json +20 -0
- package/features/unread-inbox/src/unread-inbox.mjs +446 -0
- package/features/usage-meter/DESIGN.md +49 -0
- package/features/usage-meter/README.md +18 -0
- package/features/usage-meter/codex-feature.json +13 -0
- package/features/usage-meter/src/usage-meter.mjs +200 -0
- package/package.json +17 -5
- package/packages/cli/src/config.mjs +4 -1
- package/packages/cli/src/control-server.mjs +2 -2
- package/packages/runtime-host/src/codex-injection.mjs +38 -3
- package/packages/runtime-host/src/codex-runtime.mjs +41 -20
- package/packages/runtime-host/src/feature-registry.mjs +1 -0
- package/packages/runtime-host/src/session-status-reader.mjs +40 -16
- package/features/notes/assets/icons/circle.svg +0 -15
- package/features/notes/assets/icons/ellipsis.svg +0 -17
- package/features/notes/assets/icons/external-link.svg +0 -17
- package/features/notes/assets/icons/list-todo.svg +0 -19
- package/features/notes/assets/icons/message-square-quote.svg +0 -17
- package/features/notes/assets/icons/paperclip.svg +0 -15
- package/features/notes/assets/icons/star-off.svg +0 -17
- package/features/notes/assets/icons/star.svg +0 -15
- package/features/notes/assets/icons/sticky-note.svg +0 -16
- package/features/notes/assets/icons/trash-2.svg +0 -19
- package/features/notes/src/http-server.mjs +0 -580
- package/features/notes/web/app.js +0 -1202
- package/features/notes/web/codex-artifact-document.svg +0 -1
- package/features/notes/web/codex-attachment.svg +0 -1
- package/features/notes/web/codex-build.svg +0 -1
- package/features/notes/web/codex-code.svg +0 -1
- package/features/notes/web/codex-cplusplus.svg +0 -1
- package/features/notes/web/codex-css.svg +0 -1
- package/features/notes/web/codex-document.svg +0 -1
- package/features/notes/web/codex-file.svg +0 -1
- package/features/notes/web/codex-folder.svg +0 -1
- package/features/notes/web/codex-hashes.svg +0 -1
- package/features/notes/web/codex-html.svg +0 -1
- package/features/notes/web/codex-image.svg +0 -1
- package/features/notes/web/codex-java.svg +0 -1
- package/features/notes/web/codex-javascript.svg +0 -1
- package/features/notes/web/codex-json.svg +0 -1
- package/features/notes/web/codex-notebook.svg +0 -1
- package/features/notes/web/codex-pdf.svg +0 -1
- package/features/notes/web/codex-pencil.svg +0 -3
- package/features/notes/web/codex-php.svg +0 -1
- package/features/notes/web/codex-plus.svg +0 -3
- package/features/notes/web/codex-presentation.svg +0 -1
- package/features/notes/web/codex-python.svg +0 -1
- package/features/notes/web/codex-react.svg +0 -1
- package/features/notes/web/codex-rust.svg +0 -1
- package/features/notes/web/codex-shell.svg +0 -1
- package/features/notes/web/codex-skill.svg +0 -1
- package/features/notes/web/codex-spreadsheet.svg +0 -1
- package/features/notes/web/codex-task.svg +0 -3
- package/features/notes/web/codex-terminal.svg +0 -1
- package/features/notes/web/codex-toml.svg +0 -1
- package/features/notes/web/codex-typescript.svg +0 -1
- package/features/notes/web/codex-view-all.svg +0 -3
- package/features/notes/web/file-icons.mjs +0 -68
- package/features/notes/web/i18n.mjs +0 -237
- package/features/notes/web/index.html +0 -15
- package/features/notes/web/styles.css +0 -1155
- package/features/notes/web/todo-drag.mjs +0 -18
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
<!-- @license lucide-static v1.31.0 - ISC -->
|
|
2
|
-
<svg
|
|
3
|
-
class="lucide lucide-message-square-quote"
|
|
4
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
5
|
-
width="24"
|
|
6
|
-
height="24"
|
|
7
|
-
viewBox="0 0 24 24"
|
|
8
|
-
fill="none"
|
|
9
|
-
stroke="currentColor"
|
|
10
|
-
stroke-width="2"
|
|
11
|
-
stroke-linecap="round"
|
|
12
|
-
stroke-linejoin="round"
|
|
13
|
-
>
|
|
14
|
-
<path d="M14 14a2 2 0 0 0 2-2V8h-2" />
|
|
15
|
-
<path d="M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z" />
|
|
16
|
-
<path d="M8 14a2 2 0 0 0 2-2V8H8" />
|
|
17
|
-
</svg>
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
<!-- @license lucide-static v1.31.0 - ISC -->
|
|
2
|
-
<svg
|
|
3
|
-
class="lucide lucide-paperclip"
|
|
4
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
5
|
-
width="24"
|
|
6
|
-
height="24"
|
|
7
|
-
viewBox="0 0 24 24"
|
|
8
|
-
fill="none"
|
|
9
|
-
stroke="currentColor"
|
|
10
|
-
stroke-width="2"
|
|
11
|
-
stroke-linecap="round"
|
|
12
|
-
stroke-linejoin="round"
|
|
13
|
-
>
|
|
14
|
-
<path d="m16 6-8.414 8.586a2 2 0 0 0 2.829 2.829l8.414-8.586a4 4 0 1 0-5.657-5.657l-8.379 8.551a6 6 0 1 0 8.485 8.485l8.379-8.551" />
|
|
15
|
-
</svg>
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
<!-- @license lucide-static v1.31.0 - ISC -->
|
|
2
|
-
<svg
|
|
3
|
-
class="lucide lucide-star-off"
|
|
4
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
5
|
-
width="24"
|
|
6
|
-
height="24"
|
|
7
|
-
viewBox="0 0 24 24"
|
|
8
|
-
fill="none"
|
|
9
|
-
stroke="currentColor"
|
|
10
|
-
stroke-width="2"
|
|
11
|
-
stroke-linecap="round"
|
|
12
|
-
stroke-linejoin="round"
|
|
13
|
-
>
|
|
14
|
-
<path d="m10.344 4.688 1.181-2.393a.53.53 0 0 1 .95 0l2.31 4.679a2.12 2.12 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.237 3.152" />
|
|
15
|
-
<path d="m17.945 17.945.43 2.505a.53.53 0 0 1-.771.56l-4.618-2.428a2.12 2.12 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.12 2.12 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a8 8 0 0 0 .4-.099" />
|
|
16
|
-
<path d="m2 2 20 20" />
|
|
17
|
-
</svg>
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
<!-- @license lucide-static v1.31.0 - ISC -->
|
|
2
|
-
<svg
|
|
3
|
-
class="lucide lucide-star"
|
|
4
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
5
|
-
width="24"
|
|
6
|
-
height="24"
|
|
7
|
-
viewBox="0 0 24 24"
|
|
8
|
-
fill="none"
|
|
9
|
-
stroke="currentColor"
|
|
10
|
-
stroke-width="2"
|
|
11
|
-
stroke-linecap="round"
|
|
12
|
-
stroke-linejoin="round"
|
|
13
|
-
>
|
|
14
|
-
<path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z" />
|
|
15
|
-
</svg>
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
<!-- @license lucide-static v1.31.0 - ISC -->
|
|
2
|
-
<svg
|
|
3
|
-
class="lucide lucide-sticky-note"
|
|
4
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
5
|
-
width="24"
|
|
6
|
-
height="24"
|
|
7
|
-
viewBox="0 0 24 24"
|
|
8
|
-
fill="none"
|
|
9
|
-
stroke="currentColor"
|
|
10
|
-
stroke-width="2"
|
|
11
|
-
stroke-linecap="round"
|
|
12
|
-
stroke-linejoin="round"
|
|
13
|
-
>
|
|
14
|
-
<path d="M21 9a2.4 2.4 0 0 0-.706-1.706l-3.588-3.588A2.4 2.4 0 0 0 15 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2z" />
|
|
15
|
-
<path d="M15 3v5a1 1 0 0 0 1 1h5" />
|
|
16
|
-
</svg>
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
<!-- @license lucide-static v1.31.0 - ISC -->
|
|
2
|
-
<svg
|
|
3
|
-
class="lucide lucide-trash-2"
|
|
4
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
5
|
-
width="24"
|
|
6
|
-
height="24"
|
|
7
|
-
viewBox="0 0 24 24"
|
|
8
|
-
fill="none"
|
|
9
|
-
stroke="currentColor"
|
|
10
|
-
stroke-width="2"
|
|
11
|
-
stroke-linecap="round"
|
|
12
|
-
stroke-linejoin="round"
|
|
13
|
-
>
|
|
14
|
-
<path d="M10 11v6" />
|
|
15
|
-
<path d="M14 11v6" />
|
|
16
|
-
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" />
|
|
17
|
-
<path d="M3 6h18" />
|
|
18
|
-
<path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
|
|
19
|
-
</svg>
|
|
@@ -1,580 +0,0 @@
|
|
|
1
|
-
import { execFile } from "node:child_process";
|
|
2
|
-
import { createReadStream } from "node:fs";
|
|
3
|
-
import { readFile, stat } from "node:fs/promises";
|
|
4
|
-
import { createServer } from "node:http";
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
import { pipeline } from "node:stream/promises";
|
|
7
|
-
import { promisify } from "node:util";
|
|
8
|
-
import { fileURLToPath } from "node:url";
|
|
9
|
-
|
|
10
|
-
import { ProductError } from "./product.mjs";
|
|
11
|
-
|
|
12
|
-
const execFileAsync = promisify(execFile);
|
|
13
|
-
const DEFAULT_WEB_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../web");
|
|
14
|
-
const LUCIDE_ICON_ROOT = path.resolve(DEFAULT_WEB_ROOT, "../assets/icons");
|
|
15
|
-
const MAX_BODY_BYTES = 1024 * 1024;
|
|
16
|
-
const MAX_PREVIEW_BYTES = 64 * 1024 * 1024;
|
|
17
|
-
const LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "::1", "[::1]"]);
|
|
18
|
-
const CODEX_EMBED_ORIGIN = "app://-";
|
|
19
|
-
const LUCIDE_ICON_NAMES = [
|
|
20
|
-
"circle",
|
|
21
|
-
"ellipsis",
|
|
22
|
-
"external-link",
|
|
23
|
-
"list-todo",
|
|
24
|
-
"message-square-quote",
|
|
25
|
-
"paperclip",
|
|
26
|
-
"star",
|
|
27
|
-
"star-off",
|
|
28
|
-
"trash-2",
|
|
29
|
-
];
|
|
30
|
-
const THIN_LUCIDE_ICON_NAMES = new Set([
|
|
31
|
-
"ellipsis",
|
|
32
|
-
"external-link",
|
|
33
|
-
"list-todo",
|
|
34
|
-
"message-square-quote",
|
|
35
|
-
"paperclip",
|
|
36
|
-
"star",
|
|
37
|
-
"star-off",
|
|
38
|
-
"trash-2",
|
|
39
|
-
]);
|
|
40
|
-
const CODEX_FILE_ICON_KINDS = [
|
|
41
|
-
"artifact-document",
|
|
42
|
-
"build",
|
|
43
|
-
"code",
|
|
44
|
-
"cplusplus",
|
|
45
|
-
"css",
|
|
46
|
-
"document",
|
|
47
|
-
"file",
|
|
48
|
-
"folder",
|
|
49
|
-
"hashes",
|
|
50
|
-
"html",
|
|
51
|
-
"image",
|
|
52
|
-
"java",
|
|
53
|
-
"javascript",
|
|
54
|
-
"json",
|
|
55
|
-
"notebook",
|
|
56
|
-
"pdf",
|
|
57
|
-
"php",
|
|
58
|
-
"presentation",
|
|
59
|
-
"python",
|
|
60
|
-
"react",
|
|
61
|
-
"rust",
|
|
62
|
-
"shell",
|
|
63
|
-
"skill",
|
|
64
|
-
"spreadsheet",
|
|
65
|
-
"terminal",
|
|
66
|
-
"toml",
|
|
67
|
-
"typescript",
|
|
68
|
-
];
|
|
69
|
-
|
|
70
|
-
function pickFilesJxa(locale) {
|
|
71
|
-
const prompt = locale === "zh-CN" ? "将文件固定到 CodexNotes" : "Pin files to CodexNotes";
|
|
72
|
-
return String.raw`
|
|
73
|
-
const app = Application.currentApplication();
|
|
74
|
-
app.includeStandardAdditions = true;
|
|
75
|
-
const selected = app.chooseFile({
|
|
76
|
-
withPrompt: ${JSON.stringify(prompt)},
|
|
77
|
-
multipleSelectionsAllowed: true,
|
|
78
|
-
invisibles: false,
|
|
79
|
-
});
|
|
80
|
-
JSON.stringify(selected.map((item) => item.toString()));
|
|
81
|
-
`;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const STATIC_FILES = new Map([
|
|
85
|
-
["/", { file: "index.html", type: "text/html; charset=utf-8" }],
|
|
86
|
-
["/app.js", { file: "app.js", type: "text/javascript; charset=utf-8" }],
|
|
87
|
-
["/styles.css", { file: "styles.css", type: "text/css; charset=utf-8" }],
|
|
88
|
-
["/i18n.mjs", { file: "i18n.mjs", type: "text/javascript; charset=utf-8" }],
|
|
89
|
-
["/file-icons.mjs", { file: "file-icons.mjs", type: "text/javascript; charset=utf-8" }],
|
|
90
|
-
["/todo-drag.mjs", { file: "todo-drag.mjs", type: "text/javascript; charset=utf-8" }],
|
|
91
|
-
...LUCIDE_ICON_NAMES.map((name) => [
|
|
92
|
-
`/icons/${name}.svg`,
|
|
93
|
-
{
|
|
94
|
-
absoluteFile: path.join(LUCIDE_ICON_ROOT, `${name}.svg`),
|
|
95
|
-
type: "image/svg+xml",
|
|
96
|
-
opticalStrokeWidth: THIN_LUCIDE_ICON_NAMES.has(name) ? "1.5" : null,
|
|
97
|
-
},
|
|
98
|
-
]),
|
|
99
|
-
["/icons/circle-check.svg", { file: "codex-task.svg", type: "image/svg+xml" }],
|
|
100
|
-
["/icons/attachment.svg", { file: "codex-attachment.svg", type: "image/svg+xml" }],
|
|
101
|
-
["/icons/edit-pencil.svg", { file: "codex-pencil.svg", type: "image/svg+xml" }],
|
|
102
|
-
// Extracted from the Codex Summary section action so the Add silhouette and
|
|
103
|
-
// optical weight match the host instead of Lucide's heavier stroked plus.
|
|
104
|
-
["/icons/plus.svg", { file: "codex-plus.svg", type: "image/svg+xml" }],
|
|
105
|
-
...CODEX_FILE_ICON_KINDS.map((kind) => [
|
|
106
|
-
`/icons/file-${kind}.svg`,
|
|
107
|
-
{ file: `codex-${kind}.svg`, type: "image/svg+xml" },
|
|
108
|
-
]),
|
|
109
|
-
]);
|
|
110
|
-
|
|
111
|
-
function sendJson(response, status, payload) {
|
|
112
|
-
const body = JSON.stringify(payload);
|
|
113
|
-
response.writeHead(status, {
|
|
114
|
-
"Content-Type": "application/json; charset=utf-8",
|
|
115
|
-
"Content-Length": Buffer.byteLength(body),
|
|
116
|
-
"Cache-Control": "no-store",
|
|
117
|
-
});
|
|
118
|
-
response.end(body);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function sendEmpty(response, status = 204) {
|
|
122
|
-
response.writeHead(status, { "Cache-Control": "no-store" });
|
|
123
|
-
response.end();
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
async function readJson(request) {
|
|
127
|
-
const contentType = request.headers["content-type"] ?? "";
|
|
128
|
-
if (!contentType.toLowerCase().startsWith("application/json")) {
|
|
129
|
-
throw new ProductError("Content-Type must be application/json", {
|
|
130
|
-
code: "INVALID_CONTENT_TYPE",
|
|
131
|
-
status: 415,
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
const chunks = [];
|
|
135
|
-
let size = 0;
|
|
136
|
-
for await (const chunk of request) {
|
|
137
|
-
size += chunk.length;
|
|
138
|
-
if (size > MAX_BODY_BYTES) {
|
|
139
|
-
throw new ProductError("Request body is too large", {
|
|
140
|
-
code: "BODY_TOO_LARGE",
|
|
141
|
-
status: 413,
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
chunks.push(chunk);
|
|
145
|
-
}
|
|
146
|
-
if (size === 0) return {};
|
|
147
|
-
let parsed;
|
|
148
|
-
try {
|
|
149
|
-
parsed = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
150
|
-
} catch {
|
|
151
|
-
throw new ProductError("Request body must be valid JSON", { code: "INVALID_JSON" });
|
|
152
|
-
}
|
|
153
|
-
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
154
|
-
throw new ProductError("Request body must be a JSON object", { code: "INVALID_BODY" });
|
|
155
|
-
}
|
|
156
|
-
return parsed;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function idFrom(pathname, prefix, suffix = "") {
|
|
160
|
-
if (!pathname.startsWith(prefix) || !pathname.endsWith(suffix)) return null;
|
|
161
|
-
const raw = pathname.slice(prefix.length, suffix ? -suffix.length : undefined);
|
|
162
|
-
if (!raw || raw.includes("/")) return null;
|
|
163
|
-
let decoded;
|
|
164
|
-
try {
|
|
165
|
-
decoded = decodeURIComponent(raw);
|
|
166
|
-
} catch {
|
|
167
|
-
throw new ProductError("Route id must use valid URL encoding", { code: "INVALID_ID" });
|
|
168
|
-
}
|
|
169
|
-
if (!decoded || decoded.includes("/")) {
|
|
170
|
-
throw new ProductError("Route id is invalid", { code: "INVALID_ID" });
|
|
171
|
-
}
|
|
172
|
-
return decoded;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function normalizedLocale(value) {
|
|
176
|
-
return String(value ?? "").toLowerCase().startsWith("zh") ? "zh-CN" : "en";
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function requestLocale(request) {
|
|
180
|
-
return normalizedLocale(request.headers["accept-language"]?.split(",", 1)[0]);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
async function chooseFiles({ locale = "en" } = {}) {
|
|
184
|
-
if (process.platform === "win32") return chooseFilesWindows(locale);
|
|
185
|
-
if (process.platform !== "darwin") {
|
|
186
|
-
throw new ProductError("The system picker is currently available on macOS", {
|
|
187
|
-
code: "PICKER_UNAVAILABLE",
|
|
188
|
-
status: 501,
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
try {
|
|
192
|
-
const { stdout } = await execFileAsync("osascript", ["-l", "JavaScript", "-e", pickFilesJxa(locale)], {
|
|
193
|
-
maxBuffer: 1024 * 1024,
|
|
194
|
-
});
|
|
195
|
-
const paths = JSON.parse(stdout.trim() || "[]");
|
|
196
|
-
return { cancelled: false, paths };
|
|
197
|
-
} catch (error) {
|
|
198
|
-
if (String(error.stderr ?? error.message).includes("-128")) {
|
|
199
|
-
return { cancelled: true, paths: [] };
|
|
200
|
-
}
|
|
201
|
-
throw new ProductError("The system file picker could not be opened", {
|
|
202
|
-
code: "PICKER_FAILED",
|
|
203
|
-
status: 503,
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// Windows 系统文件选择器:WinForms OpenFileDialog(STA 必需)。
|
|
209
|
-
async function chooseFilesWindows(locale) {
|
|
210
|
-
const psSingleQuoted = (value) => "'" + String(value).replaceAll("'", "''") + "'";
|
|
211
|
-
const script = [
|
|
212
|
-
"[Console]::OutputEncoding = [System.Text.Encoding]::UTF8",
|
|
213
|
-
"Add-Type -AssemblyName System.Windows.Forms | Out-Null",
|
|
214
|
-
"$dialog = New-Object System.Windows.Forms.OpenFileDialog",
|
|
215
|
-
"$dialog.Title = " + psSingleQuoted(locale === "zh-CN" ? "选择要添加的文件" : "Choose files to add"),
|
|
216
|
-
"$dialog.Multiselect = $true",
|
|
217
|
-
"if ($dialog.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) { $dialog.FileNames | ForEach-Object { [Console]::Out.WriteLine($_) } } else { [Console]::Out.Write('CANCELED') }",
|
|
218
|
-
].join("; ");
|
|
219
|
-
try {
|
|
220
|
-
const { stdout } = await execFileAsync("powershell.exe", ["-NoProfile", "-STA", "-Command", script], {
|
|
221
|
-
maxBuffer: 1024 * 1024,
|
|
222
|
-
});
|
|
223
|
-
const output = String(stdout).trim();
|
|
224
|
-
if (output === "CANCELED") return { cancelled: true, paths: [] };
|
|
225
|
-
const paths = output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
226
|
-
return { cancelled: false, paths };
|
|
227
|
-
} catch (error) {
|
|
228
|
-
throw new ProductError("The system file picker could not be opened", {
|
|
229
|
-
code: "PICKER_FAILED",
|
|
230
|
-
status: 503,
|
|
231
|
-
});
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
export async function revealFileInSystemManager(filePath, {
|
|
236
|
-
platform = process.platform,
|
|
237
|
-
executeFile = execFileAsync,
|
|
238
|
-
} = {}) {
|
|
239
|
-
let command;
|
|
240
|
-
let args;
|
|
241
|
-
if (platform === "darwin") {
|
|
242
|
-
command = "/usr/bin/open";
|
|
243
|
-
args = ["-R", filePath];
|
|
244
|
-
} else if (platform === "win32") {
|
|
245
|
-
command = "explorer.exe";
|
|
246
|
-
args = ["/select,", filePath];
|
|
247
|
-
} else {
|
|
248
|
-
throw new ProductError("Reveal in the system file manager is not available on this platform", {
|
|
249
|
-
code: "REVEAL_UNAVAILABLE",
|
|
250
|
-
status: 501,
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
try {
|
|
254
|
-
await executeFile(command, args);
|
|
255
|
-
return { revealed: true };
|
|
256
|
-
} catch (error) {
|
|
257
|
-
throw new ProductError("The file could not be revealed in the system file manager", {
|
|
258
|
-
code: error.code ?? "REVEAL_FAILED",
|
|
259
|
-
status: 503,
|
|
260
|
-
});
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
function formatHost(host) {
|
|
265
|
-
return host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
function applyCodexEmbedCors(request, response) {
|
|
269
|
-
if (request.headers.origin !== CODEX_EMBED_ORIGIN) return false;
|
|
270
|
-
response.setHeader("Access-Control-Allow-Origin", CODEX_EMBED_ORIGIN);
|
|
271
|
-
response.setHeader("Access-Control-Allow-Methods", "GET, HEAD, POST, PATCH, DELETE, OPTIONS");
|
|
272
|
-
response.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
273
|
-
response.setHeader("Access-Control-Allow-Private-Network", "true");
|
|
274
|
-
response.setHeader("Vary", "Origin");
|
|
275
|
-
return true;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
function validateMutationOrigin(request, address, host) {
|
|
279
|
-
const expectedHost = formatHost(host);
|
|
280
|
-
const expectedOrigin = `http://${expectedHost}:${address.port}`;
|
|
281
|
-
const expectedHostHeader = new URL(expectedOrigin).host;
|
|
282
|
-
if (request.headers.host !== expectedHostHeader) {
|
|
283
|
-
throw new ProductError("Mutation requests are restricted to the exact bound loopback Host", {
|
|
284
|
-
code: "HOST_NOT_ALLOWED",
|
|
285
|
-
status: 403,
|
|
286
|
-
});
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
const origin = request.headers.origin;
|
|
290
|
-
if (!origin) return;
|
|
291
|
-
if (origin === CODEX_EMBED_ORIGIN) return;
|
|
292
|
-
let parsed;
|
|
293
|
-
try {
|
|
294
|
-
parsed = new URL(origin);
|
|
295
|
-
} catch {
|
|
296
|
-
throw new ProductError("Request origin is invalid", { code: "INVALID_ORIGIN", status: 403 });
|
|
297
|
-
}
|
|
298
|
-
if (parsed.origin !== expectedOrigin) {
|
|
299
|
-
throw new ProductError("Mutation requests are restricted to this local CodexNotes origin", {
|
|
300
|
-
code: "ORIGIN_NOT_ALLOWED",
|
|
301
|
-
status: 403,
|
|
302
|
-
});
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
export function createCodexNotesServer({
|
|
307
|
-
product,
|
|
308
|
-
host = "127.0.0.1",
|
|
309
|
-
port = 47832,
|
|
310
|
-
webRoot = DEFAULT_WEB_ROOT,
|
|
311
|
-
filePicker = chooseFiles,
|
|
312
|
-
fileRevealer = revealFileInSystemManager,
|
|
313
|
-
instanceId = process.env.MINECODEX_FEATURE_INSTANCE_ID ?? "unconfigured",
|
|
314
|
-
} = {}) {
|
|
315
|
-
if (!product) throw new Error("product is required");
|
|
316
|
-
if (!LOOPBACK_HOSTS.has(host)) {
|
|
317
|
-
throw new Error("CodexNotes only supports loopback hosts (127.0.0.1, localhost, or ::1)");
|
|
318
|
-
}
|
|
319
|
-
const eventClients = new Set();
|
|
320
|
-
let heartbeat = null;
|
|
321
|
-
|
|
322
|
-
function broadcast(change) {
|
|
323
|
-
const payload = `id: ${change.revision}\nevent: change\ndata: ${JSON.stringify(change)}\n\n`;
|
|
324
|
-
for (const client of eventClients) client.write(payload);
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
product.on("change", broadcast);
|
|
328
|
-
|
|
329
|
-
const server = createServer(async (request, response) => {
|
|
330
|
-
try {
|
|
331
|
-
applyCodexEmbedCors(request, response);
|
|
332
|
-
const url = new URL(request.url ?? "/", `http://${formatHost(host)}:${port}`);
|
|
333
|
-
const { pathname } = url;
|
|
334
|
-
const method = request.method ?? "GET";
|
|
335
|
-
if (!["GET", "HEAD"].includes(method)) {
|
|
336
|
-
validateMutationOrigin(request, server.address(), host);
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
if (method === "OPTIONS") {
|
|
340
|
-
response.writeHead(204, { "Access-Control-Max-Age": "600" });
|
|
341
|
-
response.end();
|
|
342
|
-
return;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
if (method === "GET" && pathname === "/api/health") {
|
|
346
|
-
sendJson(response, 200, {
|
|
347
|
-
ok: true,
|
|
348
|
-
service: "notes",
|
|
349
|
-
protocolVersion: 1,
|
|
350
|
-
instanceId,
|
|
351
|
-
revision: product.revision,
|
|
352
|
-
});
|
|
353
|
-
return;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
if (method === "GET" && pathname === "/api/state") {
|
|
357
|
-
const surface = url.searchParams.get("surface") ?? "all";
|
|
358
|
-
sendJson(response, 200, await product.getState({
|
|
359
|
-
surface,
|
|
360
|
-
kind: url.searchParams.get("kind"),
|
|
361
|
-
id: url.searchParams.get("id"),
|
|
362
|
-
}));
|
|
363
|
-
return;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
if (method === "GET" && pathname === "/api/events") {
|
|
367
|
-
response.writeHead(200, {
|
|
368
|
-
"Content-Type": "text/event-stream; charset=utf-8",
|
|
369
|
-
"Cache-Control": "no-cache, no-transform",
|
|
370
|
-
Connection: "keep-alive",
|
|
371
|
-
"X-Accel-Buffering": "no",
|
|
372
|
-
});
|
|
373
|
-
response.write(`event: ready\ndata: ${JSON.stringify({ revision: product.revision })}\n\n`);
|
|
374
|
-
eventClients.add(response);
|
|
375
|
-
request.on("close", () => eventClients.delete(response));
|
|
376
|
-
return;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
if (method === "POST" && pathname === "/api/todos") {
|
|
380
|
-
sendJson(response, 201, product.createTodo(await readJson(request)));
|
|
381
|
-
return;
|
|
382
|
-
}
|
|
383
|
-
if (method === "POST" && pathname === "/api/todos/reorder") {
|
|
384
|
-
const body = await readJson(request);
|
|
385
|
-
sendJson(response, 200, product.reorderTodos(body.ids));
|
|
386
|
-
return;
|
|
387
|
-
}
|
|
388
|
-
if (method === "DELETE" && pathname === "/api/todos/completed") {
|
|
389
|
-
sendJson(response, 200, product.clearCompletedTodos());
|
|
390
|
-
return;
|
|
391
|
-
}
|
|
392
|
-
const todoId = idFrom(pathname, "/api/todos/");
|
|
393
|
-
if (todoId && method === "PATCH") {
|
|
394
|
-
sendJson(response, 200, product.updateTodo(todoId, await readJson(request)));
|
|
395
|
-
return;
|
|
396
|
-
}
|
|
397
|
-
if (todoId && method === "DELETE") {
|
|
398
|
-
product.deleteTodo(todoId);
|
|
399
|
-
sendEmpty(response);
|
|
400
|
-
return;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
if (method === "POST" && pathname === "/api/prompts") {
|
|
404
|
-
sendJson(response, 201, product.createPrompt(await readJson(request)));
|
|
405
|
-
return;
|
|
406
|
-
}
|
|
407
|
-
const promptUseId = idFrom(pathname, "/api/prompts/", "/use");
|
|
408
|
-
if (promptUseId && method === "POST") {
|
|
409
|
-
sendJson(response, 200, product.markPromptUsed(promptUseId, await readJson(request)));
|
|
410
|
-
return;
|
|
411
|
-
}
|
|
412
|
-
const promptId = idFrom(pathname, "/api/prompts/");
|
|
413
|
-
if (promptId && method === "PATCH") {
|
|
414
|
-
sendJson(response, 200, product.updatePrompt(promptId, await readJson(request)));
|
|
415
|
-
return;
|
|
416
|
-
}
|
|
417
|
-
if (promptId && method === "DELETE") {
|
|
418
|
-
product.deletePrompt(promptId);
|
|
419
|
-
sendEmpty(response);
|
|
420
|
-
return;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
if (method === "POST" && pathname === "/api/files") {
|
|
424
|
-
sendJson(response, 200, await product.addFiles(await readJson(request)));
|
|
425
|
-
return;
|
|
426
|
-
}
|
|
427
|
-
if (method === "POST" && pathname === "/api/files/pick") {
|
|
428
|
-
const picked = await filePicker({ locale: requestLocale(request) });
|
|
429
|
-
if (picked.cancelled) {
|
|
430
|
-
sendJson(response, 200, { cancelled: true, added: [], existing: [], rejected: [], revision: product.revision });
|
|
431
|
-
} else {
|
|
432
|
-
sendJson(response, 200, { cancelled: false, ...await product.addFiles({ paths: picked.paths }) });
|
|
433
|
-
}
|
|
434
|
-
return;
|
|
435
|
-
}
|
|
436
|
-
const fileUseId = idFrom(pathname, "/api/files/", "/use");
|
|
437
|
-
if (fileUseId && method === "POST") {
|
|
438
|
-
sendJson(response, 200, await product.markFileUsed(fileUseId, await readJson(request)));
|
|
439
|
-
return;
|
|
440
|
-
}
|
|
441
|
-
const fileRevealId = idFrom(pathname, "/api/files/", "/reveal");
|
|
442
|
-
if (fileRevealId && method === "POST") {
|
|
443
|
-
const file = product.getFile(fileRevealId);
|
|
444
|
-
await fileRevealer(file.path);
|
|
445
|
-
sendJson(response, 200, { revealed: true });
|
|
446
|
-
return;
|
|
447
|
-
}
|
|
448
|
-
const filePreviewId = idFrom(pathname, "/api/files/", "/preview");
|
|
449
|
-
if (filePreviewId && (method === "GET" || method === "HEAD")) {
|
|
450
|
-
const file = product.getFile(filePreviewId);
|
|
451
|
-
if (!file.mimeType.startsWith("image/")) {
|
|
452
|
-
throw new ProductError("Only pinned image files have previews", {
|
|
453
|
-
code: "PREVIEW_UNAVAILABLE",
|
|
454
|
-
status: 415,
|
|
455
|
-
});
|
|
456
|
-
}
|
|
457
|
-
let info;
|
|
458
|
-
try {
|
|
459
|
-
info = await stat(file.path);
|
|
460
|
-
} catch (error) {
|
|
461
|
-
if (error?.code === "ENOENT" || error?.code === "ENOTDIR") {
|
|
462
|
-
product.removeMissingFile(file.id);
|
|
463
|
-
throw new ProductError("The source image no longer exists", {
|
|
464
|
-
code: "FILE_MISSING",
|
|
465
|
-
status: 410,
|
|
466
|
-
});
|
|
467
|
-
}
|
|
468
|
-
throw error;
|
|
469
|
-
}
|
|
470
|
-
if (!info.isFile()) {
|
|
471
|
-
product.removeMissingFile(file.id);
|
|
472
|
-
throw new ProductError("The source image no longer exists", {
|
|
473
|
-
code: "FILE_MISSING",
|
|
474
|
-
status: 410,
|
|
475
|
-
});
|
|
476
|
-
}
|
|
477
|
-
if (info.size > MAX_PREVIEW_BYTES) {
|
|
478
|
-
throw new ProductError("The source image is too large to preview", {
|
|
479
|
-
code: "PREVIEW_TOO_LARGE",
|
|
480
|
-
status: 413,
|
|
481
|
-
details: { maxBytes: MAX_PREVIEW_BYTES },
|
|
482
|
-
});
|
|
483
|
-
}
|
|
484
|
-
const etag = `W/"${info.size.toString(16)}-${Math.trunc(info.mtimeMs).toString(16)}"`;
|
|
485
|
-
if (request.headers["if-none-match"] === etag) {
|
|
486
|
-
response.writeHead(304, {
|
|
487
|
-
ETag: etag,
|
|
488
|
-
"Cache-Control": "private, max-age=0, must-revalidate",
|
|
489
|
-
});
|
|
490
|
-
response.end();
|
|
491
|
-
return;
|
|
492
|
-
}
|
|
493
|
-
response.writeHead(200, {
|
|
494
|
-
"Content-Type": file.mimeType,
|
|
495
|
-
"Content-Length": info.size,
|
|
496
|
-
"Cache-Control": "private, max-age=0, must-revalidate",
|
|
497
|
-
ETag: etag,
|
|
498
|
-
"Last-Modified": info.mtime.toUTCString(),
|
|
499
|
-
"Content-Security-Policy": "default-src 'none'; sandbox",
|
|
500
|
-
"X-Content-Type-Options": "nosniff",
|
|
501
|
-
"Referrer-Policy": "no-referrer",
|
|
502
|
-
});
|
|
503
|
-
if (method === "HEAD") response.end();
|
|
504
|
-
else await pipeline(createReadStream(file.path), response);
|
|
505
|
-
return;
|
|
506
|
-
}
|
|
507
|
-
const fileId = idFrom(pathname, "/api/files/");
|
|
508
|
-
if (fileId && method === "DELETE") {
|
|
509
|
-
product.deleteFile(fileId);
|
|
510
|
-
sendEmpty(response);
|
|
511
|
-
return;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
const staticEntry = STATIC_FILES.get(pathname);
|
|
515
|
-
if (staticEntry && (method === "GET" || method === "HEAD")) {
|
|
516
|
-
const source = await readFile(staticEntry.absoluteFile ?? path.join(webRoot, staticEntry.file));
|
|
517
|
-
const body = staticEntry.opticalStrokeWidth
|
|
518
|
-
? Buffer.from(source.toString("utf8").replace(
|
|
519
|
-
/stroke-width="[^"]+"/,
|
|
520
|
-
`stroke-width="${staticEntry.opticalStrokeWidth}"`,
|
|
521
|
-
))
|
|
522
|
-
: source;
|
|
523
|
-
response.writeHead(200, {
|
|
524
|
-
"Content-Type": staticEntry.type,
|
|
525
|
-
"Content-Length": body.length,
|
|
526
|
-
"Cache-Control": "no-cache",
|
|
527
|
-
"Content-Security-Policy": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'",
|
|
528
|
-
"X-Content-Type-Options": "nosniff",
|
|
529
|
-
"Referrer-Policy": "no-referrer",
|
|
530
|
-
});
|
|
531
|
-
if (method === "HEAD") response.end();
|
|
532
|
-
else response.end(body);
|
|
533
|
-
return;
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
sendJson(response, 404, { error: { code: "NOT_FOUND", message: "Route was not found" } });
|
|
537
|
-
} catch (error) {
|
|
538
|
-
const status = error instanceof ProductError ? error.status : 500;
|
|
539
|
-
const code = error instanceof ProductError ? error.code : "INTERNAL_ERROR";
|
|
540
|
-
const message = error instanceof ProductError ? error.message : "CodexNotes could not complete the request";
|
|
541
|
-
if (status >= 500) console.error(error);
|
|
542
|
-
if (!response.headersSent) {
|
|
543
|
-
sendJson(response, status, { error: { code, message, details: error.details ?? null } });
|
|
544
|
-
} else {
|
|
545
|
-
response.destroy();
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
});
|
|
549
|
-
|
|
550
|
-
return {
|
|
551
|
-
server,
|
|
552
|
-
get origin() {
|
|
553
|
-
const address = server.address();
|
|
554
|
-
if (!address || typeof address === "string") return null;
|
|
555
|
-
return `http://${formatHost(host)}:${address.port}`;
|
|
556
|
-
},
|
|
557
|
-
async listen() {
|
|
558
|
-
await new Promise((resolve, reject) => {
|
|
559
|
-
server.once("error", reject);
|
|
560
|
-
server.listen(port, host, () => {
|
|
561
|
-
server.off("error", reject);
|
|
562
|
-
resolve();
|
|
563
|
-
});
|
|
564
|
-
});
|
|
565
|
-
heartbeat = setInterval(() => {
|
|
566
|
-
for (const client of eventClients) client.write(": heartbeat\n\n");
|
|
567
|
-
}, 20_000);
|
|
568
|
-
heartbeat.unref();
|
|
569
|
-
return this;
|
|
570
|
-
},
|
|
571
|
-
async close() {
|
|
572
|
-
if (heartbeat) clearInterval(heartbeat);
|
|
573
|
-
for (const client of eventClients) client.end();
|
|
574
|
-
eventClients.clear();
|
|
575
|
-
product.off("change", broadcast);
|
|
576
|
-
if (!server.listening) return;
|
|
577
|
-
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
|
|
578
|
-
},
|
|
579
|
-
};
|
|
580
|
-
}
|