copilot-hub 0.1.20 → 0.1.23

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 (68) hide show
  1. package/README.md +22 -13
  2. package/apps/agent-engine/dist/config.js +91 -16
  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 +7 -8
  8. package/apps/control-plane/dist/config.js +91 -16
  9. package/apps/control-plane/dist/copilot-hub.js +2 -2
  10. package/apps/control-plane/dist/index.js +16 -0
  11. package/apps/control-plane/dist/test/hub-model-utils.test.js +110 -13
  12. package/package.json +3 -3
  13. package/packages/core/dist/agent-supervisor.d.ts +5 -0
  14. package/packages/core/dist/agent-supervisor.js +11 -0
  15. package/packages/core/dist/agent-supervisor.js.map +1 -1
  16. package/packages/core/dist/bot-manager.js +17 -1
  17. package/packages/core/dist/bot-manager.js.map +1 -1
  18. package/packages/core/dist/bot-runtime.d.ts +4 -0
  19. package/packages/core/dist/bot-runtime.js +5 -1
  20. package/packages/core/dist/bot-runtime.js.map +1 -1
  21. package/packages/core/dist/codex-app-client.d.ts +13 -2
  22. package/packages/core/dist/codex-app-client.js +51 -13
  23. package/packages/core/dist/codex-app-client.js.map +1 -1
  24. package/packages/core/dist/codex-app-utils.d.ts +6 -0
  25. package/packages/core/dist/codex-app-utils.js +49 -0
  26. package/packages/core/dist/codex-app-utils.js.map +1 -1
  27. package/packages/core/dist/codex-provider.d.ts +3 -1
  28. package/packages/core/dist/codex-provider.js +3 -1
  29. package/packages/core/dist/codex-provider.js.map +1 -1
  30. package/packages/core/dist/config-paths.d.ts +11 -0
  31. package/packages/core/dist/config-paths.js +42 -0
  32. package/packages/core/dist/config-paths.js.map +1 -0
  33. package/packages/core/dist/index.d.ts +1 -0
  34. package/packages/core/dist/index.js +1 -0
  35. package/packages/core/dist/index.js.map +1 -1
  36. package/packages/core/dist/kernel-control-plane.d.ts +1 -0
  37. package/packages/core/dist/kernel-control-plane.js +132 -13
  38. package/packages/core/dist/kernel-control-plane.js.map +1 -1
  39. package/packages/core/dist/provider-factory.d.ts +2 -0
  40. package/packages/core/dist/provider-factory.js +3 -0
  41. package/packages/core/dist/provider-factory.js.map +1 -1
  42. package/packages/core/dist/provider-options.js +24 -17
  43. package/packages/core/dist/provider-options.js.map +1 -1
  44. package/packages/core/dist/state-store.d.ts +1 -0
  45. package/packages/core/dist/state-store.js +28 -2
  46. package/packages/core/dist/state-store.js.map +1 -1
  47. package/packages/core/dist/telegram-channel.d.ts +1 -0
  48. package/packages/core/dist/telegram-channel.js +5 -1
  49. package/packages/core/dist/telegram-channel.js.map +1 -1
  50. package/packages/core/package.json +4 -0
  51. package/scripts/dist/cli.mjs +115 -267
  52. package/scripts/dist/codex-runtime.mjs +352 -0
  53. package/scripts/dist/codex-version.mjs +91 -0
  54. package/scripts/dist/configure.mjs +8 -9
  55. package/scripts/dist/daemon.mjs +65 -4
  56. package/scripts/dist/install-layout.mjs +140 -0
  57. package/scripts/dist/service.mjs +9 -6
  58. package/scripts/dist/supervisor.mjs +35 -8
  59. package/scripts/src/cli.mts +136 -308
  60. package/scripts/src/codex-runtime.mts +499 -0
  61. package/scripts/src/codex-version.mts +114 -0
  62. package/scripts/src/configure.mts +9 -10
  63. package/scripts/src/daemon.mts +76 -4
  64. package/scripts/src/install-layout.mts +207 -0
  65. package/scripts/src/service.mts +9 -6
  66. package/scripts/src/supervisor.mts +36 -8
  67. package/scripts/test/codex-version.test.mjs +21 -0
  68. package/scripts/test/install-layout.test.mjs +82 -0
