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
@@ -14,6 +14,7 @@ const engineExamplePath = path.join(repoRoot, "apps", "agent-engine", ".env.exam
14
14
  const controlPlaneEnvPath = path.join(repoRoot, "apps", "control-plane", ".env");
15
15
  const controlPlaneExamplePath = path.join(repoRoot, "apps", "control-plane", ".env.example");
16
16
  const TELEGRAM_TOKEN_PATTERN = /^\d{5,}:[A-Za-z0-9_-]{20,}$/;
17
+ const DEFAULT_CONTROL_PLANE_TOKEN_ENV = "HUB_TELEGRAM_TOKEN";
17
18
 
18
19
  const args = new Set(process.argv.slice(2));
19
20
  const requiredOnly = args.has("--required-only");
@@ -33,9 +34,8 @@ async function main() {
33
34
  if (requiredOnly) {
34
35
  await configureRequiredTokens({ rl, controlPlaneLines });
35
36
  } else {
36
- await configureAll({ rl, engineLines, controlPlaneLines });
37
+ await configureAll({ rl, controlPlaneLines });
37
38
  console.log("\nSaved:");
38
- console.log(`- ${relativeFromRepo(engineEnvPath)}`);
39
39
  console.log(`- ${relativeFromRepo(controlPlaneEnvPath)}`);
40
40
  console.log("\nNext step:");
41
41
  console.log("1) npm run start");
@@ -53,7 +53,7 @@ async function configureRequiredTokens({ rl, controlPlaneLines }) {
53
53
 
54
54
  const controlPlaneTokenEnvName = nonEmpty(
55
55
  controlPlaneMap.HUB_TELEGRAM_TOKEN_ENV,
56
- "HUB_TELEGRAM_TOKEN",
56
+ DEFAULT_CONTROL_PLANE_TOKEN_ENV,
57
57
  );
58
58
  setEnvValue(controlPlaneLines, "HUB_TELEGRAM_TOKEN_ENV", controlPlaneTokenEnvName);
59
59
 
@@ -72,55 +72,33 @@ async function configureRequiredTokens({ rl, controlPlaneLines }) {
72
72
  }
73
73
 
74
74
  console.log("Missing or invalid hub token. Please enter a valid Telegram bot token.");
75
- const value = await askRequiredTelegramToken(
76
- rl,
77
- `Token value for ${controlPlaneTokenEnvName} (control-plane)`,
78
- );
75
+ const value = await askRequiredTelegramToken(rl, "Control-plane Telegram token");
79
76
  setEnvValue(controlPlaneLines, controlPlaneTokenEnvName, value);
80
77
  console.log("Required token saved.");
81
78
  }
82
79
 
83
- async function configureAll({ rl, engineLines, controlPlaneLines }) {
84
- const engineMap = parseEnvMap(engineLines);
80
+ async function configureAll({ rl, controlPlaneLines }) {
85
81
  const controlPlaneMap = parseEnvMap(controlPlaneLines);
86
82
 
87
- console.log("\nCopilot Hub token configuration\n");
83
+ console.log("\nCopilot Hub control-plane configuration\n");
88
84
 
89
85
  const controlPlaneTokenEnvDefault = nonEmpty(
90
86
  controlPlaneMap.HUB_TELEGRAM_TOKEN_ENV,
91
- "HUB_TELEGRAM_TOKEN",
92
- );
93
- const controlPlaneTokenEnvName = await ask(
94
- rl,
95
- "control-plane token variable",
96
- controlPlaneTokenEnvDefault,
97
- );
98
- setEnvValue(controlPlaneLines, "HUB_TELEGRAM_TOKEN_ENV", controlPlaneTokenEnvName);
99
- const currentControlPlaneToken = parseEnvMap(controlPlaneLines)[controlPlaneTokenEnvName] ?? "";
100
- const newControlPlaneToken = await ask(
101
- rl,
102
- `Token value for ${controlPlaneTokenEnvName} (control-plane, Enter to keep current)`,
103
- "",
87
+ DEFAULT_CONTROL_PLANE_TOKEN_ENV,
104
88
  );
105
- if (newControlPlaneToken) {
106
- setEnvValue(controlPlaneLines, controlPlaneTokenEnvName, newControlPlaneToken);
107
- } else if (!currentControlPlaneToken) {
108
- console.log(`- No value set for ${controlPlaneTokenEnvName} yet.`);
109
- }
89
+ setEnvValue(controlPlaneLines, "HUB_TELEGRAM_TOKEN_ENV", controlPlaneTokenEnvDefault);
90
+ const currentControlPlaneToken = String(
91
+ parseEnvMap(controlPlaneLines)[controlPlaneTokenEnvDefault] ?? "",
92
+ ).trim();
110
93
 
111
- const configureAgentToken = await askYesNo(rl, "Configure TELEGRAM_TOKEN_AGENT_1 now?", true);
112
- if (configureAgentToken) {
113
- const currentAgentToken = engineMap.TELEGRAM_TOKEN_AGENT_1 ?? "";
114
- const newAgentToken = await ask(
115
- rl,
116
- "Token value for TELEGRAM_TOKEN_AGENT_1 (agent-engine, Enter to keep current)",
117
- "",
118
- );
119
- if (newAgentToken) {
120
- setEnvValue(engineLines, "TELEGRAM_TOKEN_AGENT_1", newAgentToken);
121
- } else if (!currentAgentToken) {
122
- console.log("- No value set for TELEGRAM_TOKEN_AGENT_1 yet.");
123
- }
94
+ const newControlPlaneToken = currentControlPlaneToken
95
+ ? await askTelegramToken(rl, "Control-plane Telegram token (press Enter to keep current)", true)
96
+ : await askRequiredTelegramToken(rl, "Control-plane Telegram token");
97
+
98
+ if (newControlPlaneToken) {
99
+ setEnvValue(controlPlaneLines, controlPlaneTokenEnvDefault, newControlPlaneToken);
100
+ } else {
101
+ console.log("- Control-plane token left unchanged.");
124
102
  }
125
103
  }
126
104
 
@@ -212,15 +190,6 @@ function escapeRegex(value) {
212
190
  return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
213
191
  }
214
192
 
215
- async function ask(rl, label, fallback) {
216
- const value = await rl.question(`${label}${fallback ? ` [${fallback}]` : ""}: `);
217
- const normalized = String(value ?? "").trim();
218
- if (!normalized) {
219
- return String(fallback ?? "").trim();
220
- }
221
- return normalized;
222
- }
223
-
224
193
  async function askRequired(rl, label) {
225
194
  while (true) {
226
195
  const value = await rl.question(`${label}: `);
@@ -242,22 +211,18 @@ async function askRequiredTelegramToken(rl, label) {
242
211
  }
243
212
  }
244
213
 
245
- async function askYesNo(rl, label, defaultYes) {
246
- const suffix = defaultYes ? "[Y/n]" : "[y/N]";
247
- const answer = await rl.question(`${label} ${suffix}: `);
248
- const value = String(answer ?? "")
249
- .trim()
250
- .toLowerCase();
251
- if (!value) {
252
- return defaultYes;
253
- }
254
- if (value === "y" || value === "yes") {
255
- return true;
256
- }
257
- if (value === "n" || value === "no") {
258
- return false;
214
+ async function askTelegramToken(rl, label, allowEmpty) {
215
+ while (true) {
216
+ const value = await rl.question(`${label}: `);
217
+ const normalized = String(value ?? "").trim();
218
+ if (!normalized && allowEmpty) {
219
+ return "";
220
+ }
221
+ if (isUsableTelegramToken(normalized)) {
222
+ return normalized;
223
+ }
224
+ console.log("Token format looks invalid. Expected format like: 123456789:AA...");
259
225
  }
260
- return defaultYes;
261
226
  }
262
227
 
263
228
  function relativeFromRepo(filePath) {
@@ -5,6 +5,13 @@ 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 {
10
+ buildCodexCompatibilityError,
11
+ probeCodexVersion,
12
+ resolveCodexBinForStart,
13
+ resolveCompatibleInstalledCodexBin,
14
+ } from "./codex-runtime.mjs";
8
15
 
9
16
  const __filename = fileURLToPath(import.meta.url);
10
17
  const __dirname = path.dirname(__filename);
@@ -22,6 +29,9 @@ const agentEngineLogPath = path.join(logsDir, "agent-engine.log");
22
29
  const daemonScriptPath = path.join(repoRoot, "scripts", "dist", "daemon.mjs");
23
30
  const supervisorScriptPath = path.join(repoRoot, "scripts", "dist", "supervisor.mjs");
24
31
  const nodeBin = process.execPath;
32
+ const agentEngineEnvPath = path.join(repoRoot, "apps", "agent-engine", ".env");
33
+ const controlPlaneEnvPath = path.join(repoRoot, "apps", "control-plane", ".env");
34
+ const codexInstallCommand = `npm install -g ${codexInstallPackageSpec}`;
25
35
 
26
36
  const BASE_CHECK_MS = 5000;
27
37
  const MAX_BACKOFF_MS = 60000;
@@ -120,6 +130,26 @@ async function runDaemonLoop() {
120
130
  const state = { stopping: false, shuttingDown: false };
121
131
  setupSignalHandlers(state);
122
132
 
133
+ try {
134
+ ensureCompatibleCodexForDaemon();
135
+ clearLastStartupError();
136
+ } catch (error) {
137
+ writeLastStartupError({
138
+ detectedAt: new Date().toISOString(),
139
+ reason: getErrorMessage(error),
140
+ action: buildCodexCompatibilityAction(error),
141
+ });
142
+ console.error(`[daemon] fatal startup error: ${getErrorMessage(error)}`);
143
+ console.error(`[daemon] action required: ${buildCodexCompatibilityAction(error)}`);
144
+ state.stopping = true;
145
+ await shutdownDaemon(state, {
146
+ reason: "fatal-codex-compatibility",
147
+ exitCode: 1,
148
+ pauseBeforeExit: true,
149
+ });
150
+ return;
151
+ }
152
+
123
153
  console.log(`[daemon] running (pid ${process.pid})`);
124
154
 
125
155
  let failureCount = 0;
@@ -614,6 +644,45 @@ function printLastStartupError() {
614
644
  }
615
645
  }
616
646
 
647
+ function ensureCompatibleCodexForDaemon(): void {
648
+ const resolved = resolveCodexBinForStart({
649
+ repoRoot,
650
+ agentEngineEnvPath,
651
+ controlPlaneEnvPath,
652
+ });
653
+ const currentProbe = probeCodexVersion({
654
+ codexBin: resolved.bin,
655
+ repoRoot,
656
+ });
657
+ if (currentProbe.ok && currentProbe.compatible) {
658
+ return;
659
+ }
660
+
661
+ if (!resolved.userConfigured) {
662
+ const compatibleInstalled = resolveCompatibleInstalledCodexBin({ repoRoot });
663
+ if (compatibleInstalled) {
664
+ return;
665
+ }
666
+ }
667
+
668
+ throw new Error(
669
+ buildCodexCompatibilityError({
670
+ resolved,
671
+ probe: currentProbe,
672
+ includeInstallHint: !resolved.userConfigured,
673
+ installCommand: codexInstallCommand,
674
+ }),
675
+ );
676
+ }
677
+
678
+ function buildCodexCompatibilityAction(error: unknown): string {
679
+ const message = getErrorMessage(error);
680
+ if (message.includes("Install a compatible version with")) {
681
+ return `Install a compatible version with '${codexInstallCommand}', then restart the service.`;
682
+ }
683
+ return "Update that binary or point CODEX_BIN to a compatible executable, then restart the service.";
684
+ }
685
+
617
686
  function printUsage() {
618
687
  console.log("Usage: node scripts/dist/daemon.mjs <start|run|stop|status|help>");
619
688
  }
@@ -0,0 +1,21 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { compareSemver, isCodexVersionCompatible } from "../dist/codex-version.mjs";
4
+
5
+ test("isCodexVersionCompatible accepts only validated stable 0.113.x releases", () => {
6
+ assert.equal(isCodexVersionCompatible("0.113.0"), true);
7
+ assert.equal(isCodexVersionCompatible("0.113.9"), true);
8
+ assert.equal(isCodexVersionCompatible("0.112.9"), false);
9
+ assert.equal(isCodexVersionCompatible("0.114.0"), false);
10
+ });
11
+
12
+ test("isCodexVersionCompatible rejects prerelease builds outside the validated lane", () => {
13
+ assert.equal(isCodexVersionCompatible("0.113.1-alpha.1"), false);
14
+ assert.equal(isCodexVersionCompatible("0.114.0-alpha.1"), false);
15
+ });
16
+
17
+ test("compareSemver keeps prerelease ordering stable", () => {
18
+ assert.equal(compareSemver("0.113.0", "0.113.0"), 0);
19
+ assert.equal(compareSemver("0.113.1", "0.113.0"), 1);
20
+ assert.equal(compareSemver("0.113.0-alpha.1", "0.113.0-alpha.2"), -1);
21
+ });