pockcode 0.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/build/client/assets/addon-fit-DthTIhi3.js +1 -0
- package/build/client/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/build/client/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/build/client/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/build/client/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/build/client/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/build/client/assets/index-D7-eRInW.css +2 -0
- package/build/client/assets/index-DQn6eSCt.js +31 -0
- package/build/client/assets/xterm-DooSxjI5.js +36 -0
- package/build/client/index.html +27 -0
- package/dist/assets/api.server-D6ONM6ZJ.js +1736 -0
- package/dist/assets/auth.server-B_P8Jm6O.js +87 -0
- package/dist/assets/chat-status-monitor.server-D2VG_D8P.js +136 -0
- package/dist/assets/chats.service-H6m23Fna.js +4684 -0
- package/dist/assets/manager.server-Cru4ZxHo.js +1334 -0
- package/dist/assets/message-schedule-monitor.server-C4NZg62k.js +32 -0
- package/dist/assets/message-schedules.service-Be9t4LDg.js +407 -0
- package/dist/assets/runtime-paths.server-D6rRjGVb.js +32 -0
- package/dist/assets/socket.server-7dC-9Fnw.js +436 -0
- package/dist/pockcode.js +305 -0
- package/package.json +71 -0
- package/prisma/schema.prisma +234 -0
|
@@ -0,0 +1,1736 @@
|
|
|
1
|
+
import { c as readWorkspaceTree, d as readRecordField, f as readStringField, l as HttpError, o as listWorkspaceDirectory, s as readWorkspaceResource, u as readBooleanField } from "./socket.server-7dC-9Fnw.js";
|
|
2
|
+
import { A as listProviderAdapters, B as readNumber, C as createAccount, D as readConnectedAccountLimits, E as listAccounts, F as resolveCodexAccountHome, H as ensureDatabase, I as startCodexMcpOauthLogin, L as updateCodexInstructions, N as readCodexInstructions, P as reloadCodexMcpServerConfig, R as asJsonObject, S as authenticateAccount, T as listAccountModels, U as prisma, V as readString, a as executeMessage, b as updateChat, c as interruptChatRun, f as refreshChat, g as reviewChat, h as respondToServerRequest, i as deleteQueuedChatRun, j as listCodexMcpServerStatuses, k as updateAccount, l as listChats, m as reorderQueuedChatRuns, n as compactChat, o as forkChat, r as createChat, t as archiveChat, u as listMessages, v as steerQueuedChatRun, w as deleteAccount, x as updateQueuedChatRun, z as readBoolean } from "./chats.service-H6m23Fna.js";
|
|
3
|
+
import { a as listMessageSchedules, c as updateMessageSchedule, i as listMessageScheduleRuns, n as createMessageSchedule, r as getMessageSchedule, t as archiveMessageSchedule } from "./message-schedules.service-Be9t4LDg.js";
|
|
4
|
+
import { a as readPluginSetting, c as listPluginRegistrations, d as deleteWorkspaceHistory, f as listWorkspaceHistory, i as restartPlugin, l as readPluginRegistration, n as getPluginStatus, o as readPluginState, p as saveWorkspaceHistory, s as writePluginSetting, t as createPluginContext, u as verifyTelegramBotToken } from "./manager.server-Cru4ZxHo.js";
|
|
5
|
+
import { realpathSync } from "node:fs";
|
|
6
|
+
import { mkdir, readFile, realpath, stat, writeFile } from "node:fs/promises";
|
|
7
|
+
import path, { join } from "node:path";
|
|
8
|
+
import { randomUUID } from "node:crypto";
|
|
9
|
+
import { Prisma } from "@prisma/client";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import { execFile, spawn } from "node:child_process";
|
|
12
|
+
import { promisify } from "node:util";
|
|
13
|
+
import { parse, stringify } from "smol-toml";
|
|
14
|
+
//#region app/server/cloudflared.service.ts
|
|
15
|
+
var execFileAsync$1 = promisify(execFile);
|
|
16
|
+
var temporaryTunnelReadyTimeoutMs = 35e3;
|
|
17
|
+
var maxLogLines = 80;
|
|
18
|
+
var tryCloudflareUrlPattern = /https:\/\/[a-z0-9-]+(?:\.[a-z0-9-]+)*\.trycloudflare\.com/iu;
|
|
19
|
+
var temporaryTunnels = /* @__PURE__ */ new Map();
|
|
20
|
+
async function readCloudflaredStatus() {
|
|
21
|
+
const installation = await readCloudflaredInstallation();
|
|
22
|
+
if (!installation.installed) return {
|
|
23
|
+
installed: false,
|
|
24
|
+
message: installation.message,
|
|
25
|
+
namedTunnels: [],
|
|
26
|
+
temporaryTunnels: listTemporaryTunnels()
|
|
27
|
+
};
|
|
28
|
+
try {
|
|
29
|
+
return {
|
|
30
|
+
installed: true,
|
|
31
|
+
namedTunnels: await listNamedTunnels(),
|
|
32
|
+
temporaryTunnels: listTemporaryTunnels(),
|
|
33
|
+
version: installation.version
|
|
34
|
+
};
|
|
35
|
+
} catch (error) {
|
|
36
|
+
const namedTunnelsAuthRequired = isMissingOriginCertError(error);
|
|
37
|
+
return {
|
|
38
|
+
installed: true,
|
|
39
|
+
namedTunnels: [],
|
|
40
|
+
namedTunnelsAuthRequired,
|
|
41
|
+
namedTunnelsError: namedTunnelsAuthRequired ? "Named tunnels require a cloudflared origin certificate. Run `cloudflared tunnel login` or set TUNNEL_ORIGIN_CERT, then refresh. Temporary tunnels still work without named-tunnel login." : cleanCommandError(error, "Unable to list cloudflared tunnels."),
|
|
42
|
+
temporaryTunnels: listTemporaryTunnels(),
|
|
43
|
+
version: installation.version
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async function startTemporaryCloudflaredTunnel(originUrl) {
|
|
48
|
+
const normalizedOriginUrl = normalizeOriginUrl(originUrl);
|
|
49
|
+
await ensureCloudflaredInstalled();
|
|
50
|
+
const record = {
|
|
51
|
+
child: null,
|
|
52
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
53
|
+
id: randomUUID(),
|
|
54
|
+
logs: [],
|
|
55
|
+
originUrl: normalizedOriginUrl,
|
|
56
|
+
status: "starting",
|
|
57
|
+
stopRequested: false
|
|
58
|
+
};
|
|
59
|
+
temporaryTunnels.set(record.id, record);
|
|
60
|
+
const child = spawn("cloudflared", [
|
|
61
|
+
"tunnel",
|
|
62
|
+
"--url",
|
|
63
|
+
normalizedOriginUrl
|
|
64
|
+
], { stdio: [
|
|
65
|
+
"ignore",
|
|
66
|
+
"pipe",
|
|
67
|
+
"pipe"
|
|
68
|
+
] });
|
|
69
|
+
record.child = child;
|
|
70
|
+
attachTemporaryTunnelListeners(record, child);
|
|
71
|
+
await waitForTemporaryTunnelUrl(record);
|
|
72
|
+
return readCloudflaredStatus();
|
|
73
|
+
}
|
|
74
|
+
async function stopTemporaryCloudflaredTunnel(id) {
|
|
75
|
+
const record = temporaryTunnels.get(id);
|
|
76
|
+
if (!record) throw new HttpError(404, "Temporary tunnel was not found.");
|
|
77
|
+
record.stopRequested = true;
|
|
78
|
+
record.stoppedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
79
|
+
record.status = "stopped";
|
|
80
|
+
appendLog(record, "Stopped by Pockcode.");
|
|
81
|
+
if (record.child && !record.child.killed) {
|
|
82
|
+
record.child.kill("SIGTERM");
|
|
83
|
+
setTimeout(() => {
|
|
84
|
+
if (record.child && !record.child.killed) record.child.kill("SIGKILL");
|
|
85
|
+
}, 2e3).unref();
|
|
86
|
+
} else {
|
|
87
|
+
record.status = "stopped";
|
|
88
|
+
record.child = null;
|
|
89
|
+
}
|
|
90
|
+
return readCloudflaredStatus();
|
|
91
|
+
}
|
|
92
|
+
async function deleteNamedCloudflaredTunnel(identifier) {
|
|
93
|
+
const tunnelIdentifier = normalizeTunnelIdentifier(identifier);
|
|
94
|
+
await ensureCloudflaredInstalled();
|
|
95
|
+
await runCloudflared([
|
|
96
|
+
"tunnel",
|
|
97
|
+
"delete",
|
|
98
|
+
"-f",
|
|
99
|
+
tunnelIdentifier
|
|
100
|
+
], 6e4);
|
|
101
|
+
return readCloudflaredStatus();
|
|
102
|
+
}
|
|
103
|
+
async function readCloudflaredInstallation() {
|
|
104
|
+
try {
|
|
105
|
+
return {
|
|
106
|
+
installed: true,
|
|
107
|
+
version: parseCloudflaredVersion((await runCloudflared(["--version"], 1e4)).stdout)
|
|
108
|
+
};
|
|
109
|
+
} catch (error) {
|
|
110
|
+
return {
|
|
111
|
+
installed: false,
|
|
112
|
+
message: cleanCommandError(error, "cloudflared is not installed or not available on PATH.")
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async function ensureCloudflaredInstalled() {
|
|
117
|
+
const installation = await readCloudflaredInstallation();
|
|
118
|
+
if (!installation.installed) throw new HttpError(400, installation.message);
|
|
119
|
+
}
|
|
120
|
+
async function listNamedTunnels() {
|
|
121
|
+
const result = await runCloudflared([
|
|
122
|
+
"tunnel",
|
|
123
|
+
"list",
|
|
124
|
+
"--output",
|
|
125
|
+
"json"
|
|
126
|
+
], 3e4);
|
|
127
|
+
const parsed = JSON.parse(result.stdout.trim() || "[]");
|
|
128
|
+
return (Array.isArray(parsed) ? parsed : parsed && typeof parsed === "object" && Array.isArray(parsed.tunnels) ? parsed.tunnels : []).map(readNamedTunnel).filter((tunnel) => Boolean(tunnel));
|
|
129
|
+
}
|
|
130
|
+
function readNamedTunnel(value) {
|
|
131
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
132
|
+
const record = value;
|
|
133
|
+
const id = readRecordString(record, "id") ?? readRecordString(record, "uuid");
|
|
134
|
+
if (!id) return null;
|
|
135
|
+
const name = readRecordString(record, "name") ?? id;
|
|
136
|
+
const createdAt = readRecordString(record, "createdAt") ?? readRecordString(record, "created_at");
|
|
137
|
+
const connections = Array.isArray(record.connections) ? record.connections : [];
|
|
138
|
+
const rawStatus = readRecordString(record, "status");
|
|
139
|
+
const status = rawStatus === "active" || rawStatus === "inactive" ? rawStatus : connections.length ? "active" : "inactive";
|
|
140
|
+
return {
|
|
141
|
+
connectionCount: connections.length,
|
|
142
|
+
createdAt,
|
|
143
|
+
id,
|
|
144
|
+
name,
|
|
145
|
+
status
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function attachTemporaryTunnelListeners(record, child) {
|
|
149
|
+
const handleOutput = (chunk) => {
|
|
150
|
+
const text = chunk.toString("utf8");
|
|
151
|
+
appendLog(record, text);
|
|
152
|
+
const match = text.match(tryCloudflareUrlPattern) ?? record.logs.join("\n").match(tryCloudflareUrlPattern);
|
|
153
|
+
if (match?.[0] && !record.publicUrl) {
|
|
154
|
+
record.publicUrl = match[0];
|
|
155
|
+
record.status = "running";
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
child.stdout.on("data", handleOutput);
|
|
159
|
+
child.stderr.on("data", handleOutput);
|
|
160
|
+
child.once("error", (error) => {
|
|
161
|
+
appendLog(record, cleanCommandError(error, "cloudflared failed to start."));
|
|
162
|
+
record.child = null;
|
|
163
|
+
record.exitCode = null;
|
|
164
|
+
record.status = "exited";
|
|
165
|
+
record.stoppedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
166
|
+
});
|
|
167
|
+
child.once("exit", (code, signal) => {
|
|
168
|
+
record.child = null;
|
|
169
|
+
record.exitCode = code;
|
|
170
|
+
record.signal = signal;
|
|
171
|
+
record.status = record.stopRequested ? "stopped" : "exited";
|
|
172
|
+
record.stoppedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
async function waitForTemporaryTunnelUrl(record) {
|
|
176
|
+
const startedAt = Date.now();
|
|
177
|
+
while (Date.now() - startedAt < temporaryTunnelReadyTimeoutMs) {
|
|
178
|
+
if (record.publicUrl) return;
|
|
179
|
+
if (record.status === "exited" || record.status === "stopped") throw new HttpError(400, lastMeaningfulLog(record) ?? "cloudflared exited before creating a temporary tunnel.");
|
|
180
|
+
await delay(100);
|
|
181
|
+
}
|
|
182
|
+
await stopTemporaryCloudflaredTunnel(record.id);
|
|
183
|
+
throw new HttpError(504, "Timed out waiting for cloudflared to publish the temporary tunnel URL.");
|
|
184
|
+
}
|
|
185
|
+
async function runCloudflared(args, timeout) {
|
|
186
|
+
try {
|
|
187
|
+
return await execFileAsync$1("cloudflared", args, {
|
|
188
|
+
maxBuffer: 2 * 1024 * 1024,
|
|
189
|
+
timeout
|
|
190
|
+
});
|
|
191
|
+
} catch (error) {
|
|
192
|
+
throw new HttpError(400, cleanCommandError(error, "cloudflared command failed."));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function listTemporaryTunnels() {
|
|
196
|
+
return Array.from(temporaryTunnels.values()).map(serializeTemporaryTunnel).sort((left, right) => Date.parse(right.createdAt) - Date.parse(left.createdAt));
|
|
197
|
+
}
|
|
198
|
+
function serializeTemporaryTunnel(record) {
|
|
199
|
+
return {
|
|
200
|
+
createdAt: record.createdAt,
|
|
201
|
+
exitCode: record.exitCode,
|
|
202
|
+
id: record.id,
|
|
203
|
+
logs: record.logs.slice(-12),
|
|
204
|
+
originUrl: record.originUrl,
|
|
205
|
+
publicUrl: record.publicUrl,
|
|
206
|
+
signal: record.signal,
|
|
207
|
+
status: record.status,
|
|
208
|
+
stoppedAt: record.stoppedAt
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
function appendLog(record, text) {
|
|
212
|
+
const lines = text.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean);
|
|
213
|
+
record.logs.push(...lines);
|
|
214
|
+
if (record.logs.length > maxLogLines) record.logs.splice(0, record.logs.length - maxLogLines);
|
|
215
|
+
}
|
|
216
|
+
function lastMeaningfulLog(record) {
|
|
217
|
+
return record.logs.slice().reverse().find((line) => line.trim()) ?? null;
|
|
218
|
+
}
|
|
219
|
+
function normalizeOriginUrl(value) {
|
|
220
|
+
let parsed;
|
|
221
|
+
try {
|
|
222
|
+
parsed = new URL(value.trim());
|
|
223
|
+
} catch {
|
|
224
|
+
throw new HttpError(400, "url must be a valid http or https URL.");
|
|
225
|
+
}
|
|
226
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new HttpError(400, "url must use http or https.");
|
|
227
|
+
if (!parsed.hostname) throw new HttpError(400, "url must include a host.");
|
|
228
|
+
return parsed.toString();
|
|
229
|
+
}
|
|
230
|
+
function normalizeTunnelIdentifier(value) {
|
|
231
|
+
const identifier = value.trim();
|
|
232
|
+
if (!identifier) throw new HttpError(400, "Tunnel id is required.");
|
|
233
|
+
if (identifier.length > 200 || /[\r\n]/u.test(identifier)) throw new HttpError(400, "Tunnel id is invalid.");
|
|
234
|
+
return identifier;
|
|
235
|
+
}
|
|
236
|
+
function parseCloudflaredVersion(output) {
|
|
237
|
+
return output.trim().replace(/^cloudflared\s+version\s+/iu, "") || "cloudflared";
|
|
238
|
+
}
|
|
239
|
+
function readRecordString(record, key) {
|
|
240
|
+
const value = record[key];
|
|
241
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
242
|
+
}
|
|
243
|
+
function cleanCommandError(error, fallback) {
|
|
244
|
+
if ((error && typeof error === "object" ? error : {}).code === "ENOENT") return "cloudflared is not installed or not available on PATH.";
|
|
245
|
+
return formatCommandMessage(commandErrorText(error).replace(/^Command failed: cloudflared[^\n]*\n?/iu, "").trim()) || fallback;
|
|
246
|
+
}
|
|
247
|
+
function isMissingOriginCertError(error) {
|
|
248
|
+
const text = commandErrorText(error).toLowerCase();
|
|
249
|
+
return text.includes("origin certificate") || text.includes("origincert") || text.includes("tunnel_origin_cert");
|
|
250
|
+
}
|
|
251
|
+
function commandErrorText(error) {
|
|
252
|
+
const record = error && typeof error === "object" ? error : {};
|
|
253
|
+
const stderr = typeof record.stderr === "string" ? record.stderr : "";
|
|
254
|
+
const stdout = typeof record.stdout === "string" ? record.stdout : "";
|
|
255
|
+
const message = error instanceof Error ? error.message : "";
|
|
256
|
+
return stderr || stdout || message;
|
|
257
|
+
}
|
|
258
|
+
function formatCommandMessage(text) {
|
|
259
|
+
const messages = text.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean).map((line) => {
|
|
260
|
+
try {
|
|
261
|
+
const parsed = JSON.parse(line);
|
|
262
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
263
|
+
const message = parsed.message;
|
|
264
|
+
if (typeof message === "string" && message.trim()) return message.trim();
|
|
265
|
+
}
|
|
266
|
+
} catch {
|
|
267
|
+
return line;
|
|
268
|
+
}
|
|
269
|
+
return line;
|
|
270
|
+
});
|
|
271
|
+
return messages.filter((line, index) => messages.indexOf(line) === index).join("\n");
|
|
272
|
+
}
|
|
273
|
+
function delay(ms) {
|
|
274
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
275
|
+
}
|
|
276
|
+
//#endregion
|
|
277
|
+
//#region app/server/git.service.ts
|
|
278
|
+
var execFileAsync = promisify(execFile);
|
|
279
|
+
var workspaceRoot = realpathSync(homedir());
|
|
280
|
+
async function readGitStatus(inputPath) {
|
|
281
|
+
const cwd = await resolveWorkspacePath(inputPath);
|
|
282
|
+
if (!await isGitRepository(cwd)) return {
|
|
283
|
+
ahead: 0,
|
|
284
|
+
behind: 0,
|
|
285
|
+
branch: "No repository",
|
|
286
|
+
changes: [],
|
|
287
|
+
commits: [],
|
|
288
|
+
isRepository: false,
|
|
289
|
+
message: "This workspace is not initialized as a Git repository."
|
|
290
|
+
};
|
|
291
|
+
const status = await runGit(cwd, [
|
|
292
|
+
"status",
|
|
293
|
+
"--short",
|
|
294
|
+
"--branch"
|
|
295
|
+
]);
|
|
296
|
+
const commits = await readGitLog(cwd);
|
|
297
|
+
return {
|
|
298
|
+
...parseGitStatus(status.stdout),
|
|
299
|
+
commits,
|
|
300
|
+
isRepository: true
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
async function initGitRepository(inputPath) {
|
|
304
|
+
const cwd = await resolveWorkspacePath(inputPath);
|
|
305
|
+
await runGit(cwd, ["init"]);
|
|
306
|
+
return readGitStatus(cwd);
|
|
307
|
+
}
|
|
308
|
+
async function stageGitPaths(inputPath, paths) {
|
|
309
|
+
const cwd = await resolveWorkspacePath(inputPath);
|
|
310
|
+
await ensureGitRepository(cwd);
|
|
311
|
+
await runGit(cwd, [
|
|
312
|
+
"add",
|
|
313
|
+
"--",
|
|
314
|
+
...paths.length ? paths : ["."]
|
|
315
|
+
]);
|
|
316
|
+
return readGitStatus(cwd);
|
|
317
|
+
}
|
|
318
|
+
async function unstageGitPaths(inputPath, paths) {
|
|
319
|
+
const cwd = await resolveWorkspacePath(inputPath);
|
|
320
|
+
await ensureGitRepository(cwd);
|
|
321
|
+
await runGit(cwd, [
|
|
322
|
+
"restore",
|
|
323
|
+
"--staged",
|
|
324
|
+
"--",
|
|
325
|
+
...paths.length ? paths : ["."]
|
|
326
|
+
]);
|
|
327
|
+
return readGitStatus(cwd);
|
|
328
|
+
}
|
|
329
|
+
async function discardGitPaths(inputPath, paths) {
|
|
330
|
+
const cwd = await resolveWorkspacePath(inputPath);
|
|
331
|
+
await ensureGitRepository(cwd);
|
|
332
|
+
const targets = paths.length ? paths : ["."];
|
|
333
|
+
await runGit(cwd, [
|
|
334
|
+
"restore",
|
|
335
|
+
"--staged",
|
|
336
|
+
"--worktree",
|
|
337
|
+
"--",
|
|
338
|
+
...targets
|
|
339
|
+
]).catch(() => void 0);
|
|
340
|
+
await runGit(cwd, [
|
|
341
|
+
"clean",
|
|
342
|
+
"-f",
|
|
343
|
+
"--",
|
|
344
|
+
...targets
|
|
345
|
+
]).catch(() => void 0);
|
|
346
|
+
return readGitStatus(cwd);
|
|
347
|
+
}
|
|
348
|
+
async function commitGitChanges(inputPath, message) {
|
|
349
|
+
const cwd = await resolveWorkspacePath(inputPath);
|
|
350
|
+
await ensureGitRepository(cwd);
|
|
351
|
+
if (!message.trim()) throw new HttpError(400, "Commit message is required.");
|
|
352
|
+
await runGit(cwd, [
|
|
353
|
+
"commit",
|
|
354
|
+
"-m",
|
|
355
|
+
message.trim()
|
|
356
|
+
]);
|
|
357
|
+
return readGitStatus(cwd);
|
|
358
|
+
}
|
|
359
|
+
async function pullGitRepository(inputPath) {
|
|
360
|
+
const cwd = await resolveWorkspacePath(inputPath);
|
|
361
|
+
await ensureGitRepository(cwd);
|
|
362
|
+
await runGit(cwd, ["pull", "--ff-only"]);
|
|
363
|
+
return readGitStatus(cwd);
|
|
364
|
+
}
|
|
365
|
+
async function pushGitRepository(inputPath) {
|
|
366
|
+
const cwd = await resolveWorkspacePath(inputPath);
|
|
367
|
+
await ensureGitRepository(cwd);
|
|
368
|
+
await runGit(cwd, ["push"]);
|
|
369
|
+
return readGitStatus(cwd);
|
|
370
|
+
}
|
|
371
|
+
async function ensureGitRepository(cwd) {
|
|
372
|
+
if (!await isGitRepository(cwd)) throw new HttpError(400, "This workspace is not initialized as a Git repository.");
|
|
373
|
+
}
|
|
374
|
+
async function isGitRepository(cwd) {
|
|
375
|
+
try {
|
|
376
|
+
return (await runGit(cwd, ["rev-parse", "--is-inside-work-tree"])).stdout.trim() === "true";
|
|
377
|
+
} catch {
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
function parseGitStatus(output) {
|
|
382
|
+
const lines = output.split(/\r?\n/).filter(Boolean);
|
|
383
|
+
return {
|
|
384
|
+
...parseBranchLine(lines[0]?.startsWith("## ") ? lines.shift() ?? "" : ""),
|
|
385
|
+
changes: lines.map(parseStatusLine).filter((change) => Boolean(change))
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
function parseBranchLine(line) {
|
|
389
|
+
const [left, tracking = ""] = line.replace(/^##\s+/, "").split("...");
|
|
390
|
+
const branch = left || "HEAD";
|
|
391
|
+
const upstream = tracking.replace(/\s+\[.*\]$/, "") || void 0;
|
|
392
|
+
return {
|
|
393
|
+
ahead: Number.parseInt(tracking.match(/ahead (\d+)/)?.[1] ?? "0", 10),
|
|
394
|
+
behind: Number.parseInt(tracking.match(/behind (\d+)/)?.[1] ?? "0", 10),
|
|
395
|
+
branch,
|
|
396
|
+
upstream
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
function parseStatusLine(line) {
|
|
400
|
+
const indexStatus = line[0] ?? " ";
|
|
401
|
+
const workingTreeStatus = line[1] ?? " ";
|
|
402
|
+
const rawPath = line.slice(3);
|
|
403
|
+
if (!rawPath) return null;
|
|
404
|
+
const [originalPath, pathName] = rawPath.includes(" -> ") ? rawPath.split(" -> ", 2) : [void 0, rawPath];
|
|
405
|
+
return {
|
|
406
|
+
indexStatus,
|
|
407
|
+
originalPath,
|
|
408
|
+
path: pathName,
|
|
409
|
+
staged: indexStatus !== " " && indexStatus !== "?",
|
|
410
|
+
status: statusFromCode(indexStatus !== " " && indexStatus !== "?" ? indexStatus : workingTreeStatus),
|
|
411
|
+
workingTreeStatus
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
function statusFromCode(code) {
|
|
415
|
+
if (code === "?") return "untracked";
|
|
416
|
+
if (code === "A") return "added";
|
|
417
|
+
if (code === "D") return "deleted";
|
|
418
|
+
if (code === "R") return "renamed";
|
|
419
|
+
return "modified";
|
|
420
|
+
}
|
|
421
|
+
async function readGitLog(cwd) {
|
|
422
|
+
try {
|
|
423
|
+
return (await runGit(cwd, [
|
|
424
|
+
"log",
|
|
425
|
+
"--date-order",
|
|
426
|
+
"--decorate=short",
|
|
427
|
+
"--pretty=format:%h%x09%D%x09%s%x09%an",
|
|
428
|
+
"-n",
|
|
429
|
+
"16"
|
|
430
|
+
])).stdout.split(/\r?\n/).filter(Boolean).map((line) => {
|
|
431
|
+
const [hash = "", refs = "", subject = "", author = ""] = line.split(" ");
|
|
432
|
+
return {
|
|
433
|
+
author,
|
|
434
|
+
hash,
|
|
435
|
+
refs,
|
|
436
|
+
subject
|
|
437
|
+
};
|
|
438
|
+
});
|
|
439
|
+
} catch {
|
|
440
|
+
return [];
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
async function runGit(cwd, args) {
|
|
444
|
+
try {
|
|
445
|
+
return await execFileAsync("git", args, {
|
|
446
|
+
cwd,
|
|
447
|
+
maxBuffer: 1024 * 1024,
|
|
448
|
+
timeout: 12e4
|
|
449
|
+
});
|
|
450
|
+
} catch (error) {
|
|
451
|
+
throw new HttpError(400, (error instanceof Error ? error.message : "Git command failed.").replace(/^Command failed: git [^\n]+\n?/, "").trim() || "Git command failed.");
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
async function resolveWorkspacePath(inputPath) {
|
|
455
|
+
const requested = inputPath?.trim();
|
|
456
|
+
if (!requested) throw new HttpError(400, "path is required.");
|
|
457
|
+
const resolvedPath = await resolveRealPath(resolveUserPath(requested));
|
|
458
|
+
if (!isPathWithinWorkspaceRoot(resolvedPath)) throw new HttpError(403, "Path is outside the file browser home.");
|
|
459
|
+
if (!(await stat(resolvedPath)).isDirectory()) throw new HttpError(400, "Path is not a directory.");
|
|
460
|
+
return resolvedPath;
|
|
461
|
+
}
|
|
462
|
+
async function resolveRealPath(inputPath) {
|
|
463
|
+
try {
|
|
464
|
+
return await realpath(path.resolve(inputPath));
|
|
465
|
+
} catch {
|
|
466
|
+
throw new HttpError(400, "Directory path does not exist.");
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
function resolveUserPath(inputPath) {
|
|
470
|
+
if (inputPath === "~") return workspaceRoot;
|
|
471
|
+
if (inputPath.startsWith("~/")) return path.join(workspaceRoot, inputPath.slice(2));
|
|
472
|
+
if (path.isAbsolute(inputPath)) return inputPath;
|
|
473
|
+
return path.join(workspaceRoot, inputPath);
|
|
474
|
+
}
|
|
475
|
+
function isPathWithinWorkspaceRoot(inputPath) {
|
|
476
|
+
const relativePath = path.relative(workspaceRoot, inputPath);
|
|
477
|
+
return relativePath === "" || !relativePath.startsWith("..") && !path.isAbsolute(relativePath);
|
|
478
|
+
}
|
|
479
|
+
//#endregion
|
|
480
|
+
//#region app/server/mcp.service.ts
|
|
481
|
+
var codexProviderId = "codex";
|
|
482
|
+
var serverNamePattern = /^[A-Za-z0-9_-]+$/;
|
|
483
|
+
var maxStatusErrorLength = 2e3;
|
|
484
|
+
async function listMcpServers() {
|
|
485
|
+
await ensureDatabase();
|
|
486
|
+
return (await prisma.mcpServer.findMany({
|
|
487
|
+
include: { installations: true },
|
|
488
|
+
orderBy: { name: "asc" }
|
|
489
|
+
})).map(serializeMcpServer);
|
|
490
|
+
}
|
|
491
|
+
async function getMcpServer(serverId) {
|
|
492
|
+
return serializeMcpServer(await getMcpServerRecord(serverId));
|
|
493
|
+
}
|
|
494
|
+
async function createMcpServer(dto) {
|
|
495
|
+
await ensureDatabase();
|
|
496
|
+
const draft = readMcpServerDraft(dto, { requireTransport: true });
|
|
497
|
+
const name = draft.name;
|
|
498
|
+
const transport = draft.transport;
|
|
499
|
+
if (!name) throw new HttpError(400, "name is required.");
|
|
500
|
+
if (!transport) throw new HttpError(400, "transport is required.");
|
|
501
|
+
const created = await prisma.mcpServer.create({
|
|
502
|
+
data: {
|
|
503
|
+
adapterSettings: draft.adapterSettings,
|
|
504
|
+
config: draft.config ?? {},
|
|
505
|
+
displayName: draft.displayName ?? null,
|
|
506
|
+
enabled: draft.enabled,
|
|
507
|
+
name,
|
|
508
|
+
required: draft.required,
|
|
509
|
+
startupTimeoutSec: draft.startupTimeoutSec,
|
|
510
|
+
toolPolicy: draft.toolPolicy,
|
|
511
|
+
toolTimeoutSec: draft.toolTimeoutSec,
|
|
512
|
+
transport
|
|
513
|
+
},
|
|
514
|
+
include: { installations: true }
|
|
515
|
+
}).catch((error) => {
|
|
516
|
+
if (isUniqueConstraintError(error)) throw new HttpError(409, "An MCP server with this name already exists.");
|
|
517
|
+
throw error;
|
|
518
|
+
});
|
|
519
|
+
await replaceMcpServerInstallations(created.id, dto.accountIds);
|
|
520
|
+
return getMcpServer(created.id);
|
|
521
|
+
}
|
|
522
|
+
async function updateMcpServer(serverId, dto) {
|
|
523
|
+
await ensureDatabase();
|
|
524
|
+
await getMcpServerRecord(serverId);
|
|
525
|
+
const draft = readMcpServerDraft(dto, { requireTransport: false });
|
|
526
|
+
await prisma.mcpServer.update({
|
|
527
|
+
where: { id: serverId },
|
|
528
|
+
data: {
|
|
529
|
+
adapterSettings: draft.adapterSettings === void 0 ? void 0 : draft.adapterSettings,
|
|
530
|
+
config: draft.config === void 0 ? void 0 : draft.config,
|
|
531
|
+
displayName: draft.displayName,
|
|
532
|
+
enabled: draft.enabled,
|
|
533
|
+
name: draft.name,
|
|
534
|
+
required: draft.required,
|
|
535
|
+
startupTimeoutSec: draft.startupTimeoutSec,
|
|
536
|
+
toolPolicy: draft.toolPolicy === void 0 ? void 0 : draft.toolPolicy,
|
|
537
|
+
toolTimeoutSec: draft.toolTimeoutSec,
|
|
538
|
+
transport: draft.transport
|
|
539
|
+
}
|
|
540
|
+
}).catch((error) => {
|
|
541
|
+
if (isUniqueConstraintError(error)) throw new HttpError(409, "An MCP server with this name already exists.");
|
|
542
|
+
throw error;
|
|
543
|
+
});
|
|
544
|
+
await replaceMcpServerInstallations(serverId, dto.accountIds);
|
|
545
|
+
return getMcpServer(serverId);
|
|
546
|
+
}
|
|
547
|
+
async function deleteMcpServer(serverId) {
|
|
548
|
+
await ensureDatabase();
|
|
549
|
+
const server = await getMcpServerRecord(serverId);
|
|
550
|
+
const accountIds = server.installations.map((installation) => installation.accountId);
|
|
551
|
+
const response = serializeMcpServer(server);
|
|
552
|
+
await prisma.mcpServer.delete({ where: { id: serverId } });
|
|
553
|
+
await Promise.allSettled([...new Set(accountIds)].map((accountId) => syncCodexAccountMcpConfig(accountId)));
|
|
554
|
+
return response;
|
|
555
|
+
}
|
|
556
|
+
async function syncMcpServer(serverId, dto = {}) {
|
|
557
|
+
await ensureDatabase();
|
|
558
|
+
const previousAccountIds = (await getMcpServerRecord(serverId)).installations.map((installation) => installation.accountId);
|
|
559
|
+
await replaceMcpServerInstallations(serverId, dto.accountIds);
|
|
560
|
+
const server = await getMcpServerRecord(serverId);
|
|
561
|
+
const targetAccountIds = dto.accountIds !== void 0 ? uniqueStrings([...previousAccountIds, ...dto.accountIds]) : uniqueStrings(server.installations.map((installation) => installation.accountId));
|
|
562
|
+
const failed = (await Promise.all(targetAccountIds.map((accountId) => syncCodexAccountMcpConfig(accountId)))).find((result) => result.error);
|
|
563
|
+
if (failed) throw new HttpError(502, failed.error ?? "Unable to sync MCP server.");
|
|
564
|
+
return getMcpServer(serverId);
|
|
565
|
+
}
|
|
566
|
+
async function listMcpServerStatuses(accountId) {
|
|
567
|
+
await ensureDatabase();
|
|
568
|
+
const account = await getCodexAccount(accountId);
|
|
569
|
+
const installations = await prisma.mcpServerInstallation.findMany({
|
|
570
|
+
include: { server: true },
|
|
571
|
+
orderBy: { server: { name: "asc" } },
|
|
572
|
+
where: {
|
|
573
|
+
accountId,
|
|
574
|
+
providerId: codexProviderId
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
let rawStatuses = [];
|
|
578
|
+
try {
|
|
579
|
+
rawStatuses = await listCodexMcpServerStatuses(account);
|
|
580
|
+
} catch (error) {
|
|
581
|
+
const message = errorMessage(error);
|
|
582
|
+
return {
|
|
583
|
+
accountId,
|
|
584
|
+
data: installations.map((installation) => statusItemFromInstallation(installation, message))
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
const statusByName = /* @__PURE__ */ new Map();
|
|
588
|
+
for (const rawStatus of rawStatuses) {
|
|
589
|
+
const name = readString(asJsonObject(rawStatus)?.name);
|
|
590
|
+
if (name) statusByName.set(name, rawStatus);
|
|
591
|
+
}
|
|
592
|
+
const items = installations.map((installation) => {
|
|
593
|
+
const rawStatus = statusByName.get(installation.server.name);
|
|
594
|
+
return rawStatus ? statusItemFromRaw(accountId, installation.server.id, rawStatus, installation.lastError) : statusItemFromInstallation(installation, installation.lastError);
|
|
595
|
+
});
|
|
596
|
+
for (const rawStatus of rawStatuses) {
|
|
597
|
+
const name = readString(asJsonObject(rawStatus)?.name);
|
|
598
|
+
if (name && !installations.some((installation) => installation.server.name === name)) items.push(statusItemFromRaw(accountId, null, rawStatus, null));
|
|
599
|
+
}
|
|
600
|
+
return {
|
|
601
|
+
accountId,
|
|
602
|
+
data: items
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
async function startMcpServerOauthLogin(serverId, dto) {
|
|
606
|
+
await ensureDatabase();
|
|
607
|
+
const server = await getMcpServerRecord(serverId);
|
|
608
|
+
const transport = serializeMcpTransport(server);
|
|
609
|
+
if (transport.type !== "streamable_http") throw new HttpError(400, "OAuth login is only available for HTTP MCP servers.");
|
|
610
|
+
const account = await getCodexAccount(dto.accountId);
|
|
611
|
+
if (!await prisma.mcpServerInstallation.findFirst({ where: {
|
|
612
|
+
accountId: account.id,
|
|
613
|
+
providerId: codexProviderId,
|
|
614
|
+
serverId
|
|
615
|
+
} })) throw new HttpError(400, "Install this MCP server to the selected account before starting OAuth login.");
|
|
616
|
+
const sync = await syncCodexAccountMcpConfig(account.id);
|
|
617
|
+
if (sync.error) throw new HttpError(502, sync.error);
|
|
618
|
+
const scopes = dto.scopes?.length ? dto.scopes : transport.scopes;
|
|
619
|
+
return startCodexMcpOauthLogin(account, server.name, scopes);
|
|
620
|
+
}
|
|
621
|
+
async function getMcpServerRecord(serverId) {
|
|
622
|
+
await ensureDatabase();
|
|
623
|
+
const server = await prisma.mcpServer.findUnique({
|
|
624
|
+
include: { installations: true },
|
|
625
|
+
where: { id: serverId }
|
|
626
|
+
});
|
|
627
|
+
if (!server) throw new HttpError(404, "MCP server not found.");
|
|
628
|
+
return server;
|
|
629
|
+
}
|
|
630
|
+
async function getCodexAccount(accountId) {
|
|
631
|
+
const account = await prisma.providerAccount.findUnique({ where: { id: accountId } });
|
|
632
|
+
if (!account) throw new HttpError(404, "Provider account not found.");
|
|
633
|
+
if (account.providerId !== codexProviderId) throw new HttpError(400, "MCP sync currently supports Codex accounts only.");
|
|
634
|
+
return account;
|
|
635
|
+
}
|
|
636
|
+
async function replaceMcpServerInstallations(serverId, accountIds) {
|
|
637
|
+
if (accountIds === void 0) return;
|
|
638
|
+
const uniqueAccountIds = uniqueStrings(accountIds);
|
|
639
|
+
if (!uniqueAccountIds.length) {
|
|
640
|
+
await prisma.mcpServerInstallation.deleteMany({ where: {
|
|
641
|
+
providerId: codexProviderId,
|
|
642
|
+
serverId
|
|
643
|
+
} });
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
const accounts = await prisma.providerAccount.findMany({ where: {
|
|
647
|
+
id: { in: uniqueAccountIds },
|
|
648
|
+
providerId: codexProviderId
|
|
649
|
+
} });
|
|
650
|
+
const foundIds = new Set(accounts.map((account) => account.id));
|
|
651
|
+
const missing = uniqueAccountIds.find((accountId) => !foundIds.has(accountId));
|
|
652
|
+
if (missing) throw new HttpError(400, `Codex account ${missing} was not found.`);
|
|
653
|
+
await prisma.mcpServerInstallation.deleteMany({ where: {
|
|
654
|
+
accountId: { notIn: uniqueAccountIds },
|
|
655
|
+
providerId: codexProviderId,
|
|
656
|
+
serverId
|
|
657
|
+
} });
|
|
658
|
+
await Promise.all(uniqueAccountIds.map((accountId) => prisma.mcpServerInstallation.upsert({
|
|
659
|
+
create: {
|
|
660
|
+
accountId,
|
|
661
|
+
providerId: codexProviderId,
|
|
662
|
+
serverId
|
|
663
|
+
},
|
|
664
|
+
update: { enabled: true },
|
|
665
|
+
where: { serverId_providerId_accountId: {
|
|
666
|
+
accountId,
|
|
667
|
+
providerId: codexProviderId,
|
|
668
|
+
serverId
|
|
669
|
+
} }
|
|
670
|
+
})));
|
|
671
|
+
}
|
|
672
|
+
async function syncCodexAccountMcpConfig(accountId) {
|
|
673
|
+
const account = await getCodexAccount(accountId);
|
|
674
|
+
const installations = await prisma.mcpServerInstallation.findMany({
|
|
675
|
+
include: { server: true },
|
|
676
|
+
orderBy: { server: { name: "asc" } },
|
|
677
|
+
where: {
|
|
678
|
+
accountId,
|
|
679
|
+
providerId: codexProviderId
|
|
680
|
+
}
|
|
681
|
+
});
|
|
682
|
+
const startedAt = /* @__PURE__ */ new Date();
|
|
683
|
+
try {
|
|
684
|
+
await writeCodexMcpConfig(account, installations);
|
|
685
|
+
} catch (error) {
|
|
686
|
+
const message = errorMessage(error);
|
|
687
|
+
await markInstallationsSynced(accountId, startedAt, "ERROR", message);
|
|
688
|
+
return {
|
|
689
|
+
accountId,
|
|
690
|
+
error: message,
|
|
691
|
+
status: "ERROR"
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
try {
|
|
695
|
+
await reloadCodexMcpServerConfig(account);
|
|
696
|
+
} catch (error) {
|
|
697
|
+
const message = errorMessage(error);
|
|
698
|
+
await markInstallationsSynced(accountId, startedAt, "ERROR", message);
|
|
699
|
+
return {
|
|
700
|
+
accountId,
|
|
701
|
+
error: message,
|
|
702
|
+
status: "ERROR"
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
await markInstallationsSynced(accountId, startedAt, "SYNCED", null);
|
|
706
|
+
return {
|
|
707
|
+
accountId,
|
|
708
|
+
error: null,
|
|
709
|
+
status: "SYNCED"
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
async function markInstallationsSynced(accountId, syncedAt, status, error) {
|
|
713
|
+
await prisma.mcpServerInstallation.updateMany({
|
|
714
|
+
data: {
|
|
715
|
+
lastError: error ? truncateError(error) : null,
|
|
716
|
+
lastStatus: status,
|
|
717
|
+
lastSyncAt: syncedAt
|
|
718
|
+
},
|
|
719
|
+
where: {
|
|
720
|
+
accountId,
|
|
721
|
+
providerId: codexProviderId
|
|
722
|
+
}
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
async function writeCodexMcpConfig(account, installations) {
|
|
726
|
+
const codexHome = resolveCodexAccountHome(account);
|
|
727
|
+
await mkdir(codexHome, {
|
|
728
|
+
mode: 448,
|
|
729
|
+
recursive: true
|
|
730
|
+
});
|
|
731
|
+
const configPath = join(codexHome, "config.toml");
|
|
732
|
+
const currentConfig = await readCodexTomlConfig(configPath);
|
|
733
|
+
const mcpServers = {};
|
|
734
|
+
for (const installation of installations) mcpServers[installation.server.name] = codexConfigForServer(installation.server, installation);
|
|
735
|
+
if (Object.keys(mcpServers).length) currentConfig.mcp_servers = mcpServers;
|
|
736
|
+
else delete currentConfig.mcp_servers;
|
|
737
|
+
await writeFile(configPath, stringify(currentConfig), "utf8");
|
|
738
|
+
}
|
|
739
|
+
async function readCodexTomlConfig(configPath) {
|
|
740
|
+
const source = await readFile(configPath, "utf8").catch((error) => {
|
|
741
|
+
if (error.code === "ENOENT") return "";
|
|
742
|
+
throw error;
|
|
743
|
+
});
|
|
744
|
+
if (!source.trim()) return {};
|
|
745
|
+
const parsed = parse(source);
|
|
746
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("Codex config.toml must contain a TOML table.");
|
|
747
|
+
return parsed;
|
|
748
|
+
}
|
|
749
|
+
function codexConfigForServer(server, installation) {
|
|
750
|
+
const transport = serializeMcpTransport(server);
|
|
751
|
+
const toolPolicy = serializeToolPolicy(server.toolPolicy);
|
|
752
|
+
const entry = {
|
|
753
|
+
enabled: server.enabled && installation.enabled,
|
|
754
|
+
required: server.required
|
|
755
|
+
};
|
|
756
|
+
if (transport.type === "stdio") {
|
|
757
|
+
entry.command = transport.command;
|
|
758
|
+
if (transport.args.length) entry.args = transport.args;
|
|
759
|
+
if (Object.keys(transport.env).length) entry.env = transport.env;
|
|
760
|
+
if (transport.envVars.length) entry.env_vars = transport.envVars.map(envVarName);
|
|
761
|
+
if (transport.cwd) entry.cwd = transport.cwd;
|
|
762
|
+
} else {
|
|
763
|
+
entry.url = transport.url;
|
|
764
|
+
if (transport.bearerTokenEnvVar) entry.bearer_token_env_var = transport.bearerTokenEnvVar;
|
|
765
|
+
if (Object.keys(transport.httpHeaders).length) entry.http_headers = transport.httpHeaders;
|
|
766
|
+
if (Object.keys(transport.envHttpHeaders).length) entry.env_http_headers = transport.envHttpHeaders;
|
|
767
|
+
if (transport.oauthClientId) entry.oauth = { client_id: transport.oauthClientId };
|
|
768
|
+
if (transport.oauthResource) entry.oauth_resource = transport.oauthResource;
|
|
769
|
+
if (transport.scopes.length) entry.scopes = transport.scopes;
|
|
770
|
+
}
|
|
771
|
+
if (server.startupTimeoutSec !== null) entry.startup_timeout_sec = server.startupTimeoutSec;
|
|
772
|
+
if (server.toolTimeoutSec !== null) entry.tool_timeout_sec = server.toolTimeoutSec;
|
|
773
|
+
if (toolPolicy.defaultToolsApprovalMode) entry.default_tools_approval_mode = toolPolicy.defaultToolsApprovalMode;
|
|
774
|
+
if (toolPolicy.enabledTools?.length) entry.enabled_tools = toolPolicy.enabledTools;
|
|
775
|
+
if (toolPolicy.disabledTools?.length) entry.disabled_tools = toolPolicy.disabledTools;
|
|
776
|
+
if (toolPolicy.tools && Object.keys(toolPolicy.tools).length) {
|
|
777
|
+
const tools = {};
|
|
778
|
+
for (const [toolName, override] of Object.entries(toolPolicy.tools)) tools[toolName] = override.approvalMode ? { approval_mode: override.approvalMode } : {};
|
|
779
|
+
entry.tools = tools;
|
|
780
|
+
}
|
|
781
|
+
return entry;
|
|
782
|
+
}
|
|
783
|
+
function readMcpServerDraft(dto, options) {
|
|
784
|
+
const transport = readTransportConfig(dto.transport, options.requireTransport);
|
|
785
|
+
const split = transport ? splitTransportConfig(transport) : null;
|
|
786
|
+
return {
|
|
787
|
+
adapterSettings: dto.adapterSettings === void 0 ? void 0 : readJsonObject(dto.adapterSettings, "adapterSettings"),
|
|
788
|
+
config: split?.config,
|
|
789
|
+
displayName: dto.displayName === void 0 ? void 0 : readOptionalString(dto.displayName, "displayName", 160),
|
|
790
|
+
enabled: dto.enabled === void 0 ? void 0 : readBooleanValue(dto.enabled, "enabled"),
|
|
791
|
+
name: dto.name === void 0 ? void 0 : readMcpServerName(dto.name),
|
|
792
|
+
required: dto.required === void 0 ? void 0 : readBooleanValue(dto.required, "required"),
|
|
793
|
+
startupTimeoutSec: dto.startupTimeoutSec === void 0 ? void 0 : readOptionalPositiveNumber(dto.startupTimeoutSec, "startupTimeoutSec"),
|
|
794
|
+
toolPolicy: dto.toolPolicy === void 0 ? void 0 : readToolPolicy(dto.toolPolicy),
|
|
795
|
+
toolTimeoutSec: dto.toolTimeoutSec === void 0 ? void 0 : readOptionalPositiveNumber(dto.toolTimeoutSec, "toolTimeoutSec"),
|
|
796
|
+
transport: split?.transport
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
function readTransportConfig(value, required) {
|
|
800
|
+
if (value === void 0) {
|
|
801
|
+
if (required) throw new HttpError(400, "transport is required.");
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
const record = readObject(value, "transport");
|
|
805
|
+
const rawType = readString(record.type);
|
|
806
|
+
const type = rawType === "http" ? "streamable_http" : rawType;
|
|
807
|
+
if (type === "stdio") return {
|
|
808
|
+
args: readStringArray(record.args, "transport.args"),
|
|
809
|
+
command: readRequiredString(record.command, "transport.command", 500),
|
|
810
|
+
cwd: readOptionalString(record.cwd, "transport.cwd", 1e3),
|
|
811
|
+
env: readStringRecord(record.env, "transport.env"),
|
|
812
|
+
envVars: readEnvVarRefs(record.envVars ?? record.env_vars),
|
|
813
|
+
type
|
|
814
|
+
};
|
|
815
|
+
if (type === "streamable_http") return {
|
|
816
|
+
bearerTokenEnvVar: readOptionalString(record.bearerTokenEnvVar ?? record.bearer_token_env_var, "transport.bearerTokenEnvVar", 200),
|
|
817
|
+
envHttpHeaders: readStringRecord(record.envHttpHeaders ?? record.env_http_headers, "transport.envHttpHeaders"),
|
|
818
|
+
httpHeaders: readStringRecord(record.httpHeaders ?? record.http_headers, "transport.httpHeaders"),
|
|
819
|
+
oauthClientId: readOptionalString(record.oauthClientId ?? asJsonObject(record.oauth)?.client_id, "transport.oauthClientId", 500),
|
|
820
|
+
oauthResource: readOptionalString(record.oauthResource ?? record.oauth_resource, "transport.oauthResource", 1e3),
|
|
821
|
+
scopes: readStringArray(record.scopes, "transport.scopes"),
|
|
822
|
+
type,
|
|
823
|
+
url: readHttpUrl(record.url, "transport.url")
|
|
824
|
+
};
|
|
825
|
+
throw new HttpError(400, "transport.type must be stdio or streamable_http.");
|
|
826
|
+
}
|
|
827
|
+
function splitTransportConfig(transport) {
|
|
828
|
+
const { type, ...config } = transport;
|
|
829
|
+
return {
|
|
830
|
+
config,
|
|
831
|
+
transport: type
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
function serializeMcpServer(server) {
|
|
835
|
+
return {
|
|
836
|
+
adapterSettings: serializeJsonObject(server.adapterSettings),
|
|
837
|
+
createdAt: server.createdAt.toISOString(),
|
|
838
|
+
displayName: server.displayName,
|
|
839
|
+
enabled: server.enabled,
|
|
840
|
+
id: server.id,
|
|
841
|
+
installations: server.installations.map(serializeMcpInstallation),
|
|
842
|
+
name: server.name,
|
|
843
|
+
required: server.required,
|
|
844
|
+
startupTimeoutSec: server.startupTimeoutSec,
|
|
845
|
+
toolPolicy: serializeToolPolicy(server.toolPolicy),
|
|
846
|
+
toolTimeoutSec: server.toolTimeoutSec,
|
|
847
|
+
transport: serializeMcpTransport(server),
|
|
848
|
+
updatedAt: server.updatedAt.toISOString()
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
function serializeMcpInstallation(installation) {
|
|
852
|
+
return {
|
|
853
|
+
accountId: installation.accountId,
|
|
854
|
+
createdAt: installation.createdAt.toISOString(),
|
|
855
|
+
enabled: installation.enabled,
|
|
856
|
+
id: installation.id,
|
|
857
|
+
lastError: installation.lastError,
|
|
858
|
+
lastStatus: installation.lastStatus,
|
|
859
|
+
lastSyncAt: installation.lastSyncAt?.toISOString() ?? null,
|
|
860
|
+
providerId: installation.providerId,
|
|
861
|
+
serverId: installation.serverId,
|
|
862
|
+
updatedAt: installation.updatedAt.toISOString()
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
function serializeMcpTransport(server) {
|
|
866
|
+
const config = serializeJsonObject(server.config);
|
|
867
|
+
if (server.transport === "stdio") return readTransportConfig({
|
|
868
|
+
...config,
|
|
869
|
+
type: "stdio"
|
|
870
|
+
}, true);
|
|
871
|
+
return readTransportConfig({
|
|
872
|
+
...config,
|
|
873
|
+
type: "streamable_http"
|
|
874
|
+
}, true);
|
|
875
|
+
}
|
|
876
|
+
function readToolPolicy(value) {
|
|
877
|
+
const record = readJsonObject(value, "toolPolicy");
|
|
878
|
+
return {
|
|
879
|
+
defaultToolsApprovalMode: readApprovalMode(record.defaultToolsApprovalMode ?? record.default_tools_approval_mode, "toolPolicy.defaultToolsApprovalMode"),
|
|
880
|
+
disabledTools: record.disabledTools === void 0 && record.disabled_tools === void 0 ? void 0 : readStringArray(record.disabledTools ?? record.disabled_tools, "toolPolicy.disabledTools"),
|
|
881
|
+
enabledTools: record.enabledTools === void 0 && record.enabled_tools === void 0 ? void 0 : readStringArray(record.enabledTools ?? record.enabled_tools, "toolPolicy.enabledTools"),
|
|
882
|
+
tools: readToolOverrides(record.tools)
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
function serializeToolPolicy(value) {
|
|
886
|
+
return readToolPolicy(serializeJsonObject(value));
|
|
887
|
+
}
|
|
888
|
+
function readToolOverrides(value) {
|
|
889
|
+
if (value === void 0) return;
|
|
890
|
+
const record = readObject(value, "toolPolicy.tools");
|
|
891
|
+
const tools = {};
|
|
892
|
+
for (const [toolName, override] of Object.entries(record)) {
|
|
893
|
+
if (!toolName.trim()) throw new HttpError(400, "Tool override names cannot be empty.");
|
|
894
|
+
const overrideRecord = readObject(override, `toolPolicy.tools.${toolName}`);
|
|
895
|
+
tools[toolName] = { approvalMode: readApprovalMode(overrideRecord.approvalMode ?? overrideRecord.approval_mode, `toolPolicy.tools.${toolName}.approvalMode`) };
|
|
896
|
+
}
|
|
897
|
+
return tools;
|
|
898
|
+
}
|
|
899
|
+
function statusItemFromInstallation(installation, error) {
|
|
900
|
+
return {
|
|
901
|
+
accountId: installation.accountId,
|
|
902
|
+
authStatus: "unknown",
|
|
903
|
+
error: error ?? null,
|
|
904
|
+
lastError: installation.lastError,
|
|
905
|
+
name: installation.server.name,
|
|
906
|
+
resourceCount: 0,
|
|
907
|
+
resourceTemplateCount: 0,
|
|
908
|
+
serverId: installation.serverId,
|
|
909
|
+
serverInfo: null,
|
|
910
|
+
toolCount: 0,
|
|
911
|
+
tools: []
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
function statusItemFromRaw(accountId, serverId, rawStatus, lastError) {
|
|
915
|
+
const record = asJsonObject(rawStatus) ?? {};
|
|
916
|
+
const tools = readStatusNames(record.tools);
|
|
917
|
+
const resources = readStatusNames(record.resources);
|
|
918
|
+
const resourceTemplates = readStatusNames(record.resourceTemplates ?? record.resource_templates);
|
|
919
|
+
return {
|
|
920
|
+
accountId,
|
|
921
|
+
authStatus: readAuthStatus(record.authStatus ?? record.auth_status),
|
|
922
|
+
error: readString(record.error ?? record.startupError ?? record.startup_error) ?? null,
|
|
923
|
+
lastError: lastError ?? null,
|
|
924
|
+
name: readString(record.name) ?? "unknown",
|
|
925
|
+
raw: isJsonSerializable(rawStatus) ? rawStatus : void 0,
|
|
926
|
+
resourceCount: resources.length,
|
|
927
|
+
resourceTemplateCount: resourceTemplates.length,
|
|
928
|
+
serverId,
|
|
929
|
+
serverInfo: asJsonObject(record.serverInfo ?? record.server_info) ?? null,
|
|
930
|
+
toolCount: tools.length,
|
|
931
|
+
tools
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
function readStatusNames(value) {
|
|
935
|
+
if (Array.isArray(value)) return value.map((item) => typeof item === "string" ? item : readString(asJsonObject(item)?.name)).filter((name) => Boolean(name));
|
|
936
|
+
const record = asJsonObject(value);
|
|
937
|
+
return record ? Object.keys(record) : [];
|
|
938
|
+
}
|
|
939
|
+
function readAuthStatus(value) {
|
|
940
|
+
if (value === "unsupported" || value === "notLoggedIn" || value === "bearerToken" || value === "oAuth") return value;
|
|
941
|
+
if (value === "not_logged_in") return "notLoggedIn";
|
|
942
|
+
if (value === "bearer_token") return "bearerToken";
|
|
943
|
+
if (value === "oauth" || value === "o_auth") return "oAuth";
|
|
944
|
+
return "unknown";
|
|
945
|
+
}
|
|
946
|
+
function readMcpServerName(value) {
|
|
947
|
+
const name = readRequiredString(value, "name", 80);
|
|
948
|
+
if (!serverNamePattern.test(name)) throw new HttpError(400, "MCP server names can only contain letters, numbers, -, and _.");
|
|
949
|
+
return name;
|
|
950
|
+
}
|
|
951
|
+
function readHttpUrl(value, field) {
|
|
952
|
+
const url = readRequiredString(value, field, 2e3);
|
|
953
|
+
let parsed;
|
|
954
|
+
try {
|
|
955
|
+
parsed = new URL(url);
|
|
956
|
+
} catch {
|
|
957
|
+
throw new HttpError(400, `${field} must be a valid URL.`);
|
|
958
|
+
}
|
|
959
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new HttpError(400, `${field} must use http or https.`);
|
|
960
|
+
return url;
|
|
961
|
+
}
|
|
962
|
+
function readObject(value, field) {
|
|
963
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new HttpError(400, `${field} must be an object.`);
|
|
964
|
+
return value;
|
|
965
|
+
}
|
|
966
|
+
function readJsonObject(value, field) {
|
|
967
|
+
return readObject(value, field);
|
|
968
|
+
}
|
|
969
|
+
function readRequiredString(value, field, maxLength) {
|
|
970
|
+
if (typeof value !== "string" || !value.trim()) throw new HttpError(400, `${field} is required.`);
|
|
971
|
+
const trimmed = value.trim();
|
|
972
|
+
if (trimmed.length > maxLength) throw new HttpError(400, `${field} must be ${maxLength} characters or fewer.`);
|
|
973
|
+
return trimmed;
|
|
974
|
+
}
|
|
975
|
+
function readOptionalString(value, field, maxLength) {
|
|
976
|
+
if (value === void 0 || value === null || value === "") return null;
|
|
977
|
+
return readRequiredString(value, field, maxLength) || null;
|
|
978
|
+
}
|
|
979
|
+
function readBooleanValue(value, field) {
|
|
980
|
+
const booleanValue = readBoolean(value);
|
|
981
|
+
if (booleanValue === void 0) throw new HttpError(400, `${field} must be a boolean.`);
|
|
982
|
+
return booleanValue;
|
|
983
|
+
}
|
|
984
|
+
function readOptionalPositiveNumber(value, field) {
|
|
985
|
+
if (value === null || value === "") return null;
|
|
986
|
+
const numberValue = readNumber(value);
|
|
987
|
+
if (numberValue === void 0 || numberValue < 0) throw new HttpError(400, `${field} must be a positive number.`);
|
|
988
|
+
return numberValue;
|
|
989
|
+
}
|
|
990
|
+
function readStringArray(value, field) {
|
|
991
|
+
if (value === void 0 || value === null) return [];
|
|
992
|
+
if (!Array.isArray(value)) throw new HttpError(400, `${field} must be an array.`);
|
|
993
|
+
return value.map((item, index) => readRequiredString(item, `${field}[${index}]`, 500)).filter((item, index, values) => values.indexOf(item) === index);
|
|
994
|
+
}
|
|
995
|
+
function readStringRecord(value, field) {
|
|
996
|
+
if (value === void 0 || value === null) return {};
|
|
997
|
+
const record = readObject(value, field);
|
|
998
|
+
return Object.fromEntries(Object.entries(record).map(([key, item]) => [readRequiredString(key, `${field} key`, 200), readRequiredString(item, `${field}.${key}`, 2e3)]));
|
|
999
|
+
}
|
|
1000
|
+
function readEnvVarRefs(value) {
|
|
1001
|
+
if (value === void 0 || value === null) return [];
|
|
1002
|
+
if (!Array.isArray(value)) throw new HttpError(400, "transport.envVars must be an array.");
|
|
1003
|
+
return value.map((item, index) => {
|
|
1004
|
+
if (typeof item === "string") return readRequiredString(item, `transport.envVars[${index}]`, 200);
|
|
1005
|
+
const record = readObject(item, `transport.envVars[${index}]`);
|
|
1006
|
+
const name = readRequiredString(record.name, `transport.envVars[${index}].name`, 200);
|
|
1007
|
+
const source = record.source === "local" || record.source === "remote" ? record.source : void 0;
|
|
1008
|
+
return source ? {
|
|
1009
|
+
name,
|
|
1010
|
+
source
|
|
1011
|
+
} : { name };
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
function envVarName(value) {
|
|
1015
|
+
return typeof value === "string" ? value : value.name;
|
|
1016
|
+
}
|
|
1017
|
+
function readApprovalMode(value, field) {
|
|
1018
|
+
if (value === void 0 || value === null || value === "") return null;
|
|
1019
|
+
if (value === "auto" || value === "prompt" || value === "approve") return value;
|
|
1020
|
+
throw new HttpError(400, `${field} must be auto, prompt, or approve.`);
|
|
1021
|
+
}
|
|
1022
|
+
function serializeJsonObject(value) {
|
|
1023
|
+
return asJsonObject(value) ?? {};
|
|
1024
|
+
}
|
|
1025
|
+
function uniqueStrings(values) {
|
|
1026
|
+
return values.map((value) => value.trim()).filter((value, index, next) => Boolean(value) && next.indexOf(value) === index);
|
|
1027
|
+
}
|
|
1028
|
+
function isUniqueConstraintError(error) {
|
|
1029
|
+
return error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002";
|
|
1030
|
+
}
|
|
1031
|
+
function errorMessage(error) {
|
|
1032
|
+
return error instanceof Error ? error.message : "Request failed.";
|
|
1033
|
+
}
|
|
1034
|
+
function truncateError(error) {
|
|
1035
|
+
return error.length > maxStatusErrorLength ? `${error.slice(0, maxStatusErrorLength - 3)}...` : error;
|
|
1036
|
+
}
|
|
1037
|
+
function isJsonSerializable(value) {
|
|
1038
|
+
try {
|
|
1039
|
+
JSON.stringify(value);
|
|
1040
|
+
return true;
|
|
1041
|
+
} catch {
|
|
1042
|
+
return false;
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
//#endregion
|
|
1046
|
+
//#region app/server/providers/types.server.ts
|
|
1047
|
+
function serializeProviderDefinition(definition) {
|
|
1048
|
+
return definition;
|
|
1049
|
+
}
|
|
1050
|
+
//#endregion
|
|
1051
|
+
//#region app/server/providers.service.ts
|
|
1052
|
+
async function listProviders() {
|
|
1053
|
+
await ensureDatabase();
|
|
1054
|
+
return listProviderAdapters().map((adapter) => serializeProviderDefinition(adapter.definition));
|
|
1055
|
+
}
|
|
1056
|
+
//#endregion
|
|
1057
|
+
//#region app/server/plugins.service.ts
|
|
1058
|
+
async function listPlugins() {
|
|
1059
|
+
return Promise.all(listPluginRegistrations().map((registration) => serializePlugin(registration.definition.id)));
|
|
1060
|
+
}
|
|
1061
|
+
async function updatePlugin(pluginId, dto) {
|
|
1062
|
+
const registration = readKnownPlugin(pluginId);
|
|
1063
|
+
const current = await readPluginSetting(pluginId, registration.definition.defaultSettings);
|
|
1064
|
+
const settings = dto.settings === void 0 ? current.settings : filterSettings(dto.settings, registration.definition.settingsFields.map((field) => field.key), current.settings);
|
|
1065
|
+
const secrets = dto.secrets === void 0 ? current.secrets : filterSecrets(dto.secrets, registration.definition.secretFields.map((field) => field.key), current.secrets);
|
|
1066
|
+
const enabled = dto.enabled ?? current.enabled;
|
|
1067
|
+
if (pluginId === "telegram" && enabled) await verifyTelegramBotToken(readSecret(secrets, "botToken"));
|
|
1068
|
+
await writePluginSetting({
|
|
1069
|
+
enabled,
|
|
1070
|
+
pluginId,
|
|
1071
|
+
secrets,
|
|
1072
|
+
settings
|
|
1073
|
+
});
|
|
1074
|
+
await restartPlugin(pluginId);
|
|
1075
|
+
return serializePlugin(pluginId);
|
|
1076
|
+
}
|
|
1077
|
+
async function runPluginAction(pluginId, action) {
|
|
1078
|
+
const registration = readKnownPlugin(pluginId);
|
|
1079
|
+
const handler = registration.actions?.[action];
|
|
1080
|
+
if (!handler) throw new HttpError(404, "Plugin action not found.");
|
|
1081
|
+
const result = await handler(createPluginContext(pluginId, await readPluginSetting(pluginId, registration.definition.defaultSettings)));
|
|
1082
|
+
await restartPlugin(pluginId);
|
|
1083
|
+
return {
|
|
1084
|
+
message: result?.message,
|
|
1085
|
+
plugin: await serializePlugin(pluginId)
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
async function serializePlugin(pluginId) {
|
|
1089
|
+
const registration = readKnownPlugin(pluginId);
|
|
1090
|
+
const setting = await readPluginSetting(pluginId, registration.definition.defaultSettings);
|
|
1091
|
+
const state = await readPluginState(pluginId);
|
|
1092
|
+
return {
|
|
1093
|
+
description: registration.definition.description,
|
|
1094
|
+
enabled: setting.enabled,
|
|
1095
|
+
icon: registration.definition.icon,
|
|
1096
|
+
id: registration.definition.id,
|
|
1097
|
+
label: registration.definition.label,
|
|
1098
|
+
secretConfigured: Object.fromEntries(registration.definition.secretFields.map((field) => [field.key, Boolean(readSecret(setting.secrets, field.key))])),
|
|
1099
|
+
secrets: setting.secrets,
|
|
1100
|
+
secretFields: registration.definition.secretFields,
|
|
1101
|
+
settings: setting.settings,
|
|
1102
|
+
settingsFields: registration.definition.settingsFields,
|
|
1103
|
+
stateSummary: registration.summarizeState?.(state) ?? {},
|
|
1104
|
+
status: setting.enabled ? getPluginStatus(pluginId) : {
|
|
1105
|
+
state: "disabled",
|
|
1106
|
+
message: null,
|
|
1107
|
+
updatedAt: null
|
|
1108
|
+
}
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
function readKnownPlugin(pluginId) {
|
|
1112
|
+
try {
|
|
1113
|
+
return readPluginRegistration(pluginId);
|
|
1114
|
+
} catch {
|
|
1115
|
+
throw new HttpError(404, "Plugin not found.");
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
function filterSettings(value, allowedKeys, current) {
|
|
1119
|
+
const next = { ...current };
|
|
1120
|
+
for (const key of allowedKeys) if (value[key] !== void 0) next[key] = value[key];
|
|
1121
|
+
return next;
|
|
1122
|
+
}
|
|
1123
|
+
function filterSecrets(value, allowedKeys, current) {
|
|
1124
|
+
const next = { ...current };
|
|
1125
|
+
for (const key of allowedKeys) {
|
|
1126
|
+
const secret = value[key];
|
|
1127
|
+
if (typeof secret === "string" && secret.trim()) next[key] = secret.trim();
|
|
1128
|
+
if (secret === null) delete next[key];
|
|
1129
|
+
}
|
|
1130
|
+
return next;
|
|
1131
|
+
}
|
|
1132
|
+
function readSecret(secrets, key) {
|
|
1133
|
+
const value = secrets[key];
|
|
1134
|
+
return typeof value === "string" ? value : "";
|
|
1135
|
+
}
|
|
1136
|
+
//#endregion
|
|
1137
|
+
//#region app/server/api.server.ts
|
|
1138
|
+
function installApiServer(middlewares) {
|
|
1139
|
+
middlewares.use((req, res, next) => {
|
|
1140
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
1141
|
+
if (!url.pathname.startsWith("/api/")) {
|
|
1142
|
+
next();
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
handleApiRequest(req, res, url).catch((error) => {
|
|
1146
|
+
sendRouteError(res, error);
|
|
1147
|
+
});
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
async function handleApiRequest(req, res, url) {
|
|
1151
|
+
const method = req.method ?? "GET";
|
|
1152
|
+
if (url.pathname === "/api/providers") {
|
|
1153
|
+
requireMethod(method, ["GET"]);
|
|
1154
|
+
sendJson(res, await listProviders());
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
if (url.pathname === "/api/plugins") {
|
|
1158
|
+
requireMethod(method, ["GET"]);
|
|
1159
|
+
sendJson(res, await listPlugins());
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1162
|
+
const pluginActionMatch = url.pathname.match(/^\/api\/plugins\/([^/]+)\/actions\/([^/]+)$/);
|
|
1163
|
+
if (pluginActionMatch) {
|
|
1164
|
+
requireMethod(method, ["POST"]);
|
|
1165
|
+
sendJson(res, await runPluginAction(decodeURIComponent(pluginActionMatch[1]), decodeURIComponent(pluginActionMatch[2])));
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
const pluginMatch = url.pathname.match(/^\/api\/plugins\/([^/]+)$/);
|
|
1169
|
+
if (pluginMatch) {
|
|
1170
|
+
requireMethod(method, ["PATCH"]);
|
|
1171
|
+
sendJson(res, await updatePlugin(decodeURIComponent(pluginMatch[1]), readPluginSettingsUpdateRequest(await readNodeJsonBody(req))));
|
|
1172
|
+
return;
|
|
1173
|
+
}
|
|
1174
|
+
if (url.pathname === "/api/providers/codex/instructions") {
|
|
1175
|
+
requireMethod(method, ["GET", "PUT"]);
|
|
1176
|
+
if (method === "PUT") {
|
|
1177
|
+
sendJson(res, await updateCodexInstructions(readCodexInstructionsRequest(await readNodeJsonBody(req)).instructions));
|
|
1178
|
+
return;
|
|
1179
|
+
}
|
|
1180
|
+
sendJson(res, await readCodexInstructions());
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
if (url.pathname === "/api/mcp-servers") {
|
|
1184
|
+
requireMethod(method, ["GET", "POST"]);
|
|
1185
|
+
if (method === "POST") {
|
|
1186
|
+
sendJson(res, await createMcpServer(await readNodeJsonBody(req)), 201);
|
|
1187
|
+
return;
|
|
1188
|
+
}
|
|
1189
|
+
sendJson(res, await listMcpServers());
|
|
1190
|
+
return;
|
|
1191
|
+
}
|
|
1192
|
+
if (url.pathname === "/api/mcp-servers/status") {
|
|
1193
|
+
requireMethod(method, ["GET"]);
|
|
1194
|
+
sendJson(res, await listMcpServerStatuses(readStringField(url.searchParams.get("accountId"), "accountId", { required: true })));
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
const mcpServerSyncMatch = url.pathname.match(/^\/api\/mcp-servers\/([^/]+)\/sync$/);
|
|
1198
|
+
if (mcpServerSyncMatch) {
|
|
1199
|
+
requireMethod(method, ["POST"]);
|
|
1200
|
+
sendJson(res, await syncMcpServer(decodeURIComponent(mcpServerSyncMatch[1]), await readNodeJsonBody(req)));
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
const mcpServerOauthLoginMatch = url.pathname.match(/^\/api\/mcp-servers\/([^/]+)\/oauth-login$/);
|
|
1204
|
+
if (mcpServerOauthLoginMatch) {
|
|
1205
|
+
requireMethod(method, ["POST"]);
|
|
1206
|
+
sendJson(res, await startMcpServerOauthLogin(decodeURIComponent(mcpServerOauthLoginMatch[1]), await readNodeJsonBody(req)));
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1209
|
+
const mcpServerMatch = url.pathname.match(/^\/api\/mcp-servers\/([^/]+)$/);
|
|
1210
|
+
if (mcpServerMatch) {
|
|
1211
|
+
const serverId = decodeURIComponent(mcpServerMatch[1]);
|
|
1212
|
+
requireMethod(method, [
|
|
1213
|
+
"DELETE",
|
|
1214
|
+
"GET",
|
|
1215
|
+
"PATCH"
|
|
1216
|
+
]);
|
|
1217
|
+
if (method === "DELETE") {
|
|
1218
|
+
sendJson(res, await deleteMcpServer(serverId));
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
if (method === "PATCH") {
|
|
1222
|
+
sendJson(res, await updateMcpServer(serverId, await readNodeJsonBody(req)));
|
|
1223
|
+
return;
|
|
1224
|
+
}
|
|
1225
|
+
sendJson(res, await getMcpServer(serverId));
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1228
|
+
if (url.pathname === "/api/provider-accounts") {
|
|
1229
|
+
requireMethod(method, ["GET", "POST"]);
|
|
1230
|
+
if (method === "POST") {
|
|
1231
|
+
sendJson(res, await createAccount(readCreateAccountRequest(await readNodeJsonBody(req))), 201);
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
sendJson(res, await listAccounts());
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
if (url.pathname === "/api/provider-accounts/limits") {
|
|
1238
|
+
requireMethod(method, ["GET"]);
|
|
1239
|
+
sendJson(res, await readConnectedAccountLimits());
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
if (url.pathname === "/api/chats") {
|
|
1243
|
+
requireMethod(method, ["GET", "POST"]);
|
|
1244
|
+
if (method === "POST") {
|
|
1245
|
+
sendJson(res, await createChat(readCreateChatRequest(await readNodeJsonBody(req))), 201);
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1248
|
+
sendJson(res, await listChats(url.searchParams.get("workingDirectory")));
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
if (url.pathname === "/api/schedules") {
|
|
1252
|
+
requireMethod(method, ["GET", "POST"]);
|
|
1253
|
+
if (method === "POST") {
|
|
1254
|
+
sendJson(res, await createMessageSchedule(readCreateMessageScheduleRequest(await readNodeJsonBody(req))), 201);
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1257
|
+
sendJson(res, await listMessageSchedules(url.searchParams.get("workingDirectory")));
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
const scheduleRunsMatch = url.pathname.match(/^\/api\/schedules\/([^/]+)\/runs$/);
|
|
1261
|
+
if (scheduleRunsMatch) {
|
|
1262
|
+
requireMethod(method, ["GET"]);
|
|
1263
|
+
sendJson(res, await listMessageScheduleRuns(decodeURIComponent(scheduleRunsMatch[1])));
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
const scheduleMatch = url.pathname.match(/^\/api\/schedules\/([^/]+)$/);
|
|
1267
|
+
if (scheduleMatch) {
|
|
1268
|
+
const scheduleId = decodeURIComponent(scheduleMatch[1]);
|
|
1269
|
+
requireMethod(method, [
|
|
1270
|
+
"DELETE",
|
|
1271
|
+
"GET",
|
|
1272
|
+
"PATCH"
|
|
1273
|
+
]);
|
|
1274
|
+
if (method === "DELETE") {
|
|
1275
|
+
sendJson(res, await archiveMessageSchedule(scheduleId));
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
if (method === "PATCH") {
|
|
1279
|
+
sendJson(res, await updateMessageSchedule(scheduleId, readUpdateMessageScheduleRequest(await readNodeJsonBody(req))));
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1282
|
+
sendJson(res, await getMessageSchedule(scheduleId));
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
const chatMessagesMatch = url.pathname.match(/^\/api\/chats\/([^/]+)\/messages$/);
|
|
1286
|
+
if (chatMessagesMatch) {
|
|
1287
|
+
const chatId = decodeURIComponent(chatMessagesMatch[1]);
|
|
1288
|
+
requireMethod(method, ["GET", "POST"]);
|
|
1289
|
+
if (method === "POST") {
|
|
1290
|
+
sendJson(res, await executeMessage(chatId, readExecuteChatRequest(await readNodeJsonBody(req))), 202);
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1293
|
+
sendJson(res, await listMessages(chatId, readIntegerParam(url.searchParams.get("limit"), 1e3)));
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
const chatInterruptMatch = url.pathname.match(/^\/api\/chats\/([^/]+)\/interrupt$/);
|
|
1297
|
+
if (chatInterruptMatch) {
|
|
1298
|
+
requireMethod(method, ["POST"]);
|
|
1299
|
+
sendJson(res, await interruptChatRun(decodeURIComponent(chatInterruptMatch[1])));
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1302
|
+
const chatForkMatch = url.pathname.match(/^\/api\/chats\/([^/]+)\/fork$/);
|
|
1303
|
+
if (chatForkMatch) {
|
|
1304
|
+
requireMethod(method, ["POST"]);
|
|
1305
|
+
sendJson(res, await forkChat(decodeURIComponent(chatForkMatch[1]), readForkChatRequest(await readNodeJsonBody(req))), 201);
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1308
|
+
const chatCompactMatch = url.pathname.match(/^\/api\/chats\/([^/]+)\/compact$/);
|
|
1309
|
+
if (chatCompactMatch) {
|
|
1310
|
+
requireMethod(method, ["POST"]);
|
|
1311
|
+
sendJson(res, await compactChat(decodeURIComponent(chatCompactMatch[1]), readCompactChatRequest(await readNodeJsonBody(req))));
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
const chatReviewMatch = url.pathname.match(/^\/api\/chats\/([^/]+)\/review$/);
|
|
1315
|
+
if (chatReviewMatch) {
|
|
1316
|
+
requireMethod(method, ["POST"]);
|
|
1317
|
+
sendJson(res, await reviewChat(decodeURIComponent(chatReviewMatch[1]), readReviewChatRequest(await readNodeJsonBody(req))));
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1320
|
+
const chatRefreshMatch = url.pathname.match(/^\/api\/chats\/([^/]+)\/refresh$/);
|
|
1321
|
+
if (chatRefreshMatch) {
|
|
1322
|
+
requireMethod(method, ["POST"]);
|
|
1323
|
+
sendJson(res, await refreshChat(decodeURIComponent(chatRefreshMatch[1])));
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1326
|
+
const serverRequestMatch = url.pathname.match(/^\/api\/chats\/([^/]+)\/server-requests\/([^/]+)$/);
|
|
1327
|
+
if (serverRequestMatch) {
|
|
1328
|
+
requireMethod(method, ["POST"]);
|
|
1329
|
+
sendJson(res, await respondToServerRequest(decodeURIComponent(serverRequestMatch[1]), decodeURIComponent(serverRequestMatch[2]), readServerRequestResponseRequest(await readNodeJsonBody(req))));
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
const queuedRunsReorderMatch = url.pathname.match(/^\/api\/chats\/([^/]+)\/runs\/reorder$/);
|
|
1333
|
+
if (queuedRunsReorderMatch) {
|
|
1334
|
+
requireMethod(method, ["POST"]);
|
|
1335
|
+
sendJson(res, await reorderQueuedChatRuns(decodeURIComponent(queuedRunsReorderMatch[1]), readReorderQueuedChatRunsRequest(await readNodeJsonBody(req))));
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1338
|
+
const queuedRunSteerMatch = url.pathname.match(/^\/api\/chats\/([^/]+)\/runs\/([^/]+)\/steer$/);
|
|
1339
|
+
if (queuedRunSteerMatch) {
|
|
1340
|
+
requireMethod(method, ["POST"]);
|
|
1341
|
+
sendJson(res, await steerQueuedChatRun(decodeURIComponent(queuedRunSteerMatch[1]), decodeURIComponent(queuedRunSteerMatch[2])));
|
|
1342
|
+
return;
|
|
1343
|
+
}
|
|
1344
|
+
const queuedRunMatch = url.pathname.match(/^\/api\/chats\/([^/]+)\/runs\/([^/]+)$/);
|
|
1345
|
+
if (queuedRunMatch) {
|
|
1346
|
+
const chatId = decodeURIComponent(queuedRunMatch[1]);
|
|
1347
|
+
const runId = decodeURIComponent(queuedRunMatch[2]);
|
|
1348
|
+
requireMethod(method, ["DELETE", "PATCH"]);
|
|
1349
|
+
if (method === "DELETE") {
|
|
1350
|
+
sendJson(res, await deleteQueuedChatRun(chatId, runId));
|
|
1351
|
+
return;
|
|
1352
|
+
}
|
|
1353
|
+
sendJson(res, await updateQueuedChatRun(chatId, runId, readUpdateQueuedChatRunRequest(await readNodeJsonBody(req))));
|
|
1354
|
+
return;
|
|
1355
|
+
}
|
|
1356
|
+
const chatMatch = url.pathname.match(/^\/api\/chats\/([^/]+)$/);
|
|
1357
|
+
if (chatMatch) {
|
|
1358
|
+
const chatId = decodeURIComponent(chatMatch[1]);
|
|
1359
|
+
requireMethod(method, ["DELETE", "PATCH"]);
|
|
1360
|
+
if (method === "DELETE") {
|
|
1361
|
+
sendJson(res, await archiveChat(chatId));
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1364
|
+
sendJson(res, await updateChat(chatId, readUpdateChatRequest(await readNodeJsonBody(req))));
|
|
1365
|
+
return;
|
|
1366
|
+
}
|
|
1367
|
+
const accountAuthMatch = url.pathname.match(/^\/api\/provider-accounts\/([^/]+)\/authenticate$/);
|
|
1368
|
+
if (accountAuthMatch) {
|
|
1369
|
+
requireMethod(method, ["POST"]);
|
|
1370
|
+
const body = await readNodeJsonBody(req);
|
|
1371
|
+
sendJson(res, await authenticateAccount(decodeURIComponent(accountAuthMatch[1]), readAuthMode(body.mode)));
|
|
1372
|
+
return;
|
|
1373
|
+
}
|
|
1374
|
+
const accountModelsMatch = url.pathname.match(/^\/api\/provider-accounts\/([^/]+)\/models$/);
|
|
1375
|
+
if (accountModelsMatch) {
|
|
1376
|
+
requireMethod(method, ["GET"]);
|
|
1377
|
+
sendJson(res, await listAccountModels(decodeURIComponent(accountModelsMatch[1])));
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
const accountMatch = url.pathname.match(/^\/api\/provider-accounts\/([^/]+)$/);
|
|
1381
|
+
if (accountMatch) {
|
|
1382
|
+
requireMethod(method, ["DELETE", "PATCH"]);
|
|
1383
|
+
const accountId = decodeURIComponent(accountMatch[1]);
|
|
1384
|
+
if (method === "DELETE") {
|
|
1385
|
+
sendJson(res, await deleteAccount(accountId));
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
sendJson(res, await updateAccount(accountId, readUpdateAccountRequest(await readNodeJsonBody(req))));
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1391
|
+
if (url.pathname === "/api/workspaces") {
|
|
1392
|
+
requireMethod(method, [
|
|
1393
|
+
"DELETE",
|
|
1394
|
+
"GET",
|
|
1395
|
+
"POST"
|
|
1396
|
+
]);
|
|
1397
|
+
if (method === "POST") {
|
|
1398
|
+
sendJson(res, await saveWorkspaceHistory(readStringField((await readNodeJsonBody(req)).path, "path", { required: true })), 201);
|
|
1399
|
+
return;
|
|
1400
|
+
}
|
|
1401
|
+
if (method === "DELETE") {
|
|
1402
|
+
sendJson(res, await deleteWorkspaceHistory(readStringField(url.searchParams.get("path"), "path", { required: true })));
|
|
1403
|
+
return;
|
|
1404
|
+
}
|
|
1405
|
+
sendJson(res, await listWorkspaceHistory());
|
|
1406
|
+
return;
|
|
1407
|
+
}
|
|
1408
|
+
if (url.pathname === "/api/workspaces/directories") {
|
|
1409
|
+
requireMethod(method, ["GET"]);
|
|
1410
|
+
sendJson(res, await listWorkspaceDirectory(url.searchParams.get("path"), url.searchParams.get("hidden") === "1"));
|
|
1411
|
+
return;
|
|
1412
|
+
}
|
|
1413
|
+
if (url.pathname === "/api/workspaces/tree") {
|
|
1414
|
+
requireMethod(method, ["GET"]);
|
|
1415
|
+
sendJson(res, await readWorkspaceTree(url.searchParams.get("path"), url.searchParams.get("hidden") === "1"));
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1418
|
+
if (url.pathname === "/api/workspaces/resource") {
|
|
1419
|
+
requireMethod(method, ["GET"]);
|
|
1420
|
+
sendJson(res, await readWorkspaceResource(url.searchParams.get("path")));
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
if (url.pathname === "/api/cloudflared/status") {
|
|
1424
|
+
requireMethod(method, ["GET"]);
|
|
1425
|
+
sendJson(res, await readCloudflaredStatus());
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1428
|
+
if (url.pathname === "/api/cloudflared/temporary") {
|
|
1429
|
+
requireMethod(method, ["POST"]);
|
|
1430
|
+
sendJson(res, await startTemporaryCloudflaredTunnel(readStringField((await readNodeJsonBody(req)).url, "url", {
|
|
1431
|
+
required: true,
|
|
1432
|
+
maxLength: 2e3
|
|
1433
|
+
})), 201);
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
const cloudflaredTemporaryMatch = url.pathname.match(/^\/api\/cloudflared\/temporary\/([^/]+)$/);
|
|
1437
|
+
if (cloudflaredTemporaryMatch) {
|
|
1438
|
+
requireMethod(method, ["DELETE"]);
|
|
1439
|
+
sendJson(res, await stopTemporaryCloudflaredTunnel(decodeURIComponent(cloudflaredTemporaryMatch[1])));
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1442
|
+
const cloudflaredTunnelMatch = url.pathname.match(/^\/api\/cloudflared\/tunnels\/([^/]+)$/);
|
|
1443
|
+
if (cloudflaredTunnelMatch) {
|
|
1444
|
+
requireMethod(method, ["DELETE"]);
|
|
1445
|
+
sendJson(res, await deleteNamedCloudflaredTunnel(decodeURIComponent(cloudflaredTunnelMatch[1])));
|
|
1446
|
+
return;
|
|
1447
|
+
}
|
|
1448
|
+
if (url.pathname === "/api/git/status") {
|
|
1449
|
+
requireMethod(method, ["GET"]);
|
|
1450
|
+
sendJson(res, await readGitStatus(url.searchParams.get("path") ?? void 0));
|
|
1451
|
+
return;
|
|
1452
|
+
}
|
|
1453
|
+
if (url.pathname === "/api/git/init") {
|
|
1454
|
+
requireMethod(method, ["POST"]);
|
|
1455
|
+
sendJson(res, await initGitRepository(readStringField((await readNodeJsonBody(req)).path, "path", { required: true })), 201);
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1458
|
+
if (url.pathname === "/api/git/stage") {
|
|
1459
|
+
requireMethod(method, ["POST"]);
|
|
1460
|
+
const body = await readNodeJsonBody(req);
|
|
1461
|
+
sendJson(res, await stageGitPaths(readStringField(body.path, "path", { required: true }), readStringArrayField(body.paths, "paths")));
|
|
1462
|
+
return;
|
|
1463
|
+
}
|
|
1464
|
+
if (url.pathname === "/api/git/unstage") {
|
|
1465
|
+
requireMethod(method, ["POST"]);
|
|
1466
|
+
const body = await readNodeJsonBody(req);
|
|
1467
|
+
sendJson(res, await unstageGitPaths(readStringField(body.path, "path", { required: true }), readStringArrayField(body.paths, "paths")));
|
|
1468
|
+
return;
|
|
1469
|
+
}
|
|
1470
|
+
if (url.pathname === "/api/git/discard") {
|
|
1471
|
+
requireMethod(method, ["POST"]);
|
|
1472
|
+
const body = await readNodeJsonBody(req);
|
|
1473
|
+
sendJson(res, await discardGitPaths(readStringField(body.path, "path", { required: true }), readStringArrayField(body.paths, "paths")));
|
|
1474
|
+
return;
|
|
1475
|
+
}
|
|
1476
|
+
if (url.pathname === "/api/git/commit") {
|
|
1477
|
+
requireMethod(method, ["POST"]);
|
|
1478
|
+
const body = await readNodeJsonBody(req);
|
|
1479
|
+
sendJson(res, await commitGitChanges(readStringField(body.path, "path", { required: true }), readStringField(body.message, "message", {
|
|
1480
|
+
required: true,
|
|
1481
|
+
maxLength: 500
|
|
1482
|
+
})));
|
|
1483
|
+
return;
|
|
1484
|
+
}
|
|
1485
|
+
if (url.pathname === "/api/git/pull") {
|
|
1486
|
+
requireMethod(method, ["POST"]);
|
|
1487
|
+
sendJson(res, await pullGitRepository(readStringField((await readNodeJsonBody(req)).path, "path", { required: true })));
|
|
1488
|
+
return;
|
|
1489
|
+
}
|
|
1490
|
+
if (url.pathname === "/api/git/push") {
|
|
1491
|
+
requireMethod(method, ["POST"]);
|
|
1492
|
+
sendJson(res, await pushGitRepository(readStringField((await readNodeJsonBody(req)).path, "path", { required: true })));
|
|
1493
|
+
return;
|
|
1494
|
+
}
|
|
1495
|
+
throw new HttpError(404, "API route not found.");
|
|
1496
|
+
}
|
|
1497
|
+
function readCreateAccountRequest(body) {
|
|
1498
|
+
return {
|
|
1499
|
+
displayName: readStringField(body.displayName, "displayName", { maxLength: 100 }),
|
|
1500
|
+
providerId: readStringField(body.providerId, "providerId", { required: true }),
|
|
1501
|
+
runtimeDefaults: readRecordField(body.runtimeDefaults, "runtimeDefaults"),
|
|
1502
|
+
settings: readRecordField(body.settings, "settings")
|
|
1503
|
+
};
|
|
1504
|
+
}
|
|
1505
|
+
function readPluginSettingsUpdateRequest(body) {
|
|
1506
|
+
return {
|
|
1507
|
+
enabled: readBooleanField(body.enabled, "enabled"),
|
|
1508
|
+
secrets: readRecordField(body.secrets, "secrets"),
|
|
1509
|
+
settings: readRecordField(body.settings, "settings")
|
|
1510
|
+
};
|
|
1511
|
+
}
|
|
1512
|
+
function readCodexInstructionsRequest(body) {
|
|
1513
|
+
if (body.instructions === void 0) return { instructions: "" };
|
|
1514
|
+
if (typeof body.instructions !== "string") throw new HttpError(400, "instructions must be a string.");
|
|
1515
|
+
if (body.instructions.length > 1e5) throw new HttpError(400, "instructions must be 100000 characters or fewer.");
|
|
1516
|
+
return { instructions: body.instructions };
|
|
1517
|
+
}
|
|
1518
|
+
function readCreateChatRequest(body) {
|
|
1519
|
+
return {
|
|
1520
|
+
accountId: readStringField(body.accountId, "accountId", { required: true }),
|
|
1521
|
+
autoRotateAccount: readBooleanField(body.autoRotateAccount, "autoRotateAccount"),
|
|
1522
|
+
collaborationMode: readStringField(body.collaborationMode, "collaborationMode"),
|
|
1523
|
+
model: readStringField(body.model, "model"),
|
|
1524
|
+
permissionMode: readStringField(body.permissionMode, "permissionMode"),
|
|
1525
|
+
providerId: readStringField(body.providerId, "providerId"),
|
|
1526
|
+
reasoningEffort: readStringField(body.reasoningEffort, "reasoningEffort"),
|
|
1527
|
+
serviceTier: readStringField(body.serviceTier, "serviceTier"),
|
|
1528
|
+
title: readStringField(body.title, "title", { maxLength: 160 }),
|
|
1529
|
+
workingDirectory: readStringField(body.workingDirectory, "workingDirectory", { required: true })
|
|
1530
|
+
};
|
|
1531
|
+
}
|
|
1532
|
+
function readCreateMessageScheduleRequest(body) {
|
|
1533
|
+
return {
|
|
1534
|
+
accountId: readStringField(body.accountId, "accountId", { required: true }),
|
|
1535
|
+
collaborationMode: readNullableStringField(body.collaborationMode, "collaborationMode"),
|
|
1536
|
+
firstRunAt: readStringField(body.firstRunAt, "firstRunAt", { required: true }),
|
|
1537
|
+
goalObjective: readNullableStringField(body.goalObjective, "goalObjective"),
|
|
1538
|
+
message: readStringField(body.message, "message", {
|
|
1539
|
+
required: true,
|
|
1540
|
+
maxLength: 2e4
|
|
1541
|
+
}),
|
|
1542
|
+
model: readNullableStringField(body.model, "model"),
|
|
1543
|
+
permissionMode: readNullableStringField(body.permissionMode, "permissionMode"),
|
|
1544
|
+
reasoningEffort: readNullableStringField(body.reasoningEffort, "reasoningEffort"),
|
|
1545
|
+
recurrence: readScheduleRecurrence(body.recurrence),
|
|
1546
|
+
serviceTier: readNullableStringField(body.serviceTier, "serviceTier"),
|
|
1547
|
+
status: readCreateScheduleStatus(body.status),
|
|
1548
|
+
title: readStringField(body.title, "title", { maxLength: 160 }),
|
|
1549
|
+
workingDirectory: readStringField(body.workingDirectory, "workingDirectory", { required: true })
|
|
1550
|
+
};
|
|
1551
|
+
}
|
|
1552
|
+
function readUpdateMessageScheduleRequest(body) {
|
|
1553
|
+
return {
|
|
1554
|
+
accountId: readNullableStringField(body.accountId, "accountId"),
|
|
1555
|
+
collaborationMode: readNullableStringField(body.collaborationMode, "collaborationMode"),
|
|
1556
|
+
firstRunAt: readNullableStringField(body.firstRunAt, "firstRunAt"),
|
|
1557
|
+
goalObjective: readNullableStringField(body.goalObjective, "goalObjective"),
|
|
1558
|
+
message: readStringField(body.message, "message", { maxLength: 2e4 }),
|
|
1559
|
+
model: readNullableStringField(body.model, "model"),
|
|
1560
|
+
permissionMode: readNullableStringField(body.permissionMode, "permissionMode"),
|
|
1561
|
+
reasoningEffort: readNullableStringField(body.reasoningEffort, "reasoningEffort"),
|
|
1562
|
+
recurrence: readScheduleRecurrence(body.recurrence),
|
|
1563
|
+
serviceTier: readNullableStringField(body.serviceTier, "serviceTier"),
|
|
1564
|
+
status: readScheduleStatus(body.status),
|
|
1565
|
+
title: readStringField(body.title, "title", { maxLength: 160 })
|
|
1566
|
+
};
|
|
1567
|
+
}
|
|
1568
|
+
function readUpdateChatRequest(body) {
|
|
1569
|
+
return {
|
|
1570
|
+
accountId: readNullableStringField(body.accountId, "accountId"),
|
|
1571
|
+
autoRotateAccount: readBooleanField(body.autoRotateAccount, "autoRotateAccount"),
|
|
1572
|
+
collaborationMode: readNullableStringField(body.collaborationMode, "collaborationMode"),
|
|
1573
|
+
model: readNullableStringField(body.model, "model"),
|
|
1574
|
+
permissionMode: readNullableStringField(body.permissionMode, "permissionMode"),
|
|
1575
|
+
reasoningEffort: readNullableStringField(body.reasoningEffort, "reasoningEffort"),
|
|
1576
|
+
serviceTier: readNullableStringField(body.serviceTier, "serviceTier"),
|
|
1577
|
+
title: readStringField(body.title, "title", { maxLength: 160 }),
|
|
1578
|
+
workingDirectory: readStringField(body.workingDirectory, "workingDirectory")
|
|
1579
|
+
};
|
|
1580
|
+
}
|
|
1581
|
+
function readForkChatRequest(body) {
|
|
1582
|
+
return { lastTurnId: readNullableStringField(body.lastTurnId, "lastTurnId") };
|
|
1583
|
+
}
|
|
1584
|
+
function readCompactChatRequest(_body) {
|
|
1585
|
+
return {};
|
|
1586
|
+
}
|
|
1587
|
+
function readReviewChatRequest(body) {
|
|
1588
|
+
const target = body.target;
|
|
1589
|
+
if (target !== void 0 && target !== null && target !== "uncommittedChanges" && target !== "baseBranch" && target !== "commit" && target !== "custom") throw new HttpError(400, "target must be uncommittedChanges, baseBranch, commit, or custom.");
|
|
1590
|
+
const delivery = body.delivery;
|
|
1591
|
+
if (delivery !== void 0 && delivery !== null && delivery !== "inline" && delivery !== "detached") throw new HttpError(400, "delivery must be inline or detached.");
|
|
1592
|
+
return {
|
|
1593
|
+
baseBranch: readNullableStringField(body.baseBranch, "baseBranch"),
|
|
1594
|
+
commitSha: readNullableStringField(body.commitSha, "commitSha"),
|
|
1595
|
+
commitTitle: readNullableStringField(body.commitTitle, "commitTitle"),
|
|
1596
|
+
delivery,
|
|
1597
|
+
instructions: readNullableStringField(body.instructions, "instructions"),
|
|
1598
|
+
target
|
|
1599
|
+
};
|
|
1600
|
+
}
|
|
1601
|
+
function readExecuteChatRequest(body) {
|
|
1602
|
+
return {
|
|
1603
|
+
accountId: readStringField(body.accountId, "accountId"),
|
|
1604
|
+
attachments: readChatAttachments(body.attachments),
|
|
1605
|
+
collaborationMode: readNullableStringField(body.collaborationMode, "collaborationMode"),
|
|
1606
|
+
content: readStringField(body.content, "content", { required: true }),
|
|
1607
|
+
delivery: body.delivery === "queue" || body.delivery === "steer" ? body.delivery : void 0,
|
|
1608
|
+
goalObjective: readNullableStringField(body.goalObjective, "goalObjective"),
|
|
1609
|
+
metadata: readRecordField(body.metadata, "metadata"),
|
|
1610
|
+
permissionMode: readNullableStringField(body.permissionMode, "permissionMode")
|
|
1611
|
+
};
|
|
1612
|
+
}
|
|
1613
|
+
function readUpdateQueuedChatRunRequest(body) {
|
|
1614
|
+
return { content: readStringField(body.content, "content", { required: true }) };
|
|
1615
|
+
}
|
|
1616
|
+
function readReorderQueuedChatRunsRequest(body) {
|
|
1617
|
+
if (!Array.isArray(body.runIds)) throw new HttpError(400, "runIds must be an array.");
|
|
1618
|
+
return { runIds: body.runIds.map((runId) => readStringField(runId, "runIds[]", { required: true })).filter((runId, index, runIds) => runIds.indexOf(runId) === index) };
|
|
1619
|
+
}
|
|
1620
|
+
function readServerRequestResponseRequest(body) {
|
|
1621
|
+
const kind = body.kind;
|
|
1622
|
+
if (kind !== "approval" && kind !== "permissions" && kind !== "userInput") throw new HttpError(400, "kind must be approval, permissions, or userInput.");
|
|
1623
|
+
return {
|
|
1624
|
+
decision: body.decision,
|
|
1625
|
+
kind,
|
|
1626
|
+
result: body.result
|
|
1627
|
+
};
|
|
1628
|
+
}
|
|
1629
|
+
function readChatAttachments(value) {
|
|
1630
|
+
if (value === void 0) return;
|
|
1631
|
+
if (!Array.isArray(value)) throw new HttpError(400, "attachments must be an array.");
|
|
1632
|
+
return value.slice(0, 20).map((item, index) => {
|
|
1633
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) throw new HttpError(400, `attachments[${index}] must be an object.`);
|
|
1634
|
+
const record = item;
|
|
1635
|
+
const kind = record.kind;
|
|
1636
|
+
if (kind !== "file" && kind !== "folder" && kind !== "image") throw new HttpError(400, `attachments[${index}].kind is invalid.`);
|
|
1637
|
+
const size = record.size;
|
|
1638
|
+
return {
|
|
1639
|
+
dataUrl: readStringField(record.dataUrl, `attachments[${index}].dataUrl`),
|
|
1640
|
+
kind,
|
|
1641
|
+
mimeType: readStringField(record.mimeType, `attachments[${index}].mimeType`),
|
|
1642
|
+
name: readStringField(record.name, `attachments[${index}].name`, {
|
|
1643
|
+
required: true,
|
|
1644
|
+
maxLength: 240
|
|
1645
|
+
}),
|
|
1646
|
+
path: readStringField(record.path, `attachments[${index}].path`, { maxLength: 1e3 }),
|
|
1647
|
+
size: typeof size === "number" && Number.isFinite(size) ? size : void 0
|
|
1648
|
+
};
|
|
1649
|
+
});
|
|
1650
|
+
}
|
|
1651
|
+
function readUpdateAccountRequest(body) {
|
|
1652
|
+
return {
|
|
1653
|
+
displayName: readStringField(body.displayName, "displayName", { maxLength: 100 }),
|
|
1654
|
+
runtimeDefaults: readRecordField(body.runtimeDefaults, "runtimeDefaults"),
|
|
1655
|
+
settings: readRecordField(body.settings, "settings")
|
|
1656
|
+
};
|
|
1657
|
+
}
|
|
1658
|
+
function readScheduleRecurrence(value) {
|
|
1659
|
+
if (value === void 0) return;
|
|
1660
|
+
const record = readRecordField(value, "recurrence") ?? {};
|
|
1661
|
+
const frequency = record.frequency;
|
|
1662
|
+
if (frequency !== void 0 && frequency !== "none" && frequency !== "daily" && frequency !== "weekly" && frequency !== "monthly") throw new HttpError(400, "recurrence.frequency must be none, daily, weekly, or monthly.");
|
|
1663
|
+
return {
|
|
1664
|
+
anchorDay: readOptionalNumber(record.anchorDay, "recurrence.anchorDay") ?? void 0,
|
|
1665
|
+
endAt: readNullableStringField(record.endAt, "recurrence.endAt"),
|
|
1666
|
+
frequency,
|
|
1667
|
+
interval: readOptionalNumber(record.interval, "recurrence.interval") ?? void 0,
|
|
1668
|
+
maxRuns: readOptionalNumber(record.maxRuns, "recurrence.maxRuns")
|
|
1669
|
+
};
|
|
1670
|
+
}
|
|
1671
|
+
function readCreateScheduleStatus(value) {
|
|
1672
|
+
if (value === void 0) return;
|
|
1673
|
+
if (value === "ACTIVE" || value === "PAUSED") return value;
|
|
1674
|
+
throw new HttpError(400, "status must be ACTIVE or PAUSED.");
|
|
1675
|
+
}
|
|
1676
|
+
function readScheduleStatus(value) {
|
|
1677
|
+
if (value === void 0) return;
|
|
1678
|
+
if (value === "ACTIVE" || value === "PAUSED" || value === "COMPLETED" || value === "ARCHIVED") return value;
|
|
1679
|
+
throw new HttpError(400, "status must be ACTIVE, PAUSED, COMPLETED, or ARCHIVED.");
|
|
1680
|
+
}
|
|
1681
|
+
function readOptionalNumber(value, field) {
|
|
1682
|
+
if (value === void 0) return;
|
|
1683
|
+
if (value === null || value === "") return null;
|
|
1684
|
+
if (typeof value !== "number" || !Number.isFinite(value)) throw new HttpError(400, `${field} must be a number.`);
|
|
1685
|
+
return value;
|
|
1686
|
+
}
|
|
1687
|
+
function readNullableStringField(value, field) {
|
|
1688
|
+
if (value === null) return null;
|
|
1689
|
+
return readStringField(value, field);
|
|
1690
|
+
}
|
|
1691
|
+
function readStringArrayField(value, field) {
|
|
1692
|
+
if (value === void 0) return [];
|
|
1693
|
+
if (!Array.isArray(value)) throw new HttpError(400, `${field} must be an array.`);
|
|
1694
|
+
return value.map((item, index) => readStringField(item, `${field}[${index}]`, { required: true }));
|
|
1695
|
+
}
|
|
1696
|
+
function readAuthMode(value) {
|
|
1697
|
+
if (value === void 0 || value === null) return "browser";
|
|
1698
|
+
if (value === "browser" || value === "device" || value === "local") return value;
|
|
1699
|
+
throw new HttpError(400, "mode must be browser, device, or local.");
|
|
1700
|
+
}
|
|
1701
|
+
function readIntegerParam(value, fallback) {
|
|
1702
|
+
if (!value) return fallback;
|
|
1703
|
+
const parsed = Number.parseInt(value, 10);
|
|
1704
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
1705
|
+
}
|
|
1706
|
+
async function readNodeJsonBody(req) {
|
|
1707
|
+
const chunks = [];
|
|
1708
|
+
for await (const chunk of req) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
1709
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
1710
|
+
if (!text.trim()) return {};
|
|
1711
|
+
try {
|
|
1712
|
+
const value = JSON.parse(text);
|
|
1713
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new HttpError(400, "JSON body must be an object.");
|
|
1714
|
+
return value;
|
|
1715
|
+
} catch (error) {
|
|
1716
|
+
if (error instanceof HttpError) throw error;
|
|
1717
|
+
throw new HttpError(400, "Request body must be valid JSON.");
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
function requireMethod(method, allowed) {
|
|
1721
|
+
if (!allowed.includes(method)) throw new HttpError(405, `${method} is not supported for this route.`);
|
|
1722
|
+
}
|
|
1723
|
+
function sendJson(res, data, status = 200) {
|
|
1724
|
+
res.statusCode = status;
|
|
1725
|
+
res.setHeader("Content-Type", "application/json");
|
|
1726
|
+
res.end(JSON.stringify(data));
|
|
1727
|
+
}
|
|
1728
|
+
function sendRouteError(res, error) {
|
|
1729
|
+
if (error instanceof HttpError) {
|
|
1730
|
+
sendJson(res, { error: error.message }, error.status);
|
|
1731
|
+
return;
|
|
1732
|
+
}
|
|
1733
|
+
sendJson(res, { error: error instanceof Error ? error.message : "Request failed." }, 500);
|
|
1734
|
+
}
|
|
1735
|
+
//#endregion
|
|
1736
|
+
export { handleApiRequest, installApiServer, sendRouteError };
|