codex-copilot-dx 0.4.25 → 0.4.26

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.
package/README.md CHANGED
@@ -15,7 +15,7 @@ Supports both HTTP SSE streaming and non-streaming.
15
15
  ## Prerequisites
16
16
 
17
17
  - GitHub Copilot subscription (Individual, Business, or Enterprise)
18
- - Node.js 18+
18
+ - Node.js 18.17+
19
19
  - [Codex Desktop](https://openai.com/codex), [Claude Code](https://claude.com/claude-code), and/or Claude App installed
20
20
 
21
21
  ## Usage
@@ -28,7 +28,7 @@ npx codex-copilot-dx@latest
28
28
 
29
29
  On first run, it will:
30
30
  1. Authenticate with GitHub via device flow (if needed), after first trying compatible local Copilot token sources
31
- 2. Print the local package version and warn when a newer npm release is available
31
+ 2. Print the local package version and check for a newer npm release in the background
32
32
  3. Start the adapter on loopback (`127.0.0.1:2026`)
33
33
  4. Configure Codex (`~/.codex/config.toml`) to use the adapter, including stale shell env base URLs if present
34
34
  5. Configure Claude Code (`~/.claude/settings.json`) to use the adapter; it creates the file when missing, otherwise backs up `settings.json.bak` before updating the local API env keys
@@ -50,6 +50,8 @@ Run a read-only config check without starting the adapter or changing files:
50
50
  codex-copilot-dx doctor
51
51
  ```
52
52
 
53
+ The command exits with status `1` when it finds an invalid configuration and `0` when checks contain only OK or warning results.
54
+
53
55
  The doctor checks the GitHub token, Codex config, Claude Code settings, Claude App gateway profile, and whether the local adapter port is listening.
54
56
 
55
57
  For a read-only live check of the saved GitHub token, Copilot entitlement, and models endpoint, run:
@@ -99,10 +101,13 @@ Environment variables:
99
101
  | `CCDX_MAX_BODY_BYTES` | `134217728` | Maximum compressed/raw request body size |
100
102
  | `CCDX_MAX_DECODED_BODY_BYTES` | `268435456` | Maximum decoded request body size after decompression |
101
103
  | `CCDX_UPSTREAM_TIMEOUT_MS` | `120000` | Timeout for non-streaming upstream Copilot requests |
104
+ | `CCDX_STREAM_HANDSHAKE_TIMEOUT_MS` | `120000` | Timeout while waiting for upstream streaming response headers |
105
+ | `CCDX_STREAM_IDLE_TIMEOUT_MS` | `120000` | Maximum idle time between upstream streaming body chunks |
102
106
  | `CCDX_UPSTREAM_RETRIES` | `2` | Retries for transient Copilot upstream network errors; capped at `5` |
103
107
  | `CCDX_UPSTREAM_RETRY_DELAY_MS` | `300` | Initial upstream retry backoff in milliseconds; capped at `5000` |
104
108
  | `CCDX_LOG_PATH` | unset | Mirror terminal logs to a file; set to `1` for `~/.local/share/codex-copilot-dx/debug.log` |
105
109
  | `CCDX_LOG_LEVEL` | `info` | Set to `debug` to include upstream request attempts, status codes, retry causes, and timings |
110
+ | `CCDX_LOG_MAX_BYTES` | `16777216` | Rotate the debug log at this size, retaining one `.1` backup; set to `0` to disable rotation |
106
111
  | `CCDX_IMG_MAX_DIM` | `2048` | Max long edge in pixels for image downscaling |
107
112
  | `CCDX_IMG_QUALITY` | `85` | WebP quality used when re-encoding images |
108
113
  | `CCDX_IMG_MIN_BYTES` | `100000` | Images smaller than this are left untouched |
@@ -123,7 +128,9 @@ Environment variables:
123
128
  | `CCDX_RESPONSE_HISTORY_MAX_BYTES` | `67108864` | Total in-memory byte budget for locally expanded Responses history |
124
129
  | `CCDX_RESPONSE_HISTORY_MAX_ENTRIES` | `4096` | Maximum stored incremental Responses history nodes |
125
130
  | `CCDX_USAGE_PATH` | `~/.local/share/codex-copilot-dx/usage.jsonl` | Local JSONL token usage log |
131
+ | `CCDX_USAGE_MAX_BYTES` | `33554432` | Rotate the usage log at this size, retaining one `.1` backup; set to `0` to disable rotation |
126
132
  | `CCDX_DISABLE_USAGE` | unset | Set to `1` to disable usage logging |
133
+ | `CCDX_SHUTDOWN_TIMEOUT_MS` | `5000` | Time to drain active HTTP connections before forcing shutdown |
127
134
 
128
135
  ### Usage logging
129
136
 
@@ -148,3 +155,12 @@ Newer ChatGPT/Codex clients can advertise an `image_gen` namespace that already
148
155
  ## License
149
156
 
150
157
  MIT
158
+
159
+ ## Development
160
+
161
+ ```bash
162
+ npm ci
163
+ npm run verify
164
+ ```
165
+
166
+ `npm test` runs the unit and handler-level suite. `npm run test:smoke` starts a real local HTTP adapter with fully injected offline upstreams. `npm run pack:check` verifies the npm tarball contents without publishing. The CI workflow runs all three checks on supported Node.js release lines.
package/bin/cli.mjs CHANGED
@@ -9,22 +9,58 @@ import { cacheModelEndpoints, listModels, refreshVSCodeVersion } from "../src/co
9
9
  import { claudeDesktopModelDefsFromCopilotModels, claudeDesktopModelIds, gptModelIdsFromCopilotModels, parseModelAliasEnv } from "../src/models.mjs";
10
10
  import { status } from "../src/status.mjs";
11
11
  import { configureLogging } from "../src/log.mjs";
12
- import { printUsageSummary } from "../src/usage.mjs";
12
+ import { flushUsageWrites, printUsageSummary } from "../src/usage.mjs";
13
13
  import { checkForUpdate, localPackageVersion } from "../src/version.mjs";
14
14
  import { runDoctor } from "../src/doctor.mjs";
15
15
  import { checkRunningAdapter } from "../src/running-adapter.mjs";
16
16
  import { assertSafeAdapterHost, isLoopbackHost } from "../src/security.mjs";
17
17
  import { loadModelCache, saveModelCache } from "../src/model-cache.mjs";
18
+ import { initializeModelRegistry, runInBackground } from "../src/startup.mjs";
19
+ import { cliHelp, parseCliArgs, parseRuntimeOptions } from "../src/cli-options.mjs";
20
+ import { closeHttpServer } from "../src/shutdown.mjs";
18
21
 
19
- const ADAPTER_PORT = parseInt(process.env.ADAPTER_PORT || "2026");
20
- const ADAPTER_HOST = process.env.ADAPTER_HOST || "127.0.0.1";
21
- const MODEL_REFRESH_TIMEOUT_MS = parseInt(process.env.CCDX_MODEL_REFRESH_TIMEOUT_MS || "5000", 10);
22
- const EXISTING_ADAPTER_TIMEOUT_MS = parseInt(process.env.CCDX_EXISTING_ADAPTER_TIMEOUT_MS || "500", 10);
23
22
  const LOCAL_VERSION = localPackageVersion();
24
- const command = process.argv[2];
25
- const CONFIGURE_CLAUDE_DESKTOP = command === "--configure-claude-desktop" || process.env.CCDX_CONFIGURE_CLAUDE_DESKTOP === "1";
23
+
24
+ let CLI;
25
+ try {
26
+ CLI = parseCliArgs(process.argv.slice(2));
27
+ } catch (e) {
28
+ console.error(e.message);
29
+ console.error("Run codex-copilot-dx --help for usage.");
30
+ process.exit(2);
31
+ }
32
+
33
+ if (CLI.command === "help") {
34
+ console.log(cliHelp());
35
+ process.exit(0);
36
+ }
37
+ if (CLI.command === "version") {
38
+ console.log(`codex-copilot-dx v${LOCAL_VERSION}`);
39
+ process.exit(0);
40
+ }
41
+ if (CLI.command === "usage") {
42
+ await printUsageSummary();
43
+ process.exit(0);
44
+ }
45
+
46
+ let RUNTIME;
47
+ try {
48
+ RUNTIME = parseRuntimeOptions(process.env);
49
+ } catch (e) {
50
+ console.error(e.message);
51
+ process.exit(2);
52
+ }
53
+
54
+ const ADAPTER_PORT = RUNTIME.adapterPort;
55
+ const ADAPTER_HOST = RUNTIME.adapterHost;
56
+ const MODEL_REFRESH_TIMEOUT_MS = RUNTIME.modelRefreshTimeoutMs;
57
+ const EXISTING_ADAPTER_TIMEOUT_MS = RUNTIME.existingAdapterTimeoutMs;
58
+ const CONFIGURE_CLAUDE_DESKTOP = CLI.configureClaudeDesktop || process.env.CCDX_CONFIGURE_CLAUDE_DESKTOP === "1";
26
59
  const LOGGING = configureLogging();
27
60
  const MODEL_REGISTRY = { modelDefs: undefined, source: "built-in" };
61
+ let activeServer = null;
62
+ let modelRefreshTimer = null;
63
+ let shuttingDown = false;
28
64
 
29
65
  if (LOGGING.filePath) {
30
66
  console.log(status("info", `Debug log: ${LOGGING.filePath}`));
@@ -140,23 +176,13 @@ async function reuseRunningAdapterIfAvailable() {
140
176
  return true;
141
177
  }
142
178
 
143
- if (command === "--version" || command === "-v" || command === "version") {
144
- console.log(`codex-copilot-dx v${LOCAL_VERSION}`);
145
- process.exit(0);
146
- }
147
-
148
- if (command === "usage") {
149
- await printUsageSummary();
150
- process.exit(0);
151
- }
152
-
153
- if (command === "doctor" || command === "--doctor") {
154
- await runDoctor({
179
+ if (CLI.command === "doctor") {
180
+ const checks = await runDoctor({
155
181
  port: ADAPTER_PORT,
156
182
  host: ADAPTER_HOST,
157
- online: process.argv.slice(3).includes("--online"),
183
+ online: CLI.online,
158
184
  });
159
- process.exit(0);
185
+ process.exit(checks.some((check) => check.kind === "err") ? 1 : 0);
160
186
  }
161
187
 
162
188
  console.log(`
@@ -175,21 +201,22 @@ async function printUpdateNotice() {
175
201
  }
176
202
  }
