copilot-hub 0.1.20 → 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 (51) 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/daemon.mjs +58 -0
  47. package/scripts/src/cli.mts +166 -233
  48. package/scripts/src/codex-runtime.mts +499 -0
  49. package/scripts/src/codex-version.mts +114 -0
  50. package/scripts/src/daemon.mts +69 -0
  51. package/scripts/test/codex-version.test.mjs +21 -0
@@ -5,6 +5,14 @@ import process, { stdin as input, stdout as output } from "node:process";
5
5
  import { 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
+ buildCodexCompatibilityNotice,
12
+ probeCodexVersion,
13
+ resolveCodexBinForStart,
14
+ resolveCompatibleInstalledCodexBin,
15
+ } from "./codex-runtime.mjs";
8
16
 
9
17
  const __filename = fileURLToPath(import.meta.url);
10
18
  const __dirname = path.dirname(__filename);
@@ -15,8 +23,7 @@ const servicePromptStatePath = path.join(runtimeDir, "service-onboarding.json");
15
23
  const nodeBin = process.execPath;
16
24
  const agentEngineEnvPath = path.join(repoRoot, "apps", "agent-engine", ".env");
17
25
  const controlPlaneEnvPath = path.join(repoRoot, "apps", "control-plane", ".env");
18
- const codexNpmPackage = "@openai/codex";
19
- const codexInstallCommand = `npm install -g ${codexNpmPackage}`;
26
+ const codexInstallCommand = `npm install -g ${codexInstallPackageSpec}`;
20
27
  const packageVersion = readPackageVersion();
21
28
 
22
29
  const rawArgs = process.argv
@@ -66,6 +73,10 @@ async function main() {
66
73
  }
