codex-copilot-dx 0.4.9 → 0.4.11

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
@@ -38,6 +38,16 @@ Claude Code picks up the new `ANTHROPIC_BASE_URL` on its next launch.
38
38
 
39
39
  Do not set Claude Code by manually exporting `ANTHROPIC_BASE_URL` or `ANTHROPIC_AUTH_TOKEN` in your shell. Let `codex-copilot-dx` write the local config files instead. If you previously exported those variables, remove them from shell startup files and restart the terminal before launching Claude Code.
40
40
 
41
+ ### Diagnostics
42
+
43
+ Run a read-only config check without starting the adapter or changing files:
44
+
45
+ ```bash
46
+ codex-copilot-dx doctor
47
+ ```
48
+
49
+ The doctor checks the GitHub token, Codex config, Claude Code settings, Claude App gateway profile, and whether the local adapter port is listening.
50
+
41
51
  ### Claude App opt-in
42
52
 
43
53
  Claude App support is opt-in so the default Codex Desktop and Claude Code setup stays unchanged:
package/bin/cli.mjs CHANGED
@@ -9,6 +9,7 @@ import { refreshVSCodeVersion } from "../src/copilot.mjs";
9
9
  import { status } from "../src/status.mjs";
10
10
  import { printUsageSummary } from "../src/usage.mjs";
11
11
  import { checkForUpdate, localPackageVersion } from "../src/version.mjs";
12
+ import { runDoctor } from "../src/doctor.mjs";
12
13
 
13
14
  const ADAPTER_PORT = parseInt(process.env.ADAPTER_PORT || "2026");
14
15
  const ADAPTER_HOST = process.env.ADAPTER_HOST || "127.0.0.1";
@@ -30,6 +31,11 @@ if (command === "usage") {
30
31
  process.exit(0);
31
32
  }
32
33
 