177
203
 
178
- // Await the update check up front so the notice is shown even on the
179
- // "reuse existing adapter" path, which exits early below.
180
- await printUpdateNotice();
181
-
182
204
  try {
183
205
  assertSafeAdapterHost(ADAPTER_HOST, process.env);
206
+ void runInBackground(printUpdateNotice);
184
207
  if (await reuseRunningAdapterIfAvailable()) process.exit(0);
185
208
 
186
209
  // Ensure GitHub login, using device flow if no token exists.
187
210
  await ensureAuth();
188
211
 
189
212
  // Refresh the VS Code version in the background; fallback is non-blocking.
190
- refreshVSCodeVersion();
191
- loadCachedModelRegistry();
192
- const claudeDesktopModelDefs = await refreshClaudeDesktopModelDefs();
213
+ void refreshVSCodeVersion();
214
+ const modelInitialization = await initializeModelRegistry({
215
+ loadCached: loadCachedModelRegistry,
216
+ currentModelDefs: () => MODEL_REGISTRY.modelDefs,
217
+ refresh: refreshClaudeDesktopModelDefs,
218
+ });
219
+ const claudeDesktopModelDefs = modelInitialization.modelDefs;
193
220
 
194
221
  if (!isLoopbackHost(ADAPTER_HOST)) {
195
222
  console.log(status("warn", `ADAPTER_HOST=${ADAPTER_HOST} exposes the adapter beyond loopback because CCDX_ALLOW_LAN=1 is set. Use only on trusted networks.`));
@@ -200,7 +227,14 @@ try {
200
227
  : currentClaudeDesktopApiKey();
201
228
 
202
229
  // Start the in-process adapter.
203
- await startAdapter(ADAPTER_PORT, ADAPTER_HOST, { claudeDesktopApiKey, claudeDesktopModelDefs, modelRegistry: MODEL_REGISTRY });
230
+ activeServer = await startAdapter(ADAPTER_PORT, ADAPTER_HOST, {
231
+ claudeDesktopApiKey,
232
+ claudeDesktopModelDefs,
233
+ modelRegistry: MODEL_REGISTRY,
234
+ upstreamTimeoutMs: RUNTIME.upstreamTimeoutMs,
235
+ streamHandshakeTimeoutMs: RUNTIME.streamHandshakeTimeoutMs,
236
+ streamIdleTimeoutMs: RUNTIME.streamIdleTimeoutMs,
237
+ });
204
238
 
205
239
  // Point Codex and Claude Code at the adapter.
206
240
  ensureCodexConfig(ADAPTER_PORT);
@@ -223,12 +257,12 @@ try {
223
257
 
224
258
  // Periodically refresh the model list + endpoint cache so a long-running
225
259
  // adapter picks up newly released models without a restart.
226
- const refreshIntervalMs = parseInt(process.env.CCDX_MODEL_REFRESH_INTERVAL_MS || String(30 * 60 * 1000), 10);
260
+ const refreshIntervalMs = RUNTIME.modelRefreshIntervalMs;
227
261
  if (Number.isFinite(refreshIntervalMs) && refreshIntervalMs > 0) {
228
- const timer = setInterval(() => {
262
+ modelRefreshTimer = setInterval(() => {
229
263
  refreshClaudeDesktopModelDefs().catch(() => {});
230
264
  }, refreshIntervalMs);
231
- if (typeof timer.unref === "function") timer.unref();
265
+ modelRefreshTimer.unref?.();
232
266
  }
233
267
 
234
268
  console.log(`
@@ -239,11 +273,30 @@ try {
239
273
  Press Ctrl+C to stop.
240
274
  `);
241
275
 
242
- process.on("SIGINT", () => {
243
- console.log(`\n${status("wait", "Shutting down...")}`);
244
- process.exit(0);
245
- });
276
+ const shutdown = async (signal) => {
277
+ if (shuttingDown) return;
278
+ shuttingDown = true;
279
+ console.log(`\n${status("wait", `Shutting down on ${signal}...`)}`);
280
+ let exitCode = 0;
281
+ try {
282
+ if (modelRefreshTimer) clearInterval(modelRefreshTimer);
283
+ const result = await closeHttpServer(activeServer, { timeoutMs: RUNTIME.shutdownTimeoutMs });
284
+ if (result.forced) console.warn(status("warn", "Forced remaining adapter connections closed"));
285
+ await flushUsageWrites();
286
+ } catch (e) {
287
+ exitCode = 1;
288
+ console.error(status("err", `Shutdown failed: ${e.message}`));
289
+ } finally {
290
+ LOGGING.cleanup();
291
+ process.exit(exitCode);
292
+ }
293
+ };
294
+ process.once("SIGINT", () => { void shutdown("SIGINT"); });
295
+ process.once("SIGTERM", () => { void shutdown("SIGTERM"); });
246
296
  } catch (e) {
247
297
  console.error(status("err", e.message));
298
+ await closeHttpServer(activeServer, { timeoutMs: RUNTIME.shutdownTimeoutMs }).catch(() => {});
299
+ await flushUsageWrites();
300
+ LOGGING.cleanup();
248
301
  process.exit(1);
249
302
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-copilot-dx",
3
- "version": "0.4.25",
3
+ "version": "0.4.26",
4
4
  "description": "Use Codex Desktop, Claude Code, and Claude App with GitHub Copilot",
5
5
  "type": "module",
6
6
  "bin": {
@@ -25,8 +25,15 @@
25
25
  "responses-api"
26
26
  ],
27
27
  "license": "MIT",
28
+ "engines": {
29
+ "node": ">=18.17"
30
+ },
28
31
  "scripts": {
29
- "test": "node --test"
32
+ "test": "node --test",
33
+ "test:smoke": "node scripts/offline-smoke.mjs",
34
+ "test:e2e": "bash test/e2e.sh",
35
+ "pack:check": "npm pack --dry-run --json",
36
+ "verify": "npm test && npm run test:smoke && npm run pack:check"
30
37
  },
31
38
  "dependencies": {
32
39
  "gpt-tokenizer": "^3.4.0",