codeharbor 0.1.12 → 0.1.14

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.
Files changed (3) hide show
  1. package/README.md +10 -1
  2. package/dist/cli.js +689 -116
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -24,30 +24,78 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  ));
25
25
 
26
26
  // src/cli.ts
27
- var import_node_child_process6 = require("child_process");
27
+ var import_node_child_process7 = require("child_process");
28
28
  var import_node_fs11 = __toESM(require("fs"));
29
- var import_node_path12 = __toESM(require("path"));
29
+ var import_node_path14 = __toESM(require("path"));
30
30
  var import_commander = require("commander");
31
31
 
32
32
  // src/app.ts
33
- var import_node_child_process4 = require("child_process");
34
- var import_node_util2 = require("util");
33
+ var import_node_child_process5 = require("child_process");
34
+ var import_node_util3 = require("util");
35
35
 
36
36
  // src/admin-server.ts
37
- var import_node_child_process2 = require("child_process");
37
+ var import_node_child_process3 = require("child_process");
38
38
  var import_node_fs4 = __toESM(require("fs"));
39
39
  var import_node_http = __toESM(require("http"));
40
- var import_node_path4 = __toESM(require("path"));
41
- var import_node_util = require("util");
40
+ var import_node_path5 = __toESM(require("path"));
41
+ var import_node_util2 = require("util");
42
42
 
43
43
  // src/init.ts
44
44
  var import_node_fs = __toESM(require("fs"));
45
- var import_node_path = __toESM(require("path"));
45
+ var import_node_path2 = __toESM(require("path"));
46
46
  var import_promises = require("readline/promises");
47
47
  var import_dotenv = __toESM(require("dotenv"));
