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 +18 -2
- package/bin/cli.mjs +89 -36
- package/package.json +9 -2
- package/src/adapter.mjs +111 -379
- package/src/atomic-file.mjs +63 -0
- package/src/claude-config.mjs +7 -7
- package/src/claude-desktop-config.mjs +3 -4
- package/src/cli-options.mjs +69 -0
- package/src/config.mjs +10 -6
- package/src/copilot.mjs +20 -161
- package/src/file-rotation.mjs +30 -0
- package/src/http-transport.mjs +182 -0
- package/src/image-optimization.mjs +151 -0
- package/src/log.mjs +9 -1
- package/src/response-history.mjs +195 -0
- package/src/shutdown.mjs +29 -0
- package/src/startup.mjs +19 -0
- package/src/stream.mjs +2 -1
- package/src/usage.mjs +57 -14
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
|
|
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
|
-
|
|
25
|
-
|
|
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 === "
|
|
144
|
-
|
|
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:
|
|
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
|
-
|
|
192
|
-
|
|
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, {
|
|
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 =
|
|
260
|
+
const refreshIntervalMs = RUNTIME.modelRefreshIntervalMs;
|
|
227
261
|
if (Number.isFinite(refreshIntervalMs) && refreshIntervalMs > 0) {
|
|
228
|
-
|
|
262
|
+
modelRefreshTimer = setInterval(() => {
|
|
229
263
|
refreshClaudeDesktopModelDefs().catch(() => {});
|
|
230
264
|
}, refreshIntervalMs);
|
|
231
|
-
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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.
|
|
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",
|