codeharbor 0.1.10 → 0.1.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -3
- package/dist/cli.js +1504 -1370
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -30,14 +30,14 @@ var import_node_path12 = __toESM(require("path"));
|
|
|
30
30
|
var import_commander = require("commander");
|
|
31
31
|
|
|
32
32
|
// src/app.ts
|
|
33
|
-
var
|
|
33
|
+
var import_node_child_process4 = require("child_process");
|
|
34
34
|
var import_node_util2 = require("util");
|
|
35
35
|
|
|
36
36
|
// src/admin-server.ts
|
|
37
|
-
var
|
|
38
|
-
var
|
|
37
|
+
var import_node_child_process2 = require("child_process");
|
|
38
|
+
var import_node_fs4 = __toESM(require("fs"));
|
|
39
39
|
var import_node_http = __toESM(require("http"));
|
|
40
|
-
var
|
|
40
|
+
var import_node_path4 = __toESM(require("path"));
|
|
41
41
|
var import_node_util = require("util");
|
|
42
42
|
|
|
43
43
|
// src/init.ts
|
|
@@ -220,152 +220,514 @@ async function askYesNo(rl, question, defaultValue) {
|
|
|
220
220
|
return answer === "y" || answer === "yes";
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
-
// src/
|
|
224
|
-
var
|
|
225
|
-
var
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
223
|
+
// src/service-manager.ts
|
|
224
|
+
var import_node_child_process = require("child_process");
|
|
225
|
+
var import_node_fs3 = __toESM(require("fs"));
|
|
226
|
+
var import_node_os2 = __toESM(require("os"));
|
|
227
|
+
var import_node_path3 = __toESM(require("path"));
|
|
228
|
+
|
|
229
|
+
// src/runtime-home.ts
|
|
230
|
+
var import_node_fs2 = __toESM(require("fs"));
|
|
231
|
+
var import_node_os = __toESM(require("os"));
|
|
232
|
+
var import_node_path2 = __toESM(require("path"));
|
|
233
|
+
var LEGACY_RUNTIME_HOME = "/opt/codeharbor";
|
|
234
|
+
var USER_RUNTIME_HOME_DIR = ".codeharbor";
|
|
235
|
+
var DEFAULT_RUNTIME_HOME = import_node_path2.default.resolve(import_node_os.default.homedir(), USER_RUNTIME_HOME_DIR);
|
|
236
|
+
var RUNTIME_HOME_ENV_KEY = "CODEHARBOR_HOME";
|
|
237
|
+
function resolveRuntimeHome(env = process.env) {
|
|
238
|
+
const configured = env[RUNTIME_HOME_ENV_KEY]?.trim();
|
|
239
|
+
if (configured) {
|
|
240
|
+
return import_node_path2.default.resolve(configured);
|
|
230
241
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
logger;
|
|
235
|
-
stateStore;
|
|
236
|
-
configService;
|
|
237
|
-
host;
|
|
238
|
-
port;
|
|
239
|
-
adminToken;
|
|
240
|
-
adminIpAllowlist;
|
|
241
|
-
adminAllowedOrigins;
|
|
242
|
-
cwd;
|
|
243
|
-
checkCodex;
|
|
244
|
-
checkMatrix;
|
|
245
|
-
server = null;
|
|
246
|
-
address = null;
|
|
247
|
-
constructor(config, logger, stateStore, configService, options) {
|
|
248
|
-
this.config = config;
|
|
249
|
-
this.logger = logger;
|
|
250
|
-
this.stateStore = stateStore;
|
|
251
|
-
this.configService = configService;
|
|
252
|
-
this.host = options.host;
|
|
253
|
-
this.port = options.port;
|
|
254
|
-
this.adminToken = options.adminToken;
|
|
255
|
-
this.adminIpAllowlist = normalizeAllowlist(options.adminIpAllowlist ?? []);
|
|
256
|
-
this.adminAllowedOrigins = normalizeOriginAllowlist(options.adminAllowedOrigins ?? []);
|
|
257
|
-
this.cwd = options.cwd ?? process.cwd();
|
|
258
|
-
this.checkCodex = options.checkCodex ?? defaultCheckCodex;
|
|
259
|
-
this.checkMatrix = options.checkMatrix ?? defaultCheckMatrix;
|
|
242
|
+
const legacyEnvPath = import_node_path2.default.resolve(LEGACY_RUNTIME_HOME, ".env");
|
|
243
|
+
if (import_node_fs2.default.existsSync(legacyEnvPath)) {
|
|
244
|
+
return LEGACY_RUNTIME_HOME;
|
|
260
245
|
}
|
|
261
|
-
|
|
262
|
-
|
|
246
|
+
return resolveUserRuntimeHome(env);
|
|
247
|
+
}
|
|
248
|
+
function resolveUserRuntimeHome(env = process.env) {
|
|
249
|
+
const home = env.HOME?.trim() || import_node_os.default.homedir();
|
|
250
|
+
return import_node_path2.default.resolve(home, USER_RUNTIME_HOME_DIR);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// src/service-manager.ts
|
|
254
|
+
var SYSTEMD_DIR = "/etc/systemd/system";
|
|
255
|
+
var MAIN_SERVICE_NAME = "codeharbor.service";
|
|
256
|
+
var ADMIN_SERVICE_NAME = "codeharbor-admin.service";
|
|
257
|
+
var SUDOERS_DIR = "/etc/sudoers.d";
|
|
258
|
+
var RESTART_SUDOERS_FILE = "codeharbor-restart";
|
|
259
|
+
function resolveDefaultRunUser(env = process.env) {
|
|
260
|
+
const sudoUser = env.SUDO_USER?.trim();
|
|
261
|
+
if (sudoUser) {
|
|
262
|
+
return sudoUser;
|
|
263
263
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
264
|
+
const user = env.USER?.trim();
|
|
265
|
+
if (user) {
|
|
266
|
+
return user;
|
|
267
|
+
}
|
|
268
|
+
try {
|
|
269
|
+
return import_node_os2.default.userInfo().username;
|
|
270
|
+
} catch {
|
|
271
|
+
return "root";
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
function resolveRuntimeHomeForUser(runUser, env = process.env, explicitRuntimeHome) {
|
|
275
|
+
const configuredRuntimeHome = explicitRuntimeHome?.trim() || env[RUNTIME_HOME_ENV_KEY]?.trim();
|
|
276
|
+
if (configuredRuntimeHome) {
|
|
277
|
+
return import_node_path3.default.resolve(configuredRuntimeHome);
|
|
278
|
+
}
|
|
279
|
+
const userHome = resolveUserHome(runUser);
|
|
280
|
+
if (userHome) {
|
|
281
|
+
return import_node_path3.default.resolve(userHome, USER_RUNTIME_HOME_DIR);
|
|
282
|
+
}
|
|
283
|
+
return import_node_path3.default.resolve(import_node_os2.default.homedir(), USER_RUNTIME_HOME_DIR);
|
|
284
|
+
}
|
|
285
|
+
function buildMainServiceUnit(options) {
|
|
286
|
+
validateUnitOptions(options);
|
|
287
|
+
const runtimeHome2 = import_node_path3.default.resolve(options.runtimeHome);
|
|
288
|
+
return [
|
|
289
|
+
"[Unit]",
|
|
290
|
+
"Description=CodeHarbor main service",
|
|
291
|
+
"After=network-online.target",
|
|
292
|
+
"Wants=network-online.target",
|
|
293
|
+
"",
|
|
294
|
+
"[Service]",
|
|
295
|
+
"Type=simple",
|
|
296
|
+
`User=${options.runUser}`,
|
|
297
|
+
`WorkingDirectory=${runtimeHome2}`,
|
|
298
|
+
`Environment=CODEHARBOR_HOME=${runtimeHome2}`,
|
|
299
|
+
`ExecStart=${import_node_path3.default.resolve(options.nodeBinPath)} ${import_node_path3.default.resolve(options.cliScriptPath)} start`,
|
|
300
|
+
"Restart=always",
|
|
301
|
+
"RestartSec=3",
|
|
302
|
+
"NoNewPrivileges=true",
|
|
303
|
+
"PrivateTmp=true",
|
|
304
|
+
"ProtectSystem=full",
|
|
305
|
+
"ProtectHome=false",
|
|
306
|
+
`ReadWritePaths=${runtimeHome2}`,
|
|
307
|
+
"",
|
|
308
|
+
"[Install]",
|
|
309
|
+
"WantedBy=multi-user.target",
|
|
310
|
+
""
|
|
311
|
+
].join("\n");
|
|
312
|
+
}
|
|
313
|
+
function buildAdminServiceUnit(options) {
|
|
314
|
+
validateUnitOptions(options);
|
|
315
|
+
const runtimeHome2 = import_node_path3.default.resolve(options.runtimeHome);
|
|
316
|
+
return [
|
|
317
|
+
"[Unit]",
|
|
318
|
+
"Description=CodeHarbor admin service",
|
|
319
|
+
"After=network-online.target",
|
|
320
|
+
"Wants=network-online.target",
|
|
321
|
+
"",
|
|
322
|
+
"[Service]",
|
|
323
|
+
"Type=simple",
|
|
324
|
+
`User=${options.runUser}`,
|
|
325
|
+
`WorkingDirectory=${runtimeHome2}`,
|
|
326
|
+
`Environment=CODEHARBOR_HOME=${runtimeHome2}`,
|
|
327
|
+
`ExecStart=${import_node_path3.default.resolve(options.nodeBinPath)} ${import_node_path3.default.resolve(options.cliScriptPath)} admin serve`,
|
|
328
|
+
"Restart=always",
|
|
329
|
+
"RestartSec=3",
|
|
330
|
+
"NoNewPrivileges=true",
|
|
331
|
+
"PrivateTmp=true",
|
|
332
|
+
"ProtectSystem=full",
|
|
333
|
+
"ProtectHome=false",
|
|
334
|
+
`ReadWritePaths=${runtimeHome2}`,
|
|
335
|
+
"",
|
|
336
|
+
"[Install]",
|
|
337
|
+
"WantedBy=multi-user.target",
|
|
338
|
+
""
|
|
339
|
+
].join("\n");
|
|
340
|
+
}
|
|
341
|
+
function buildRestartSudoersPolicy(options) {
|
|
342
|
+
const runUser = options.runUser.trim();
|
|
343
|
+
const systemctlPath = options.systemctlPath.trim();
|
|
344
|
+
validateSimpleValue(runUser, "runUser");
|
|
345
|
+
validateSimpleValue(systemctlPath, "systemctlPath");
|
|
346
|
+
if (!import_node_path3.default.isAbsolute(systemctlPath)) {
|
|
347
|
+
throw new Error("systemctlPath must be an absolute path.");
|
|
348
|
+
}
|
|
349
|
+
return [
|
|
350
|
+
"# Managed by CodeHarbor service install; do not edit manually.",
|
|
351
|
+
`Defaults:${runUser} !requiretty`,
|
|
352
|
+
`${runUser} ALL=(root) NOPASSWD: ${systemctlPath} restart ${MAIN_SERVICE_NAME}, ${systemctlPath} restart ${ADMIN_SERVICE_NAME}`,
|
|
353
|
+
""
|
|
354
|
+
].join("\n");
|
|
355
|
+
}
|
|
356
|
+
function installSystemdServices(options) {
|
|
357
|
+
assertLinuxWithSystemd();
|
|
358
|
+
assertRootPrivileges();
|
|
359
|
+
const output = options.output ?? process.stdout;
|
|
360
|
+
const runUser = options.runUser.trim();
|
|
361
|
+
const runtimeHome2 = import_node_path3.default.resolve(options.runtimeHome);
|
|
362
|
+
validateSimpleValue(runUser, "runUser");
|
|
363
|
+
validateSimpleValue(runtimeHome2, "runtimeHome");
|
|
364
|
+
validateSimpleValue(options.nodeBinPath, "nodeBinPath");
|
|
365
|
+
validateSimpleValue(options.cliScriptPath, "cliScriptPath");
|
|
366
|
+
ensureUserExists(runUser);
|
|
367
|
+
const runGroup = resolveUserGroup(runUser);
|
|
368
|
+
import_node_fs3.default.mkdirSync(runtimeHome2, { recursive: true });
|
|
369
|
+
runCommand("chown", ["-R", `${runUser}:${runGroup}`, runtimeHome2]);
|
|
370
|
+
const mainPath = import_node_path3.default.join(SYSTEMD_DIR, MAIN_SERVICE_NAME);
|
|
371
|
+
const adminPath = import_node_path3.default.join(SYSTEMD_DIR, ADMIN_SERVICE_NAME);
|
|
372
|
+
const restartSudoersPath = import_node_path3.default.join(SUDOERS_DIR, RESTART_SUDOERS_FILE);
|
|
373
|
+
const unitOptions = {
|
|
374
|
+
runUser,
|
|
375
|
+
runtimeHome: runtimeHome2,
|
|
376
|
+
nodeBinPath: options.nodeBinPath,
|
|
377
|
+
cliScriptPath: options.cliScriptPath
|
|
378
|
+
};
|
|
379
|
+
import_node_fs3.default.writeFileSync(mainPath, buildMainServiceUnit(unitOptions), "utf8");
|
|
380
|
+
if (options.installAdmin) {
|
|
381
|
+
import_node_fs3.default.writeFileSync(adminPath, buildAdminServiceUnit(unitOptions), "utf8");
|
|
382
|
+
if (runUser !== "root") {
|
|
383
|
+
const policy = buildRestartSudoersPolicy({
|
|
384
|
+
runUser,
|
|
385
|
+
systemctlPath: resolveSystemctlPath()
|
|
289
386
|
});
|
|
290
|
-
|
|
387
|
+
import_node_fs3.default.mkdirSync(SUDOERS_DIR, { recursive: true });
|
|
388
|
+
import_node_fs3.default.writeFileSync(restartSudoersPath, policy, "utf8");
|
|
389
|
+
import_node_fs3.default.chmodSync(restartSudoersPath, 288);
|
|
390
|
+
}
|
|
291
391
|
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
392
|
+
runSystemctl(["daemon-reload"]);
|
|
393
|
+
if (options.startNow) {
|
|
394
|
+
runSystemctl(["enable", "--now", MAIN_SERVICE_NAME]);
|
|
395
|
+
if (options.installAdmin) {
|
|
396
|
+
runSystemctl(["enable", "--now", ADMIN_SERVICE_NAME]);
|
|
397
|
+
}
|
|
398
|
+
} else {
|
|
399
|
+
runSystemctl(["enable", MAIN_SERVICE_NAME]);
|
|
400
|
+
if (options.installAdmin) {
|
|
401
|
+
runSystemctl(["enable", ADMIN_SERVICE_NAME]);
|
|
295
402
|
}
|
|
296
|
-
const server = this.server;
|
|
297
|
-
this.server = null;
|
|
298
|
-
this.address = null;
|
|
299
|
-
await new Promise((resolve, reject) => {
|
|
300
|
-
server.close((error) => {
|
|
301
|
-
if (error) {
|
|
302
|
-
reject(error);
|
|
303
|
-
return;
|
|
304
|
-
}
|
|
305
|
-
resolve();
|
|
306
|
-
});
|
|
307
|
-
});
|
|
308
403
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
404
|
+
output.write(`Installed systemd unit: ${mainPath}
|
|
405
|
+
`);
|
|
406
|
+
if (options.installAdmin) {
|
|
407
|
+
output.write(`Installed systemd unit: ${adminPath}
|
|
408
|
+
`);
|
|
409
|
+
if (runUser !== "root") {
|
|
410
|
+
output.write(`Installed sudoers policy: ${restartSudoersPath}
|
|
411
|
+
`);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
output.write("Done. Check status with: systemctl status codeharbor --no-pager\n");
|
|
415
|
+
}
|
|
416
|
+
function uninstallSystemdServices(options) {
|
|
417
|
+
assertLinuxWithSystemd();
|
|
418
|
+
assertRootPrivileges();
|
|
419
|
+
const output = options.output ?? process.stdout;
|
|
420
|
+
const mainPath = import_node_path3.default.join(SYSTEMD_DIR, MAIN_SERVICE_NAME);
|
|
421
|
+
const adminPath = import_node_path3.default.join(SYSTEMD_DIR, ADMIN_SERVICE_NAME);
|
|
422
|
+
const restartSudoersPath = import_node_path3.default.join(SUDOERS_DIR, RESTART_SUDOERS_FILE);
|
|
423
|
+
stopAndDisableIfPresent(MAIN_SERVICE_NAME);
|
|
424
|
+
if (import_node_fs3.default.existsSync(mainPath)) {
|
|
425
|
+
import_node_fs3.default.unlinkSync(mainPath);
|
|
426
|
+
}
|
|
427
|
+
if (options.removeAdmin) {
|
|
428
|
+
stopAndDisableIfPresent(ADMIN_SERVICE_NAME);
|
|
429
|
+
if (import_node_fs3.default.existsSync(adminPath)) {
|
|
430
|
+
import_node_fs3.default.unlinkSync(adminPath);
|
|
431
|
+
}
|
|
432
|
+
if (import_node_fs3.default.existsSync(restartSudoersPath)) {
|
|
433
|
+
import_node_fs3.default.unlinkSync(restartSudoersPath);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
runSystemctl(["daemon-reload"]);
|
|
437
|
+
runSystemctlIgnoreFailure(["reset-failed"]);
|
|
438
|
+
output.write(`Removed systemd unit: ${mainPath}
|
|
439
|
+
`);
|
|
440
|
+
if (options.removeAdmin) {
|
|
441
|
+
output.write(`Removed systemd unit: ${adminPath}
|
|
442
|
+
`);
|
|
443
|
+
output.write(`Removed sudoers policy: ${restartSudoersPath}
|
|
444
|
+
`);
|
|
445
|
+
}
|
|
446
|
+
output.write("Done.\n");
|
|
447
|
+
}
|
|
448
|
+
function restartSystemdServices(options) {
|
|
449
|
+
assertLinuxWithSystemd();
|
|
450
|
+
const output = options.output ?? process.stdout;
|
|
451
|
+
const runWithSudoFallback = options.allowSudoFallback ?? true;
|
|
452
|
+
const systemctlRunner = hasRootPrivileges() || !runWithSudoFallback ? runSystemctl : runSystemctlWithNonInteractiveSudo;
|
|
453
|
+
systemctlRunner(["restart", MAIN_SERVICE_NAME]);
|
|
454
|
+
output.write(`Restarted service: ${MAIN_SERVICE_NAME}
|
|
455
|
+
`);
|
|
456
|
+
if (options.restartAdmin) {
|
|
457
|
+
systemctlRunner(["restart", ADMIN_SERVICE_NAME]);
|
|
458
|
+
output.write(`Restarted service: ${ADMIN_SERVICE_NAME}
|
|
459
|
+
`);
|
|
460
|
+
}
|
|
461
|
+
output.write("Done.\n");
|
|
462
|
+
}
|
|
463
|
+
function resolveUserHome(runUser) {
|
|
464
|
+
try {
|
|
465
|
+
const passwdRaw = import_node_fs3.default.readFileSync("/etc/passwd", "utf8");
|
|
466
|
+
const line = passwdRaw.split(/\r?\n/).find((item) => item.startsWith(`${runUser}:`));
|
|
467
|
+
if (!line) {
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
const fields = line.split(":");
|
|
471
|
+
return fields[5] ? fields[5].trim() : null;
|
|
472
|
+
} catch {
|
|
473
|
+
return null;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
function validateUnitOptions(options) {
|
|
477
|
+
validateSimpleValue(options.runUser, "runUser");
|
|
478
|
+
validateSimpleValue(options.runtimeHome, "runtimeHome");
|
|
479
|
+
validateSimpleValue(options.nodeBinPath, "nodeBinPath");
|
|
480
|
+
validateSimpleValue(options.cliScriptPath, "cliScriptPath");
|
|
481
|
+
}
|
|
482
|
+
function validateSimpleValue(value, key) {
|
|
483
|
+
if (!value.trim()) {
|
|
484
|
+
throw new Error(`${key} cannot be empty.`);
|
|
485
|
+
}
|
|
486
|
+
if (/[\r\n]/.test(value)) {
|
|
487
|
+
throw new Error(`${key} contains invalid newline characters.`);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
function assertLinuxWithSystemd() {
|
|
491
|
+
if (process.platform !== "linux") {
|
|
492
|
+
throw new Error("Systemd service install only supports Linux.");
|
|
493
|
+
}
|
|
494
|
+
try {
|
|
495
|
+
(0, import_node_child_process.execFileSync)("systemctl", ["--version"], { stdio: "ignore" });
|
|
496
|
+
} catch {
|
|
497
|
+
throw new Error("systemctl is required but not found.");
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
function assertRootPrivileges() {
|
|
501
|
+
if (!hasRootPrivileges()) {
|
|
502
|
+
throw new Error("Root privileges are required. Run with sudo.");
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
function hasRootPrivileges() {
|
|
506
|
+
if (typeof process.getuid !== "function") {
|
|
507
|
+
return true;
|
|
508
|
+
}
|
|
509
|
+
return process.getuid() === 0;
|
|
510
|
+
}
|
|
511
|
+
function ensureUserExists(runUser) {
|
|
512
|
+
runCommand("id", ["-u", runUser]);
|
|
513
|
+
}
|
|
514
|
+
function resolveUserGroup(runUser) {
|
|
515
|
+
return runCommand("id", ["-gn", runUser]).trim();
|
|
516
|
+
}
|
|
517
|
+
function runSystemctl(args) {
|
|
518
|
+
runCommand("systemctl", args);
|
|
519
|
+
}
|
|
520
|
+
function runSystemctlWithNonInteractiveSudo(args) {
|
|
521
|
+
const systemctlPath = resolveSystemctlPath();
|
|
522
|
+
try {
|
|
523
|
+
runCommand("sudo", ["-n", systemctlPath, ...args]);
|
|
524
|
+
} catch (error) {
|
|
525
|
+
throw new Error(
|
|
526
|
+
"Root privileges are required. Configure passwordless sudo for the CodeHarbor service user or run the CLI command manually with sudo.",
|
|
527
|
+
{ cause: error }
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
function stopAndDisableIfPresent(unitName) {
|
|
532
|
+
runSystemctlIgnoreFailure(["disable", "--now", unitName]);
|
|
533
|
+
}
|
|
534
|
+
function runSystemctlIgnoreFailure(args) {
|
|
535
|
+
try {
|
|
536
|
+
runCommand("systemctl", args);
|
|
537
|
+
} catch {
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
function resolveSystemctlPath() {
|
|
541
|
+
const candidates = [];
|
|
542
|
+
const pathEntries = (process.env.PATH ?? "").split(import_node_path3.default.delimiter).filter(Boolean);
|
|
543
|
+
for (const entry of pathEntries) {
|
|
544
|
+
candidates.push(import_node_path3.default.join(entry, "systemctl"));
|
|
545
|
+
}
|
|
546
|
+
candidates.push("/usr/bin/systemctl", "/bin/systemctl", "/usr/local/bin/systemctl");
|
|
547
|
+
for (const candidate of candidates) {
|
|
548
|
+
if (import_node_path3.default.isAbsolute(candidate) && import_node_fs3.default.existsSync(candidate)) {
|
|
549
|
+
return candidate;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
throw new Error("Unable to resolve absolute systemctl path.");
|
|
553
|
+
}
|
|
554
|
+
function runCommand(file, args) {
|
|
555
|
+
try {
|
|
556
|
+
return (0, import_node_child_process.execFileSync)(file, args, {
|
|
557
|
+
encoding: "utf8",
|
|
558
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
559
|
+
});
|
|
560
|
+
} catch (error) {
|
|
561
|
+
throw new Error(formatCommandError(file, args, error), { cause: error });
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
function formatCommandError(file, args, error) {
|
|
565
|
+
const command = `${file} ${args.join(" ")}`.trim();
|
|
566
|
+
if (error && typeof error === "object") {
|
|
567
|
+
const maybeError = error;
|
|
568
|
+
const stderr = bufferToTrimmedString(maybeError.stderr);
|
|
569
|
+
const stdout = bufferToTrimmedString(maybeError.stdout);
|
|
570
|
+
const details = stderr || stdout || maybeError.message || "command failed";
|
|
571
|
+
return `Command failed: ${command}. ${details}`;
|
|
572
|
+
}
|
|
573
|
+
return `Command failed: ${command}. ${String(error)}`;
|
|
574
|
+
}
|
|
575
|
+
function bufferToTrimmedString(value) {
|
|
576
|
+
if (!value) {
|
|
577
|
+
return "";
|
|
578
|
+
}
|
|
579
|
+
const text = typeof value === "string" ? value : value.toString("utf8");
|
|
580
|
+
return text.trim();
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// src/admin-server.ts
|
|
584
|
+
var execFileAsync = (0, import_node_util.promisify)(import_node_child_process2.execFile);
|
|
585
|
+
var HttpError = class extends Error {
|
|
586
|
+
statusCode;
|
|
587
|
+
constructor(statusCode, message) {
|
|
588
|
+
super(message);
|
|
589
|
+
this.statusCode = statusCode;
|
|
590
|
+
}
|
|
591
|
+
};
|
|
592
|
+
var AdminServer = class {
|
|
593
|
+
config;
|
|
594
|
+
logger;
|
|
595
|
+
stateStore;
|
|
596
|
+
configService;
|
|
597
|
+
host;
|
|
598
|
+
port;
|
|
599
|
+
adminToken;
|
|
600
|
+
adminIpAllowlist;
|
|
601
|
+
adminAllowedOrigins;
|
|
602
|
+
cwd;
|
|
603
|
+
checkCodex;
|
|
604
|
+
checkMatrix;
|
|
605
|
+
restartServices;
|
|
606
|
+
server = null;
|
|
607
|
+
address = null;
|
|
608
|
+
constructor(config, logger, stateStore, configService, options) {
|
|
609
|
+
this.config = config;
|
|
610
|
+
this.logger = logger;
|
|
611
|
+
this.stateStore = stateStore;
|
|
612
|
+
this.configService = configService;
|
|
613
|
+
this.host = options.host;
|
|
614
|
+
this.port = options.port;
|
|
615
|
+
this.adminToken = options.adminToken;
|
|
616
|
+
this.adminIpAllowlist = normalizeAllowlist(options.adminIpAllowlist ?? []);
|
|
617
|
+
this.adminAllowedOrigins = normalizeOriginAllowlist(options.adminAllowedOrigins ?? []);
|
|
618
|
+
this.cwd = options.cwd ?? process.cwd();
|
|
619
|
+
this.checkCodex = options.checkCodex ?? defaultCheckCodex;
|
|
620
|
+
this.checkMatrix = options.checkMatrix ?? defaultCheckMatrix;
|
|
621
|
+
this.restartServices = options.restartServices ?? defaultRestartServices;
|
|
622
|
+
}
|
|
623
|
+
getAddress() {
|
|
624
|
+
return this.address;
|
|
625
|
+
}
|
|
626
|
+
async start() {
|
|
627
|
+
if (this.server) {
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
this.server = import_node_http.default.createServer((req, res) => {
|
|
631
|
+
void this.handleRequest(req, res);
|
|
632
|
+
});
|
|
633
|
+
await new Promise((resolve, reject) => {
|
|
634
|
+
if (!this.server) {
|
|
635
|
+
reject(new Error("admin server is not initialized"));
|
|
343
636
|
return;
|
|
344
637
|
}
|
|
345
|
-
|
|
346
|
-
|
|
638
|
+
this.server.once("error", reject);
|
|
639
|
+
this.server.listen(this.port, this.host, () => {
|
|
640
|
+
this.server?.removeListener("error", reject);
|
|
641
|
+
const address = this.server?.address();
|
|
642
|
+
if (!address || typeof address === "string") {
|
|
643
|
+
reject(new Error("failed to resolve admin server address"));
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
this.address = {
|
|
647
|
+
host: address.address,
|
|
648
|
+
port: address.port
|
|
649
|
+
};
|
|
650
|
+
resolve();
|
|
651
|
+
});
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
async stop() {
|
|
655
|
+
if (!this.server) {
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
const server = this.server;
|
|
659
|
+
this.server = null;
|
|
660
|
+
this.address = null;
|
|
661
|
+
await new Promise((resolve, reject) => {
|
|
662
|
+
server.close((error) => {
|
|
663
|
+
if (error) {
|
|
664
|
+
reject(error);
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
resolve();
|
|
668
|
+
});
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
async handleRequest(req, res) {
|
|
672
|
+
try {
|
|
673
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
674
|
+
this.setSecurityHeaders(res);
|
|
675
|
+
const corsDecision = this.resolveCors(req);
|
|
676
|
+
this.setCorsHeaders(res, corsDecision);
|
|
677
|
+
if (!this.isClientAllowed(req)) {
|
|
678
|
+
this.sendJson(res, 403, {
|
|
347
679
|
ok: false,
|
|
348
|
-
error: "
|
|
680
|
+
error: "Forbidden by ADMIN_IP_ALLOWLIST."
|
|
349
681
|
});
|
|
350
682
|
return;
|
|
351
683
|
}
|
|
352
|
-
if (
|
|
353
|
-
this.sendJson(res,
|
|
354
|
-
ok:
|
|
355
|
-
|
|
356
|
-
effective: "next_start_for_env_changes"
|
|
684
|
+
if (url.pathname.startsWith("/api/admin/") && corsDecision.origin && !corsDecision.allowed) {
|
|
685
|
+
this.sendJson(res, 403, {
|
|
686
|
+
ok: false,
|
|
687
|
+
error: "Forbidden by ADMIN_ALLOWED_ORIGINS."
|
|
357
688
|
});
|
|
358
689
|
return;
|
|
359
690
|
}
|
|
360
|
-
if (req.method === "
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
}
|
|
368
|
-
|
|
691
|
+
if (req.method === "OPTIONS") {
|
|
692
|
+
if (corsDecision.origin && !corsDecision.allowed) {
|
|
693
|
+
this.sendJson(res, 403, {
|
|
694
|
+
ok: false,
|
|
695
|
+
error: "Forbidden by ADMIN_ALLOWED_ORIGINS."
|
|
696
|
+
});
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
res.writeHead(204);
|
|
700
|
+
res.end();
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
if (req.method === "GET" && isUiPath(url.pathname)) {
|
|
704
|
+
this.sendHtml(res, renderAdminConsoleHtml());
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
if (url.pathname.startsWith("/api/admin/") && !this.isAuthorized(req)) {
|
|
708
|
+
this.sendJson(res, 401, {
|
|
709
|
+
ok: false,
|
|
710
|
+
error: "Unauthorized. Provide Authorization: Bearer <ADMIN_TOKEN>."
|
|
711
|
+
});
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
if (req.method === "GET" && url.pathname === "/api/admin/config/global") {
|
|
715
|
+
this.sendJson(res, 200, {
|
|
716
|
+
ok: true,
|
|
717
|
+
data: buildGlobalConfigSnapshot(this.config),
|
|
718
|
+
effective: "next_start_for_env_changes"
|
|
719
|
+
});
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
if (req.method === "PUT" && url.pathname === "/api/admin/config/global") {
|
|
723
|
+
const body = await readJsonBody(req);
|
|
724
|
+
const actor = readActor(req);
|
|
725
|
+
const result = this.updateGlobalConfig(body, actor);
|
|
726
|
+
this.sendJson(res, 200, {
|
|
727
|
+
ok: true,
|
|
728
|
+
...result
|
|
729
|
+
});
|
|
730
|
+
return;
|
|
369
731
|
}
|
|
370
732
|
if (req.method === "GET" && url.pathname === "/api/admin/config/rooms") {
|
|
371
733
|
this.sendJson(res, 200, {
|
|
@@ -420,6 +782,33 @@ var AdminServer = class {
|
|
|
420
782
|
});
|
|
421
783
|
return;
|
|
422
784
|
}
|
|
785
|
+
if (req.method === "POST" && url.pathname === "/api/admin/service/restart") {
|
|
786
|
+
const body = asObject(await readJsonBody(req), "service restart payload");
|
|
787
|
+
const restartAdmin = normalizeBoolean(body.withAdmin, false);
|
|
788
|
+
const actor = readActor(req);
|
|
789
|
+
try {
|
|
790
|
+
const result = await this.restartServices(restartAdmin);
|
|
791
|
+
this.stateStore.appendConfigRevision(
|
|
792
|
+
actor,
|
|
793
|
+
restartAdmin ? "restart services (main + admin)" : "restart service (main)",
|
|
794
|
+
JSON.stringify({
|
|
795
|
+
type: "service_restart",
|
|
796
|
+
restartAdmin,
|
|
797
|
+
restarted: result.restarted
|
|
798
|
+
})
|
|
799
|
+
);
|
|
800
|
+
this.sendJson(res, 200, {
|
|
801
|
+
ok: true,
|
|
802
|
+
restarted: result.restarted
|
|
803
|
+
});
|
|
804
|
+
return;
|
|
805
|
+
} catch (error) {
|
|
806
|
+
throw new HttpError(
|
|
807
|
+
500,
|
|
808
|
+
`Service restart failed: ${formatError(error)}. Install services via "codeharbor service install --with-admin" to auto-configure restart permissions, or run CLI command manually with sudo.`
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
423
812
|
this.sendJson(res, 404, {
|
|
424
813
|
ok: false,
|
|
425
814
|
error: `Not found: ${req.method ?? "GET"} ${url.pathname}`
|
|
@@ -450,7 +839,7 @@ var AdminServer = class {
|
|
|
450
839
|
updatedKeys.push("matrixCommandPrefix");
|
|
451
840
|
}
|
|
452
841
|
if ("codexWorkdir" in body) {
|
|
453
|
-
const workdir =
|
|
842
|
+
const workdir = import_node_path4.default.resolve(String(body.codexWorkdir ?? "").trim());
|
|
454
843
|
ensureDirectory(workdir, "codexWorkdir");
|
|
455
844
|
this.config.codexWorkdir = workdir;
|
|
456
845
|
envUpdates.CODEX_WORKDIR = workdir;
|
|
@@ -674,11 +1063,11 @@ var AdminServer = class {
|
|
|
674
1063
|
return this.adminIpAllowlist.includes(normalizedRemote);
|
|
675
1064
|
}
|
|
676
1065
|
persistEnvUpdates(updates) {
|
|
677
|
-
const envPath =
|
|
678
|
-
const examplePath =
|
|
679
|
-
const template =
|
|
1066
|
+
const envPath = import_node_path4.default.resolve(this.cwd, ".env");
|
|
1067
|
+
const examplePath = import_node_path4.default.resolve(this.cwd, ".env.example");
|
|
1068
|
+
const template = import_node_fs4.default.existsSync(envPath) ? import_node_fs4.default.readFileSync(envPath, "utf8") : import_node_fs4.default.existsSync(examplePath) ? import_node_fs4.default.readFileSync(examplePath, "utf8") : "";
|
|
680
1069
|
const next = applyEnvOverrides(template, updates);
|
|
681
|
-
|
|
1070
|
+
import_node_fs4.default.writeFileSync(envPath, next, "utf8");
|
|
682
1071
|
}
|
|
683
1072
|
resolveCors(req) {
|
|
684
1073
|
const origin = normalizeOriginHeader(req.headers.origin);
|
|
@@ -705,7 +1094,7 @@ var AdminServer = class {
|
|
|
705
1094
|
}
|
|
706
1095
|
res.setHeader("Access-Control-Allow-Origin", corsDecision.origin);
|
|
707
1096
|
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Admin-Token, X-Admin-Actor");
|
|
708
|
-
res.setHeader("Access-Control-Allow-Methods", "GET, PUT, DELETE, OPTIONS");
|
|
1097
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, PUT, POST, DELETE, OPTIONS");
|
|
709
1098
|
appendVaryHeader(res, "Origin");
|
|
710
1099
|
}
|
|
711
1100
|
setSecurityHeaders(res) {
|
|
@@ -776,6 +1165,22 @@ function parseJsonLoose(raw) {
|
|
|
776
1165
|
return raw;
|
|
777
1166
|
}
|
|
778
1167
|
}
|
|
1168
|
+
async function defaultRestartServices(restartAdmin) {
|
|
1169
|
+
const outputChunks = [];
|
|
1170
|
+
const output = {
|
|
1171
|
+
write: (chunk) => {
|
|
1172
|
+
outputChunks.push(typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8"));
|
|
1173
|
+
return true;
|
|
1174
|
+
}
|
|
1175
|
+
};
|
|
1176
|
+
restartSystemdServices({
|
|
1177
|
+
restartAdmin,
|
|
1178
|
+
output
|
|
1179
|
+
});
|
|
1180
|
+
return {
|
|
1181
|
+
restarted: restartAdmin ? ["codeharbor", "codeharbor-admin"] : ["codeharbor"]
|
|
1182
|
+
};
|
|
1183
|
+
}
|
|
779
1184
|
function isUiPath(pathname) {
|
|
780
1185
|
return pathname === "/" || pathname === "/index.html" || pathname === "/settings/global" || pathname === "/settings/rooms" || pathname === "/health" || pathname === "/audit";
|
|
781
1186
|
}
|
|
@@ -924,7 +1329,7 @@ function normalizeNonNegativeInt(value, fallback) {
|
|
|
924
1329
|
return normalizePositiveInt(value, fallback, 0, Number.MAX_SAFE_INTEGER);
|
|
925
1330
|
}
|
|
926
1331
|
function ensureDirectory(targetPath, fieldName) {
|
|
927
|
-
if (!
|
|
1332
|
+
if (!import_node_fs4.default.existsSync(targetPath) || !import_node_fs4.default.statSync(targetPath).isDirectory()) {
|
|
928
1333
|
throw new HttpError(400, `${fieldName} must be an existing directory: ${targetPath}`);
|
|
929
1334
|
}
|
|
930
1335
|
}
|
|
@@ -1266,6 +1671,8 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
|
|
|
1266
1671
|
<div class="actions">
|
|
1267
1672
|
<button id="global-save-btn" type="button">Save Global Config</button>
|
|
1268
1673
|
<button id="global-reload-btn" type="button" class="secondary">Reload</button>
|
|
1674
|
+
<button id="global-restart-main-btn" type="button" class="secondary">Restart Main Service</button>
|
|
1675
|
+
<button id="global-restart-all-btn" type="button" class="secondary">Restart Main + Admin</button>
|
|
1269
1676
|
</div>
|
|
1270
1677
|
<p class="muted">Saving global config updates .env and requires restart to fully take effect.</p>
|
|
1271
1678
|
</section>
|
|
@@ -1408,6 +1815,12 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
|
|
|
1408
1815
|
|
|
1409
1816
|
document.getElementById("global-save-btn").addEventListener("click", saveGlobal);
|
|
1410
1817
|
document.getElementById("global-reload-btn").addEventListener("click", loadGlobal);
|
|
1818
|
+
document.getElementById("global-restart-main-btn").addEventListener("click", function () {
|
|
1819
|
+
restartManagedServices(false);
|
|
1820
|
+
});
|
|
1821
|
+
document.getElementById("global-restart-all-btn").addEventListener("click", function () {
|
|
1822
|
+
restartManagedServices(true);
|
|
1823
|
+
});
|
|
1411
1824
|
document.getElementById("room-load-btn").addEventListener("click", loadRoom);
|
|
1412
1825
|
document.getElementById("room-save-btn").addEventListener("click", saveRoom);
|
|
1413
1826
|
document.getElementById("room-delete-btn").addEventListener("click", deleteRoom);
|
|
@@ -1611,6 +2024,19 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
|
|
|
1611
2024
|
}
|
|
1612
2025
|
}
|
|
1613
2026
|
|
|
2027
|
+
async function restartManagedServices(withAdmin) {
|
|
2028
|
+
try {
|
|
2029
|
+
var response = await apiRequest("/api/admin/service/restart", "POST", {
|
|
2030
|
+
withAdmin: Boolean(withAdmin)
|
|
2031
|
+
});
|
|
2032
|
+
var restarted = Array.isArray(response.restarted) ? response.restarted.join(", ") : "codeharbor";
|
|
2033
|
+
var suffix = withAdmin ? " Admin page may reconnect during restart." : "";
|
|
2034
|
+
showNotice("warn", "Restart requested: " + restarted + "." + suffix);
|
|
2035
|
+
} catch (error) {
|
|
2036
|
+
showNotice("error", "Failed to restart service(s): " + error.message);
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
|
|
1614
2040
|
async function refreshRoomList() {
|
|
1615
2041
|
try {
|
|
1616
2042
|
var response = await apiRequest("/api/admin/config/rooms", "GET");
|
|
@@ -1836,8 +2262,8 @@ async function defaultCheckMatrix(homeserver, timeoutMs) {
|
|
|
1836
2262
|
|
|
1837
2263
|
// src/channels/matrix-channel.ts
|
|
1838
2264
|
var import_promises2 = __toESM(require("fs/promises"));
|
|
1839
|
-
var
|
|
1840
|
-
var
|
|
2265
|
+
var import_node_os3 = __toESM(require("os"));
|
|
2266
|
+
var import_node_path5 = __toESM(require("path"));
|
|
1841
2267
|
var import_matrix_js_sdk = require("matrix-js-sdk");
|
|
1842
2268
|
|
|
1843
2269
|
// src/utils/message.ts
|
|
@@ -2260,10 +2686,10 @@ var MatrixChannel = class {
|
|
|
2260
2686
|
}
|
|
2261
2687
|
const bytes = Buffer.from(await response.arrayBuffer());
|
|
2262
2688
|
const extension = resolveFileExtension(fileName, mimeType);
|
|
2263
|
-
const directory =
|
|
2689
|
+
const directory = import_node_path5.default.join(import_node_os3.default.tmpdir(), "codeharbor-media");
|
|
2264
2690
|
await import_promises2.default.mkdir(directory, { recursive: true });
|
|
2265
2691
|
const safeEventId = sanitizeFilename(eventId);
|
|
2266
|
-
const targetPath =
|
|
2692
|
+
const targetPath = import_node_path5.default.join(directory, `${safeEventId}-${index}${extension}`);
|
|
2267
2693
|
await import_promises2.default.writeFile(targetPath, bytes);
|
|
2268
2694
|
return targetPath;
|
|
2269
2695
|
}
|
|
@@ -2348,7 +2774,7 @@ function sanitizeFilename(value) {
|
|
|
2348
2774
|
return value.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 80);
|
|
2349
2775
|
}
|
|
2350
2776
|
function resolveFileExtension(fileName, mimeType) {
|
|
2351
|
-
const ext =
|
|
2777
|
+
const ext = import_node_path5.default.extname(fileName).trim();
|
|
2352
2778
|
if (ext) {
|
|
2353
2779
|
return ext;
|
|
2354
2780
|
}
|
|
@@ -2365,14 +2791,14 @@ function resolveFileExtension(fileName, mimeType) {
|
|
|
2365
2791
|
}
|
|
2366
2792
|
|
|
2367
2793
|
// src/config-service.ts
|
|
2368
|
-
var
|
|
2369
|
-
var
|
|
2794
|
+
var import_node_fs5 = __toESM(require("fs"));
|
|
2795
|
+
var import_node_path6 = __toESM(require("path"));
|
|
2370
2796
|
var ConfigService = class {
|
|
2371
2797
|
stateStore;
|
|
2372
2798
|
defaultWorkdir;
|
|
2373
2799
|
constructor(stateStore, defaultWorkdir) {
|
|
2374
2800
|
this.stateStore = stateStore;
|
|
2375
|
-
this.defaultWorkdir =
|
|
2801
|
+
this.defaultWorkdir = import_node_path6.default.resolve(defaultWorkdir);
|
|
2376
2802
|
}
|
|
2377
2803
|
resolveRoomConfig(roomId, fallbackPolicy) {
|
|
2378
2804
|
const room = this.stateStore.getRoomSettings(roomId);
|
|
@@ -2443,8 +2869,8 @@ function normalizeRoomSettingsInput(input) {
|
|
|
2443
2869
|
if (!roomId) {
|
|
2444
2870
|
throw new Error("roomId is required.");
|
|
2445
2871
|
}
|
|
2446
|
-
const workdir =
|
|
2447
|
-
if (!
|
|
2872
|
+
const workdir = import_node_path6.default.resolve(input.workdir);
|
|
2873
|
+
if (!import_node_fs5.default.existsSync(workdir) || !import_node_fs5.default.statSync(workdir).isDirectory()) {
|
|
2448
2874
|
throw new Error(`workdir does not exist or is not a directory: ${workdir}`);
|
|
2449
2875
|
}
|
|
2450
2876
|
return {
|
|
@@ -2459,7 +2885,7 @@ function normalizeRoomSettingsInput(input) {
|
|
|
2459
2885
|
}
|
|
2460
2886
|
|
|
2461
2887
|
// src/executor/codex-executor.ts
|
|
2462
|
-
var
|
|
2888
|
+
var import_node_child_process3 = require("child_process");
|
|
2463
2889
|
var import_node_readline = __toESM(require("readline"));
|
|
2464
2890
|
var CodexExecutionCancelledError = class extends Error {
|
|
2465
2891
|
constructor(message = "codex execution cancelled") {
|
|
@@ -2477,7 +2903,7 @@ var CodexExecutor = class {
|
|
|
2477
2903
|
}
|
|
2478
2904
|
startExecution(prompt, sessionId, onProgress, startOptions) {
|
|
2479
2905
|
const args = buildCodexArgs(prompt, sessionId, this.options, startOptions);
|
|
2480
|
-
const child = (0,
|
|
2906
|
+
const child = (0, import_node_child_process3.spawn)(this.options.bin, args, {
|
|
2481
2907
|
cwd: startOptions?.workdir ?? this.options.workdir,
|
|
2482
2908
|
env: {
|
|
2483
2909
|
...process.env,
|
|
@@ -2713,20 +3139,20 @@ var import_async_mutex = require("async-mutex");
|
|
|
2713
3139
|
var import_promises3 = __toESM(require("fs/promises"));
|
|
2714
3140
|
|
|
2715
3141
|
// src/compat/cli-compat-recorder.ts
|
|
2716
|
-
var
|
|
2717
|
-
var
|
|
3142
|
+
var import_node_fs6 = __toESM(require("fs"));
|
|
3143
|
+
var import_node_path7 = __toESM(require("path"));
|
|
2718
3144
|
var CliCompatRecorder = class {
|
|
2719
3145
|
filePath;
|
|
2720
3146
|
chain = Promise.resolve();
|
|
2721
3147
|
constructor(filePath) {
|
|
2722
|
-
this.filePath =
|
|
2723
|
-
|
|
3148
|
+
this.filePath = import_node_path7.default.resolve(filePath);
|
|
3149
|
+
import_node_fs6.default.mkdirSync(import_node_path7.default.dirname(this.filePath), { recursive: true });
|
|
2724
3150
|
}
|
|
2725
3151
|
append(entry) {
|
|
2726
3152
|
const payload = `${JSON.stringify(entry)}
|
|
2727
3153
|
`;
|
|
2728
3154
|
this.chain = this.chain.then(async () => {
|
|
2729
|
-
await
|
|
3155
|
+
await import_node_fs6.default.promises.appendFile(this.filePath, payload, "utf8");
|
|
2730
3156
|
});
|
|
2731
3157
|
return this.chain;
|
|
2732
3158
|
}
|
|
@@ -4114,8 +4540,8 @@ ${result.review}
|
|
|
4114
4540
|
}
|
|
4115
4541
|
|
|
4116
4542
|
// src/store/state-store.ts
|
|
4117
|
-
var
|
|
4118
|
-
var
|
|
4543
|
+
var import_node_fs7 = __toESM(require("fs"));
|
|
4544
|
+
var import_node_path8 = __toESM(require("path"));
|
|
4119
4545
|
var ONE_DAY_MS = 24 * 60 * 60 * 1e3;
|
|
4120
4546
|
var PRUNE_INTERVAL_MS = 5 * 60 * 1e3;
|
|
4121
4547
|
var SQLITE_MODULE_ID = `node:${"sqlite"}`;
|
|
@@ -4141,7 +4567,7 @@ var StateStore = class {
|
|
|
4141
4567
|
this.maxProcessedEventsPerSession = maxProcessedEventsPerSession;
|
|
4142
4568
|
this.maxSessionAgeMs = maxSessionAgeDays * ONE_DAY_MS;
|
|
4143
4569
|
this.maxSessions = maxSessions;
|
|
4144
|
-
|
|
4570
|
+
import_node_fs7.default.mkdirSync(import_node_path8.default.dirname(this.dbPath), { recursive: true });
|
|
4145
4571
|
this.db = new DatabaseSync(this.dbPath);
|
|
4146
4572
|
this.initializeSchema();
|
|
4147
4573
|
this.importLegacyStateIfNeeded();
|
|
@@ -4386,7 +4812,7 @@ var StateStore = class {
|
|
|
4386
4812
|
`);
|
|
4387
4813
|
}
|
|
4388
4814
|
importLegacyStateIfNeeded() {
|
|
4389
|
-
if (!this.legacyJsonPath || !
|
|
4815
|
+
if (!this.legacyJsonPath || !import_node_fs7.default.existsSync(this.legacyJsonPath)) {
|
|
4390
4816
|
return;
|
|
4391
4817
|
}
|
|
4392
4818
|
const countRow = this.db.prepare("SELECT COUNT(*) AS count FROM sessions").get();
|
|
@@ -4416,1296 +4842,1004 @@ var StateStore = class {
|
|
|
4416
4842
|
}
|
|
4417
4843
|
}
|
|
4418
4844
|
this.db.exec("COMMIT");
|
|
4419
|
-
} catch (error) {
|
|
4420
|
-
this.db.exec("ROLLBACK");
|
|
4421
|
-
throw error;
|
|
4422
|
-
}
|
|
4423
|
-
}
|
|
4424
|
-
maybePruneExpiredSessions() {
|
|
4425
|
-
const now = Date.now();
|
|
4426
|
-
if (now - this.lastPruneAt < PRUNE_INTERVAL_MS) {
|
|
4427
|
-
return;
|
|
4428
|
-
}
|
|
4429
|
-
this.lastPruneAt = now;
|
|
4430
|
-
if (this.pruneSessions(now)) {
|
|
4431
|
-
this.touchDatabase();
|
|
4432
|
-
}
|
|
4433
|
-
}
|
|
4434
|
-
pruneSessions(now = Date.now()) {
|
|
4435
|
-
let changed = false;
|
|
4436
|
-
if (this.pruneExpiredSessions(now)) {
|
|
4437
|
-
changed = true;
|
|
4438
|
-
}
|
|
4439
|
-
if (this.pruneExcessSessions()) {
|
|
4440
|
-
changed = true;
|
|
4441
|
-
}
|
|
4442
|
-
return changed;
|
|
4443
|
-
}
|
|
4444
|
-
pruneExpiredSessions(now) {
|
|
4445
|
-
if (this.maxSessionAgeMs <= 0) {
|
|
4446
|
-
return false;
|
|
4447
|
-
}
|
|
4448
|
-
const result = this.db.prepare("DELETE FROM sessions WHERE updated_at < ?1").run(now - this.maxSessionAgeMs);
|
|
4449
|
-
return (result.changes ?? 0) > 0;
|
|
4450
|
-
}
|
|
4451
|
-
pruneExcessSessions() {
|
|
4452
|
-
if (this.maxSessions <= 0) {
|
|
4453
|
-
return false;
|
|
4454
|
-
}
|
|
4455
|
-
const row = this.db.prepare("SELECT COUNT(*) AS count FROM sessions").get();
|
|
4456
|
-
const count = row?.count ?? 0;
|
|
4457
|
-
if (count <= this.maxSessions) {
|
|
4458
|
-
return false;
|
|
4459
|
-
}
|
|
4460
|
-
const removeCount = count - this.maxSessions;
|
|
4461
|
-
const result = this.db.prepare(
|
|
4462
|
-
"DELETE FROM sessions WHERE session_key IN (SELECT session_key FROM sessions ORDER BY updated_at ASC LIMIT ?1)"
|
|
4463
|
-
).run(removeCount);
|
|
4464
|
-
return (result.changes ?? 0) > 0;
|
|
4465
|
-
}
|
|
4466
|
-
touchDatabase() {
|
|
4467
|
-
this.db.exec("PRAGMA wal_checkpoint(PASSIVE)");
|
|
4468
|
-
}
|
|
4469
|
-
};
|
|
4470
|
-
function loadLegacyState(filePath) {
|
|
4471
|
-
try {
|
|
4472
|
-
const raw = import_node_fs5.default.readFileSync(filePath, "utf8");
|
|
4473
|
-
const parsed = JSON.parse(raw);
|
|
4474
|
-
if (!parsed.sessions || typeof parsed.sessions !== "object") {
|
|
4475
|
-
return null;
|
|
4476
|
-
}
|
|
4477
|
-
normalizeLegacyState(parsed);
|
|
4478
|
-
return parsed;
|
|
4479
|
-
} catch {
|
|
4480
|
-
return null;
|
|
4481
|
-
}
|
|
4482
|
-
}
|
|
4483
|
-
function parseUpdatedAt(updatedAt) {
|
|
4484
|
-
const timestamp = Date.parse(updatedAt);
|
|
4485
|
-
return Number.isFinite(timestamp) ? timestamp : Date.now();
|
|
4486
|
-
}
|
|
4487
|
-
function parseOptionalTimestamp(value) {
|
|
4488
|
-
if (!value) {
|
|
4489
|
-
return null;
|
|
4490
|
-
}
|
|
4491
|
-
const ts = Date.parse(value);
|
|
4492
|
-
return Number.isFinite(ts) ? ts : null;
|
|
4493
|
-
}
|
|
4494
|
-
function normalizeLegacyState(state) {
|
|
4495
|
-
for (const session of Object.values(state.sessions)) {
|
|
4496
|
-
if (!Array.isArray(session.processedEventIds)) {
|
|
4497
|
-
session.processedEventIds = [];
|
|
4498
|
-
}
|
|
4499
|
-
if (typeof session.codexSessionId !== "string" && session.codexSessionId !== null) {
|
|
4500
|
-
session.codexSessionId = null;
|
|
4501
|
-
}
|
|
4502
|
-
if (typeof session.updatedAt !== "string") {
|
|
4503
|
-
session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4504
|
-
}
|
|
4505
|
-
if (typeof session.activeUntil !== "string" && session.activeUntil !== null) {
|
|
4506
|
-
session.activeUntil = null;
|
|
4507
|
-
}
|
|
4508
|
-
}
|
|
4509
|
-
}
|
|
4510
|
-
function boolToInt(value) {
|
|
4511
|
-
return value ? 1 : 0;
|
|
4512
|
-
}
|
|
4513
|
-
|
|
4514
|
-
// src/app.ts
|
|
4515
|
-
var execFileAsync2 = (0, import_node_util2.promisify)(import_node_child_process3.execFile);
|
|
4516
|
-
var CodeHarborApp = class {
|
|
4517
|
-
config;
|
|
4518
|
-
logger;
|
|
4519
|
-
stateStore;
|
|
4520
|
-
channel;
|
|
4521
|
-
orchestrator;
|
|
4522
|
-
configService;
|
|
4523
|
-
constructor(config) {
|
|
4524
|
-
this.config = config;
|
|
4525
|
-
this.logger = new Logger(config.logLevel);
|
|
4526
|
-
this.stateStore = new StateStore(
|
|
4527
|
-
config.stateDbPath,
|
|
4528
|
-
config.legacyStateJsonPath,
|
|
4529
|
-
config.maxProcessedEventsPerSession,
|
|
4530
|
-
config.maxSessionAgeDays,
|
|
4531
|
-
config.maxSessions
|
|
4532
|
-
);
|
|
4533
|
-
this.configService = new ConfigService(this.stateStore, config.codexWorkdir);
|
|
4534
|
-
const executor = new CodexExecutor({
|
|
4535
|
-
bin: config.codexBin,
|
|
4536
|
-
model: config.codexModel,
|
|
4537
|
-
workdir: config.codexWorkdir,
|
|
4538
|
-
dangerousBypass: config.codexDangerousBypass,
|
|
4539
|
-
timeoutMs: config.codexExecTimeoutMs,
|
|
4540
|
-
sandboxMode: config.codexSandboxMode,
|
|
4541
|
-
approvalPolicy: config.codexApprovalPolicy,
|
|
4542
|
-
extraArgs: config.codexExtraArgs,
|
|
4543
|
-
extraEnv: config.codexExtraEnv
|
|
4544
|
-
});
|
|
4545
|
-
this.channel = new MatrixChannel(config, this.logger);
|
|
4546
|
-
this.orchestrator = new Orchestrator(this.channel, executor, this.stateStore, this.logger, {
|
|
4547
|
-
progressUpdatesEnabled: config.matrixProgressUpdates,
|
|
4548
|
-
progressMinIntervalMs: config.matrixProgressMinIntervalMs,
|
|
4549
|
-
typingTimeoutMs: config.matrixTypingTimeoutMs,
|
|
4550
|
-
commandPrefix: config.matrixCommandPrefix,
|
|
4551
|
-
matrixUserId: config.matrixUserId,
|
|
4552
|
-
sessionActiveWindowMinutes: config.sessionActiveWindowMinutes,
|
|
4553
|
-
defaultGroupTriggerPolicy: config.defaultGroupTriggerPolicy,
|
|
4554
|
-
roomTriggerPolicies: config.roomTriggerPolicies,
|
|
4555
|
-
rateLimiterOptions: config.rateLimiter,
|
|
4556
|
-
cliCompat: config.cliCompat,
|
|
4557
|
-
multiAgentWorkflow: config.agentWorkflow,
|
|
4558
|
-
configService: this.configService,
|
|
4559
|
-
defaultCodexWorkdir: config.codexWorkdir
|
|
4560
|
-
});
|
|
4561
|
-
}
|
|
4562
|
-
async start() {
|
|
4563
|
-
this.logger.info("CodeHarbor starting", {
|
|
4564
|
-
matrixHomeserver: this.config.matrixHomeserver,
|
|
4565
|
-
workdir: this.config.codexWorkdir,
|
|
4566
|
-
prefix: this.config.matrixCommandPrefix || "<none>"
|
|
4567
|
-
});
|
|
4568
|
-
await this.channel.start(this.orchestrator.handleMessage.bind(this.orchestrator));
|
|
4569
|
-
this.logger.info("CodeHarbor is running.");
|
|
4570
|
-
}
|
|
4571
|
-
async stop() {
|
|
4572
|
-
this.logger.info("CodeHarbor stopping.");
|
|
4573
|
-
try {
|
|
4574
|
-
await this.channel.stop();
|
|
4575
|
-
} finally {
|
|
4576
|
-
await this.stateStore.flush();
|
|
4577
|
-
}
|
|
4578
|
-
}
|
|
4579
|
-
};
|
|
4580
|
-
var CodeHarborAdminApp = class {
|
|
4581
|
-
config;
|
|
4582
|
-
logger;
|
|
4583
|
-
stateStore;
|
|
4584
|
-
configService;
|
|
4585
|
-
adminServer;
|
|
4586
|
-
constructor(config, options) {
|
|
4587
|
-
this.config = config;
|
|
4588
|
-
this.logger = new Logger(config.logLevel);
|
|
4589
|
-
this.stateStore = new StateStore(
|
|
4590
|
-
config.stateDbPath,
|
|
4591
|
-
config.legacyStateJsonPath,
|
|
4592
|
-
config.maxProcessedEventsPerSession,
|
|
4593
|
-
config.maxSessionAgeDays,
|
|
4594
|
-
config.maxSessions
|
|
4595
|
-
);
|
|
4596
|
-
this.configService = new ConfigService(this.stateStore, config.codexWorkdir);
|
|
4597
|
-
this.adminServer = new AdminServer(config, this.logger, this.stateStore, this.configService, {
|
|
4598
|
-
host: options?.host ?? config.adminBindHost,
|
|
4599
|
-
port: options?.port ?? config.adminPort,
|
|
4600
|
-
adminToken: config.adminToken,
|
|
4601
|
-
adminIpAllowlist: config.adminIpAllowlist,
|
|
4602
|
-
adminAllowedOrigins: config.adminAllowedOrigins
|
|
4603
|
-
});
|
|
4604
|
-
}
|
|
4605
|
-
async start() {
|
|
4606
|
-
await this.adminServer.start();
|
|
4607
|
-
const address = this.adminServer.getAddress();
|
|
4608
|
-
this.logger.info("CodeHarbor admin server started", {
|
|
4609
|
-
host: address?.host ?? this.config.adminBindHost,
|
|
4610
|
-
port: address?.port ?? this.config.adminPort,
|
|
4611
|
-
tokenProtected: Boolean(this.config.adminToken)
|
|
4612
|
-
});
|
|
4613
|
-
}
|
|
4614
|
-
async stop() {
|
|
4615
|
-
this.logger.info("CodeHarbor admin server stopping.");
|
|
4616
|
-
try {
|
|
4617
|
-
await this.adminServer.stop();
|
|
4618
|
-
} finally {
|
|
4619
|
-
await this.stateStore.flush();
|
|
4620
|
-
}
|
|
4621
|
-
}
|
|
4622
|
-
};
|
|
4623
|
-
async function runDoctor(config) {
|
|
4624
|
-
const logger = new Logger(config.logLevel);
|
|
4625
|
-
logger.info("Doctor check started");
|
|
4626
|
-
try {
|
|
4627
|
-
const { stdout } = await execFileAsync2(config.codexBin, ["--version"]);
|
|
4628
|
-
logger.info("codex available", { version: stdout.trim() });
|
|
4629
|
-
} catch (error) {
|
|
4630
|
-
logger.error("codex unavailable", error);
|
|
4631
|
-
throw error;
|
|
4632
|
-
}
|
|
4633
|
-
try {
|
|
4634
|
-
const controller = new AbortController();
|
|
4635
|
-
const timer = setTimeout(() => controller.abort(), config.doctorHttpTimeoutMs);
|
|
4636
|
-
timer.unref?.();
|
|
4637
|
-
const response = await fetch(`${config.matrixHomeserver}/_matrix/client/versions`, {
|
|
4638
|
-
signal: controller.signal
|
|
4639
|
-
}).finally(() => {
|
|
4640
|
-
clearTimeout(timer);
|
|
4641
|
-
});
|
|
4642
|
-
if (!response.ok) {
|
|
4643
|
-
throw new Error(`HTTP ${response.status}`);
|
|
4644
|
-
}
|
|
4645
|
-
const body = await response.json();
|
|
4646
|
-
logger.info("matrix reachable", { versions: body.versions ?? [] });
|
|
4647
|
-
} catch (error) {
|
|
4648
|
-
logger.error("matrix unreachable", error);
|
|
4649
|
-
throw error;
|
|
4650
|
-
}
|
|
4651
|
-
logger.info("Doctor check passed");
|
|
4652
|
-
}
|
|
4653
|
-
|
|
4654
|
-
// src/utils/admin-host.ts
|
|
4655
|
-
function isNonLoopbackHost(host) {
|
|
4656
|
-
const normalized = host.trim().toLowerCase();
|
|
4657
|
-
if (!normalized) {
|
|
4658
|
-
return false;
|
|
4659
|
-
}
|
|
4660
|
-
return !(normalized === "localhost" || normalized === "127.0.0.1" || normalized === "::1" || normalized === "[::1]");
|
|
4661
|
-
}
|
|
4662
|
-
|
|
4663
|
-
// src/config.ts
|
|
4664
|
-
var import_node_fs6 = __toESM(require("fs"));
|
|
4665
|
-
var import_node_path7 = __toESM(require("path"));
|
|
4666
|
-
var import_dotenv2 = __toESM(require("dotenv"));
|
|
4667
|
-
var import_zod = require("zod");
|
|
4668
|
-
var configSchema = import_zod.z.object({
|
|
4669
|
-
MATRIX_HOMESERVER: import_zod.z.string().url(),
|
|
4670
|
-
MATRIX_USER_ID: import_zod.z.string().min(1),
|
|
4671
|
-
MATRIX_ACCESS_TOKEN: import_zod.z.string().min(1),
|
|
4672
|
-
MATRIX_COMMAND_PREFIX: import_zod.z.string().default("!code"),
|
|
4673
|
-
CODEX_BIN: import_zod.z.string().default("codex"),
|
|
4674
|
-
CODEX_MODEL: import_zod.z.string().optional(),
|
|
4675
|
-
CODEX_WORKDIR: import_zod.z.string().default("."),
|
|
4676
|
-
CODEX_DANGEROUS_BYPASS: import_zod.z.string().default("false").transform((v) => v.toLowerCase() === "true"),
|
|
4677
|
-
CODEX_EXEC_TIMEOUT_MS: import_zod.z.string().default("600000").transform((v) => Number.parseInt(v, 10)).pipe(import_zod.z.number().int().positive()),
|
|
4678
|
-
CODEX_SANDBOX_MODE: import_zod.z.string().optional(),
|
|
4679
|
-
CODEX_APPROVAL_POLICY: import_zod.z.string().optional(),
|
|
4680
|
-
CODEX_EXTRA_ARGS: import_zod.z.string().default(""),
|
|
4681
|
-
CODEX_EXTRA_ENV_JSON: import_zod.z.string().default(""),
|
|
4682
|
-
AGENT_WORKFLOW_ENABLED: import_zod.z.string().default("false").transform((v) => v.toLowerCase() === "true"),
|
|
4683
|
-
AGENT_WORKFLOW_AUTO_REPAIR_MAX_ROUNDS: import_zod.z.string().default("1").transform((v) => Number.parseInt(v, 10)).pipe(import_zod.z.number().int().min(0).max(10)),
|
|
4684
|
-
STATE_DB_PATH: import_zod.z.string().default("data/state.db"),
|
|
4685
|
-
STATE_PATH: import_zod.z.string().default("data/state.json"),
|
|
4686
|
-
MAX_PROCESSED_EVENTS_PER_SESSION: import_zod.z.string().default("200").transform((v) => Number.parseInt(v, 10)).pipe(import_zod.z.number().int().positive()),
|
|
4687
|
-
MAX_SESSION_AGE_DAYS: import_zod.z.string().default("30").transform((v) => Number.parseInt(v, 10)).pipe(import_zod.z.number().int().positive()),
|
|
4688
|
-
MAX_SESSIONS: import_zod.z.string().default("5000").transform((v) => Number.parseInt(v, 10)).pipe(import_zod.z.number().int().positive()),
|
|
4689
|
-
REPLY_CHUNK_SIZE: import_zod.z.string().default("3500").transform((v) => Number.parseInt(v, 10)).pipe(import_zod.z.number().int().positive()),
|
|
4690
|
-
MATRIX_PROGRESS_UPDATES: import_zod.z.string().default("true").transform((v) => v.toLowerCase() === "true"),
|
|
4691
|
-
MATRIX_PROGRESS_MIN_INTERVAL_MS: import_zod.z.string().default("2500").transform((v) => Number.parseInt(v, 10)).pipe(import_zod.z.number().int().positive()),
|
|
4692
|
-
MATRIX_TYPING_TIMEOUT_MS: import_zod.z.string().default("10000").transform((v) => Number.parseInt(v, 10)).pipe(import_zod.z.number().int().positive()),
|
|
4693
|
-
SESSION_ACTIVE_WINDOW_MINUTES: import_zod.z.string().default("20").transform((v) => Number.parseInt(v, 10)).pipe(import_zod.z.number().int().positive()),
|
|
4694
|
-
GROUP_TRIGGER_ALLOW_MENTION: import_zod.z.string().default("true").transform((v) => v.toLowerCase() === "true"),
|
|
4695
|
-
GROUP_TRIGGER_ALLOW_REPLY: import_zod.z.string().default("true").transform((v) => v.toLowerCase() === "true"),
|
|
4696
|
-
GROUP_TRIGGER_ALLOW_ACTIVE_WINDOW: import_zod.z.string().default("true").transform((v) => v.toLowerCase() === "true"),
|
|
4697
|
-
GROUP_TRIGGER_ALLOW_PREFIX: import_zod.z.string().default("true").transform((v) => v.toLowerCase() === "true"),
|
|
4698
|
-
ROOM_TRIGGER_POLICY_JSON: import_zod.z.string().default(""),
|
|
4699
|
-
RATE_LIMIT_WINDOW_SECONDS: import_zod.z.string().default("60").transform((v) => Number.parseInt(v, 10)).pipe(import_zod.z.number().int().positive()),
|
|
4700
|
-
RATE_LIMIT_MAX_REQUESTS_PER_USER: import_zod.z.string().default("20").transform((v) => Number.parseInt(v, 10)).pipe(import_zod.z.number().int().nonnegative()),
|
|
4701
|
-
RATE_LIMIT_MAX_REQUESTS_PER_ROOM: import_zod.z.string().default("120").transform((v) => Number.parseInt(v, 10)).pipe(import_zod.z.number().int().nonnegative()),
|
|
4702
|
-
RATE_LIMIT_MAX_CONCURRENT_GLOBAL: import_zod.z.string().default("8").transform((v) => Number.parseInt(v, 10)).pipe(import_zod.z.number().int().nonnegative()),
|
|
4703
|
-
RATE_LIMIT_MAX_CONCURRENT_PER_USER: import_zod.z.string().default("1").transform((v) => Number.parseInt(v, 10)).pipe(import_zod.z.number().int().nonnegative()),
|
|
4704
|
-
RATE_LIMIT_MAX_CONCURRENT_PER_ROOM: import_zod.z.string().default("4").transform((v) => Number.parseInt(v, 10)).pipe(import_zod.z.number().int().nonnegative()),
|
|
4705
|
-
CLI_COMPAT_MODE: import_zod.z.string().default("false").transform((v) => v.toLowerCase() === "true"),
|
|
4706
|
-
CLI_COMPAT_PASSTHROUGH_EVENTS: import_zod.z.string().default("true").transform((v) => v.toLowerCase() === "true"),
|
|
4707
|
-
CLI_COMPAT_PRESERVE_WHITESPACE: import_zod.z.string().default("true").transform((v) => v.toLowerCase() === "true"),
|
|
4708
|
-
CLI_COMPAT_DISABLE_REPLY_CHUNK_SPLIT: import_zod.z.string().default("false").transform((v) => v.toLowerCase() === "true"),
|
|
4709
|
-
CLI_COMPAT_PROGRESS_THROTTLE_MS: import_zod.z.string().default("300").transform((v) => Number.parseInt(v, 10)).pipe(import_zod.z.number().int().nonnegative()),
|
|
4710
|
-
CLI_COMPAT_FETCH_MEDIA: import_zod.z.string().default("true").transform((v) => v.toLowerCase() === "true"),
|
|
4711
|
-
CLI_COMPAT_RECORD_PATH: import_zod.z.string().default(""),
|
|
4712
|
-
DOCTOR_HTTP_TIMEOUT_MS: import_zod.z.string().default("10000").transform((v) => Number.parseInt(v, 10)).pipe(import_zod.z.number().int().positive()),
|
|
4713
|
-
ADMIN_BIND_HOST: import_zod.z.string().default("127.0.0.1"),
|
|
4714
|
-
ADMIN_PORT: import_zod.z.string().default("8787").transform((v) => Number.parseInt(v, 10)).pipe(import_zod.z.number().int().min(1).max(65535)),
|
|
4715
|
-
ADMIN_TOKEN: import_zod.z.string().default(""),
|
|
4716
|
-
ADMIN_IP_ALLOWLIST: import_zod.z.string().default(""),
|
|
4717
|
-
ADMIN_ALLOWED_ORIGINS: import_zod.z.string().default(""),
|
|
4718
|
-
LOG_LEVEL: import_zod.z.enum(["debug", "info", "warn", "error"]).default("info")
|
|
4719
|
-
}).transform((v) => ({
|
|
4720
|
-
matrixHomeserver: v.MATRIX_HOMESERVER,
|
|
4721
|
-
matrixUserId: v.MATRIX_USER_ID,
|
|
4722
|
-
matrixAccessToken: v.MATRIX_ACCESS_TOKEN,
|
|
4723
|
-
matrixCommandPrefix: v.MATRIX_COMMAND_PREFIX,
|
|
4724
|
-
codexBin: v.CODEX_BIN,
|
|
4725
|
-
codexModel: v.CODEX_MODEL?.trim() || null,
|
|
4726
|
-
codexWorkdir: import_node_path7.default.resolve(v.CODEX_WORKDIR),
|
|
4727
|
-
codexDangerousBypass: v.CODEX_DANGEROUS_BYPASS,
|
|
4728
|
-
codexExecTimeoutMs: v.CODEX_EXEC_TIMEOUT_MS,
|
|
4729
|
-
codexSandboxMode: v.CODEX_SANDBOX_MODE?.trim() || null,
|
|
4730
|
-
codexApprovalPolicy: v.CODEX_APPROVAL_POLICY?.trim() || null,
|
|
4731
|
-
codexExtraArgs: parseExtraArgs(v.CODEX_EXTRA_ARGS),
|
|
4732
|
-
codexExtraEnv: parseExtraEnv(v.CODEX_EXTRA_ENV_JSON),
|
|
4733
|
-
agentWorkflow: {
|
|
4734
|
-
enabled: v.AGENT_WORKFLOW_ENABLED,
|
|
4735
|
-
autoRepairMaxRounds: v.AGENT_WORKFLOW_AUTO_REPAIR_MAX_ROUNDS
|
|
4736
|
-
},
|
|
4737
|
-
stateDbPath: import_node_path7.default.resolve(v.STATE_DB_PATH),
|
|
4738
|
-
legacyStateJsonPath: v.STATE_PATH.trim() ? import_node_path7.default.resolve(v.STATE_PATH) : null,
|
|
4739
|
-
maxProcessedEventsPerSession: v.MAX_PROCESSED_EVENTS_PER_SESSION,
|
|
4740
|
-
maxSessionAgeDays: v.MAX_SESSION_AGE_DAYS,
|
|
4741
|
-
maxSessions: v.MAX_SESSIONS,
|
|
4742
|
-
replyChunkSize: v.REPLY_CHUNK_SIZE,
|
|
4743
|
-
matrixProgressUpdates: v.MATRIX_PROGRESS_UPDATES,
|
|
4744
|
-
matrixProgressMinIntervalMs: v.MATRIX_PROGRESS_MIN_INTERVAL_MS,
|
|
4745
|
-
matrixTypingTimeoutMs: v.MATRIX_TYPING_TIMEOUT_MS,
|
|
4746
|
-
sessionActiveWindowMinutes: v.SESSION_ACTIVE_WINDOW_MINUTES,
|
|
4747
|
-
defaultGroupTriggerPolicy: {
|
|
4748
|
-
allowMention: v.GROUP_TRIGGER_ALLOW_MENTION,
|
|
4749
|
-
allowReply: v.GROUP_TRIGGER_ALLOW_REPLY,
|
|
4750
|
-
allowActiveWindow: v.GROUP_TRIGGER_ALLOW_ACTIVE_WINDOW,
|
|
4751
|
-
allowPrefix: v.GROUP_TRIGGER_ALLOW_PREFIX
|
|
4752
|
-
},
|
|
4753
|
-
roomTriggerPolicies: parseRoomTriggerPolicyOverrides(v.ROOM_TRIGGER_POLICY_JSON),
|
|
4754
|
-
rateLimiter: {
|
|
4755
|
-
windowMs: v.RATE_LIMIT_WINDOW_SECONDS * 1e3,
|
|
4756
|
-
maxRequestsPerUser: v.RATE_LIMIT_MAX_REQUESTS_PER_USER,
|
|
4757
|
-
maxRequestsPerRoom: v.RATE_LIMIT_MAX_REQUESTS_PER_ROOM,
|
|
4758
|
-
maxConcurrentGlobal: v.RATE_LIMIT_MAX_CONCURRENT_GLOBAL,
|
|
4759
|
-
maxConcurrentPerUser: v.RATE_LIMIT_MAX_CONCURRENT_PER_USER,
|
|
4760
|
-
maxConcurrentPerRoom: v.RATE_LIMIT_MAX_CONCURRENT_PER_ROOM
|
|
4761
|
-
},
|
|
4762
|
-
cliCompat: {
|
|
4763
|
-
enabled: v.CLI_COMPAT_MODE,
|
|
4764
|
-
passThroughEvents: v.CLI_COMPAT_PASSTHROUGH_EVENTS,
|
|
4765
|
-
preserveWhitespace: v.CLI_COMPAT_PRESERVE_WHITESPACE,
|
|
4766
|
-
disableReplyChunkSplit: v.CLI_COMPAT_DISABLE_REPLY_CHUNK_SPLIT,
|
|
4767
|
-
progressThrottleMs: v.CLI_COMPAT_PROGRESS_THROTTLE_MS,
|
|
4768
|
-
fetchMedia: v.CLI_COMPAT_FETCH_MEDIA,
|
|
4769
|
-
recordPath: v.CLI_COMPAT_RECORD_PATH.trim() ? import_node_path7.default.resolve(v.CLI_COMPAT_RECORD_PATH) : null
|
|
4770
|
-
},
|
|
4771
|
-
doctorHttpTimeoutMs: v.DOCTOR_HTTP_TIMEOUT_MS,
|
|
4772
|
-
adminBindHost: v.ADMIN_BIND_HOST.trim() || "127.0.0.1",
|
|
4773
|
-
adminPort: v.ADMIN_PORT,
|
|
4774
|
-
adminToken: v.ADMIN_TOKEN.trim() || null,
|
|
4775
|
-
adminIpAllowlist: parseCsvList(v.ADMIN_IP_ALLOWLIST),
|
|
4776
|
-
adminAllowedOrigins: parseCsvList(v.ADMIN_ALLOWED_ORIGINS),
|
|
4777
|
-
logLevel: v.LOG_LEVEL
|
|
4778
|
-
}));
|
|
4779
|
-
function loadEnvFromFile(filePath = import_node_path7.default.resolve(process.cwd(), ".env"), env = process.env) {
|
|
4780
|
-
import_dotenv2.default.config({
|
|
4781
|
-
path: filePath,
|
|
4782
|
-
processEnv: env,
|
|
4783
|
-
quiet: true
|
|
4784
|
-
});
|
|
4785
|
-
}
|
|
4786
|
-
function loadConfig(env = process.env) {
|
|
4787
|
-
const parsed = configSchema.safeParse(env);
|
|
4788
|
-
if (!parsed.success) {
|
|
4789
|
-
const message = parsed.error.issues.map((issue) => `${issue.path.join(".") || "config"}: ${issue.message}`).join("; ");
|
|
4790
|
-
throw new Error(`Invalid configuration: ${message}`);
|
|
4791
|
-
}
|
|
4792
|
-
import_node_fs6.default.mkdirSync(import_node_path7.default.dirname(parsed.data.stateDbPath), { recursive: true });
|
|
4793
|
-
if (parsed.data.legacyStateJsonPath) {
|
|
4794
|
-
import_node_fs6.default.mkdirSync(import_node_path7.default.dirname(parsed.data.legacyStateJsonPath), { recursive: true });
|
|
4795
|
-
}
|
|
4796
|
-
return parsed.data;
|
|
4797
|
-
}
|
|
4798
|
-
function parseRoomTriggerPolicyOverrides(raw) {
|
|
4799
|
-
const trimmed = raw.trim();
|
|
4800
|
-
if (!trimmed) {
|
|
4801
|
-
return {};
|
|
4802
|
-
}
|
|
4803
|
-
let parsed;
|
|
4804
|
-
try {
|
|
4805
|
-
parsed = JSON.parse(trimmed);
|
|
4806
|
-
} catch {
|
|
4807
|
-
throw new Error("ROOM_TRIGGER_POLICY_JSON must be valid JSON.");
|
|
4808
|
-
}
|
|
4809
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
4810
|
-
throw new Error("ROOM_TRIGGER_POLICY_JSON must be an object keyed by room id.");
|
|
4811
|
-
}
|
|
4812
|
-
const output = {};
|
|
4813
|
-
for (const [roomId, value] of Object.entries(parsed)) {
|
|
4814
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
4815
|
-
throw new Error(`ROOM_TRIGGER_POLICY_JSON[${roomId}] must be an object.`);
|
|
4816
|
-
}
|
|
4817
|
-
const item = value;
|
|
4818
|
-
const override = {};
|
|
4819
|
-
for (const key of ["allowMention", "allowReply", "allowActiveWindow", "allowPrefix"]) {
|
|
4820
|
-
if (item[key] === void 0) {
|
|
4821
|
-
continue;
|
|
4822
|
-
}
|
|
4823
|
-
if (typeof item[key] !== "boolean") {
|
|
4824
|
-
throw new Error(`ROOM_TRIGGER_POLICY_JSON[${roomId}].${key} must be boolean.`);
|
|
4825
|
-
}
|
|
4826
|
-
override[key] = item[key];
|
|
4827
|
-
}
|
|
4828
|
-
output[roomId] = override;
|
|
4829
|
-
}
|
|
4830
|
-
return output;
|
|
4831
|
-
}
|
|
4832
|
-
function parseExtraArgs(raw) {
|
|
4833
|
-
const trimmed = raw.trim();
|
|
4834
|
-
if (!trimmed) {
|
|
4835
|
-
return [];
|
|
4836
|
-
}
|
|
4837
|
-
return trimmed.split(/\s+/).map((entry) => entry.trim()).filter((entry) => entry.length > 0);
|
|
4838
|
-
}
|
|
4839
|
-
function parseExtraEnv(raw) {
|
|
4840
|
-
const trimmed = raw.trim();
|
|
4841
|
-
if (!trimmed) {
|
|
4842
|
-
return {};
|
|
4843
|
-
}
|
|
4844
|
-
let parsed;
|
|
4845
|
-
try {
|
|
4846
|
-
parsed = JSON.parse(trimmed);
|
|
4847
|
-
} catch {
|
|
4848
|
-
throw new Error("CODEX_EXTRA_ENV_JSON must be valid JSON object.");
|
|
4849
|
-
}
|
|
4850
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
4851
|
-
throw new Error("CODEX_EXTRA_ENV_JSON must be a key/value object.");
|
|
4852
|
-
}
|
|
4853
|
-
const output = {};
|
|
4854
|
-
for (const [key, value] of Object.entries(parsed)) {
|
|
4855
|
-
if (typeof value !== "string") {
|
|
4856
|
-
throw new Error(`CODEX_EXTRA_ENV_JSON[${key}] must be string.`);
|
|
4857
|
-
}
|
|
4858
|
-
output[key] = value;
|
|
4859
|
-
}
|
|
4860
|
-
return output;
|
|
4861
|
-
}
|
|
4862
|
-
function parseCsvList(raw) {
|
|
4863
|
-
return raw.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
|
|
4864
|
-
}
|
|
4865
|
-
|
|
4866
|
-
// src/config-snapshot.ts
|
|
4867
|
-
var import_node_fs7 = __toESM(require("fs"));
|
|
4868
|
-
var import_node_path8 = __toESM(require("path"));
|
|
4869
|
-
var import_zod2 = require("zod");
|
|
4870
|
-
var CONFIG_SNAPSHOT_SCHEMA_VERSION = 1;
|
|
4871
|
-
var CONFIG_SNAPSHOT_ENV_KEYS = [
|
|
4872
|
-
"MATRIX_HOMESERVER",
|
|
4873
|
-
"MATRIX_USER_ID",
|
|
4874
|
-
"MATRIX_ACCESS_TOKEN",
|
|
4875
|
-
"MATRIX_COMMAND_PREFIX",
|
|
4876
|
-
"CODEX_BIN",
|
|
4877
|
-
"CODEX_MODEL",
|
|
4878
|
-
"CODEX_WORKDIR",
|
|
4879
|
-
"CODEX_DANGEROUS_BYPASS",
|
|
4880
|
-
"CODEX_EXEC_TIMEOUT_MS",
|
|
4881
|
-
"CODEX_SANDBOX_MODE",
|
|
4882
|
-
"CODEX_APPROVAL_POLICY",
|
|
4883
|
-
"CODEX_EXTRA_ARGS",
|
|
4884
|
-
"CODEX_EXTRA_ENV_JSON",
|
|
4885
|
-
"AGENT_WORKFLOW_ENABLED",
|
|
4886
|
-
"AGENT_WORKFLOW_AUTO_REPAIR_MAX_ROUNDS",
|
|
4887
|
-
"STATE_DB_PATH",
|
|
4888
|
-
"STATE_PATH",
|
|
4889
|
-
"MAX_PROCESSED_EVENTS_PER_SESSION",
|
|
4890
|
-
"MAX_SESSION_AGE_DAYS",
|
|
4891
|
-
"MAX_SESSIONS",
|
|
4892
|
-
"REPLY_CHUNK_SIZE",
|
|
4893
|
-
"MATRIX_PROGRESS_UPDATES",
|
|
4894
|
-
"MATRIX_PROGRESS_MIN_INTERVAL_MS",
|
|
4895
|
-
"MATRIX_TYPING_TIMEOUT_MS",
|
|
4896
|
-
"SESSION_ACTIVE_WINDOW_MINUTES",
|
|
4897
|
-
"GROUP_TRIGGER_ALLOW_MENTION",
|
|
4898
|
-
"GROUP_TRIGGER_ALLOW_REPLY",
|
|
4899
|
-
"GROUP_TRIGGER_ALLOW_ACTIVE_WINDOW",
|
|
4900
|
-
"GROUP_TRIGGER_ALLOW_PREFIX",
|
|
4901
|
-
"ROOM_TRIGGER_POLICY_JSON",
|
|
4902
|
-
"RATE_LIMIT_WINDOW_SECONDS",
|
|
4903
|
-
"RATE_LIMIT_MAX_REQUESTS_PER_USER",
|
|
4904
|
-
"RATE_LIMIT_MAX_REQUESTS_PER_ROOM",
|
|
4905
|
-
"RATE_LIMIT_MAX_CONCURRENT_GLOBAL",
|
|
4906
|
-
"RATE_LIMIT_MAX_CONCURRENT_PER_USER",
|
|
4907
|
-
"RATE_LIMIT_MAX_CONCURRENT_PER_ROOM",
|
|
4908
|
-
"CLI_COMPAT_MODE",
|
|
4909
|
-
"CLI_COMPAT_PASSTHROUGH_EVENTS",
|
|
4910
|
-
"CLI_COMPAT_PRESERVE_WHITESPACE",
|
|
4911
|
-
"CLI_COMPAT_DISABLE_REPLY_CHUNK_SPLIT",
|
|
4912
|
-
"CLI_COMPAT_PROGRESS_THROTTLE_MS",
|
|
4913
|
-
"CLI_COMPAT_FETCH_MEDIA",
|
|
4914
|
-
"CLI_COMPAT_RECORD_PATH",
|
|
4915
|
-
"DOCTOR_HTTP_TIMEOUT_MS",
|
|
4916
|
-
"ADMIN_BIND_HOST",
|
|
4917
|
-
"ADMIN_PORT",
|
|
4918
|
-
"ADMIN_TOKEN",
|
|
4919
|
-
"ADMIN_IP_ALLOWLIST",
|
|
4920
|
-
"ADMIN_ALLOWED_ORIGINS",
|
|
4921
|
-
"LOG_LEVEL"
|
|
4922
|
-
];
|
|
4923
|
-
var BOOLEAN_STRING = /^(true|false)$/i;
|
|
4924
|
-
var INTEGER_STRING = /^-?\d+$/;
|
|
4925
|
-
var LOG_LEVELS = ["debug", "info", "warn", "error"];
|
|
4926
|
-
var roomSnapshotSchema = import_zod2.z.object({
|
|
4927
|
-
roomId: import_zod2.z.string().min(1),
|
|
4928
|
-
enabled: import_zod2.z.boolean(),
|
|
4929
|
-
allowMention: import_zod2.z.boolean(),
|
|
4930
|
-
allowReply: import_zod2.z.boolean(),
|
|
4931
|
-
allowActiveWindow: import_zod2.z.boolean(),
|
|
4932
|
-
allowPrefix: import_zod2.z.boolean(),
|
|
4933
|
-
workdir: import_zod2.z.string().min(1)
|
|
4934
|
-
}).strict();
|
|
4935
|
-
var envSnapshotSchema = import_zod2.z.object({
|
|
4936
|
-
MATRIX_HOMESERVER: import_zod2.z.string().url(),
|
|
4937
|
-
MATRIX_USER_ID: import_zod2.z.string().min(1),
|
|
4938
|
-
MATRIX_ACCESS_TOKEN: import_zod2.z.string().min(1),
|
|
4939
|
-
MATRIX_COMMAND_PREFIX: import_zod2.z.string(),
|
|
4940
|
-
CODEX_BIN: import_zod2.z.string().min(1),
|
|
4941
|
-
CODEX_MODEL: import_zod2.z.string(),
|
|
4942
|
-
CODEX_WORKDIR: import_zod2.z.string().min(1),
|
|
4943
|
-
CODEX_DANGEROUS_BYPASS: booleanStringSchema("CODEX_DANGEROUS_BYPASS"),
|
|
4944
|
-
CODEX_EXEC_TIMEOUT_MS: integerStringSchema("CODEX_EXEC_TIMEOUT_MS", 1),
|
|
4945
|
-
CODEX_SANDBOX_MODE: import_zod2.z.string(),
|
|
4946
|
-
CODEX_APPROVAL_POLICY: import_zod2.z.string(),
|
|
4947
|
-
CODEX_EXTRA_ARGS: import_zod2.z.string(),
|
|
4948
|
-
CODEX_EXTRA_ENV_JSON: jsonObjectStringSchema("CODEX_EXTRA_ENV_JSON", true),
|
|
4949
|
-
AGENT_WORKFLOW_ENABLED: booleanStringSchema("AGENT_WORKFLOW_ENABLED"),
|
|
4950
|
-
AGENT_WORKFLOW_AUTO_REPAIR_MAX_ROUNDS: integerStringSchema("AGENT_WORKFLOW_AUTO_REPAIR_MAX_ROUNDS", 0, 10),
|
|
4951
|
-
STATE_DB_PATH: import_zod2.z.string().min(1),
|
|
4952
|
-
STATE_PATH: import_zod2.z.string(),
|
|
4953
|
-
MAX_PROCESSED_EVENTS_PER_SESSION: integerStringSchema("MAX_PROCESSED_EVENTS_PER_SESSION", 1),
|
|
4954
|
-
MAX_SESSION_AGE_DAYS: integerStringSchema("MAX_SESSION_AGE_DAYS", 1),
|
|
4955
|
-
MAX_SESSIONS: integerStringSchema("MAX_SESSIONS", 1),
|
|
4956
|
-
REPLY_CHUNK_SIZE: integerStringSchema("REPLY_CHUNK_SIZE", 1),
|
|
4957
|
-
MATRIX_PROGRESS_UPDATES: booleanStringSchema("MATRIX_PROGRESS_UPDATES"),
|
|
4958
|
-
MATRIX_PROGRESS_MIN_INTERVAL_MS: integerStringSchema("MATRIX_PROGRESS_MIN_INTERVAL_MS", 1),
|
|
4959
|
-
MATRIX_TYPING_TIMEOUT_MS: integerStringSchema("MATRIX_TYPING_TIMEOUT_MS", 1),
|
|
4960
|
-
SESSION_ACTIVE_WINDOW_MINUTES: integerStringSchema("SESSION_ACTIVE_WINDOW_MINUTES", 1),
|
|
4961
|
-
GROUP_TRIGGER_ALLOW_MENTION: booleanStringSchema("GROUP_TRIGGER_ALLOW_MENTION"),
|
|
4962
|
-
GROUP_TRIGGER_ALLOW_REPLY: booleanStringSchema("GROUP_TRIGGER_ALLOW_REPLY"),
|
|
4963
|
-
GROUP_TRIGGER_ALLOW_ACTIVE_WINDOW: booleanStringSchema("GROUP_TRIGGER_ALLOW_ACTIVE_WINDOW"),
|
|
4964
|
-
GROUP_TRIGGER_ALLOW_PREFIX: booleanStringSchema("GROUP_TRIGGER_ALLOW_PREFIX"),
|
|
4965
|
-
ROOM_TRIGGER_POLICY_JSON: jsonObjectStringSchema("ROOM_TRIGGER_POLICY_JSON", true),
|
|
4966
|
-
RATE_LIMIT_WINDOW_SECONDS: integerStringSchema("RATE_LIMIT_WINDOW_SECONDS", 1),
|
|
4967
|
-
RATE_LIMIT_MAX_REQUESTS_PER_USER: integerStringSchema("RATE_LIMIT_MAX_REQUESTS_PER_USER", 0),
|
|
4968
|
-
RATE_LIMIT_MAX_REQUESTS_PER_ROOM: integerStringSchema("RATE_LIMIT_MAX_REQUESTS_PER_ROOM", 0),
|
|
4969
|
-
RATE_LIMIT_MAX_CONCURRENT_GLOBAL: integerStringSchema("RATE_LIMIT_MAX_CONCURRENT_GLOBAL", 0),
|
|
4970
|
-
RATE_LIMIT_MAX_CONCURRENT_PER_USER: integerStringSchema("RATE_LIMIT_MAX_CONCURRENT_PER_USER", 0),
|
|
4971
|
-
RATE_LIMIT_MAX_CONCURRENT_PER_ROOM: integerStringSchema("RATE_LIMIT_MAX_CONCURRENT_PER_ROOM", 0),
|
|
4972
|
-
CLI_COMPAT_MODE: booleanStringSchema("CLI_COMPAT_MODE"),
|
|
4973
|
-
CLI_COMPAT_PASSTHROUGH_EVENTS: booleanStringSchema("CLI_COMPAT_PASSTHROUGH_EVENTS"),
|
|
4974
|
-
CLI_COMPAT_PRESERVE_WHITESPACE: booleanStringSchema("CLI_COMPAT_PRESERVE_WHITESPACE"),
|
|
4975
|
-
CLI_COMPAT_DISABLE_REPLY_CHUNK_SPLIT: booleanStringSchema("CLI_COMPAT_DISABLE_REPLY_CHUNK_SPLIT"),
|
|
4976
|
-
CLI_COMPAT_PROGRESS_THROTTLE_MS: integerStringSchema("CLI_COMPAT_PROGRESS_THROTTLE_MS", 0),
|
|
4977
|
-
CLI_COMPAT_FETCH_MEDIA: booleanStringSchema("CLI_COMPAT_FETCH_MEDIA"),
|
|
4978
|
-
CLI_COMPAT_RECORD_PATH: import_zod2.z.string(),
|
|
4979
|
-
DOCTOR_HTTP_TIMEOUT_MS: integerStringSchema("DOCTOR_HTTP_TIMEOUT_MS", 1),
|
|
4980
|
-
ADMIN_BIND_HOST: import_zod2.z.string(),
|
|
4981
|
-
ADMIN_PORT: integerStringSchema("ADMIN_PORT", 1, 65535),
|
|
4982
|
-
ADMIN_TOKEN: import_zod2.z.string(),
|
|
4983
|
-
ADMIN_IP_ALLOWLIST: import_zod2.z.string(),
|
|
4984
|
-
ADMIN_ALLOWED_ORIGINS: import_zod2.z.string().default(""),
|
|
4985
|
-
LOG_LEVEL: import_zod2.z.enum(LOG_LEVELS)
|
|
4986
|
-
}).strict();
|
|
4987
|
-
var configSnapshotSchema = import_zod2.z.object({
|
|
4988
|
-
schemaVersion: import_zod2.z.literal(CONFIG_SNAPSHOT_SCHEMA_VERSION),
|
|
4989
|
-
exportedAt: import_zod2.z.string().datetime({ offset: true }),
|
|
4990
|
-
env: envSnapshotSchema,
|
|
4991
|
-
rooms: import_zod2.z.array(roomSnapshotSchema)
|
|
4992
|
-
}).strict().superRefine((value, ctx) => {
|
|
4993
|
-
const seen = /* @__PURE__ */ new Set();
|
|
4994
|
-
for (const room of value.rooms) {
|
|
4995
|
-
const roomId = room.roomId.trim();
|
|
4996
|
-
if (!roomId) {
|
|
4997
|
-
ctx.addIssue({
|
|
4998
|
-
code: import_zod2.z.ZodIssueCode.custom,
|
|
4999
|
-
message: "rooms[].roomId cannot be empty."
|
|
5000
|
-
});
|
|
5001
|
-
continue;
|
|
5002
|
-
}
|
|
5003
|
-
if (seen.has(roomId)) {
|
|
5004
|
-
ctx.addIssue({
|
|
5005
|
-
code: import_zod2.z.ZodIssueCode.custom,
|
|
5006
|
-
message: `Duplicate room id in snapshot: ${roomId}`
|
|
5007
|
-
});
|
|
5008
|
-
}
|
|
5009
|
-
seen.add(roomId);
|
|
5010
|
-
}
|
|
5011
|
-
});
|
|
5012
|
-
function buildConfigSnapshot(config, roomSettings, now = /* @__PURE__ */ new Date()) {
|
|
5013
|
-
return {
|
|
5014
|
-
schemaVersion: CONFIG_SNAPSHOT_SCHEMA_VERSION,
|
|
5015
|
-
exportedAt: now.toISOString(),
|
|
5016
|
-
env: buildSnapshotEnv(config),
|
|
5017
|
-
rooms: roomSettings.map((room) => ({
|
|
5018
|
-
roomId: room.roomId,
|
|
5019
|
-
enabled: room.enabled,
|
|
5020
|
-
allowMention: room.allowMention,
|
|
5021
|
-
allowReply: room.allowReply,
|
|
5022
|
-
allowActiveWindow: room.allowActiveWindow,
|
|
5023
|
-
allowPrefix: room.allowPrefix,
|
|
5024
|
-
workdir: room.workdir
|
|
5025
|
-
}))
|
|
5026
|
-
};
|
|
5027
|
-
}
|
|
5028
|
-
function parseConfigSnapshot(raw) {
|
|
5029
|
-
const parsed = configSnapshotSchema.safeParse(raw);
|
|
5030
|
-
if (!parsed.success) {
|
|
5031
|
-
const message = parsed.error.issues.map((issue) => `${issue.path.join(".") || "snapshot"}: ${issue.message}`).join("; ");
|
|
5032
|
-
throw new Error(`Invalid config snapshot: ${message}`);
|
|
5033
|
-
}
|
|
5034
|
-
return parsed.data;
|
|
5035
|
-
}
|
|
5036
|
-
function serializeConfigSnapshot(snapshot) {
|
|
5037
|
-
return `${JSON.stringify(snapshot, null, 2)}
|
|
5038
|
-
`;
|
|
5039
|
-
}
|
|
5040
|
-
async function runConfigExportCommand(options = {}) {
|
|
5041
|
-
const cwd = options.cwd ?? process.cwd();
|
|
5042
|
-
const output = options.output ?? process.stdout;
|
|
5043
|
-
const config = loadConfig(options.env ?? process.env);
|
|
5044
|
-
const stateStore = new StateStore(
|
|
5045
|
-
config.stateDbPath,
|
|
5046
|
-
config.legacyStateJsonPath,
|
|
5047
|
-
config.maxProcessedEventsPerSession,
|
|
5048
|
-
config.maxSessionAgeDays,
|
|
5049
|
-
config.maxSessions
|
|
5050
|
-
);
|
|
5051
|
-
try {
|
|
5052
|
-
const snapshot = buildConfigSnapshot(config, stateStore.listRoomSettings(), options.now ?? /* @__PURE__ */ new Date());
|
|
5053
|
-
const serialized = serializeConfigSnapshot(snapshot);
|
|
5054
|
-
if (options.outputPath) {
|
|
5055
|
-
const targetPath = import_node_path8.default.resolve(cwd, options.outputPath);
|
|
5056
|
-
import_node_fs7.default.mkdirSync(import_node_path8.default.dirname(targetPath), { recursive: true });
|
|
5057
|
-
import_node_fs7.default.writeFileSync(targetPath, serialized, "utf8");
|
|
5058
|
-
output.write(`Exported config snapshot to ${targetPath}
|
|
5059
|
-
`);
|
|
5060
|
-
return;
|
|
5061
|
-
}
|
|
5062
|
-
output.write(serialized);
|
|
5063
|
-
} finally {
|
|
5064
|
-
await stateStore.flush();
|
|
5065
|
-
}
|
|
5066
|
-
}
|
|
5067
|
-
async function runConfigImportCommand(options) {
|
|
5068
|
-
const cwd = options.cwd ?? process.cwd();
|
|
5069
|
-
const output = options.output ?? process.stdout;
|
|
5070
|
-
const actor = options.actor?.trim() || "cli:config-import";
|
|
5071
|
-
const sourcePath = import_node_path8.default.resolve(cwd, options.filePath);
|
|
5072
|
-
if (!import_node_fs7.default.existsSync(sourcePath)) {
|
|
5073
|
-
throw new Error(`Config snapshot file not found: ${sourcePath}`);
|
|
5074
|
-
}
|
|
5075
|
-
const snapshot = parseConfigSnapshot(parseJsonFile(sourcePath));
|
|
5076
|
-
const normalizedEnv = normalizeSnapshotEnv(snapshot.env, cwd);
|
|
5077
|
-
ensureDirectory2(normalizedEnv.CODEX_WORKDIR, "CODEX_WORKDIR");
|
|
5078
|
-
const normalizedRooms = normalizeSnapshotRooms(snapshot.rooms, cwd);
|
|
5079
|
-
if (options.dryRun) {
|
|
5080
|
-
output.write(
|
|
5081
|
-
[
|
|
5082
|
-
`Config snapshot is valid: ${sourcePath}`,
|
|
5083
|
-
`- schemaVersion: ${snapshot.schemaVersion}`,
|
|
5084
|
-
`- rooms: ${normalizedRooms.length}`,
|
|
5085
|
-
"- dry-run: no changes were written"
|
|
5086
|
-
].join("\n") + "\n"
|
|
5087
|
-
);
|
|
5088
|
-
return;
|
|
5089
|
-
}
|
|
5090
|
-
persistEnvSnapshot(cwd, normalizedEnv);
|
|
5091
|
-
const stateStore = new StateStore(
|
|
5092
|
-
normalizedEnv.STATE_DB_PATH,
|
|
5093
|
-
normalizedEnv.STATE_PATH ? normalizedEnv.STATE_PATH : null,
|
|
5094
|
-
parseIntStrict(normalizedEnv.MAX_PROCESSED_EVENTS_PER_SESSION),
|
|
5095
|
-
parseIntStrict(normalizedEnv.MAX_SESSION_AGE_DAYS),
|
|
5096
|
-
parseIntStrict(normalizedEnv.MAX_SESSIONS)
|
|
5097
|
-
);
|
|
5098
|
-
try {
|
|
5099
|
-
synchronizeRoomSettings(stateStore, normalizedRooms);
|
|
5100
|
-
stateStore.appendConfigRevision(
|
|
5101
|
-
actor,
|
|
5102
|
-
`import config snapshot from ${import_node_path8.default.basename(sourcePath)}`,
|
|
5103
|
-
JSON.stringify({
|
|
5104
|
-
type: "config_snapshot_import",
|
|
5105
|
-
sourcePath,
|
|
5106
|
-
roomCount: normalizedRooms.length,
|
|
5107
|
-
envKeyCount: CONFIG_SNAPSHOT_ENV_KEYS.length
|
|
5108
|
-
})
|
|
5109
|
-
);
|
|
5110
|
-
} finally {
|
|
5111
|
-
await stateStore.flush();
|
|
5112
|
-
}
|
|
5113
|
-
output.write(
|
|
5114
|
-
[
|
|
5115
|
-
`Imported config snapshot from ${sourcePath}`,
|
|
5116
|
-
`- updated .env in ${import_node_path8.default.resolve(cwd, ".env")}`,
|
|
5117
|
-
`- synchronized room settings: ${normalizedRooms.length}`,
|
|
5118
|
-
"- restart required: yes (global env settings are restart-scoped)"
|
|
5119
|
-
].join("\n") + "\n"
|
|
5120
|
-
);
|
|
5121
|
-
}
|
|
5122
|
-
function buildSnapshotEnv(config) {
|
|
5123
|
-
return {
|
|
5124
|
-
MATRIX_HOMESERVER: config.matrixHomeserver,
|
|
5125
|
-
MATRIX_USER_ID: config.matrixUserId,
|
|
5126
|
-
MATRIX_ACCESS_TOKEN: config.matrixAccessToken,
|
|
5127
|
-
MATRIX_COMMAND_PREFIX: config.matrixCommandPrefix,
|
|
5128
|
-
CODEX_BIN: config.codexBin,
|
|
5129
|
-
CODEX_MODEL: config.codexModel ?? "",
|
|
5130
|
-
CODEX_WORKDIR: config.codexWorkdir,
|
|
5131
|
-
CODEX_DANGEROUS_BYPASS: String(config.codexDangerousBypass),
|
|
5132
|
-
CODEX_EXEC_TIMEOUT_MS: String(config.codexExecTimeoutMs),
|
|
5133
|
-
CODEX_SANDBOX_MODE: config.codexSandboxMode ?? "",
|
|
5134
|
-
CODEX_APPROVAL_POLICY: config.codexApprovalPolicy ?? "",
|
|
5135
|
-
CODEX_EXTRA_ARGS: config.codexExtraArgs.join(" "),
|
|
5136
|
-
CODEX_EXTRA_ENV_JSON: serializeJsonObject(config.codexExtraEnv),
|
|
5137
|
-
AGENT_WORKFLOW_ENABLED: String(config.agentWorkflow.enabled),
|
|
5138
|
-
AGENT_WORKFLOW_AUTO_REPAIR_MAX_ROUNDS: String(config.agentWorkflow.autoRepairMaxRounds),
|
|
5139
|
-
STATE_DB_PATH: config.stateDbPath,
|
|
5140
|
-
STATE_PATH: config.legacyStateJsonPath ?? "",
|
|
5141
|
-
MAX_PROCESSED_EVENTS_PER_SESSION: String(config.maxProcessedEventsPerSession),
|
|
5142
|
-
MAX_SESSION_AGE_DAYS: String(config.maxSessionAgeDays),
|
|
5143
|
-
MAX_SESSIONS: String(config.maxSessions),
|
|
5144
|
-
REPLY_CHUNK_SIZE: String(config.replyChunkSize),
|
|
5145
|
-
MATRIX_PROGRESS_UPDATES: String(config.matrixProgressUpdates),
|
|
5146
|
-
MATRIX_PROGRESS_MIN_INTERVAL_MS: String(config.matrixProgressMinIntervalMs),
|
|
5147
|
-
MATRIX_TYPING_TIMEOUT_MS: String(config.matrixTypingTimeoutMs),
|
|
5148
|
-
SESSION_ACTIVE_WINDOW_MINUTES: String(config.sessionActiveWindowMinutes),
|
|
5149
|
-
GROUP_TRIGGER_ALLOW_MENTION: String(config.defaultGroupTriggerPolicy.allowMention),
|
|
5150
|
-
GROUP_TRIGGER_ALLOW_REPLY: String(config.defaultGroupTriggerPolicy.allowReply),
|
|
5151
|
-
GROUP_TRIGGER_ALLOW_ACTIVE_WINDOW: String(config.defaultGroupTriggerPolicy.allowActiveWindow),
|
|
5152
|
-
GROUP_TRIGGER_ALLOW_PREFIX: String(config.defaultGroupTriggerPolicy.allowPrefix),
|
|
5153
|
-
ROOM_TRIGGER_POLICY_JSON: serializeJsonObject(config.roomTriggerPolicies),
|
|
5154
|
-
RATE_LIMIT_WINDOW_SECONDS: String(Math.max(1, Math.round(config.rateLimiter.windowMs / 1e3))),
|
|
5155
|
-
RATE_LIMIT_MAX_REQUESTS_PER_USER: String(config.rateLimiter.maxRequestsPerUser),
|
|
5156
|
-
RATE_LIMIT_MAX_REQUESTS_PER_ROOM: String(config.rateLimiter.maxRequestsPerRoom),
|
|
5157
|
-
RATE_LIMIT_MAX_CONCURRENT_GLOBAL: String(config.rateLimiter.maxConcurrentGlobal),
|
|
5158
|
-
RATE_LIMIT_MAX_CONCURRENT_PER_USER: String(config.rateLimiter.maxConcurrentPerUser),
|
|
5159
|
-
RATE_LIMIT_MAX_CONCURRENT_PER_ROOM: String(config.rateLimiter.maxConcurrentPerRoom),
|
|
5160
|
-
CLI_COMPAT_MODE: String(config.cliCompat.enabled),
|
|
5161
|
-
CLI_COMPAT_PASSTHROUGH_EVENTS: String(config.cliCompat.passThroughEvents),
|
|
5162
|
-
CLI_COMPAT_PRESERVE_WHITESPACE: String(config.cliCompat.preserveWhitespace),
|
|
5163
|
-
CLI_COMPAT_DISABLE_REPLY_CHUNK_SPLIT: String(config.cliCompat.disableReplyChunkSplit),
|
|
5164
|
-
CLI_COMPAT_PROGRESS_THROTTLE_MS: String(config.cliCompat.progressThrottleMs),
|
|
5165
|
-
CLI_COMPAT_FETCH_MEDIA: String(config.cliCompat.fetchMedia),
|
|
5166
|
-
CLI_COMPAT_RECORD_PATH: config.cliCompat.recordPath ?? "",
|
|
5167
|
-
DOCTOR_HTTP_TIMEOUT_MS: String(config.doctorHttpTimeoutMs),
|
|
5168
|
-
ADMIN_BIND_HOST: config.adminBindHost,
|
|
5169
|
-
ADMIN_PORT: String(config.adminPort),
|
|
5170
|
-
ADMIN_TOKEN: config.adminToken ?? "",
|
|
5171
|
-
ADMIN_IP_ALLOWLIST: config.adminIpAllowlist.join(","),
|
|
5172
|
-
ADMIN_ALLOWED_ORIGINS: config.adminAllowedOrigins.join(","),
|
|
5173
|
-
LOG_LEVEL: config.logLevel
|
|
5174
|
-
};
|
|
5175
|
-
}
|
|
5176
|
-
function parseJsonFile(filePath) {
|
|
5177
|
-
try {
|
|
5178
|
-
const raw = import_node_fs7.default.readFileSync(filePath, "utf8");
|
|
5179
|
-
return JSON.parse(raw);
|
|
5180
|
-
} catch (error) {
|
|
5181
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
5182
|
-
throw new Error(`Failed to parse snapshot JSON: ${message}`, {
|
|
5183
|
-
cause: error
|
|
5184
|
-
});
|
|
4845
|
+
} catch (error) {
|
|
4846
|
+
this.db.exec("ROLLBACK");
|
|
4847
|
+
throw error;
|
|
4848
|
+
}
|
|
5185
4849
|
}
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
CODEX_WORKDIR: import_node_path8.default.resolve(cwd, env.CODEX_WORKDIR),
|
|
5191
|
-
STATE_DB_PATH: import_node_path8.default.resolve(cwd, env.STATE_DB_PATH),
|
|
5192
|
-
STATE_PATH: env.STATE_PATH.trim() ? import_node_path8.default.resolve(cwd, env.STATE_PATH) : "",
|
|
5193
|
-
CLI_COMPAT_RECORD_PATH: env.CLI_COMPAT_RECORD_PATH.trim() ? import_node_path8.default.resolve(cwd, env.CLI_COMPAT_RECORD_PATH) : ""
|
|
5194
|
-
};
|
|
5195
|
-
}
|
|
5196
|
-
function normalizeSnapshotRooms(rooms, cwd) {
|
|
5197
|
-
const normalized = [];
|
|
5198
|
-
const seen = /* @__PURE__ */ new Set();
|
|
5199
|
-
for (const room of rooms) {
|
|
5200
|
-
const roomId = room.roomId.trim();
|
|
5201
|
-
if (!roomId) {
|
|
5202
|
-
throw new Error("roomId is required for every room in snapshot.");
|
|
4850
|
+
maybePruneExpiredSessions() {
|
|
4851
|
+
const now = Date.now();
|
|
4852
|
+
if (now - this.lastPruneAt < PRUNE_INTERVAL_MS) {
|
|
4853
|
+
return;
|
|
5203
4854
|
}
|
|
5204
|
-
|
|
5205
|
-
|
|
4855
|
+
this.lastPruneAt = now;
|
|
4856
|
+
if (this.pruneSessions(now)) {
|
|
4857
|
+
this.touchDatabase();
|
|
5206
4858
|
}
|
|
5207
|
-
seen.add(roomId);
|
|
5208
|
-
const workdir = import_node_path8.default.resolve(cwd, room.workdir);
|
|
5209
|
-
ensureDirectory2(workdir, `room workdir (${roomId})`);
|
|
5210
|
-
normalized.push({
|
|
5211
|
-
roomId,
|
|
5212
|
-
enabled: room.enabled,
|
|
5213
|
-
allowMention: room.allowMention,
|
|
5214
|
-
allowReply: room.allowReply,
|
|
5215
|
-
allowActiveWindow: room.allowActiveWindow,
|
|
5216
|
-
allowPrefix: room.allowPrefix,
|
|
5217
|
-
workdir
|
|
5218
|
-
});
|
|
5219
4859
|
}
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
stateStore.deleteRoomSettings(room.roomId);
|
|
4860
|
+
pruneSessions(now = Date.now()) {
|
|
4861
|
+
let changed = false;
|
|
4862
|
+
if (this.pruneExpiredSessions(now)) {
|
|
4863
|
+
changed = true;
|
|
4864
|
+
}
|
|
4865
|
+
if (this.pruneExcessSessions()) {
|
|
4866
|
+
changed = true;
|
|
5228
4867
|
}
|
|
4868
|
+
return changed;
|
|
5229
4869
|
}
|
|
5230
|
-
|
|
5231
|
-
|
|
4870
|
+
pruneExpiredSessions(now) {
|
|
4871
|
+
if (this.maxSessionAgeMs <= 0) {
|
|
4872
|
+
return false;
|
|
4873
|
+
}
|
|
4874
|
+
const result = this.db.prepare("DELETE FROM sessions WHERE updated_at < ?1").run(now - this.maxSessionAgeMs);
|
|
4875
|
+
return (result.changes ?? 0) > 0;
|
|
5232
4876
|
}
|
|
5233
|
-
|
|
5234
|
-
|
|
5235
|
-
|
|
5236
|
-
|
|
5237
|
-
|
|
5238
|
-
|
|
5239
|
-
|
|
5240
|
-
|
|
4877
|
+
pruneExcessSessions() {
|
|
4878
|
+
if (this.maxSessions <= 0) {
|
|
4879
|
+
return false;
|
|
4880
|
+
}
|
|
4881
|
+
const row = this.db.prepare("SELECT COUNT(*) AS count FROM sessions").get();
|
|
4882
|
+
const count = row?.count ?? 0;
|
|
4883
|
+
if (count <= this.maxSessions) {
|
|
4884
|
+
return false;
|
|
4885
|
+
}
|
|
4886
|
+
const removeCount = count - this.maxSessions;
|
|
4887
|
+
const result = this.db.prepare(
|
|
4888
|
+
"DELETE FROM sessions WHERE session_key IN (SELECT session_key FROM sessions ORDER BY updated_at ASC LIMIT ?1)"
|
|
4889
|
+
).run(removeCount);
|
|
4890
|
+
return (result.changes ?? 0) > 0;
|
|
5241
4891
|
}
|
|
5242
|
-
|
|
5243
|
-
|
|
5244
|
-
}
|
|
5245
|
-
function ensureDirectory2(dirPath, label) {
|
|
5246
|
-
if (!import_node_fs7.default.existsSync(dirPath) || !import_node_fs7.default.statSync(dirPath).isDirectory()) {
|
|
5247
|
-
throw new Error(`${label} does not exist or is not a directory: ${dirPath}`);
|
|
4892
|
+
touchDatabase() {
|
|
4893
|
+
this.db.exec("PRAGMA wal_checkpoint(PASSIVE)");
|
|
5248
4894
|
}
|
|
5249
|
-
}
|
|
5250
|
-
function
|
|
5251
|
-
|
|
5252
|
-
|
|
5253
|
-
|
|
4895
|
+
};
|
|
4896
|
+
function loadLegacyState(filePath) {
|
|
4897
|
+
try {
|
|
4898
|
+
const raw = import_node_fs7.default.readFileSync(filePath, "utf8");
|
|
4899
|
+
const parsed = JSON.parse(raw);
|
|
4900
|
+
if (!parsed.sessions || typeof parsed.sessions !== "object") {
|
|
4901
|
+
return null;
|
|
4902
|
+
}
|
|
4903
|
+
normalizeLegacyState(parsed);
|
|
4904
|
+
return parsed;
|
|
4905
|
+
} catch {
|
|
4906
|
+
return null;
|
|
5254
4907
|
}
|
|
5255
|
-
return value;
|
|
5256
4908
|
}
|
|
5257
|
-
function
|
|
5258
|
-
|
|
4909
|
+
function parseUpdatedAt(updatedAt) {
|
|
4910
|
+
const timestamp = Date.parse(updatedAt);
|
|
4911
|
+
return Number.isFinite(timestamp) ? timestamp : Date.now();
|
|
5259
4912
|
}
|
|
5260
|
-
function
|
|
5261
|
-
|
|
5262
|
-
|
|
5263
|
-
}
|
|
4913
|
+
function parseOptionalTimestamp(value) {
|
|
4914
|
+
if (!value) {
|
|
4915
|
+
return null;
|
|
4916
|
+
}
|
|
4917
|
+
const ts = Date.parse(value);
|
|
4918
|
+
return Number.isFinite(ts) ? ts : null;
|
|
5264
4919
|
}
|
|
5265
|
-
function
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
return false;
|
|
4920
|
+
function normalizeLegacyState(state) {
|
|
4921
|
+
for (const session of Object.values(state.sessions)) {
|
|
4922
|
+
if (!Array.isArray(session.processedEventIds)) {
|
|
4923
|
+
session.processedEventIds = [];
|
|
5270
4924
|
}
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
return false;
|
|
4925
|
+
if (typeof session.codexSessionId !== "string" && session.codexSessionId !== null) {
|
|
4926
|
+
session.codexSessionId = null;
|
|
5274
4927
|
}
|
|
5275
|
-
|
|
5276
|
-
|
|
5277
|
-
message: `${key} must be an integer string in range [${min}, ${max}].`
|
|
5278
|
-
});
|
|
5279
|
-
}
|
|
5280
|
-
function jsonObjectStringSchema(key, allowEmpty) {
|
|
5281
|
-
return import_zod2.z.string().refine((value) => {
|
|
5282
|
-
const trimmed = value.trim();
|
|
5283
|
-
if (!trimmed) {
|
|
5284
|
-
return allowEmpty;
|
|
4928
|
+
if (typeof session.updatedAt !== "string") {
|
|
4929
|
+
session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5285
4930
|
}
|
|
5286
|
-
|
|
5287
|
-
|
|
5288
|
-
parsed = JSON.parse(trimmed);
|
|
5289
|
-
} catch {
|
|
5290
|
-
return false;
|
|
4931
|
+
if (typeof session.activeUntil !== "string" && session.activeUntil !== null) {
|
|
4932
|
+
session.activeUntil = null;
|
|
5291
4933
|
}
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
|
|
4934
|
+
}
|
|
4935
|
+
}
|
|
4936
|
+
function boolToInt(value) {
|
|
4937
|
+
return value ? 1 : 0;
|
|
5296
4938
|
}
|
|
5297
4939
|
|
|
5298
|
-
// src/
|
|
5299
|
-
var
|
|
5300
|
-
var
|
|
5301
|
-
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
|
|
5309
|
-
|
|
5310
|
-
|
|
5311
|
-
|
|
5312
|
-
|
|
5313
|
-
|
|
5314
|
-
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
|
|
4940
|
+
// src/app.ts
|
|
4941
|
+
var execFileAsync2 = (0, import_node_util2.promisify)(import_node_child_process4.execFile);
|
|
4942
|
+
var CodeHarborApp = class {
|
|
4943
|
+
config;
|
|
4944
|
+
logger;
|
|
4945
|
+
stateStore;
|
|
4946
|
+
channel;
|
|
4947
|
+
orchestrator;
|
|
4948
|
+
configService;
|
|
4949
|
+
constructor(config) {
|
|
4950
|
+
this.config = config;
|
|
4951
|
+
this.logger = new Logger(config.logLevel);
|
|
4952
|
+
this.stateStore = new StateStore(
|
|
4953
|
+
config.stateDbPath,
|
|
4954
|
+
config.legacyStateJsonPath,
|
|
4955
|
+
config.maxProcessedEventsPerSession,
|
|
4956
|
+
config.maxSessionAgeDays,
|
|
4957
|
+
config.maxSessions
|
|
4958
|
+
);
|
|
4959
|
+
this.configService = new ConfigService(this.stateStore, config.codexWorkdir);
|
|
4960
|
+
const executor = new CodexExecutor({
|
|
4961
|
+
bin: config.codexBin,
|
|
4962
|
+
model: config.codexModel,
|
|
4963
|
+
workdir: config.codexWorkdir,
|
|
4964
|
+
dangerousBypass: config.codexDangerousBypass,
|
|
4965
|
+
timeoutMs: config.codexExecTimeoutMs,
|
|
4966
|
+
sandboxMode: config.codexSandboxMode,
|
|
4967
|
+
approvalPolicy: config.codexApprovalPolicy,
|
|
4968
|
+
extraArgs: config.codexExtraArgs,
|
|
4969
|
+
extraEnv: config.codexExtraEnv
|
|
4970
|
+
});
|
|
4971
|
+
this.channel = new MatrixChannel(config, this.logger);
|
|
4972
|
+
this.orchestrator = new Orchestrator(this.channel, executor, this.stateStore, this.logger, {
|
|
4973
|
+
progressUpdatesEnabled: config.matrixProgressUpdates,
|
|
4974
|
+
progressMinIntervalMs: config.matrixProgressMinIntervalMs,
|
|
4975
|
+
typingTimeoutMs: config.matrixTypingTimeoutMs,
|
|
4976
|
+
commandPrefix: config.matrixCommandPrefix,
|
|
4977
|
+
matrixUserId: config.matrixUserId,
|
|
4978
|
+
sessionActiveWindowMinutes: config.sessionActiveWindowMinutes,
|
|
4979
|
+
defaultGroupTriggerPolicy: config.defaultGroupTriggerPolicy,
|
|
4980
|
+
roomTriggerPolicies: config.roomTriggerPolicies,
|
|
4981
|
+
rateLimiterOptions: config.rateLimiter,
|
|
4982
|
+
cliCompat: config.cliCompat,
|
|
4983
|
+
multiAgentWorkflow: config.agentWorkflow,
|
|
4984
|
+
configService: this.configService,
|
|
4985
|
+
defaultCodexWorkdir: config.codexWorkdir
|
|
4986
|
+
});
|
|
4987
|
+
}
|
|
4988
|
+
async start() {
|
|
4989
|
+
this.logger.info("CodeHarbor starting", {
|
|
4990
|
+
matrixHomeserver: this.config.matrixHomeserver,
|
|
4991
|
+
workdir: this.config.codexWorkdir,
|
|
4992
|
+
prefix: this.config.matrixCommandPrefix || "<none>"
|
|
4993
|
+
});
|
|
4994
|
+
await this.channel.start(this.orchestrator.handleMessage.bind(this.orchestrator));
|
|
4995
|
+
this.logger.info("CodeHarbor is running.");
|
|
4996
|
+
}
|
|
4997
|
+
async stop() {
|
|
4998
|
+
this.logger.info("CodeHarbor stopping.");
|
|
4999
|
+
try {
|
|
5000
|
+
await this.channel.stop();
|
|
5001
|
+
} finally {
|
|
5002
|
+
await this.stateStore.flush();
|
|
5003
|
+
}
|
|
5004
|
+
}
|
|
5005
|
+
};
|
|
5006
|
+
var CodeHarborAdminApp = class {
|
|
5007
|
+
config;
|
|
5008
|
+
logger;
|
|
5009
|
+
stateStore;
|
|
5010
|
+
configService;
|
|
5011
|
+
adminServer;
|
|
5012
|
+
constructor(config, options) {
|
|
5013
|
+
this.config = config;
|
|
5014
|
+
this.logger = new Logger(config.logLevel);
|
|
5015
|
+
this.stateStore = new StateStore(
|
|
5016
|
+
config.stateDbPath,
|
|
5017
|
+
config.legacyStateJsonPath,
|
|
5018
|
+
config.maxProcessedEventsPerSession,
|
|
5019
|
+
config.maxSessionAgeDays,
|
|
5020
|
+
config.maxSessions
|
|
5021
|
+
);
|
|
5022
|
+
this.configService = new ConfigService(this.stateStore, config.codexWorkdir);
|
|
5023
|
+
this.adminServer = new AdminServer(config, this.logger, this.stateStore, this.configService, {
|
|
5024
|
+
host: options?.host ?? config.adminBindHost,
|
|
5025
|
+
port: options?.port ?? config.adminPort,
|
|
5026
|
+
adminToken: config.adminToken,
|
|
5027
|
+
adminIpAllowlist: config.adminIpAllowlist,
|
|
5028
|
+
adminAllowedOrigins: config.adminAllowedOrigins
|
|
5320
5029
|
});
|
|
5321
5030
|
}
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
|
|
5329
|
-
|
|
5330
|
-
});
|
|
5331
|
-
}
|
|
5031
|
+
async start() {
|
|
5032
|
+
await this.adminServer.start();
|
|
5033
|
+
const address = this.adminServer.getAddress();
|
|
5034
|
+
this.logger.info("CodeHarbor admin server started", {
|
|
5035
|
+
host: address?.host ?? this.config.adminBindHost,
|
|
5036
|
+
port: address?.port ?? this.config.adminPort,
|
|
5037
|
+
tokenProtected: Boolean(this.config.adminToken)
|
|
5038
|
+
});
|
|
5332
5039
|
}
|
|
5333
|
-
|
|
5334
|
-
|
|
5040
|
+
async stop() {
|
|
5041
|
+
this.logger.info("CodeHarbor admin server stopping.");
|
|
5335
5042
|
try {
|
|
5336
|
-
|
|
5337
|
-
}
|
|
5338
|
-
|
|
5339
|
-
level: "error",
|
|
5340
|
-
code: "invalid_matrix_homeserver",
|
|
5341
|
-
check: "MATRIX_HOMESERVER",
|
|
5342
|
-
message: `Invalid URL: "${matrixHomeserver}".`,
|
|
5343
|
-
fix: "Set MATRIX_HOMESERVER to a full URL, for example https://matrix.example.com."
|
|
5344
|
-
});
|
|
5043
|
+
await this.adminServer.stop();
|
|
5044
|
+
} finally {
|
|
5045
|
+
await this.stateStore.flush();
|
|
5345
5046
|
}
|
|
5346
5047
|
}
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
code: "invalid_matrix_user_id",
|
|
5352
|
-
check: "MATRIX_USER_ID",
|
|
5353
|
-
message: `Unexpected Matrix user id format: "${matrixUserId}".`,
|
|
5354
|
-
fix: "Set MATRIX_USER_ID like @bot:example.com."
|
|
5355
|
-
});
|
|
5356
|
-
}
|
|
5357
|
-
const codexBin = readEnv(env, "CODEX_BIN") || "codex";
|
|
5048
|
+
};
|
|
5049
|
+
async function runDoctor(config) {
|
|
5050
|
+
const logger = new Logger(config.logLevel);
|
|
5051
|
+
logger.info("Doctor check started");
|
|
5358
5052
|
try {
|
|
5359
|
-
await
|
|
5053
|
+
const { stdout } = await execFileAsync2(config.codexBin, ["--version"]);
|
|
5054
|
+
logger.info("codex available", { version: stdout.trim() });
|
|
5360
5055
|
} catch (error) {
|
|
5361
|
-
|
|
5362
|
-
|
|
5363
|
-
level: "error",
|
|
5364
|
-
code: "missing_codex_bin",
|
|
5365
|
-
check: "CODEX_BIN",
|
|
5366
|
-
message: `Unable to execute "${codexBin}"${reason}.`,
|
|
5367
|
-
fix: `Install Codex CLI and ensure "${codexBin}" is in PATH, or set CODEX_BIN=/absolute/path/to/codex.`
|
|
5368
|
-
});
|
|
5056
|
+
logger.error("codex unavailable", error);
|
|
5057
|
+
throw error;
|
|
5369
5058
|
}
|
|
5370
|
-
|
|
5371
|
-
|
|
5372
|
-
|
|
5373
|
-
|
|
5374
|
-
|
|
5375
|
-
|
|
5376
|
-
|
|
5377
|
-
|
|
5378
|
-
fix: `Set CODEX_WORKDIR to an existing directory, for example CODEX_WORKDIR=${cwd}.`
|
|
5059
|
+
try {
|
|
5060
|
+
const controller = new AbortController();
|
|
5061
|
+
const timer = setTimeout(() => controller.abort(), config.doctorHttpTimeoutMs);
|
|
5062
|
+
timer.unref?.();
|
|
5063
|
+
const response = await fetch(`${config.matrixHomeserver}/_matrix/client/versions`, {
|
|
5064
|
+
signal: controller.signal
|
|
5065
|
+
}).finally(() => {
|
|
5066
|
+
clearTimeout(timer);
|
|
5379
5067
|
});
|
|
5068
|
+
if (!response.ok) {
|
|
5069
|
+
throw new Error(`HTTP ${response.status}`);
|
|
5070
|
+
}
|
|
5071
|
+
const body = await response.json();
|
|
5072
|
+
logger.info("matrix reachable", { versions: body.versions ?? [] });
|
|
5073
|
+
} catch (error) {
|
|
5074
|
+
logger.error("matrix unreachable", error);
|
|
5075
|
+
throw error;
|
|
5380
5076
|
}
|
|
5381
|
-
|
|
5382
|
-
ok: issues.every((issue) => issue.level !== "error"),
|
|
5383
|
-
issues
|
|
5384
|
-
};
|
|
5077
|
+
logger.info("Doctor check passed");
|
|
5385
5078
|
}
|
|
5386
|
-
|
|
5387
|
-
|
|
5388
|
-
|
|
5389
|
-
const
|
|
5390
|
-
if (
|
|
5391
|
-
|
|
5392
|
-
}
|
|
5393
|
-
|
|
5394
|
-
|
|
5395
|
-
|
|
5079
|
+
|
|
5080
|
+
// src/utils/admin-host.ts
|
|
5081
|
+
function isNonLoopbackHost(host) {
|
|
5082
|
+
const normalized = host.trim().toLowerCase();
|
|
5083
|
+
if (!normalized) {
|
|
5084
|
+
return false;
|
|
5085
|
+
}
|
|
5086
|
+
return !(normalized === "localhost" || normalized === "127.0.0.1" || normalized === "::1" || normalized === "[::1]");
|
|
5087
|
+
}
|
|
5088
|
+
|
|
5089
|
+
// src/config.ts
|
|
5090
|
+
var import_node_fs8 = __toESM(require("fs"));
|
|
5091
|
+
var import_node_path9 = __toESM(require("path"));
|
|
5092
|
+
var import_dotenv2 = __toESM(require("dotenv"));
|
|
5093
|
+
var import_zod = require("zod");
|
|
5094
|
+
var configSchema = import_zod.z.object({
|
|
5095
|
+
MATRIX_HOMESERVER: import_zod.z.string().url(),
|
|
5096
|
+
MATRIX_USER_ID: import_zod.z.string().min(1),
|
|
5097
|
+
MATRIX_ACCESS_TOKEN: import_zod.z.string().min(1),
|
|
5098
|
+
MATRIX_COMMAND_PREFIX: import_zod.z.string().default("!code"),
|
|
5099
|
+
CODEX_BIN: import_zod.z.string().default("codex"),
|
|
5100
|
+
CODEX_MODEL: import_zod.z.string().optional(),
|
|
5101
|
+
CODEX_WORKDIR: import_zod.z.string().default("."),
|
|
5102
|
+
CODEX_DANGEROUS_BYPASS: import_zod.z.string().default("false").transform((v) => v.toLowerCase() === "true"),
|
|
5103
|
+
CODEX_EXEC_TIMEOUT_MS: import_zod.z.string().default("600000").transform((v) => Number.parseInt(v, 10)).pipe(import_zod.z.number().int().positive()),
|
|
5104
|
+
CODEX_SANDBOX_MODE: import_zod.z.string().optional(),
|
|
5105
|
+
CODEX_APPROVAL_POLICY: import_zod.z.string().optional(),
|
|
5106
|
+
CODEX_EXTRA_ARGS: import_zod.z.string().default(""),
|
|
5107
|
+
CODEX_EXTRA_ENV_JSON: import_zod.z.string().default(""),
|
|
5108
|
+
AGENT_WORKFLOW_ENABLED: import_zod.z.string().default("false").transform((v) => v.toLowerCase() === "true"),
|
|
5109
|
+
AGENT_WORKFLOW_AUTO_REPAIR_MAX_ROUNDS: import_zod.z.string().default("1").transform((v) => Number.parseInt(v, 10)).pipe(import_zod.z.number().int().min(0).max(10)),
|
|
5110
|
+
STATE_DB_PATH: import_zod.z.string().default("data/state.db"),
|
|
5111
|
+
STATE_PATH: import_zod.z.string().default("data/state.json"),
|
|
5112
|
+
MAX_PROCESSED_EVENTS_PER_SESSION: import_zod.z.string().default("200").transform((v) => Number.parseInt(v, 10)).pipe(import_zod.z.number().int().positive()),
|
|
5113
|
+
MAX_SESSION_AGE_DAYS: import_zod.z.string().default("30").transform((v) => Number.parseInt(v, 10)).pipe(import_zod.z.number().int().positive()),
|
|
5114
|
+
MAX_SESSIONS: import_zod.z.string().default("5000").transform((v) => Number.parseInt(v, 10)).pipe(import_zod.z.number().int().positive()),
|
|
5115
|
+
REPLY_CHUNK_SIZE: import_zod.z.string().default("3500").transform((v) => Number.parseInt(v, 10)).pipe(import_zod.z.number().int().positive()),
|
|
5116
|
+
MATRIX_PROGRESS_UPDATES: import_zod.z.string().default("true").transform((v) => v.toLowerCase() === "true"),
|
|
5117
|
+
MATRIX_PROGRESS_MIN_INTERVAL_MS: import_zod.z.string().default("2500").transform((v) => Number.parseInt(v, 10)).pipe(import_zod.z.number().int().positive()),
|
|
5118
|
+
MATRIX_TYPING_TIMEOUT_MS: import_zod.z.string().default("10000").transform((v) => Number.parseInt(v, 10)).pipe(import_zod.z.number().int().positive()),
|
|
5119
|
+
SESSION_ACTIVE_WINDOW_MINUTES: import_zod.z.string().default("20").transform((v) => Number.parseInt(v, 10)).pipe(import_zod.z.number().int().positive()),
|
|
5120
|
+
GROUP_TRIGGER_ALLOW_MENTION: import_zod.z.string().default("true").transform((v) => v.toLowerCase() === "true"),
|
|
5121
|
+
GROUP_TRIGGER_ALLOW_REPLY: import_zod.z.string().default("true").transform((v) => v.toLowerCase() === "true"),
|
|
5122
|
+
GROUP_TRIGGER_ALLOW_ACTIVE_WINDOW: import_zod.z.string().default("true").transform((v) => v.toLowerCase() === "true"),
|
|
5123
|
+
GROUP_TRIGGER_ALLOW_PREFIX: import_zod.z.string().default("true").transform((v) => v.toLowerCase() === "true"),
|
|
5124
|
+
ROOM_TRIGGER_POLICY_JSON: import_zod.z.string().default(""),
|
|
5125
|
+
RATE_LIMIT_WINDOW_SECONDS: import_zod.z.string().default("60").transform((v) => Number.parseInt(v, 10)).pipe(import_zod.z.number().int().positive()),
|
|
5126
|
+
RATE_LIMIT_MAX_REQUESTS_PER_USER: import_zod.z.string().default("20").transform((v) => Number.parseInt(v, 10)).pipe(import_zod.z.number().int().nonnegative()),
|
|
5127
|
+
RATE_LIMIT_MAX_REQUESTS_PER_ROOM: import_zod.z.string().default("120").transform((v) => Number.parseInt(v, 10)).pipe(import_zod.z.number().int().nonnegative()),
|
|
5128
|
+
RATE_LIMIT_MAX_CONCURRENT_GLOBAL: import_zod.z.string().default("8").transform((v) => Number.parseInt(v, 10)).pipe(import_zod.z.number().int().nonnegative()),
|
|
5129
|
+
RATE_LIMIT_MAX_CONCURRENT_PER_USER: import_zod.z.string().default("1").transform((v) => Number.parseInt(v, 10)).pipe(import_zod.z.number().int().nonnegative()),
|
|
5130
|
+
RATE_LIMIT_MAX_CONCURRENT_PER_ROOM: import_zod.z.string().default("4").transform((v) => Number.parseInt(v, 10)).pipe(import_zod.z.number().int().nonnegative()),
|
|
5131
|
+
CLI_COMPAT_MODE: import_zod.z.string().default("false").transform((v) => v.toLowerCase() === "true"),
|
|
5132
|
+
CLI_COMPAT_PASSTHROUGH_EVENTS: import_zod.z.string().default("true").transform((v) => v.toLowerCase() === "true"),
|
|
5133
|
+
CLI_COMPAT_PRESERVE_WHITESPACE: import_zod.z.string().default("true").transform((v) => v.toLowerCase() === "true"),
|
|
5134
|
+
CLI_COMPAT_DISABLE_REPLY_CHUNK_SPLIT: import_zod.z.string().default("false").transform((v) => v.toLowerCase() === "true"),
|
|
5135
|
+
CLI_COMPAT_PROGRESS_THROTTLE_MS: import_zod.z.string().default("300").transform((v) => Number.parseInt(v, 10)).pipe(import_zod.z.number().int().nonnegative()),
|
|
5136
|
+
CLI_COMPAT_FETCH_MEDIA: import_zod.z.string().default("true").transform((v) => v.toLowerCase() === "true"),
|
|
5137
|
+
CLI_COMPAT_RECORD_PATH: import_zod.z.string().default(""),
|
|
5138
|
+
DOCTOR_HTTP_TIMEOUT_MS: import_zod.z.string().default("10000").transform((v) => Number.parseInt(v, 10)).pipe(import_zod.z.number().int().positive()),
|
|
5139
|
+
ADMIN_BIND_HOST: import_zod.z.string().default("127.0.0.1"),
|
|
5140
|
+
ADMIN_PORT: import_zod.z.string().default("8787").transform((v) => Number.parseInt(v, 10)).pipe(import_zod.z.number().int().min(1).max(65535)),
|
|
5141
|
+
ADMIN_TOKEN: import_zod.z.string().default(""),
|
|
5142
|
+
ADMIN_IP_ALLOWLIST: import_zod.z.string().default(""),
|
|
5143
|
+
ADMIN_ALLOWED_ORIGINS: import_zod.z.string().default(""),
|
|
5144
|
+
LOG_LEVEL: import_zod.z.enum(["debug", "info", "warn", "error"]).default("info")
|
|
5145
|
+
}).transform((v) => ({
|
|
5146
|
+
matrixHomeserver: v.MATRIX_HOMESERVER,
|
|
5147
|
+
matrixUserId: v.MATRIX_USER_ID,
|
|
5148
|
+
matrixAccessToken: v.MATRIX_ACCESS_TOKEN,
|
|
5149
|
+
matrixCommandPrefix: v.MATRIX_COMMAND_PREFIX,
|
|
5150
|
+
codexBin: v.CODEX_BIN,
|
|
5151
|
+
codexModel: v.CODEX_MODEL?.trim() || null,
|
|
5152
|
+
codexWorkdir: import_node_path9.default.resolve(v.CODEX_WORKDIR),
|
|
5153
|
+
codexDangerousBypass: v.CODEX_DANGEROUS_BYPASS,
|
|
5154
|
+
codexExecTimeoutMs: v.CODEX_EXEC_TIMEOUT_MS,
|
|
5155
|
+
codexSandboxMode: v.CODEX_SANDBOX_MODE?.trim() || null,
|
|
5156
|
+
codexApprovalPolicy: v.CODEX_APPROVAL_POLICY?.trim() || null,
|
|
5157
|
+
codexExtraArgs: parseExtraArgs(v.CODEX_EXTRA_ARGS),
|
|
5158
|
+
codexExtraEnv: parseExtraEnv(v.CODEX_EXTRA_ENV_JSON),
|
|
5159
|
+
agentWorkflow: {
|
|
5160
|
+
enabled: v.AGENT_WORKFLOW_ENABLED,
|
|
5161
|
+
autoRepairMaxRounds: v.AGENT_WORKFLOW_AUTO_REPAIR_MAX_ROUNDS
|
|
5162
|
+
},
|
|
5163
|
+
stateDbPath: import_node_path9.default.resolve(v.STATE_DB_PATH),
|
|
5164
|
+
legacyStateJsonPath: v.STATE_PATH.trim() ? import_node_path9.default.resolve(v.STATE_PATH) : null,
|
|
5165
|
+
maxProcessedEventsPerSession: v.MAX_PROCESSED_EVENTS_PER_SESSION,
|
|
5166
|
+
maxSessionAgeDays: v.MAX_SESSION_AGE_DAYS,
|
|
5167
|
+
maxSessions: v.MAX_SESSIONS,
|
|
5168
|
+
replyChunkSize: v.REPLY_CHUNK_SIZE,
|
|
5169
|
+
matrixProgressUpdates: v.MATRIX_PROGRESS_UPDATES,
|
|
5170
|
+
matrixProgressMinIntervalMs: v.MATRIX_PROGRESS_MIN_INTERVAL_MS,
|
|
5171
|
+
matrixTypingTimeoutMs: v.MATRIX_TYPING_TIMEOUT_MS,
|
|
5172
|
+
sessionActiveWindowMinutes: v.SESSION_ACTIVE_WINDOW_MINUTES,
|
|
5173
|
+
defaultGroupTriggerPolicy: {
|
|
5174
|
+
allowMention: v.GROUP_TRIGGER_ALLOW_MENTION,
|
|
5175
|
+
allowReply: v.GROUP_TRIGGER_ALLOW_REPLY,
|
|
5176
|
+
allowActiveWindow: v.GROUP_TRIGGER_ALLOW_ACTIVE_WINDOW,
|
|
5177
|
+
allowPrefix: v.GROUP_TRIGGER_ALLOW_PREFIX
|
|
5178
|
+
},
|
|
5179
|
+
roomTriggerPolicies: parseRoomTriggerPolicyOverrides(v.ROOM_TRIGGER_POLICY_JSON),
|
|
5180
|
+
rateLimiter: {
|
|
5181
|
+
windowMs: v.RATE_LIMIT_WINDOW_SECONDS * 1e3,
|
|
5182
|
+
maxRequestsPerUser: v.RATE_LIMIT_MAX_REQUESTS_PER_USER,
|
|
5183
|
+
maxRequestsPerRoom: v.RATE_LIMIT_MAX_REQUESTS_PER_ROOM,
|
|
5184
|
+
maxConcurrentGlobal: v.RATE_LIMIT_MAX_CONCURRENT_GLOBAL,
|
|
5185
|
+
maxConcurrentPerUser: v.RATE_LIMIT_MAX_CONCURRENT_PER_USER,
|
|
5186
|
+
maxConcurrentPerRoom: v.RATE_LIMIT_MAX_CONCURRENT_PER_ROOM
|
|
5187
|
+
},
|
|
5188
|
+
cliCompat: {
|
|
5189
|
+
enabled: v.CLI_COMPAT_MODE,
|
|
5190
|
+
passThroughEvents: v.CLI_COMPAT_PASSTHROUGH_EVENTS,
|
|
5191
|
+
preserveWhitespace: v.CLI_COMPAT_PRESERVE_WHITESPACE,
|
|
5192
|
+
disableReplyChunkSplit: v.CLI_COMPAT_DISABLE_REPLY_CHUNK_SPLIT,
|
|
5193
|
+
progressThrottleMs: v.CLI_COMPAT_PROGRESS_THROTTLE_MS,
|
|
5194
|
+
fetchMedia: v.CLI_COMPAT_FETCH_MEDIA,
|
|
5195
|
+
recordPath: v.CLI_COMPAT_RECORD_PATH.trim() ? import_node_path9.default.resolve(v.CLI_COMPAT_RECORD_PATH) : null
|
|
5196
|
+
},
|
|
5197
|
+
doctorHttpTimeoutMs: v.DOCTOR_HTTP_TIMEOUT_MS,
|
|
5198
|
+
adminBindHost: v.ADMIN_BIND_HOST.trim() || "127.0.0.1",
|
|
5199
|
+
adminPort: v.ADMIN_PORT,
|
|
5200
|
+
adminToken: v.ADMIN_TOKEN.trim() || null,
|
|
5201
|
+
adminIpAllowlist: parseCsvList(v.ADMIN_IP_ALLOWLIST),
|
|
5202
|
+
adminAllowedOrigins: parseCsvList(v.ADMIN_ALLOWED_ORIGINS),
|
|
5203
|
+
logLevel: v.LOG_LEVEL
|
|
5204
|
+
}));
|
|
5205
|
+
function loadEnvFromFile(filePath = import_node_path9.default.resolve(process.cwd(), ".env"), env = process.env) {
|
|
5206
|
+
import_dotenv2.default.config({
|
|
5207
|
+
path: filePath,
|
|
5208
|
+
processEnv: env,
|
|
5209
|
+
quiet: true
|
|
5210
|
+
});
|
|
5211
|
+
}
|
|
5212
|
+
function loadConfig(env = process.env) {
|
|
5213
|
+
const parsed = configSchema.safeParse(env);
|
|
5214
|
+
if (!parsed.success) {
|
|
5215
|
+
const message = parsed.error.issues.map((issue) => `${issue.path.join(".") || "config"}: ${issue.message}`).join("; ");
|
|
5216
|
+
throw new Error(`Invalid configuration: ${message}`);
|
|
5217
|
+
}
|
|
5218
|
+
import_node_fs8.default.mkdirSync(import_node_path9.default.dirname(parsed.data.stateDbPath), { recursive: true });
|
|
5219
|
+
if (parsed.data.legacyStateJsonPath) {
|
|
5220
|
+
import_node_fs8.default.mkdirSync(import_node_path9.default.dirname(parsed.data.legacyStateJsonPath), { recursive: true });
|
|
5221
|
+
}
|
|
5222
|
+
return parsed.data;
|
|
5223
|
+
}
|
|
5224
|
+
function parseRoomTriggerPolicyOverrides(raw) {
|
|
5225
|
+
const trimmed = raw.trim();
|
|
5226
|
+
if (!trimmed) {
|
|
5227
|
+
return {};
|
|
5228
|
+
}
|
|
5229
|
+
let parsed;
|
|
5230
|
+
try {
|
|
5231
|
+
parsed = JSON.parse(trimmed);
|
|
5232
|
+
} catch {
|
|
5233
|
+
throw new Error("ROOM_TRIGGER_POLICY_JSON must be valid JSON.");
|
|
5396
5234
|
}
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
lines.push(`- [${level}] ${issue.check}: ${issue.message}`);
|
|
5400
|
-
lines.push(` fix: ${issue.fix}`);
|
|
5235
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
5236
|
+
throw new Error("ROOM_TRIGGER_POLICY_JSON must be an object keyed by room id.");
|
|
5401
5237
|
}
|
|
5402
|
-
|
|
5403
|
-
|
|
5238
|
+
const output = {};
|
|
5239
|
+
for (const [roomId, value] of Object.entries(parsed)) {
|
|
5240
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
5241
|
+
throw new Error(`ROOM_TRIGGER_POLICY_JSON[${roomId}] must be an object.`);
|
|
5242
|
+
}
|
|
5243
|
+
const item = value;
|
|
5244
|
+
const override = {};
|
|
5245
|
+
for (const key of ["allowMention", "allowReply", "allowActiveWindow", "allowPrefix"]) {
|
|
5246
|
+
if (item[key] === void 0) {
|
|
5247
|
+
continue;
|
|
5248
|
+
}
|
|
5249
|
+
if (typeof item[key] !== "boolean") {
|
|
5250
|
+
throw new Error(`ROOM_TRIGGER_POLICY_JSON[${roomId}].${key} must be boolean.`);
|
|
5251
|
+
}
|
|
5252
|
+
override[key] = item[key];
|
|
5253
|
+
}
|
|
5254
|
+
output[roomId] = override;
|
|
5255
|
+
}
|
|
5256
|
+
return output;
|
|
5404
5257
|
}
|
|
5405
|
-
|
|
5406
|
-
|
|
5258
|
+
function parseExtraArgs(raw) {
|
|
5259
|
+
const trimmed = raw.trim();
|
|
5260
|
+
if (!trimmed) {
|
|
5261
|
+
return [];
|
|
5262
|
+
}
|
|
5263
|
+
return trimmed.split(/\s+/).map((entry) => entry.trim()).filter((entry) => entry.length > 0);
|
|
5407
5264
|
}
|
|
5408
|
-
function
|
|
5265
|
+
function parseExtraEnv(raw) {
|
|
5266
|
+
const trimmed = raw.trim();
|
|
5267
|
+
if (!trimmed) {
|
|
5268
|
+
return {};
|
|
5269
|
+
}
|
|
5270
|
+
let parsed;
|
|
5409
5271
|
try {
|
|
5410
|
-
|
|
5272
|
+
parsed = JSON.parse(trimmed);
|
|
5411
5273
|
} catch {
|
|
5412
|
-
|
|
5274
|
+
throw new Error("CODEX_EXTRA_ENV_JSON must be valid JSON object.");
|
|
5275
|
+
}
|
|
5276
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
5277
|
+
throw new Error("CODEX_EXTRA_ENV_JSON must be a key/value object.");
|
|
5278
|
+
}
|
|
5279
|
+
const output = {};
|
|
5280
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
5281
|
+
if (typeof value !== "string") {
|
|
5282
|
+
throw new Error(`CODEX_EXTRA_ENV_JSON[${key}] must be string.`);
|
|
5283
|
+
}
|
|
5284
|
+
output[key] = value;
|
|
5413
5285
|
}
|
|
5286
|
+
return output;
|
|
5414
5287
|
}
|
|
5415
|
-
function
|
|
5416
|
-
return
|
|
5288
|
+
function parseCsvList(raw) {
|
|
5289
|
+
return raw.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
|
|
5417
5290
|
}
|
|
5418
5291
|
|
|
5419
|
-
// src/
|
|
5292
|
+
// src/config-snapshot.ts
|
|
5420
5293
|
var import_node_fs9 = __toESM(require("fs"));
|
|
5421
|
-
var import_node_os2 = __toESM(require("os"));
|
|
5422
5294
|
var import_node_path10 = __toESM(require("path"));
|
|
5423
|
-
var
|
|
5424
|
-
var
|
|
5425
|
-
var
|
|
5426
|
-
|
|
5427
|
-
|
|
5428
|
-
|
|
5429
|
-
|
|
5430
|
-
|
|
5431
|
-
|
|
5432
|
-
|
|
5433
|
-
|
|
5434
|
-
|
|
5295
|
+
var import_zod2 = require("zod");
|
|
5296
|
+
var CONFIG_SNAPSHOT_SCHEMA_VERSION = 1;
|
|
5297
|
+
var CONFIG_SNAPSHOT_ENV_KEYS = [
|
|
5298
|
+
"MATRIX_HOMESERVER",
|
|
5299
|
+
"MATRIX_USER_ID",
|
|
5300
|
+
"MATRIX_ACCESS_TOKEN",
|
|
5301
|
+
"MATRIX_COMMAND_PREFIX",
|
|
5302
|
+
"CODEX_BIN",
|
|
5303
|
+
"CODEX_MODEL",
|
|
5304
|
+
"CODEX_WORKDIR",
|
|
5305
|
+
"CODEX_DANGEROUS_BYPASS",
|
|
5306
|
+
"CODEX_EXEC_TIMEOUT_MS",
|
|
5307
|
+
"CODEX_SANDBOX_MODE",
|
|
5308
|
+
"CODEX_APPROVAL_POLICY",
|
|
5309
|
+
"CODEX_EXTRA_ARGS",
|
|
5310
|
+
"CODEX_EXTRA_ENV_JSON",
|
|
5311
|
+
"AGENT_WORKFLOW_ENABLED",
|
|
5312
|
+
"AGENT_WORKFLOW_AUTO_REPAIR_MAX_ROUNDS",
|
|
5313
|
+
"STATE_DB_PATH",
|
|
5314
|
+
"STATE_PATH",
|
|
5315
|
+
"MAX_PROCESSED_EVENTS_PER_SESSION",
|
|
5316
|
+
"MAX_SESSION_AGE_DAYS",
|
|
5317
|
+
"MAX_SESSIONS",
|
|
5318
|
+
"REPLY_CHUNK_SIZE",
|
|
5319
|
+
"MATRIX_PROGRESS_UPDATES",
|
|
5320
|
+
"MATRIX_PROGRESS_MIN_INTERVAL_MS",
|
|
5321
|
+
"MATRIX_TYPING_TIMEOUT_MS",
|
|
5322
|
+
"SESSION_ACTIVE_WINDOW_MINUTES",
|
|
5323
|
+
"GROUP_TRIGGER_ALLOW_MENTION",
|
|
5324
|
+
"GROUP_TRIGGER_ALLOW_REPLY",
|
|
5325
|
+
"GROUP_TRIGGER_ALLOW_ACTIVE_WINDOW",
|
|
5326
|
+
"GROUP_TRIGGER_ALLOW_PREFIX",
|
|
5327
|
+
"ROOM_TRIGGER_POLICY_JSON",
|
|
5328
|
+
"RATE_LIMIT_WINDOW_SECONDS",
|
|
5329
|
+
"RATE_LIMIT_MAX_REQUESTS_PER_USER",
|
|
5330
|
+
"RATE_LIMIT_MAX_REQUESTS_PER_ROOM",
|
|
5331
|
+
"RATE_LIMIT_MAX_CONCURRENT_GLOBAL",
|
|
5332
|
+
"RATE_LIMIT_MAX_CONCURRENT_PER_USER",
|
|
5333
|
+
"RATE_LIMIT_MAX_CONCURRENT_PER_ROOM",
|
|
5334
|
+
"CLI_COMPAT_MODE",
|
|
5335
|
+
"CLI_COMPAT_PASSTHROUGH_EVENTS",
|
|
5336
|
+
"CLI_COMPAT_PRESERVE_WHITESPACE",
|
|
5337
|
+
"CLI_COMPAT_DISABLE_REPLY_CHUNK_SPLIT",
|
|
5338
|
+
"CLI_COMPAT_PROGRESS_THROTTLE_MS",
|
|
5339
|
+
"CLI_COMPAT_FETCH_MEDIA",
|
|
5340
|
+
"CLI_COMPAT_RECORD_PATH",
|
|
5341
|
+
"DOCTOR_HTTP_TIMEOUT_MS",
|
|
5342
|
+
"ADMIN_BIND_HOST",
|
|
5343
|
+
"ADMIN_PORT",
|
|
5344
|
+
"ADMIN_TOKEN",
|
|
5345
|
+
"ADMIN_IP_ALLOWLIST",
|
|
5346
|
+
"ADMIN_ALLOWED_ORIGINS",
|
|
5347
|
+
"LOG_LEVEL"
|
|
5348
|
+
];
|
|
5349
|
+
var BOOLEAN_STRING = /^(true|false)$/i;
|
|
5350
|
+
var INTEGER_STRING = /^-?\d+$/;
|
|
5351
|
+
var LOG_LEVELS = ["debug", "info", "warn", "error"];
|
|
5352
|
+
var roomSnapshotSchema = import_zod2.z.object({
|
|
5353
|
+
roomId: import_zod2.z.string().min(1),
|
|
5354
|
+
enabled: import_zod2.z.boolean(),
|
|
5355
|
+
allowMention: import_zod2.z.boolean(),
|
|
5356
|
+
allowReply: import_zod2.z.boolean(),
|
|
5357
|
+
allowActiveWindow: import_zod2.z.boolean(),
|
|
5358
|
+
allowPrefix: import_zod2.z.boolean(),
|
|
5359
|
+
workdir: import_zod2.z.string().min(1)
|
|
5360
|
+
}).strict();
|
|
5361
|
+
var envSnapshotSchema = import_zod2.z.object({
|
|
5362
|
+
MATRIX_HOMESERVER: import_zod2.z.string().url(),
|
|
5363
|
+
MATRIX_USER_ID: import_zod2.z.string().min(1),
|
|
5364
|
+
MATRIX_ACCESS_TOKEN: import_zod2.z.string().min(1),
|
|
5365
|
+
MATRIX_COMMAND_PREFIX: import_zod2.z.string(),
|
|
5366
|
+
CODEX_BIN: import_zod2.z.string().min(1),
|
|
5367
|
+
CODEX_MODEL: import_zod2.z.string(),
|
|
5368
|
+
CODEX_WORKDIR: import_zod2.z.string().min(1),
|
|
5369
|
+
CODEX_DANGEROUS_BYPASS: booleanStringSchema("CODEX_DANGEROUS_BYPASS"),
|
|
5370
|
+
CODEX_EXEC_TIMEOUT_MS: integerStringSchema("CODEX_EXEC_TIMEOUT_MS", 1),
|
|
5371
|
+
CODEX_SANDBOX_MODE: import_zod2.z.string(),
|
|
5372
|
+
CODEX_APPROVAL_POLICY: import_zod2.z.string(),
|
|
5373
|
+
CODEX_EXTRA_ARGS: import_zod2.z.string(),
|
|
5374
|
+
CODEX_EXTRA_ENV_JSON: jsonObjectStringSchema("CODEX_EXTRA_ENV_JSON", true),
|
|
5375
|
+
AGENT_WORKFLOW_ENABLED: booleanStringSchema("AGENT_WORKFLOW_ENABLED"),
|
|
5376
|
+
AGENT_WORKFLOW_AUTO_REPAIR_MAX_ROUNDS: integerStringSchema("AGENT_WORKFLOW_AUTO_REPAIR_MAX_ROUNDS", 0, 10),
|
|
5377
|
+
STATE_DB_PATH: import_zod2.z.string().min(1),
|
|
5378
|
+
STATE_PATH: import_zod2.z.string(),
|
|
5379
|
+
MAX_PROCESSED_EVENTS_PER_SESSION: integerStringSchema("MAX_PROCESSED_EVENTS_PER_SESSION", 1),
|
|
5380
|
+
MAX_SESSION_AGE_DAYS: integerStringSchema("MAX_SESSION_AGE_DAYS", 1),
|
|
5381
|
+
MAX_SESSIONS: integerStringSchema("MAX_SESSIONS", 1),
|
|
5382
|
+
REPLY_CHUNK_SIZE: integerStringSchema("REPLY_CHUNK_SIZE", 1),
|
|
5383
|
+
MATRIX_PROGRESS_UPDATES: booleanStringSchema("MATRIX_PROGRESS_UPDATES"),
|
|
5384
|
+
MATRIX_PROGRESS_MIN_INTERVAL_MS: integerStringSchema("MATRIX_PROGRESS_MIN_INTERVAL_MS", 1),
|
|
5385
|
+
MATRIX_TYPING_TIMEOUT_MS: integerStringSchema("MATRIX_TYPING_TIMEOUT_MS", 1),
|
|
5386
|
+
SESSION_ACTIVE_WINDOW_MINUTES: integerStringSchema("SESSION_ACTIVE_WINDOW_MINUTES", 1),
|
|
5387
|
+
GROUP_TRIGGER_ALLOW_MENTION: booleanStringSchema("GROUP_TRIGGER_ALLOW_MENTION"),
|
|
5388
|
+
GROUP_TRIGGER_ALLOW_REPLY: booleanStringSchema("GROUP_TRIGGER_ALLOW_REPLY"),
|
|
5389
|
+
GROUP_TRIGGER_ALLOW_ACTIVE_WINDOW: booleanStringSchema("GROUP_TRIGGER_ALLOW_ACTIVE_WINDOW"),
|
|
5390
|
+
GROUP_TRIGGER_ALLOW_PREFIX: booleanStringSchema("GROUP_TRIGGER_ALLOW_PREFIX"),
|
|
5391
|
+
ROOM_TRIGGER_POLICY_JSON: jsonObjectStringSchema("ROOM_TRIGGER_POLICY_JSON", true),
|
|
5392
|
+
RATE_LIMIT_WINDOW_SECONDS: integerStringSchema("RATE_LIMIT_WINDOW_SECONDS", 1),
|
|
5393
|
+
RATE_LIMIT_MAX_REQUESTS_PER_USER: integerStringSchema("RATE_LIMIT_MAX_REQUESTS_PER_USER", 0),
|
|
5394
|
+
RATE_LIMIT_MAX_REQUESTS_PER_ROOM: integerStringSchema("RATE_LIMIT_MAX_REQUESTS_PER_ROOM", 0),
|
|
5395
|
+
RATE_LIMIT_MAX_CONCURRENT_GLOBAL: integerStringSchema("RATE_LIMIT_MAX_CONCURRENT_GLOBAL", 0),
|
|
5396
|
+
RATE_LIMIT_MAX_CONCURRENT_PER_USER: integerStringSchema("RATE_LIMIT_MAX_CONCURRENT_PER_USER", 0),
|
|
5397
|
+
RATE_LIMIT_MAX_CONCURRENT_PER_ROOM: integerStringSchema("RATE_LIMIT_MAX_CONCURRENT_PER_ROOM", 0),
|
|
5398
|
+
CLI_COMPAT_MODE: booleanStringSchema("CLI_COMPAT_MODE"),
|
|
5399
|
+
CLI_COMPAT_PASSTHROUGH_EVENTS: booleanStringSchema("CLI_COMPAT_PASSTHROUGH_EVENTS"),
|
|
5400
|
+
CLI_COMPAT_PRESERVE_WHITESPACE: booleanStringSchema("CLI_COMPAT_PRESERVE_WHITESPACE"),
|
|
5401
|
+
CLI_COMPAT_DISABLE_REPLY_CHUNK_SPLIT: booleanStringSchema("CLI_COMPAT_DISABLE_REPLY_CHUNK_SPLIT"),
|
|
5402
|
+
CLI_COMPAT_PROGRESS_THROTTLE_MS: integerStringSchema("CLI_COMPAT_PROGRESS_THROTTLE_MS", 0),
|
|
5403
|
+
CLI_COMPAT_FETCH_MEDIA: booleanStringSchema("CLI_COMPAT_FETCH_MEDIA"),
|
|
5404
|
+
CLI_COMPAT_RECORD_PATH: import_zod2.z.string(),
|
|
5405
|
+
DOCTOR_HTTP_TIMEOUT_MS: integerStringSchema("DOCTOR_HTTP_TIMEOUT_MS", 1),
|
|
5406
|
+
ADMIN_BIND_HOST: import_zod2.z.string(),
|
|
5407
|
+
ADMIN_PORT: integerStringSchema("ADMIN_PORT", 1, 65535),
|
|
5408
|
+
ADMIN_TOKEN: import_zod2.z.string(),
|
|
5409
|
+
ADMIN_IP_ALLOWLIST: import_zod2.z.string(),
|
|
5410
|
+
ADMIN_ALLOWED_ORIGINS: import_zod2.z.string().default(""),
|
|
5411
|
+
LOG_LEVEL: import_zod2.z.enum(LOG_LEVELS)
|
|
5412
|
+
}).strict();
|
|
5413
|
+
var configSnapshotSchema = import_zod2.z.object({
|
|
5414
|
+
schemaVersion: import_zod2.z.literal(CONFIG_SNAPSHOT_SCHEMA_VERSION),
|
|
5415
|
+
exportedAt: import_zod2.z.string().datetime({ offset: true }),
|
|
5416
|
+
env: envSnapshotSchema,
|
|
5417
|
+
rooms: import_zod2.z.array(roomSnapshotSchema)
|
|
5418
|
+
}).strict().superRefine((value, ctx) => {
|
|
5419
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5420
|
+
for (const room of value.rooms) {
|
|
5421
|
+
const roomId = room.roomId.trim();
|
|
5422
|
+
if (!roomId) {
|
|
5423
|
+
ctx.addIssue({
|
|
5424
|
+
code: import_zod2.z.ZodIssueCode.custom,
|
|
5425
|
+
message: "rooms[].roomId cannot be empty."
|
|
5426
|
+
});
|
|
5427
|
+
continue;
|
|
5428
|
+
}
|
|
5429
|
+
if (seen.has(roomId)) {
|
|
5430
|
+
ctx.addIssue({
|
|
5431
|
+
code: import_zod2.z.ZodIssueCode.custom,
|
|
5432
|
+
message: `Duplicate room id in snapshot: ${roomId}`
|
|
5433
|
+
});
|
|
5434
|
+
}
|
|
5435
|
+
seen.add(roomId);
|
|
5435
5436
|
}
|
|
5436
|
-
|
|
5437
|
-
|
|
5438
|
-
|
|
5439
|
-
|
|
5440
|
-
|
|
5437
|
+
});
|
|
5438
|
+
function buildConfigSnapshot(config, roomSettings, now = /* @__PURE__ */ new Date()) {
|
|
5439
|
+
return {
|
|
5440
|
+
schemaVersion: CONFIG_SNAPSHOT_SCHEMA_VERSION,
|
|
5441
|
+
exportedAt: now.toISOString(),
|
|
5442
|
+
env: buildSnapshotEnv(config),
|
|
5443
|
+
rooms: roomSettings.map((room) => ({
|
|
5444
|
+
roomId: room.roomId,
|
|
5445
|
+
enabled: room.enabled,
|
|
5446
|
+
allowMention: room.allowMention,
|
|
5447
|
+
allowReply: room.allowReply,
|
|
5448
|
+
allowActiveWindow: room.allowActiveWindow,
|
|
5449
|
+
allowPrefix: room.allowPrefix,
|
|
5450
|
+
workdir: room.workdir
|
|
5451
|
+
}))
|
|
5452
|
+
};
|
|
5441
5453
|
}
|
|
5442
|
-
|
|
5443
|
-
|
|
5444
|
-
|
|
5445
|
-
|
|
5446
|
-
|
|
5447
|
-
var import_node_path11 = __toESM(require("path"));
|
|
5448
|
-
var SYSTEMD_DIR = "/etc/systemd/system";
|
|
5449
|
-
var MAIN_SERVICE_NAME = "codeharbor.service";
|
|
5450
|
-
var ADMIN_SERVICE_NAME = "codeharbor-admin.service";
|
|
5451
|
-
function resolveDefaultRunUser(env = process.env) {
|
|
5452
|
-
const sudoUser = env.SUDO_USER?.trim();
|
|
5453
|
-
if (sudoUser) {
|
|
5454
|
-
return sudoUser;
|
|
5455
|
-
}
|
|
5456
|
-
const user = env.USER?.trim();
|
|
5457
|
-
if (user) {
|
|
5458
|
-
return user;
|
|
5454
|
+
function parseConfigSnapshot(raw) {
|
|
5455
|
+
const parsed = configSnapshotSchema.safeParse(raw);
|
|
5456
|
+
if (!parsed.success) {
|
|
5457
|
+
const message = parsed.error.issues.map((issue) => `${issue.path.join(".") || "snapshot"}: ${issue.message}`).join("; ");
|
|
5458
|
+
throw new Error(`Invalid config snapshot: ${message}`);
|
|
5459
5459
|
}
|
|
5460
|
+
return parsed.data;
|
|
5461
|
+
}
|
|
5462
|
+
function serializeConfigSnapshot(snapshot) {
|
|
5463
|
+
return `${JSON.stringify(snapshot, null, 2)}
|
|
5464
|
+
`;
|
|
5465
|
+
}
|
|
5466
|
+
async function runConfigExportCommand(options = {}) {
|
|
5467
|
+
const cwd = options.cwd ?? process.cwd();
|
|
5468
|
+
const output = options.output ?? process.stdout;
|
|
5469
|
+
const config = loadConfig(options.env ?? process.env);
|
|
5470
|
+
const stateStore = new StateStore(
|
|
5471
|
+
config.stateDbPath,
|
|
5472
|
+
config.legacyStateJsonPath,
|
|
5473
|
+
config.maxProcessedEventsPerSession,
|
|
5474
|
+
config.maxSessionAgeDays,
|
|
5475
|
+
config.maxSessions
|
|
5476
|
+
);
|
|
5460
5477
|
try {
|
|
5461
|
-
|
|
5462
|
-
|
|
5463
|
-
|
|
5478
|
+
const snapshot = buildConfigSnapshot(config, stateStore.listRoomSettings(), options.now ?? /* @__PURE__ */ new Date());
|
|
5479
|
+
const serialized = serializeConfigSnapshot(snapshot);
|
|
5480
|
+
if (options.outputPath) {
|
|
5481
|
+
const targetPath = import_node_path10.default.resolve(cwd, options.outputPath);
|
|
5482
|
+
import_node_fs9.default.mkdirSync(import_node_path10.default.dirname(targetPath), { recursive: true });
|
|
5483
|
+
import_node_fs9.default.writeFileSync(targetPath, serialized, "utf8");
|
|
5484
|
+
output.write(`Exported config snapshot to ${targetPath}
|
|
5485
|
+
`);
|
|
5486
|
+
return;
|
|
5487
|
+
}
|
|
5488
|
+
output.write(serialized);
|
|
5489
|
+
} finally {
|
|
5490
|
+
await stateStore.flush();
|
|
5464
5491
|
}
|
|
5465
5492
|
}
|
|
5466
|
-
function
|
|
5467
|
-
const
|
|
5468
|
-
|
|
5469
|
-
|
|
5493
|
+
async function runConfigImportCommand(options) {
|
|
5494
|
+
const cwd = options.cwd ?? process.cwd();
|
|
5495
|
+
const output = options.output ?? process.stdout;
|
|
5496
|
+
const actor = options.actor?.trim() || "cli:config-import";
|
|
5497
|
+
const sourcePath = import_node_path10.default.resolve(cwd, options.filePath);
|
|
5498
|
+
if (!import_node_fs9.default.existsSync(sourcePath)) {
|
|
5499
|
+
throw new Error(`Config snapshot file not found: ${sourcePath}`);
|
|
5470
5500
|
}
|
|
5471
|
-
const
|
|
5472
|
-
|
|
5473
|
-
|
|
5501
|
+
const snapshot = parseConfigSnapshot(parseJsonFile(sourcePath));
|
|
5502
|
+
const normalizedEnv = normalizeSnapshotEnv(snapshot.env, cwd);
|
|
5503
|
+
ensureDirectory2(normalizedEnv.CODEX_WORKDIR, "CODEX_WORKDIR");
|
|
5504
|
+
const normalizedRooms = normalizeSnapshotRooms(snapshot.rooms, cwd);
|
|
5505
|
+
if (options.dryRun) {
|
|
5506
|
+
output.write(
|
|
5507
|
+
[
|
|
5508
|
+
`Config snapshot is valid: ${sourcePath}`,
|
|
5509
|
+
`- schemaVersion: ${snapshot.schemaVersion}`,
|
|
5510
|
+
`- rooms: ${normalizedRooms.length}`,
|
|
5511
|
+
"- dry-run: no changes were written"
|
|
5512
|
+
].join("\n") + "\n"
|
|
5513
|
+
);
|
|
5514
|
+
return;
|
|
5515
|
+
}
|
|
5516
|
+
persistEnvSnapshot(cwd, normalizedEnv);
|
|
5517
|
+
const stateStore = new StateStore(
|
|
5518
|
+
normalizedEnv.STATE_DB_PATH,
|
|
5519
|
+
normalizedEnv.STATE_PATH ? normalizedEnv.STATE_PATH : null,
|
|
5520
|
+
parseIntStrict(normalizedEnv.MAX_PROCESSED_EVENTS_PER_SESSION),
|
|
5521
|
+
parseIntStrict(normalizedEnv.MAX_SESSION_AGE_DAYS),
|
|
5522
|
+
parseIntStrict(normalizedEnv.MAX_SESSIONS)
|
|
5523
|
+
);
|
|
5524
|
+
try {
|
|
5525
|
+
synchronizeRoomSettings(stateStore, normalizedRooms);
|
|
5526
|
+
stateStore.appendConfigRevision(
|
|
5527
|
+
actor,
|
|
5528
|
+
`import config snapshot from ${import_node_path10.default.basename(sourcePath)}`,
|
|
5529
|
+
JSON.stringify({
|
|
5530
|
+
type: "config_snapshot_import",
|
|
5531
|
+
sourcePath,
|
|
5532
|
+
roomCount: normalizedRooms.length,
|
|
5533
|
+
envKeyCount: CONFIG_SNAPSHOT_ENV_KEYS.length
|
|
5534
|
+
})
|
|
5535
|
+
);
|
|
5536
|
+
} finally {
|
|
5537
|
+
await stateStore.flush();
|
|
5474
5538
|
}
|
|
5475
|
-
|
|
5539
|
+
output.write(
|
|
5540
|
+
[
|
|
5541
|
+
`Imported config snapshot from ${sourcePath}`,
|
|
5542
|
+
`- updated .env in ${import_node_path10.default.resolve(cwd, ".env")}`,
|
|
5543
|
+
`- synchronized room settings: ${normalizedRooms.length}`,
|
|
5544
|
+
"- restart required: yes (global env settings are restart-scoped)"
|
|
5545
|
+
].join("\n") + "\n"
|
|
5546
|
+
);
|
|
5476
5547
|
}
|
|
5477
|
-
function
|
|
5478
|
-
|
|
5479
|
-
|
|
5480
|
-
|
|
5481
|
-
|
|
5482
|
-
|
|
5483
|
-
|
|
5484
|
-
|
|
5485
|
-
|
|
5486
|
-
|
|
5487
|
-
|
|
5488
|
-
|
|
5489
|
-
|
|
5490
|
-
|
|
5491
|
-
|
|
5492
|
-
|
|
5493
|
-
|
|
5494
|
-
|
|
5495
|
-
"
|
|
5496
|
-
|
|
5497
|
-
|
|
5498
|
-
|
|
5499
|
-
|
|
5500
|
-
|
|
5501
|
-
|
|
5502
|
-
|
|
5503
|
-
|
|
5548
|
+
function buildSnapshotEnv(config) {
|
|
5549
|
+
return {
|
|
5550
|
+
MATRIX_HOMESERVER: config.matrixHomeserver,
|
|
5551
|
+
MATRIX_USER_ID: config.matrixUserId,
|
|
5552
|
+
MATRIX_ACCESS_TOKEN: config.matrixAccessToken,
|
|
5553
|
+
MATRIX_COMMAND_PREFIX: config.matrixCommandPrefix,
|
|
5554
|
+
CODEX_BIN: config.codexBin,
|
|
5555
|
+
CODEX_MODEL: config.codexModel ?? "",
|
|
5556
|
+
CODEX_WORKDIR: config.codexWorkdir,
|
|
5557
|
+
CODEX_DANGEROUS_BYPASS: String(config.codexDangerousBypass),
|
|
5558
|
+
CODEX_EXEC_TIMEOUT_MS: String(config.codexExecTimeoutMs),
|
|
5559
|
+
CODEX_SANDBOX_MODE: config.codexSandboxMode ?? "",
|
|
5560
|
+
CODEX_APPROVAL_POLICY: config.codexApprovalPolicy ?? "",
|
|
5561
|
+
CODEX_EXTRA_ARGS: config.codexExtraArgs.join(" "),
|
|
5562
|
+
CODEX_EXTRA_ENV_JSON: serializeJsonObject(config.codexExtraEnv),
|
|
5563
|
+
AGENT_WORKFLOW_ENABLED: String(config.agentWorkflow.enabled),
|
|
5564
|
+
AGENT_WORKFLOW_AUTO_REPAIR_MAX_ROUNDS: String(config.agentWorkflow.autoRepairMaxRounds),
|
|
5565
|
+
STATE_DB_PATH: config.stateDbPath,
|
|
5566
|
+
STATE_PATH: config.legacyStateJsonPath ?? "",
|
|
5567
|
+
MAX_PROCESSED_EVENTS_PER_SESSION: String(config.maxProcessedEventsPerSession),
|
|
5568
|
+
MAX_SESSION_AGE_DAYS: String(config.maxSessionAgeDays),
|
|
5569
|
+
MAX_SESSIONS: String(config.maxSessions),
|
|
5570
|
+
REPLY_CHUNK_SIZE: String(config.replyChunkSize),
|
|
5571
|
+
MATRIX_PROGRESS_UPDATES: String(config.matrixProgressUpdates),
|
|
5572
|
+
MATRIX_PROGRESS_MIN_INTERVAL_MS: String(config.matrixProgressMinIntervalMs),
|
|
5573
|
+
MATRIX_TYPING_TIMEOUT_MS: String(config.matrixTypingTimeoutMs),
|
|
5574
|
+
SESSION_ACTIVE_WINDOW_MINUTES: String(config.sessionActiveWindowMinutes),
|
|
5575
|
+
GROUP_TRIGGER_ALLOW_MENTION: String(config.defaultGroupTriggerPolicy.allowMention),
|
|
5576
|
+
GROUP_TRIGGER_ALLOW_REPLY: String(config.defaultGroupTriggerPolicy.allowReply),
|
|
5577
|
+
GROUP_TRIGGER_ALLOW_ACTIVE_WINDOW: String(config.defaultGroupTriggerPolicy.allowActiveWindow),
|
|
5578
|
+
GROUP_TRIGGER_ALLOW_PREFIX: String(config.defaultGroupTriggerPolicy.allowPrefix),
|
|
5579
|
+
ROOM_TRIGGER_POLICY_JSON: serializeJsonObject(config.roomTriggerPolicies),
|
|
5580
|
+
RATE_LIMIT_WINDOW_SECONDS: String(Math.max(1, Math.round(config.rateLimiter.windowMs / 1e3))),
|
|
5581
|
+
RATE_LIMIT_MAX_REQUESTS_PER_USER: String(config.rateLimiter.maxRequestsPerUser),
|
|
5582
|
+
RATE_LIMIT_MAX_REQUESTS_PER_ROOM: String(config.rateLimiter.maxRequestsPerRoom),
|
|
5583
|
+
RATE_LIMIT_MAX_CONCURRENT_GLOBAL: String(config.rateLimiter.maxConcurrentGlobal),
|
|
5584
|
+
RATE_LIMIT_MAX_CONCURRENT_PER_USER: String(config.rateLimiter.maxConcurrentPerUser),
|
|
5585
|
+
RATE_LIMIT_MAX_CONCURRENT_PER_ROOM: String(config.rateLimiter.maxConcurrentPerRoom),
|
|
5586
|
+
CLI_COMPAT_MODE: String(config.cliCompat.enabled),
|
|
5587
|
+
CLI_COMPAT_PASSTHROUGH_EVENTS: String(config.cliCompat.passThroughEvents),
|
|
5588
|
+
CLI_COMPAT_PRESERVE_WHITESPACE: String(config.cliCompat.preserveWhitespace),
|
|
5589
|
+
CLI_COMPAT_DISABLE_REPLY_CHUNK_SPLIT: String(config.cliCompat.disableReplyChunkSplit),
|
|
5590
|
+
CLI_COMPAT_PROGRESS_THROTTLE_MS: String(config.cliCompat.progressThrottleMs),
|
|
5591
|
+
CLI_COMPAT_FETCH_MEDIA: String(config.cliCompat.fetchMedia),
|
|
5592
|
+
CLI_COMPAT_RECORD_PATH: config.cliCompat.recordPath ?? "",
|
|
5593
|
+
DOCTOR_HTTP_TIMEOUT_MS: String(config.doctorHttpTimeoutMs),
|
|
5594
|
+
ADMIN_BIND_HOST: config.adminBindHost,
|
|
5595
|
+
ADMIN_PORT: String(config.adminPort),
|
|
5596
|
+
ADMIN_TOKEN: config.adminToken ?? "",
|
|
5597
|
+
ADMIN_IP_ALLOWLIST: config.adminIpAllowlist.join(","),
|
|
5598
|
+
ADMIN_ALLOWED_ORIGINS: config.adminAllowedOrigins.join(","),
|
|
5599
|
+
LOG_LEVEL: config.logLevel
|
|
5600
|
+
};
|
|
5504
5601
|
}
|
|
5505
|
-
function
|
|
5506
|
-
|
|
5507
|
-
|
|
5508
|
-
|
|
5509
|
-
|
|
5510
|
-
|
|
5511
|
-
|
|
5512
|
-
|
|
5513
|
-
|
|
5514
|
-
|
|
5515
|
-
"Type=simple",
|
|
5516
|
-
`User=${options.runUser}`,
|
|
5517
|
-
`WorkingDirectory=${runtimeHome2}`,
|
|
5518
|
-
`Environment=CODEHARBOR_HOME=${runtimeHome2}`,
|
|
5519
|
-
`ExecStart=${import_node_path11.default.resolve(options.nodeBinPath)} ${import_node_path11.default.resolve(options.cliScriptPath)} admin serve`,
|
|
5520
|
-
"Restart=always",
|
|
5521
|
-
"RestartSec=3",
|
|
5522
|
-
"NoNewPrivileges=true",
|
|
5523
|
-
"PrivateTmp=true",
|
|
5524
|
-
"ProtectSystem=full",
|
|
5525
|
-
"ProtectHome=false",
|
|
5526
|
-
`ReadWritePaths=${runtimeHome2}`,
|
|
5527
|
-
"",
|
|
5528
|
-
"[Install]",
|
|
5529
|
-
"WantedBy=multi-user.target",
|
|
5530
|
-
""
|
|
5531
|
-
].join("\n");
|
|
5602
|
+
function parseJsonFile(filePath) {
|
|
5603
|
+
try {
|
|
5604
|
+
const raw = import_node_fs9.default.readFileSync(filePath, "utf8");
|
|
5605
|
+
return JSON.parse(raw);
|
|
5606
|
+
} catch (error) {
|
|
5607
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5608
|
+
throw new Error(`Failed to parse snapshot JSON: ${message}`, {
|
|
5609
|
+
cause: error
|
|
5610
|
+
});
|
|
5611
|
+
}
|
|
5532
5612
|
}
|
|
5533
|
-
function
|
|
5534
|
-
|
|
5535
|
-
|
|
5536
|
-
|
|
5537
|
-
|
|
5538
|
-
|
|
5539
|
-
|
|
5540
|
-
validateSimpleValue(runtimeHome2, "runtimeHome");
|
|
5541
|
-
validateSimpleValue(options.nodeBinPath, "nodeBinPath");
|
|
5542
|
-
validateSimpleValue(options.cliScriptPath, "cliScriptPath");
|
|
5543
|
-
ensureUserExists(runUser);
|
|
5544
|
-
const runGroup = resolveUserGroup(runUser);
|
|
5545
|
-
import_node_fs10.default.mkdirSync(runtimeHome2, { recursive: true });
|
|
5546
|
-
runCommand("chown", ["-R", `${runUser}:${runGroup}`, runtimeHome2]);
|
|
5547
|
-
const mainPath = import_node_path11.default.join(SYSTEMD_DIR, MAIN_SERVICE_NAME);
|
|
5548
|
-
const adminPath = import_node_path11.default.join(SYSTEMD_DIR, ADMIN_SERVICE_NAME);
|
|
5549
|
-
const unitOptions = {
|
|
5550
|
-
runUser,
|
|
5551
|
-
runtimeHome: runtimeHome2,
|
|
5552
|
-
nodeBinPath: options.nodeBinPath,
|
|
5553
|
-
cliScriptPath: options.cliScriptPath
|
|
5613
|
+
function normalizeSnapshotEnv(env, cwd) {
|
|
5614
|
+
return {
|
|
5615
|
+
...env,
|
|
5616
|
+
CODEX_WORKDIR: import_node_path10.default.resolve(cwd, env.CODEX_WORKDIR),
|
|
5617
|
+
STATE_DB_PATH: import_node_path10.default.resolve(cwd, env.STATE_DB_PATH),
|
|
5618
|
+
STATE_PATH: env.STATE_PATH.trim() ? import_node_path10.default.resolve(cwd, env.STATE_PATH) : "",
|
|
5619
|
+
CLI_COMPAT_RECORD_PATH: env.CLI_COMPAT_RECORD_PATH.trim() ? import_node_path10.default.resolve(cwd, env.CLI_COMPAT_RECORD_PATH) : ""
|
|
5554
5620
|
};
|
|
5555
|
-
|
|
5556
|
-
|
|
5557
|
-
|
|
5558
|
-
|
|
5559
|
-
|
|
5560
|
-
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
runSystemctl(["enable", "--now", ADMIN_SERVICE_NAME]);
|
|
5621
|
+
}
|
|
5622
|
+
function normalizeSnapshotRooms(rooms, cwd) {
|
|
5623
|
+
const normalized = [];
|
|
5624
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5625
|
+
for (const room of rooms) {
|
|
5626
|
+
const roomId = room.roomId.trim();
|
|
5627
|
+
if (!roomId) {
|
|
5628
|
+
throw new Error("roomId is required for every room in snapshot.");
|
|
5564
5629
|
}
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
|
|
5630
|
+
if (seen.has(roomId)) {
|
|
5631
|
+
throw new Error(`Duplicate roomId in snapshot: ${roomId}`);
|
|
5632
|
+
}
|
|
5633
|
+
seen.add(roomId);
|
|
5634
|
+
const workdir = import_node_path10.default.resolve(cwd, room.workdir);
|
|
5635
|
+
ensureDirectory2(workdir, `room workdir (${roomId})`);
|
|
5636
|
+
normalized.push({
|
|
5637
|
+
roomId,
|
|
5638
|
+
enabled: room.enabled,
|
|
5639
|
+
allowMention: room.allowMention,
|
|
5640
|
+
allowReply: room.allowReply,
|
|
5641
|
+
allowActiveWindow: room.allowActiveWindow,
|
|
5642
|
+
allowPrefix: room.allowPrefix,
|
|
5643
|
+
workdir
|
|
5644
|
+
});
|
|
5645
|
+
}
|
|
5646
|
+
return normalized;
|
|
5647
|
+
}
|
|
5648
|
+
function synchronizeRoomSettings(stateStore, rooms) {
|
|
5649
|
+
const incoming = new Map(rooms.map((room) => [room.roomId, room]));
|
|
5650
|
+
const existing = stateStore.listRoomSettings();
|
|
5651
|
+
for (const room of existing) {
|
|
5652
|
+
if (!incoming.has(room.roomId)) {
|
|
5653
|
+
stateStore.deleteRoomSettings(room.roomId);
|
|
5569
5654
|
}
|
|
5570
5655
|
}
|
|
5571
|
-
|
|
5572
|
-
|
|
5573
|
-
if (options.installAdmin) {
|
|
5574
|
-
output.write(`Installed systemd unit: ${adminPath}
|
|
5575
|
-
`);
|
|
5656
|
+
for (const room of rooms) {
|
|
5657
|
+
stateStore.upsertRoomSettings(room);
|
|
5576
5658
|
}
|
|
5577
|
-
output.write("Done. Check status with: systemctl status codeharbor --no-pager\n");
|
|
5578
5659
|
}
|
|
5579
|
-
function
|
|
5580
|
-
|
|
5581
|
-
|
|
5582
|
-
const
|
|
5583
|
-
const
|
|
5584
|
-
const
|
|
5585
|
-
|
|
5586
|
-
if (import_node_fs10.default.existsSync(mainPath)) {
|
|
5587
|
-
import_node_fs10.default.unlinkSync(mainPath);
|
|
5588
|
-
}
|
|
5589
|
-
if (options.removeAdmin) {
|
|
5590
|
-
stopAndDisableIfPresent(ADMIN_SERVICE_NAME);
|
|
5591
|
-
if (import_node_fs10.default.existsSync(adminPath)) {
|
|
5592
|
-
import_node_fs10.default.unlinkSync(adminPath);
|
|
5593
|
-
}
|
|
5660
|
+
function persistEnvSnapshot(cwd, env) {
|
|
5661
|
+
const envPath = import_node_path10.default.resolve(cwd, ".env");
|
|
5662
|
+
const examplePath = import_node_path10.default.resolve(cwd, ".env.example");
|
|
5663
|
+
const template = import_node_fs9.default.existsSync(envPath) ? import_node_fs9.default.readFileSync(envPath, "utf8") : import_node_fs9.default.existsSync(examplePath) ? import_node_fs9.default.readFileSync(examplePath, "utf8") : "";
|
|
5664
|
+
const overrides = {};
|
|
5665
|
+
for (const key of CONFIG_SNAPSHOT_ENV_KEYS) {
|
|
5666
|
+
overrides[key] = env[key];
|
|
5594
5667
|
}
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
|
|
5598
|
-
|
|
5599
|
-
if (
|
|
5600
|
-
|
|
5601
|
-
`);
|
|
5668
|
+
const next = applyEnvOverrides(template, overrides);
|
|
5669
|
+
import_node_fs9.default.writeFileSync(envPath, next, "utf8");
|
|
5670
|
+
}
|
|
5671
|
+
function ensureDirectory2(dirPath, label) {
|
|
5672
|
+
if (!import_node_fs9.default.existsSync(dirPath) || !import_node_fs9.default.statSync(dirPath).isDirectory()) {
|
|
5673
|
+
throw new Error(`${label} does not exist or is not a directory: ${dirPath}`);
|
|
5602
5674
|
}
|
|
5603
|
-
output.write("Done.\n");
|
|
5604
5675
|
}
|
|
5605
|
-
function
|
|
5606
|
-
|
|
5607
|
-
|
|
5608
|
-
|
|
5609
|
-
runSystemctl(["restart", MAIN_SERVICE_NAME]);
|
|
5610
|
-
output.write(`Restarted service: ${MAIN_SERVICE_NAME}
|
|
5611
|
-
`);
|
|
5612
|
-
if (options.restartAdmin) {
|
|
5613
|
-
runSystemctl(["restart", ADMIN_SERVICE_NAME]);
|
|
5614
|
-
output.write(`Restarted service: ${ADMIN_SERVICE_NAME}
|
|
5615
|
-
`);
|
|
5676
|
+
function parseIntStrict(raw) {
|
|
5677
|
+
const value = Number.parseInt(raw, 10);
|
|
5678
|
+
if (!Number.isFinite(value)) {
|
|
5679
|
+
throw new Error(`Invalid integer value: ${raw}`);
|
|
5616
5680
|
}
|
|
5617
|
-
|
|
5681
|
+
return value;
|
|
5618
5682
|
}
|
|
5619
|
-
function
|
|
5620
|
-
|
|
5621
|
-
|
|
5622
|
-
|
|
5623
|
-
|
|
5624
|
-
|
|
5683
|
+
function serializeJsonObject(value) {
|
|
5684
|
+
return Object.keys(value).length > 0 ? JSON.stringify(value) : "";
|
|
5685
|
+
}
|
|
5686
|
+
function booleanStringSchema(key) {
|
|
5687
|
+
return import_zod2.z.string().refine((value) => BOOLEAN_STRING.test(value), {
|
|
5688
|
+
message: `${key} must be a boolean string (true/false).`
|
|
5689
|
+
});
|
|
5690
|
+
}
|
|
5691
|
+
function integerStringSchema(key, min, max = Number.MAX_SAFE_INTEGER) {
|
|
5692
|
+
return import_zod2.z.string().refine((value) => {
|
|
5693
|
+
const trimmed = value.trim();
|
|
5694
|
+
if (!INTEGER_STRING.test(trimmed)) {
|
|
5695
|
+
return false;
|
|
5625
5696
|
}
|
|
5626
|
-
const
|
|
5627
|
-
|
|
5628
|
-
|
|
5629
|
-
|
|
5630
|
-
|
|
5697
|
+
const parsed = Number.parseInt(trimmed, 10);
|
|
5698
|
+
if (!Number.isFinite(parsed)) {
|
|
5699
|
+
return false;
|
|
5700
|
+
}
|
|
5701
|
+
return parsed >= min && parsed <= max;
|
|
5702
|
+
}, {
|
|
5703
|
+
message: `${key} must be an integer string in range [${min}, ${max}].`
|
|
5704
|
+
});
|
|
5631
5705
|
}
|
|
5632
|
-
function
|
|
5633
|
-
|
|
5634
|
-
|
|
5635
|
-
|
|
5636
|
-
|
|
5706
|
+
function jsonObjectStringSchema(key, allowEmpty) {
|
|
5707
|
+
return import_zod2.z.string().refine((value) => {
|
|
5708
|
+
const trimmed = value.trim();
|
|
5709
|
+
if (!trimmed) {
|
|
5710
|
+
return allowEmpty;
|
|
5711
|
+
}
|
|
5712
|
+
let parsed;
|
|
5713
|
+
try {
|
|
5714
|
+
parsed = JSON.parse(trimmed);
|
|
5715
|
+
} catch {
|
|
5716
|
+
return false;
|
|
5717
|
+
}
|
|
5718
|
+
return Boolean(parsed) && typeof parsed === "object" && !Array.isArray(parsed);
|
|
5719
|
+
}, {
|
|
5720
|
+
message: `${key} must be an empty string or a JSON object string.`
|
|
5721
|
+
});
|
|
5637
5722
|
}
|
|
5638
|
-
|
|
5639
|
-
|
|
5640
|
-
|
|
5723
|
+
|
|
5724
|
+
// src/preflight.ts
|
|
5725
|
+
var import_node_child_process5 = require("child_process");
|
|
5726
|
+
var import_node_fs10 = __toESM(require("fs"));
|
|
5727
|
+
var import_node_path11 = __toESM(require("path"));
|
|
5728
|
+
var import_node_util3 = require("util");
|
|
5729
|
+
var execFileAsync3 = (0, import_node_util3.promisify)(import_node_child_process5.execFile);
|
|
5730
|
+
var REQUIRED_ENV_KEYS = ["MATRIX_HOMESERVER", "MATRIX_USER_ID", "MATRIX_ACCESS_TOKEN"];
|
|
5731
|
+
async function runStartupPreflight(options = {}) {
|
|
5732
|
+
const env = options.env ?? process.env;
|
|
5733
|
+
const cwd = options.cwd ?? process.cwd();
|
|
5734
|
+
const checkCodexBinary = options.checkCodexBinary ?? defaultCheckCodexBinary;
|
|
5735
|
+
const fileExists = options.fileExists ?? import_node_fs10.default.existsSync;
|
|
5736
|
+
const isDirectory = options.isDirectory ?? defaultIsDirectory;
|
|
5737
|
+
const issues = [];
|
|
5738
|
+
const envPath = import_node_path11.default.resolve(cwd, ".env");
|
|
5739
|
+
if (!fileExists(envPath)) {
|
|
5740
|
+
issues.push({
|
|
5741
|
+
level: "warn",
|
|
5742
|
+
code: "missing_dotenv",
|
|
5743
|
+
check: ".env",
|
|
5744
|
+
message: `No .env file found at ${envPath}.`,
|
|
5745
|
+
fix: 'Run "codeharbor init" to create baseline config.'
|
|
5746
|
+
});
|
|
5641
5747
|
}
|
|
5642
|
-
|
|
5643
|
-
|
|
5748
|
+
for (const key of REQUIRED_ENV_KEYS) {
|
|
5749
|
+
if (!readEnv(env, key)) {
|
|
5750
|
+
issues.push({
|
|
5751
|
+
level: "error",
|
|
5752
|
+
code: "missing_env",
|
|
5753
|
+
check: key,
|
|
5754
|
+
message: `${key} is required.`,
|
|
5755
|
+
fix: `Run "codeharbor init" or set ${key} in .env.`
|
|
5756
|
+
});
|
|
5757
|
+
}
|
|
5644
5758
|
}
|
|
5645
|
-
|
|
5646
|
-
|
|
5647
|
-
|
|
5648
|
-
|
|
5759
|
+
const matrixHomeserver = readEnv(env, "MATRIX_HOMESERVER");
|
|
5760
|
+
if (matrixHomeserver) {
|
|
5761
|
+
try {
|
|
5762
|
+
new URL(matrixHomeserver);
|
|
5763
|
+
} catch {
|
|
5764
|
+
issues.push({
|
|
5765
|
+
level: "error",
|
|
5766
|
+
code: "invalid_matrix_homeserver",
|
|
5767
|
+
check: "MATRIX_HOMESERVER",
|
|
5768
|
+
message: `Invalid URL: "${matrixHomeserver}".`,
|
|
5769
|
+
fix: "Set MATRIX_HOMESERVER to a full URL, for example https://matrix.example.com."
|
|
5770
|
+
});
|
|
5771
|
+
}
|
|
5772
|
+
}
|
|
5773
|
+
const matrixUserId = readEnv(env, "MATRIX_USER_ID");
|
|
5774
|
+
if (matrixUserId && !/^@[^:\s]+:.+/.test(matrixUserId)) {
|
|
5775
|
+
issues.push({
|
|
5776
|
+
level: "error",
|
|
5777
|
+
code: "invalid_matrix_user_id",
|
|
5778
|
+
check: "MATRIX_USER_ID",
|
|
5779
|
+
message: `Unexpected Matrix user id format: "${matrixUserId}".`,
|
|
5780
|
+
fix: "Set MATRIX_USER_ID like @bot:example.com."
|
|
5781
|
+
});
|
|
5649
5782
|
}
|
|
5783
|
+
const codexBin = readEnv(env, "CODEX_BIN") || "codex";
|
|
5650
5784
|
try {
|
|
5651
|
-
|
|
5652
|
-
} catch {
|
|
5653
|
-
|
|
5785
|
+
await checkCodexBinary(codexBin);
|
|
5786
|
+
} catch (error) {
|
|
5787
|
+
const reason = error instanceof Error && error.message ? ` (${error.message})` : "";
|
|
5788
|
+
issues.push({
|
|
5789
|
+
level: "error",
|
|
5790
|
+
code: "missing_codex_bin",
|
|
5791
|
+
check: "CODEX_BIN",
|
|
5792
|
+
message: `Unable to execute "${codexBin}"${reason}.`,
|
|
5793
|
+
fix: `Install Codex CLI and ensure "${codexBin}" is in PATH, or set CODEX_BIN=/absolute/path/to/codex.`
|
|
5794
|
+
});
|
|
5795
|
+
}
|
|
5796
|
+
const configuredWorkdir = readEnv(env, "CODEX_WORKDIR");
|
|
5797
|
+
const workdir = import_node_path11.default.resolve(cwd, configuredWorkdir || cwd);
|
|
5798
|
+
if (!fileExists(workdir) || !isDirectory(workdir)) {
|
|
5799
|
+
issues.push({
|
|
5800
|
+
level: "error",
|
|
5801
|
+
code: "invalid_codex_workdir",
|
|
5802
|
+
check: "CODEX_WORKDIR",
|
|
5803
|
+
message: `Working directory does not exist or is not a directory: ${workdir}.`,
|
|
5804
|
+
fix: `Set CODEX_WORKDIR to an existing directory, for example CODEX_WORKDIR=${cwd}.`
|
|
5805
|
+
});
|
|
5654
5806
|
}
|
|
5807
|
+
return {
|
|
5808
|
+
ok: issues.every((issue) => issue.level !== "error"),
|
|
5809
|
+
issues
|
|
5810
|
+
};
|
|
5655
5811
|
}
|
|
5656
|
-
function
|
|
5657
|
-
|
|
5658
|
-
|
|
5812
|
+
function formatPreflightReport(result, commandName) {
|
|
5813
|
+
const lines = [];
|
|
5814
|
+
const errors = result.issues.filter((issue) => issue.level === "error").length;
|
|
5815
|
+
const warnings = result.issues.filter((issue) => issue.level === "warn").length;
|
|
5816
|
+
if (result.ok) {
|
|
5817
|
+
lines.push(`Preflight check passed for "codeharbor ${commandName}" with ${warnings} warning(s).`);
|
|
5818
|
+
} else {
|
|
5819
|
+
lines.push(
|
|
5820
|
+
`Preflight check failed for "codeharbor ${commandName}" with ${errors} error(s) and ${warnings} warning(s).`
|
|
5821
|
+
);
|
|
5659
5822
|
}
|
|
5660
|
-
|
|
5661
|
-
|
|
5823
|
+
for (const issue of result.issues) {
|
|
5824
|
+
const level = issue.level.toUpperCase();
|
|
5825
|
+
lines.push(`- [${level}] ${issue.check}: ${issue.message}`);
|
|
5826
|
+
lines.push(` fix: ${issue.fix}`);
|
|
5662
5827
|
}
|
|
5828
|
+
return `${lines.join("\n")}
|
|
5829
|
+
`;
|
|
5663
5830
|
}
|
|
5664
|
-
function
|
|
5665
|
-
|
|
5666
|
-
}
|
|
5667
|
-
function resolveUserGroup(runUser) {
|
|
5668
|
-
return runCommand("id", ["-gn", runUser]).trim();
|
|
5669
|
-
}
|
|
5670
|
-
function runSystemctl(args) {
|
|
5671
|
-
runCommand("systemctl", args);
|
|
5672
|
-
}
|
|
5673
|
-
function stopAndDisableIfPresent(unitName) {
|
|
5674
|
-
runSystemctlIgnoreFailure(["disable", "--now", unitName]);
|
|
5831
|
+
async function defaultCheckCodexBinary(bin) {
|
|
5832
|
+
await execFileAsync3(bin, ["--version"]);
|
|
5675
5833
|
}
|
|
5676
|
-
function
|
|
5834
|
+
function defaultIsDirectory(targetPath) {
|
|
5677
5835
|
try {
|
|
5678
|
-
|
|
5836
|
+
return import_node_fs10.default.statSync(targetPath).isDirectory();
|
|
5679
5837
|
} catch {
|
|
5838
|
+
return false;
|
|
5680
5839
|
}
|
|
5681
5840
|
}
|
|
5682
|
-
function
|
|
5683
|
-
|
|
5684
|
-
return (0, import_node_child_process5.execFileSync)(file, args, {
|
|
5685
|
-
encoding: "utf8",
|
|
5686
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
5687
|
-
});
|
|
5688
|
-
} catch (error) {
|
|
5689
|
-
throw new Error(formatCommandError(file, args, error), { cause: error });
|
|
5690
|
-
}
|
|
5691
|
-
}
|
|
5692
|
-
function formatCommandError(file, args, error) {
|
|
5693
|
-
const command = `${file} ${args.join(" ")}`.trim();
|
|
5694
|
-
if (error && typeof error === "object") {
|
|
5695
|
-
const maybeError = error;
|
|
5696
|
-
const stderr = bufferToTrimmedString(maybeError.stderr);
|
|
5697
|
-
const stdout = bufferToTrimmedString(maybeError.stdout);
|
|
5698
|
-
const details = stderr || stdout || maybeError.message || "command failed";
|
|
5699
|
-
return `Command failed: ${command}. ${details}`;
|
|
5700
|
-
}
|
|
5701
|
-
return `Command failed: ${command}. ${String(error)}`;
|
|
5702
|
-
}
|
|
5703
|
-
function bufferToTrimmedString(value) {
|
|
5704
|
-
if (!value) {
|
|
5705
|
-
return "";
|
|
5706
|
-
}
|
|
5707
|
-
const text = typeof value === "string" ? value : value.toString("utf8");
|
|
5708
|
-
return text.trim();
|
|
5841
|
+
function readEnv(env, key) {
|
|
5842
|
+
return env[key]?.trim() ?? "";
|
|
5709
5843
|
}
|
|
5710
5844
|
|
|
5711
5845
|
// src/cli.ts
|