67
74
  case "restart": {
68
75
  runNode(["scripts/dist/ensure-shared-build.mjs"]);
76
+ await ensureCompatibleCodexBinary({
77
+ autoInstall: false,
78
+ purpose: "restart",
79
+ });
69
80
  if (isServiceAlreadyInstalled()) {
70
81
  runNode(["scripts/dist/service.mjs", "stop"]);
71
82
  runNode(["scripts/dist/service.mjs", "start"]);
@@ -91,9 +102,25 @@ async function main() {
91
102
  return;
92
103
  }
93
104
  case "service": {
105
+ const serviceAction = String(rawArgs[1] ?? "")
106
+ .trim()
107
+ .toLowerCase();
108
+ if (serviceAction === "install" || serviceAction === "start") {
109
+ await ensureCompatibleCodexBinary({
110
+ autoInstall: false,
111
+ purpose: "service",
112
+ });
113
+ }
94
114
  runNode(["scripts/dist/service.mjs", ...rawArgs.slice(1)]);
95
115
  return;
96
116
  }
117
+ case "_update_resume": {
118
+ await resumeAfterUpdate({
119
+ serviceInstalled: rawArgs.includes("--service-installed"),
120
+ runningBeforeUpdate: rawArgs.includes("--resume-running"),
121
+ });
122
+ return;
123
+ }
97
124
  case "update":
98
125
  case "upgrade": {
99
126
  await runSelfUpdate();
@@ -139,27 +166,16 @@ function runNodeCapture(scriptArgs, stdioMode = "pipe") {
139
166
  }
140
167
 
141
168
  async function ensureCodexLogin() {
142
- const resolved = resolveCodexBinForStart();
143
- let codexBin = resolved.bin;
144
- let status = runCodex(codexBin, ["login", "status"], "pipe");
169
+ const codexBin = await ensureCompatibleCodexBinary({
170
+ autoInstall: false,
171
+ purpose: "start",
172
+ });
173
+ const status = runCodex(codexBin, ["login", "status"], "pipe");
145
174
  if (status.ok) {
146
175
  console.log("Codex login already configured.");
147
176
  return;
148
177
  }
149
178
 
150
- if (status.errorCode === "ENOENT") {
151
- codexBin = await recoverCodexBinary({
152
- resolved,
153
- status,
154
- });
155
-
156
- status = runCodex(codexBin, ["login", "status"], "pipe");
157
- if (status.ok) {
158
- console.log("Codex login already configured.");
159
- return;
160
- }
161
- }
162
-
163
179
  const reason = status.errorMessage || status.stderr || status.stdout;
164
180
  if (!process.stdin.isTTY) {
165
181
  throw new Error(
@@ -296,6 +312,33 @@ async function runSelfUpdate() {
296
312
  }
297
313
 
298
314
  console.log("copilot-hub updated to latest.");
315
+ const resume = runNodeCapture(
316
+ [
317
+ "scripts/dist/cli.mjs",
318
+ "_update_resume",
319
+ ...(serviceInstalled ? ["--service-installed"] : ["--local-mode"]),
320
+ ...(runningBeforeUpdate ? ["--resume-running"] : ["--stopped"]),
321
+ ],
322
+ "inherit",
323
+ );
324
+ if (!resume.ok) {
325
+ console.log(
326
+ "Update completed, but post-update Codex validation or restart failed. Run 'copilot-hub start' manually.",
327
+ );
328
+ }
329
+ }
330
+
331
+ async function resumeAfterUpdate({
332
+ serviceInstalled,
333
+ runningBeforeUpdate,
334
+ }: {
335
+ serviceInstalled: boolean;
336
+ runningBeforeUpdate: boolean;
337
+ }) {
338
+ await ensureCompatibleCodexBinary({
339
+ autoInstall: true,
340
+ purpose: "update",
341
+ });
299
342
 
300
343
  if (!runningBeforeUpdate) {
301
344
  console.log("Services remain stopped. Run 'copilot-hub start' when ready.");
@@ -306,7 +349,6 @@ async function runSelfUpdate() {
306
349
  const startService = runNodeCapture(["scripts/dist/service.mjs", "start"], "inherit");
307
350
  if (!startService.ok) {
308
351
  console.log("Update completed, but service start failed. Run 'copilot-hub start' manually.");
309
- return;
310
352
  }
311
353
  return;
312
354
  }
@@ -382,45 +424,91 @@ function writeServicePromptState(decision) {
382
424
  }
383
425
  }
384
426
 
385
- async function recoverCodexBinary({ resolved, status }) {
386
- const detected = findDetectedCodexBin();
387
- if (detected && detected !== resolved.bin) {
388
- const probe = runCodex(detected, ["--version"], "pipe");
389
- if (probe.ok) {
390
- console.log(`Detected Codex binary: ${detected}`);
391
- return detected;
427
+ async function ensureCompatibleCodexBinary({
428
+ autoInstall,
429
+ purpose,
430
+ }: {
431
+ autoInstall: boolean;
432
+ purpose: "start" | "restart" | "service" | "update";
433
+ }): Promise<string> {
434
+ const resolved = resolveCodexBinForStart({
435
+ repoRoot,
436
+ agentEngineEnvPath,
437
+ controlPlaneEnvPath,
438
+ });
439
+ const currentProbe = probeCodexVersion({
440
+ codexBin: resolved.bin,
441
+ repoRoot,
442
+ });
443
+
444
+ if (currentProbe.ok && currentProbe.compatible) {
445
+ return resolved.bin;
446
+ }
447
+
448
+ if (!resolved.userConfigured) {
449
+ const compatibleInstalled = resolveCompatibleInstalledCodexBin({ repoRoot });
450
+ if (compatibleInstalled) {
451
+ if (compatibleInstalled !== resolved.bin) {
452
+ const probe = probeCodexVersion({
453
+ codexBin: compatibleInstalled,
454
+ repoRoot,
455
+ });
456
+ if (probe.ok) {
457
+ console.log(`Using compatible Codex CLI ${probe.version} from '${compatibleInstalled}'.`);
458
+ } else {
459
+ console.log(`Using compatible Codex CLI from '${compatibleInstalled}'.`);
460
+ }
461
+ }
462
+ return compatibleInstalled;
392
463
  }
393
464
  }
394
465
 
395
466
  if (resolved.userConfigured) {
396
- return resolved.bin;
467
+ throw new Error(
468
+ buildCodexCompatibilityError({
469
+ resolved,
470
+ probe: currentProbe,
471
+ includeInstallHint: false,
472
+ installCommand: codexInstallCommand,
473
+ }),
474
+ );
397
475
  }
398
476
 
399
- if (!process.stdin.isTTY) {
477
+ if (!autoInstall && !process.stdin.isTTY) {
400
478
  throw new Error(
401
- [
402
- status.errorMessage || `Codex binary '${resolved.bin}' was not found.`,
403
- `Install Codex CLI with '${codexInstallCommand}' or set CODEX_BIN, then retry 'npm run start'.`,
404
- ]
405
- .filter(Boolean)
406
- .join("\n"),
479
+ buildCodexCompatibilityError({
480
+ resolved,
481
+ probe: currentProbe,
482
+ includeInstallHint: true,
483
+ installCommand: codexInstallCommand,
484
+ }),
407
485
  );
408
486
  }
409
487
 
410
- console.log("Codex CLI was not found on this machine.");
411
- const rl = createInterface({ input, output });
412
- let installNow = false;
413
- try {
414
- installNow = await askYesNo(rl, `Install Codex CLI now (${codexInstallCommand})?`, true);
415
- } finally {
416
- rl.close();
488
+ let shouldInstall = autoInstall;
489
+ if (!autoInstall) {
490
+ console.log(buildCodexCompatibilityNotice({ resolved, probe: currentProbe }));
491
+ const rl = createInterface({ input, output });
492
+ try {
493
+ shouldInstall = await askYesNo(
494
+ rl,
495
+ `Install compatible Codex CLI now (${codexInstallCommand})?`,
496
+ true,
497
+ );
498
+ } finally {
499
+ rl.close();
500
+ }
417
501
  }
418
502
 
419
- if (!installNow) {
420
- throw new Error("Codex CLI is required before starting services.");
503
+ if (!shouldInstall) {
504
+ throw new Error(
505
+ purpose === "update"
506
+ ? "Compatible Codex CLI is required before restarting services."
507
+ : "Compatible Codex CLI is required before starting services.",
508
+ );
421
509
  }
422
510
 
423
- const install = runNpm(["install", "-g", codexNpmPackage], "inherit");
511
+ const install = runNpm(["install", "-g", codexInstallPackageSpec], "inherit");
424
512
  if (!install.ok) {
425
513
  throw new Error(
426
514
  [
@@ -435,28 +523,50 @@ async function recoverCodexBinary({ resolved, status }) {
435
523
  );
436
524
  }
437
525
 
438
- const installed = resolveInstalledCodexBin();
526
+ const installed = resolveCompatibleInstalledCodexBin({ repoRoot });
439
527
  if (!installed) {
440
528
  throw new Error(
441
529
  [
442
- "Codex CLI appears installed, but no runnable 'codex' binary was detected.",
443
- "Set CODEX_BIN to the full Codex executable path, then retry 'npm run start'.",
530
+ `Compatible Codex CLI was not detected after installation.`,
531
+ "Set CODEX_BIN to a compatible executable path, then retry.",
444
532
  ].join("\n"),
445
533
  );
446
534
  }
447
535
 
448
- console.log(`Codex CLI installed. Using '${installed}'.`);
536
+ const installedProbe = probeCodexVersion({
537
+ codexBin: installed,
538
+ repoRoot,
539
+ });
540
+ if (installedProbe.ok) {
541
+ console.log(`Codex CLI ready: ${installedProbe.version} from '${installed}'.`);
542
+ } else {
543
+ console.log(`Codex CLI ready from '${installed}'.`);
544
+ }
449
545
  return installed;
450
546
  }
451
547
 
452
548
  function runCodex(codexBin, args, stdioMode) {
453
549
  const stdio: any = stdioMode === "inherit" ? "inherit" : ["ignore", "pipe", "pipe"];
454
- const result = spawnSync(codexBin, args, {
455
- cwd: repoRoot,
456
- stdio,
457
- shell: false,
458
- encoding: "utf8",
459
- });
550
+ const result =
551
+ process.platform === "win32" && /\.(cmd|bat)$/i.test(String(codexBin ?? ""))
552
+ ? spawnSync(
553
+ [
554
+ quoteWindowsShellValue(codexBin),
555
+ ...args.map((arg) => quoteWindowsShellValue(arg)),
556
+ ].join(" "),
557
+ {
558
+ cwd: repoRoot,
559
+ stdio,
560
+ shell: true,
561
+ encoding: "utf8",
562
+ },
563
+ )
564
+ : spawnSync(codexBin, args, {
565
+ cwd: repoRoot,
566
+ stdio,
567
+ shell: false,
568
+ encoding: "utf8",
569
+ });
460
570
 
461
571
  if (result.error) {
462
572
  return {
@@ -478,144 +588,8 @@ function runCodex(codexBin, args, stdioMode) {
478
588
  };
479
589
  }
480
590
 
481
- function resolveCodexBinForStart() {
482
- const fromEnv = nonEmpty(process.env.CODEX_BIN);
483
- if (fromEnv) {
484
- return {
485
- bin: fromEnv,
486
- source: "process_env",
487
- userConfigured: true,
488
- };
489
- }
490
-
491
- for (const [source, envPath] of [
492
- ["agent_env", agentEngineEnvPath],
493
- ["control_plane_env", controlPlaneEnvPath],
494
- ]) {
495
- const value = readEnvValue(envPath, "CODEX_BIN");
496
- if (value) {
497
- return {
498
- bin: value,
499
- source,
500
- userConfigured: true,
501
- };
502
- }
503
- }
504
-
505
- const detected = findDetectedCodexBin();
506
- if (detected) {
507
- return {
508
- bin: detected,
509
- source: "detected",
510
- userConfigured: false,
511
- };
512
- }
513
-
514
- return {
515
- bin: "codex",
516
- source: "default",
517
- userConfigured: false,
518
- };
519
- }
520
-
521
- function resolveInstalledCodexBin() {
522
- const candidates = dedupe(
523
- ["codex", findDetectedCodexBin(), findWindowsNpmGlobalCodexBin(), findVscodeCodexExe()].filter(
524
- Boolean,
525
- ),
526
- );
527
-
528
- for (const candidate of candidates) {
529
- const status = runCodex(candidate, ["--version"], "pipe");
530
- if (status.ok) {
531
- return candidate;
532
- }
533
- }
534
-
535
- return "";
536
- }
537
-
538
- function findDetectedCodexBin() {
539
- if (process.platform !== "win32") {
540
- return "";
541
- }
542
-
543
- return findVscodeCodexExe() || findWindowsNpmGlobalCodexBin() || "";
544
- }
545
-
546
- function findVscodeCodexExe() {
547
- const userProfile = nonEmpty(process.env.USERPROFILE);
548
- if (!userProfile) {
549
- return "";
550
- }
551
-
552
- const extensionsDir = path.join(userProfile, ".vscode", "extensions");
553
- if (!fs.existsSync(extensionsDir)) {
554
- return "";
555
- }
556
-
557
- const candidates = fs
558
- .readdirSync(extensionsDir, { withFileTypes: true })
559
- .filter((entry) => entry.isDirectory())
560
- .map((entry) => entry.name)
561
- .filter((name) => name.startsWith("openai.chatgpt-"))
562
- .sort()
563
- .reverse();
564
-
565
- for (const folder of candidates) {
566
- const exePath = path.join(extensionsDir, folder, "bin", "windows-x86_64", "codex.exe");
567
- if (fs.existsSync(exePath)) {
568
- return exePath;
569
- }
570
- }
571
-
572
- return "";
573
- }
574
-
575
- function findWindowsNpmGlobalCodexBin() {
576
- if (process.platform !== "win32") {
577
- return "";
578
- }
579
-
580
- const candidates = [];
581
- const appData = nonEmpty(process.env.APPDATA);
582
- if (appData) {
583
- candidates.push(path.join(appData, "npm", "codex.cmd"));
584
- candidates.push(path.join(appData, "npm", "codex.exe"));
585
- candidates.push(path.join(appData, "npm", "codex"));
586
- }
587
-
588
- const npmPrefix = readNpmPrefix();
589
- if (npmPrefix) {
590
- candidates.push(path.join(npmPrefix, "codex.cmd"));
591
- candidates.push(path.join(npmPrefix, "codex.exe"));
592
- candidates.push(path.join(npmPrefix, "codex"));
593
- }
594
-
595
- for (const candidate of dedupe(candidates)) {
596
- if (fs.existsSync(candidate)) {
597
- return candidate;
598
- }
599
- }
600
-
601
- return "";
602
- }
603
-
604
- function readNpmPrefix() {
605
- const result = spawnNpm(["config", "get", "prefix"], {
606
- cwd: repoRoot,
607
- stdio: ["ignore", "pipe", "pipe"],
608
- encoding: "utf8",
609
- });
610
- if (result.error || result.status !== 0) {
611
- return "";
612
- }
613
-
614
- const value = String(result.stdout ?? "").trim();
615
- if (!value || value.toLowerCase() === "undefined") {
616
- return "";
617
- }
618
- return value;
591
+ function quoteWindowsShellValue(value) {
592
+ return `"${String(value ?? "").replace(/"/g, '\\"')}"`;
619
593
  }
620
594
 
621
595
  function runNpm(args, stdioMode) {
@@ -646,34 +620,6 @@ function runNpm(args, stdioMode) {
646
620
  };
647
621
  }
648
622
 
649
- function readEnvValue(filePath, key) {
650
- if (!fs.existsSync(filePath)) {
651
- return "";
652
- }
653
-
654
- const lines = fs.readFileSync(filePath, "utf8").split(/\r?\n/);
655
- const pattern = new RegExp(`^\\s*${escapeRegex(key)}\\s*=\\s*(.*)\\s*$`);
656
- for (const line of lines) {
657
- const match = line.match(pattern);
658
- if (!match) {
659
- continue;
660
- }
661
- return unquote(match[1]);
662
- }
663
- return "";
664
- }
665
-
666
- function unquote(value) {
667
- const raw = String(value ?? "").trim();
668
- if (!raw) {
669
- return "";
670
- }
671
- if ((raw.startsWith('"') && raw.endsWith('"')) || (raw.startsWith("'") && raw.endsWith("'"))) {
672
- return raw.slice(1, -1).trim();
673
- }
674
- return raw;
675
- }
676
-
677
623
  async function askYesNo(rl, label, defaultYes) {
678
624
  const suffix = defaultYes ? "[Y/n]" : "[y/N]";
679
625
  const answer = await rl.question(`${label} ${suffix}: `);
@@ -692,15 +638,6 @@ async function askYesNo(rl, label, defaultYes) {
692
638
  return defaultYes;
693
639
  }
694
640
 
695
- function escapeRegex(value) {
696
- return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
697
- }
698
-
699
- function nonEmpty(value) {
700
- const normalized = String(value ?? "").trim();
701
- return normalized || "";
702
- }
703
-
704
641
  function firstLine(value) {
705
642
  return (
706
643
  String(value ?? "")
@@ -740,10 +677,6 @@ function normalizeErrorCode(error) {
740
677
  .toUpperCase();
741
678
  }
742
679
 
743
- function dedupe(values: string[]) {
744
- return [...new Set(values.map((value) => String(value).trim()).filter(Boolean))];
745
- }
746
-
747
680
  function spawnNpm(args, options) {
748
681
  if (process.platform === "win32") {
749
682
  const comspec = process.env.ComSpec || "cmd.exe";