copilot-hub 0.1.19 → 0.1.21

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 (53) hide show
  1. package/README.md +3 -2
  2. package/apps/agent-engine/dist/config.js +58 -0
  3. package/apps/agent-engine/dist/index.js +90 -16
  4. package/apps/control-plane/dist/channels/codex-quota-cache.js +16 -0
  5. package/apps/control-plane/dist/channels/hub-model-utils.js +244 -24
  6. package/apps/control-plane/dist/channels/hub-ops-commands.js +631 -279
  7. package/apps/control-plane/dist/channels/telegram-channel.js +5 -7
  8. package/apps/control-plane/dist/config.js +58 -0
  9. package/apps/control-plane/dist/index.js +16 -0
  10. package/apps/control-plane/dist/test/hub-model-utils.test.js +110 -13
  11. package/package.json +3 -2
  12. package/packages/core/dist/agent-supervisor.d.ts +5 -0
  13. package/packages/core/dist/agent-supervisor.js +11 -0
  14. package/packages/core/dist/agent-supervisor.js.map +1 -1
  15. package/packages/core/dist/bot-manager.js +17 -1
  16. package/packages/core/dist/bot-manager.js.map +1 -1
  17. package/packages/core/dist/bot-runtime.d.ts +4 -0
  18. package/packages/core/dist/bot-runtime.js +5 -1
  19. package/packages/core/dist/bot-runtime.js.map +1 -1
  20. package/packages/core/dist/codex-app-client.d.ts +13 -2
  21. package/packages/core/dist/codex-app-client.js +51 -13
  22. package/packages/core/dist/codex-app-client.js.map +1 -1
  23. package/packages/core/dist/codex-app-utils.d.ts +6 -0
  24. package/packages/core/dist/codex-app-utils.js +49 -0
  25. package/packages/core/dist/codex-app-utils.js.map +1 -1
  26. package/packages/core/dist/codex-provider.d.ts +3 -1
  27. package/packages/core/dist/codex-provider.js +3 -1
  28. package/packages/core/dist/codex-provider.js.map +1 -1
  29. package/packages/core/dist/kernel-control-plane.d.ts +1 -0
  30. package/packages/core/dist/kernel-control-plane.js +132 -13
  31. package/packages/core/dist/kernel-control-plane.js.map +1 -1
  32. package/packages/core/dist/provider-factory.d.ts +2 -0
  33. package/packages/core/dist/provider-factory.js +3 -0
  34. package/packages/core/dist/provider-factory.js.map +1 -1
  35. package/packages/core/dist/provider-options.js +24 -17
  36. package/packages/core/dist/provider-options.js.map +1 -1
  37. package/packages/core/dist/state-store.d.ts +1 -0
  38. package/packages/core/dist/state-store.js +28 -2
  39. package/packages/core/dist/state-store.js.map +1 -1
  40. package/packages/core/dist/telegram-channel.d.ts +1 -0
  41. package/packages/core/dist/telegram-channel.js +3 -0
  42. package/packages/core/dist/telegram-channel.js.map +1 -1
  43. package/scripts/dist/cli.mjs +132 -203
  44. package/scripts/dist/codex-runtime.mjs +352 -0
  45. package/scripts/dist/codex-version.mjs +91 -0
  46. package/scripts/dist/configure.mjs +26 -49
  47. package/scripts/dist/daemon.mjs +58 -0
  48. package/scripts/src/cli.mts +166 -233
  49. package/scripts/src/codex-runtime.mts +499 -0
  50. package/scripts/src/codex-version.mts +114 -0
  51. package/scripts/src/configure.mts +30 -65
  52. package/scripts/src/daemon.mts +69 -0
  53. package/scripts/test/codex-version.test.mjs +21 -0