package/README.md CHANGED
@@ -82,6 +82,7 @@ copilot-hub start
82
82
 
83
83
  `start` runs guided setup automatically if required values are missing.
84
84
  On interactive terminals, `start` can also offer OS-native service installation when it is not yet configured.
85
+ `start` also verifies the supported Codex CLI range and can install the validated version automatically when needed.
85
86
 
86
87
  ## Quick start from source
87
88
 
@@ -160,16 +161,29 @@ Default values are already applied, and actions start from that agent workspace
160
161
 
161
162
  - Never commit real bot tokens.
162
163
  - If a token is leaked, regenerate it in `@BotFather` using `/revoke`.
163
- - Keep local runtime files (`data/`, `logs/`) private.
164
+ - Keep local runtime files private.
164
165
 
165
166
  ## Startup troubleshooting
166
167
 
167
168
  - If `npm run start` fails, first read the error and follow the suggested action.
168
- - `npm run start` now auto-detects Codex from VS Code (Windows) and can install Codex CLI automatically if missing.
169
+ - `npm run start` now checks that Codex CLI is inside the supported range and can install the validated version automatically if missing or outside that range.
169
170
  - For Codex login issues, run `codex login` (or the configured `CODEX_BIN`) and retry `npm run start`.
170
- - If auto-install is skipped or unavailable, install Codex CLI with `npm install -g @openai/codex` or set `CODEX_BIN` in `.env`.
171
+ - If auto-install is skipped or unavailable, install Codex CLI with `npm install -g @openai/codex@0.113.0` or set `CODEX_BIN` in your Copilot Hub config to a binary in the supported `0.113.x` range.
171
172
  - If you are still stuck, ask your favorite LLM with the exact error output.
172
173
 
174
+ ## Upgrades
175
+
176
+ Use the standard npm flow:
177
+
178
+ ```bash
179
+ npm install -g copilot-hub@latest
180
+ copilot-hub restart
181
+ ```
182
+
183
+ If Copilot Hub is not already running, use `copilot-hub start` after the install instead of `copilot-hub restart`.
184
+
185
+ Tokens, registry data, logs and runtime state are now stored in a persistent per-user Copilot Hub directory, so they survive package upgrades. The runtime also no longer keeps the installed package directory as its working directory, so `npm install -g copilot-hub@latest` can replace the package cleanly on Windows while the service is running.
186
+
173
187
  ## Commands
174
188
 
175
189
  ```bash
@@ -179,19 +193,12 @@ npm run restart
179
193
  npm run status
180
194
  npm run logs
181
195
  npm run configure
182
- npm run update
183
196
  npm run test
184
197
  npm run lint
185
198
  npm run format:check
186
199
  npm run check:apps
187
200
  ```
188
201
 
189
- Global update command:
190
-
191
- ```bash
192
- copilot-hub update
193
- ```
194
-
195
202
  Service mode (optional, OS-native):
196
203
 