48
+
49
+ // src/codex-bin.ts
50
+ var import_node_child_process = require("child_process");
51
+ var import_node_os = __toESM(require("os"));
52
+ var import_node_path = __toESM(require("path"));
53
+ var import_node_util = require("util");
54
+ var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
55
+ function buildCodexBinCandidates(configuredBin, env = process.env) {
56
+ const normalized = configuredBin.trim() || "codex";
57
+ const home = env.HOME?.trim() || import_node_os.default.homedir();
58
+ const npmGlobalBin = home ? import_node_path.default.resolve(home, ".npm-global/bin/codex") : "";
59
+ const candidates = [
60
+ normalized,
61
+ "codex",
62
+ npmGlobalBin,
63
+ "/usr/bin/codex",
64
+ "/usr/local/bin/codex",
65
+ "/opt/homebrew/bin/codex"
66
+ ];
67
+ const seen = /* @__PURE__ */ new Set();
68
+ const output = [];
69
+ for (const candidate of candidates) {
70
+ const trimmed = candidate.trim();
71
+ if (!trimmed || seen.has(trimmed)) {
72
+ continue;
73
+ }
74
+ seen.add(trimmed);
75
+ output.push(trimmed);
76
+ }
77
+ return output;
78
+ }
79
+ async function findWorkingCodexBin(configuredBin, options = {}) {
80
+ const checkBinary = options.checkBinary ?? defaultCheckBinary;
81
+ const candidates = buildCodexBinCandidates(configuredBin, options.env);
82
+ for (const candidate of candidates) {
83
+ try {
84
+ await checkBinary(candidate);
85
+ return candidate;
86
+ } catch {
87
+ }
88
+ }
89
+ return null;
90
+ }
91
+ async function defaultCheckBinary(bin) {
92
+ await execFileAsync(bin, ["--version"]);
93
+ }
94
+
95
+ // src/init.ts
48
96
  async function runInitCommand(options = {}) {
49
97
  const cwd = options.cwd ?? process.cwd();
50
- const envPath = import_node_path.default.resolve(cwd, ".env");
98
+ const envPath = import_node_path2.default.resolve(cwd, ".env");
51
99
  const templatePath = resolveInitTemplatePath(cwd);
52
100
  const input = options.input ?? process.stdin;
53
101
  const output = options.output ?? process.stdout;
@@ -70,6 +118,7 @@ async function runInitCommand(options = {}) {
70
118
  output.write("CodeHarbor setup wizard\n");
71
119
  output.write(`Target file: ${envPath}
72
120
  `);
121
+ const detectedCodexBin = await findWorkingCodexBin(existingValues.CODEX_BIN ?? "codex");
73
122
  const questions = [
74
123
  {
75
124
  key: "MATRIX_HOMESERVER",
@@ -109,14 +158,14 @@ async function runInitCommand(options = {}) {
109
158
  {
110
159
  key: "CODEX_BIN",
111
160
  label: "Codex binary",
112
- fallbackValue: "codex"
161
+ fallbackValue: detectedCodexBin ?? "codex"
113
162
  },
114
163
  {
115
164
  key: "CODEX_WORKDIR",
116
165
  label: "Codex working directory",
117
166
  fallbackValue: cwd,
118
167
  validate: (value) => {
119
- const resolved = import_node_path.default.resolve(cwd, value);
168
+ const resolved = import_node_path2.default.resolve(cwd, value);
120
169
  if (!import_node_fs.default.existsSync(resolved) || !import_node_fs.default.statSync(resolved).isDirectory()) {
121
170
  return `Directory does not exist: ${resolved}`;
122
171
  }
@@ -144,8 +193,8 @@ async function runInitCommand(options = {}) {
144
193
  }
145
194
  function resolveInitTemplatePath(cwd) {
146
195
  const candidates = [
147
- import_node_path.default.resolve(cwd, ".env.example"),
148
- import_node_path.default.resolve(__dirname, "..", ".env.example")
196
+ import_node_path2.default.resolve(cwd, ".env.example"),
197
+ import_node_path2.default.resolve(__dirname, "..", ".env.example")
149
198
  ];
150
199
  for (const candidate of candidates) {
151
200
  if (import_node_fs.default.existsSync(candidate)) {
@@ -221,33 +270,33 @@ async function askYesNo(rl, question, defaultValue) {
221
270
  }
222
271
 
223
272
  // src/service-manager.ts
224
- var import_node_child_process = require("child_process");
273
+ var import_node_child_process2 = require("child_process");
225
274
  var import_node_fs3 = __toESM(require("fs"));
226
- var import_node_os2 = __toESM(require("os"));
227
- var import_node_path3 = __toESM(require("path"));
275
+ var import_node_os3 = __toESM(require("os"));
276
+ var import_node_path4 = __toESM(require("path"));
228
277
 
229
278
  // src/runtime-home.ts
230
279
  var import_node_fs2 = __toESM(require("fs"));
231
- var import_node_os = __toESM(require("os"));
232
- var import_node_path2 = __toESM(require("path"));
280
+ var import_node_os2 = __toESM(require("os"));
281
+ var import_node_path3 = __toESM(require("path"));
233
282
  var LEGACY_RUNTIME_HOME = "/opt/codeharbor";
234
283
  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);
284
+ var DEFAULT_RUNTIME_HOME = import_node_path3.default.resolve(import_node_os2.default.homedir(), USER_RUNTIME_HOME_DIR);
236
285
  var RUNTIME_HOME_ENV_KEY = "CODEHARBOR_HOME";
237
286
  function resolveRuntimeHome(env = process.env) {
238
287
  const configured = env[RUNTIME_HOME_ENV_KEY]?.trim();
239
288
  if (configured) {
240
- return import_node_path2.default.resolve(configured);
289
+ return import_node_path3.default.resolve(configured);
241
290
  }
242
- const legacyEnvPath = import_node_path2.default.resolve(LEGACY_RUNTIME_HOME, ".env");
291
+ const legacyEnvPath = import_node_path3.default.resolve(LEGACY_RUNTIME_HOME, ".env");
243
292
  if (import_node_fs2.default.existsSync(legacyEnvPath)) {
244
293
  return LEGACY_RUNTIME_HOME;
245
294
  }
246
295
  return resolveUserRuntimeHome(env);
247
296
  }
248
297
  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);
298
+ const home = env.HOME?.trim() || import_node_os2.default.homedir();
299
+ return import_node_path3.default.resolve(home, USER_RUNTIME_HOME_DIR);
251
300
  }
252
301
 
253
302
  // src/service-manager.ts
@@ -266,7 +315,7 @@ function resolveDefaultRunUser(env = process.env) {
266
315
  return user;
267
316
  }
268
317
  try {
269
- return import_node_os2.default.userInfo().username;
318
+ return import_node_os3.default.userInfo().username;
270
319
  } catch {
271
320
  return "root";
272
321
  }
@@ -274,17 +323,17 @@ function resolveDefaultRunUser(env = process.env) {
274
323
  function resolveRuntimeHomeForUser(runUser, env = process.env, explicitRuntimeHome) {
275
324
  const configuredRuntimeHome = explicitRuntimeHome?.trim() || env[RUNTIME_HOME_ENV_KEY]?.trim();
276
325
  if (configuredRuntimeHome) {
277
- return import_node_path3.default.resolve(configuredRuntimeHome);
326
+ return import_node_path4.default.resolve(configuredRuntimeHome);
278
327
  }
279
328
  const userHome = resolveUserHome(runUser);
280
329
  if (userHome) {
281
- return import_node_path3.default.resolve(userHome, USER_RUNTIME_HOME_DIR);
330
+ return import_node_path4.default.resolve(userHome, USER_RUNTIME_HOME_DIR);
282
331
  }
283
- return import_node_path3.default.resolve(import_node_os2.default.homedir(), USER_RUNTIME_HOME_DIR);
332
+ return import_node_path4.default.resolve(import_node_os3.default.homedir(), USER_RUNTIME_HOME_DIR);
284
333
  }
285
334
  function buildMainServiceUnit(options) {
286
335
  validateUnitOptions(options);
287
- const runtimeHome2 = import_node_path3.default.resolve(options.runtimeHome);
336
+ const runtimeHome2 = import_node_path4.default.resolve(options.runtimeHome);
288
337
  return [
289
338
  "[Unit]",
290
339
  "Description=CodeHarbor main service",
@@ -296,7 +345,7 @@ function buildMainServiceUnit(options) {
296
345
  `User=${options.runUser}`,
297
346
  `WorkingDirectory=${runtimeHome2}`,
298
347
  `Environment=CODEHARBOR_HOME=${runtimeHome2}`,
299
- `ExecStart=${import_node_path3.default.resolve(options.nodeBinPath)} ${import_node_path3.default.resolve(options.cliScriptPath)} start`,
348
+ `ExecStart=${import_node_path4.default.resolve(options.nodeBinPath)} ${import_node_path4.default.resolve(options.cliScriptPath)} start`,
300
349
  "Restart=always",
301
350
  "RestartSec=3",
302
351
  "NoNewPrivileges=true",
@@ -312,7 +361,7 @@ function buildMainServiceUnit(options) {
312
361
  }
313
362
  function buildAdminServiceUnit(options) {
314
363
  validateUnitOptions(options);
315
- const runtimeHome2 = import_node_path3.default.resolve(options.runtimeHome);
364
+ const runtimeHome2 = import_node_path4.default.resolve(options.runtimeHome);
316
365
  return [
317
366
  "[Unit]",
318
367
  "Description=CodeHarbor admin service",
@@ -324,7 +373,7 @@ function buildAdminServiceUnit(options) {
324
373
  `User=${options.runUser}`,
325
374
  `WorkingDirectory=${runtimeHome2}`,
326
375
  `Environment=CODEHARBOR_HOME=${runtimeHome2}`,
327
- `ExecStart=${import_node_path3.default.resolve(options.nodeBinPath)} ${import_node_path3.default.resolve(options.cliScriptPath)} admin serve`,
376
+ `ExecStart=${import_node_path4.default.resolve(options.nodeBinPath)} ${import_node_path4.default.resolve(options.cliScriptPath)} admin serve`,
328
377
  "Restart=always",
329
378
  "RestartSec=3",
330
379
  "NoNewPrivileges=true",
@@ -343,7 +392,7 @@ function buildRestartSudoersPolicy(options) {
343
392
  const systemctlPath = options.systemctlPath.trim();
344
393
  validateSimpleValue(runUser, "runUser");
345
394
  validateSimpleValue(systemctlPath, "systemctlPath");
346
- if (!import_node_path3.default.isAbsolute(systemctlPath)) {
395
+ if (!import_node_path4.default.isAbsolute(systemctlPath)) {
347
396
  throw new Error("systemctlPath must be an absolute path.");
348
397
  }
349
398
  return [
@@ -358,7 +407,7 @@ function installSystemdServices(options) {
358
407
  assertRootPrivileges();
359
408
  const output = options.output ?? process.stdout;
360
409
  const runUser = options.runUser.trim();
361
- const runtimeHome2 = import_node_path3.default.resolve(options.runtimeHome);
410
+ const runtimeHome2 = import_node_path4.default.resolve(options.runtimeHome);
362
411
  validateSimpleValue(runUser, "runUser");
363
412
  validateSimpleValue(runtimeHome2, "runtimeHome");
364
413
  validateSimpleValue(options.nodeBinPath, "nodeBinPath");
@@ -367,9 +416,9 @@ function installSystemdServices(options) {
367
416
  const runGroup = resolveUserGroup(runUser);
368
417
  import_node_fs3.default.mkdirSync(runtimeHome2, { recursive: true });
369
418
  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);
419
+ const mainPath = import_node_path4.default.join(SYSTEMD_DIR, MAIN_SERVICE_NAME);
420
+ const adminPath = import_node_path4.default.join(SYSTEMD_DIR, ADMIN_SERVICE_NAME);
421
+ const restartSudoersPath = import_node_path4.default.join(SUDOERS_DIR, RESTART_SUDOERS_FILE);
373
422
  const unitOptions = {
374
423
  runUser,
375
424
  runtimeHome: runtimeHome2,
@@ -417,9 +466,9 @@ function uninstallSystemdServices(options) {
417
466
  assertLinuxWithSystemd();
418
467
  assertRootPrivileges();
419
468
  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);
469
+ const mainPath = import_node_path4.default.join(SYSTEMD_DIR, MAIN_SERVICE_NAME);
470
+ const adminPath = import_node_path4.default.join(SYSTEMD_DIR, ADMIN_SERVICE_NAME);
471
+ const restartSudoersPath = import_node_path4.default.join(SUDOERS_DIR, RESTART_SUDOERS_FILE);
423
472
  stopAndDisableIfPresent(MAIN_SERVICE_NAME);
424
473
  if (import_node_fs3.default.existsSync(mainPath)) {
425
474
  import_node_fs3.default.unlinkSync(mainPath);
@@ -492,7 +541,7 @@ function assertLinuxWithSystemd() {
492
541
  throw new Error("Systemd service install only supports Linux.");
493
542
  }
494
543
  try {
495
- (0, import_node_child_process.execFileSync)("systemctl", ["--version"], { stdio: "ignore" });
544
+ (0, import_node_child_process2.execFileSync)("systemctl", ["--version"], { stdio: "ignore" });
496
545
  } catch {
497
546
  throw new Error("systemctl is required but not found.");
498
547
  }
@@ -539,13 +588,13 @@ function runSystemctlIgnoreFailure(args) {
539
588
  }
540
589
  function resolveSystemctlPath() {
541
590
  const candidates = [];
542
- const pathEntries = (process.env.PATH ?? "").split(import_node_path3.default.delimiter).filter(Boolean);
591
+ const pathEntries = (process.env.PATH ?? "").split(import_node_path4.default.delimiter).filter(Boolean);
543
592
  for (const entry of pathEntries) {
544
- candidates.push(import_node_path3.default.join(entry, "systemctl"));
593
+ candidates.push(import_node_path4.default.join(entry, "systemctl"));
545
594
  }
546
595
  candidates.push("/usr/bin/systemctl", "/bin/systemctl", "/usr/local/bin/systemctl");
547
596
  for (const candidate of candidates) {
548
- if (import_node_path3.default.isAbsolute(candidate) && import_node_fs3.default.existsSync(candidate)) {
597
+ if (import_node_path4.default.isAbsolute(candidate) && import_node_fs3.default.existsSync(candidate)) {
549
598
  return candidate;
550
599
  }
551
600
  }
@@ -553,7 +602,7 @@ function resolveSystemctlPath() {
553
602
  }
554
603
  function runCommand(file, args) {
555
604
  try {
556
- return (0, import_node_child_process.execFileSync)(file, args, {
605
+ return (0, import_node_child_process2.execFileSync)(file, args, {
557
606
  encoding: "utf8",
558
607
  stdio: ["ignore", "pipe", "pipe"]
559
608
  });
@@ -581,7 +630,7 @@ function bufferToTrimmedString(value) {
581
630
  }
582
631
 
583
632
  // src/admin-server.ts
584
- var execFileAsync = (0, import_node_util.promisify)(import_node_child_process2.execFile);
633
+ var execFileAsync2 = (0, import_node_util2.promisify)(import_node_child_process3.execFile);
585
634
  var HttpError = class extends Error {
586
635
  statusCode;
587
636
  constructor(statusCode, message) {
@@ -839,7 +888,7 @@ var AdminServer = class {
839
888
  updatedKeys.push("matrixCommandPrefix");
840
889
  }
841
890
  if ("codexWorkdir" in body) {
842
- const workdir = import_node_path4.default.resolve(String(body.codexWorkdir ?? "").trim());
891
+ const workdir = import_node_path5.default.resolve(String(body.codexWorkdir ?? "").trim());
843
892
  ensureDirectory(workdir, "codexWorkdir");
844
893
  this.config.codexWorkdir = workdir;
845
894
  envUpdates.CODEX_WORKDIR = workdir;
@@ -1063,8 +1112,8 @@ var AdminServer = class {
1063
1112
  return this.adminIpAllowlist.includes(normalizedRemote);
1064
1113
  }
1065
1114
  persistEnvUpdates(updates) {
1066
- const envPath = import_node_path4.default.resolve(this.cwd, ".env");
1067
- const examplePath = import_node_path4.default.resolve(this.cwd, ".env.example");
1115
+ const envPath = import_node_path5.default.resolve(this.cwd, ".env");
1116
+ const examplePath = import_node_path5.default.resolve(this.cwd, ".env.example");
1068
1117
  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") : "";
1069
1118
  const next = applyEnvOverrides(template, updates);
1070
1119
  import_node_fs4.default.writeFileSync(envPath, next, "utf8");
@@ -2219,7 +2268,7 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
2219
2268
  `;
2220
2269
  async function defaultCheckCodex(bin) {
2221
2270
  try {
2222
- const { stdout } = await execFileAsync(bin, ["--version"]);
2271
+ const { stdout } = await execFileAsync2(bin, ["--version"]);
2223
2272
  return {
2224
2273
  ok: true,
2225
2274
  version: stdout.trim() || null,
@@ -2262,8 +2311,8 @@ async function defaultCheckMatrix(homeserver, timeoutMs) {
2262
2311
 
2263
2312
  // src/channels/matrix-channel.ts
2264
2313
  var import_promises2 = __toESM(require("fs/promises"));
2265
- var import_node_os3 = __toESM(require("os"));
2266
- var import_node_path5 = __toESM(require("path"));
2314
+ var import_node_os4 = __toESM(require("os"));
2315
+ var import_node_path6 = __toESM(require("path"));
2267
2316
  var import_matrix_js_sdk = require("matrix-js-sdk");
2268
2317
 
2269
2318
  // src/utils/message.ts
@@ -2686,10 +2735,10 @@ var MatrixChannel = class {
2686
2735
  }
2687
2736
  const bytes = Buffer.from(await response.arrayBuffer());
2688
2737
  const extension = resolveFileExtension(fileName, mimeType);
2689
- const directory = import_node_path5.default.join(import_node_os3.default.tmpdir(), "codeharbor-media");
2738
+ const directory = import_node_path6.default.join(import_node_os4.default.tmpdir(), "codeharbor-media");
2690
2739
  await import_promises2.default.mkdir(directory, { recursive: true });
2691
2740
  const safeEventId = sanitizeFilename(eventId);
2692
- const targetPath = import_node_path5.default.join(directory, `${safeEventId}-${index}${extension}`);
2741
+ const targetPath = import_node_path6.default.join(directory, `${safeEventId}-${index}${extension}`);
2693
2742
  await import_promises2.default.writeFile(targetPath, bytes);
2694
2743
  return targetPath;
2695
2744
  }
@@ -2774,7 +2823,7 @@ function sanitizeFilename(value) {
2774
2823
  return value.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 80);
2775
2824
  }
2776
2825
  function resolveFileExtension(fileName, mimeType) {
2777
- const ext = import_node_path5.default.extname(fileName).trim();
2826
+ const ext = import_node_path6.default.extname(fileName).trim();
2778
2827
  if (ext) {
2779
2828
  return ext;
2780
2829
  }
@@ -2792,13 +2841,13 @@ function resolveFileExtension(fileName, mimeType) {
2792
2841
 
2793
2842
  // src/config-service.ts
2794
2843
  var import_node_fs5 = __toESM(require("fs"));
2795
- var import_node_path6 = __toESM(require("path"));
2844
+ var import_node_path7 = __toESM(require("path"));
2796
2845
  var ConfigService = class {
2797
2846
  stateStore;
2798
2847
  defaultWorkdir;
2799
2848
  constructor(stateStore, defaultWorkdir) {
2800
2849
  this.stateStore = stateStore;
2801
- this.defaultWorkdir = import_node_path6.default.resolve(defaultWorkdir);
2850
+ this.defaultWorkdir = import_node_path7.default.resolve(defaultWorkdir);
2802
2851
  }
2803
2852
  resolveRoomConfig(roomId, fallbackPolicy) {
2804
2853
  const room = this.stateStore.getRoomSettings(roomId);
@@ -2869,7 +2918,7 @@ function normalizeRoomSettingsInput(input) {
2869
2918
  if (!roomId) {
2870
2919
  throw new Error("roomId is required.");
2871
2920
  }
2872
- const workdir = import_node_path6.default.resolve(input.workdir);
2921
+ const workdir = import_node_path7.default.resolve(input.workdir);
2873
2922
  if (!import_node_fs5.default.existsSync(workdir) || !import_node_fs5.default.statSync(workdir).isDirectory()) {
2874
2923
  throw new Error(`workdir does not exist or is not a directory: ${workdir}`);
2875
2924
  }
@@ -2885,7 +2934,7 @@ function normalizeRoomSettingsInput(input) {
2885
2934
  }
2886
2935
 
2887
2936
  // src/executor/codex-executor.ts
2888
- var import_node_child_process3 = require("child_process");
2937
+ var import_node_child_process4 = require("child_process");
2889
2938
  var import_node_readline = __toESM(require("readline"));
2890
2939
  var CodexExecutionCancelledError = class extends Error {
2891
2940
  constructor(message = "codex execution cancelled") {
@@ -2903,7 +2952,7 @@ var CodexExecutor = class {
2903
2952
  }
2904
2953
  startExecution(prompt, sessionId, onProgress, startOptions) {
2905
2954
  const args = buildCodexArgs(prompt, sessionId, this.options, startOptions);
2906
- const child = (0, import_node_child_process3.spawn)(this.options.bin, args, {
2955
+ const child = (0, import_node_child_process4.spawn)(this.options.bin, args, {
2907
2956
  cwd: startOptions?.workdir ?? this.options.workdir,
2908
2957
  env: {
2909
2958
  ...process.env,
@@ -3136,17 +3185,17 @@ function stringify(value) {
3136
3185
 
3137
3186
  // src/orchestrator.ts
3138
3187
  var import_async_mutex = require("async-mutex");
3139
- var import_promises3 = __toESM(require("fs/promises"));
3188
+ var import_promises4 = __toESM(require("fs/promises"));
3140
3189
 
3141
3190
  // src/compat/cli-compat-recorder.ts
3142
3191
  var import_node_fs6 = __toESM(require("fs"));
3143
- var import_node_path7 = __toESM(require("path"));
3192
+ var import_node_path8 = __toESM(require("path"));
3144
3193
  var CliCompatRecorder = class {
3145
3194
  filePath;
3146
3195
  chain = Promise.resolve();
3147
3196
  constructor(filePath) {
3148
- this.filePath = import_node_path7.default.resolve(filePath);
3149
- import_node_fs6.default.mkdirSync(import_node_path7.default.dirname(this.filePath), { recursive: true });
3197
+ this.filePath = import_node_path8.default.resolve(filePath);
3198
+ import_node_fs6.default.mkdirSync(import_node_path8.default.dirname(this.filePath), { recursive: true });
3150
3199
  }
3151
3200
  append(entry) {
3152
3201
  const payload = `${JSON.stringify(entry)}
@@ -3623,6 +3672,296 @@ function createIdleWorkflowSnapshot() {
3623
3672
  };
3624
3673
  }
3625
3674
 
3675
+ // src/workflow/autodev.ts
3676
+ var import_promises3 = __toESM(require("fs/promises"));
3677
+ var import_node_path9 = __toESM(require("path"));
3678
+ function parseAutoDevCommand(text) {
3679
+ const normalized = text.trim();
3680
+ if (!/^\/autodev(?:\s|$)/i.test(normalized)) {
3681
+ return null;
3682
+ }
3683
+ const parts = normalized.split(/\s+/);
3684
+ if (parts.length === 1 || parts[1]?.toLowerCase() === "status") {
3685
+ return { kind: "status" };
3686
+ }
3687
+ if (parts[1]?.toLowerCase() !== "run") {
3688
+ return null;
3689
+ }
3690
+ const taskId = normalized.replace(/^\/autodev\s+run\s*/i, "").trim();
3691
+ return {
3692
+ kind: "run",
3693
+ taskId: taskId || null
3694
+ };
3695
+ }
3696
+ async function loadAutoDevContext(workdir) {
3697
+ const requirementsPath = import_node_path9.default.join(workdir, "REQUIREMENTS.md");
3698
+ const taskListPath = import_node_path9.default.join(workdir, "TASK_LIST.md");
3699
+ const requirementsContent = await readOptionalFile(requirementsPath);
3700
+ const taskListContent = await readOptionalFile(taskListPath);
3701
+ return {
3702
+ workdir,
3703
+ requirementsPath,
3704
+ taskListPath,
3705
+ requirementsContent,
3706
+ taskListContent,
3707
+ tasks: taskListContent ? parseTasks(taskListContent) : []
3708
+ };
3709
+ }
3710
+ function summarizeAutoDevTasks(tasks) {
3711
+ const summary = {
3712
+ total: tasks.length,
3713
+ pending: 0,
3714
+ inProgress: 0,
3715
+ completed: 0,
3716
+ cancelled: 0,
3717
+ blocked: 0
3718
+ };
3719
+ for (const task of tasks) {
3720
+ if (task.status === "pending") {
3721
+ summary.pending += 1;
3722
+ continue;
3723
+ }
3724
+ if (task.status === "in_progress") {
3725
+ summary.inProgress += 1;
3726
+ continue;
3727
+ }
3728
+ if (task.status === "completed") {
3729
+ summary.completed += 1;
3730
+ continue;
3731
+ }
3732
+ if (task.status === "cancelled") {
3733
+ summary.cancelled += 1;
3734
+ continue;
3735
+ }
3736
+ summary.blocked += 1;
3737
+ }
3738
+ return summary;
3739
+ }
3740
+ function selectAutoDevTask(tasks, taskId) {
3741
+ if (taskId) {
3742
+ const normalizedTarget = taskId.trim().toLowerCase();
3743
+ return tasks.find((task) => task.id.toLowerCase() === normalizedTarget) ?? null;
3744
+ }
3745
+ const inProgressTask = tasks.find((task) => task.status === "in_progress");
3746
+ if (inProgressTask) {
3747
+ return inProgressTask;
3748
+ }
3749
+ return tasks.find((task) => task.status === "pending") ?? null;
3750
+ }
3751
+ function buildAutoDevObjective(task) {
3752
+ return [
3753
+ "\u4F60\u6B63\u5728\u6267\u884C CodeHarbor AutoDev \u4EFB\u52A1\uFF0C\u8BF7\u5728\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55\u5B8C\u6210\u6307\u5B9A\u5F00\u53D1\u76EE\u6807\u3002",
3754
+ "",
3755
+ `\u4EFB\u52A1ID: ${task.id}`,
3756
+ `\u4EFB\u52A1\u63CF\u8FF0: ${task.description}`,
3757
+ "",
3758
+ "\u4E0A\u4E0B\u6587\u6587\u4EF6\uFF1A",
3759
+ "- REQUIREMENTS.md\uFF08\u9700\u6C42\u57FA\u7EBF\uFF09",
3760
+ "- TASK_LIST.md\uFF08\u4EFB\u52A1\u72B6\u6001\uFF09",
3761
+ "",
3762
+ "\u6267\u884C\u8981\u6C42\uFF1A",
3763
+ "1. \u5148\u8BFB\u53D6 REQUIREMENTS.md \u548C TASK_LIST.md\uFF0C\u786E\u8BA4\u8FB9\u754C\u4E0E\u7EA6\u675F\u3002",
3764
+ "2. \u5728\u5F53\u524D\u4ED3\u5E93\u76F4\u63A5\u5B8C\u6210\u4EE3\u7801\u4E0E\u6D4B\u8BD5\u6539\u52A8\u3002",
3765
+ "3. \u8FD0\u884C\u53D7\u5F71\u54CD\u9A8C\u8BC1\u547D\u4EE4\u5E76\u6C47\u603B\u7ED3\u679C\u3002",
3766
+ "4. \u8F93\u51FA\u6539\u52A8\u6587\u4EF6\u548C\u98CE\u9669\u8BF4\u660E\u3002"
3767
+ ].join("\n");
3768
+ }
3769
+ function formatTaskForDisplay(task) {
3770
+ return `${task.id} ${task.description} (${statusToSymbol(task.status)})`;
3771
+ }
3772
+ function statusToSymbol(status) {
3773
+ if (status === "pending") {
3774
+ return "\u2B1C";
3775
+ }
3776
+ if (status === "in_progress") {
3777
+ return "\u{1F504}";
3778
+ }
3779
+ if (status === "completed") {
3780
+ return "\u2705";
3781
+ }
3782
+ if (status === "cancelled") {
3783
+ return "\u274C";
3784
+ }
3785
+ return "\u{1F6AB}";
3786
+ }
3787
+ async function updateAutoDevTaskStatus(taskListPath, task, nextStatus) {
3788
+ const content = await import_promises3.default.readFile(taskListPath, "utf8");
3789
+ const lines = splitLines(content);
3790
+ if (task.lineIndex < 0 || task.lineIndex >= lines.length) {
3791
+ throw new Error(`task ${task.id} line index out of range`);
3792
+ }
3793
+ const updatedLine = replaceLineStatus(lines[task.lineIndex] ?? "", task.id, nextStatus);
3794
+ if (!updatedLine) {
3795
+ throw new Error(`failed to update task status for ${task.id}`);
3796
+ }
3797
+ lines[task.lineIndex] = updatedLine;
3798
+ await import_promises3.default.writeFile(taskListPath, lines.join("\n"), "utf8");
3799
+ return {
3800
+ ...task,
3801
+ status: nextStatus
3802
+ };
3803
+ }
3804
+ async function readOptionalFile(filePath) {
3805
+ try {
3806
+ return await import_promises3.default.readFile(filePath, "utf8");
3807
+ } catch (error) {
3808
+ if (error.code === "ENOENT") {
3809
+ return null;
3810
+ }
3811
+ throw error;
3812
+ }
3813
+ }
3814
+ function parseTasks(content) {
3815
+ const lines = splitLines(content);
3816
+ const tasks = [];
3817
+ for (let index = 0; index < lines.length; index += 1) {
3818
+ const line = lines[index] ?? "";
3819
+ const tableTask = parseTableTaskLine(line, index);
3820
+ if (tableTask) {
3821
+ tasks.push(tableTask);
3822
+ continue;
3823
+ }
3824
+ const listTask = parseListTaskLine(line, index);
3825
+ if (listTask) {
3826
+ tasks.push(listTask);
3827
+ }
3828
+ }
3829
+ return tasks;
3830
+ }
3831
+ function parseTableTaskLine(line, lineIndex) {
3832
+ const trimmed = line.trim();
3833
+ if (!trimmed.startsWith("|")) {
3834
+ return null;
3835
+ }
3836
+ const cells = trimmed.split("|").slice(1, -1).map((cell) => cell.trim());
3837
+ if (cells.length < 3) {
3838
+ return null;
3839
+ }
3840
+ const taskId = cells[0] ?? "";
3841
+ if (!isLikelyTaskId(taskId)) {
3842
+ return null;
3843
+ }
3844
+ const statusCell = cells[cells.length - 1] ?? "";
3845
+ const status = parseStatusToken(statusCell);
3846
+ if (!status) {
3847
+ return null;
3848
+ }
3849
+ return {
3850
+ id: taskId,
3851
+ description: cells[1] ?? taskId,
3852
+ status,
3853
+ lineIndex
3854
+ };
3855
+ }
3856
+ function parseListTaskLine(line, lineIndex) {
3857
+ const checkboxMatch = line.match(/^\s*[-*]\s+\[( |x|X)\]\s+(.+)$/);
3858
+ if (checkboxMatch) {
3859
+ const rawText2 = checkboxMatch[2]?.trim() ?? "";
3860
+ const taskId2 = extractTaskId(rawText2);
3861
+ if (!taskId2) {
3862
+ return null;
3863
+ }
3864
+ return {
3865
+ id: taskId2,
3866
+ description: stripTaskIdPrefix(rawText2, taskId2),
3867
+ status: checkboxMatch[1]?.toLowerCase() === "x" ? "completed" : "pending",
3868
+ lineIndex
3869
+ };
3870
+ }
3871
+ const symbolMatch = line.match(/^\s*[-*]\s*(⬜|🔄|✅|❌|🚫)\s+(.+)$/);
3872
+ if (!symbolMatch) {
3873
+ return null;
3874
+ }
3875
+ const rawText = symbolMatch[2]?.trim() ?? "";
3876
+ const taskId = extractTaskId(rawText);
3877
+ if (!taskId) {
3878
+ return null;
3879
+ }
3880
+ const status = parseStatusToken(symbolMatch[1] ?? "");
3881
+ if (!status) {
3882
+ return null;
3883
+ }
3884
+ return {
3885
+ id: taskId,
3886
+ description: stripTaskIdPrefix(rawText, taskId),
3887
+ status,
3888
+ lineIndex
3889
+ };
3890
+ }
3891
+ function stripTaskIdPrefix(text, taskId) {
3892
+ const normalized = text.trim();
3893
+ const escapedId = escapeRegex(taskId);
3894
+ return normalized.replace(new RegExp(`^${escapedId}[\\s:\uFF1A\\-]+`, "i"), "").trim() || normalized;
3895
+ }
3896
+ function extractTaskId(text) {
3897
+ const normalized = text.trim();
3898
+ const bracketMatch = normalized.match(/\(([A-Za-z][A-Za-z0-9._-]*)\)/);
3899
+ if (bracketMatch?.[1] && isLikelyTaskId(bracketMatch[1])) {
3900
+ return bracketMatch[1];
3901
+ }
3902
+ const token = normalized.split(/\s+/)[0]?.replace(/[,::|]+$/, "") ?? "";
3903
+ if (!isLikelyTaskId(token)) {
3904
+ return null;
3905
+ }
3906
+ return token;
3907
+ }
3908
+ function isLikelyTaskId(taskId) {
3909
+ if (!/^[A-Za-z][A-Za-z0-9._-]*$/.test(taskId)) {
3910
+ return false;
3911
+ }
3912
+ return /\d/.test(taskId);
3913
+ }
3914
+ function parseStatusToken(text) {
3915
+ const normalized = text.trim().toLowerCase();
3916
+ if (!normalized) {
3917
+ return null;
3918
+ }
3919
+ if (normalized.includes("\u2705") || normalized.includes("[x]") || normalized.includes("done")) {
3920
+ return "completed";
3921
+ }
3922
+ if (normalized.includes("\u2B1C") || normalized.includes("\u2610") || normalized.includes("[ ]") || normalized === "todo") {
3923
+ return "pending";
3924
+ }
3925
+ if (normalized.includes("\u{1F504}") || normalized.includes("\u8FDB\u884C\u4E2D") || normalized.includes("in progress")) {
3926
+ return "in_progress";
3927
+ }
3928
+ if (normalized.includes("\u274C") || normalized.includes("\u53D6\u6D88") || normalized.includes("cancel")) {
3929
+ return "cancelled";
3930
+ }
3931
+ if (normalized.includes("\u{1F6AB}") || normalized.includes("\u963B\u585E") || normalized.includes("block")) {
3932
+ return "blocked";
3933
+ }
3934
+ return null;
3935
+ }
3936
+ function replaceLineStatus(line, taskId, status) {
3937
+ const trimmed = line.trim();
3938
+ const symbol = statusToSymbol(status);
3939
+ if (trimmed.startsWith("|")) {
3940
+ const cells = trimmed.split("|").slice(1, -1).map((cell) => cell.trim());
3941
+ if (cells.length >= 3 && cells[0]?.toLowerCase() === taskId.toLowerCase()) {
3942
+ const rawParts = line.split("|");
3943
+ if (rawParts.length >= 3) {
3944
+ rawParts[rawParts.length - 2] = ` ${symbol} `;
3945
+ return rawParts.join("|");
3946
+ }
3947
+ }
3948
+ }
3949
+ if (/\[( |x|X)\]/.test(line)) {
3950
+ const checkbox = status === "completed" ? "[x]" : "[ ]";
3951
+ return line.replace(/\[( |x|X)\]/, checkbox);
3952
+ }
3953
+ if (/(⬜|🔄|✅|❌|🚫)/.test(line)) {
3954
+ return line.replace(/(⬜|🔄|✅|❌|🚫)/, symbol);
3955
+ }
3956
+ return null;
3957
+ }
3958
+ function splitLines(content) {
3959
+ return content.replace(/\r\n/g, "\n").split("\n");
3960
+ }
3961
+ function escapeRegex(value) {
3962
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3963
+ }
3964
+
3626
3965
  // src/orchestrator.ts
3627
3966
  var RequestMetrics = class {
3628
3967
  total = 0;
@@ -3710,6 +4049,7 @@ var Orchestrator = class {
3710
4049
  cliCompatRecorder;
3711
4050
  workflowRunner;
3712
4051
  workflowSnapshots = /* @__PURE__ */ new Map();
4052
+ autoDevSnapshots = /* @__PURE__ */ new Map();
3713
4053
  metrics = new RequestMetrics();
3714
4054
  lastLockPruneAt = 0;
3715
4055
  constructor(channel, executor, stateStore, logger, options) {
@@ -3804,11 +4144,17 @@ var Orchestrator = class {
3804
4144
  return;
3805
4145
  }
3806
4146
  const workflowCommand = this.workflowRunner.isEnabled() ? parseWorkflowCommand(route.prompt) : null;
4147
+ const autoDevCommand = this.workflowRunner.isEnabled() ? parseAutoDevCommand(route.prompt) : null;
3807
4148
  if (workflowCommand?.kind === "status") {
3808
4149
  await this.handleWorkflowStatusCommand(sessionKey, message);
3809
4150
  this.stateStore.markEventProcessed(sessionKey, message.eventId);
3810
4151
  return;
3811
4152
  }
4153
+ if (autoDevCommand?.kind === "status") {
4154
+ await this.handleAutoDevStatusCommand(sessionKey, message, roomConfig.workdir);
4155
+ this.stateStore.markEventProcessed(sessionKey, message.eventId);
4156
+ return;
4157
+ }
3812
4158
  const rateDecision = this.rateLimiter.tryAcquire({
3813
4159
  userId: message.senderId,
3814
4160
  roomId: message.conversationId
@@ -3857,6 +4203,37 @@ var Orchestrator = class {
3857
4203
  }
3858
4204
  return;
3859
4205
  }
4206
+ if (autoDevCommand?.kind === "run") {
4207
+ const executionStartedAt = Date.now();
4208
+ let sendDurationMs2 = 0;
4209
+ this.stateStore.activateSession(sessionKey, this.sessionActiveWindowMs);
4210
+ try {
4211
+ const sendStartedAt = Date.now();
4212
+ await this.handleAutoDevRunCommand(
4213
+ autoDevCommand.taskId,
4214
+ sessionKey,
4215
+ message,
4216
+ requestId,
4217
+ roomConfig.workdir
4218
+ );
4219
+ sendDurationMs2 += Date.now() - sendStartedAt;
4220
+ this.stateStore.markEventProcessed(sessionKey, message.eventId);
4221
+ this.metrics.record("success", queueWaitMs, Date.now() - executionStartedAt, sendDurationMs2);
4222
+ } catch (error) {
4223
+ sendDurationMs2 += await this.sendAutoDevFailure(message.conversationId, error);
4224
+ this.stateStore.commitExecutionHandled(sessionKey, message.eventId);
4225
+ const status = classifyExecutionOutcome(error);
4226
+ this.metrics.record(status, queueWaitMs, Date.now() - executionStartedAt, sendDurationMs2);
4227
+ this.logger.error("AutoDev request failed", {
4228
+ requestId,
4229
+ sessionKey,
4230
+ error: formatError2(error)
4231
+ });
4232
+ } finally {
4233
+ rateDecision.release?.();
4234
+ }
4235
+ return;
4236
+ }
3860
4237
  this.stateStore.activateSession(sessionKey, this.sessionActiveWindowMs);
3861
4238
  const previousCodexSessionId = this.stateStore.getCodexSessionId(sessionKey);
3862
4239
  const executionPrompt = this.buildExecutionPrompt(route.prompt, message);
@@ -4078,6 +4455,7 @@ var Orchestrator = class {
4078
4455
  const limiter = this.rateLimiter.snapshot();
4079
4456
  const runtime = this.sessionRuntime.getRuntimeStats();
4080
4457
  const workflow = this.workflowSnapshots.get(sessionKey) ?? createIdleWorkflowSnapshot();
4458
+ const autoDev = this.autoDevSnapshots.get(sessionKey) ?? createIdleAutoDevSnapshot();
4081
4459
  await this.channel.sendNotice(
4082
4460
  message.conversationId,
4083
4461
  `[CodeHarbor] \u5F53\u524D\u72B6\u6001
@@ -4091,7 +4469,8 @@ var Orchestrator = class {
4091
4469
  - \u5E73\u5747\u8017\u65F6: queue=${metrics.avgQueueMs}ms, exec=${metrics.avgExecMs}ms, send=${metrics.avgSendMs}ms
4092
4470
  - \u9650\u6D41\u5E76\u53D1: global=${limiter.activeGlobal}, users=${limiter.activeUsers}, rooms=${limiter.activeRooms}
4093
4471
  - CLI runtime: workers=${runtime.workerCount}, running=${runtime.runningCount}, compat_mode=${this.cliCompat.enabled ? "on" : "off"}
4094
- - Multi-Agent workflow: enabled=${this.workflowRunner.isEnabled() ? "on" : "off"}, state=${workflow.state}`
4472
+ - Multi-Agent workflow: enabled=${this.workflowRunner.isEnabled() ? "on" : "off"}, state=${workflow.state}
4473
+ - AutoDev: enabled=${this.workflowRunner.isEnabled() ? "on" : "off"}, state=${autoDev.state}, task=${autoDev.taskId ?? "N/A"}`
4095
4474
  );
4096
4475
  }
4097
4476
  async handleWorkflowStatusCommand(sessionKey, message) {
@@ -4108,11 +4487,158 @@ var Orchestrator = class {
4108
4487
  - error: ${snapshot.error ?? "N/A"}`
4109
4488
  );
4110
4489
  }
4490
+ async handleAutoDevStatusCommand(sessionKey, message, workdir) {
4491
+ const snapshot = this.autoDevSnapshots.get(sessionKey) ?? createIdleAutoDevSnapshot();
4492
+ try {
4493
+ const context = await loadAutoDevContext(workdir);
4494
+ const summary = summarizeAutoDevTasks(context.tasks);
4495
+ const nextTask = selectAutoDevTask(context.tasks);
4496
+ await this.channel.sendNotice(
4497
+ message.conversationId,
4498
+ `[CodeHarbor] AutoDev \u72B6\u6001
4499
+ - workdir: ${workdir}
4500
+ - REQUIREMENTS.md: ${context.requirementsContent ? "found" : "missing"}
4501
+ - TASK_LIST.md: ${context.taskListContent ? "found" : "missing"}
4502
+ - tasks: total=${summary.total}, pending=${summary.pending}, in_progress=${summary.inProgress}, completed=${summary.completed}, blocked=${summary.blocked}, cancelled=${summary.cancelled}
4503
+ - nextTask: ${nextTask ? formatTaskForDisplay(nextTask) : "N/A"}
4504
+ - runState: ${snapshot.state}
4505
+ - runTask: ${snapshot.taskId ? `${snapshot.taskId} ${snapshot.taskDescription ?? ""}`.trim() : "N/A"}
4506
+ - runApproved: ${snapshot.approved === null ? "N/A" : snapshot.approved ? "yes" : "no"}
4507
+ - runError: ${snapshot.error ?? "N/A"}`
4508
+ );
4509
+ } catch (error) {
4510
+ await this.channel.sendNotice(message.conversationId, `[CodeHarbor] AutoDev \u72B6\u6001\u8BFB\u53D6\u5931\u8D25: ${formatError2(error)}`);
4511
+ }
4512
+ }
4513
+ async handleAutoDevRunCommand(taskId, sessionKey, message, requestId, workdir) {
4514
+ const requestedTaskId = taskId?.trim() || null;
4515
+ const context = await loadAutoDevContext(workdir);
4516
+ if (!context.requirementsContent) {
4517
+ await this.channel.sendNotice(
4518
+ message.conversationId,
4519
+ `[CodeHarbor] AutoDev \u9700\u8981 ${context.requirementsPath}\uFF0C\u8BF7\u5148\u51C6\u5907\u9700\u6C42\u6587\u6863\u3002`
4520
+ );
4521
+ return;
4522
+ }
4523
+ if (!context.taskListContent) {
4524
+ await this.channel.sendNotice(
4525
+ message.conversationId,
4526
+ `[CodeHarbor] AutoDev \u9700\u8981 ${context.taskListPath}\uFF0C\u8BF7\u5148\u51C6\u5907\u4EFB\u52A1\u6E05\u5355\u3002`
4527
+ );
4528
+ return;
4529
+ }
4530
+ if (context.tasks.length === 0) {
4531
+ await this.channel.sendNotice(
4532
+ message.conversationId,
4533
+ "[CodeHarbor] \u672A\u5728 TASK_LIST.md \u8BC6\u522B\u5230\u4EFB\u52A1\uFF08\u9700\u5305\u542B\u4EFB\u52A1 ID \u4E0E\u72B6\u6001\u5217\uFF09\u3002"
4534
+ );
4535
+ return;
4536
+ }
4537
+ const selectedTask = selectAutoDevTask(context.tasks, requestedTaskId);
4538
+ if (!selectedTask) {
4539
+ if (requestedTaskId) {
4540
+ await this.channel.sendNotice(message.conversationId, `[CodeHarbor] \u672A\u627E\u5230\u4EFB\u52A1 ${requestedTaskId}\u3002`);
4541
+ return;
4542
+ }
4543
+ await this.channel.sendNotice(message.conversationId, "[CodeHarbor] \u5F53\u524D\u6CA1\u6709\u53EF\u6267\u884C\u4EFB\u52A1\uFF08pending/in_progress\uFF09\u3002");
4544
+ return;
4545
+ }
4546
+ if (selectedTask.status === "completed") {
4547
+ await this.channel.sendNotice(message.conversationId, `[CodeHarbor] \u4EFB\u52A1 ${selectedTask.id} \u5DF2\u5B8C\u6210\uFF08\u2705\uFF09\u3002`);
4548
+ return;
4549
+ }
4550
+ if (selectedTask.status === "cancelled") {
4551
+ await this.channel.sendNotice(message.conversationId, `[CodeHarbor] \u4EFB\u52A1 ${selectedTask.id} \u5DF2\u53D6\u6D88\uFF08\u274C\uFF09\u3002`);
4552
+ return;
4553
+ }
4554
+ let activeTask = selectedTask;
4555
+ let promotedToInProgress = false;
4556
+ if (selectedTask.status === "pending") {
4557
+ activeTask = await updateAutoDevTaskStatus(context.taskListPath, selectedTask, "in_progress");
4558
+ promotedToInProgress = true;
4559
+ }
4560
+ const startedAtIso = (/* @__PURE__ */ new Date()).toISOString();
4561
+ this.autoDevSnapshots.set(sessionKey, {
4562
+ state: "running",
4563
+ startedAt: startedAtIso,
4564
+ endedAt: null,
4565
+ taskId: activeTask.id,
4566
+ taskDescription: activeTask.description,
4567
+ approved: null,
4568
+ repairRounds: 0,
4569
+ error: null
4570
+ });
4571
+ await this.channel.sendNotice(
4572
+ message.conversationId,
4573
+ `[CodeHarbor] AutoDev \u542F\u52A8\u4EFB\u52A1 ${activeTask.id}: ${activeTask.description}`
4574
+ );
4575
+ try {
4576
+ const result = await this.handleWorkflowRunCommand(
4577
+ buildAutoDevObjective(activeTask),
4578
+ sessionKey,
4579
+ message,
4580
+ requestId,
4581
+ workdir
4582
+ );
4583
+ if (!result) {
4584
+ return;
4585
+ }
4586
+ let finalTask = activeTask;
4587
+ if (result.approved) {
4588
+ finalTask = await updateAutoDevTaskStatus(context.taskListPath, activeTask, "completed");
4589
+ }
4590
+ const endedAtIso = (/* @__PURE__ */ new Date()).toISOString();
4591
+ this.autoDevSnapshots.set(sessionKey, {
4592
+ state: "succeeded",
4593
+ startedAt: startedAtIso,
4594
+ endedAt: endedAtIso,
4595
+ taskId: finalTask.id,
4596
+ taskDescription: finalTask.description,
4597
+ approved: result.approved,
4598
+ repairRounds: result.repairRounds,
4599
+ error: null
4600
+ });
4601
+ const refreshed = await loadAutoDevContext(workdir);
4602
+ const nextTask = selectAutoDevTask(refreshed.tasks);
4603
+ await this.channel.sendNotice(
4604
+ message.conversationId,
4605
+ `[CodeHarbor] AutoDev \u4EFB\u52A1\u7ED3\u679C
4606
+ - task: ${finalTask.id}
4607
+ - reviewer approved: ${result.approved ? "yes" : "no"}
4608
+ - task status: ${statusToSymbol(finalTask.status)}
4609
+ - nextTask: ${nextTask ? formatTaskForDisplay(nextTask) : "N/A"}`
4610
+ );
4611
+ } catch (error) {
4612
+ if (promotedToInProgress) {
4613
+ try {
4614
+ await updateAutoDevTaskStatus(context.taskListPath, activeTask, "pending");
4615
+ } catch (restoreError) {
4616
+ this.logger.warn("Failed to restore AutoDev task status after failure", {
4617
+ taskId: activeTask.id,
4618
+ error: formatError2(restoreError)
4619
+ });
4620
+ }
4621
+ }
4622
+ const status = classifyExecutionOutcome(error);
4623
+ const endedAtIso = (/* @__PURE__ */ new Date()).toISOString();
4624
+ this.autoDevSnapshots.set(sessionKey, {
4625
+ state: status === "cancelled" ? "idle" : "failed",
4626
+ startedAt: startedAtIso,
4627
+ endedAt: endedAtIso,
4628
+ taskId: activeTask.id,
4629
+ taskDescription: activeTask.description,
4630
+ approved: null,
4631
+ repairRounds: 0,
4632
+ error: formatError2(error)
4633
+ });
4634
+ throw error;
4635
+ }
4636
+ }
4111
4637
  async handleWorkflowRunCommand(objective, sessionKey, message, requestId, workdir) {
4112
4638
  const normalizedObjective = objective.trim();
4113
4639
  if (!normalizedObjective) {
4114
4640
  await this.channel.sendNotice(message.conversationId, "[CodeHarbor] /agents run \u9700\u8981\u63D0\u4F9B\u4EFB\u52A1\u76EE\u6807\u3002");
4115
- return;
4641
+ return null;
4116
4642
  }
4117
4643
  const requestStartedAt = Date.now();
4118
4644
  let progressNoticeEventId = null;
@@ -4174,6 +4700,7 @@ var Orchestrator = class {
4174
4700
  });
4175
4701
  await this.channel.sendMessage(message.conversationId, buildWorkflowResultReply(result));
4176
4702
  await this.finishProgress(progressCtx, `\u591A\u667A\u80FD\u4F53\u6D41\u7A0B\u5B8C\u6210\uFF08\u8017\u65F6 ${formatDurationMs(Date.now() - requestStartedAt)}\uFF09`);
4703
+ return result;
4177
4704
  } catch (error) {
4178
4705
  const status = classifyExecutionOutcome(error);
4179
4706
  const endedAtIso = (/* @__PURE__ */ new Date()).toISOString();
@@ -4206,6 +4733,16 @@ var Orchestrator = class {
4206
4733
  await this.channel.sendMessage(conversationId, `[CodeHarbor] Multi-Agent workflow \u5931\u8D25: ${formatError2(error)}`);
4207
4734
  return Date.now() - startedAt;
4208
4735
  }
4736
+ async sendAutoDevFailure(conversationId, error) {
4737
+ const startedAt = Date.now();
4738
+ const status = classifyExecutionOutcome(error);
4739
+ if (status === "cancelled") {
4740
+ await this.channel.sendNotice(conversationId, "[CodeHarbor] AutoDev \u5DF2\u53D6\u6D88\u3002");
4741
+ return Date.now() - startedAt;
4742
+ }
4743
+ await this.channel.sendMessage(conversationId, `[CodeHarbor] AutoDev \u5931\u8D25: ${formatError2(error)}`);
4744
+ return Date.now() - startedAt;
4745
+ }
4209
4746
  async handleStopCommand(sessionKey, message, requestId) {
4210
4747
  this.stateStore.deactivateSession(sessionKey);
4211
4748
  this.stateStore.clearCodexSessionId(sessionKey);
@@ -4401,6 +4938,18 @@ ${attachmentSummary}
4401
4938
  }
4402
4939
  }
4403
4940
  };
4941
+ function createIdleAutoDevSnapshot() {
4942
+ return {
4943
+ state: "idle",
4944
+ startedAt: null,
4945
+ endedAt: null,
4946
+ taskId: null,
4947
+ taskDescription: null,
4948
+ approved: null,
4949
+ repairRounds: 0,
4950
+ error: null
4951
+ };
4952
+ }
4404
4953
  function buildSessionKey(message) {
4405
4954
  return `${message.channel}:${message.conversationId}:${message.senderId}`;
4406
4955
  }
@@ -4424,7 +4973,7 @@ async function cleanupAttachmentFiles(imagePaths) {
4424
4973
  await Promise.all(
4425
4974
  imagePaths.map(async (imagePath) => {
4426
4975
  try {
4427
- await import_promises3.default.unlink(imagePath);
4976
+ await import_promises4.default.unlink(imagePath);
4428
4977
  } catch {
4429
4978
  }
4430
4979
  })
@@ -4474,11 +5023,11 @@ function stripLeadingBotMention(text, matrixUserId) {
4474
5023
  if (!matrixUserId) {
4475
5024
  return text;
4476
5025
  }
4477
- const escapedUserId = escapeRegex(matrixUserId);
5026
+ const escapedUserId = escapeRegex2(matrixUserId);
4478
5027
  const mentionPattern = new RegExp(`^\\s*(?:<)?${escapedUserId}(?:>)?[\\s,:\uFF0C\uFF1A-]*`, "i");
4479
5028
  return text.replace(mentionPattern, "").trim();
4480
5029
  }
4481
- function escapeRegex(value) {
5030
+ function escapeRegex2(value) {
4482
5031
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4483
5032
  }
4484
5033
  function formatDurationMs(durationMs) {
@@ -4541,7 +5090,7 @@ ${result.review}
4541
5090
 
4542
5091
  // src/store/state-store.ts
4543
5092
  var import_node_fs7 = __toESM(require("fs"));
4544
- var import_node_path8 = __toESM(require("path"));
5093
+ var import_node_path10 = __toESM(require("path"));
4545
5094
  var ONE_DAY_MS = 24 * 60 * 60 * 1e3;
4546
5095
  var PRUNE_INTERVAL_MS = 5 * 60 * 1e3;
4547
5096
  var SQLITE_MODULE_ID = `node:${"sqlite"}`;
@@ -4567,7 +5116,7 @@ var StateStore = class {
4567
5116
  this.maxProcessedEventsPerSession = maxProcessedEventsPerSession;
4568
5117
  this.maxSessionAgeMs = maxSessionAgeDays * ONE_DAY_MS;
4569
5118
  this.maxSessions = maxSessions;
4570
- import_node_fs7.default.mkdirSync(import_node_path8.default.dirname(this.dbPath), { recursive: true });
5119
+ import_node_fs7.default.mkdirSync(import_node_path10.default.dirname(this.dbPath), { recursive: true });
4571
5120
  this.db = new DatabaseSync(this.dbPath);
4572
5121
  this.initializeSchema();
4573
5122
  this.importLegacyStateIfNeeded();
@@ -4938,7 +5487,7 @@ function boolToInt(value) {
4938
5487
  }
4939
5488
 
4940
5489
  // src/app.ts
4941
- var execFileAsync2 = (0, import_node_util2.promisify)(import_node_child_process4.execFile);
5490
+ var execFileAsync3 = (0, import_node_util3.promisify)(import_node_child_process5.execFile);
4942
5491
  var CodeHarborApp = class {
4943
5492
  config;
4944
5493
  logger;
@@ -5050,7 +5599,7 @@ async function runDoctor(config) {
5050
5599
  const logger = new Logger(config.logLevel);
5051
5600
  logger.info("Doctor check started");
5052
5601
  try {
5053
- const { stdout } = await execFileAsync2(config.codexBin, ["--version"]);
5602
+ const { stdout } = await execFileAsync3(config.codexBin, ["--version"]);
5054
5603
  logger.info("codex available", { version: stdout.trim() });
5055
5604
  } catch (error) {
5056
5605
  logger.error("codex unavailable", error);
@@ -5088,7 +5637,7 @@ function isNonLoopbackHost(host) {
5088
5637
 
5089
5638
  // src/config.ts
5090
5639
  var import_node_fs8 = __toESM(require("fs"));
5091
- var import_node_path9 = __toESM(require("path"));
5640
+ var import_node_path11 = __toESM(require("path"));
5092
5641
  var import_dotenv2 = __toESM(require("dotenv"));
5093
5642
  var import_zod = require("zod");
5094
5643
  var configSchema = import_zod.z.object({
@@ -5149,7 +5698,7 @@ var configSchema = import_zod.z.object({
5149
5698
  matrixCommandPrefix: v.MATRIX_COMMAND_PREFIX,
5150
5699
  codexBin: v.CODEX_BIN,
5151
5700
  codexModel: v.CODEX_MODEL?.trim() || null,
5152
- codexWorkdir: import_node_path9.default.resolve(v.CODEX_WORKDIR),
5701
+ codexWorkdir: import_node_path11.default.resolve(v.CODEX_WORKDIR),
5153
5702
  codexDangerousBypass: v.CODEX_DANGEROUS_BYPASS,
5154
5703
  codexExecTimeoutMs: v.CODEX_EXEC_TIMEOUT_MS,
5155
5704
  codexSandboxMode: v.CODEX_SANDBOX_MODE?.trim() || null,
@@ -5160,8 +5709,8 @@ var configSchema = import_zod.z.object({
5160
5709
  enabled: v.AGENT_WORKFLOW_ENABLED,
5161
5710
  autoRepairMaxRounds: v.AGENT_WORKFLOW_AUTO_REPAIR_MAX_ROUNDS
5162
5711
  },
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,
5712
+ stateDbPath: import_node_path11.default.resolve(v.STATE_DB_PATH),
5713
+ legacyStateJsonPath: v.STATE_PATH.trim() ? import_node_path11.default.resolve(v.STATE_PATH) : null,
5165
5714
  maxProcessedEventsPerSession: v.MAX_PROCESSED_EVENTS_PER_SESSION,
5166
5715
  maxSessionAgeDays: v.MAX_SESSION_AGE_DAYS,
5167
5716
  maxSessions: v.MAX_SESSIONS,
@@ -5192,7 +5741,7 @@ var configSchema = import_zod.z.object({
5192
5741
  disableReplyChunkSplit: v.CLI_COMPAT_DISABLE_REPLY_CHUNK_SPLIT,
5193
5742
  progressThrottleMs: v.CLI_COMPAT_PROGRESS_THROTTLE_MS,
5194
5743
  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
5744
+ recordPath: v.CLI_COMPAT_RECORD_PATH.trim() ? import_node_path11.default.resolve(v.CLI_COMPAT_RECORD_PATH) : null
5196
5745
  },
5197
5746
  doctorHttpTimeoutMs: v.DOCTOR_HTTP_TIMEOUT_MS,
5198
5747
  adminBindHost: v.ADMIN_BIND_HOST.trim() || "127.0.0.1",
@@ -5202,7 +5751,7 @@ var configSchema = import_zod.z.object({
5202
5751
  adminAllowedOrigins: parseCsvList(v.ADMIN_ALLOWED_ORIGINS),
5203
5752
  logLevel: v.LOG_LEVEL
5204
5753
  }));
5205
- function loadEnvFromFile(filePath = import_node_path9.default.resolve(process.cwd(), ".env"), env = process.env) {
5754
+ function loadEnvFromFile(filePath = import_node_path11.default.resolve(process.cwd(), ".env"), env = process.env) {
5206
5755
  import_dotenv2.default.config({
5207
5756
  path: filePath,
5208
5757
  processEnv: env,
@@ -5215,9 +5764,9 @@ function loadConfig(env = process.env) {
5215
5764
  const message = parsed.error.issues.map((issue) => `${issue.path.join(".") || "config"}: ${issue.message}`).join("; ");
5216
5765
  throw new Error(`Invalid configuration: ${message}`);
5217
5766
  }
5218
- import_node_fs8.default.mkdirSync(import_node_path9.default.dirname(parsed.data.stateDbPath), { recursive: true });
5767
+ import_node_fs8.default.mkdirSync(import_node_path11.default.dirname(parsed.data.stateDbPath), { recursive: true });
5219
5768
  if (parsed.data.legacyStateJsonPath) {
5220
- import_node_fs8.default.mkdirSync(import_node_path9.default.dirname(parsed.data.legacyStateJsonPath), { recursive: true });
5769
+ import_node_fs8.default.mkdirSync(import_node_path11.default.dirname(parsed.data.legacyStateJsonPath), { recursive: true });
5221
5770
  }
5222
5771
  return parsed.data;
5223
5772
  }
@@ -5291,7 +5840,7 @@ function parseCsvList(raw) {
5291
5840
 
5292
5841
  // src/config-snapshot.ts
5293
5842
  var import_node_fs9 = __toESM(require("fs"));
5294
- var import_node_path10 = __toESM(require("path"));
5843
+ var import_node_path12 = __toESM(require("path"));
5295
5844
  var import_zod2 = require("zod");
5296
5845
  var CONFIG_SNAPSHOT_SCHEMA_VERSION = 1;
5297
5846
  var CONFIG_SNAPSHOT_ENV_KEYS = [
@@ -5478,8 +6027,8 @@ async function runConfigExportCommand(options = {}) {
5478
6027
  const snapshot = buildConfigSnapshot(config, stateStore.listRoomSettings(), options.now ?? /* @__PURE__ */ new Date());
5479
6028
  const serialized = serializeConfigSnapshot(snapshot);
5480
6029
  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 });
6030
+ const targetPath = import_node_path12.default.resolve(cwd, options.outputPath);
6031
+ import_node_fs9.default.mkdirSync(import_node_path12.default.dirname(targetPath), { recursive: true });
5483
6032
  import_node_fs9.default.writeFileSync(targetPath, serialized, "utf8");
5484
6033
  output.write(`Exported config snapshot to ${targetPath}
5485
6034
  `);
@@ -5494,7 +6043,7 @@ async function runConfigImportCommand(options) {
5494
6043
  const cwd = options.cwd ?? process.cwd();
5495
6044
  const output = options.output ?? process.stdout;
5496
6045
  const actor = options.actor?.trim() || "cli:config-import";
5497
- const sourcePath = import_node_path10.default.resolve(cwd, options.filePath);
6046
+ const sourcePath = import_node_path12.default.resolve(cwd, options.filePath);
5498
6047
  if (!import_node_fs9.default.existsSync(sourcePath)) {
5499
6048
  throw new Error(`Config snapshot file not found: ${sourcePath}`);
5500
6049
  }
@@ -5525,7 +6074,7 @@ async function runConfigImportCommand(options) {
5525
6074
  synchronizeRoomSettings(stateStore, normalizedRooms);
5526
6075
  stateStore.appendConfigRevision(
5527
6076
  actor,
5528
- `import config snapshot from ${import_node_path10.default.basename(sourcePath)}`,
6077
+ `import config snapshot from ${import_node_path12.default.basename(sourcePath)}`,
5529
6078
  JSON.stringify({
5530
6079
  type: "config_snapshot_import",
5531
6080
  sourcePath,
@@ -5539,7 +6088,7 @@ async function runConfigImportCommand(options) {
5539
6088
  output.write(
5540
6089
  [
5541
6090
  `Imported config snapshot from ${sourcePath}`,
5542
- `- updated .env in ${import_node_path10.default.resolve(cwd, ".env")}`,
6091
+ `- updated .env in ${import_node_path12.default.resolve(cwd, ".env")}`,
5543
6092
  `- synchronized room settings: ${normalizedRooms.length}`,
5544
6093
  "- restart required: yes (global env settings are restart-scoped)"
5545
6094
  ].join("\n") + "\n"
@@ -5613,10 +6162,10 @@ function parseJsonFile(filePath) {
5613
6162
  function normalizeSnapshotEnv(env, cwd) {
5614
6163
  return {
5615
6164
  ...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) : ""
6165
+ CODEX_WORKDIR: import_node_path12.default.resolve(cwd, env.CODEX_WORKDIR),
6166
+ STATE_DB_PATH: import_node_path12.default.resolve(cwd, env.STATE_DB_PATH),
6167
+ STATE_PATH: env.STATE_PATH.trim() ? import_node_path12.default.resolve(cwd, env.STATE_PATH) : "",
6168
+ CLI_COMPAT_RECORD_PATH: env.CLI_COMPAT_RECORD_PATH.trim() ? import_node_path12.default.resolve(cwd, env.CLI_COMPAT_RECORD_PATH) : ""
5620
6169
  };
5621
6170
  }
5622
6171
  function normalizeSnapshotRooms(rooms, cwd) {
@@ -5631,7 +6180,7 @@ function normalizeSnapshotRooms(rooms, cwd) {
5631
6180
  throw new Error(`Duplicate roomId in snapshot: ${roomId}`);
5632
6181
  }
5633
6182
  seen.add(roomId);
5634
- const workdir = import_node_path10.default.resolve(cwd, room.workdir);
6183
+ const workdir = import_node_path12.default.resolve(cwd, room.workdir);
5635
6184
  ensureDirectory2(workdir, `room workdir (${roomId})`);
5636
6185
  normalized.push({
5637
6186
  roomId,
@@ -5658,8 +6207,8 @@ function synchronizeRoomSettings(stateStore, rooms) {
5658
6207
  }
5659
6208
  }
5660
6209
  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");
6210
+ const envPath = import_node_path12.default.resolve(cwd, ".env");
6211
+ const examplePath = import_node_path12.default.resolve(cwd, ".env.example");
5663
6212
  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
6213
  const overrides = {};
5665
6214
  for (const key of CONFIG_SNAPSHOT_ENV_KEYS) {
@@ -5722,11 +6271,11 @@ function jsonObjectStringSchema(key, allowEmpty) {
5722
6271
  }
5723
6272
 
5724
6273
  // src/preflight.ts
5725
- var import_node_child_process5 = require("child_process");
6274
+ var import_node_child_process6 = require("child_process");
5726
6275
  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);
6276
+ var import_node_path13 = __toESM(require("path"));
6277
+ var import_node_util4 = require("util");
6278
+ var execFileAsync4 = (0, import_node_util4.promisify)(import_node_child_process6.execFile);
5730
6279
  var REQUIRED_ENV_KEYS = ["MATRIX_HOMESERVER", "MATRIX_USER_ID", "MATRIX_ACCESS_TOKEN"];
5731
6280
  async function runStartupPreflight(options = {}) {
5732
6281
  const env = options.env ?? process.env;
@@ -5735,7 +6284,9 @@ async function runStartupPreflight(options = {}) {
5735
6284
  const fileExists = options.fileExists ?? import_node_fs10.default.existsSync;
5736
6285
  const isDirectory = options.isDirectory ?? defaultIsDirectory;
5737
6286
  const issues = [];
5738
- const envPath = import_node_path11.default.resolve(cwd, ".env");
6287
+ const envPath = import_node_path13.default.resolve(cwd, ".env");
6288
+ let resolvedCodexBin = null;
6289
+ let usedCodexFallback = false;
5739
6290
  if (!fileExists(envPath)) {
5740
6291
  issues.push({
5741
6292
  level: "warn",
@@ -5783,18 +6334,34 @@ async function runStartupPreflight(options = {}) {
5783
6334
  const codexBin = readEnv(env, "CODEX_BIN") || "codex";
5784
6335
  try {
5785
6336
  await checkCodexBinary(codexBin);
6337
+ resolvedCodexBin = codexBin;
5786
6338
  } 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
- });
6339
+ const fallbackBin = await findWorkingCodexBin(codexBin, { env, checkBinary: checkCodexBinary });
6340
+ if (fallbackBin && fallbackBin !== codexBin) {
6341
+ resolvedCodexBin = fallbackBin;
6342
+ usedCodexFallback = true;
6343
+ issues.push({
6344
+ level: "warn",
6345
+ code: "codex_bin_fallback",
6346
+ check: "CODEX_BIN",
6347
+ message: `Configured CODEX_BIN "${codexBin}" is unavailable; fallback to "${fallbackBin}".`,
6348
+ fix: `Update CODEX_BIN=${fallbackBin} in .env to avoid fallback probing on startup.`
6349
+ });
6350
+ } else if (fallbackBin) {
6351
+ resolvedCodexBin = fallbackBin;
6352
+ } else {
6353
+ const reason = error instanceof Error && error.message ? ` (${error.message})` : "";
6354
+ issues.push({
6355
+ level: "error",
6356
+ code: "missing_codex_bin",
6357
+ check: "CODEX_BIN",
6358
+ message: `Unable to execute "${codexBin}"${reason}.`,
6359
+ fix: `Install Codex CLI and ensure "${codexBin}" is in PATH, or set CODEX_BIN=/absolute/path/to/codex.`
6360
+ });
6361
+ }
5795
6362
  }
5796
6363
  const configuredWorkdir = readEnv(env, "CODEX_WORKDIR");
5797
- const workdir = import_node_path11.default.resolve(cwd, configuredWorkdir || cwd);
6364
+ const workdir = import_node_path13.default.resolve(cwd, configuredWorkdir || cwd);
5798
6365
  if (!fileExists(workdir) || !isDirectory(workdir)) {
5799
6366
  issues.push({
5800
6367
  level: "error",
@@ -5806,7 +6373,9 @@ async function runStartupPreflight(options = {}) {
5806
6373
  }
5807
6374
  return {
5808
6375
  ok: issues.every((issue) => issue.level !== "error"),
5809
- issues
6376
+ issues,
6377
+ resolvedCodexBin,
6378
+ usedCodexFallback
5810
6379
  };
5811
6380
  }
5812
6381
  function formatPreflightReport(result, commandName) {
@@ -5829,7 +6398,7 @@ function formatPreflightReport(result, commandName) {
5829
6398
  `;
5830
6399
  }
5831
6400
  async function defaultCheckCodexBinary(bin) {
5832
- await execFileAsync3(bin, ["--version"]);
6401
+ await execFileAsync4(bin, ["--version"]);
5833
6402
  }
5834
6403
  function defaultIsDirectory(targetPath) {
5835
6404
  try {
@@ -6015,7 +6584,11 @@ if (process.argv.length <= 2) {
6015
6584
  }
6016
6585
  void program.parseAsync(process.argv);
6017
6586
  async function loadConfigWithPreflight(commandName, runtimeHomePath) {
6018
- const preflight = await runStartupPreflight({ cwd: runtimeHomePath });
6587
+ const env = { ...process.env };
6588
+ const preflight = await runStartupPreflight({ cwd: runtimeHomePath, env });
6589
+ if (preflight.resolvedCodexBin) {
6590
+ env.CODEX_BIN = preflight.resolvedCodexBin;
6591
+ }
6019
6592
  if (preflight.issues.length > 0) {
6020
6593
  const report = formatPreflightReport(preflight, commandName);
6021
6594
  if (preflight.ok) {
@@ -6026,7 +6599,7 @@ async function loadConfigWithPreflight(commandName, runtimeHomePath) {
6026
6599
  }
6027
6600
  }
6028
6601
  try {
6029
- return loadConfig();
6602
+ return loadConfig(env);
6030
6603
  } catch (error) {
6031
6604
  const message = error instanceof Error ? error.message : String(error);
6032
6605
  process.stderr.write(`Configuration error: ${message}
@@ -6056,7 +6629,7 @@ function ensureRuntimeHomeOrExit() {
6056
6629
  `);
6057
6630
  process.exit(1);
6058
6631
  }
6059
- loadEnvFromFile(import_node_path12.default.resolve(home, ".env"));
6632
+ loadEnvFromFile(import_node_path14.default.resolve(home, ".env"));
6060
6633
  runtimeHome = home;
6061
6634
  return runtimeHome;
6062
6635
  }
@@ -6071,7 +6644,7 @@ function parsePortOption(raw, fallback) {
6071
6644
  }
6072
6645
  function resolveCliVersion() {
6073
6646
  try {
6074
- const packagePath = import_node_path12.default.resolve(__dirname, "..", "package.json");
6647
+ const packagePath = import_node_path14.default.resolve(__dirname, "..", "package.json");
6075
6648
  const content = import_node_fs11.default.readFileSync(packagePath, "utf8");
6076
6649
  const parsed = JSON.parse(content);
6077
6650
  return typeof parsed.version === "string" && parsed.version.trim() ? parsed.version : "0.0.0";
@@ -6082,9 +6655,9 @@ function resolveCliVersion() {
6082
6655
  function resolveCliScriptPath() {
6083
6656
  const argvPath = process.argv[1];
6084
6657
  if (argvPath && argvPath.trim()) {
6085
- return import_node_path12.default.resolve(argvPath);
6658
+ return import_node_path14.default.resolve(argvPath);
6086
6659
  }
6087
- return import_node_path12.default.resolve(__dirname, "cli.js");
6660
+ return import_node_path14.default.resolve(__dirname, "cli.js");
6088
6661
  }
6089
6662
  function maybeReexecServiceCommandWithSudo() {
6090
6663
  if (typeof process.getuid !== "function" || process.getuid() === 0) {
@@ -6095,7 +6668,7 @@ function maybeReexecServiceCommandWithSudo() {
6095
6668
  return;
6096
6669
  }
6097
6670
  const cliScriptPath = resolveCliScriptPath();
6098
- const child = (0, import_node_child_process6.spawnSync)("sudo", [process.execPath, cliScriptPath, ...serviceArgs], {
6671
+ const child = (0, import_node_child_process7.spawnSync)("sudo", [process.execPath, cliScriptPath, ...serviceArgs], {
6099
6672
  stdio: "inherit"
6100
6673
  });
6101
6674
  if (child.error) {