@@ -0,0 +1,352 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import process from "node:process";
4
+ import { spawnSync } from "node:child_process";
5
+ import { compareSemver, codexVersionRequirementLabel, extractSemver, isCodexVersionCompatible, } from "./codex-version.mjs";
6
+ export function resolveCodexBinForStart({ repoRoot, agentEngineEnvPath, controlPlaneEnvPath, env = process.env, }) {
7
+ const fromEnv = nonEmpty(env.CODEX_BIN);
8
+ if (fromEnv) {
9
+ return buildResolvedCodexBin({
10
+ value: fromEnv,
11
+ source: "process_env",
12
+ env,
13
+ repoRoot,
14
+ });
15
+ }
16
+ for (const [source, envPath] of [
17
+ ["agent_env", agentEngineEnvPath],
18
+ ["control_plane_env", controlPlaneEnvPath],
19
+ ]) {
20
+ const value = readEnvValue(envPath, "CODEX_BIN");
21
+ if (value) {
22
+ return buildResolvedCodexBin({
23
+ value,
24
+ source,
25
+ env,
26
+ repoRoot,
27
+ });
28
+ }
29
+ }
30
+ const detected = findDetectedCodexBin(env, repoRoot);
31
+ if (detected) {
32
+ return {
33
+ bin: detected,
34
+ source: "detected",
35
+ userConfigured: false,
36
+ };
37
+ }
38
+ return {
39
+ bin: "codex",
40
+ source: "default",
41
+ userConfigured: false,
42
+ };
43
+ }
44
+ export function resolveCompatibleInstalledCodexBin({ repoRoot, env = process.env, }) {
45
+ const matches = [];
46
+ for (const candidate of listCodexBinCandidates(env, repoRoot)) {
47
+ const probe = probeCodexVersion({
48
+ codexBin: candidate,
49
+ repoRoot,
50
+ });
51
+ if (!probe.ok || !probe.compatible) {
52
+ continue;
53
+ }
54
+ matches.push({
55
+ candidate,
56
+ version: probe.version,
57
+ priority: getCodexCandidatePriority(candidate, env, repoRoot),
58
+ });
59
+ }
60
+ if (matches.length === 0) {
61
+ return "";
62
+ }
63
+ matches.sort((left, right) => {
64
+ const versionOrder = compareSemver(right.version, left.version);
65
+ if (versionOrder !== 0) {
66
+ return versionOrder;
67
+ }
68
+ return left.priority - right.priority;
69
+ });
70
+ return matches[0]?.candidate ?? "";
71
+ }
72
+ export function probeCodexVersion({ codexBin, repoRoot, }) {
73
+ const status = runCodex({
74
+ codexBin,
75
+ args: ["--version"],
76
+ repoRoot,
77
+ });
78
+ if (!status.ok) {
79
+ return {
80
+ ...status,
81
+ version: "",
82
+ rawVersion: "",
83
+ compatible: false,
84
+ };
85
+ }
86
+ const rawVersion = firstLine(status.stdout) || firstLine(status.stderr);
87
+ const version = extractSemver(rawVersion);
88
+ if (!version) {
89
+ return {
90
+ ok: false,
91
+ stdout: status.stdout,
92
+ stderr: status.stderr,
93
+ errorMessage: `Could not parse Codex version from '${rawVersion || "empty output"}'.`,
94
+ errorCode: "INVALID_VERSION",
95
+ version: "",
96
+ rawVersion,
97
+ compatible: false,
98
+ };
99
+ }
100
+ return {
101
+ ...status,
102
+ version,
103
+ rawVersion,
104
+ compatible: isCodexVersionCompatible(version),
105
+ };
106
+ }
107
+ export function buildCodexCompatibilitySummary({ resolved, probe, }) {
108
+ if (probe.ok) {
109
+ return `Codex binary '${resolved.bin}' is version ${probe.version}.`;
110
+ }
111
+ if (probe.errorCode === "ENOENT") {
112
+ return `Codex binary '${resolved.bin}' was not found.`;
113
+ }
114
+ return probe.errorMessage || `Codex binary '${resolved.bin}' is not usable.`;
115
+ }
116
+ export function buildCodexCompatibilityNotice({ resolved, probe, }) {
117
+ return [
118
+ buildCodexCompatibilitySummary({ resolved, probe }),
119
+ `copilot-hub requires Codex CLI ${codexVersionRequirementLabel}.`,
120
+ ].join("\n");
121
+ }
122
+ export function buildCodexCompatibilityError({ resolved, probe, includeInstallHint, installCommand, }) {
123
+ const lines = [
124
+ buildCodexCompatibilitySummary({ resolved, probe }),
125
+ `copilot-hub requires Codex CLI ${codexVersionRequirementLabel}.`,
126
+ ];
127
+ if (includeInstallHint) {
128
+ lines.push(`Install a compatible version with '${installCommand}', then retry.`);
129
+ }
130
+ else {
131
+ lines.push("Update that binary or point CODEX_BIN to a compatible executable, then retry.");
132
+ }
133
+ return lines.join("\n");
134
+ }
135
+ function buildResolvedCodexBin({ value, source, env, repoRoot, }) {
136
+ const normalized = String(value ?? "")
137
+ .trim()
138
+ .toLowerCase();
139
+ if (normalized && normalized !== "codex") {
140
+ return {
141
+ bin: value,
142
+ source,
143
+ userConfigured: true,
144
+ };
145
+ }
146
+ const detected = findDetectedCodexBin(env, repoRoot);
147
+ return {
148
+ bin: detected || "codex",
149
+ source,
150
+ userConfigured: false,
151
+ };
152
+ }
153
+ function findDetectedCodexBin(env, repoRoot) {
154
+ if (process.platform !== "win32") {
155
+ return "";
156
+ }
157
+ return findWindowsNpmGlobalCodexBin(env, repoRoot) || findVscodeCodexExe(env) || "";
158
+ }
159
+ function listCodexBinCandidates(env, repoRoot) {
160
+ return dedupe(["codex", findWindowsNpmGlobalCodexBin(env, repoRoot), findVscodeCodexExe(env)]);
161
+ }
162
+ function getCodexCandidatePriority(candidate, env, repoRoot) {
163
+ if (candidate === "codex") {
164
+ return 0;
165
+ }
166
+ const npmGlobal = findWindowsNpmGlobalCodexBin(env, repoRoot);
167
+ if (npmGlobal && candidate === npmGlobal) {
168
+ return 1;
169
+ }
170
+ const vscode = findVscodeCodexExe(env);
171
+ if (vscode && candidate === vscode) {
172
+ return 2;
173
+ }
174
+ return 3;
175
+ }
176
+ function findVscodeCodexExe(env) {
177
+ const userProfile = nonEmpty(env.USERPROFILE);
178
+ if (!userProfile) {
179
+ return "";
180
+ }
181
+ const extensionsDir = path.join(userProfile, ".vscode", "extensions");
182
+ if (!fs.existsSync(extensionsDir)) {
183
+ return "";
184
+ }
185
+ const candidates = fs
186
+ .readdirSync(extensionsDir, { withFileTypes: true })
187
+ .filter((entry) => entry.isDirectory())
188
+ .map((entry) => entry.name)
189
+ .filter((name) => name.startsWith("openai.chatgpt-"))
190
+ .sort()
191
+ .reverse();
192
+ for (const folder of candidates) {
193
+ const exePath = path.join(extensionsDir, folder, "bin", "windows-x86_64", "codex.exe");
194
+ if (fs.existsSync(exePath)) {
195
+ return exePath;
196
+ }
197
+ }
198
+ return "";
199
+ }
200
+ function findWindowsNpmGlobalCodexBin(env, repoRoot) {
201
+ if (process.platform !== "win32") {
202
+ return "";
203
+ }
204
+ const candidates = [];
205
+ const appData = nonEmpty(env.APPDATA);
206
+ if (appData) {
207
+ candidates.push(path.join(appData, "npm", "codex.cmd"));
208
+ candidates.push(path.join(appData, "npm", "codex.exe"));
209
+ candidates.push(path.join(appData, "npm", "codex"));
210
+ }
211
+ const npmPrefix = readNpmPrefix(repoRoot);
212
+ if (npmPrefix) {
213
+ candidates.push(path.join(npmPrefix, "codex.cmd"));
214
+ candidates.push(path.join(npmPrefix, "codex.exe"));
215
+ candidates.push(path.join(npmPrefix, "codex"));
216
+ }
217
+ for (const candidate of dedupe(candidates)) {
218
+ if (fs.existsSync(candidate)) {
219
+ return candidate;
220
+ }
221
+ }
222
+ return "";
223
+ }
224
+ function readNpmPrefix(repoRoot) {
225
+ const result = spawnNpm(["config", "get", "prefix"], repoRoot);
226
+ if (result.error || result.status !== 0) {
227
+ return "";
228
+ }
229
+ const value = String(result.stdout ?? "").trim();
230
+ if (!value || value.toLowerCase() === "undefined") {
231
+ return "";
232
+ }
233
+ return value;
234
+ }
235
+ function runCodex({ codexBin, args, repoRoot, }) {
236
+ const result = spawnCodex(codexBin, args, repoRoot);
237
+ if (result.error) {
238
+ return {
239
+ ok: false,
240
+ stdout: "",
241
+ stderr: "",
242
+ errorMessage: formatCodexSpawnError(codexBin, result.error),
243
+ errorCode: normalizeErrorCode(result.error),
244
+ };
245
+ }
246
+ const code = Number.isInteger(result.status) ? result.status : 1;
247
+ return {
248
+ ok: code === 0,
249
+ stdout: String(result.stdout ?? "").trim(),
250
+ stderr: String(result.stderr ?? "").trim(),
251
+ errorMessage: "",
252
+ errorCode: "",
253
+ };
254
+ }
255
+ function spawnCodex(codexBin, args, repoRoot) {
256
+ if (process.platform === "win32" && /\.(cmd|bat)$/i.test(codexBin)) {
257
+ const commandLine = [
258
+ quoteWindowsShellValue(codexBin),
259
+ ...args.map(quoteWindowsShellValue),
260
+ ].join(" ");
261
+ return spawnSync(commandLine, {
262
+ cwd: repoRoot,
263
+ stdio: ["ignore", "pipe", "pipe"],
264
+ shell: true,
265
+ encoding: "utf8",
266
+ });
267
+ }
268
+ return spawnSync(codexBin, args, {
269
+ cwd: repoRoot,
270
+ stdio: ["ignore", "pipe", "pipe"],
271
+ shell: false,
272
+ encoding: "utf8",
273
+ });
274
+ }
275
+ function readEnvValue(filePath, key) {
276
+ if (!fs.existsSync(filePath)) {
277
+ return "";
278
+ }
279
+ const lines = fs.readFileSync(filePath, "utf8").split(/\r?\n/);
280
+ const pattern = new RegExp(`^\\s*${escapeRegex(key)}\\s*=\\s*(.*)\\s*$`);
281
+ for (const line of lines) {
282
+ const match = line.match(pattern);
283
+ if (!match) {
284
+ continue;
285
+ }
286
+ return unquote(match[1]);
287
+ }
288
+ return "";
289
+ }
290
+ function unquote(value) {
291
+ const raw = String(value ?? "").trim();
292
+ if (!raw) {
293
+ return "";
294
+ }
295
+ if ((raw.startsWith('"') && raw.endsWith('"')) || (raw.startsWith("'") && raw.endsWith("'"))) {
296
+ return raw.slice(1, -1).trim();
297
+ }
298
+ return raw;
299
+ }
300
+ function escapeRegex(value) {
301
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
302
+ }
303
+ function nonEmpty(value) {
304
+ const normalized = String(value ?? "").trim();
305
+ return normalized || "";
306
+ }
307
+ function firstLine(value) {
308
+ return (String(value ?? "")
309
+ .split(/\r?\n/)
310
+ .map((line) => line.trim())
311
+ .find(Boolean) ?? "");
312
+ }
313
+ function formatCodexSpawnError(command, error) {
314
+ const code = normalizeErrorCode(error);
315
+ if (code === "ENOENT") {
316
+ return `Codex binary '${command}' was not found. Install Codex CLI or set CODEX_BIN.`;
317
+ }
318
+ if (code === "EPERM") {
319
+ return `Codex binary '${command}' cannot be executed (EPERM). Check permissions or CODEX_BIN.`;
320
+ }
321
+ const message = error instanceof Error ? error.message : String(error);
322
+ return `Failed to execute '${command}': ${firstLine(message)}`;
323
+ }
324
+ function normalizeErrorCode(error) {
325
+ return String(error?.code ?? "")
326
+ .trim()
327
+ .toUpperCase();
328
+ }
329
+ function dedupe(values) {
330
+ return [...new Set(values.map((value) => String(value ?? "").trim()).filter(Boolean))];
331
+ }
332
+ function quoteWindowsShellValue(value) {
333
+ return `"${String(value ?? "").replace(/"/g, '\\"')}"`;
334
+ }
335
+ function spawnNpm(args, repoRoot) {
336
+ if (process.platform === "win32") {
337
+ const comspec = process.env.ComSpec || "cmd.exe";
338
+ const commandLine = ["npm", ...args].join(" ");
339
+ return spawnSync(comspec, ["/d", "/s", "/c", commandLine], {
340
+ cwd: repoRoot,
341
+ stdio: ["ignore", "pipe", "pipe"],
342
+ shell: false,
343
+ encoding: "utf8",
344
+ });
345
+ }
346
+ return spawnSync("npm", args, {
347
+ cwd: repoRoot,
348
+ stdio: ["ignore", "pipe", "pipe"],
349
+ shell: false,
350
+ encoding: "utf8",
351
+ });
352
+ }
@@ -0,0 +1,91 @@
1
+ export const codexNpmPackage = "@openai/codex";
2
+ export const minimumCodexVersion = "0.113.0";
3
+ export const maximumCodexVersionExclusive = "0.114.0";
4
+ export const codexInstallPackageSpec = `${codexNpmPackage}@${minimumCodexVersion}`;
5
+ export const codexVersionRequirementLabel = `>= ${minimumCodexVersion} < ${maximumCodexVersionExclusive}`;
6
+ export function extractSemver(value) {
7
+ const match = String(value ?? "").match(/\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?/);
8
+ return String(match?.[0] ?? "").trim();
9
+ }
10
+ export function compareSemver(left, right) {
11
+ const a = parseSemver(left);
12
+ const b = parseSemver(right);
13
+ if (!a || !b) {
14
+ return 0;
15
+ }
16
+ if (a.major !== b.major) {
17
+ return a.major > b.major ? 1 : -1;
18
+ }
19
+ if (a.minor !== b.minor) {
20
+ return a.minor > b.minor ? 1 : -1;
21
+ }
22
+ if (a.patch !== b.patch) {
23
+ return a.patch > b.patch ? 1 : -1;
24
+ }
25
+ if (a.prerelease.length === 0 && b.prerelease.length === 0) {
26
+ return 0;
27
+ }
28
+ if (a.prerelease.length === 0) {
29
+ return 1;
30
+ }
31
+ if (b.prerelease.length === 0) {
32
+ return -1;
33
+ }
34
+ const maxLength = Math.max(a.prerelease.length, b.prerelease.length);
35
+ for (let index = 0; index < maxLength; index += 1) {
36
+ const leftPart = a.prerelease[index];
37
+ const rightPart = b.prerelease[index];
38
+ if (leftPart === undefined) {
39
+ return -1;
40
+ }
41
+ if (rightPart === undefined) {
42
+ return 1;
43
+ }
44
+ if (leftPart === rightPart) {
45
+ continue;
46
+ }
47
+ const leftNumeric = /^\d+$/.test(leftPart);
48
+ const rightNumeric = /^\d+$/.test(rightPart);
49
+ if (leftNumeric && rightNumeric) {
50
+ const leftValue = Number.parseInt(leftPart, 10);
51
+ const rightValue = Number.parseInt(rightPart, 10);
52
+ if (leftValue !== rightValue) {
53
+ return leftValue > rightValue ? 1 : -1;
54
+ }
55
+ continue;
56
+ }
57
+ if (leftNumeric) {
58
+ return -1;
59
+ }
60
+ if (rightNumeric) {
61
+ return 1;
62
+ }
63
+ return leftPart > rightPart ? 1 : -1;
64
+ }
65
+ return 0;
66
+ }
67
+ export function isCodexVersionCompatible(version) {
68
+ const parsed = parseSemver(version);
69
+ if (!parsed || parsed.prerelease.length > 0) {
70
+ return false;
71
+ }
72
+ return (compareSemver(version, minimumCodexVersion) >= 0 &&
73
+ compareSemver(version, maximumCodexVersionExclusive) < 0);
74
+ }
75
+ function parseSemver(value) {
76
+ const normalized = extractSemver(value);
77
+ const match = normalized.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/);
78
+ if (!match) {
79
+ return null;
80
+ }
81
+ const prerelease = String(match[4] ?? "")
82
+ .split(".")
83
+ .map((part) => part.trim())
84
+ .filter(Boolean);
85
+ return {
86
+ major: Number.parseInt(match[1] ?? "", 10),
87
+ minor: Number.parseInt(match[2] ?? "", 10),
88
+ patch: Number.parseInt(match[3] ?? "", 10),
89
+ prerelease,
90
+ };
91
+ }
@@ -12,6 +12,7 @@ const engineExamplePath = path.join(repoRoot, "apps", "agent-engine", ".env.exam
12
12
  const controlPlaneEnvPath = path.join(repoRoot, "apps", "control-plane", ".env");
13
13
  const controlPlaneExamplePath = path.join(repoRoot, "apps", "control-plane", ".env.example");
14
14
  const TELEGRAM_TOKEN_PATTERN = /^\d{5,}:[A-Za-z0-9_-]{20,}$/;
15
+ const DEFAULT_CONTROL_PLANE_TOKEN_ENV = "HUB_TELEGRAM_TOKEN";
15
16
  const args = new Set(process.argv.slice(2));
16
17
  const requiredOnly = args.has("--required-only");
17
18
  await main();
@@ -26,9 +27,8 @@ async function main() {
26
27
  await configureRequiredTokens({ rl, controlPlaneLines });
27
28
  }
28
29
  else {
29
- await configureAll({ rl, engineLines, controlPlaneLines });
30
+ await configureAll({ rl, controlPlaneLines });
30
31
  console.log("\nSaved:");
31
- console.log(`- ${relativeFromRepo(engineEnvPath)}`);
32
32
  console.log(`- ${relativeFromRepo(controlPlaneEnvPath)}`);
33
33
  console.log("\nNext step:");
34
34
  console.log("1) npm run start");
@@ -42,7 +42,7 @@ async function main() {
42
42
  }
43
43
  async function configureRequiredTokens({ rl, controlPlaneLines }) {
44
44
  const controlPlaneMap = parseEnvMap(controlPlaneLines);
45
- const controlPlaneTokenEnvName = nonEmpty(controlPlaneMap.HUB_TELEGRAM_TOKEN_ENV, "HUB_TELEGRAM_TOKEN");
45
+ const controlPlaneTokenEnvName = nonEmpty(controlPlaneMap.HUB_TELEGRAM_TOKEN_ENV, DEFAULT_CONTROL_PLANE_TOKEN_ENV);
46
46
  setEnvValue(controlPlaneLines, "HUB_TELEGRAM_TOKEN_ENV", controlPlaneTokenEnvName);
47
47
  const postControlPlaneMap = parseEnvMap(controlPlaneLines);
48
48
  const currentToken = String(postControlPlaneMap[controlPlaneTokenEnvName] ?? "").trim();
@@ -54,35 +54,24 @@ async function configureRequiredTokens({ rl, controlPlaneLines }) {
54
54
  throw new Error("Missing required tokens and no interactive terminal. Run 'npm run configure'.");
55
55
  }
56
56
  console.log("Missing or invalid hub token. Please enter a valid Telegram bot token.");
57
- const value = await askRequiredTelegramToken(rl, `Token value for ${controlPlaneTokenEnvName} (control-plane)`);
57
+ const value = await askRequiredTelegramToken(rl, "Control-plane Telegram token");
58
58
  setEnvValue(controlPlaneLines, controlPlaneTokenEnvName, value);
59
59
  console.log("Required token saved.");
60
60
  }
61
- async function configureAll({ rl, engineLines, controlPlaneLines }) {
62
- const engineMap = parseEnvMap(engineLines);
61
+ async function configureAll({ rl, controlPlaneLines }) {
63
62
  const controlPlaneMap = parseEnvMap(controlPlaneLines);
64
- console.log("\nCopilot Hub token configuration\n");
65
- const controlPlaneTokenEnvDefault = nonEmpty(controlPlaneMap.HUB_TELEGRAM_TOKEN_ENV, "HUB_TELEGRAM_TOKEN");
66
- const controlPlaneTokenEnvName = await ask(rl, "control-plane token variable", controlPlaneTokenEnvDefault);
67
- setEnvValue(controlPlaneLines, "HUB_TELEGRAM_TOKEN_ENV", controlPlaneTokenEnvName);
68
- const currentControlPlaneToken = parseEnvMap(controlPlaneLines)[controlPlaneTokenEnvName] ?? "";
69
- const newControlPlaneToken = await ask(rl, `Token value for ${controlPlaneTokenEnvName} (control-plane, Enter to keep current)`, "");
63
+ console.log("\nCopilot Hub control-plane configuration\n");
64
+ const controlPlaneTokenEnvDefault = nonEmpty(controlPlaneMap.HUB_TELEGRAM_TOKEN_ENV, DEFAULT_CONTROL_PLANE_TOKEN_ENV);
65
+ setEnvValue(controlPlaneLines, "HUB_TELEGRAM_TOKEN_ENV", controlPlaneTokenEnvDefault);
66
+ const currentControlPlaneToken = String(parseEnvMap(controlPlaneLines)[controlPlaneTokenEnvDefault] ?? "").trim();
67
+ const newControlPlaneToken = currentControlPlaneToken
68
+ ? await askTelegramToken(rl, "Control-plane Telegram token (press Enter to keep current)", true)
69
+ : await askRequiredTelegramToken(rl, "Control-plane Telegram token");
70
70
  if (newControlPlaneToken) {
71
- setEnvValue(controlPlaneLines, controlPlaneTokenEnvName, newControlPlaneToken);
71
+ setEnvValue(controlPlaneLines, controlPlaneTokenEnvDefault, newControlPlaneToken);
72
72
  }
73
- else if (!currentControlPlaneToken) {
74
- console.log(`- No value set for ${controlPlaneTokenEnvName} yet.`);
75
- }
76
- const configureAgentToken = await askYesNo(rl, "Configure TELEGRAM_TOKEN_AGENT_1 now?", true);
77
- if (configureAgentToken) {
78
- const currentAgentToken = engineMap.TELEGRAM_TOKEN_AGENT_1 ?? "";
79
- const newAgentToken = await ask(rl, "Token value for TELEGRAM_TOKEN_AGENT_1 (agent-engine, Enter to keep current)", "");
80
- if (newAgentToken) {
81
- setEnvValue(engineLines, "TELEGRAM_TOKEN_AGENT_1", newAgentToken);
82
- }
83
- else if (!currentAgentToken) {
84
- console.log("- No value set for TELEGRAM_TOKEN_AGENT_1 yet.");
85
- }
73
+ else {
74
+ console.log("- Control-plane token left unchanged.");
86
75
  }
87
76
  }
88
77
  function ensureEnvFile(envPath, examplePath) {
@@ -157,14 +146,6 @@ function nonEmpty(value, fallback) {
157
146
  function escapeRegex(value) {
158
147
  return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
159
148
  }
160
- async function ask(rl, label, fallback) {
161
- const value = await rl.question(`${label}${fallback ? ` [${fallback}]` : ""}: `);
162
- const normalized = String(value ?? "").trim();
163
- if (!normalized) {
164
- return String(fallback ?? "").trim();
165
- }
166
- return normalized;
167
- }
168
149
  async function askRequired(rl, label) {
169
150
  while (true) {
170
151
  const value = await rl.question(`${label}: `);
@@ -184,22 +165,18 @@ async function askRequiredTelegramToken(rl, label) {
184
165
  console.log("Token format looks invalid. Expected format like: 123456789:AA...");
185
166
  }
186
167
  }
187
- async function askYesNo(rl, label, defaultYes) {
188
- const suffix = defaultYes ? "[Y/n]" : "[y/N]";
189
- const answer = await rl.question(`${label} ${suffix}: `);
190
- const value = String(answer ?? "")
191
- .trim()
192
- .toLowerCase();
193
- if (!value) {
194
- return defaultYes;
195
- }
196
- if (value === "y" || value === "yes") {
197
- return true;
198
- }
199
- if (value === "n" || value === "no") {
200
- return false;
168
+ async function askTelegramToken(rl, label, allowEmpty) {
169
+ while (true) {
170
+ const value = await rl.question(`${label}: `);
171
+ const normalized = String(value ?? "").trim();
172
+ if (!normalized && allowEmpty) {
173
+ return "";
174
+ }
175
+ if (isUsableTelegramToken(normalized)) {
176
+ return normalized;
177
+ }
178
+ console.log("Token format looks invalid. Expected format like: 123456789:AA...");
201
179
  }
202
- return defaultYes;
203
180
  }
204
181
  function relativeFromRepo(filePath) {
205
182
  const relative = path.relative(repoRoot, filePath);
@@ -5,6 +5,8 @@ import process from "node:process";
5
5
  import { spawn, spawnSync } from "node:child_process";
6
6
  import { createInterface } from "node:readline/promises";
7
7
  import { fileURLToPath } from "node:url";
8
+ import { codexInstallPackageSpec } from "./codex-version.mjs";
9
+ import { buildCodexCompatibilityError, probeCodexVersion, resolveCodexBinForStart, resolveCompatibleInstalledCodexBin, } from "./codex-runtime.mjs";
8
10
  const __filename = fileURLToPath(import.meta.url);
9
11
  const __dirname = path.dirname(__filename);
10
12
  const repoRoot = path.resolve(__dirname, "..", "..");
@@ -19,6 +21,9 @@ const agentEngineLogPath = path.join(logsDir, "agent-engine.log");
19
21
  const daemonScriptPath = path.join(repoRoot, "scripts", "dist", "daemon.mjs");
20
22
  const supervisorScriptPath = path.join(repoRoot, "scripts", "dist", "supervisor.mjs");
21
23
  const nodeBin = process.execPath;
24
+ const agentEngineEnvPath = path.join(repoRoot, "apps", "agent-engine", ".env");
25
+ const controlPlaneEnvPath = path.join(repoRoot, "apps", "control-plane", ".env");
26
+ const codexInstallCommand = `npm install -g ${codexInstallPackageSpec}`;
22
27
  const BASE_CHECK_MS = 5000;
23
28
  const MAX_BACKOFF_MS = 60000;
24
29
  const action = String(process.argv[2] ?? "status")
@@ -103,6 +108,26 @@ async function runDaemonLoop() {
103
108
  });
104
109
  const state = { stopping: false, shuttingDown: false };
105
110
  setupSignalHandlers(state);
111
+ try {
112
+ ensureCompatibleCodexForDaemon();
113
+ clearLastStartupError();
114
+ }
115
+ catch (error) {
116
+ writeLastStartupError({
117
+ detectedAt: new Date().toISOString(),
118
+ reason: getErrorMessage(error),
119
+ action: buildCodexCompatibilityAction(error),
120
+ });
121
+ console.error(`[daemon] fatal startup error: ${getErrorMessage(error)}`);
122
+ console.error(`[daemon] action required: ${buildCodexCompatibilityAction(error)}`);
123
+ state.stopping = true;
124
+ await shutdownDaemon(state, {
125
+ reason: "fatal-codex-compatibility",
126
+ exitCode: 1,
127
+ pauseBeforeExit: true,
128
+ });
129
+ return;
130
+ }
106
131
  console.log(`[daemon] running (pid ${process.pid})`);
107
132
  let failureCount = 0;
108
133
  while (!state.stopping) {
@@ -529,6 +554,39 @@ function printLastStartupError() {
529
554
  console.log(`action: ${String(issue.action)}`);
530
555
  }
531
556
  }
557
+ function ensureCompatibleCodexForDaemon() {
558
+ const resolved = resolveCodexBinForStart({
559
+ repoRoot,
560
+ agentEngineEnvPath,
561
+ controlPlaneEnvPath,
562
+ });
563
+ const currentProbe = probeCodexVersion({
564
+ codexBin: resolved.bin,
565
+ repoRoot,
566
+ });
567
+ if (currentProbe.ok && currentProbe.compatible) {
568
+ return;
569
+ }
570
+ if (!resolved.userConfigured) {
571
+ const compatibleInstalled = resolveCompatibleInstalledCodexBin({ repoRoot });
572
+ if (compatibleInstalled) {
573
+ return;
574
+ }
575
+ }
576
+ throw new Error(buildCodexCompatibilityError({
577
+ resolved,
578
+ probe: currentProbe,
579
+ includeInstallHint: !resolved.userConfigured,
580
+ installCommand: codexInstallCommand,
581
+ }));
582
+ }
583
+ function buildCodexCompatibilityAction(error) {
584
+ const message = getErrorMessage(error);
585
+ if (message.includes("Install a compatible version with")) {
586
+ return `Install a compatible version with '${codexInstallCommand}', then restart the service.`;
587
+ }
588
+ return "Update that binary or point CODEX_BIN to a compatible executable, then restart the service.";
589
+ }
532
590
  function printUsage() {
533
591
  console.log("Usage: node scripts/dist/daemon.mjs <start|run|stop|status|help>");
534
592
  }