197
204
  ```bash
@@ -237,11 +244,13 @@ Authentication options:
237
244
 
238
245
  ## Runtime files
239
246
 
240
- - PIDs: `.copilot-hub/pids/`
241
- - Logs: `logs/`
247
+ - Config, tokens, registry, logs and runtime state live in the per-user Copilot Hub home directory.
248
+ - Windows default: `%APPDATA%\\copilot-hub`
249
+ - macOS default: `~/Library/Application Support/copilot-hub`
250
+ - Linux default: `${XDG_CONFIG_HOME:-~/.config}/copilot-hub`
242
251
 
243
252
  ## Security
244
253
 
245
254
  - Never commit real tokens.
246
- - Keep `.env` and runtime data local.
255
+ - Keep your Copilot Hub home directory private.
247
256
  - Rotate leaked tokens immediately.
@@ -1,18 +1,20 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import { spawnSync } from "node:child_process";
3
4
  import dotenv from "dotenv";
4
5
  import { createWorkspaceBoundaryPolicy, assertWorkspaceAllowed, parseWorkspaceAllowedRoots, } from "@copilot-hub/core/workspace-policy";
5
6
  import { parseTurnActivityTimeoutSetting } from "@copilot-hub/core/codex-app-utils";
7
+ import { resolveConfigBaseDir, resolveOptionalPathFromBase, resolvePathFromBase, } from "@copilot-hub/core/config-paths";
6
8
  import { getDefaultExternalWorkspaceBasePath, getKernelRootPath, } from "@copilot-hub/core/workspace-paths";
7
- dotenv.config();
9
+ const envBaseDir = loadEnvironment();
8
10
  const kernelRootPath = getKernelRootPath();
9
11
  const configuredDefaultWorkspaceRoot = String(process.env.DEFAULT_WORKSPACE_ROOT ?? "").trim();
10
12
  const defaultWorkspaceRoot = resolveWorkspaceRoot(configuredDefaultWorkspaceRoot || getDefaultExternalWorkspaceBasePath(kernelRootPath));
11
13
  const configuredProjectsBaseDir = String(process.env.PROJECTS_BASE_DIR ?? "").trim();
12
- const projectsBaseDir = path.resolve(configuredProjectsBaseDir || defaultWorkspaceRoot);
14
+ const projectsBaseDir = resolvePathFromBase(configuredProjectsBaseDir || defaultWorkspaceRoot, envBaseDir);
13
15
  const workspaceStrictMode = parseBoolean(process.env.WORKSPACE_STRICT_MODE ?? "true");
14
16
  const workspaceAllowedRoots = parseWorkspaceAllowedRoots(process.env.WORKSPACE_ALLOWED_ROOTS ?? "", {
15
- cwd: process.cwd(),
17
+ cwd: envBaseDir,
16
18
  });
17
19
  const workspacePolicy = createWorkspaceBoundaryPolicy({
18
20
  kernelRootPath,
@@ -26,15 +28,19 @@ assertWorkspaceAllowed({
26
28
  policy: workspacePolicy,
27
29
  label: "DEFAULT_WORKSPACE_ROOT",
28
30
  });
29
- const dataDir = path.resolve(process.env.BOT_DATA_DIR ?? path.join(process.cwd(), "data"));
30
- const botRegistryFilePath = path.resolve(process.env.BOT_REGISTRY_FILE ?? path.join(dataDir, "bot-registry.json"));
31
- const secretStoreFilePath = path.resolve(process.env.SECRET_STORE_FILE ?? path.join(dataDir, "secrets.json"));
31
+ const dataDir = resolvePathFromBase(process.env.BOT_DATA_DIR ?? path.join(envBaseDir, "data"), envBaseDir);
32
+ const botRegistryFilePath = resolvePathFromBase(process.env.BOT_REGISTRY_FILE ?? path.join(dataDir, "bot-registry.json"), envBaseDir);
33
+ const secretStoreFilePath = resolvePathFromBase(process.env.SECRET_STORE_FILE ?? path.join(dataDir, "secrets.json"), envBaseDir);
32
34
  const instanceLockEnabled = parseBoolean(process.env.INSTANCE_LOCK_ENABLED ?? "true");
33
- const instanceLockFilePath = path.resolve(process.env.INSTANCE_LOCK_FILE ?? path.join(dataDir, "runtime.lock"));
35
+ const instanceLockFilePath = resolvePathFromBase(process.env.INSTANCE_LOCK_FILE ?? path.join(dataDir, "runtime.lock"), envBaseDir);
34
36
  const bootstrapTelegramToken = String(process.env.TELEGRAM_BOT_TOKEN ?? "").trim();
35
37
  const defaultProviderKind = normalizeProviderKind(process.env.DEFAULT_PROVIDER_KIND ?? "codex");
36
38
  const codexBin = resolveCodexBin(process.env.CODEX_BIN);
37
- const codexHomeDir = resolveOptionalPath(process.env.CODEX_HOME_DIR);
39
+ const codexHomeDir = resolveOptionalPathFromBase(process.env.CODEX_HOME_DIR, envBaseDir);
40
+ if (codexHomeDir) {
41
+ process.env.CODEX_HOME_DIR = codexHomeDir;
42
+ process.env.CODEX_HOME = codexHomeDir;
43
+ }
38
44
  const codexSandbox = normalizeCodexSandbox(process.env.CODEX_SANDBOX ?? "danger-full-access");
39
45
  const codexApprovalPolicy = normalizeApprovalPolicy(process.env.CODEX_APPROVAL_POLICY ?? "never");
40
46
  const turnActivityTimeoutMs = parseTurnActivityTimeoutSetting(process.env.TURN_ACTIVITY_TIMEOUT_MS ?? "0", 0);
@@ -78,6 +84,7 @@ const defaultAllowedChatIds = new Set((process.env.TELEGRAM_ALLOWED_CHAT_IDS ??
78
84
  .filter(Boolean));
79
85
  fs.mkdirSync(dataDir, { recursive: true });
80
86
  export const config = {
87
+ envBaseDir,
81
88
  defaultProviderKind,
82
89
  providerDefaults: {
83
90
  defaultKind: defaultProviderKind,
@@ -117,6 +124,24 @@ export const config = {
117
124
  defaultSharedThreadId,
118
125
  defaultAllowedChatIds,
119
126
  };
127
+ function loadEnvironment() {
128
+ const configuredEnvPath = String(process.env.COPILOT_HUB_ENV_PATH ?? "").trim();
129
+ const resolvedEnvPath = configuredEnvPath ? path.resolve(configuredEnvPath) : "";
130
+ const baseDir = resolveConfigBaseDir({
131
+ configuredBaseDir: process.env.COPILOT_HUB_ENV_BASE_DIR,
132
+ configuredEnvPath: resolvedEnvPath,
133
+ cwd: process.cwd(),
134
+ });
135
+ if (configuredEnvPath) {
136
+ process.env.COPILOT_HUB_ENV_PATH = resolvedEnvPath;
137
+ dotenv.config({ path: resolvedEnvPath });
138
+ }
139
+ else {
140
+ dotenv.config();
141
+ }
142
+ process.env.COPILOT_HUB_ENV_BASE_DIR = baseDir;
143
+ return baseDir;
144
+ }
120
145
  function resolveCodexBin(rawValue) {
121
146
  const value = String(rawValue ?? "").trim();
122
147
  const normalized = value.toLowerCase();
@@ -124,6 +149,10 @@ function resolveCodexBin(rawValue) {
124
149
  return value;
125
150
  }
126
151
  if (process.platform === "win32") {
152
+ const npmGlobalCodex = findWindowsNpmGlobalCodexBin();
153
+ if (npmGlobalCodex) {
154
+ return npmGlobalCodex;
155
+ }
127
156
  const vscodeCodex = findVscodeCodexExe();
128
157
  if (vscodeCodex) {
129
158
  return vscodeCodex;
@@ -155,6 +184,59 @@ function findVscodeCodexExe() {
155
184
  }
156
185
  return null;
157
186
  }
187
+ function findWindowsNpmGlobalCodexBin() {
188
+ if (process.platform !== "win32") {
189
+ return null;
190
+ }
191
+ const candidates = [];
192
+ const appData = String(process.env.APPDATA ?? "").trim();
193
+ if (appData) {
194
+ candidates.push(path.join(appData, "npm", "codex.cmd"));
195
+ candidates.push(path.join(appData, "npm", "codex.exe"));
196
+ candidates.push(path.join(appData, "npm", "codex"));
197
+ }
198
+ const npmPrefix = readNpmPrefix();
199
+ if (npmPrefix) {
200
+ candidates.push(path.join(npmPrefix, "codex.cmd"));
201
+ candidates.push(path.join(npmPrefix, "codex.exe"));
202
+ candidates.push(path.join(npmPrefix, "codex"));
203
+ }
204
+ for (const candidate of candidates) {
205
+ if (fs.existsSync(candidate)) {
206
+ return candidate;
207
+ }
208
+ }
209
+ return null;
210
+ }
211
+ function readNpmPrefix() {
212
+ const result = spawnNpm(["config", "get", "prefix"]);
213
+ if (result.error || result.status !== 0) {
214
+ return "";
215
+ }
216
+ const value = String(result.stdout ?? "").trim();
217
+ if (!value || value.toLowerCase() === "undefined") {
218
+ return "";
219
+ }
220
+ return value;
221
+ }
222
+ function spawnNpm(args) {
223
+ if (process.platform === "win32") {
224
+ const comspec = process.env.ComSpec || "cmd.exe";
225
+ const commandLine = ["npm", ...args].join(" ");
226
+ return spawnSync(comspec, ["/d", "/s", "/c", commandLine], {
227
+ cwd: envBaseDir,
228
+ stdio: ["ignore", "pipe", "pipe"],
229
+ shell: false,
230
+ encoding: "utf8",
231
+ });
232
+ }
233
+ return spawnSync("npm", args, {
234
+ cwd: envBaseDir,
235
+ stdio: ["ignore", "pipe", "pipe"],
236
+ shell: false,
237
+ encoding: "utf8",
238
+ });
239
+ }
158
240
  function normalizeThreadMode(value) {
159
241
  const mode = String(value ?? "single")
160
242
  .trim()
@@ -176,13 +258,6 @@ function parseBoolean(value) {
176
258
  }
177
259
  throw new Error("Invalid boolean value in environment.");
178
260
  }
179
- function resolveOptionalPath(value) {
180
- const raw = String(value ?? "").trim();
181
- if (!raw) {
182
- return null;
183
- }
184
- return path.resolve(raw);
185
- }
186
261
  function normalizeCodexSandbox(value) {
187
262
  const mode = String(value ?? "")
188
263
  .trim()
@@ -218,5 +293,5 @@ function resolveWorkspaceRoot(value) {
218
293
  if (!raw) {
219
294
  throw new Error("DEFAULT_WORKSPACE_ROOT must not be empty.");
220
295
  }
221
- return path.resolve(raw);
296
+ return resolvePathFromBase(raw, envBaseDir);
222
297
  }
@@ -5,7 +5,9 @@ import { fileURLToPath } from "node:url";
5
5
  import express from "express";
6
6
  import { BotManager } from "@copilot-hub/core/bot-manager";
7
7
  import { CodexAppClient } from "@copilot-hub/core/codex-app-client";
8
+ import { buildShellWrappedCommandLine, requiresShellWrappedSpawn, } from "@copilot-hub/core/codex-app-utils";
8
9
  import { loadBotRegistry } from "@copilot-hub/core/bot-registry";
10
+ import { invalidateCodexQuotaUsageCache } from "@copilot-hub/core/telegram-channel";
9
11
  import { config } from "./config.js";
10
12
  import { InstanceLock } from "@copilot-hub/core/instance-lock";
11
13
  import { KernelControlPlane } from "@copilot-hub/core/kernel-control-plane";
@@ -215,6 +217,7 @@ function buildApiApp({ botManager, controlPlane, registryFilePath, }) {
215
217
  const refreshed = await refreshRunningBotProviders({
216
218
  botManager: requireBotManager(),
217
219
  });
220
+ invalidateCodexQuotaUsageCache();
218
221
  res.json({
219
222
  ok: true,
220
223
  switched: true,
@@ -273,8 +276,18 @@ function buildApiApp({ botManager, controlPlane, registryFilePath, }) {
273
276
  .trim()
274
277
  .toLowerCase();
275
278
  const hasModel = Object.prototype.hasOwnProperty.call(req.body ?? {}, "model");
279
+ const hasReasoningEffort = Object.prototype.hasOwnProperty.call(req.body ?? {}, "reasoningEffort");
280
+ const hasServiceTier = Object.prototype.hasOwnProperty.call(req.body ?? {}, "serviceTier");
276
281
  const rawModel = req.body?.model;
282
+ const rawReasoningEffort = req.body?.reasoningEffort;
283
+ const rawServiceTier = req.body?.serviceTier;
277
284
  const model = rawModel === null || rawModel === undefined ? null : String(rawModel).trim();
285
+ const reasoningEffort = rawReasoningEffort === null || rawReasoningEffort === undefined
286
+ ? null
287
+ : String(rawReasoningEffort).trim().toLowerCase();
288
+ const serviceTier = rawServiceTier === null || rawServiceTier === undefined
289
+ ? null
290
+ : String(rawServiceTier).trim().toLowerCase();
278
291
  if (!sandboxMode) {
279
292
  res.status(400).json({ error: "Field 'sandboxMode' is required." });
280
293
  return;
@@ -291,6 +304,12 @@ function buildApiApp({ botManager, controlPlane, registryFilePath, }) {
291
304
  if (hasModel) {
292
305
  payload.model = model;
293
306
  }
307
+ if (hasReasoningEffort) {
308
+ payload.reasoningEffort = reasoningEffort;
309
+ }
310
+ if (hasServiceTier) {
311
+ payload.serviceTier = serviceTier;
312
+ }
294
313
  const result = await controlPlane.runSystemAction(CONTROL_ACTIONS.BOTS_SET_POLICY, payload);
295
314
  res.json(result);
296
315
  }));
@@ -476,13 +495,21 @@ function startCodexDeviceAuthSession() {
476
495
  refreshedBots: [],
477
496
  refreshFailures: [],
478
497
  };
479
- const child = spawn(codexBin, ["login", "--device-auth"], {
480
- cwd: config.kernelRootPath,
481
- shell: false,
482
- stdio: ["ignore", "pipe", "pipe"],
483
- windowsHide: true,
484
- env: process.env,
485
- });
498
+ const child = requiresShellWrappedSpawn(codexBin)
499
+ ? spawn(buildShellWrappedCommandLine(codexBin, ["login", "--device-auth"]), {
500
+ cwd: config.kernelRootPath,
501
+ shell: true,
502
+ stdio: ["ignore", "pipe", "pipe"],
503
+ windowsHide: true,
504
+ env: process.env,
505
+ })
506
+ : spawn(codexBin, ["login", "--device-auth"], {
507
+ cwd: config.kernelRootPath,
508
+ shell: false,
509
+ stdio: ["ignore", "pipe", "pipe"],
510
+ windowsHide: true,
511
+ env: process.env,
512
+ });
486
513
  session.child = child;
487
514
  codexDeviceAuthSession = session;
488
515
  child.stdout.on("data", (chunk) => {
@@ -507,6 +534,7 @@ function startCodexDeviceAuthSession() {
507
534
  botManager: requireBotManager(),
508
535
  })
509
536
  .then((refreshed) => {
537
+ invalidateCodexQuotaUsageCache();
510
538
  session.refreshedBots = refreshed.refreshedBotIds;
511
539
  session.refreshFailures = refreshed.failures;
512
540
  session.status = "succeeded";
@@ -706,6 +734,8 @@ function normalizeModelCatalog(rawModels) {
706
734
  displayName: String(entry?.displayName ?? model).trim() || model,
707
735
  description: String(entry?.description ?? "").trim(),
708
736
  isDefault: entry?.isDefault === true,
737
+ supportedReasoningEfforts: normalizeReasoningCatalog(entry?.supportedReasoningEfforts),
738
+ defaultReasoningEffort: normalizeReasoningEffortValue(entry?.defaultReasoningEffort),
709
739
  });
710
740
  }
711
741
  return normalized.sort((a, b) => {
@@ -725,17 +755,58 @@ function looksLikeCodexApiKey(value) {
725
755
  }
726
756
  return key.startsWith("sk-");
727
757
  }
758
+ function normalizeReasoningCatalog(value) {
759
+ const options = [];
760
+ const seen = new Set();
761
+ for (const entry of Array.isArray(value) ? value : []) {
762
+ const option = isRecord(entry) ? entry : {};
763
+ const reasoningEffort = normalizeReasoningEffortValue(option.reasoningEffort ?? option.effort ?? option.id);
764
+ if (!reasoningEffort || seen.has(reasoningEffort)) {
765
+ continue;
766
+ }
767
+ seen.add(reasoningEffort);
768
+ options.push({
769
+ reasoningEffort,
770
+ description: String(option.description ?? "").trim(),
771
+ });
772
+ }
773
+ return options;
774
+ }
775
+ function normalizeReasoningEffortValue(value) {
776
+ const normalized = String(value ?? "")
777
+ .trim()
778
+ .toLowerCase();
779
+ if (normalized === "none" ||
780
+ normalized === "minimal" ||
781
+ normalized === "low" ||
782
+ normalized === "medium" ||
783
+ normalized === "high" ||
784
+ normalized === "xhigh") {
785
+ return normalized;
786
+ }
787
+ return null;
788
+ }
728
789
  function runCodexCommand(args, { inputText = "" } = {}) {
729
790
  const codexBin = String(config.codexBin ?? "codex").trim() || "codex";
730
- const result = spawnSync(codexBin, args, {
731
- cwd: config.kernelRootPath,
732
- shell: false,
733
- stdio: ["pipe", "pipe", "pipe"],
734
- windowsHide: true,
735
- encoding: "utf8",
736
- input: inputText,
737
- env: process.env,
738
- });
791
+ const result = requiresShellWrappedSpawn(codexBin)
792
+ ? spawnSync(buildShellWrappedCommandLine(codexBin, args), {
793
+ cwd: config.kernelRootPath,
794
+ shell: true,
795
+ stdio: ["pipe", "pipe", "pipe"],
796
+ windowsHide: true,
797
+ encoding: "utf8",
798
+ input: inputText,
799
+ env: process.env,
800
+ })
801
+ : spawnSync(codexBin, args, {
802
+ cwd: config.kernelRootPath,
803
+ shell: false,
804
+ stdio: ["pipe", "pipe", "pipe"],
805
+ windowsHide: true,
806
+ encoding: "utf8",
807
+ input: inputText,
808
+ env: process.env,
809
+ });
739
810
  if (result.error) {
740
811
  return {
741
812
  ok: false,
@@ -764,6 +835,9 @@ function formatCodexSpawnError(codexBin, error) {
764
835
  if (code === "EPERM") {
765
836
  return `Codex binary '${codexBin}' cannot be executed (EPERM).`;
766
837
  }
838
+ if (code === "EINVAL" && requiresShellWrappedSpawn(codexBin)) {
839
+ return `Codex binary '${codexBin}' must be launched through the Windows shell.`;
840
+ }
767
841
  const message = error instanceof Error ? error.message : String(error);
768
842
  return `Failed to execute '${codexBin}': ${firstNonEmptyLine(message)}`;
769
843
  }
@@ -0,0 +1,16 @@
1
+ let cachedCodexUsage = null;
2
+ export function readCachedCodexQuotaSnapshot(now) {
3
+ if (cachedCodexUsage && now < cachedCodexUsage.expiresAt) {
4
+ return cachedCodexUsage.snapshot;
5
+ }
6
+ return null;
7
+ }
8
+ export function writeCachedCodexQuotaSnapshot(snapshot, expiresAt) {
9
+ cachedCodexUsage = {
10
+ expiresAt,
11
+ snapshot,
12
+ };
13
+ }
14
+ export function invalidateCodexQuotaUsageCache() {
15
+ cachedCodexUsage = null;
16
+ }