codeharbor 0.1.0 → 0.1.2
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/.env.example +78 -0
- package/README.md +229 -2
- package/dist/cli.js +2792 -49
- package/package.json +18 -5
package/dist/cli.js
CHANGED
|
@@ -24,16 +24,1770 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
24
24
|
));
|
|
25
25
|
|
|
26
26
|
// src/cli.ts
|
|
27
|
+
var import_node_fs9 = __toESM(require("fs"));
|
|
28
|
+
var import_node_path11 = __toESM(require("path"));
|
|
27
29
|
var import_commander = require("commander");
|
|
28
30
|
|
|
29
31
|
// src/app.ts
|
|
30
|
-
var
|
|
32
|
+
var import_node_child_process3 = require("child_process");
|
|
33
|
+
var import_node_util2 = require("util");
|
|
34
|
+
|
|
35
|
+
// src/admin-server.ts
|
|
36
|
+
var import_node_child_process = require("child_process");
|
|
37
|
+
var import_node_fs2 = __toESM(require("fs"));
|
|
38
|
+
var import_node_http = __toESM(require("http"));
|
|
39
|
+
var import_node_path2 = __toESM(require("path"));
|
|
31
40
|
var import_node_util = require("util");
|
|
32
41
|
|
|
42
|
+
// src/init.ts
|
|
43
|
+
var import_node_fs = __toESM(require("fs"));
|
|
44
|
+
var import_node_path = __toESM(require("path"));
|
|
45
|
+
var import_promises = require("readline/promises");
|
|
46
|
+
var import_dotenv = __toESM(require("dotenv"));
|
|
47
|
+
async function runInitCommand(options = {}) {
|
|
48
|
+
const cwd = options.cwd ?? process.cwd();
|
|
49
|
+
const envPath = import_node_path.default.resolve(cwd, ".env");
|
|
50
|
+
const templatePath = resolveInitTemplatePath(cwd);
|
|
51
|
+
const input = options.input ?? process.stdin;
|
|
52
|
+
const output = options.output ?? process.stdout;
|
|
53
|
+
const templateContent = import_node_fs.default.readFileSync(templatePath, "utf8");
|
|
54
|
+
const existingContent = import_node_fs.default.existsSync(envPath) ? import_node_fs.default.readFileSync(envPath, "utf8") : "";
|
|
55
|
+
const existingValues = existingContent ? import_dotenv.default.parse(existingContent) : {};
|
|
56
|
+
const rl = (0, import_promises.createInterface)({ input, output });
|
|
57
|
+
try {
|
|
58
|
+
if (existingContent && !options.force) {
|
|
59
|
+
const overwrite = await askYesNo(
|
|
60
|
+
rl,
|
|
61
|
+
"Detected existing .env file. Overwrite with guided setup?",
|
|
62
|
+
false
|
|
63
|
+
);
|
|
64
|
+
if (!overwrite) {
|
|
65
|
+
output.write("Init aborted. Keep existing .env unchanged.\n");
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
output.write("CodeHarbor setup wizard\n");
|
|
70
|
+
output.write(`Target file: ${envPath}
|
|
71
|
+
`);
|
|
72
|
+
const questions = [
|
|
73
|
+
{
|
|
74
|
+
key: "MATRIX_HOMESERVER",
|
|
75
|
+
label: "Matrix homeserver URL",
|
|
76
|
+
required: true,
|
|
77
|
+
validate: (value) => {
|
|
78
|
+
try {
|
|
79
|
+
new URL(value);
|
|
80
|
+
return null;
|
|
81
|
+
} catch {
|
|
82
|
+
return "Please enter a valid URL, for example https://matrix.example.com";
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
key: "MATRIX_USER_ID",
|
|
88
|
+
label: "Matrix bot user id",
|
|
89
|
+
required: true,
|
|
90
|
+
validate: (value) => {
|
|
91
|
+
if (!/^@[^:\s]+:.+/.test(value)) {
|
|
92
|
+
return "Please enter a Matrix user id like @bot:example.com";
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
key: "MATRIX_ACCESS_TOKEN",
|
|
99
|
+
label: "Matrix access token",
|
|
100
|
+
required: true,
|
|
101
|
+
hiddenDefault: true
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
key: "MATRIX_COMMAND_PREFIX",
|
|
105
|
+
label: "Group command prefix",
|
|
106
|
+
fallbackValue: "!code"
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
key: "CODEX_BIN",
|
|
110
|
+
label: "Codex binary",
|
|
111
|
+
fallbackValue: "codex"
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
key: "CODEX_WORKDIR",
|
|
115
|
+
label: "Codex working directory",
|
|
116
|
+
fallbackValue: cwd,
|
|
117
|
+
validate: (value) => {
|
|
118
|
+
const resolved = import_node_path.default.resolve(cwd, value);
|
|
119
|
+
if (!import_node_fs.default.existsSync(resolved) || !import_node_fs.default.statSync(resolved).isDirectory()) {
|
|
120
|
+
return `Directory does not exist: ${resolved}`;
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
];
|
|
126
|
+
const updates = {};
|
|
127
|
+
for (const question of questions) {
|
|
128
|
+
const existingValue = (existingValues[question.key] ?? "").trim();
|
|
129
|
+
const value = await askValue(rl, question, existingValue);
|
|
130
|
+
updates[question.key] = value;
|
|
131
|
+
}
|
|
132
|
+
const mergedContent = applyEnvOverrides(templateContent, updates);
|
|
133
|
+
import_node_fs.default.writeFileSync(envPath, mergedContent, "utf8");
|
|
134
|
+
output.write(`Wrote ${envPath}
|
|
135
|
+
`);
|
|
136
|
+
output.write("Next steps:\n");
|
|
137
|
+
output.write("1. codex login\n");
|
|
138
|
+
output.write("2. codeharbor doctor\n");
|
|
139
|
+
output.write("3. codeharbor start\n");
|
|
140
|
+
} finally {
|
|
141
|
+
rl.close();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function resolveInitTemplatePath(cwd) {
|
|
145
|
+
const candidates = [
|
|
146
|
+
import_node_path.default.resolve(cwd, ".env.example"),
|
|
147
|
+
import_node_path.default.resolve(__dirname, "..", ".env.example")
|
|
148
|
+
];
|
|
149
|
+
for (const candidate of candidates) {
|
|
150
|
+
if (import_node_fs.default.existsSync(candidate)) {
|
|
151
|
+
return candidate;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
throw new Error(`Cannot find template file. Tried: ${candidates.join(", ")}`);
|
|
155
|
+
}
|
|
156
|
+
function applyEnvOverrides(template, overrides) {
|
|
157
|
+
const lines = template.split(/\r?\n/);
|
|
158
|
+
const seen = /* @__PURE__ */ new Set();
|
|
159
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
160
|
+
const line = lines[i];
|
|
161
|
+
const match = /^([A-Z0-9_]+)=(.*)$/.exec(line.trim());
|
|
162
|
+
if (!match) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const key = match[1];
|
|
166
|
+
if (!(key in overrides)) {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
lines[i] = `${key}=${formatEnvValue(overrides[key] ?? "")}`;
|
|
170
|
+
seen.add(key);
|
|
171
|
+
}
|
|
172
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
173
|
+
if (seen.has(key)) {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
lines.push(`${key}=${formatEnvValue(value)}`);
|
|
177
|
+
}
|
|
178
|
+
const content = lines.join("\n");
|
|
179
|
+
return content.endsWith("\n") ? content : `${content}
|
|
180
|
+
`;
|
|
181
|
+
}
|
|
182
|
+
function formatEnvValue(value) {
|
|
183
|
+
if (!value) {
|
|
184
|
+
return "";
|
|
185
|
+
}
|
|
186
|
+
if (/^[A-Za-z0-9_./:@+-]+$/.test(value)) {
|
|
187
|
+
return value;
|
|
188
|
+
}
|
|
189
|
+
return JSON.stringify(value);
|
|
190
|
+
}
|
|
191
|
+
async function askValue(rl, question, existingValue) {
|
|
192
|
+
while (true) {
|
|
193
|
+
const fallback = question.fallbackValue ?? "";
|
|
194
|
+
const displayDefault = existingValue || fallback;
|
|
195
|
+
const hint = displayDefault ? question.hiddenDefault ? "[already set]" : `[${displayDefault}]` : "";
|
|
196
|
+
const answer = (await rl.question(`${question.label} ${hint}: `)).trim();
|
|
197
|
+
const finalValue = answer || existingValue || fallback;
|
|
198
|
+
if (question.required && !finalValue) {
|
|
199
|
+
rl.write("This value is required.\n");
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (question.validate) {
|
|
203
|
+
const reason = question.validate(finalValue);
|
|
204
|
+
if (reason) {
|
|
205
|
+
rl.write(`${reason}
|
|
206
|
+
`);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return finalValue;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
async function askYesNo(rl, question, defaultValue) {
|
|
214
|
+
const defaultHint = defaultValue ? "[Y/n]" : "[y/N]";
|
|
215
|
+
const answer = (await rl.question(`${question} ${defaultHint}: `)).trim().toLowerCase();
|
|
216
|
+
if (!answer) {
|
|
217
|
+
return defaultValue;
|
|
218
|
+
}
|
|
219
|
+
return answer === "y" || answer === "yes";
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// src/admin-server.ts
|
|
223
|
+
var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
|
|
224
|
+
var HttpError = class extends Error {
|
|
225
|
+
statusCode;
|
|
226
|
+
constructor(statusCode, message) {
|
|
227
|
+
super(message);
|
|
228
|
+
this.statusCode = statusCode;
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
var AdminServer = class {
|
|
232
|
+
config;
|
|
233
|
+
logger;
|
|
234
|
+
stateStore;
|
|
235
|
+
configService;
|
|
236
|
+
host;
|
|
237
|
+
port;
|
|
238
|
+
adminToken;
|
|
239
|
+
adminIpAllowlist;
|
|
240
|
+
adminAllowedOrigins;
|
|
241
|
+
cwd;
|
|
242
|
+
checkCodex;
|
|
243
|
+
checkMatrix;
|
|
244
|
+
server = null;
|
|
245
|
+
address = null;
|
|
246
|
+
constructor(config, logger, stateStore, configService, options) {
|
|
247
|
+
this.config = config;
|
|
248
|
+
this.logger = logger;
|
|
249
|
+
this.stateStore = stateStore;
|
|
250
|
+
this.configService = configService;
|
|
251
|
+
this.host = options.host;
|
|
252
|
+
this.port = options.port;
|
|
253
|
+
this.adminToken = options.adminToken;
|
|
254
|
+
this.adminIpAllowlist = normalizeAllowlist(options.adminIpAllowlist ?? []);
|
|
255
|
+
this.adminAllowedOrigins = normalizeOriginAllowlist(options.adminAllowedOrigins ?? []);
|
|
256
|
+
this.cwd = options.cwd ?? process.cwd();
|
|
257
|
+
this.checkCodex = options.checkCodex ?? defaultCheckCodex;
|
|
258
|
+
this.checkMatrix = options.checkMatrix ?? defaultCheckMatrix;
|
|
259
|
+
}
|
|
260
|
+
getAddress() {
|
|
261
|
+
return this.address;
|
|
262
|
+
}
|
|
263
|
+
async start() {
|
|
264
|
+
if (this.server) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
this.server = import_node_http.default.createServer((req, res) => {
|
|
268
|
+
void this.handleRequest(req, res);
|
|
269
|
+
});
|
|
270
|
+
await new Promise((resolve, reject) => {
|
|
271
|
+
if (!this.server) {
|
|
272
|
+
reject(new Error("admin server is not initialized"));
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
this.server.once("error", reject);
|
|
276
|
+
this.server.listen(this.port, this.host, () => {
|
|
277
|
+
this.server?.removeListener("error", reject);
|
|
278
|
+
const address = this.server?.address();
|
|
279
|
+
if (!address || typeof address === "string") {
|
|
280
|
+
reject(new Error("failed to resolve admin server address"));
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
this.address = {
|
|
284
|
+
host: address.address,
|
|
285
|
+
port: address.port
|
|
286
|
+
};
|
|
287
|
+
resolve();
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
async stop() {
|
|
292
|
+
if (!this.server) {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
const server = this.server;
|
|
296
|
+
this.server = null;
|
|
297
|
+
this.address = null;
|
|
298
|
+
await new Promise((resolve, reject) => {
|
|
299
|
+
server.close((error) => {
|
|
300
|
+
if (error) {
|
|
301
|
+
reject(error);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
resolve();
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
async handleRequest(req, res) {
|
|
309
|
+
try {
|
|
310
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
311
|
+
this.setSecurityHeaders(res);
|
|
312
|
+
const corsDecision = this.resolveCors(req);
|
|
313
|
+
this.setCorsHeaders(res, corsDecision);
|
|
314
|
+
if (!this.isClientAllowed(req)) {
|
|
315
|
+
this.sendJson(res, 403, {
|
|
316
|
+
ok: false,
|
|
317
|
+
error: "Forbidden by ADMIN_IP_ALLOWLIST."
|
|
318
|
+
});
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
if (url.pathname.startsWith("/api/admin/") && corsDecision.origin && !corsDecision.allowed) {
|
|
322
|
+
this.sendJson(res, 403, {
|
|
323
|
+
ok: false,
|
|
324
|
+
error: "Forbidden by ADMIN_ALLOWED_ORIGINS."
|
|
325
|
+
});
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
if (req.method === "OPTIONS") {
|
|
329
|
+
if (corsDecision.origin && !corsDecision.allowed) {
|
|
330
|
+
this.sendJson(res, 403, {
|
|
331
|
+
ok: false,
|
|
332
|
+
error: "Forbidden by ADMIN_ALLOWED_ORIGINS."
|
|
333
|
+
});
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
res.writeHead(204);
|
|
337
|
+
res.end();
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
if (req.method === "GET" && isUiPath(url.pathname)) {
|
|
341
|
+
this.sendHtml(res, renderAdminConsoleHtml());
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
if (url.pathname.startsWith("/api/admin/") && !this.isAuthorized(req)) {
|
|
345
|
+
this.sendJson(res, 401, {
|
|
346
|
+
ok: false,
|
|
347
|
+
error: "Unauthorized. Provide Authorization: Bearer <ADMIN_TOKEN>."
|
|
348
|
+
});
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
if (req.method === "GET" && url.pathname === "/api/admin/config/global") {
|
|
352
|
+
this.sendJson(res, 200, {
|
|
353
|
+
ok: true,
|
|
354
|
+
data: buildGlobalConfigSnapshot(this.config),
|
|
355
|
+
effective: "next_start_for_env_changes"
|
|
356
|
+
});
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
if (req.method === "PUT" && url.pathname === "/api/admin/config/global") {
|
|
360
|
+
const body = await readJsonBody(req);
|
|
361
|
+
const actor = readActor(req);
|
|
362
|
+
const result = this.updateGlobalConfig(body, actor);
|
|
363
|
+
this.sendJson(res, 200, {
|
|
364
|
+
ok: true,
|
|
365
|
+
...result
|
|
366
|
+
});
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
if (req.method === "GET" && url.pathname === "/api/admin/config/rooms") {
|
|
370
|
+
this.sendJson(res, 200, {
|
|
371
|
+
ok: true,
|
|
372
|
+
data: this.configService.listRoomSettings()
|
|
373
|
+
});
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
const roomMatch = /^\/api\/admin\/config\/rooms\/(.+)$/.exec(url.pathname);
|
|
377
|
+
if (roomMatch) {
|
|
378
|
+
const roomId = decodeURIComponent(roomMatch[1]);
|
|
379
|
+
if (req.method === "GET") {
|
|
380
|
+
const room = this.configService.getRoomSettings(roomId);
|
|
381
|
+
if (!room) {
|
|
382
|
+
throw new HttpError(404, `room settings not found for ${roomId}`);
|
|
383
|
+
}
|
|
384
|
+
this.sendJson(res, 200, { ok: true, data: room });
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
if (req.method === "PUT") {
|
|
388
|
+
const body = await readJsonBody(req);
|
|
389
|
+
const actor = readActor(req);
|
|
390
|
+
const room = this.updateRoomConfig(roomId, body, actor);
|
|
391
|
+
this.sendJson(res, 200, { ok: true, data: room });
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
if (req.method === "DELETE") {
|
|
395
|
+
const actor = readActor(req);
|
|
396
|
+
this.configService.deleteRoomSettings(roomId, actor);
|
|
397
|
+
this.sendJson(res, 200, { ok: true, roomId });
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
if (req.method === "GET" && url.pathname === "/api/admin/audit") {
|
|
402
|
+
const limit = normalizePositiveInt(url.searchParams.get("limit"), 20, 1, 200);
|
|
403
|
+
this.sendJson(res, 200, {
|
|
404
|
+
ok: true,
|
|
405
|
+
data: this.stateStore.listConfigRevisions(limit).map((entry) => formatAuditEntry(entry))
|
|
406
|
+
});
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
if (req.method === "GET" && url.pathname === "/api/admin/health") {
|
|
410
|
+
const [codex, matrix] = await Promise.all([
|
|
411
|
+
this.checkCodex(this.config.codexBin),
|
|
412
|
+
this.checkMatrix(this.config.matrixHomeserver, this.config.doctorHttpTimeoutMs)
|
|
413
|
+
]);
|
|
414
|
+
this.sendJson(res, 200, {
|
|
415
|
+
ok: codex.ok && matrix.ok,
|
|
416
|
+
codex,
|
|
417
|
+
matrix,
|
|
418
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
419
|
+
});
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
this.sendJson(res, 404, {
|
|
423
|
+
ok: false,
|
|
424
|
+
error: `Not found: ${req.method ?? "GET"} ${url.pathname}`
|
|
425
|
+
});
|
|
426
|
+
} catch (error) {
|
|
427
|
+
if (error instanceof HttpError) {
|
|
428
|
+
this.sendJson(res, error.statusCode, {
|
|
429
|
+
ok: false,
|
|
430
|
+
error: error.message
|
|
431
|
+
});
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
this.logger.error("Admin API request failed", error);
|
|
435
|
+
this.sendJson(res, 500, {
|
|
436
|
+
ok: false,
|
|
437
|
+
error: formatError(error)
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
updateGlobalConfig(rawBody, actor) {
|
|
442
|
+
const body = asObject(rawBody, "global config payload");
|
|
443
|
+
const envUpdates = {};
|
|
444
|
+
const updatedKeys = [];
|
|
445
|
+
if ("matrixCommandPrefix" in body) {
|
|
446
|
+
const value = String(body.matrixCommandPrefix ?? "");
|
|
447
|
+
this.config.matrixCommandPrefix = value;
|
|
448
|
+
envUpdates.MATRIX_COMMAND_PREFIX = value;
|
|
449
|
+
updatedKeys.push("matrixCommandPrefix");
|
|
450
|
+
}
|
|
451
|
+
if ("codexWorkdir" in body) {
|
|
452
|
+
const workdir = import_node_path2.default.resolve(String(body.codexWorkdir ?? "").trim());
|
|
453
|
+
ensureDirectory(workdir, "codexWorkdir");
|
|
454
|
+
this.config.codexWorkdir = workdir;
|
|
455
|
+
envUpdates.CODEX_WORKDIR = workdir;
|
|
456
|
+
updatedKeys.push("codexWorkdir");
|
|
457
|
+
}
|
|
458
|
+
if ("rateLimiter" in body) {
|
|
459
|
+
const limiter = asObject(body.rateLimiter, "rateLimiter");
|
|
460
|
+
if ("windowMs" in limiter) {
|
|
461
|
+
const value = normalizePositiveInt(limiter.windowMs, this.config.rateLimiter.windowMs, 1, Number.MAX_SAFE_INTEGER);
|
|
462
|
+
this.config.rateLimiter.windowMs = value;
|
|
463
|
+
envUpdates.RATE_LIMIT_WINDOW_SECONDS = String(Math.max(1, Math.round(value / 1e3)));
|
|
464
|
+
updatedKeys.push("rateLimiter.windowMs");
|
|
465
|
+
}
|
|
466
|
+
if ("maxRequestsPerUser" in limiter) {
|
|
467
|
+
const value = normalizeNonNegativeInt(limiter.maxRequestsPerUser, this.config.rateLimiter.maxRequestsPerUser);
|
|
468
|
+
this.config.rateLimiter.maxRequestsPerUser = value;
|
|
469
|
+
envUpdates.RATE_LIMIT_MAX_REQUESTS_PER_USER = String(value);
|
|
470
|
+
updatedKeys.push("rateLimiter.maxRequestsPerUser");
|
|
471
|
+
}
|
|
472
|
+
if ("maxRequestsPerRoom" in limiter) {
|
|
473
|
+
const value = normalizeNonNegativeInt(limiter.maxRequestsPerRoom, this.config.rateLimiter.maxRequestsPerRoom);
|
|
474
|
+
this.config.rateLimiter.maxRequestsPerRoom = value;
|
|
475
|
+
envUpdates.RATE_LIMIT_MAX_REQUESTS_PER_ROOM = String(value);
|
|
476
|
+
updatedKeys.push("rateLimiter.maxRequestsPerRoom");
|
|
477
|
+
}
|
|
478
|
+
if ("maxConcurrentGlobal" in limiter) {
|
|
479
|
+
const value = normalizeNonNegativeInt(limiter.maxConcurrentGlobal, this.config.rateLimiter.maxConcurrentGlobal);
|
|
480
|
+
this.config.rateLimiter.maxConcurrentGlobal = value;
|
|
481
|
+
envUpdates.RATE_LIMIT_MAX_CONCURRENT_GLOBAL = String(value);
|
|
482
|
+
updatedKeys.push("rateLimiter.maxConcurrentGlobal");
|
|
483
|
+
}
|
|
484
|
+
if ("maxConcurrentPerUser" in limiter) {
|
|
485
|
+
const value = normalizeNonNegativeInt(limiter.maxConcurrentPerUser, this.config.rateLimiter.maxConcurrentPerUser);
|
|
486
|
+
this.config.rateLimiter.maxConcurrentPerUser = value;
|
|
487
|
+
envUpdates.RATE_LIMIT_MAX_CONCURRENT_PER_USER = String(value);
|
|
488
|
+
updatedKeys.push("rateLimiter.maxConcurrentPerUser");
|
|
489
|
+
}
|
|
490
|
+
if ("maxConcurrentPerRoom" in limiter) {
|
|
491
|
+
const value = normalizeNonNegativeInt(limiter.maxConcurrentPerRoom, this.config.rateLimiter.maxConcurrentPerRoom);
|
|
492
|
+
this.config.rateLimiter.maxConcurrentPerRoom = value;
|
|
493
|
+
envUpdates.RATE_LIMIT_MAX_CONCURRENT_PER_ROOM = String(value);
|
|
494
|
+
updatedKeys.push("rateLimiter.maxConcurrentPerRoom");
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
if ("defaultGroupTriggerPolicy" in body) {
|
|
498
|
+
const policy = asObject(body.defaultGroupTriggerPolicy, "defaultGroupTriggerPolicy");
|
|
499
|
+
if ("allowMention" in policy) {
|
|
500
|
+
const value = normalizeBoolean(policy.allowMention, this.config.defaultGroupTriggerPolicy.allowMention);
|
|
501
|
+
this.config.defaultGroupTriggerPolicy.allowMention = value;
|
|
502
|
+
envUpdates.GROUP_TRIGGER_ALLOW_MENTION = String(value);
|
|
503
|
+
updatedKeys.push("defaultGroupTriggerPolicy.allowMention");
|
|
504
|
+
}
|
|
505
|
+
if ("allowReply" in policy) {
|
|
506
|
+
const value = normalizeBoolean(policy.allowReply, this.config.defaultGroupTriggerPolicy.allowReply);
|
|
507
|
+
this.config.defaultGroupTriggerPolicy.allowReply = value;
|
|
508
|
+
envUpdates.GROUP_TRIGGER_ALLOW_REPLY = String(value);
|
|
509
|
+
updatedKeys.push("defaultGroupTriggerPolicy.allowReply");
|
|
510
|
+
}
|
|
511
|
+
if ("allowActiveWindow" in policy) {
|
|
512
|
+
const value = normalizeBoolean(policy.allowActiveWindow, this.config.defaultGroupTriggerPolicy.allowActiveWindow);
|
|
513
|
+
this.config.defaultGroupTriggerPolicy.allowActiveWindow = value;
|
|
514
|
+
envUpdates.GROUP_TRIGGER_ALLOW_ACTIVE_WINDOW = String(value);
|
|
515
|
+
updatedKeys.push("defaultGroupTriggerPolicy.allowActiveWindow");
|
|
516
|
+
}
|
|
517
|
+
if ("allowPrefix" in policy) {
|
|
518
|
+
const value = normalizeBoolean(policy.allowPrefix, this.config.defaultGroupTriggerPolicy.allowPrefix);
|
|
519
|
+
this.config.defaultGroupTriggerPolicy.allowPrefix = value;
|
|
520
|
+
envUpdates.GROUP_TRIGGER_ALLOW_PREFIX = String(value);
|
|
521
|
+
updatedKeys.push("defaultGroupTriggerPolicy.allowPrefix");
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
if ("matrixProgressUpdates" in body) {
|
|
525
|
+
const value = normalizeBoolean(body.matrixProgressUpdates, this.config.matrixProgressUpdates);
|
|
526
|
+
this.config.matrixProgressUpdates = value;
|
|
527
|
+
envUpdates.MATRIX_PROGRESS_UPDATES = String(value);
|
|
528
|
+
updatedKeys.push("matrixProgressUpdates");
|
|
529
|
+
}
|
|
530
|
+
if ("matrixProgressMinIntervalMs" in body) {
|
|
531
|
+
const value = normalizePositiveInt(
|
|
532
|
+
body.matrixProgressMinIntervalMs,
|
|
533
|
+
this.config.matrixProgressMinIntervalMs,
|
|
534
|
+
1,
|
|
535
|
+
Number.MAX_SAFE_INTEGER
|
|
536
|
+
);
|
|
537
|
+
this.config.matrixProgressMinIntervalMs = value;
|
|
538
|
+
envUpdates.MATRIX_PROGRESS_MIN_INTERVAL_MS = String(value);
|
|
539
|
+
updatedKeys.push("matrixProgressMinIntervalMs");
|
|
540
|
+
}
|
|
541
|
+
if ("matrixTypingTimeoutMs" in body) {
|
|
542
|
+
const value = normalizePositiveInt(
|
|
543
|
+
body.matrixTypingTimeoutMs,
|
|
544
|
+
this.config.matrixTypingTimeoutMs,
|
|
545
|
+
1,
|
|
546
|
+
Number.MAX_SAFE_INTEGER
|
|
547
|
+
);
|
|
548
|
+
this.config.matrixTypingTimeoutMs = value;
|
|
549
|
+
envUpdates.MATRIX_TYPING_TIMEOUT_MS = String(value);
|
|
550
|
+
updatedKeys.push("matrixTypingTimeoutMs");
|
|
551
|
+
}
|
|
552
|
+
if ("sessionActiveWindowMinutes" in body) {
|
|
553
|
+
const value = normalizePositiveInt(
|
|
554
|
+
body.sessionActiveWindowMinutes,
|
|
555
|
+
this.config.sessionActiveWindowMinutes,
|
|
556
|
+
1,
|
|
557
|
+
Number.MAX_SAFE_INTEGER
|
|
558
|
+
);
|
|
559
|
+
this.config.sessionActiveWindowMinutes = value;
|
|
560
|
+
envUpdates.SESSION_ACTIVE_WINDOW_MINUTES = String(value);
|
|
561
|
+
updatedKeys.push("sessionActiveWindowMinutes");
|
|
562
|
+
}
|
|
563
|
+
if ("cliCompat" in body) {
|
|
564
|
+
const compat = asObject(body.cliCompat, "cliCompat");
|
|
565
|
+
if ("enabled" in compat) {
|
|
566
|
+
const value = normalizeBoolean(compat.enabled, this.config.cliCompat.enabled);
|
|
567
|
+
this.config.cliCompat.enabled = value;
|
|
568
|
+
envUpdates.CLI_COMPAT_MODE = String(value);
|
|
569
|
+
updatedKeys.push("cliCompat.enabled");
|
|
570
|
+
}
|
|
571
|
+
if ("passThroughEvents" in compat) {
|
|
572
|
+
const value = normalizeBoolean(compat.passThroughEvents, this.config.cliCompat.passThroughEvents);
|
|
573
|
+
this.config.cliCompat.passThroughEvents = value;
|
|
574
|
+
envUpdates.CLI_COMPAT_PASSTHROUGH_EVENTS = String(value);
|
|
575
|
+
updatedKeys.push("cliCompat.passThroughEvents");
|
|
576
|
+
}
|
|
577
|
+
if ("preserveWhitespace" in compat) {
|
|
578
|
+
const value = normalizeBoolean(compat.preserveWhitespace, this.config.cliCompat.preserveWhitespace);
|
|
579
|
+
this.config.cliCompat.preserveWhitespace = value;
|
|
580
|
+
envUpdates.CLI_COMPAT_PRESERVE_WHITESPACE = String(value);
|
|
581
|
+
updatedKeys.push("cliCompat.preserveWhitespace");
|
|
582
|
+
}
|
|
583
|
+
if ("disableReplyChunkSplit" in compat) {
|
|
584
|
+
const value = normalizeBoolean(compat.disableReplyChunkSplit, this.config.cliCompat.disableReplyChunkSplit);
|
|
585
|
+
this.config.cliCompat.disableReplyChunkSplit = value;
|
|
586
|
+
envUpdates.CLI_COMPAT_DISABLE_REPLY_CHUNK_SPLIT = String(value);
|
|
587
|
+
updatedKeys.push("cliCompat.disableReplyChunkSplit");
|
|
588
|
+
}
|
|
589
|
+
if ("progressThrottleMs" in compat) {
|
|
590
|
+
const value = normalizeNonNegativeInt(compat.progressThrottleMs, this.config.cliCompat.progressThrottleMs);
|
|
591
|
+
this.config.cliCompat.progressThrottleMs = value;
|
|
592
|
+
envUpdates.CLI_COMPAT_PROGRESS_THROTTLE_MS = String(value);
|
|
593
|
+
updatedKeys.push("cliCompat.progressThrottleMs");
|
|
594
|
+
}
|
|
595
|
+
if ("fetchMedia" in compat) {
|
|
596
|
+
const value = normalizeBoolean(compat.fetchMedia, this.config.cliCompat.fetchMedia);
|
|
597
|
+
this.config.cliCompat.fetchMedia = value;
|
|
598
|
+
envUpdates.CLI_COMPAT_FETCH_MEDIA = String(value);
|
|
599
|
+
updatedKeys.push("cliCompat.fetchMedia");
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
if (updatedKeys.length === 0) {
|
|
603
|
+
throw new HttpError(400, "No supported global config fields provided.");
|
|
604
|
+
}
|
|
605
|
+
this.persistEnvUpdates(envUpdates);
|
|
606
|
+
this.stateStore.appendConfigRevision(
|
|
607
|
+
actor,
|
|
608
|
+
`update global config: ${updatedKeys.join(", ")}`,
|
|
609
|
+
JSON.stringify({
|
|
610
|
+
type: "global_config_update",
|
|
611
|
+
updates: envUpdates
|
|
612
|
+
})
|
|
613
|
+
);
|
|
614
|
+
return {
|
|
615
|
+
data: buildGlobalConfigSnapshot(this.config),
|
|
616
|
+
updatedKeys,
|
|
617
|
+
restartRequired: true
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
updateRoomConfig(roomId, rawBody, actor) {
|
|
621
|
+
const body = asObject(rawBody, "room config payload");
|
|
622
|
+
const current = this.configService.getRoomSettings(roomId);
|
|
623
|
+
return this.configService.updateRoomSettings({
|
|
624
|
+
roomId,
|
|
625
|
+
enabled: normalizeBoolean(body.enabled, current?.enabled ?? true),
|
|
626
|
+
allowMention: normalizeBoolean(body.allowMention, current?.allowMention ?? true),
|
|
627
|
+
allowReply: normalizeBoolean(body.allowReply, current?.allowReply ?? true),
|
|
628
|
+
allowActiveWindow: normalizeBoolean(body.allowActiveWindow, current?.allowActiveWindow ?? true),
|
|
629
|
+
allowPrefix: normalizeBoolean(body.allowPrefix, current?.allowPrefix ?? true),
|
|
630
|
+
workdir: normalizeString(body.workdir, current?.workdir ?? this.config.codexWorkdir, "workdir"),
|
|
631
|
+
actor,
|
|
632
|
+
summary: normalizeOptionalString(body.summary)
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
isAuthorized(req) {
|
|
636
|
+
if (!this.adminToken) {
|
|
637
|
+
return true;
|
|
638
|
+
}
|
|
639
|
+
const authorization = req.headers.authorization ?? "";
|
|
640
|
+
const bearer = authorization.startsWith("Bearer ") ? authorization.slice("Bearer ".length).trim() : "";
|
|
641
|
+
const fromHeader = normalizeHeaderValue(req.headers["x-admin-token"]);
|
|
642
|
+
return bearer === this.adminToken || fromHeader === this.adminToken;
|
|
643
|
+
}
|
|
644
|
+
isClientAllowed(req) {
|
|
645
|
+
if (this.adminIpAllowlist.length === 0) {
|
|
646
|
+
return true;
|
|
647
|
+
}
|
|
648
|
+
const normalizedRemote = normalizeRemoteAddress(req.socket.remoteAddress);
|
|
649
|
+
if (!normalizedRemote) {
|
|
650
|
+
return false;
|
|
651
|
+
}
|
|
652
|
+
return this.adminIpAllowlist.includes(normalizedRemote);
|
|
653
|
+
}
|
|
654
|
+
persistEnvUpdates(updates) {
|
|
655
|
+
const envPath = import_node_path2.default.resolve(this.cwd, ".env");
|
|
656
|
+
const examplePath = import_node_path2.default.resolve(this.cwd, ".env.example");
|
|
657
|
+
const template = import_node_fs2.default.existsSync(envPath) ? import_node_fs2.default.readFileSync(envPath, "utf8") : import_node_fs2.default.existsSync(examplePath) ? import_node_fs2.default.readFileSync(examplePath, "utf8") : "";
|
|
658
|
+
const next = applyEnvOverrides(template, updates);
|
|
659
|
+
import_node_fs2.default.writeFileSync(envPath, next, "utf8");
|
|
660
|
+
}
|
|
661
|
+
resolveCors(req) {
|
|
662
|
+
const origin = normalizeOriginHeader(req.headers.origin);
|
|
663
|
+
if (!origin) {
|
|
664
|
+
return { origin: null, allowed: true };
|
|
665
|
+
}
|
|
666
|
+
if (isSameOriginRequest(req, origin)) {
|
|
667
|
+
return { origin, allowed: true };
|
|
668
|
+
}
|
|
669
|
+
if (this.adminAllowedOrigins.includes("*")) {
|
|
670
|
+
return { origin, allowed: true };
|
|
671
|
+
}
|
|
672
|
+
if (this.adminAllowedOrigins.length === 0) {
|
|
673
|
+
return { origin, allowed: false };
|
|
674
|
+
}
|
|
675
|
+
return {
|
|
676
|
+
origin,
|
|
677
|
+
allowed: this.adminAllowedOrigins.includes(origin)
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
setCorsHeaders(res, corsDecision) {
|
|
681
|
+
if (!corsDecision.origin || !corsDecision.allowed) {
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
res.setHeader("Access-Control-Allow-Origin", corsDecision.origin);
|
|
685
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Admin-Token, X-Admin-Actor");
|
|
686
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, PUT, DELETE, OPTIONS");
|
|
687
|
+
appendVaryHeader(res, "Origin");
|
|
688
|
+
}
|
|
689
|
+
setSecurityHeaders(res) {
|
|
690
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
691
|
+
res.setHeader("X-Frame-Options", "DENY");
|
|
692
|
+
res.setHeader("Referrer-Policy", "no-referrer");
|
|
693
|
+
res.setHeader("Permissions-Policy", "camera=(), microphone=(), geolocation=()");
|
|
694
|
+
res.setHeader("Cross-Origin-Resource-Policy", "same-origin");
|
|
695
|
+
res.setHeader("Cross-Origin-Opener-Policy", "same-origin");
|
|
696
|
+
res.setHeader(
|
|
697
|
+
"Content-Security-Policy",
|
|
698
|
+
"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
sendHtml(res, html) {
|
|
702
|
+
res.statusCode = 200;
|
|
703
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
704
|
+
res.end(html);
|
|
705
|
+
}
|
|
706
|
+
sendJson(res, statusCode, payload) {
|
|
707
|
+
res.statusCode = statusCode;
|
|
708
|
+
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
709
|
+
res.end(JSON.stringify(payload));
|
|
710
|
+
}
|
|
711
|
+
};
|
|
712
|
+
function buildGlobalConfigSnapshot(config) {
|
|
713
|
+
return {
|
|
714
|
+
matrixCommandPrefix: config.matrixCommandPrefix,
|
|
715
|
+
codexWorkdir: config.codexWorkdir,
|
|
716
|
+
rateLimiter: { ...config.rateLimiter },
|
|
717
|
+
defaultGroupTriggerPolicy: { ...config.defaultGroupTriggerPolicy },
|
|
718
|
+
matrixProgressUpdates: config.matrixProgressUpdates,
|
|
719
|
+
matrixProgressMinIntervalMs: config.matrixProgressMinIntervalMs,
|
|
720
|
+
matrixTypingTimeoutMs: config.matrixTypingTimeoutMs,
|
|
721
|
+
sessionActiveWindowMinutes: config.sessionActiveWindowMinutes,
|
|
722
|
+
cliCompat: { ...config.cliCompat }
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
function formatAuditEntry(entry) {
|
|
726
|
+
return {
|
|
727
|
+
id: entry.id,
|
|
728
|
+
actor: entry.actor,
|
|
729
|
+
summary: entry.summary,
|
|
730
|
+
payloadJson: entry.payloadJson,
|
|
731
|
+
payload: parseJsonLoose(entry.payloadJson),
|
|
732
|
+
createdAt: entry.createdAt,
|
|
733
|
+
createdAtIso: new Date(entry.createdAt).toISOString()
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
function parseJsonLoose(raw) {
|
|
737
|
+
try {
|
|
738
|
+
return JSON.parse(raw);
|
|
739
|
+
} catch {
|
|
740
|
+
return raw;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
function isUiPath(pathname) {
|
|
744
|
+
return pathname === "/" || pathname === "/index.html" || pathname === "/settings/global" || pathname === "/settings/rooms" || pathname === "/health" || pathname === "/audit";
|
|
745
|
+
}
|
|
746
|
+
function normalizeAllowlist(entries) {
|
|
747
|
+
const output = /* @__PURE__ */ new Set();
|
|
748
|
+
for (const entry of entries) {
|
|
749
|
+
const normalized = normalizeRemoteAddress(entry);
|
|
750
|
+
if (normalized) {
|
|
751
|
+
output.add(normalized);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
return [...output];
|
|
755
|
+
}
|
|
756
|
+
function normalizeOriginAllowlist(entries) {
|
|
757
|
+
const output = /* @__PURE__ */ new Set();
|
|
758
|
+
for (const entry of entries) {
|
|
759
|
+
const normalized = normalizeOrigin(entry);
|
|
760
|
+
if (normalized) {
|
|
761
|
+
output.add(normalized);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
return [...output];
|
|
765
|
+
}
|
|
766
|
+
function normalizeRemoteAddress(value) {
|
|
767
|
+
if (!value) {
|
|
768
|
+
return null;
|
|
769
|
+
}
|
|
770
|
+
const trimmed = value.trim().toLowerCase();
|
|
771
|
+
if (!trimmed) {
|
|
772
|
+
return null;
|
|
773
|
+
}
|
|
774
|
+
const withoutZone = trimmed.includes("%") ? trimmed.slice(0, trimmed.indexOf("%")) : trimmed;
|
|
775
|
+
if (withoutZone === "::1" || withoutZone === "0:0:0:0:0:0:0:1") {
|
|
776
|
+
return "127.0.0.1";
|
|
777
|
+
}
|
|
778
|
+
if (withoutZone.startsWith("::ffff:")) {
|
|
779
|
+
return withoutZone.slice("::ffff:".length);
|
|
780
|
+
}
|
|
781
|
+
return withoutZone;
|
|
782
|
+
}
|
|
783
|
+
function normalizeOriginHeader(value) {
|
|
784
|
+
if (!value) {
|
|
785
|
+
return null;
|
|
786
|
+
}
|
|
787
|
+
const raw = Array.isArray(value) ? value[0] ?? "" : value;
|
|
788
|
+
return normalizeOrigin(raw);
|
|
789
|
+
}
|
|
790
|
+
function normalizeOrigin(value) {
|
|
791
|
+
const trimmed = value.trim();
|
|
792
|
+
if (!trimmed) {
|
|
793
|
+
return null;
|
|
794
|
+
}
|
|
795
|
+
if (trimmed === "*") {
|
|
796
|
+
return "*";
|
|
797
|
+
}
|
|
798
|
+
try {
|
|
799
|
+
const parsed = new URL(trimmed);
|
|
800
|
+
return `${parsed.protocol}//${parsed.host}`.toLowerCase();
|
|
801
|
+
} catch {
|
|
802
|
+
return null;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
function isSameOriginRequest(req, origin) {
|
|
806
|
+
const host = normalizeHeaderValue(req.headers.host);
|
|
807
|
+
if (!host) {
|
|
808
|
+
return false;
|
|
809
|
+
}
|
|
810
|
+
const forwardedProto = normalizeHeaderValue(req.headers["x-forwarded-proto"]);
|
|
811
|
+
const protocol = forwardedProto || "http";
|
|
812
|
+
return origin === `${protocol}://${host}`.toLowerCase();
|
|
813
|
+
}
|
|
814
|
+
function appendVaryHeader(res, headerName) {
|
|
815
|
+
const current = res.getHeader("Vary");
|
|
816
|
+
const existing = typeof current === "string" ? current.split(",").map((v) => v.trim()).filter(Boolean) : [];
|
|
817
|
+
if (!existing.includes(headerName)) {
|
|
818
|
+
existing.push(headerName);
|
|
819
|
+
}
|
|
820
|
+
res.setHeader("Vary", existing.join(", "));
|
|
821
|
+
}
|
|
822
|
+
function renderAdminConsoleHtml() {
|
|
823
|
+
return ADMIN_CONSOLE_HTML;
|
|
824
|
+
}
|
|
825
|
+
async function readJsonBody(req) {
|
|
826
|
+
const chunks = [];
|
|
827
|
+
for await (const chunk of req) {
|
|
828
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
829
|
+
}
|
|
830
|
+
if (chunks.length === 0) {
|
|
831
|
+
return {};
|
|
832
|
+
}
|
|
833
|
+
const raw = Buffer.concat(chunks).toString("utf8").trim();
|
|
834
|
+
if (!raw) {
|
|
835
|
+
return {};
|
|
836
|
+
}
|
|
837
|
+
try {
|
|
838
|
+
return JSON.parse(raw);
|
|
839
|
+
} catch {
|
|
840
|
+
throw new HttpError(400, "Request body must be valid JSON.");
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
function asObject(value, name) {
|
|
844
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
845
|
+
throw new HttpError(400, `${name} must be a JSON object.`);
|
|
846
|
+
}
|
|
847
|
+
return value;
|
|
848
|
+
}
|
|
849
|
+
function normalizeBoolean(value, fallback) {
|
|
850
|
+
if (value === void 0) {
|
|
851
|
+
return fallback;
|
|
852
|
+
}
|
|
853
|
+
if (typeof value !== "boolean") {
|
|
854
|
+
throw new HttpError(400, "Expected boolean value.");
|
|
855
|
+
}
|
|
856
|
+
return value;
|
|
857
|
+
}
|
|
858
|
+
function normalizeString(value, fallback, fieldName) {
|
|
859
|
+
if (value === void 0) {
|
|
860
|
+
return fallback;
|
|
861
|
+
}
|
|
862
|
+
if (typeof value !== "string") {
|
|
863
|
+
throw new HttpError(400, `Expected string value for ${fieldName}.`);
|
|
864
|
+
}
|
|
865
|
+
return value.trim();
|
|
866
|
+
}
|
|
867
|
+
function normalizeOptionalString(value) {
|
|
868
|
+
if (value === void 0 || value === null) {
|
|
869
|
+
return null;
|
|
870
|
+
}
|
|
871
|
+
if (typeof value !== "string") {
|
|
872
|
+
throw new HttpError(400, "Expected string value.");
|
|
873
|
+
}
|
|
874
|
+
const trimmed = value.trim();
|
|
875
|
+
return trimmed || null;
|
|
876
|
+
}
|
|
877
|
+
function normalizePositiveInt(value, fallback, min, max) {
|
|
878
|
+
if (value === void 0 || value === null) {
|
|
879
|
+
return fallback;
|
|
880
|
+
}
|
|
881
|
+
const parsed = Number.parseInt(String(value), 10);
|
|
882
|
+
if (!Number.isFinite(parsed) || parsed < min || parsed > max) {
|
|
883
|
+
throw new HttpError(400, `Expected integer in range [${min}, ${max}].`);
|
|
884
|
+
}
|
|
885
|
+
return parsed;
|
|
886
|
+
}
|
|
887
|
+
function normalizeNonNegativeInt(value, fallback) {
|
|
888
|
+
return normalizePositiveInt(value, fallback, 0, Number.MAX_SAFE_INTEGER);
|
|
889
|
+
}
|
|
890
|
+
function ensureDirectory(targetPath, fieldName) {
|
|
891
|
+
if (!import_node_fs2.default.existsSync(targetPath) || !import_node_fs2.default.statSync(targetPath).isDirectory()) {
|
|
892
|
+
throw new HttpError(400, `${fieldName} must be an existing directory: ${targetPath}`);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
function normalizeHeaderValue(value) {
|
|
896
|
+
if (!value) {
|
|
897
|
+
return "";
|
|
898
|
+
}
|
|
899
|
+
if (Array.isArray(value)) {
|
|
900
|
+
return value[0]?.trim() ?? "";
|
|
901
|
+
}
|
|
902
|
+
return value.trim();
|
|
903
|
+
}
|
|
904
|
+
function readActor(req) {
|
|
905
|
+
const actor = normalizeHeaderValue(req.headers["x-admin-actor"]);
|
|
906
|
+
return actor || null;
|
|
907
|
+
}
|
|
908
|
+
function formatError(error) {
|
|
909
|
+
if (error instanceof Error) {
|
|
910
|
+
return error.message;
|
|
911
|
+
}
|
|
912
|
+
return String(error);
|
|
913
|
+
}
|
|
914
|
+
var ADMIN_CONSOLE_HTML = `<!doctype html>
|
|
915
|
+
<html lang="en">
|
|
916
|
+
<head>
|
|
917
|
+
<meta charset="utf-8" />
|
|
918
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
919
|
+
<title>CodeHarbor Admin Console</title>
|
|
920
|
+
<style>
|
|
921
|
+
:root {
|
|
922
|
+
--bg-start: #0f172a;
|
|
923
|
+
--bg-end: #1e293b;
|
|
924
|
+
--panel: #0b1224cc;
|
|
925
|
+
--panel-border: #334155;
|
|
926
|
+
--text: #e2e8f0;
|
|
927
|
+
--muted: #94a3b8;
|
|
928
|
+
--accent: #22d3ee;
|
|
929
|
+
--accent-strong: #06b6d4;
|
|
930
|
+
--danger: #f43f5e;
|
|
931
|
+
--ok: #10b981;
|
|
932
|
+
--warn: #f59e0b;
|
|
933
|
+
}
|
|
934
|
+
* {
|
|
935
|
+
box-sizing: border-box;
|
|
936
|
+
}
|
|
937
|
+
body {
|
|
938
|
+
margin: 0;
|
|
939
|
+
font-family: "IBM Plex Sans", "Segoe UI", "Helvetica Neue", sans-serif;
|
|
940
|
+
color: var(--text);
|
|
941
|
+
background: radial-gradient(1200px 600px at 20% -10%, #1d4ed8 0%, transparent 55%),
|
|
942
|
+
radial-gradient(1000px 500px at 100% 0%, #0f766e 0%, transparent 55%),
|
|
943
|
+
linear-gradient(135deg, var(--bg-start), var(--bg-end));
|
|
944
|
+
min-height: 100vh;
|
|
945
|
+
}
|
|
946
|
+
.shell {
|
|
947
|
+
max-width: 1100px;
|
|
948
|
+
margin: 0 auto;
|
|
949
|
+
padding: 20px 16px 40px;
|
|
950
|
+
}
|
|
951
|
+
.header {
|
|
952
|
+
background: var(--panel);
|
|
953
|
+
border: 1px solid var(--panel-border);
|
|
954
|
+
border-radius: 16px;
|
|
955
|
+
padding: 16px;
|
|
956
|
+
backdrop-filter: blur(8px);
|
|
957
|
+
}
|
|
958
|
+
.title {
|
|
959
|
+
margin: 0 0 8px;
|
|
960
|
+
font-size: 24px;
|
|
961
|
+
letter-spacing: 0.2px;
|
|
962
|
+
}
|
|
963
|
+
.subtitle {
|
|
964
|
+
margin: 0 0 14px;
|
|
965
|
+
color: var(--muted);
|
|
966
|
+
font-size: 14px;
|
|
967
|
+
}
|
|
968
|
+
.tabs {
|
|
969
|
+
display: flex;
|
|
970
|
+
gap: 8px;
|
|
971
|
+
flex-wrap: wrap;
|
|
972
|
+
margin-bottom: 12px;
|
|
973
|
+
}
|
|
974
|
+
.tab {
|
|
975
|
+
color: var(--text);
|
|
976
|
+
text-decoration: none;
|
|
977
|
+
border: 1px solid var(--panel-border);
|
|
978
|
+
border-radius: 999px;
|
|
979
|
+
padding: 6px 12px;
|
|
980
|
+
font-size: 13px;
|
|
981
|
+
}
|
|
982
|
+
.tab.active {
|
|
983
|
+
border-color: var(--accent);
|
|
984
|
+
background: #155e7555;
|
|
985
|
+
}
|
|
986
|
+
.auth-row {
|
|
987
|
+
display: grid;
|
|
988
|
+
grid-template-columns: repeat(2, minmax(220px, 1fr)) auto auto;
|
|
989
|
+
gap: 8px;
|
|
990
|
+
align-items: end;
|
|
991
|
+
}
|
|
992
|
+
.field {
|
|
993
|
+
display: flex;
|
|
994
|
+
flex-direction: column;
|
|
995
|
+
gap: 4px;
|
|
996
|
+
}
|
|
997
|
+
.field-label {
|
|
998
|
+
font-size: 12px;
|
|
999
|
+
color: var(--muted);
|
|
1000
|
+
}
|
|
1001
|
+
input,
|
|
1002
|
+
button,
|
|
1003
|
+
textarea {
|
|
1004
|
+
font: inherit;
|
|
1005
|
+
}
|
|
1006
|
+
input[type="text"],
|
|
1007
|
+
input[type="password"],
|
|
1008
|
+
input[type="number"] {
|
|
1009
|
+
border: 1px solid var(--panel-border);
|
|
1010
|
+
background: #0f172acc;
|
|
1011
|
+
color: var(--text);
|
|
1012
|
+
border-radius: 10px;
|
|
1013
|
+
padding: 8px 10px;
|
|
1014
|
+
}
|
|
1015
|
+
button {
|
|
1016
|
+
border: 1px solid var(--accent);
|
|
1017
|
+
background: #164e63;
|
|
1018
|
+
color: #ecfeff;
|
|
1019
|
+
border-radius: 10px;
|
|
1020
|
+
padding: 8px 12px;
|
|
1021
|
+
cursor: pointer;
|
|
1022
|
+
}
|
|
1023
|
+
button.secondary {
|
|
1024
|
+
border-color: var(--panel-border);
|
|
1025
|
+
background: #1e293b;
|
|
1026
|
+
color: var(--text);
|
|
1027
|
+
}
|
|
1028
|
+
button.danger {
|
|
1029
|
+
border-color: var(--danger);
|
|
1030
|
+
background: #881337;
|
|
1031
|
+
}
|
|
1032
|
+
.notice {
|
|
1033
|
+
margin: 12px 0 0;
|
|
1034
|
+
border-radius: 10px;
|
|
1035
|
+
padding: 8px 10px;
|
|
1036
|
+
font-size: 13px;
|
|
1037
|
+
border: 1px solid #334155;
|
|
1038
|
+
color: var(--muted);
|
|
1039
|
+
}
|
|
1040
|
+
.notice.ok {
|
|
1041
|
+
border-color: #065f46;
|
|
1042
|
+
color: #d1fae5;
|
|
1043
|
+
background: #064e3b88;
|
|
1044
|
+
}
|
|
1045
|
+
.notice.error {
|
|
1046
|
+
border-color: #881337;
|
|
1047
|
+
color: #ffe4e6;
|
|
1048
|
+
background: #4c051988;
|
|
1049
|
+
}
|
|
1050
|
+
.notice.warn {
|
|
1051
|
+
border-color: #92400e;
|
|
1052
|
+
color: #fef3c7;
|
|
1053
|
+
background: #78350f88;
|
|
1054
|
+
}
|
|
1055
|
+
.panel {
|
|
1056
|
+
margin-top: 14px;
|
|
1057
|
+
background: var(--panel);
|
|
1058
|
+
border: 1px solid var(--panel-border);
|
|
1059
|
+
border-radius: 16px;
|
|
1060
|
+
padding: 16px;
|
|
1061
|
+
}
|
|
1062
|
+
.panel[hidden] {
|
|
1063
|
+
display: none;
|
|
1064
|
+
}
|
|
1065
|
+
.panel-title {
|
|
1066
|
+
margin: 0 0 12px;
|
|
1067
|
+
}
|
|
1068
|
+
.grid {
|
|
1069
|
+
display: grid;
|
|
1070
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
1071
|
+
gap: 10px;
|
|
1072
|
+
}
|
|
1073
|
+
.full {
|
|
1074
|
+
grid-column: 1 / -1;
|
|
1075
|
+
}
|
|
1076
|
+
.checkbox {
|
|
1077
|
+
display: flex;
|
|
1078
|
+
gap: 8px;
|
|
1079
|
+
align-items: center;
|
|
1080
|
+
font-size: 14px;
|
|
1081
|
+
}
|
|
1082
|
+
.actions {
|
|
1083
|
+
display: flex;
|
|
1084
|
+
gap: 8px;
|
|
1085
|
+
flex-wrap: wrap;
|
|
1086
|
+
margin-top: 12px;
|
|
1087
|
+
}
|
|
1088
|
+
.table-wrap {
|
|
1089
|
+
overflow-x: auto;
|
|
1090
|
+
border: 1px solid #334155;
|
|
1091
|
+
border-radius: 12px;
|
|
1092
|
+
margin-top: 12px;
|
|
1093
|
+
}
|
|
1094
|
+
table {
|
|
1095
|
+
width: 100%;
|
|
1096
|
+
border-collapse: collapse;
|
|
1097
|
+
min-width: 720px;
|
|
1098
|
+
}
|
|
1099
|
+
th,
|
|
1100
|
+
td {
|
|
1101
|
+
border-bottom: 1px solid #334155;
|
|
1102
|
+
text-align: left;
|
|
1103
|
+
padding: 8px;
|
|
1104
|
+
font-size: 12px;
|
|
1105
|
+
vertical-align: top;
|
|
1106
|
+
}
|
|
1107
|
+
th {
|
|
1108
|
+
color: var(--muted);
|
|
1109
|
+
}
|
|
1110
|
+
pre {
|
|
1111
|
+
margin: 0;
|
|
1112
|
+
white-space: pre-wrap;
|
|
1113
|
+
word-break: break-word;
|
|
1114
|
+
font-size: 11px;
|
|
1115
|
+
color: #cbd5e1;
|
|
1116
|
+
}
|
|
1117
|
+
.muted {
|
|
1118
|
+
color: var(--muted);
|
|
1119
|
+
font-size: 12px;
|
|
1120
|
+
}
|
|
1121
|
+
@media (max-width: 900px) {
|
|
1122
|
+
.auth-row {
|
|
1123
|
+
grid-template-columns: 1fr;
|
|
1124
|
+
}
|
|
1125
|
+
.grid {
|
|
1126
|
+
grid-template-columns: 1fr;
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
</style>
|
|
1130
|
+
</head>
|
|
1131
|
+
<body>
|
|
1132
|
+
<main class="shell">
|
|
1133
|
+
<section class="header">
|
|
1134
|
+
<h1 class="title">CodeHarbor Admin Console</h1>
|
|
1135
|
+
<p class="subtitle">Manage global settings, room policies, health checks, and config audit records.</p>
|
|
1136
|
+
<nav class="tabs">
|
|
1137
|
+
<a class="tab" data-page="settings-global" href="#/settings/global">Global</a>
|
|
1138
|
+
<a class="tab" data-page="settings-rooms" href="#/settings/rooms">Rooms</a>
|
|
1139
|
+
<a class="tab" data-page="health" href="#/health">Health</a>
|
|
1140
|
+
<a class="tab" data-page="audit" href="#/audit">Audit</a>
|
|
1141
|
+
</nav>
|
|
1142
|
+
<div class="auth-row">
|
|
1143
|
+
<label class="field">
|
|
1144
|
+
<span class="field-label">Admin Token (optional)</span>
|
|
1145
|
+
<input id="auth-token" type="password" placeholder="ADMIN_TOKEN" />
|
|
1146
|
+
</label>
|
|
1147
|
+
<label class="field">
|
|
1148
|
+
<span class="field-label">Actor (for audit logs)</span>
|
|
1149
|
+
<input id="auth-actor" type="text" placeholder="your-name" />
|
|
1150
|
+
</label>
|
|
1151
|
+
<button id="auth-save-btn" type="button" class="secondary">Save Auth</button>
|
|
1152
|
+
<button id="auth-clear-btn" type="button" class="secondary">Clear Auth</button>
|
|
1153
|
+
</div>
|
|
1154
|
+
<div id="notice" class="notice">Ready.</div>
|
|
1155
|
+
</section>
|
|
1156
|
+
|
|
1157
|
+
<section class="panel" data-view="settings-global">
|
|
1158
|
+
<h2 class="panel-title">Global Config</h2>
|
|
1159
|
+
<div class="grid">
|
|
1160
|
+
<label class="field">
|
|
1161
|
+
<span class="field-label">Command Prefix</span>
|
|
1162
|
+
<input id="global-matrix-prefix" type="text" />
|
|
1163
|
+
</label>
|
|
1164
|
+
<label class="field">
|
|
1165
|
+
<span class="field-label">Default Workdir</span>
|
|
1166
|
+
<input id="global-workdir" type="text" />
|
|
1167
|
+
</label>
|
|
1168
|
+
<label class="field">
|
|
1169
|
+
<span class="field-label">Progress Interval (ms)</span>
|
|
1170
|
+
<input id="global-progress-interval" type="number" min="1" />
|
|
1171
|
+
</label>
|
|
1172
|
+
<label class="field">
|
|
1173
|
+
<span class="field-label">Typing Timeout (ms)</span>
|
|
1174
|
+
<input id="global-typing-timeout" type="number" min="1" />
|
|
1175
|
+
</label>
|
|
1176
|
+
<label class="field">
|
|
1177
|
+
<span class="field-label">Session Active Window (minutes)</span>
|
|
1178
|
+
<input id="global-active-window" type="number" min="1" />
|
|
1179
|
+
</label>
|
|
1180
|
+
<label class="checkbox">
|
|
1181
|
+
<input id="global-progress-enabled" type="checkbox" />
|
|
1182
|
+
<span>Enable progress updates</span>
|
|
1183
|
+
</label>
|
|
1184
|
+
|
|
1185
|
+
<label class="field">
|
|
1186
|
+
<span class="field-label">Rate Window (ms)</span>
|
|
1187
|
+
<input id="global-rate-window" type="number" min="1" />
|
|
1188
|
+
</label>
|
|
1189
|
+
<label class="field">
|
|
1190
|
+
<span class="field-label">Rate Max Requests / User</span>
|
|
1191
|
+
<input id="global-rate-user" type="number" min="0" />
|
|
1192
|
+
</label>
|
|
1193
|
+
<label class="field">
|
|
1194
|
+
<span class="field-label">Rate Max Requests / Room</span>
|
|
1195
|
+
<input id="global-rate-room" type="number" min="0" />
|
|
1196
|
+
</label>
|
|
1197
|
+
<label class="field">
|
|
1198
|
+
<span class="field-label">Max Concurrent Global</span>
|
|
1199
|
+
<input id="global-concurrency-global" type="number" min="0" />
|
|
1200
|
+
</label>
|
|
1201
|
+
<label class="field">
|
|
1202
|
+
<span class="field-label">Max Concurrent / User</span>
|
|
1203
|
+
<input id="global-concurrency-user" type="number" min="0" />
|
|
1204
|
+
</label>
|
|
1205
|
+
<label class="field">
|
|
1206
|
+
<span class="field-label">Max Concurrent / Room</span>
|
|
1207
|
+
<input id="global-concurrency-room" type="number" min="0" />
|
|
1208
|
+
</label>
|
|
1209
|
+
|
|
1210
|
+
<label class="checkbox"><input id="global-trigger-mention" type="checkbox" /><span>Trigger: mention</span></label>
|
|
1211
|
+
<label class="checkbox"><input id="global-trigger-reply" type="checkbox" /><span>Trigger: reply</span></label>
|
|
1212
|
+
<label class="checkbox"><input id="global-trigger-window" type="checkbox" /><span>Trigger: active window</span></label>
|
|
1213
|
+
<label class="checkbox"><input id="global-trigger-prefix" type="checkbox" /><span>Trigger: prefix</span></label>
|
|
1214
|
+
|
|
1215
|
+
<label class="checkbox"><input id="global-cli-enabled" type="checkbox" /><span>CLI compat mode</span></label>
|
|
1216
|
+
<label class="checkbox"><input id="global-cli-pass" type="checkbox" /><span>CLI passthrough events</span></label>
|
|
1217
|
+
<label class="checkbox"><input id="global-cli-whitespace" type="checkbox" /><span>Preserve whitespace</span></label>
|
|
1218
|
+
<label class="checkbox"><input id="global-cli-disable-split" type="checkbox" /><span>Disable reply split</span></label>
|
|
1219
|
+
<label class="field">
|
|
1220
|
+
<span class="field-label">CLI progress throttle (ms)</span>
|
|
1221
|
+
<input id="global-cli-throttle" type="number" min="0" />
|
|
1222
|
+
</label>
|
|
1223
|
+
<label class="checkbox"><input id="global-cli-fetch-media" type="checkbox" /><span>Fetch media attachments</span></label>
|
|
1224
|
+
</div>
|
|
1225
|
+
<div class="actions">
|
|
1226
|
+
<button id="global-save-btn" type="button">Save Global Config</button>
|
|
1227
|
+
<button id="global-reload-btn" type="button" class="secondary">Reload</button>
|
|
1228
|
+
</div>
|
|
1229
|
+
<p class="muted">Saving global config updates .env and requires restart to fully take effect.</p>
|
|
1230
|
+
</section>
|
|
1231
|
+
|
|
1232
|
+
<section class="panel" data-view="settings-rooms" hidden>
|
|
1233
|
+
<h2 class="panel-title">Room Config</h2>
|
|
1234
|
+
<div class="grid">
|
|
1235
|
+
<label class="field">
|
|
1236
|
+
<span class="field-label">Room ID</span>
|
|
1237
|
+
<input id="room-id" type="text" placeholder="!room:example.com" />
|
|
1238
|
+
</label>
|
|
1239
|
+
<label class="field">
|
|
1240
|
+
<span class="field-label">Audit Summary (optional)</span>
|
|
1241
|
+
<input id="room-summary" type="text" placeholder="bind room to project A" />
|
|
1242
|
+
</label>
|
|
1243
|
+
<label class="field full">
|
|
1244
|
+
<span class="field-label">Workdir</span>
|
|
1245
|
+
<input id="room-workdir" type="text" />
|
|
1246
|
+
</label>
|
|
1247
|
+
<label class="checkbox"><input id="room-enabled" type="checkbox" /><span>Enabled</span></label>
|
|
1248
|
+
<label class="checkbox"><input id="room-mention" type="checkbox" /><span>Allow mention trigger</span></label>
|
|
1249
|
+
<label class="checkbox"><input id="room-reply" type="checkbox" /><span>Allow reply trigger</span></label>
|
|
1250
|
+
<label class="checkbox"><input id="room-window" type="checkbox" /><span>Allow active-window trigger</span></label>
|
|
1251
|
+
<label class="checkbox"><input id="room-prefix" type="checkbox" /><span>Allow prefix trigger</span></label>
|
|
1252
|
+
</div>
|
|
1253
|
+
<div class="actions">
|
|
1254
|
+
<button id="room-load-btn" type="button" class="secondary">Load Room</button>
|
|
1255
|
+
<button id="room-save-btn" type="button">Save Room</button>
|
|
1256
|
+
<button id="room-delete-btn" type="button" class="danger">Delete Room</button>
|
|
1257
|
+
<button id="room-refresh-btn" type="button" class="secondary">Refresh List</button>
|
|
1258
|
+
</div>
|
|
1259
|
+
<div class="table-wrap">
|
|
1260
|
+
<table>
|
|
1261
|
+
<thead>
|
|
1262
|
+
<tr>
|
|
1263
|
+
<th>Room ID</th>
|
|
1264
|
+
<th>Enabled</th>
|
|
1265
|
+
<th>Workdir</th>
|
|
1266
|
+
<th>Updated At</th>
|
|
1267
|
+
</tr>
|
|
1268
|
+
</thead>
|
|
1269
|
+
<tbody id="room-list-body"></tbody>
|
|
1270
|
+
</table>
|
|
1271
|
+
</div>
|
|
1272
|
+
</section>
|
|
1273
|
+
|
|
1274
|
+
<section class="panel" data-view="health" hidden>
|
|
1275
|
+
<h2 class="panel-title">Health Check</h2>
|
|
1276
|
+
<div class="actions">
|
|
1277
|
+
<button id="health-refresh-btn" type="button">Run Health Check</button>
|
|
1278
|
+
</div>
|
|
1279
|
+
<div class="table-wrap">
|
|
1280
|
+
<table>
|
|
1281
|
+
<thead>
|
|
1282
|
+
<tr>
|
|
1283
|
+
<th>Component</th>
|
|
1284
|
+
<th>Status</th>
|
|
1285
|
+
<th>Details</th>
|
|
1286
|
+
</tr>
|
|
1287
|
+
</thead>
|
|
1288
|
+
<tbody id="health-body"></tbody>
|
|
1289
|
+
</table>
|
|
1290
|
+
</div>
|
|
1291
|
+
</section>
|
|
1292
|
+
|
|
1293
|
+
<section class="panel" data-view="audit" hidden>
|
|
1294
|
+
<h2 class="panel-title">Config Audit</h2>
|
|
1295
|
+
<div class="actions">
|
|
1296
|
+
<label class="field" style="max-width: 120px;">
|
|
1297
|
+
<span class="field-label">Limit</span>
|
|
1298
|
+
<input id="audit-limit" type="number" min="1" max="200" value="30" />
|
|
1299
|
+
</label>
|
|
1300
|
+
<button id="audit-refresh-btn" type="button">Refresh Audit</button>
|
|
1301
|
+
</div>
|
|
1302
|
+
<div class="table-wrap">
|
|
1303
|
+
<table>
|
|
1304
|
+
<thead>
|
|
1305
|
+
<tr>
|
|
1306
|
+
<th>ID</th>
|
|
1307
|
+
<th>Time</th>
|
|
1308
|
+
<th>Actor</th>
|
|
1309
|
+
<th>Summary</th>
|
|
1310
|
+
<th>Payload</th>
|
|
1311
|
+
</tr>
|
|
1312
|
+
</thead>
|
|
1313
|
+
<tbody id="audit-body"></tbody>
|
|
1314
|
+
</table>
|
|
1315
|
+
</div>
|
|
1316
|
+
</section>
|
|
1317
|
+
</main>
|
|
1318
|
+
|
|
1319
|
+
<script>
|
|
1320
|
+
(function () {
|
|
1321
|
+
"use strict";
|
|
1322
|
+
|
|
1323
|
+
var routeToView = {
|
|
1324
|
+
"#/settings/global": "settings-global",
|
|
1325
|
+
"#/settings/rooms": "settings-rooms",
|
|
1326
|
+
"#/health": "health",
|
|
1327
|
+
"#/audit": "audit"
|
|
1328
|
+
};
|
|
1329
|
+
var pathToRoute = {
|
|
1330
|
+
"/settings/global": "#/settings/global",
|
|
1331
|
+
"/settings/rooms": "#/settings/rooms",
|
|
1332
|
+
"/health": "#/health",
|
|
1333
|
+
"/audit": "#/audit"
|
|
1334
|
+
};
|
|
1335
|
+
var storageTokenKey = "codeharbor.admin.token";
|
|
1336
|
+
var storageActorKey = "codeharbor.admin.actor";
|
|
1337
|
+
var loaded = {
|
|
1338
|
+
"settings-global": false,
|
|
1339
|
+
"settings-rooms": false,
|
|
1340
|
+
health: false,
|
|
1341
|
+
audit: false
|
|
1342
|
+
};
|
|
1343
|
+
|
|
1344
|
+
var tokenInput = document.getElementById("auth-token");
|
|
1345
|
+
var actorInput = document.getElementById("auth-actor");
|
|
1346
|
+
var noticeNode = document.getElementById("notice");
|
|
1347
|
+
var roomListBody = document.getElementById("room-list-body");
|
|
1348
|
+
var healthBody = document.getElementById("health-body");
|
|
1349
|
+
var auditBody = document.getElementById("audit-body");
|
|
1350
|
+
|
|
1351
|
+
tokenInput.value = localStorage.getItem(storageTokenKey) || "";
|
|
1352
|
+
actorInput.value = localStorage.getItem(storageActorKey) || "";
|
|
1353
|
+
|
|
1354
|
+
document.getElementById("auth-save-btn").addEventListener("click", function () {
|
|
1355
|
+
localStorage.setItem(storageTokenKey, tokenInput.value.trim());
|
|
1356
|
+
localStorage.setItem(storageActorKey, actorInput.value.trim());
|
|
1357
|
+
showNotice("ok", "Auth settings saved to localStorage.");
|
|
1358
|
+
});
|
|
1359
|
+
|
|
1360
|
+
document.getElementById("auth-clear-btn").addEventListener("click", function () {
|
|
1361
|
+
tokenInput.value = "";
|
|
1362
|
+
actorInput.value = "";
|
|
1363
|
+
localStorage.removeItem(storageTokenKey);
|
|
1364
|
+
localStorage.removeItem(storageActorKey);
|
|
1365
|
+
showNotice("warn", "Auth settings cleared.");
|
|
1366
|
+
});
|
|
1367
|
+
|
|
1368
|
+
document.getElementById("global-save-btn").addEventListener("click", saveGlobal);
|
|
1369
|
+
document.getElementById("global-reload-btn").addEventListener("click", loadGlobal);
|
|
1370
|
+
document.getElementById("room-load-btn").addEventListener("click", loadRoom);
|
|
1371
|
+
document.getElementById("room-save-btn").addEventListener("click", saveRoom);
|
|
1372
|
+
document.getElementById("room-delete-btn").addEventListener("click", deleteRoom);
|
|
1373
|
+
document.getElementById("room-refresh-btn").addEventListener("click", refreshRoomList);
|
|
1374
|
+
document.getElementById("health-refresh-btn").addEventListener("click", loadHealth);
|
|
1375
|
+
document.getElementById("audit-refresh-btn").addEventListener("click", loadAudit);
|
|
1376
|
+
|
|
1377
|
+
window.addEventListener("hashchange", handleRoute);
|
|
1378
|
+
|
|
1379
|
+
if (!window.location.hash) {
|
|
1380
|
+
window.location.hash = pathToRoute[window.location.pathname] || "#/settings/global";
|
|
1381
|
+
} else {
|
|
1382
|
+
handleRoute();
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
function getCurrentView() {
|
|
1386
|
+
return routeToView[window.location.hash] || "settings-global";
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
function handleRoute() {
|
|
1390
|
+
var view = getCurrentView();
|
|
1391
|
+
var panels = document.querySelectorAll("[data-view]");
|
|
1392
|
+
for (var i = 0; i < panels.length; i += 1) {
|
|
1393
|
+
var panel = panels[i];
|
|
1394
|
+
panel.hidden = panel.getAttribute("data-view") !== view;
|
|
1395
|
+
}
|
|
1396
|
+
var tabs = document.querySelectorAll(".tab");
|
|
1397
|
+
for (var j = 0; j < tabs.length; j += 1) {
|
|
1398
|
+
var tab = tabs[j];
|
|
1399
|
+
if (tab.getAttribute("data-page") === view) {
|
|
1400
|
+
tab.classList.add("active");
|
|
1401
|
+
} else {
|
|
1402
|
+
tab.classList.remove("active");
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
ensureLoaded(view);
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
function ensureLoaded(view) {
|
|
1409
|
+
if (loaded[view]) {
|
|
1410
|
+
return;
|
|
1411
|
+
}
|
|
1412
|
+
if (view === "settings-global") {
|
|
1413
|
+
loadGlobal();
|
|
1414
|
+
} else if (view === "settings-rooms") {
|
|
1415
|
+
refreshRoomList();
|
|
1416
|
+
} else if (view === "health") {
|
|
1417
|
+
loadHealth();
|
|
1418
|
+
} else if (view === "audit") {
|
|
1419
|
+
loadAudit();
|
|
1420
|
+
}
|
|
1421
|
+
loaded[view] = true;
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
async function apiRequest(path, method, body) {
|
|
1425
|
+
var headers = {};
|
|
1426
|
+
var token = tokenInput.value.trim();
|
|
1427
|
+
var actor = actorInput.value.trim();
|
|
1428
|
+
if (token) {
|
|
1429
|
+
headers.authorization = "Bearer " + token;
|
|
1430
|
+
}
|
|
1431
|
+
if (actor) {
|
|
1432
|
+
headers["x-admin-actor"] = actor;
|
|
1433
|
+
}
|
|
1434
|
+
if (body !== undefined) {
|
|
1435
|
+
headers["content-type"] = "application/json";
|
|
1436
|
+
}
|
|
1437
|
+
var response = await fetch(path, {
|
|
1438
|
+
method: method || "GET",
|
|
1439
|
+
headers: headers,
|
|
1440
|
+
body: body === undefined ? undefined : JSON.stringify(body)
|
|
1441
|
+
});
|
|
1442
|
+
var text = await response.text();
|
|
1443
|
+
var payload;
|
|
1444
|
+
try {
|
|
1445
|
+
payload = text ? JSON.parse(text) : {};
|
|
1446
|
+
} catch (error) {
|
|
1447
|
+
payload = { raw: text };
|
|
1448
|
+
}
|
|
1449
|
+
if (!response.ok) {
|
|
1450
|
+
var message = payload && payload.error ? payload.error : response.status + " " + response.statusText;
|
|
1451
|
+
throw new Error(message);
|
|
1452
|
+
}
|
|
1453
|
+
return payload;
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
function asNumber(inputId, fallback) {
|
|
1457
|
+
var value = Number.parseInt(document.getElementById(inputId).value, 10);
|
|
1458
|
+
return Number.isFinite(value) ? value : fallback;
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
function asBool(inputId) {
|
|
1462
|
+
return Boolean(document.getElementById(inputId).checked);
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
function asText(inputId) {
|
|
1466
|
+
return document.getElementById(inputId).value.trim();
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
function showNotice(type, message) {
|
|
1470
|
+
noticeNode.className = "notice " + type;
|
|
1471
|
+
noticeNode.textContent = message;
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
function renderEmptyRow(body, columns, text) {
|
|
1475
|
+
body.innerHTML = "";
|
|
1476
|
+
var row = document.createElement("tr");
|
|
1477
|
+
var cell = document.createElement("td");
|
|
1478
|
+
cell.colSpan = columns;
|
|
1479
|
+
cell.textContent = text;
|
|
1480
|
+
row.appendChild(cell);
|
|
1481
|
+
body.appendChild(row);
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
async function loadGlobal() {
|
|
1485
|
+
try {
|
|
1486
|
+
var response = await apiRequest("/api/admin/config/global", "GET");
|
|
1487
|
+
var data = response.data || {};
|
|
1488
|
+
var rateLimiter = data.rateLimiter || {};
|
|
1489
|
+
var trigger = data.defaultGroupTriggerPolicy || {};
|
|
1490
|
+
var cliCompat = data.cliCompat || {};
|
|
1491
|
+
|
|
1492
|
+
document.getElementById("global-matrix-prefix").value = data.matrixCommandPrefix || "";
|
|
1493
|
+
document.getElementById("global-workdir").value = data.codexWorkdir || "";
|
|
1494
|
+
document.getElementById("global-progress-enabled").checked = Boolean(data.matrixProgressUpdates);
|
|
1495
|
+
document.getElementById("global-progress-interval").value = String(data.matrixProgressMinIntervalMs || 2500);
|
|
1496
|
+
document.getElementById("global-typing-timeout").value = String(data.matrixTypingTimeoutMs || 10000);
|
|
1497
|
+
document.getElementById("global-active-window").value = String(data.sessionActiveWindowMinutes || 20);
|
|
1498
|
+
document.getElementById("global-rate-window").value = String(rateLimiter.windowMs || 60000);
|
|
1499
|
+
document.getElementById("global-rate-user").value = String(rateLimiter.maxRequestsPerUser || 0);
|
|
1500
|
+
document.getElementById("global-rate-room").value = String(rateLimiter.maxRequestsPerRoom || 0);
|
|
1501
|
+
document.getElementById("global-concurrency-global").value = String(rateLimiter.maxConcurrentGlobal || 0);
|
|
1502
|
+
document.getElementById("global-concurrency-user").value = String(rateLimiter.maxConcurrentPerUser || 0);
|
|
1503
|
+
document.getElementById("global-concurrency-room").value = String(rateLimiter.maxConcurrentPerRoom || 0);
|
|
1504
|
+
|
|
1505
|
+
document.getElementById("global-trigger-mention").checked = Boolean(trigger.allowMention);
|
|
1506
|
+
document.getElementById("global-trigger-reply").checked = Boolean(trigger.allowReply);
|
|
1507
|
+
document.getElementById("global-trigger-window").checked = Boolean(trigger.allowActiveWindow);
|
|
1508
|
+
document.getElementById("global-trigger-prefix").checked = Boolean(trigger.allowPrefix);
|
|
1509
|
+
|
|
1510
|
+
document.getElementById("global-cli-enabled").checked = Boolean(cliCompat.enabled);
|
|
1511
|
+
document.getElementById("global-cli-pass").checked = Boolean(cliCompat.passThroughEvents);
|
|
1512
|
+
document.getElementById("global-cli-whitespace").checked = Boolean(cliCompat.preserveWhitespace);
|
|
1513
|
+
document.getElementById("global-cli-disable-split").checked = Boolean(cliCompat.disableReplyChunkSplit);
|
|
1514
|
+
document.getElementById("global-cli-throttle").value = String(cliCompat.progressThrottleMs || 0);
|
|
1515
|
+
document.getElementById("global-cli-fetch-media").checked = Boolean(cliCompat.fetchMedia);
|
|
1516
|
+
|
|
1517
|
+
showNotice("ok", "Global config loaded.");
|
|
1518
|
+
} catch (error) {
|
|
1519
|
+
showNotice("error", "Failed to load global config: " + error.message);
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
async function saveGlobal() {
|
|
1524
|
+
try {
|
|
1525
|
+
var body = {
|
|
1526
|
+
matrixCommandPrefix: asText("global-matrix-prefix"),
|
|
1527
|
+
codexWorkdir: asText("global-workdir"),
|
|
1528
|
+
matrixProgressUpdates: asBool("global-progress-enabled"),
|
|
1529
|
+
matrixProgressMinIntervalMs: asNumber("global-progress-interval", 2500),
|
|
1530
|
+
matrixTypingTimeoutMs: asNumber("global-typing-timeout", 10000),
|
|
1531
|
+
sessionActiveWindowMinutes: asNumber("global-active-window", 20),
|
|
1532
|
+
rateLimiter: {
|
|
1533
|
+
windowMs: asNumber("global-rate-window", 60000),
|
|
1534
|
+
maxRequestsPerUser: asNumber("global-rate-user", 20),
|
|
1535
|
+
maxRequestsPerRoom: asNumber("global-rate-room", 120),
|
|
1536
|
+
maxConcurrentGlobal: asNumber("global-concurrency-global", 8),
|
|
1537
|
+
maxConcurrentPerUser: asNumber("global-concurrency-user", 1),
|
|
1538
|
+
maxConcurrentPerRoom: asNumber("global-concurrency-room", 4)
|
|
1539
|
+
},
|
|
1540
|
+
defaultGroupTriggerPolicy: {
|
|
1541
|
+
allowMention: asBool("global-trigger-mention"),
|
|
1542
|
+
allowReply: asBool("global-trigger-reply"),
|
|
1543
|
+
allowActiveWindow: asBool("global-trigger-window"),
|
|
1544
|
+
allowPrefix: asBool("global-trigger-prefix")
|
|
1545
|
+
},
|
|
1546
|
+
cliCompat: {
|
|
1547
|
+
enabled: asBool("global-cli-enabled"),
|
|
1548
|
+
passThroughEvents: asBool("global-cli-pass"),
|
|
1549
|
+
preserveWhitespace: asBool("global-cli-whitespace"),
|
|
1550
|
+
disableReplyChunkSplit: asBool("global-cli-disable-split"),
|
|
1551
|
+
progressThrottleMs: asNumber("global-cli-throttle", 300),
|
|
1552
|
+
fetchMedia: asBool("global-cli-fetch-media")
|
|
1553
|
+
}
|
|
1554
|
+
};
|
|
1555
|
+
var response = await apiRequest("/api/admin/config/global", "PUT", body);
|
|
1556
|
+
var keys = Array.isArray(response.updatedKeys) ? response.updatedKeys.join(", ") : "global config";
|
|
1557
|
+
showNotice("warn", "Saved: " + keys + ". Restart is required.");
|
|
1558
|
+
await loadAudit();
|
|
1559
|
+
} catch (error) {
|
|
1560
|
+
showNotice("error", "Failed to save global config: " + error.message);
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
async function refreshRoomList() {
|
|
1565
|
+
try {
|
|
1566
|
+
var response = await apiRequest("/api/admin/config/rooms", "GET");
|
|
1567
|
+
var items = Array.isArray(response.data) ? response.data : [];
|
|
1568
|
+
roomListBody.innerHTML = "";
|
|
1569
|
+
if (items.length === 0) {
|
|
1570
|
+
renderEmptyRow(roomListBody, 4, "No room settings.");
|
|
1571
|
+
return;
|
|
1572
|
+
}
|
|
1573
|
+
for (var i = 0; i < items.length; i += 1) {
|
|
1574
|
+
var item = items[i];
|
|
1575
|
+
var row = document.createElement("tr");
|
|
1576
|
+
appendCell(row, item.roomId || "");
|
|
1577
|
+
appendCell(row, String(Boolean(item.enabled)));
|
|
1578
|
+
appendCell(row, item.workdir || "");
|
|
1579
|
+
appendCell(row, item.updatedAt ? new Date(item.updatedAt).toISOString() : "-");
|
|
1580
|
+
roomListBody.appendChild(row);
|
|
1581
|
+
}
|
|
1582
|
+
showNotice("ok", "Loaded " + items.length + " room setting(s).");
|
|
1583
|
+
} catch (error) {
|
|
1584
|
+
showNotice("error", "Failed to load room list: " + error.message);
|
|
1585
|
+
renderEmptyRow(roomListBody, 4, "Failed to load room settings.");
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
function appendCell(row, text) {
|
|
1590
|
+
var cell = document.createElement("td");
|
|
1591
|
+
cell.textContent = text;
|
|
1592
|
+
row.appendChild(cell);
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
async function loadRoom() {
|
|
1596
|
+
var roomId = asText("room-id");
|
|
1597
|
+
if (!roomId) {
|
|
1598
|
+
showNotice("warn", "Room ID is required.");
|
|
1599
|
+
return;
|
|
1600
|
+
}
|
|
1601
|
+
try {
|
|
1602
|
+
var response = await apiRequest("/api/admin/config/rooms/" + encodeURIComponent(roomId), "GET");
|
|
1603
|
+
fillRoomForm(response.data || {});
|
|
1604
|
+
showNotice("ok", "Room config loaded for " + roomId + ".");
|
|
1605
|
+
} catch (error) {
|
|
1606
|
+
showNotice("error", "Failed to load room config: " + error.message);
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
function fillRoomForm(data) {
|
|
1611
|
+
document.getElementById("room-enabled").checked = Boolean(data.enabled);
|
|
1612
|
+
document.getElementById("room-mention").checked = Boolean(data.allowMention);
|
|
1613
|
+
document.getElementById("room-reply").checked = Boolean(data.allowReply);
|
|
1614
|
+
document.getElementById("room-window").checked = Boolean(data.allowActiveWindow);
|
|
1615
|
+
document.getElementById("room-prefix").checked = Boolean(data.allowPrefix);
|
|
1616
|
+
document.getElementById("room-workdir").value = data.workdir || "";
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
async function saveRoom() {
|
|
1620
|
+
var roomId = asText("room-id");
|
|
1621
|
+
if (!roomId) {
|
|
1622
|
+
showNotice("warn", "Room ID is required.");
|
|
1623
|
+
return;
|
|
1624
|
+
}
|
|
1625
|
+
try {
|
|
1626
|
+
var body = {
|
|
1627
|
+
enabled: asBool("room-enabled"),
|
|
1628
|
+
allowMention: asBool("room-mention"),
|
|
1629
|
+
allowReply: asBool("room-reply"),
|
|
1630
|
+
allowActiveWindow: asBool("room-window"),
|
|
1631
|
+
allowPrefix: asBool("room-prefix"),
|
|
1632
|
+
workdir: asText("room-workdir"),
|
|
1633
|
+
summary: asText("room-summary")
|
|
1634
|
+
};
|
|
1635
|
+
await apiRequest("/api/admin/config/rooms/" + encodeURIComponent(roomId), "PUT", body);
|
|
1636
|
+
showNotice("ok", "Room config saved for " + roomId + ".");
|
|
1637
|
+
await refreshRoomList();
|
|
1638
|
+
await loadAudit();
|
|
1639
|
+
} catch (error) {
|
|
1640
|
+
showNotice("error", "Failed to save room config: " + error.message);
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
async function deleteRoom() {
|
|
1645
|
+
var roomId = asText("room-id");
|
|
1646
|
+
if (!roomId) {
|
|
1647
|
+
showNotice("warn", "Room ID is required.");
|
|
1648
|
+
return;
|
|
1649
|
+
}
|
|
1650
|
+
if (!window.confirm("Delete room config for " + roomId + "?")) {
|
|
1651
|
+
return;
|
|
1652
|
+
}
|
|
1653
|
+
try {
|
|
1654
|
+
await apiRequest("/api/admin/config/rooms/" + encodeURIComponent(roomId), "DELETE");
|
|
1655
|
+
showNotice("ok", "Room config deleted for " + roomId + ".");
|
|
1656
|
+
await refreshRoomList();
|
|
1657
|
+
await loadAudit();
|
|
1658
|
+
} catch (error) {
|
|
1659
|
+
showNotice("error", "Failed to delete room config: " + error.message);
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
async function loadHealth() {
|
|
1664
|
+
try {
|
|
1665
|
+
var response = await apiRequest("/api/admin/health", "GET");
|
|
1666
|
+
healthBody.innerHTML = "";
|
|
1667
|
+
|
|
1668
|
+
var codex = response.codex || {};
|
|
1669
|
+
var matrix = response.matrix || {};
|
|
1670
|
+
|
|
1671
|
+
appendHealthRow("Codex", Boolean(codex.ok), codex.ok ? (codex.version || "ok") : (codex.error || "failed"));
|
|
1672
|
+
appendHealthRow(
|
|
1673
|
+
"Matrix",
|
|
1674
|
+
Boolean(matrix.ok),
|
|
1675
|
+
matrix.ok ? "HTTP " + matrix.status + " " + JSON.stringify(matrix.versions || []) : (matrix.error || "failed")
|
|
1676
|
+
);
|
|
1677
|
+
appendHealthRow("Overall", Boolean(response.ok), response.timestamp || "");
|
|
1678
|
+
showNotice("ok", "Health check completed.");
|
|
1679
|
+
} catch (error) {
|
|
1680
|
+
showNotice("error", "Health check failed: " + error.message);
|
|
1681
|
+
renderEmptyRow(healthBody, 3, "Failed to run health check.");
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
function appendHealthRow(component, ok, detail) {
|
|
1686
|
+
var row = document.createElement("tr");
|
|
1687
|
+
appendCell(row, component);
|
|
1688
|
+
appendCell(row, ok ? "OK" : "FAIL");
|
|
1689
|
+
appendCell(row, detail);
|
|
1690
|
+
healthBody.appendChild(row);
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
async function loadAudit() {
|
|
1694
|
+
var limit = asNumber("audit-limit", 30);
|
|
1695
|
+
if (limit < 1) {
|
|
1696
|
+
limit = 1;
|
|
1697
|
+
}
|
|
1698
|
+
if (limit > 200) {
|
|
1699
|
+
limit = 200;
|
|
1700
|
+
}
|
|
1701
|
+
try {
|
|
1702
|
+
var response = await apiRequest("/api/admin/audit?limit=" + limit, "GET");
|
|
1703
|
+
var items = Array.isArray(response.data) ? response.data : [];
|
|
1704
|
+
auditBody.innerHTML = "";
|
|
1705
|
+
if (items.length === 0) {
|
|
1706
|
+
renderEmptyRow(auditBody, 5, "No audit records.");
|
|
1707
|
+
return;
|
|
1708
|
+
}
|
|
1709
|
+
for (var i = 0; i < items.length; i += 1) {
|
|
1710
|
+
var item = items[i];
|
|
1711
|
+
var row = document.createElement("tr");
|
|
1712
|
+
appendCell(row, String(item.id || ""));
|
|
1713
|
+
appendCell(row, item.createdAtIso || "");
|
|
1714
|
+
appendCell(row, item.actor || "-");
|
|
1715
|
+
appendCell(row, item.summary || "");
|
|
1716
|
+
var payloadCell = document.createElement("td");
|
|
1717
|
+
var payloadNode = document.createElement("pre");
|
|
1718
|
+
payloadNode.textContent = formatPayload(item);
|
|
1719
|
+
payloadCell.appendChild(payloadNode);
|
|
1720
|
+
row.appendChild(payloadCell);
|
|
1721
|
+
auditBody.appendChild(row);
|
|
1722
|
+
}
|
|
1723
|
+
showNotice("ok", "Audit loaded: " + items.length + " record(s).");
|
|
1724
|
+
} catch (error) {
|
|
1725
|
+
showNotice("error", "Failed to load audit: " + error.message);
|
|
1726
|
+
renderEmptyRow(auditBody, 5, "Failed to load audit records.");
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
function formatPayload(item) {
|
|
1731
|
+
if (item.payload && typeof item.payload === "object") {
|
|
1732
|
+
return JSON.stringify(item.payload, null, 2);
|
|
1733
|
+
}
|
|
1734
|
+
if (typeof item.payloadJson === "string" && item.payloadJson) {
|
|
1735
|
+
return item.payloadJson;
|
|
1736
|
+
}
|
|
1737
|
+
return "";
|
|
1738
|
+
}
|
|
1739
|
+
})();
|
|
1740
|
+
</script>
|
|
1741
|
+
</body>
|
|
1742
|
+
</html>
|
|
1743
|
+
`;
|
|
1744
|
+
async function defaultCheckCodex(bin) {
|
|
1745
|
+
try {
|
|
1746
|
+
const { stdout } = await execFileAsync(bin, ["--version"]);
|
|
1747
|
+
return {
|
|
1748
|
+
ok: true,
|
|
1749
|
+
version: stdout.trim() || null,
|
|
1750
|
+
error: null
|
|
1751
|
+
};
|
|
1752
|
+
} catch (error) {
|
|
1753
|
+
return {
|
|
1754
|
+
ok: false,
|
|
1755
|
+
version: null,
|
|
1756
|
+
error: formatError(error)
|
|
1757
|
+
};
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
async function defaultCheckMatrix(homeserver, timeoutMs) {
|
|
1761
|
+
const controller = new AbortController();
|
|
1762
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
1763
|
+
timer.unref?.();
|
|
1764
|
+
try {
|
|
1765
|
+
const response = await fetch(`${homeserver}/_matrix/client/versions`, {
|
|
1766
|
+
signal: controller.signal
|
|
1767
|
+
});
|
|
1768
|
+
const versions = response.ok ? (await response.json()).versions ?? [] : [];
|
|
1769
|
+
return {
|
|
1770
|
+
ok: response.ok,
|
|
1771
|
+
status: response.status,
|
|
1772
|
+
versions,
|
|
1773
|
+
error: response.ok ? null : `HTTP ${response.status}`
|
|
1774
|
+
};
|
|
1775
|
+
} catch (error) {
|
|
1776
|
+
return {
|
|
1777
|
+
ok: false,
|
|
1778
|
+
status: null,
|
|
1779
|
+
versions: [],
|
|
1780
|
+
error: formatError(error)
|
|
1781
|
+
};
|
|
1782
|
+
} finally {
|
|
1783
|
+
clearTimeout(timer);
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
|
|
33
1787
|
// src/channels/matrix-channel.ts
|
|
34
|
-
var
|
|
1788
|
+
var import_promises2 = __toESM(require("fs/promises"));
|
|
35
1789
|
var import_node_os = __toESM(require("os"));
|
|
36
|
-
var
|
|
1790
|
+
var import_node_path3 = __toESM(require("path"));
|
|
37
1791
|
var import_matrix_js_sdk = require("matrix-js-sdk");
|
|
38
1792
|
|
|
39
1793
|
// src/utils/message.ts
|
|
@@ -456,11 +2210,11 @@ var MatrixChannel = class {
|
|
|
456
2210
|
}
|
|
457
2211
|
const bytes = Buffer.from(await response.arrayBuffer());
|
|
458
2212
|
const extension = resolveFileExtension(fileName, mimeType);
|
|
459
|
-
const directory =
|
|
460
|
-
await
|
|
2213
|
+
const directory = import_node_path3.default.join(import_node_os.default.tmpdir(), "codeharbor-media");
|
|
2214
|
+
await import_promises2.default.mkdir(directory, { recursive: true });
|
|
461
2215
|
const safeEventId = sanitizeFilename(eventId);
|
|
462
|
-
const targetPath =
|
|
463
|
-
await
|
|
2216
|
+
const targetPath = import_node_path3.default.join(directory, `${safeEventId}-${index}${extension}`);
|
|
2217
|
+
await import_promises2.default.writeFile(targetPath, bytes);
|
|
464
2218
|
return targetPath;
|
|
465
2219
|
}
|
|
466
2220
|
};
|
|
@@ -544,7 +2298,7 @@ function sanitizeFilename(value) {
|
|
|
544
2298
|
return value.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 80);
|
|
545
2299
|
}
|
|
546
2300
|
function resolveFileExtension(fileName, mimeType) {
|
|
547
|
-
const ext =
|
|
2301
|
+
const ext = import_node_path3.default.extname(fileName).trim();
|
|
548
2302
|
if (ext) {
|
|
549
2303
|
return ext;
|
|
550
2304
|
}
|
|
@@ -560,8 +2314,102 @@ function resolveFileExtension(fileName, mimeType) {
|
|
|
560
2314
|
return ".bin";
|
|
561
2315
|
}
|
|
562
2316
|
|
|
2317
|
+
// src/config-service.ts
|
|
2318
|
+
var import_node_fs3 = __toESM(require("fs"));
|
|
2319
|
+
var import_node_path4 = __toESM(require("path"));
|
|
2320
|
+
var ConfigService = class {
|
|
2321
|
+
stateStore;
|
|
2322
|
+
defaultWorkdir;
|
|
2323
|
+
constructor(stateStore, defaultWorkdir) {
|
|
2324
|
+
this.stateStore = stateStore;
|
|
2325
|
+
this.defaultWorkdir = import_node_path4.default.resolve(defaultWorkdir);
|
|
2326
|
+
}
|
|
2327
|
+
resolveRoomConfig(roomId, fallbackPolicy) {
|
|
2328
|
+
const room = this.stateStore.getRoomSettings(roomId);
|
|
2329
|
+
if (!room) {
|
|
2330
|
+
return {
|
|
2331
|
+
source: "default",
|
|
2332
|
+
enabled: true,
|
|
2333
|
+
triggerPolicy: fallbackPolicy,
|
|
2334
|
+
workdir: this.defaultWorkdir
|
|
2335
|
+
};
|
|
2336
|
+
}
|
|
2337
|
+
return {
|
|
2338
|
+
source: "room",
|
|
2339
|
+
enabled: room.enabled,
|
|
2340
|
+
triggerPolicy: {
|
|
2341
|
+
allowMention: room.allowMention,
|
|
2342
|
+
allowReply: room.allowReply,
|
|
2343
|
+
allowActiveWindow: room.allowActiveWindow,
|
|
2344
|
+
allowPrefix: room.allowPrefix
|
|
2345
|
+
},
|
|
2346
|
+
workdir: room.workdir
|
|
2347
|
+
};
|
|
2348
|
+
}
|
|
2349
|
+
getRoomSettings(roomId) {
|
|
2350
|
+
return this.stateStore.getRoomSettings(roomId);
|
|
2351
|
+
}
|
|
2352
|
+
listRoomSettings() {
|
|
2353
|
+
return this.stateStore.listRoomSettings();
|
|
2354
|
+
}
|
|
2355
|
+
updateRoomSettings(input) {
|
|
2356
|
+
const normalized = normalizeRoomSettingsInput(input);
|
|
2357
|
+
this.stateStore.upsertRoomSettings(normalized);
|
|
2358
|
+
const revisionPayload = JSON.stringify({
|
|
2359
|
+
type: "room_settings_upsert",
|
|
2360
|
+
roomId: normalized.roomId,
|
|
2361
|
+
enabled: normalized.enabled,
|
|
2362
|
+
allowMention: normalized.allowMention,
|
|
2363
|
+
allowReply: normalized.allowReply,
|
|
2364
|
+
allowActiveWindow: normalized.allowActiveWindow,
|
|
2365
|
+
allowPrefix: normalized.allowPrefix,
|
|
2366
|
+
workdir: normalized.workdir
|
|
2367
|
+
});
|
|
2368
|
+
const summary = input.summary?.trim() || `upsert room settings for ${normalized.roomId}`;
|
|
2369
|
+
const actor = input.actor?.trim() || null;
|
|
2370
|
+
this.stateStore.appendConfigRevision(actor, summary, revisionPayload);
|
|
2371
|
+
const latest = this.stateStore.getRoomSettings(normalized.roomId);
|
|
2372
|
+
if (!latest) {
|
|
2373
|
+
throw new Error(`Failed to persist room settings for ${normalized.roomId}`);
|
|
2374
|
+
}
|
|
2375
|
+
return latest;
|
|
2376
|
+
}
|
|
2377
|
+
deleteRoomSettings(roomId, actor) {
|
|
2378
|
+
const normalizedRoomId = roomId.trim();
|
|
2379
|
+
if (!normalizedRoomId) {
|
|
2380
|
+
throw new Error("roomId is required.");
|
|
2381
|
+
}
|
|
2382
|
+
this.stateStore.deleteRoomSettings(normalizedRoomId);
|
|
2383
|
+
const summary = `delete room settings for ${normalizedRoomId}`;
|
|
2384
|
+
const payload = JSON.stringify({
|
|
2385
|
+
type: "room_settings_delete",
|
|
2386
|
+
roomId: normalizedRoomId
|
|
2387
|
+
});
|
|
2388
|
+
this.stateStore.appendConfigRevision(actor?.trim() || null, summary, payload);
|
|
2389
|
+
}
|
|
2390
|
+
};
|
|
2391
|
+
function normalizeRoomSettingsInput(input) {
|
|
2392
|
+
const roomId = input.roomId.trim();
|
|
2393
|
+
if (!roomId) {
|
|
2394
|
+
throw new Error("roomId is required.");
|
|
2395
|
+
}
|
|
2396
|
+
const workdir = import_node_path4.default.resolve(input.workdir);
|
|
2397
|
+
if (!import_node_fs3.default.existsSync(workdir) || !import_node_fs3.default.statSync(workdir).isDirectory()) {
|
|
2398
|
+
throw new Error(`workdir does not exist or is not a directory: ${workdir}`);
|
|
2399
|
+
}
|
|
2400
|
+
return {
|
|
2401
|
+
roomId,
|
|
2402
|
+
enabled: input.enabled,
|
|
2403
|
+
allowMention: input.allowMention,
|
|
2404
|
+
allowReply: input.allowReply,
|
|
2405
|
+
allowActiveWindow: input.allowActiveWindow,
|
|
2406
|
+
allowPrefix: input.allowPrefix,
|
|
2407
|
+
workdir
|
|
2408
|
+
};
|
|
2409
|
+
}
|
|
2410
|
+
|
|
563
2411
|
// src/executor/codex-executor.ts
|
|
564
|
-
var
|
|
2412
|
+
var import_node_child_process2 = require("child_process");
|
|
565
2413
|
var import_node_readline = __toESM(require("readline"));
|
|
566
2414
|
var CodexExecutionCancelledError = class extends Error {
|
|
567
2415
|
constructor(message = "codex execution cancelled") {
|
|
@@ -579,8 +2427,8 @@ var CodexExecutor = class {
|
|
|
579
2427
|
}
|
|
580
2428
|
startExecution(prompt, sessionId, onProgress, startOptions) {
|
|
581
2429
|
const args = buildCodexArgs(prompt, sessionId, this.options, startOptions);
|
|
582
|
-
const child = (0,
|
|
583
|
-
cwd: this.options.workdir,
|
|
2430
|
+
const child = (0, import_node_child_process2.spawn)(this.options.bin, args, {
|
|
2431
|
+
cwd: startOptions?.workdir ?? this.options.workdir,
|
|
584
2432
|
env: {
|
|
585
2433
|
...process.env,
|
|
586
2434
|
...this.options.extraEnv
|
|
@@ -812,23 +2660,23 @@ function stringify(value) {
|
|
|
812
2660
|
|
|
813
2661
|
// src/orchestrator.ts
|
|
814
2662
|
var import_async_mutex = require("async-mutex");
|
|
815
|
-
var
|
|
2663
|
+
var import_promises3 = __toESM(require("fs/promises"));
|
|
816
2664
|
|
|
817
2665
|
// src/compat/cli-compat-recorder.ts
|
|
818
|
-
var
|
|
819
|
-
var
|
|
2666
|
+
var import_node_fs4 = __toESM(require("fs"));
|
|
2667
|
+
var import_node_path5 = __toESM(require("path"));
|
|
820
2668
|
var CliCompatRecorder = class {
|
|
821
2669
|
filePath;
|
|
822
2670
|
chain = Promise.resolve();
|
|
823
2671
|
constructor(filePath) {
|
|
824
|
-
this.filePath =
|
|
825
|
-
|
|
2672
|
+
this.filePath = import_node_path5.default.resolve(filePath);
|
|
2673
|
+
import_node_fs4.default.mkdirSync(import_node_path5.default.dirname(this.filePath), { recursive: true });
|
|
826
2674
|
}
|
|
827
2675
|
append(entry) {
|
|
828
2676
|
const payload = `${JSON.stringify(entry)}
|
|
829
2677
|
`;
|
|
830
2678
|
this.chain = this.chain.then(async () => {
|
|
831
|
-
await
|
|
2679
|
+
await import_node_fs4.default.promises.appendFile(this.filePath, payload, "utf8");
|
|
832
2680
|
});
|
|
833
2681
|
return this.chain;
|
|
834
2682
|
}
|
|
@@ -1112,6 +2960,8 @@ var Orchestrator = class {
|
|
|
1112
2960
|
sessionActiveWindowMs;
|
|
1113
2961
|
defaultGroupTriggerPolicy;
|
|
1114
2962
|
roomTriggerPolicies;
|
|
2963
|
+
configService;
|
|
2964
|
+
defaultCodexWorkdir;
|
|
1115
2965
|
rateLimiter;
|
|
1116
2966
|
cliCompat;
|
|
1117
2967
|
cliCompatRecorder;
|
|
@@ -1149,6 +2999,8 @@ var Orchestrator = class {
|
|
|
1149
2999
|
allowPrefix: true
|
|
1150
3000
|
};
|
|
1151
3001
|
this.roomTriggerPolicies = options?.roomTriggerPolicies ?? {};
|
|
3002
|
+
this.configService = options?.configService ?? null;
|
|
3003
|
+
this.defaultCodexWorkdir = options?.defaultCodexWorkdir ?? process.cwd();
|
|
1152
3004
|
this.rateLimiter = new RateLimiter(
|
|
1153
3005
|
options?.rateLimiterOptions ?? {
|
|
1154
3006
|
windowMs: 6e4,
|
|
@@ -1184,7 +3036,8 @@ var Orchestrator = class {
|
|
|
1184
3036
|
this.logger.debug("Duplicate event ignored", { requestId, eventId: message.eventId, sessionKey, queueWaitMs });
|
|
1185
3037
|
return;
|
|
1186
3038
|
}
|
|
1187
|
-
const
|
|
3039
|
+
const roomConfig = this.resolveRoomRuntimeConfig(message.conversationId);
|
|
3040
|
+
const route = this.routeMessage(message, sessionKey, roomConfig);
|
|
1188
3041
|
if (route.kind === "ignore") {
|
|
1189
3042
|
this.metrics.record("ignored", queueWaitMs, 0, 0);
|
|
1190
3043
|
this.logger.debug("Message ignored by routing policy", {
|
|
@@ -1253,6 +3106,8 @@ var Orchestrator = class {
|
|
|
1253
3106
|
hasCodexSession: Boolean(previousCodexSessionId),
|
|
1254
3107
|
queueWaitMs,
|
|
1255
3108
|
attachmentCount: message.attachments.length,
|
|
3109
|
+
workdir: roomConfig.workdir,
|
|
3110
|
+
roomConfigSource: roomConfig.source,
|
|
1256
3111
|
isDirectMessage: message.isDirectMessage,
|
|
1257
3112
|
mentionsBot: message.mentionsBot,
|
|
1258
3113
|
repliesToBot: message.repliesToBot
|
|
@@ -1289,7 +3144,8 @@ var Orchestrator = class {
|
|
|
1289
3144
|
},
|
|
1290
3145
|
{
|
|
1291
3146
|
passThroughRawEvents: this.cliCompat.enabled && this.cliCompat.passThroughEvents,
|
|
1292
|
-
imagePaths
|
|
3147
|
+
imagePaths,
|
|
3148
|
+
workdir: roomConfig.workdir
|
|
1293
3149
|
}
|
|
1294
3150
|
);
|
|
1295
3151
|
const running = this.runningExecutions.get(sessionKey);
|
|
@@ -1350,7 +3206,7 @@ var Orchestrator = class {
|
|
|
1350
3206
|
try {
|
|
1351
3207
|
await this.channel.sendMessage(
|
|
1352
3208
|
message.conversationId,
|
|
1353
|
-
`[CodeHarbor] Failed to process request: ${
|
|
3209
|
+
`[CodeHarbor] Failed to process request: ${formatError2(error)}`
|
|
1354
3210
|
);
|
|
1355
3211
|
} catch (sendError) {
|
|
1356
3212
|
this.logger.error("Failed to send error reply to Matrix", sendError);
|
|
@@ -1365,7 +3221,7 @@ var Orchestrator = class {
|
|
|
1365
3221
|
queueWaitMs,
|
|
1366
3222
|
executionDurationMs,
|
|
1367
3223
|
totalDurationMs: Date.now() - receivedAt,
|
|
1368
|
-
error:
|
|
3224
|
+
error: formatError2(error)
|
|
1369
3225
|
});
|
|
1370
3226
|
} finally {
|
|
1371
3227
|
const running = this.runningExecutions.get(sessionKey);
|
|
@@ -1378,13 +3234,16 @@ var Orchestrator = class {
|
|
|
1378
3234
|
}
|
|
1379
3235
|
});
|
|
1380
3236
|
}
|
|
1381
|
-
routeMessage(message, sessionKey) {
|
|
3237
|
+
routeMessage(message, sessionKey, roomConfig) {
|
|
1382
3238
|
const incomingRaw = message.text;
|
|
1383
3239
|
const incomingTrimmed = incomingRaw.trim();
|
|
1384
3240
|
if (!incomingTrimmed && message.attachments.length === 0) {
|
|
1385
3241
|
return { kind: "ignore" };
|
|
1386
3242
|
}
|
|
1387
|
-
|
|
3243
|
+
if (!message.isDirectMessage && !roomConfig.enabled) {
|
|
3244
|
+
return { kind: "ignore" };
|
|
3245
|
+
}
|
|
3246
|
+
const groupPolicy = message.isDirectMessage ? null : roomConfig.triggerPolicy;
|
|
1388
3247
|
const prefixAllowed = message.isDirectMessage || Boolean(groupPolicy?.allowPrefix);
|
|
1389
3248
|
const prefixTriggered = prefixAllowed && this.commandPrefix.length > 0;
|
|
1390
3249
|
const prefixedText = prefixTriggered ? extractCommandText(incomingTrimmed, this.commandPrefix) : null;
|
|
@@ -1426,6 +3285,7 @@ var Orchestrator = class {
|
|
|
1426
3285
|
return;
|
|
1427
3286
|
}
|
|
1428
3287
|
const status = this.stateStore.getSessionStatus(sessionKey);
|
|
3288
|
+
const roomConfig = this.resolveRoomRuntimeConfig(message.conversationId);
|
|
1429
3289
|
const scope = message.isDirectMessage ? "\u79C1\u804A\uFF08\u514D\u524D\u7F00\uFF09" : "\u7FA4\u804A\uFF08\u6309\u623F\u95F4\u89E6\u53D1\u7B56\u7565\uFF09";
|
|
1430
3290
|
const activeUntil = status.activeUntil ?? "\u672A\u6FC0\u6D3B";
|
|
1431
3291
|
const metrics = this.metrics.snapshot(this.runningExecutions.size);
|
|
@@ -1438,6 +3298,7 @@ var Orchestrator = class {
|
|
|
1438
3298
|
- \u6FC0\u6D3B\u4E2D: ${status.isActive ? "\u662F" : "\u5426"}
|
|
1439
3299
|
- activeUntil: ${activeUntil}
|
|
1440
3300
|
- \u5DF2\u7ED1\u5B9A Codex \u4F1A\u8BDD: ${status.hasCodexSession ? "\u662F" : "\u5426"}
|
|
3301
|
+
- \u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55: ${roomConfig.workdir}
|
|
1441
3302
|
- \u8FD0\u884C\u4E2D\u4EFB\u52A1: ${metrics.activeExecutions}
|
|
1442
3303
|
- \u6307\u6807: total=${metrics.total}, success=${metrics.success}, failed=${metrics.failed}, timeout=${metrics.timeout}, cancelled=${metrics.cancelled}, rate_limited=${metrics.rateLimited}
|
|
1443
3304
|
- \u5E73\u5747\u8017\u65F6: queue=${metrics.avgQueueMs}ms, exec=${metrics.avgExecMs}ms, send=${metrics.avgSendMs}ms
|
|
@@ -1559,6 +3420,18 @@ var Orchestrator = class {
|
|
|
1559
3420
|
allowPrefix: override.allowPrefix ?? this.defaultGroupTriggerPolicy.allowPrefix
|
|
1560
3421
|
};
|
|
1561
3422
|
}
|
|
3423
|
+
resolveRoomRuntimeConfig(conversationId) {
|
|
3424
|
+
const fallbackPolicy = this.resolveGroupPolicy(conversationId);
|
|
3425
|
+
if (!this.configService) {
|
|
3426
|
+
return {
|
|
3427
|
+
source: "default",
|
|
3428
|
+
enabled: true,
|
|
3429
|
+
triggerPolicy: fallbackPolicy,
|
|
3430
|
+
workdir: this.defaultCodexWorkdir
|
|
3431
|
+
};
|
|
3432
|
+
}
|
|
3433
|
+
return this.configService.resolveRoomConfig(conversationId, fallbackPolicy);
|
|
3434
|
+
}
|
|
1562
3435
|
buildExecutionPrompt(prompt, message) {
|
|
1563
3436
|
if (message.attachments.length === 0) {
|
|
1564
3437
|
return prompt;
|
|
@@ -1631,7 +3504,7 @@ ${attachmentSummary}
|
|
|
1631
3504
|
function buildSessionKey(message) {
|
|
1632
3505
|
return `${message.channel}:${message.conversationId}:${message.senderId}`;
|
|
1633
3506
|
}
|
|
1634
|
-
function
|
|
3507
|
+
function formatError2(error) {
|
|
1635
3508
|
if (error instanceof Error) {
|
|
1636
3509
|
return error.message;
|
|
1637
3510
|
}
|
|
@@ -1651,7 +3524,7 @@ async function cleanupAttachmentFiles(imagePaths) {
|
|
|
1651
3524
|
await Promise.all(
|
|
1652
3525
|
imagePaths.map(async (imagePath) => {
|
|
1653
3526
|
try {
|
|
1654
|
-
await
|
|
3527
|
+
await import_promises3.default.unlink(imagePath);
|
|
1655
3528
|
} catch {
|
|
1656
3529
|
}
|
|
1657
3530
|
})
|
|
@@ -1730,7 +3603,7 @@ function classifyExecutionOutcome(error) {
|
|
|
1730
3603
|
if (error instanceof CodexExecutionCancelledError) {
|
|
1731
3604
|
return "cancelled";
|
|
1732
3605
|
}
|
|
1733
|
-
const message =
|
|
3606
|
+
const message = formatError2(error).toLowerCase();
|
|
1734
3607
|
if (message.includes("timed out")) {
|
|
1735
3608
|
return "timeout";
|
|
1736
3609
|
}
|
|
@@ -1742,14 +3615,14 @@ function buildFailureProgressSummary(status, startedAt, error) {
|
|
|
1742
3615
|
return `\u5904\u7406\u5DF2\u53D6\u6D88\uFF08\u8017\u65F6 ${elapsed}\uFF09`;
|
|
1743
3616
|
}
|
|
1744
3617
|
if (status === "timeout") {
|
|
1745
|
-
return `\u5904\u7406\u8D85\u65F6\uFF08\u8017\u65F6 ${elapsed}\uFF09: ${
|
|
3618
|
+
return `\u5904\u7406\u8D85\u65F6\uFF08\u8017\u65F6 ${elapsed}\uFF09: ${formatError2(error)}`;
|
|
1746
3619
|
}
|
|
1747
|
-
return `\u5904\u7406\u5931\u8D25\uFF08\u8017\u65F6 ${elapsed}\uFF09: ${
|
|
3620
|
+
return `\u5904\u7406\u5931\u8D25\uFF08\u8017\u65F6 ${elapsed}\uFF09: ${formatError2(error)}`;
|
|
1748
3621
|
}
|
|
1749
3622
|
|
|
1750
3623
|
// src/store/state-store.ts
|
|
1751
|
-
var
|
|
1752
|
-
var
|
|
3624
|
+
var import_node_fs5 = __toESM(require("fs"));
|
|
3625
|
+
var import_node_path6 = __toESM(require("path"));
|
|
1753
3626
|
var ONE_DAY_MS = 24 * 60 * 60 * 1e3;
|
|
1754
3627
|
var PRUNE_INTERVAL_MS = 5 * 60 * 1e3;
|
|
1755
3628
|
var SQLITE_MODULE_ID = `node:${"sqlite"}`;
|
|
@@ -1775,7 +3648,7 @@ var StateStore = class {
|
|
|
1775
3648
|
this.maxProcessedEventsPerSession = maxProcessedEventsPerSession;
|
|
1776
3649
|
this.maxSessionAgeMs = maxSessionAgeDays * ONE_DAY_MS;
|
|
1777
3650
|
this.maxSessions = maxSessions;
|
|
1778
|
-
|
|
3651
|
+
import_node_fs5.default.mkdirSync(import_node_path6.default.dirname(this.dbPath), { recursive: true });
|
|
1779
3652
|
this.db = new DatabaseSync(this.dbPath);
|
|
1780
3653
|
this.initializeSchema();
|
|
1781
3654
|
this.importLegacyStateIfNeeded();
|
|
@@ -1874,6 +3747,83 @@ var StateStore = class {
|
|
|
1874
3747
|
throw error;
|
|
1875
3748
|
}
|
|
1876
3749
|
}
|
|
3750
|
+
getRoomSettings(roomId) {
|
|
3751
|
+
const row = this.db.prepare(
|
|
3752
|
+
"SELECT room_id, enabled, allow_mention, allow_reply, allow_active_window, allow_prefix, workdir, updated_at FROM room_settings WHERE room_id = ?1"
|
|
3753
|
+
).get(roomId);
|
|
3754
|
+
if (!row) {
|
|
3755
|
+
return null;
|
|
3756
|
+
}
|
|
3757
|
+
return {
|
|
3758
|
+
roomId: row.room_id,
|
|
3759
|
+
enabled: row.enabled === 1,
|
|
3760
|
+
allowMention: row.allow_mention === 1,
|
|
3761
|
+
allowReply: row.allow_reply === 1,
|
|
3762
|
+
allowActiveWindow: row.allow_active_window === 1,
|
|
3763
|
+
allowPrefix: row.allow_prefix === 1,
|
|
3764
|
+
workdir: row.workdir,
|
|
3765
|
+
updatedAt: row.updated_at
|
|
3766
|
+
};
|
|
3767
|
+
}
|
|
3768
|
+
listRoomSettings() {
|
|
3769
|
+
const rows = this.db.prepare(
|
|
3770
|
+
"SELECT room_id, enabled, allow_mention, allow_reply, allow_active_window, allow_prefix, workdir, updated_at FROM room_settings ORDER BY room_id ASC"
|
|
3771
|
+
).all();
|
|
3772
|
+
return rows.map((row) => ({
|
|
3773
|
+
roomId: row.room_id,
|
|
3774
|
+
enabled: row.enabled === 1,
|
|
3775
|
+
allowMention: row.allow_mention === 1,
|
|
3776
|
+
allowReply: row.allow_reply === 1,
|
|
3777
|
+
allowActiveWindow: row.allow_active_window === 1,
|
|
3778
|
+
allowPrefix: row.allow_prefix === 1,
|
|
3779
|
+
workdir: row.workdir,
|
|
3780
|
+
updatedAt: row.updated_at
|
|
3781
|
+
}));
|
|
3782
|
+
}
|
|
3783
|
+
upsertRoomSettings(input) {
|
|
3784
|
+
const now = Date.now();
|
|
3785
|
+
this.db.prepare(
|
|
3786
|
+
`INSERT INTO room_settings
|
|
3787
|
+
(room_id, enabled, allow_mention, allow_reply, allow_active_window, allow_prefix, workdir, updated_at)
|
|
3788
|
+
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
|
|
3789
|
+
ON CONFLICT(room_id) DO UPDATE SET
|
|
3790
|
+
enabled = excluded.enabled,
|
|
3791
|
+
allow_mention = excluded.allow_mention,
|
|
3792
|
+
allow_reply = excluded.allow_reply,
|
|
3793
|
+
allow_active_window = excluded.allow_active_window,
|
|
3794
|
+
allow_prefix = excluded.allow_prefix,
|
|
3795
|
+
workdir = excluded.workdir,
|
|
3796
|
+
updated_at = excluded.updated_at`
|
|
3797
|
+
).run(
|
|
3798
|
+
input.roomId,
|
|
3799
|
+
boolToInt(input.enabled),
|
|
3800
|
+
boolToInt(input.allowMention),
|
|
3801
|
+
boolToInt(input.allowReply),
|
|
3802
|
+
boolToInt(input.allowActiveWindow),
|
|
3803
|
+
boolToInt(input.allowPrefix),
|
|
3804
|
+
input.workdir,
|
|
3805
|
+
now
|
|
3806
|
+
);
|
|
3807
|
+
}
|
|
3808
|
+
deleteRoomSettings(roomId) {
|
|
3809
|
+
this.db.prepare("DELETE FROM room_settings WHERE room_id = ?1").run(roomId);
|
|
3810
|
+
}
|
|
3811
|
+
appendConfigRevision(actor, summary, payloadJson) {
|
|
3812
|
+
this.db.prepare("INSERT INTO config_revisions (actor, summary, payload_json, created_at) VALUES (?1, ?2, ?3, ?4)").run(actor, summary, payloadJson, Date.now());
|
|
3813
|
+
}
|
|
3814
|
+
listConfigRevisions(limit = 20) {
|
|
3815
|
+
const safeLimit = Math.max(1, Math.floor(limit));
|
|
3816
|
+
const rows = this.db.prepare(
|
|
3817
|
+
"SELECT id, actor, summary, payload_json, created_at FROM config_revisions ORDER BY id DESC LIMIT ?1"
|
|
3818
|
+
).all(safeLimit);
|
|
3819
|
+
return rows.map((row) => ({
|
|
3820
|
+
id: row.id,
|
|
3821
|
+
actor: row.actor,
|
|
3822
|
+
summary: row.summary,
|
|
3823
|
+
payloadJson: row.payload_json,
|
|
3824
|
+
createdAt: row.created_at
|
|
3825
|
+
}));
|
|
3826
|
+
}
|
|
1877
3827
|
async flush() {
|
|
1878
3828
|
this.touchDatabase();
|
|
1879
3829
|
}
|
|
@@ -1919,10 +3869,31 @@ var StateStore = class {
|
|
|
1919
3869
|
|
|
1920
3870
|
CREATE INDEX IF NOT EXISTS idx_sessions_updated_at ON sessions(updated_at);
|
|
1921
3871
|
CREATE INDEX IF NOT EXISTS idx_events_created_at ON processed_events(created_at);
|
|
3872
|
+
|
|
3873
|
+
CREATE TABLE IF NOT EXISTS room_settings (
|
|
3874
|
+
room_id TEXT PRIMARY KEY,
|
|
3875
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
3876
|
+
allow_mention INTEGER NOT NULL DEFAULT 1,
|
|
3877
|
+
allow_reply INTEGER NOT NULL DEFAULT 1,
|
|
3878
|
+
allow_active_window INTEGER NOT NULL DEFAULT 1,
|
|
3879
|
+
allow_prefix INTEGER NOT NULL DEFAULT 1,
|
|
3880
|
+
workdir TEXT NOT NULL,
|
|
3881
|
+
updated_at INTEGER NOT NULL
|
|
3882
|
+
);
|
|
3883
|
+
|
|
3884
|
+
CREATE TABLE IF NOT EXISTS config_revisions (
|
|
3885
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
3886
|
+
actor TEXT,
|
|
3887
|
+
summary TEXT NOT NULL,
|
|
3888
|
+
payload_json TEXT NOT NULL,
|
|
3889
|
+
created_at INTEGER NOT NULL
|
|
3890
|
+
);
|
|
3891
|
+
|
|
3892
|
+
CREATE INDEX IF NOT EXISTS idx_config_revisions_created_at ON config_revisions(created_at);
|
|
1922
3893
|
`);
|
|
1923
3894
|
}
|
|
1924
3895
|
importLegacyStateIfNeeded() {
|
|
1925
|
-
if (!this.legacyJsonPath || !
|
|
3896
|
+
if (!this.legacyJsonPath || !import_node_fs5.default.existsSync(this.legacyJsonPath)) {
|
|
1926
3897
|
return;
|
|
1927
3898
|
}
|
|
1928
3899
|
const countRow = this.db.prepare("SELECT COUNT(*) AS count FROM sessions").get();
|
|
@@ -2005,7 +3976,7 @@ var StateStore = class {
|
|
|
2005
3976
|
};
|
|
2006
3977
|
function loadLegacyState(filePath) {
|
|
2007
3978
|
try {
|
|
2008
|
-
const raw =
|
|
3979
|
+
const raw = import_node_fs5.default.readFileSync(filePath, "utf8");
|
|
2009
3980
|
const parsed = JSON.parse(raw);
|
|
2010
3981
|
if (!parsed.sessions || typeof parsed.sessions !== "object") {
|
|
2011
3982
|
return null;
|
|
@@ -2043,15 +4014,19 @@ function normalizeLegacyState(state) {
|
|
|
2043
4014
|
}
|
|
2044
4015
|
}
|
|
2045
4016
|
}
|
|
4017
|
+
function boolToInt(value) {
|
|
4018
|
+
return value ? 1 : 0;
|
|
4019
|
+
}
|
|
2046
4020
|
|
|
2047
4021
|
// src/app.ts
|
|
2048
|
-
var
|
|
4022
|
+
var execFileAsync2 = (0, import_node_util2.promisify)(import_node_child_process3.execFile);
|
|
2049
4023
|
var CodeHarborApp = class {
|
|
2050
4024
|
config;
|
|
2051
4025
|
logger;
|
|
2052
4026
|
stateStore;
|
|
2053
4027
|
channel;
|
|
2054
4028
|
orchestrator;
|
|
4029
|
+
configService;
|
|
2055
4030
|
constructor(config) {
|
|
2056
4031
|
this.config = config;
|
|
2057
4032
|
this.logger = new Logger(config.logLevel);
|
|
@@ -2062,6 +4037,7 @@ var CodeHarborApp = class {
|
|
|
2062
4037
|
config.maxSessionAgeDays,
|
|
2063
4038
|
config.maxSessions
|
|
2064
4039
|
);
|
|
4040
|
+
this.configService = new ConfigService(this.stateStore, config.codexWorkdir);
|
|
2065
4041
|
const executor = new CodexExecutor({
|
|
2066
4042
|
bin: config.codexBin,
|
|
2067
4043
|
model: config.codexModel,
|
|
@@ -2084,7 +4060,9 @@ var CodeHarborApp = class {
|
|
|
2084
4060
|
defaultGroupTriggerPolicy: config.defaultGroupTriggerPolicy,
|
|
2085
4061
|
roomTriggerPolicies: config.roomTriggerPolicies,
|
|
2086
4062
|
rateLimiterOptions: config.rateLimiter,
|
|
2087
|
-
cliCompat: config.cliCompat
|
|
4063
|
+
cliCompat: config.cliCompat,
|
|
4064
|
+
configService: this.configService,
|
|
4065
|
+
defaultCodexWorkdir: config.codexWorkdir
|
|
2088
4066
|
});
|
|
2089
4067
|
}
|
|
2090
4068
|
async start() {
|
|
@@ -2105,11 +4083,54 @@ var CodeHarborApp = class {
|
|
|
2105
4083
|
}
|
|
2106
4084
|
}
|
|
2107
4085
|
};
|
|
4086
|
+
var CodeHarborAdminApp = class {
|
|
4087
|
+
config;
|
|
4088
|
+
logger;
|
|
4089
|
+
stateStore;
|
|
4090
|
+
configService;
|
|
4091
|
+
adminServer;
|
|
4092
|
+
constructor(config, options) {
|
|
4093
|
+
this.config = config;
|
|
4094
|
+
this.logger = new Logger(config.logLevel);
|
|
4095
|
+
this.stateStore = new StateStore(
|
|
4096
|
+
config.stateDbPath,
|
|
4097
|
+
config.legacyStateJsonPath,
|
|
4098
|
+
config.maxProcessedEventsPerSession,
|
|
4099
|
+
config.maxSessionAgeDays,
|
|
4100
|
+
config.maxSessions
|
|
4101
|
+
);
|
|
4102
|
+
this.configService = new ConfigService(this.stateStore, config.codexWorkdir);
|
|
4103
|
+
this.adminServer = new AdminServer(config, this.logger, this.stateStore, this.configService, {
|
|
4104
|
+
host: options?.host ?? config.adminBindHost,
|
|
4105
|
+
port: options?.port ?? config.adminPort,
|
|
4106
|
+
adminToken: config.adminToken,
|
|
4107
|
+
adminIpAllowlist: config.adminIpAllowlist,
|
|
4108
|
+
adminAllowedOrigins: config.adminAllowedOrigins
|
|
4109
|
+
});
|
|
4110
|
+
}
|
|
4111
|
+
async start() {
|
|
4112
|
+
await this.adminServer.start();
|
|
4113
|
+
const address = this.adminServer.getAddress();
|
|
4114
|
+
this.logger.info("CodeHarbor admin server started", {
|
|
4115
|
+
host: address?.host ?? this.config.adminBindHost,
|
|
4116
|
+
port: address?.port ?? this.config.adminPort,
|
|
4117
|
+
tokenProtected: Boolean(this.config.adminToken)
|
|
4118
|
+
});
|
|
4119
|
+
}
|
|
4120
|
+
async stop() {
|
|
4121
|
+
this.logger.info("CodeHarbor admin server stopping.");
|
|
4122
|
+
try {
|
|
4123
|
+
await this.adminServer.stop();
|
|
4124
|
+
} finally {
|
|
4125
|
+
await this.stateStore.flush();
|
|
4126
|
+
}
|
|
4127
|
+
}
|
|
4128
|
+
};
|
|
2108
4129
|
async function runDoctor(config) {
|
|
2109
4130
|
const logger = new Logger(config.logLevel);
|
|
2110
4131
|
logger.info("Doctor check started");
|
|
2111
4132
|
try {
|
|
2112
|
-
const { stdout } = await
|
|
4133
|
+
const { stdout } = await execFileAsync2(config.codexBin, ["--version"]);
|
|
2113
4134
|
logger.info("codex available", { version: stdout.trim() });
|
|
2114
4135
|
} catch (error) {
|
|
2115
4136
|
logger.error("codex unavailable", error);
|
|
@@ -2136,12 +4157,20 @@ async function runDoctor(config) {
|
|
|
2136
4157
|
logger.info("Doctor check passed");
|
|
2137
4158
|
}
|
|
2138
4159
|
|
|
4160
|
+
// src/utils/admin-host.ts
|
|
4161
|
+
function isNonLoopbackHost(host) {
|
|
4162
|
+
const normalized = host.trim().toLowerCase();
|
|
4163
|
+
if (!normalized) {
|
|
4164
|
+
return false;
|
|
4165
|
+
}
|
|
4166
|
+
return !(normalized === "localhost" || normalized === "127.0.0.1" || normalized === "::1" || normalized === "[::1]");
|
|
4167
|
+
}
|
|
4168
|
+
|
|
2139
4169
|
// src/config.ts
|
|
2140
|
-
var
|
|
2141
|
-
var
|
|
2142
|
-
var
|
|
4170
|
+
var import_node_fs6 = __toESM(require("fs"));
|
|
4171
|
+
var import_node_path7 = __toESM(require("path"));
|
|
4172
|
+
var import_dotenv2 = __toESM(require("dotenv"));
|
|
2143
4173
|
var import_zod = require("zod");
|
|
2144
|
-
import_dotenv.default.config();
|
|
2145
4174
|
var configSchema = import_zod.z.object({
|
|
2146
4175
|
MATRIX_HOMESERVER: import_zod.z.string().url(),
|
|
2147
4176
|
MATRIX_USER_ID: import_zod.z.string().min(1),
|
|
@@ -2149,7 +4178,7 @@ var configSchema = import_zod.z.object({
|
|
|
2149
4178
|
MATRIX_COMMAND_PREFIX: import_zod.z.string().default("!code"),
|
|
2150
4179
|
CODEX_BIN: import_zod.z.string().default("codex"),
|
|
2151
4180
|
CODEX_MODEL: import_zod.z.string().optional(),
|
|
2152
|
-
CODEX_WORKDIR: import_zod.z.string().default(
|
|
4181
|
+
CODEX_WORKDIR: import_zod.z.string().default("."),
|
|
2153
4182
|
CODEX_DANGEROUS_BYPASS: import_zod.z.string().default("false").transform((v) => v.toLowerCase() === "true"),
|
|
2154
4183
|
CODEX_EXEC_TIMEOUT_MS: import_zod.z.string().default("600000").transform((v) => Number.parseInt(v, 10)).pipe(import_zod.z.number().int().positive()),
|
|
2155
4184
|
CODEX_SANDBOX_MODE: import_zod.z.string().optional(),
|
|
@@ -2185,6 +4214,11 @@ var configSchema = import_zod.z.object({
|
|
|
2185
4214
|
CLI_COMPAT_FETCH_MEDIA: import_zod.z.string().default("true").transform((v) => v.toLowerCase() === "true"),
|
|
2186
4215
|
CLI_COMPAT_RECORD_PATH: import_zod.z.string().default(""),
|
|
2187
4216
|
DOCTOR_HTTP_TIMEOUT_MS: import_zod.z.string().default("10000").transform((v) => Number.parseInt(v, 10)).pipe(import_zod.z.number().int().positive()),
|
|
4217
|
+
ADMIN_BIND_HOST: import_zod.z.string().default("127.0.0.1"),
|
|
4218
|
+
ADMIN_PORT: import_zod.z.string().default("8787").transform((v) => Number.parseInt(v, 10)).pipe(import_zod.z.number().int().min(1).max(65535)),
|
|
4219
|
+
ADMIN_TOKEN: import_zod.z.string().default(""),
|
|
4220
|
+
ADMIN_IP_ALLOWLIST: import_zod.z.string().default(""),
|
|
4221
|
+
ADMIN_ALLOWED_ORIGINS: import_zod.z.string().default(""),
|
|
2188
4222
|
LOG_LEVEL: import_zod.z.enum(["debug", "info", "warn", "error"]).default("info")
|
|
2189
4223
|
}).transform((v) => ({
|
|
2190
4224
|
matrixHomeserver: v.MATRIX_HOMESERVER,
|
|
@@ -2193,15 +4227,15 @@ var configSchema = import_zod.z.object({
|
|
|
2193
4227
|
matrixCommandPrefix: v.MATRIX_COMMAND_PREFIX,
|
|
2194
4228
|
codexBin: v.CODEX_BIN,
|
|
2195
4229
|
codexModel: v.CODEX_MODEL?.trim() || null,
|
|
2196
|
-
codexWorkdir:
|
|
4230
|
+
codexWorkdir: import_node_path7.default.resolve(v.CODEX_WORKDIR),
|
|
2197
4231
|
codexDangerousBypass: v.CODEX_DANGEROUS_BYPASS,
|
|
2198
4232
|
codexExecTimeoutMs: v.CODEX_EXEC_TIMEOUT_MS,
|
|
2199
4233
|
codexSandboxMode: v.CODEX_SANDBOX_MODE?.trim() || null,
|
|
2200
4234
|
codexApprovalPolicy: v.CODEX_APPROVAL_POLICY?.trim() || null,
|
|
2201
4235
|
codexExtraArgs: parseExtraArgs(v.CODEX_EXTRA_ARGS),
|
|
2202
4236
|
codexExtraEnv: parseExtraEnv(v.CODEX_EXTRA_ENV_JSON),
|
|
2203
|
-
stateDbPath:
|
|
2204
|
-
legacyStateJsonPath: v.STATE_PATH.trim() ?
|
|
4237
|
+
stateDbPath: import_node_path7.default.resolve(v.STATE_DB_PATH),
|
|
4238
|
+
legacyStateJsonPath: v.STATE_PATH.trim() ? import_node_path7.default.resolve(v.STATE_PATH) : null,
|
|
2205
4239
|
maxProcessedEventsPerSession: v.MAX_PROCESSED_EVENTS_PER_SESSION,
|
|
2206
4240
|
maxSessionAgeDays: v.MAX_SESSION_AGE_DAYS,
|
|
2207
4241
|
maxSessions: v.MAX_SESSIONS,
|
|
@@ -2232,20 +4266,32 @@ var configSchema = import_zod.z.object({
|
|
|
2232
4266
|
disableReplyChunkSplit: v.CLI_COMPAT_DISABLE_REPLY_CHUNK_SPLIT,
|
|
2233
4267
|
progressThrottleMs: v.CLI_COMPAT_PROGRESS_THROTTLE_MS,
|
|
2234
4268
|
fetchMedia: v.CLI_COMPAT_FETCH_MEDIA,
|
|
2235
|
-
recordPath: v.CLI_COMPAT_RECORD_PATH.trim() ?
|
|
4269
|
+
recordPath: v.CLI_COMPAT_RECORD_PATH.trim() ? import_node_path7.default.resolve(v.CLI_COMPAT_RECORD_PATH) : null
|
|
2236
4270
|
},
|
|
2237
4271
|
doctorHttpTimeoutMs: v.DOCTOR_HTTP_TIMEOUT_MS,
|
|
4272
|
+
adminBindHost: v.ADMIN_BIND_HOST.trim() || "127.0.0.1",
|
|
4273
|
+
adminPort: v.ADMIN_PORT,
|
|
4274
|
+
adminToken: v.ADMIN_TOKEN.trim() || null,
|
|
4275
|
+
adminIpAllowlist: parseCsvList(v.ADMIN_IP_ALLOWLIST),
|
|
4276
|
+
adminAllowedOrigins: parseCsvList(v.ADMIN_ALLOWED_ORIGINS),
|
|
2238
4277
|
logLevel: v.LOG_LEVEL
|
|
2239
4278
|
}));
|
|
4279
|
+
function loadEnvFromFile(filePath = import_node_path7.default.resolve(process.cwd(), ".env"), env = process.env) {
|
|
4280
|
+
import_dotenv2.default.config({
|
|
4281
|
+
path: filePath,
|
|
4282
|
+
processEnv: env,
|
|
4283
|
+
quiet: true
|
|
4284
|
+
});
|
|
4285
|
+
}
|
|
2240
4286
|
function loadConfig(env = process.env) {
|
|
2241
4287
|
const parsed = configSchema.safeParse(env);
|
|
2242
4288
|
if (!parsed.success) {
|
|
2243
4289
|
const message = parsed.error.issues.map((issue) => `${issue.path.join(".") || "config"}: ${issue.message}`).join("; ");
|
|
2244
4290
|
throw new Error(`Invalid configuration: ${message}`);
|
|
2245
4291
|
}
|
|
2246
|
-
|
|
4292
|
+
import_node_fs6.default.mkdirSync(import_node_path7.default.dirname(parsed.data.stateDbPath), { recursive: true });
|
|
2247
4293
|
if (parsed.data.legacyStateJsonPath) {
|
|
2248
|
-
|
|
4294
|
+
import_node_fs6.default.mkdirSync(import_node_path7.default.dirname(parsed.data.legacyStateJsonPath), { recursive: true });
|
|
2249
4295
|
}
|
|
2250
4296
|
return parsed.data;
|
|
2251
4297
|
}
|
|
@@ -2313,12 +4359,584 @@ function parseExtraEnv(raw) {
|
|
|
2313
4359
|
}
|
|
2314
4360
|
return output;
|
|
2315
4361
|
}
|
|
4362
|
+
function parseCsvList(raw) {
|
|
4363
|
+
return raw.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
|
|
4364
|
+
}
|
|
4365
|
+
|
|
4366
|
+
// src/config-snapshot.ts
|
|
4367
|
+
var import_node_fs7 = __toESM(require("fs"));
|
|
4368
|
+
var import_node_path8 = __toESM(require("path"));
|
|
4369
|
+
var import_zod2 = require("zod");
|
|
4370
|
+
var CONFIG_SNAPSHOT_SCHEMA_VERSION = 1;
|
|
4371
|
+
var CONFIG_SNAPSHOT_ENV_KEYS = [
|
|
4372
|
+
"MATRIX_HOMESERVER",
|
|
4373
|
+
"MATRIX_USER_ID",
|
|
4374
|
+
"MATRIX_ACCESS_TOKEN",
|
|
4375
|
+
"MATRIX_COMMAND_PREFIX",
|
|
4376
|
+
"CODEX_BIN",
|
|
4377
|
+
"CODEX_MODEL",
|
|
4378
|
+
"CODEX_WORKDIR",
|
|
4379
|
+
"CODEX_DANGEROUS_BYPASS",
|
|
4380
|
+
"CODEX_EXEC_TIMEOUT_MS",
|
|
4381
|
+
"CODEX_SANDBOX_MODE",
|
|
4382
|
+
"CODEX_APPROVAL_POLICY",
|
|
4383
|
+
"CODEX_EXTRA_ARGS",
|
|
4384
|
+
"CODEX_EXTRA_ENV_JSON",
|
|
4385
|
+
"STATE_DB_PATH",
|
|
4386
|
+
"STATE_PATH",
|
|
4387
|
+
"MAX_PROCESSED_EVENTS_PER_SESSION",
|
|
4388
|
+
"MAX_SESSION_AGE_DAYS",
|
|
4389
|
+
"MAX_SESSIONS",
|
|
4390
|
+
"REPLY_CHUNK_SIZE",
|
|
4391
|
+
"MATRIX_PROGRESS_UPDATES",
|
|
4392
|
+
"MATRIX_PROGRESS_MIN_INTERVAL_MS",
|
|
4393
|
+
"MATRIX_TYPING_TIMEOUT_MS",
|
|
4394
|
+
"SESSION_ACTIVE_WINDOW_MINUTES",
|
|
4395
|
+
"GROUP_TRIGGER_ALLOW_MENTION",
|
|
4396
|
+
"GROUP_TRIGGER_ALLOW_REPLY",
|
|
4397
|
+
"GROUP_TRIGGER_ALLOW_ACTIVE_WINDOW",
|
|
4398
|
+
"GROUP_TRIGGER_ALLOW_PREFIX",
|
|
4399
|
+
"ROOM_TRIGGER_POLICY_JSON",
|
|
4400
|
+
"RATE_LIMIT_WINDOW_SECONDS",
|
|
4401
|
+
"RATE_LIMIT_MAX_REQUESTS_PER_USER",
|
|
4402
|
+
"RATE_LIMIT_MAX_REQUESTS_PER_ROOM",
|
|
4403
|
+
"RATE_LIMIT_MAX_CONCURRENT_GLOBAL",
|
|
4404
|
+
"RATE_LIMIT_MAX_CONCURRENT_PER_USER",
|
|
4405
|
+
"RATE_LIMIT_MAX_CONCURRENT_PER_ROOM",
|
|
4406
|
+
"CLI_COMPAT_MODE",
|
|
4407
|
+
"CLI_COMPAT_PASSTHROUGH_EVENTS",
|
|
4408
|
+
"CLI_COMPAT_PRESERVE_WHITESPACE",
|
|
4409
|
+
"CLI_COMPAT_DISABLE_REPLY_CHUNK_SPLIT",
|
|
4410
|
+
"CLI_COMPAT_PROGRESS_THROTTLE_MS",
|
|
4411
|
+
"CLI_COMPAT_FETCH_MEDIA",
|
|
4412
|
+
"CLI_COMPAT_RECORD_PATH",
|
|
4413
|
+
"DOCTOR_HTTP_TIMEOUT_MS",
|
|
4414
|
+
"ADMIN_BIND_HOST",
|
|
4415
|
+
"ADMIN_PORT",
|
|
4416
|
+
"ADMIN_TOKEN",
|
|
4417
|
+
"ADMIN_IP_ALLOWLIST",
|
|
4418
|
+
"ADMIN_ALLOWED_ORIGINS",
|
|
4419
|
+
"LOG_LEVEL"
|
|
4420
|
+
];
|
|
4421
|
+
var BOOLEAN_STRING = /^(true|false)$/i;
|
|
4422
|
+
var INTEGER_STRING = /^-?\d+$/;
|
|
4423
|
+
var LOG_LEVELS = ["debug", "info", "warn", "error"];
|
|
4424
|
+
var roomSnapshotSchema = import_zod2.z.object({
|
|
4425
|
+
roomId: import_zod2.z.string().min(1),
|
|
4426
|
+
enabled: import_zod2.z.boolean(),
|
|
4427
|
+
allowMention: import_zod2.z.boolean(),
|
|
4428
|
+
allowReply: import_zod2.z.boolean(),
|
|
4429
|
+
allowActiveWindow: import_zod2.z.boolean(),
|
|
4430
|
+
allowPrefix: import_zod2.z.boolean(),
|
|
4431
|
+
workdir: import_zod2.z.string().min(1)
|
|
4432
|
+
}).strict();
|
|
4433
|
+
var envSnapshotSchema = import_zod2.z.object({
|
|
4434
|
+
MATRIX_HOMESERVER: import_zod2.z.string().url(),
|
|
4435
|
+
MATRIX_USER_ID: import_zod2.z.string().min(1),
|
|
4436
|
+
MATRIX_ACCESS_TOKEN: import_zod2.z.string().min(1),
|
|
4437
|
+
MATRIX_COMMAND_PREFIX: import_zod2.z.string(),
|
|
4438
|
+
CODEX_BIN: import_zod2.z.string().min(1),
|
|
4439
|
+
CODEX_MODEL: import_zod2.z.string(),
|
|
4440
|
+
CODEX_WORKDIR: import_zod2.z.string().min(1),
|
|
4441
|
+
CODEX_DANGEROUS_BYPASS: booleanStringSchema("CODEX_DANGEROUS_BYPASS"),
|
|
4442
|
+
CODEX_EXEC_TIMEOUT_MS: integerStringSchema("CODEX_EXEC_TIMEOUT_MS", 1),
|
|
4443
|
+
CODEX_SANDBOX_MODE: import_zod2.z.string(),
|
|
4444
|
+
CODEX_APPROVAL_POLICY: import_zod2.z.string(),
|
|
4445
|
+
CODEX_EXTRA_ARGS: import_zod2.z.string(),
|
|
4446
|
+
CODEX_EXTRA_ENV_JSON: jsonObjectStringSchema("CODEX_EXTRA_ENV_JSON", true),
|
|
4447
|
+
STATE_DB_PATH: import_zod2.z.string().min(1),
|
|
4448
|
+
STATE_PATH: import_zod2.z.string(),
|
|
4449
|
+
MAX_PROCESSED_EVENTS_PER_SESSION: integerStringSchema("MAX_PROCESSED_EVENTS_PER_SESSION", 1),
|
|
4450
|
+
MAX_SESSION_AGE_DAYS: integerStringSchema("MAX_SESSION_AGE_DAYS", 1),
|
|
4451
|
+
MAX_SESSIONS: integerStringSchema("MAX_SESSIONS", 1),
|
|
4452
|
+
REPLY_CHUNK_SIZE: integerStringSchema("REPLY_CHUNK_SIZE", 1),
|
|
4453
|
+
MATRIX_PROGRESS_UPDATES: booleanStringSchema("MATRIX_PROGRESS_UPDATES"),
|
|
4454
|
+
MATRIX_PROGRESS_MIN_INTERVAL_MS: integerStringSchema("MATRIX_PROGRESS_MIN_INTERVAL_MS", 1),
|
|
4455
|
+
MATRIX_TYPING_TIMEOUT_MS: integerStringSchema("MATRIX_TYPING_TIMEOUT_MS", 1),
|
|
4456
|
+
SESSION_ACTIVE_WINDOW_MINUTES: integerStringSchema("SESSION_ACTIVE_WINDOW_MINUTES", 1),
|
|
4457
|
+
GROUP_TRIGGER_ALLOW_MENTION: booleanStringSchema("GROUP_TRIGGER_ALLOW_MENTION"),
|
|
4458
|
+
GROUP_TRIGGER_ALLOW_REPLY: booleanStringSchema("GROUP_TRIGGER_ALLOW_REPLY"),
|
|
4459
|
+
GROUP_TRIGGER_ALLOW_ACTIVE_WINDOW: booleanStringSchema("GROUP_TRIGGER_ALLOW_ACTIVE_WINDOW"),
|
|
4460
|
+
GROUP_TRIGGER_ALLOW_PREFIX: booleanStringSchema("GROUP_TRIGGER_ALLOW_PREFIX"),
|
|
4461
|
+
ROOM_TRIGGER_POLICY_JSON: jsonObjectStringSchema("ROOM_TRIGGER_POLICY_JSON", true),
|
|
4462
|
+
RATE_LIMIT_WINDOW_SECONDS: integerStringSchema("RATE_LIMIT_WINDOW_SECONDS", 1),
|
|
4463
|
+
RATE_LIMIT_MAX_REQUESTS_PER_USER: integerStringSchema("RATE_LIMIT_MAX_REQUESTS_PER_USER", 0),
|
|
4464
|
+
RATE_LIMIT_MAX_REQUESTS_PER_ROOM: integerStringSchema("RATE_LIMIT_MAX_REQUESTS_PER_ROOM", 0),
|
|
4465
|
+
RATE_LIMIT_MAX_CONCURRENT_GLOBAL: integerStringSchema("RATE_LIMIT_MAX_CONCURRENT_GLOBAL", 0),
|
|
4466
|
+
RATE_LIMIT_MAX_CONCURRENT_PER_USER: integerStringSchema("RATE_LIMIT_MAX_CONCURRENT_PER_USER", 0),
|
|
4467
|
+
RATE_LIMIT_MAX_CONCURRENT_PER_ROOM: integerStringSchema("RATE_LIMIT_MAX_CONCURRENT_PER_ROOM", 0),
|
|
4468
|
+
CLI_COMPAT_MODE: booleanStringSchema("CLI_COMPAT_MODE"),
|
|
4469
|
+
CLI_COMPAT_PASSTHROUGH_EVENTS: booleanStringSchema("CLI_COMPAT_PASSTHROUGH_EVENTS"),
|
|
4470
|
+
CLI_COMPAT_PRESERVE_WHITESPACE: booleanStringSchema("CLI_COMPAT_PRESERVE_WHITESPACE"),
|
|
4471
|
+
CLI_COMPAT_DISABLE_REPLY_CHUNK_SPLIT: booleanStringSchema("CLI_COMPAT_DISABLE_REPLY_CHUNK_SPLIT"),
|
|
4472
|
+
CLI_COMPAT_PROGRESS_THROTTLE_MS: integerStringSchema("CLI_COMPAT_PROGRESS_THROTTLE_MS", 0),
|
|
4473
|
+
CLI_COMPAT_FETCH_MEDIA: booleanStringSchema("CLI_COMPAT_FETCH_MEDIA"),
|
|
4474
|
+
CLI_COMPAT_RECORD_PATH: import_zod2.z.string(),
|
|
4475
|
+
DOCTOR_HTTP_TIMEOUT_MS: integerStringSchema("DOCTOR_HTTP_TIMEOUT_MS", 1),
|
|
4476
|
+
ADMIN_BIND_HOST: import_zod2.z.string(),
|
|
4477
|
+
ADMIN_PORT: integerStringSchema("ADMIN_PORT", 1, 65535),
|
|
4478
|
+
ADMIN_TOKEN: import_zod2.z.string(),
|
|
4479
|
+
ADMIN_IP_ALLOWLIST: import_zod2.z.string(),
|
|
4480
|
+
ADMIN_ALLOWED_ORIGINS: import_zod2.z.string().default(""),
|
|
4481
|
+
LOG_LEVEL: import_zod2.z.enum(LOG_LEVELS)
|
|
4482
|
+
}).strict();
|
|
4483
|
+
var configSnapshotSchema = import_zod2.z.object({
|
|
4484
|
+
schemaVersion: import_zod2.z.literal(CONFIG_SNAPSHOT_SCHEMA_VERSION),
|
|
4485
|
+
exportedAt: import_zod2.z.string().datetime({ offset: true }),
|
|
4486
|
+
env: envSnapshotSchema,
|
|
4487
|
+
rooms: import_zod2.z.array(roomSnapshotSchema)
|
|
4488
|
+
}).strict().superRefine((value, ctx) => {
|
|
4489
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4490
|
+
for (const room of value.rooms) {
|
|
4491
|
+
const roomId = room.roomId.trim();
|
|
4492
|
+
if (!roomId) {
|
|
4493
|
+
ctx.addIssue({
|
|
4494
|
+
code: import_zod2.z.ZodIssueCode.custom,
|
|
4495
|
+
message: "rooms[].roomId cannot be empty."
|
|
4496
|
+
});
|
|
4497
|
+
continue;
|
|
4498
|
+
}
|
|
4499
|
+
if (seen.has(roomId)) {
|
|
4500
|
+
ctx.addIssue({
|
|
4501
|
+
code: import_zod2.z.ZodIssueCode.custom,
|
|
4502
|
+
message: `Duplicate room id in snapshot: ${roomId}`
|
|
4503
|
+
});
|
|
4504
|
+
}
|
|
4505
|
+
seen.add(roomId);
|
|
4506
|
+
}
|
|
4507
|
+
});
|
|
4508
|
+
function buildConfigSnapshot(config, roomSettings, now = /* @__PURE__ */ new Date()) {
|
|
4509
|
+
return {
|
|
4510
|
+
schemaVersion: CONFIG_SNAPSHOT_SCHEMA_VERSION,
|
|
4511
|
+
exportedAt: now.toISOString(),
|
|
4512
|
+
env: buildSnapshotEnv(config),
|
|
4513
|
+
rooms: roomSettings.map((room) => ({
|
|
4514
|
+
roomId: room.roomId,
|
|
4515
|
+
enabled: room.enabled,
|
|
4516
|
+
allowMention: room.allowMention,
|
|
4517
|
+
allowReply: room.allowReply,
|
|
4518
|
+
allowActiveWindow: room.allowActiveWindow,
|
|
4519
|
+
allowPrefix: room.allowPrefix,
|
|
4520
|
+
workdir: room.workdir
|
|
4521
|
+
}))
|
|
4522
|
+
};
|
|
4523
|
+
}
|
|
4524
|
+
function parseConfigSnapshot(raw) {
|
|
4525
|
+
const parsed = configSnapshotSchema.safeParse(raw);
|
|
4526
|
+
if (!parsed.success) {
|
|
4527
|
+
const message = parsed.error.issues.map((issue) => `${issue.path.join(".") || "snapshot"}: ${issue.message}`).join("; ");
|
|
4528
|
+
throw new Error(`Invalid config snapshot: ${message}`);
|
|
4529
|
+
}
|
|
4530
|
+
return parsed.data;
|
|
4531
|
+
}
|
|
4532
|
+
function serializeConfigSnapshot(snapshot) {
|
|
4533
|
+
return `${JSON.stringify(snapshot, null, 2)}
|
|
4534
|
+
`;
|
|
4535
|
+
}
|
|
4536
|
+
async function runConfigExportCommand(options = {}) {
|
|
4537
|
+
const cwd = options.cwd ?? process.cwd();
|
|
4538
|
+
const output = options.output ?? process.stdout;
|
|
4539
|
+
const config = loadConfig(options.env ?? process.env);
|
|
4540
|
+
const stateStore = new StateStore(
|
|
4541
|
+
config.stateDbPath,
|
|
4542
|
+
config.legacyStateJsonPath,
|
|
4543
|
+
config.maxProcessedEventsPerSession,
|
|
4544
|
+
config.maxSessionAgeDays,
|
|
4545
|
+
config.maxSessions
|
|
4546
|
+
);
|
|
4547
|
+
try {
|
|
4548
|
+
const snapshot = buildConfigSnapshot(config, stateStore.listRoomSettings(), options.now ?? /* @__PURE__ */ new Date());
|
|
4549
|
+
const serialized = serializeConfigSnapshot(snapshot);
|
|
4550
|
+
if (options.outputPath) {
|
|
4551
|
+
const targetPath = import_node_path8.default.resolve(cwd, options.outputPath);
|
|
4552
|
+
import_node_fs7.default.mkdirSync(import_node_path8.default.dirname(targetPath), { recursive: true });
|
|
4553
|
+
import_node_fs7.default.writeFileSync(targetPath, serialized, "utf8");
|
|
4554
|
+
output.write(`Exported config snapshot to ${targetPath}
|
|
4555
|
+
`);
|
|
4556
|
+
return;
|
|
4557
|
+
}
|
|
4558
|
+
output.write(serialized);
|
|
4559
|
+
} finally {
|
|
4560
|
+
await stateStore.flush();
|
|
4561
|
+
}
|
|
4562
|
+
}
|
|
4563
|
+
async function runConfigImportCommand(options) {
|
|
4564
|
+
const cwd = options.cwd ?? process.cwd();
|
|
4565
|
+
const output = options.output ?? process.stdout;
|
|
4566
|
+
const actor = options.actor?.trim() || "cli:config-import";
|
|
4567
|
+
const sourcePath = import_node_path8.default.resolve(cwd, options.filePath);
|
|
4568
|
+
if (!import_node_fs7.default.existsSync(sourcePath)) {
|
|
4569
|
+
throw new Error(`Config snapshot file not found: ${sourcePath}`);
|
|
4570
|
+
}
|
|
4571
|
+
const snapshot = parseConfigSnapshot(parseJsonFile(sourcePath));
|
|
4572
|
+
const normalizedEnv = normalizeSnapshotEnv(snapshot.env, cwd);
|
|
4573
|
+
ensureDirectory2(normalizedEnv.CODEX_WORKDIR, "CODEX_WORKDIR");
|
|
4574
|
+
const normalizedRooms = normalizeSnapshotRooms(snapshot.rooms, cwd);
|
|
4575
|
+
if (options.dryRun) {
|
|
4576
|
+
output.write(
|
|
4577
|
+
[
|
|
4578
|
+
`Config snapshot is valid: ${sourcePath}`,
|
|
4579
|
+
`- schemaVersion: ${snapshot.schemaVersion}`,
|
|
4580
|
+
`- rooms: ${normalizedRooms.length}`,
|
|
4581
|
+
"- dry-run: no changes were written"
|
|
4582
|
+
].join("\n") + "\n"
|
|
4583
|
+
);
|
|
4584
|
+
return;
|
|
4585
|
+
}
|
|
4586
|
+
persistEnvSnapshot(cwd, normalizedEnv);
|
|
4587
|
+
const stateStore = new StateStore(
|
|
4588
|
+
normalizedEnv.STATE_DB_PATH,
|
|
4589
|
+
normalizedEnv.STATE_PATH ? normalizedEnv.STATE_PATH : null,
|
|
4590
|
+
parseIntStrict(normalizedEnv.MAX_PROCESSED_EVENTS_PER_SESSION),
|
|
4591
|
+
parseIntStrict(normalizedEnv.MAX_SESSION_AGE_DAYS),
|
|
4592
|
+
parseIntStrict(normalizedEnv.MAX_SESSIONS)
|
|
4593
|
+
);
|
|
4594
|
+
try {
|
|
4595
|
+
synchronizeRoomSettings(stateStore, normalizedRooms);
|
|
4596
|
+
stateStore.appendConfigRevision(
|
|
4597
|
+
actor,
|
|
4598
|
+
`import config snapshot from ${import_node_path8.default.basename(sourcePath)}`,
|
|
4599
|
+
JSON.stringify({
|
|
4600
|
+
type: "config_snapshot_import",
|
|
4601
|
+
sourcePath,
|
|
4602
|
+
roomCount: normalizedRooms.length,
|
|
4603
|
+
envKeyCount: CONFIG_SNAPSHOT_ENV_KEYS.length
|
|
4604
|
+
})
|
|
4605
|
+
);
|
|
4606
|
+
} finally {
|
|
4607
|
+
await stateStore.flush();
|
|
4608
|
+
}
|
|
4609
|
+
output.write(
|
|
4610
|
+
[
|
|
4611
|
+
`Imported config snapshot from ${sourcePath}`,
|
|
4612
|
+
`- updated .env in ${import_node_path8.default.resolve(cwd, ".env")}`,
|
|
4613
|
+
`- synchronized room settings: ${normalizedRooms.length}`,
|
|
4614
|
+
"- restart required: yes (global env settings are restart-scoped)"
|
|
4615
|
+
].join("\n") + "\n"
|
|
4616
|
+
);
|
|
4617
|
+
}
|
|
4618
|
+
function buildSnapshotEnv(config) {
|
|
4619
|
+
return {
|
|
4620
|
+
MATRIX_HOMESERVER: config.matrixHomeserver,
|
|
4621
|
+
MATRIX_USER_ID: config.matrixUserId,
|
|
4622
|
+
MATRIX_ACCESS_TOKEN: config.matrixAccessToken,
|
|
4623
|
+
MATRIX_COMMAND_PREFIX: config.matrixCommandPrefix,
|
|
4624
|
+
CODEX_BIN: config.codexBin,
|
|
4625
|
+
CODEX_MODEL: config.codexModel ?? "",
|
|
4626
|
+
CODEX_WORKDIR: config.codexWorkdir,
|
|
4627
|
+
CODEX_DANGEROUS_BYPASS: String(config.codexDangerousBypass),
|
|
4628
|
+
CODEX_EXEC_TIMEOUT_MS: String(config.codexExecTimeoutMs),
|
|
4629
|
+
CODEX_SANDBOX_MODE: config.codexSandboxMode ?? "",
|
|
4630
|
+
CODEX_APPROVAL_POLICY: config.codexApprovalPolicy ?? "",
|
|
4631
|
+
CODEX_EXTRA_ARGS: config.codexExtraArgs.join(" "),
|
|
4632
|
+
CODEX_EXTRA_ENV_JSON: serializeJsonObject(config.codexExtraEnv),
|
|
4633
|
+
STATE_DB_PATH: config.stateDbPath,
|
|
4634
|
+
STATE_PATH: config.legacyStateJsonPath ?? "",
|
|
4635
|
+
MAX_PROCESSED_EVENTS_PER_SESSION: String(config.maxProcessedEventsPerSession),
|
|
4636
|
+
MAX_SESSION_AGE_DAYS: String(config.maxSessionAgeDays),
|
|
4637
|
+
MAX_SESSIONS: String(config.maxSessions),
|
|
4638
|
+
REPLY_CHUNK_SIZE: String(config.replyChunkSize),
|
|
4639
|
+
MATRIX_PROGRESS_UPDATES: String(config.matrixProgressUpdates),
|
|
4640
|
+
MATRIX_PROGRESS_MIN_INTERVAL_MS: String(config.matrixProgressMinIntervalMs),
|
|
4641
|
+
MATRIX_TYPING_TIMEOUT_MS: String(config.matrixTypingTimeoutMs),
|
|
4642
|
+
SESSION_ACTIVE_WINDOW_MINUTES: String(config.sessionActiveWindowMinutes),
|
|
4643
|
+
GROUP_TRIGGER_ALLOW_MENTION: String(config.defaultGroupTriggerPolicy.allowMention),
|
|
4644
|
+
GROUP_TRIGGER_ALLOW_REPLY: String(config.defaultGroupTriggerPolicy.allowReply),
|
|
4645
|
+
GROUP_TRIGGER_ALLOW_ACTIVE_WINDOW: String(config.defaultGroupTriggerPolicy.allowActiveWindow),
|
|
4646
|
+
GROUP_TRIGGER_ALLOW_PREFIX: String(config.defaultGroupTriggerPolicy.allowPrefix),
|
|
4647
|
+
ROOM_TRIGGER_POLICY_JSON: serializeJsonObject(config.roomTriggerPolicies),
|
|
4648
|
+
RATE_LIMIT_WINDOW_SECONDS: String(Math.max(1, Math.round(config.rateLimiter.windowMs / 1e3))),
|
|
4649
|
+
RATE_LIMIT_MAX_REQUESTS_PER_USER: String(config.rateLimiter.maxRequestsPerUser),
|
|
4650
|
+
RATE_LIMIT_MAX_REQUESTS_PER_ROOM: String(config.rateLimiter.maxRequestsPerRoom),
|
|
4651
|
+
RATE_LIMIT_MAX_CONCURRENT_GLOBAL: String(config.rateLimiter.maxConcurrentGlobal),
|
|
4652
|
+
RATE_LIMIT_MAX_CONCURRENT_PER_USER: String(config.rateLimiter.maxConcurrentPerUser),
|
|
4653
|
+
RATE_LIMIT_MAX_CONCURRENT_PER_ROOM: String(config.rateLimiter.maxConcurrentPerRoom),
|
|
4654
|
+
CLI_COMPAT_MODE: String(config.cliCompat.enabled),
|
|
4655
|
+
CLI_COMPAT_PASSTHROUGH_EVENTS: String(config.cliCompat.passThroughEvents),
|
|
4656
|
+
CLI_COMPAT_PRESERVE_WHITESPACE: String(config.cliCompat.preserveWhitespace),
|
|
4657
|
+
CLI_COMPAT_DISABLE_REPLY_CHUNK_SPLIT: String(config.cliCompat.disableReplyChunkSplit),
|
|
4658
|
+
CLI_COMPAT_PROGRESS_THROTTLE_MS: String(config.cliCompat.progressThrottleMs),
|
|
4659
|
+
CLI_COMPAT_FETCH_MEDIA: String(config.cliCompat.fetchMedia),
|
|
4660
|
+
CLI_COMPAT_RECORD_PATH: config.cliCompat.recordPath ?? "",
|
|
4661
|
+
DOCTOR_HTTP_TIMEOUT_MS: String(config.doctorHttpTimeoutMs),
|
|
4662
|
+
ADMIN_BIND_HOST: config.adminBindHost,
|
|
4663
|
+
ADMIN_PORT: String(config.adminPort),
|
|
4664
|
+
ADMIN_TOKEN: config.adminToken ?? "",
|
|
4665
|
+
ADMIN_IP_ALLOWLIST: config.adminIpAllowlist.join(","),
|
|
4666
|
+
ADMIN_ALLOWED_ORIGINS: config.adminAllowedOrigins.join(","),
|
|
4667
|
+
LOG_LEVEL: config.logLevel
|
|
4668
|
+
};
|
|
4669
|
+
}
|
|
4670
|
+
function parseJsonFile(filePath) {
|
|
4671
|
+
try {
|
|
4672
|
+
const raw = import_node_fs7.default.readFileSync(filePath, "utf8");
|
|
4673
|
+
return JSON.parse(raw);
|
|
4674
|
+
} catch (error) {
|
|
4675
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4676
|
+
throw new Error(`Failed to parse snapshot JSON: ${message}`, {
|
|
4677
|
+
cause: error
|
|
4678
|
+
});
|
|
4679
|
+
}
|
|
4680
|
+
}
|
|
4681
|
+
function normalizeSnapshotEnv(env, cwd) {
|
|
4682
|
+
return {
|
|
4683
|
+
...env,
|
|
4684
|
+
CODEX_WORKDIR: import_node_path8.default.resolve(cwd, env.CODEX_WORKDIR),
|
|
4685
|
+
STATE_DB_PATH: import_node_path8.default.resolve(cwd, env.STATE_DB_PATH),
|
|
4686
|
+
STATE_PATH: env.STATE_PATH.trim() ? import_node_path8.default.resolve(cwd, env.STATE_PATH) : "",
|
|
4687
|
+
CLI_COMPAT_RECORD_PATH: env.CLI_COMPAT_RECORD_PATH.trim() ? import_node_path8.default.resolve(cwd, env.CLI_COMPAT_RECORD_PATH) : ""
|
|
4688
|
+
};
|
|
4689
|
+
}
|
|
4690
|
+
function normalizeSnapshotRooms(rooms, cwd) {
|
|
4691
|
+
const normalized = [];
|
|
4692
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4693
|
+
for (const room of rooms) {
|
|
4694
|
+
const roomId = room.roomId.trim();
|
|
4695
|
+
if (!roomId) {
|
|
4696
|
+
throw new Error("roomId is required for every room in snapshot.");
|
|
4697
|
+
}
|
|
4698
|
+
if (seen.has(roomId)) {
|
|
4699
|
+
throw new Error(`Duplicate roomId in snapshot: ${roomId}`);
|
|
4700
|
+
}
|
|
4701
|
+
seen.add(roomId);
|
|
4702
|
+
const workdir = import_node_path8.default.resolve(cwd, room.workdir);
|
|
4703
|
+
ensureDirectory2(workdir, `room workdir (${roomId})`);
|
|
4704
|
+
normalized.push({
|
|
4705
|
+
roomId,
|
|
4706
|
+
enabled: room.enabled,
|
|
4707
|
+
allowMention: room.allowMention,
|
|
4708
|
+
allowReply: room.allowReply,
|
|
4709
|
+
allowActiveWindow: room.allowActiveWindow,
|
|
4710
|
+
allowPrefix: room.allowPrefix,
|
|
4711
|
+
workdir
|
|
4712
|
+
});
|
|
4713
|
+
}
|
|
4714
|
+
return normalized;
|
|
4715
|
+
}
|
|
4716
|
+
function synchronizeRoomSettings(stateStore, rooms) {
|
|
4717
|
+
const incoming = new Map(rooms.map((room) => [room.roomId, room]));
|
|
4718
|
+
const existing = stateStore.listRoomSettings();
|
|
4719
|
+
for (const room of existing) {
|
|
4720
|
+
if (!incoming.has(room.roomId)) {
|
|
4721
|
+
stateStore.deleteRoomSettings(room.roomId);
|
|
4722
|
+
}
|
|
4723
|
+
}
|
|
4724
|
+
for (const room of rooms) {
|
|
4725
|
+
stateStore.upsertRoomSettings(room);
|
|
4726
|
+
}
|
|
4727
|
+
}
|
|
4728
|
+
function persistEnvSnapshot(cwd, env) {
|
|
4729
|
+
const envPath = import_node_path8.default.resolve(cwd, ".env");
|
|
4730
|
+
const examplePath = import_node_path8.default.resolve(cwd, ".env.example");
|
|
4731
|
+
const template = import_node_fs7.default.existsSync(envPath) ? import_node_fs7.default.readFileSync(envPath, "utf8") : import_node_fs7.default.existsSync(examplePath) ? import_node_fs7.default.readFileSync(examplePath, "utf8") : "";
|
|
4732
|
+
const overrides = {};
|
|
4733
|
+
for (const key of CONFIG_SNAPSHOT_ENV_KEYS) {
|
|
4734
|
+
overrides[key] = env[key];
|
|
4735
|
+
}
|
|
4736
|
+
const next = applyEnvOverrides(template, overrides);
|
|
4737
|
+
import_node_fs7.default.writeFileSync(envPath, next, "utf8");
|
|
4738
|
+
}
|
|
4739
|
+
function ensureDirectory2(dirPath, label) {
|
|
4740
|
+
if (!import_node_fs7.default.existsSync(dirPath) || !import_node_fs7.default.statSync(dirPath).isDirectory()) {
|
|
4741
|
+
throw new Error(`${label} does not exist or is not a directory: ${dirPath}`);
|
|
4742
|
+
}
|
|
4743
|
+
}
|
|
4744
|
+
function parseIntStrict(raw) {
|
|
4745
|
+
const value = Number.parseInt(raw, 10);
|
|
4746
|
+
if (!Number.isFinite(value)) {
|
|
4747
|
+
throw new Error(`Invalid integer value: ${raw}`);
|
|
4748
|
+
}
|
|
4749
|
+
return value;
|
|
4750
|
+
}
|
|
4751
|
+
function serializeJsonObject(value) {
|
|
4752
|
+
return Object.keys(value).length > 0 ? JSON.stringify(value) : "";
|
|
4753
|
+
}
|
|
4754
|
+
function booleanStringSchema(key) {
|
|
4755
|
+
return import_zod2.z.string().refine((value) => BOOLEAN_STRING.test(value), {
|
|
4756
|
+
message: `${key} must be a boolean string (true/false).`
|
|
4757
|
+
});
|
|
4758
|
+
}
|
|
4759
|
+
function integerStringSchema(key, min, max = Number.MAX_SAFE_INTEGER) {
|
|
4760
|
+
return import_zod2.z.string().refine((value) => {
|
|
4761
|
+
const trimmed = value.trim();
|
|
4762
|
+
if (!INTEGER_STRING.test(trimmed)) {
|
|
4763
|
+
return false;
|
|
4764
|
+
}
|
|
4765
|
+
const parsed = Number.parseInt(trimmed, 10);
|
|
4766
|
+
if (!Number.isFinite(parsed)) {
|
|
4767
|
+
return false;
|
|
4768
|
+
}
|
|
4769
|
+
return parsed >= min && parsed <= max;
|
|
4770
|
+
}, {
|
|
4771
|
+
message: `${key} must be an integer string in range [${min}, ${max}].`
|
|
4772
|
+
});
|
|
4773
|
+
}
|
|
4774
|
+
function jsonObjectStringSchema(key, allowEmpty) {
|
|
4775
|
+
return import_zod2.z.string().refine((value) => {
|
|
4776
|
+
const trimmed = value.trim();
|
|
4777
|
+
if (!trimmed) {
|
|
4778
|
+
return allowEmpty;
|
|
4779
|
+
}
|
|
4780
|
+
let parsed;
|
|
4781
|
+
try {
|
|
4782
|
+
parsed = JSON.parse(trimmed);
|
|
4783
|
+
} catch {
|
|
4784
|
+
return false;
|
|
4785
|
+
}
|
|
4786
|
+
return Boolean(parsed) && typeof parsed === "object" && !Array.isArray(parsed);
|
|
4787
|
+
}, {
|
|
4788
|
+
message: `${key} must be an empty string or a JSON object string.`
|
|
4789
|
+
});
|
|
4790
|
+
}
|
|
4791
|
+
|
|
4792
|
+
// src/preflight.ts
|
|
4793
|
+
var import_node_child_process4 = require("child_process");
|
|
4794
|
+
var import_node_fs8 = __toESM(require("fs"));
|
|
4795
|
+
var import_node_path9 = __toESM(require("path"));
|
|
4796
|
+
var import_node_util3 = require("util");
|
|
4797
|
+
var execFileAsync3 = (0, import_node_util3.promisify)(import_node_child_process4.execFile);
|
|
4798
|
+
var REQUIRED_ENV_KEYS = ["MATRIX_HOMESERVER", "MATRIX_USER_ID", "MATRIX_ACCESS_TOKEN"];
|
|
4799
|
+
async function runStartupPreflight(options = {}) {
|
|
4800
|
+
const env = options.env ?? process.env;
|
|
4801
|
+
const cwd = options.cwd ?? process.cwd();
|
|
4802
|
+
const checkCodexBinary = options.checkCodexBinary ?? defaultCheckCodexBinary;
|
|
4803
|
+
const fileExists = options.fileExists ?? import_node_fs8.default.existsSync;
|
|
4804
|
+
const isDirectory = options.isDirectory ?? defaultIsDirectory;
|
|
4805
|
+
const issues = [];
|
|
4806
|
+
const envPath = import_node_path9.default.resolve(cwd, ".env");
|
|
4807
|
+
if (!fileExists(envPath)) {
|
|
4808
|
+
issues.push({
|
|
4809
|
+
level: "warn",
|
|
4810
|
+
code: "missing_dotenv",
|
|
4811
|
+
check: ".env",
|
|
4812
|
+
message: `No .env file found at ${envPath}.`,
|
|
4813
|
+
fix: 'Run "codeharbor init" to create baseline config.'
|
|
4814
|
+
});
|
|
4815
|
+
}
|
|
4816
|
+
for (const key of REQUIRED_ENV_KEYS) {
|
|
4817
|
+
if (!readEnv(env, key)) {
|
|
4818
|
+
issues.push({
|
|
4819
|
+
level: "error",
|
|
4820
|
+
code: "missing_env",
|
|
4821
|
+
check: key,
|
|
4822
|
+
message: `${key} is required.`,
|
|
4823
|
+
fix: `Run "codeharbor init" or set ${key} in .env.`
|
|
4824
|
+
});
|
|
4825
|
+
}
|
|
4826
|
+
}
|
|
4827
|
+
const matrixHomeserver = readEnv(env, "MATRIX_HOMESERVER");
|
|
4828
|
+
if (matrixHomeserver) {
|
|
4829
|
+
try {
|
|
4830
|
+
new URL(matrixHomeserver);
|
|
4831
|
+
} catch {
|
|
4832
|
+
issues.push({
|
|
4833
|
+
level: "error",
|
|
4834
|
+
code: "invalid_matrix_homeserver",
|
|
4835
|
+
check: "MATRIX_HOMESERVER",
|
|
4836
|
+
message: `Invalid URL: "${matrixHomeserver}".`,
|
|
4837
|
+
fix: "Set MATRIX_HOMESERVER to a full URL, for example https://matrix.example.com."
|
|
4838
|
+
});
|
|
4839
|
+
}
|
|
4840
|
+
}
|
|
4841
|
+
const matrixUserId = readEnv(env, "MATRIX_USER_ID");
|
|
4842
|
+
if (matrixUserId && !/^@[^:\s]+:.+/.test(matrixUserId)) {
|
|
4843
|
+
issues.push({
|
|
4844
|
+
level: "error",
|
|
4845
|
+
code: "invalid_matrix_user_id",
|
|
4846
|
+
check: "MATRIX_USER_ID",
|
|
4847
|
+
message: `Unexpected Matrix user id format: "${matrixUserId}".`,
|
|
4848
|
+
fix: "Set MATRIX_USER_ID like @bot:example.com."
|
|
4849
|
+
});
|
|
4850
|
+
}
|
|
4851
|
+
const codexBin = readEnv(env, "CODEX_BIN") || "codex";
|
|
4852
|
+
try {
|
|
4853
|
+
await checkCodexBinary(codexBin);
|
|
4854
|
+
} catch (error) {
|
|
4855
|
+
const reason = error instanceof Error && error.message ? ` (${error.message})` : "";
|
|
4856
|
+
issues.push({
|
|
4857
|
+
level: "error",
|
|
4858
|
+
code: "missing_codex_bin",
|
|
4859
|
+
check: "CODEX_BIN",
|
|
4860
|
+
message: `Unable to execute "${codexBin}"${reason}.`,
|
|
4861
|
+
fix: `Install Codex CLI and ensure "${codexBin}" is in PATH, or set CODEX_BIN=/absolute/path/to/codex.`
|
|
4862
|
+
});
|
|
4863
|
+
}
|
|
4864
|
+
const configuredWorkdir = readEnv(env, "CODEX_WORKDIR");
|
|
4865
|
+
const workdir = import_node_path9.default.resolve(cwd, configuredWorkdir || cwd);
|
|
4866
|
+
if (!fileExists(workdir) || !isDirectory(workdir)) {
|
|
4867
|
+
issues.push({
|
|
4868
|
+
level: "error",
|
|
4869
|
+
code: "invalid_codex_workdir",
|
|
4870
|
+
check: "CODEX_WORKDIR",
|
|
4871
|
+
message: `Working directory does not exist or is not a directory: ${workdir}.`,
|
|
4872
|
+
fix: `Set CODEX_WORKDIR to an existing directory, for example CODEX_WORKDIR=${cwd}.`
|
|
4873
|
+
});
|
|
4874
|
+
}
|
|
4875
|
+
return {
|
|
4876
|
+
ok: issues.every((issue) => issue.level !== "error"),
|
|
4877
|
+
issues
|
|
4878
|
+
};
|
|
4879
|
+
}
|
|
4880
|
+
function formatPreflightReport(result, commandName) {
|
|
4881
|
+
const lines = [];
|
|
4882
|
+
const errors = result.issues.filter((issue) => issue.level === "error").length;
|
|
4883
|
+
const warnings = result.issues.filter((issue) => issue.level === "warn").length;
|
|
4884
|
+
if (result.ok) {
|
|
4885
|
+
lines.push(`Preflight check passed for "codeharbor ${commandName}" with ${warnings} warning(s).`);
|
|
4886
|
+
} else {
|
|
4887
|
+
lines.push(
|
|
4888
|
+
`Preflight check failed for "codeharbor ${commandName}" with ${errors} error(s) and ${warnings} warning(s).`
|
|
4889
|
+
);
|
|
4890
|
+
}
|
|
4891
|
+
for (const issue of result.issues) {
|
|
4892
|
+
const level = issue.level.toUpperCase();
|
|
4893
|
+
lines.push(`- [${level}] ${issue.check}: ${issue.message}`);
|
|
4894
|
+
lines.push(` fix: ${issue.fix}`);
|
|
4895
|
+
}
|
|
4896
|
+
return `${lines.join("\n")}
|
|
4897
|
+
`;
|
|
4898
|
+
}
|
|
4899
|
+
async function defaultCheckCodexBinary(bin) {
|
|
4900
|
+
await execFileAsync3(bin, ["--version"]);
|
|
4901
|
+
}
|
|
4902
|
+
function defaultIsDirectory(targetPath) {
|
|
4903
|
+
try {
|
|
4904
|
+
return import_node_fs8.default.statSync(targetPath).isDirectory();
|
|
4905
|
+
} catch {
|
|
4906
|
+
return false;
|
|
4907
|
+
}
|
|
4908
|
+
}
|
|
4909
|
+
function readEnv(env, key) {
|
|
4910
|
+
return env[key]?.trim() ?? "";
|
|
4911
|
+
}
|
|
4912
|
+
|
|
4913
|
+
// src/runtime-home.ts
|
|
4914
|
+
var import_node_path10 = __toESM(require("path"));
|
|
4915
|
+
var DEFAULT_RUNTIME_HOME = "/opt/codeharbor";
|
|
4916
|
+
var RUNTIME_HOME_ENV_KEY = "CODEHARBOR_HOME";
|
|
4917
|
+
function resolveRuntimeHome(env = process.env) {
|
|
4918
|
+
const configured = env[RUNTIME_HOME_ENV_KEY]?.trim();
|
|
4919
|
+
if (configured) {
|
|
4920
|
+
return import_node_path10.default.resolve(configured);
|
|
4921
|
+
}
|
|
4922
|
+
return DEFAULT_RUNTIME_HOME;
|
|
4923
|
+
}
|
|
2316
4924
|
|
|
2317
4925
|
// src/cli.ts
|
|
4926
|
+
var runtimeHome = null;
|
|
2318
4927
|
var program = new import_commander.Command();
|
|
2319
4928
|
program.name("codeharbor").description("Instant-messaging bridge for Codex CLI sessions").version("0.1.0");
|
|
4929
|
+
program.command("init").description("Create or update .env via guided prompts").option("-f, --force", "overwrite existing .env without confirmation").action(async (options) => {
|
|
4930
|
+
const home = ensureRuntimeHomeOrExit();
|
|
4931
|
+
await runInitCommand({ force: options.force ?? false, cwd: home });
|
|
4932
|
+
});
|
|
2320
4933
|
program.command("start").description("Start CodeHarbor service").action(async () => {
|
|
2321
|
-
const
|
|
4934
|
+
const home = ensureRuntimeHomeOrExit();
|
|
4935
|
+
const config = await loadConfigWithPreflight("start", home);
|
|
4936
|
+
if (!config) {
|
|
4937
|
+
process.exitCode = 1;
|
|
4938
|
+
return;
|
|
4939
|
+
}
|
|
2322
4940
|
const app = new CodeHarborApp(config);
|
|
2323
4941
|
await app.start();
|
|
2324
4942
|
const stop = async () => {
|
|
@@ -2333,10 +4951,135 @@ program.command("start").description("Start CodeHarbor service").action(async ()
|
|
|
2333
4951
|
});
|
|
2334
4952
|
});
|
|
2335
4953
|
program.command("doctor").description("Check codex and matrix connectivity").action(async () => {
|
|
2336
|
-
const
|
|
4954
|
+
const home = ensureRuntimeHomeOrExit();
|
|
4955
|
+
const config = await loadConfigWithPreflight("doctor", home);
|
|
4956
|
+
if (!config) {
|
|
4957
|
+
process.exitCode = 1;
|
|
4958
|
+
return;
|
|
4959
|
+
}
|
|
2337
4960
|
await runDoctor(config);
|
|
2338
4961
|
});
|
|
4962
|
+
var admin = program.command("admin").description("Admin utilities");
|
|
4963
|
+
var configCommand = program.command("config").description("Config snapshot utilities");
|
|
4964
|
+
admin.command("serve").description("Start admin config API server").option("--host <host>", "override admin bind host").option("--port <port>", "override admin bind port").option(
|
|
4965
|
+
"--allow-insecure-no-token",
|
|
4966
|
+
"allow serving admin API without ADMIN_TOKEN on non-loopback host (not recommended)"
|
|
4967
|
+
).action(async (options) => {
|
|
4968
|
+
ensureRuntimeHomeOrExit();
|
|
4969
|
+
const config = loadConfig();
|
|
4970
|
+
const host = options.host?.trim() || config.adminBindHost;
|
|
4971
|
+
const port = options.port ? parsePortOption(options.port, config.adminPort) : config.adminPort;
|
|
4972
|
+
const allowInsecureNoToken = options.allowInsecureNoToken ?? false;
|
|
4973
|
+
if (!config.adminToken && !allowInsecureNoToken && isNonLoopbackHost(host)) {
|
|
4974
|
+
process.stderr.write(
|
|
4975
|
+
[
|
|
4976
|
+
"Refusing to start admin server on non-loopback host without ADMIN_TOKEN.",
|
|
4977
|
+
"Fix: set ADMIN_TOKEN in .env, or explicitly pass --allow-insecure-no-token.",
|
|
4978
|
+
""
|
|
4979
|
+
].join("\n")
|
|
4980
|
+
);
|
|
4981
|
+
process.exitCode = 1;
|
|
4982
|
+
return;
|
|
4983
|
+
}
|
|
4984
|
+
const app = new CodeHarborAdminApp(config, { host, port });
|
|
4985
|
+
await app.start();
|
|
4986
|
+
const stop = async () => {
|
|
4987
|
+
await app.stop();
|
|
4988
|
+
process.exit(0);
|
|
4989
|
+
};
|
|
4990
|
+
process.once("SIGINT", () => {
|
|
4991
|
+
void stop();
|
|
4992
|
+
});
|
|
4993
|
+
process.once("SIGTERM", () => {
|
|
4994
|
+
void stop();
|
|
4995
|
+
});
|
|
4996
|
+
});
|
|
4997
|
+
configCommand.command("export").description("Export config snapshot as JSON").option("-o, --output <path>", "write snapshot to file instead of stdout").action(async (options) => {
|
|
4998
|
+
try {
|
|
4999
|
+
const home = ensureRuntimeHomeOrExit();
|
|
5000
|
+
await runConfigExportCommand({ outputPath: options.output, cwd: home });
|
|
5001
|
+
} catch (error) {
|
|
5002
|
+
process.stderr.write(`Config export failed: ${formatError3(error)}
|
|
5003
|
+
`);
|
|
5004
|
+
process.exitCode = 1;
|
|
5005
|
+
}
|
|
5006
|
+
});
|
|
5007
|
+
configCommand.command("import").description("Import config snapshot from JSON").argument("<file>", "snapshot file path").option("--dry-run", "validate snapshot without writing changes").action(async (file, options) => {
|
|
5008
|
+
try {
|
|
5009
|
+
const home = ensureRuntimeHomeOrExit();
|
|
5010
|
+
await runConfigImportCommand({
|
|
5011
|
+
filePath: file,
|
|
5012
|
+
dryRun: options.dryRun ?? false,
|
|
5013
|
+
cwd: home
|
|
5014
|
+
});
|
|
5015
|
+
} catch (error) {
|
|
5016
|
+
process.stderr.write(`Config import failed: ${formatError3(error)}
|
|
5017
|
+
`);
|
|
5018
|
+
process.exitCode = 1;
|
|
5019
|
+
}
|
|
5020
|
+
});
|
|
2339
5021
|
if (process.argv.length <= 2) {
|
|
2340
5022
|
process.argv.push("start");
|
|
2341
5023
|
}
|
|
2342
5024
|
void program.parseAsync(process.argv);
|
|
5025
|
+
async function loadConfigWithPreflight(commandName, runtimeHomePath) {
|
|
5026
|
+
const preflight = await runStartupPreflight({ cwd: runtimeHomePath });
|
|
5027
|
+
if (preflight.issues.length > 0) {
|
|
5028
|
+
const report = formatPreflightReport(preflight, commandName);
|
|
5029
|
+
if (preflight.ok) {
|
|
5030
|
+
process.stdout.write(report);
|
|
5031
|
+
} else {
|
|
5032
|
+
process.stderr.write(report);
|
|
5033
|
+
return null;
|
|
5034
|
+
}
|
|
5035
|
+
}
|
|
5036
|
+
try {
|
|
5037
|
+
return loadConfig();
|
|
5038
|
+
} catch (error) {
|
|
5039
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5040
|
+
process.stderr.write(`Configuration error: ${message}
|
|
5041
|
+
`);
|
|
5042
|
+
process.stderr.write('Fix: run "codeharbor init" and then retry.\n');
|
|
5043
|
+
return null;
|
|
5044
|
+
}
|
|
5045
|
+
}
|
|
5046
|
+
function ensureRuntimeHomeOrExit() {
|
|
5047
|
+
if (runtimeHome) {
|
|
5048
|
+
return runtimeHome;
|
|
5049
|
+
}
|
|
5050
|
+
const home = resolveRuntimeHome();
|
|
5051
|
+
try {
|
|
5052
|
+
import_node_fs9.default.mkdirSync(home, { recursive: true });
|
|
5053
|
+
} catch (error) {
|
|
5054
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5055
|
+
process.stderr.write(`Runtime setup failed: cannot create ${home}. ${message}
|
|
5056
|
+
`);
|
|
5057
|
+
process.exit(1);
|
|
5058
|
+
}
|
|
5059
|
+
try {
|
|
5060
|
+
process.chdir(home);
|
|
5061
|
+
} catch (error) {
|
|
5062
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5063
|
+
process.stderr.write(`Runtime setup failed: cannot switch to ${home}. ${message}
|
|
5064
|
+
`);
|
|
5065
|
+
process.exit(1);
|
|
5066
|
+
}
|
|
5067
|
+
loadEnvFromFile(import_node_path11.default.resolve(home, ".env"));
|
|
5068
|
+
runtimeHome = home;
|
|
5069
|
+
return runtimeHome;
|
|
5070
|
+
}
|
|
5071
|
+
function parsePortOption(raw, fallback) {
|
|
5072
|
+
const parsed = Number.parseInt(raw, 10);
|
|
5073
|
+
if (!Number.isFinite(parsed) || parsed < 1 || parsed > 65535) {
|
|
5074
|
+
process.stderr.write(`Invalid --port value: ${raw}; fallback to ${fallback}
|
|
5075
|
+
`);
|
|
5076
|
+
return fallback;
|
|
5077
|
+
}
|
|
5078
|
+
return parsed;
|
|
5079
|
+
}
|
|
5080
|
+
function formatError3(error) {
|
|
5081
|
+
if (error instanceof Error) {
|
|
5082
|
+
return error.message;
|
|
5083
|
+
}
|
|
5084
|
+
return String(error);
|
|
5085
|
+
}
|