34
+ if (command === "doctor" || command === "--doctor") {
35
+ await runDoctor({ port: ADAPTER_PORT, host: ADAPTER_HOST });
36
+ process.exit(0);
37
+ }
38
+
33
39
  console.log(`
34
40
  codex-copilot-dx v${LOCAL_VERSION}
35
41
  Use Codex Desktop, Claude Code, and Claude App with GitHub Copilot
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-copilot-dx",
3
- "version": "0.4.9",
3
+ "version": "0.4.11",
4
4
  "description": "Use Codex Desktop, Claude Code, and Claude App with GitHub Copilot",
5
5
  "type": "module",
6
6
  "bin": {
package/src/adapter.mjs CHANGED
@@ -122,6 +122,7 @@ export async function readJsonBody(req, {
122
122
  }
123
123
 
124
124
  function sendJsonError(res, err, fallbackStatus = 400) {
125
+ if (res.destroyed || res.writableEnded) return;
125
126
  if (!res.headersSent) res.writeHead(err?.statusCode || fallbackStatus, { "Content-Type": "application/json" });
126
127
  res.end(JSON.stringify({ error: err?.message || "Request failed" }));
127
128
  }
@@ -164,30 +165,61 @@ export function writeOrDrain(res, chunk) {
164
165
  });
165
166
  }
166
167
 
167
- function createRequestAbort(req, res) {
168
+ export function isAbortLikeError(err) {
169
+ return err?.name === "AbortError" || /operation was aborted/i.test(String(err?.message || ""));
170
+ }
171
+
172
+ export function abortErrorStatusCode(reason) {
173
+ if (reason === "upstream_timeout") return 504;
174
+ if (reason === "client_aborted" || reason === "client_closed") return 499;
175
+ return 400;
176
+ }
177
+
178
+ export function createRequestAbort(req, res) {
168
179
  const controller = new AbortController();
169
180
  let timer = null;
170
181
  let cleaned = false;
171
- const abort = () => {
172
- if (!cleaned && !controller.signal.aborted) controller.abort();
182
+ let reason = null;
183
+ const abort = (nextReason = "aborted") => {
184
+ if (!cleaned && !controller.signal.aborted) {
185
+ reason = nextReason;
186
+ controller.abort();
187
+ }
173
188
  };
174
- req.on("aborted", abort);
175
- res.on("close", abort);
189
+ const onReqAborted = () => abort("client_aborted");
190
+ const onResClose = () => {
191
+ if (!res.writableEnded) abort("client_closed");
192
+ };
193
+ req.on("aborted", onReqAborted);
194
+ res.on("close", onResClose);
176
195
  return {
177
196
  signal: controller.signal,
197
+ get reason() { return reason; },
178
198
  setTimeout(ms) {
179
199
  if (timer) clearTimeout(timer);
180
- if (ms > 0) timer = setTimeout(abort, ms);
200
+ if (ms > 0) timer = setTimeout(() => abort("upstream_timeout"), ms);
181
201
  },
182
202
  cleanup() {
183
203
  cleaned = true;
184
204
  if (timer) clearTimeout(timer);
185
- req.off("aborted", abort);
186
- res.off("close", abort);
205
+ req.off("aborted", onReqAborted);
206
+ res.off("close", onResClose);
187
207
  },
188
208
  };
189
209
  }
190
210
 
211
+ function logRequestFailure(label, err, abort) {
212
+ if (!isAbortLikeError(err)) {
213
+ console.error(status("err", `${label} request failed: ${err.message}`));
214
+ return;
215
+ }
216
+
217
+ const reason = abort?.reason || "aborted";
218
+ err.statusCode ||= abortErrorStatusCode(reason);
219
+ const detail = reason === "upstream_timeout" ? `${reason} after ${UPSTREAM_TIMEOUT_MS}ms` : reason;
220
+ console.warn(status("warn", `${label} request aborted: ${detail}`));
221
+ }
222
+
191
223
  function responsesInputItems(input) {
192
224
  if (input === undefined || input === null) return [];
193
225
  if (typeof input === "string") {
@@ -196,6 +228,17 @@ function responsesInputItems(input) {
196
228
  return Array.isArray(input) ? cloneJson(input) : [cloneJson(input)];
197
229
  }
198
230
 
231
+ export function stripInternalResponsesInputFields(inputItems) {
232
+ if (!Array.isArray(inputItems)) return inputItems;
233
+ for (const item of inputItems) {
234
+ if (!item || typeof item !== "object") continue;
235
+ for (const key of Object.keys(item)) {
236
+ if (key.startsWith("internal_")) delete item[key];
237
+ }
238
+ }
239
+ return inputItems;
240
+ }
241
+
199
242
  function responsesOutputItems(output) {
200
243
  if (!Array.isArray(output)) return [];
201
244
  return cloneJson(output.filter((item) => item?.type === "message" || item?.type === "function_call"));
@@ -303,6 +346,7 @@ export function prepareResponsesRequest(reqBody) {
303
346
  body.tools = body.tools.filter((tool) => tool?.type !== "image_generation");
304
347
  if (!body.tools.length) delete body.tools;
305
348
  }
349
+ stripInternalResponsesInputFields(body.input);
306
350
 
307
351
  return { body, inputItems: body.input };
308
352
  }
@@ -562,13 +606,16 @@ export function startAdapter(port = 2026, host = "127.0.0.1", options = {}) {
562
606
  res.writeHead(200, { "Content-Type": "application/json" });
563
607
  res.end(JSON.stringify(resp));
564
608
  } catch (e) {
565
- if (!res.headersSent) res.writeHead(502);
566
- res.end("Bad Gateway");
609
+ logRequestFailure("Responses", e, abort);
610
+ if (!res.destroyed && !res.writableEnded) {
611
+ if (!res.headersSent) res.writeHead(e?.statusCode || 502);
612
+ res.end("Bad Gateway");
613
+ }
567
614
  }
568
615
  }
569
616
  }
570
617
  } catch (e) {
571
- console.error(status("err", `Responses request failed: ${e.message}`));
618
+ logRequestFailure("Responses", e, abort);
572
619
  sendJsonError(res, e);
573
620
  } finally {
574
621
  abort.cleanup();
@@ -589,7 +636,7 @@ export function startAdapter(port = 2026, host = "127.0.0.1", options = {}) {
589
636
  console.log(status("info", `responses compact model=${model} stream=${!!parsed.stream}`));
590
637
  await proxyCopilotResponses(prepared, req, res, copilotResponsesCompact, { signal: abort.signal });
591
638
  } catch (e) {
592
- console.error(status("err", `Responses compact request failed: ${e.message}`));
639
+ logRequestFailure("Responses compact", e, abort);
593
640
  sendJsonError(res, e);
594
641
  } finally {
595
642
  abort.cleanup();
@@ -608,7 +655,13 @@ export function startAdapter(port = 2026, host = "127.0.0.1", options = {}) {
608
655
  abort.setTimeout(UPSTREAM_TIMEOUT_MS);
609
656
  listModels({ signal: abort.signal })
610
657
  .then(({ status, body }) => { res.writeHead(status, { "Content-Type": "application/json" }); res.end(body); })
611
- .catch((e) => { if (!res.headersSent) res.writeHead(502); res.end(JSON.stringify({ error: e.message })); })
658
+ .catch((e) => {
659
+ logRequestFailure("Models", e, abort);
660
+ if (!res.destroyed && !res.writableEnded) {
661
+ if (!res.headersSent) res.writeHead(e?.statusCode || 502);
662
+ res.end(JSON.stringify({ error: e.message }));
663
+ }
664
+ })
612
665
  .finally(() => abort.cleanup());
613
666
  return;
614
667
  }
@@ -671,7 +724,7 @@ export function startAdapter(port = 2026, host = "127.0.0.1", options = {}) {
671
724
  res.end(JSON.stringify(anthropicMsg));
672
725
  }
673
726
  } catch (e) {
674
- console.error(status("err", `Messages request failed: ${e.message}`));
727
+ logRequestFailure("Messages", e, abort);
675
728
  sendJsonError(res, e);
676
729
  } finally {
677
730
  abort.cleanup();
package/src/auth.mjs CHANGED
@@ -6,7 +6,10 @@ import { status } from "./status.mjs";
6
6
 
7
7
  const CLIENT_ID = "Iv1.b507a08c87ecfe98"; // Public GitHub Copilot client ID.
8
8
  const SCOPE = "read:user";
9
- const GITHUB_TOKEN_PATH = path.join(os.homedir(), ".local", "share", "copilot-api", "github_token");
9
+
10
+ export function githubTokenPath(home = os.homedir()) {
11
+ return path.join(home, ".local", "share", "copilot-api", "github_token");
12
+ }
10
13
 
11
14
  // Map GitHub polling responses to a small local state machine.
12
15
  export function interpretPoll(data) {
@@ -40,6 +43,7 @@ function openAndCopy(userCode, verificationUri) {
40
43
  }
41
44
 
42
45
  export async function ensureAuth() {
46
+ const GITHUB_TOKEN_PATH = githubTokenPath();
43
47
  if (fs.existsSync(GITHUB_TOKEN_PATH)) {
44
48
  console.log(status("ok", "GitHub token found"));
45
49
  return;
@@ -90,6 +94,7 @@ export async function ensureAuth() {
90
94
  }
91
95
 
92
96
  function writeToken(token) {
97
+ const GITHUB_TOKEN_PATH = githubTokenPath();
93
98
  fs.mkdirSync(path.dirname(GITHUB_TOKEN_PATH), { recursive: true });
94
99
  fs.writeFileSync(GITHUB_TOKEN_PATH, token, { mode: 0o600 });
95
100
  }
package/src/doctor.mjs ADDED
@@ -0,0 +1,207 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import os from "node:os";
4
+ import net from "node:net";
5
+ import { githubTokenPath } from "./auth.mjs";
6
+ import { claudeDesktopPaths } from "./claude-desktop-config.mjs";
7
+ import { status } from "./status.mjs";
8
+
9
+ function localGatewayBaseUrl(host, port) {
10
+ const safeHost = String(host || "127.0.0.1");
11
+ const urlHost = safeHost.includes(":") && !safeHost.startsWith("[") ? `[${safeHost}]` : safeHost;
12
+ return `http://${urlHost}:${port}`;
13
+ }
14
+
15
+ function readText(filePath) {
16
+ try {
17
+ return { ok: true, text: fs.readFileSync(filePath, "utf8") };
18
+ } catch (e) {
19
+ return { ok: false, missing: e?.code === "ENOENT", error: e };
20
+ }
21
+ }
22
+
23
+ function readJson(filePath) {
24
+ const raw = readText(filePath);
25
+ if (!raw.ok) return raw;
26
+ try {
27
+ return { ok: true, json: JSON.parse(raw.text) };
28
+ } catch (e) {
29
+ return { ok: false, parseError: true, error: e };
30
+ }
31
+ }
32
+
33
+ function tomlString(content, key) {
34
+ const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
35
+ const match = new RegExp(`^\\s*${escaped}\\s*=\\s*"([^"]*)"`, "m").exec(content);
36
+ return match?.[1] || "";
37
+ }
38
+
39
+ function displayPath(home, filePath) {
40
+ const rel = path.relative(home, filePath);
41
+ if (!rel.startsWith("..") && !path.isAbsolute(rel)) return `~/${rel}`;
42
+ return filePath;
43
+ }
44
+
45
+ function valueLabel(value) {
46
+ return value ? `"${value}"` : "missing";
47
+ }
48
+
49
+ export function inspectGitHubToken({ home = os.homedir() } = {}) {
50
+ const filePath = githubTokenPath(home);
51
+ const token = readText(filePath);
52
+ if (!token.ok) {
53
+ return [{ kind: "warn", message: `GitHub token not found at ${displayPath(home, filePath)}` }];
54
+ }
55
+ if (!token.text.trim()) {
56
+ return [{ kind: "err", message: `GitHub token file is empty at ${displayPath(home, filePath)}` }];
57
+ }
58
+ return [{ kind: "ok", message: "GitHub token found" }];
59
+ }
60
+
61
+ export function inspectCodexConfig({ home = os.homedir(), port = 2026 } = {}) {
62
+ const filePath = path.join(home, ".codex", "config.toml");
63
+ const expectedBaseUrl = `http://127.0.0.1:${port}/v1`;
64
+ const expectedAnthropicBaseUrl = `http://127.0.0.1:${port}`;
65
+ const config = readText(filePath);
66
+ if (!config.ok) {
67
+ return [{ kind: "warn", message: `Codex config not found at ${displayPath(home, filePath)}` }];
68
+ }
69
+
70
+ const checks = [];
71
+ const baseUrl = tomlString(config.text, "openai_base_url");
72
+ checks.push(baseUrl === expectedBaseUrl
73
+ ? { kind: "ok", message: `Codex base URL points to ${expectedBaseUrl}` }
74
+ : { kind: "warn", message: `Codex base URL is ${valueLabel(baseUrl)}; expected "${expectedBaseUrl}"` });
75
+
76
+ const missing = [];
77
+ if (tomlString(config.text, "OPENAI_BASE_URL") !== expectedBaseUrl) missing.push("OPENAI_BASE_URL");
78
+ if (tomlString(config.text, "OPENAI_API_KEY") !== "dummy") missing.push("OPENAI_API_KEY");
79
+ if (tomlString(config.text, "ANTHROPIC_BASE_URL") !== expectedAnthropicBaseUrl) missing.push("ANTHROPIC_BASE_URL");
80
+ if (tomlString(config.text, "ANTHROPIC_AUTH_TOKEN") !== "dummy") missing.push("ANTHROPIC_AUTH_TOKEN");
81
+ checks.push(missing.length === 0
82
+ ? { kind: "ok", message: "Codex shell env local API keys are configured" }
83
+ : { kind: "warn", message: `Codex shell env local API keys need update: ${missing.join(", ")}` });
84
+ return checks;
85
+ }
86
+
87
+ export function inspectClaudeCodeConfig({ home = os.homedir(), port = 2026 } = {}) {
88
+ const filePath = path.join(home, ".claude", "settings.json");
89
+ const expectedBaseUrl = `http://127.0.0.1:${port}`;
90
+ const settings = readJson(filePath);
91
+ if (!settings.ok) {
92
+ const reason = settings.parseError ? `could not parse: ${settings.error.message}` : `not found at ${displayPath(home, filePath)}`;
93
+ return [{ kind: settings.parseError ? "err" : "warn", message: `Claude Code settings ${reason}` }];
94
+ }
95
+
96
+ const env = settings.json?.env || {};
97
+ const missing = [];
98
+ if (env.ANTHROPIC_BASE_URL !== expectedBaseUrl) missing.push("ANTHROPIC_BASE_URL");
99
+ if (env.ANTHROPIC_AUTH_TOKEN !== "dummy") missing.push("ANTHROPIC_AUTH_TOKEN");
100
+ return [missing.length === 0
101
+ ? { kind: "ok", message: `Claude Code points to ${expectedBaseUrl}` }
102
+ : { kind: "warn", message: `Claude Code settings need update: ${missing.join(", ")}` }];
103
+ }
104
+
105
+ export function inspectClaudeAppConfig({
106
+ home = os.homedir(),
107
+ platform = process.platform,
108
+ env = process.env,
109
+ host = "127.0.0.1",
110
+ port = 2026,
111
+ } = {}) {
112
+ const paths = claudeDesktopPaths(home, platform, env);
113
+ const expectedBaseUrl = localGatewayBaseUrl(host, port);
114
+ const checks = [];
115
+
116
+ const normalConfig = readJson(paths.normalConfigPath);
117
+ const threepConfig = readJson(paths.threepConfigPath);
118
+ if (normalConfig.ok && threepConfig.ok
119
+ && normalConfig.json?.deploymentMode === "3p"
120
+ && threepConfig.json?.deploymentMode === "3p") {
121
+ checks.push({ kind: "ok", message: "Claude App deployment mode is 3p" });
122
+ } else {
123
+ checks.push({ kind: "warn", message: "Claude App deployment mode is not fully configured for 3p" });
124
+ }
125
+
126
+ const meta = readJson(paths.metaPath);
127
+ const appliedId = meta.ok ? String(meta.json?.appliedId || "").trim() : "";
128
+ if (!appliedId) {
129
+ checks.push({ kind: "warn", message: "Claude App gateway profile is not configured; run with --configure-claude-desktop" });
130
+ return checks;
131
+ }
132
+
133
+ const profilePath = path.join(paths.configLibraryPath, `${appliedId}.json`);
134
+ const profile = readJson(profilePath);
135
+ if (!profile.ok) {
136
+ checks.push({ kind: "warn", message: `Claude App active gateway profile not found at ${displayPath(home, profilePath)}` });
137
+ return checks;
138
+ }
139
+
140
+ const p = profile.json || {};
141
+ const missing = [];
142
+ if (p.inferenceProvider !== "gateway") missing.push("inferenceProvider");
143
+ if (p.inferenceGatewayBaseUrl !== expectedBaseUrl) missing.push(`inferenceGatewayBaseUrl expected "${expectedBaseUrl}"`);
144
+ if (p.inferenceGatewayAuthScheme !== "bearer") missing.push("inferenceGatewayAuthScheme");
145
+ if (!String(p.inferenceGatewayApiKey || "").trim()) missing.push("inferenceGatewayApiKey");
146
+ if (!String(p.inferenceModels || "").trim()) missing.push("inferenceModels");
147
+ checks.push(missing.length === 0
148
+ ? { kind: "ok", message: `Claude App gateway profile points to ${expectedBaseUrl}` }
149
+ : { kind: "warn", message: `Claude App gateway profile needs update: ${missing.join(", ")}` });
150
+ return checks;
151
+ }
152
+
153
+ function connectHost(host) {
154
+ const normalized = String(host || "127.0.0.1").replace(/^\[(.*)\]$/, "$1");
155
+ if (normalized === "0.0.0.0") return "127.0.0.1";
156
+ if (normalized === "::") return "::1";
157
+ return normalized;
158
+ }
159
+
160
+ export function checkAdapterListening({ host = "127.0.0.1", port = 2026, timeoutMs = 500 } = {}) {
161
+ return new Promise((resolve) => {
162
+ const socket = net.createConnection({ host: connectHost(host), port });
163
+ const done = (ok) => {
164
+ socket.removeAllListeners();
165
+ socket.destroy();
166
+ resolve(ok);
167
+ };
168
+ socket.setTimeout(timeoutMs);
169
+ socket.once("connect", () => done(true));
170
+ socket.once("timeout", () => done(false));
171
+ socket.once("error", () => done(false));
172
+ });
173
+ }
174
+
175
+ export async function collectDoctorChecks({
176
+ home = os.homedir(),
177
+ platform = process.platform,
178
+ env = process.env,
179
+ host = "127.0.0.1",
180
+ port = 2026,
181
+ checkAdapter = true,
182
+ checkAdapterListeningFn = checkAdapterListening,
183
+ } = {}) {
184
+ const checks = [
185
+ ...inspectGitHubToken({ home }),
186
+ ...inspectCodexConfig({ home, port }),
187
+ ...inspectClaudeCodeConfig({ home, port }),
188
+ ...inspectClaudeAppConfig({ home, platform, env, host, port }),
189
+ ];
190
+
191
+ if (checkAdapter) {
192
+ const listening = await checkAdapterListeningFn({ host, port });
193
+ checks.push(listening
194
+ ? { kind: "ok", message: `Adapter is listening on ${localGatewayBaseUrl(host, port)}` }
195
+ : { kind: "warn", message: `Adapter is not listening on ${localGatewayBaseUrl(host, port)}` });
196
+ }
197
+
198
+ return checks;
199
+ }
200
+
201
+ export async function runDoctor(options = {}) {
202
+ const log = options.log || console.log;
203
+ log("codex-copilot-dx doctor");
204
+ const checks = await collectDoctorChecks(options);
205
+ for (const check of checks) log(status(check.kind, check.message));
206
+ return checks;
207
+ }