codex-copilot-dx 0.5.4 → 0.5.6

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
@@ -33,6 +33,21 @@ global install, use `npx codex-copilot-dx@latest`.
33
33
  Both installed commands expose the same options and subcommands; help, version,
34
34
  doctor, status, and argument-error output use the command name that was invoked.
35
35
 
36
+ To query the models currently advertised as selectable for the saved Copilot
37
+ account, run:
38
+
39
+ ```bash
40
+ ccdx models
41
+ ```
42
+
43
+ This performs a fresh, read-only GitHub Copilot model-directory lookup without
44
+ starting the adapter, opening device login, reading the local model cache, or
45
+ consuming an inference request. It lists model IDs by provider with their
46
+ advertised API capabilities and reports the live Claude/Anthropic count. A model
47
+ being advertised does not guarantee that a later inference request will avoid
48
+ quota, rate-limit, or policy enforcement. `codex-copilot-dx models` is fully
49
+ equivalent.
50
+
36
51
  To change the model used by Codex Auto-review, run:
37
52
 
38
53
  ```bash
@@ -60,6 +75,8 @@ ccdx update npm
60
75
  ccdx update github
61
76
  ```
62
77
 
78
+ `ccdx update gh` is accepted as a shorthand for the GitHub source.
79
+
63
80
  The npm source installs `codex-copilot-dx@latest` through the registry already
64
81
  configured for npm, including a company mirror. The GitHub source installs the
65
82
  latest commit from `DaleXiao/codex-copilot-dx` `main` and opts in to Git fetching
@@ -68,6 +85,8 @@ for that command, as required by npm 12. It does not change npm's persistent
68
85
  interpolation. A currently running adapter keeps its loaded version until it is
69
86
  stopped and started again. The
70
87
  `codex-copilot-dx update` compatibility command behaves identically.
88
+ If a configured npm mirror has not synchronized the current release yet, use
89
+ the GitHub source to install the current `main` revision.
71
90
 
72
91
  On first run, it will:
73
92
  1. Authenticate with GitHub via device flow (if needed), after first trying compatible local Copilot token sources
@@ -194,7 +213,9 @@ Environment variables:
194
213
  | `CCDX_IMG_CACHE_MAX_BYTES` | `67108864` | Process-local byte ceiling for cached image optimization results; set to `0` to disable result caching |
195
214
  | `CCDX_DISABLE_IMG_OPT` | unset | Set to `1` to disable image optimization |
196
215
  | `CCDX_CONFIGURE_CLAUDE_DESKTOP` | unset | Set to `1` to write the Claude App 3P gateway profile during startup |
216
+ | `CCDX_AUTO_LAUNCH` | enabled | Set to `0`, `false`, `no`, or `off` to start the adapter without opening Codex or ChatGPT |
197
217
  | `CCDX_CLAUDE_DESKTOP_API_KEY` | managed profile or generated for opt-in setup | Explicit bearer key written into the Claude App profile and recognized by the adapter for model discovery |
218
+ | `CCDX_PROXY_API_KEY` | unset | Backward-compatible alias for `CCDX_CLAUDE_DESKTOP_API_KEY`; the Claude-specific variable takes precedence |
198
219
  | `CCDX_CLAUDE_MODEL_ALIASES` | built-in Claude aliases | Comma-separated Desktop-to-upstream aliases, for example `claude-sonnet-4-6=claude-sonnet-4.6` |
199
220
  | `CCDX_GITHUB_TOKEN` | unset | Explicit GitHub Copilot OAuth token to validate and import before device login |
200
221
  | `CCDX_GITHUB_TOKEN_PATH` | unset | Explicit file containing a GitHub Copilot OAuth token to validate and import before device login |
@@ -204,6 +225,7 @@ Environment variables:
204
225
  | `CCDX_TOKEN_LOCK_TIMEOUT_MS` | `600000` | Maximum time to wait for another local process to finish GitHub token login/import |
205
226
  | `CCDX_TOKEN_LOCK_STALE_MS` | `900000` | Age after which a stale GitHub token lock file can be removed |
206
227
  | `CCDX_EXISTING_ADAPTER_TIMEOUT_MS` | `500` | Timeout for detecting an already-running local adapter during startup |
228
+ | `CCDX_MODEL_REFRESH_TIMEOUT_MS` | `5000` | Timeout for a Copilot model metadata refresh or live Auto-review model lookup |
207
229
  | `CCDX_MODEL_REFRESH_INTERVAL_MS` | `7200000` | Interval for refreshing Copilot model metadata; successful lists are cached locally as last-known-good data |
208
230
  | `CCDX_RESPONSE_HISTORY_MAX_BYTES` | `67108864` | Total in-memory byte budget for locally expanded Responses history |
209
231
  | `CCDX_RESPONSE_HISTORY_MAX_ENTRIES` | `4096` | Maximum stored incremental Responses history nodes |
@@ -216,14 +238,14 @@ Environment variables:
216
238
 
217
239
  The adapter records token usage metadata to `~/.local/share/codex-copilot-dx/usage.jsonl` when upstream responses include usage fields. It logs counts, model names, API surface, and response IDs only; it does not log prompts, completions, tool arguments, or image content.
218
240
 
219
- ### Debug logging
220
-
221
- Set `CCDX_LOG_PATH=1` to mirror terminal logs to `~/.local/share/codex-copilot-dx/debug.log`, or set `CCDX_LOG_PATH` to a custom file. Add `CCDX_LOG_LEVEL=debug` to include upstream request attempts, retry causes, status codes, and timings. Debug logs do not include prompts, completions, request bodies, or authorization tokens.
222
-
223
241
  ```bash
224
242
  ccdx usage
225
243
  ```
226
244
 
245
+ ### Debug logging
246
+
247
+ Set `CCDX_LOG_PATH=1` to mirror terminal logs to `~/.local/share/codex-copilot-dx/debug.log`, or set `CCDX_LOG_PATH` to a custom file. Add `CCDX_LOG_LEVEL=debug` to include upstream request attempts, retry causes, status codes, and timings. Debug logs do not include prompts, completions, request bodies, or authorization tokens.
248
+
227
249
  ### Image optimization
228
250
 
229
251
  Long computer-use sessions can accumulate screenshots inside the conversation history. Each screenshot is shipped on later turns, and GitHub Copilot's `/responses` endpoint can reject oversized requests with `408 Request Timeout` while reading the body or `413 Payload Too Large`.
package/bin/cli.mjs CHANGED
@@ -21,6 +21,7 @@ import { formatAdapterStatus, readAdapterStatus } from "../src/cli-status.mjs";
21
21
  import { closeHttpServer } from "../src/shutdown.mjs";
22
22
  import { runAutoReviewModelCommand } from "../src/auto-review-model.mjs";
23
23
  import { autoReviewModelPreference } from "../src/user-settings.mjs";
24
+ import { fetchLiveCopilotModels, formatLiveCopilotModels } from "../src/cli-models.mjs";
24
25
 
25
26
  const LOCAL_VERSION = localPackageVersion();
26
27
  const CLI_NAME = cliCommandName();
@@ -68,6 +69,16 @@ if (CLI.command === "status") {
68
69
  process.exit(1);
69
70
  }
70
71
  }
72
+ if (CLI.command === "models") {
73
+ try {
74
+ const catalog = await fetchLiveCopilotModels();
75
+ console.log(formatLiveCopilotModels(catalog, { commandName: CLI_NAME }));
76
+ process.exit(0);
77
+ } catch (e) {
78
+ console.error(status("err", e.message));
79
+ process.exit(1);
80
+ }
81
+ }
71
82
  if (CLI.command === "auto-review-model") {
72
83
  try {
73
84
  await runAutoReviewModelCommand({ commandName: CLI_NAME });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-copilot-dx",
3
- "version": "0.5.4",
3
+ "version": "0.5.6",
4
4
  "description": "Use Codex Desktop, Claude Code, and Claude App with GitHub Copilot",
5
5
  "author": "Dale Xiao",
6
6
  "type": "module",
@@ -0,0 +1,164 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import { githubTokenPath, validateGithubToken } from "./auth.mjs";
4
+ import { buildHeaders, DEFAULT_API_BASE, FALLBACK_VSCODE_VERSION } from "./copilot.mjs";
5
+ import { status } from "./status.mjs";
6
+
7
+ const DEFAULT_MODELS_TIMEOUT_MS = 10000;
8
+ const SUPPORTED_ENDPOINTS = [
9
+ ["responses", new Set(["/responses", "/v1/responses"])],
10
+ ["chat", new Set(["/chat/completions"])],
11
+ ["messages", new Set(["/v1/messages"])],
12
+ ];
13
+
14
+ function safeText(value, fallback = "") {
15
+ const text = String(value ?? "")
16
+ .replace(/[\u0000-\u001f\u007f-\u009f]/g, " ")
17
+ .replace(/\s+/g, " ")
18
+ .trim();
19
+ return text || fallback;
20
+ }
21
+
22
+ function endpointLabels(model) {
23
+ const advertised = new Set(Array.isArray(model?.supported_endpoints) ? model.supported_endpoints : []);
24
+ return SUPPORTED_ENDPOINTS
25
+ .filter(([, values]) => [...values].some((value) => advertised.has(value)))
26
+ .map(([label]) => label);
27
+ }
28
+
29
+ export function selectableCopilotModels(payload) {
30
+ const data = Array.isArray(payload) ? payload : payload?.data;
31
+ if (!Array.isArray(data)) throw new Error("Copilot models response contained no model list");
32
+
33
+ const models = [];
34
+ const seen = new Set();
35
+ for (const model of data) {
36
+ if (model?.model_picker_enabled !== true) continue;
37
+ const policyState = safeText(model?.policy?.state).toLowerCase();
38
+ if (policyState && policyState !== "enabled") continue;
39
+ const capabilityType = safeText(model?.capabilities?.type).toLowerCase();
40
+ if (capabilityType && capabilityType !== "chat") continue;
41
+
42
+ const id = safeText(model?.id);
43
+ const endpoints = endpointLabels(model);
44
+ if (!id || id === "codex-auto-review" || !endpoints.length || seen.has(id)) continue;
45
+ seen.add(id);
46
+ models.push({
47
+ id,
48
+ vendor: safeText(model?.vendor || model?.owned_by, "Unknown"),
49
+ endpoints,
50
+ preview: model?.preview === true,
51
+ });
52
+ }
53
+
54
+ models.sort((left, right) => left.vendor.localeCompare(right.vendor, "en")
55
+ || left.id.localeCompare(right.id, "en"));
56
+ return { advertised: data.length, models };
57
+ }
58
+
59
+ function validationFailure(validation) {
60
+ const httpStatus = validation?.status ? ` (HTTP ${validation.status})` : "";
61
+ return `GitHub Copilot authentication failed: ${validation?.reason || "unknown error"}${httpStatus}`;
62
+ }
63
+
64
+ async function responseText(response) {
65
+ try {
66
+ return await response.text();
67
+ } catch {
68
+ return "";
69
+ }
70
+ }
71
+
72
+ export async function fetchLiveCopilotModels({
73
+ home = os.homedir(),
74
+ fetchImpl = fetch,
75
+ timeoutMs = DEFAULT_MODELS_TIMEOUT_MS,
76
+ vscodeVersion = FALLBACK_VSCODE_VERSION,
77
+ } = {}) {
78
+ const tokenPath = githubTokenPath(home);
79
+ let githubToken;
80
+ try {
81
+ githubToken = fs.readFileSync(tokenPath, "utf8").trim();
82
+ } catch (error) {
83
+ if (error?.code === "ENOENT") throw new Error("GitHub token not found. Start ccdx once to log in.");
84
+ throw new Error(`Could not read the GitHub token: ${error.message}`);
85
+ }
86
+ if (!githubToken) throw new Error("GitHub token file is empty. Start ccdx again to log in.");
87
+
88
+ const controller = new AbortController();
89
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
90
+ try {
91
+ const validation = await validateGithubToken(githubToken, {
92
+ fetchImpl,
93
+ signal: controller.signal,
94
+ });
95
+ if (controller.signal.aborted) throw new Error(`Live model lookup timed out after ${timeoutMs}ms`);
96
+ if (!validation.ok) throw new Error(validationFailure(validation));
97
+
98
+ const tokenData = validation.copilotTokenData;
99
+ const apiBase = tokenData.endpoints?.api || DEFAULT_API_BASE;
100
+ const headers = buildHeaders({
101
+ token: tokenData.token,
102
+ version: vscodeVersion,
103
+ initiator: "user",
104
+ vision: false,
105
+ });
106
+ const response = await fetchImpl(`${apiBase}/models`, {
107
+ headers,
108
+ redirect: "error",
109
+ signal: controller.signal,
110
+ });
111
+ const body = await responseText(response);
112
+ if (!response.ok) {
113
+ const detail = safeText(body).slice(0, 240);
114
+ throw new Error(`Copilot models endpoint returned HTTP ${response.status}${detail ? `: ${detail}` : ""}`);
115
+ }
116
+
117
+ let payload;
118
+ try {
119
+ payload = JSON.parse(body);
120
+ } catch {
121
+ throw new Error("Copilot models endpoint returned invalid JSON");
122
+ }
123
+ const catalog = selectableCopilotModels(payload);
124
+ let upstreamHost = "GitHub Copilot";
125
+ try { upstreamHost = new URL(apiBase).hostname; } catch {}
126
+ return { ...catalog, upstreamHost: safeText(upstreamHost, "GitHub Copilot") };
127
+ } catch (error) {
128
+ if (controller.signal.aborted && !/timed out/.test(error?.message || "")) {
129
+ throw new Error(`Live model lookup timed out after ${timeoutMs}ms`);
130
+ }
131
+ throw error;
132
+ } finally {
133
+ clearTimeout(timer);
134
+ }
135
+ }
136
+
137
+ export function formatLiveCopilotModels(catalog, { commandName = "ccdx" } = {}) {
138
+ const models = Array.isArray(catalog?.models) ? catalog.models : [];
139
+ const responses = models.filter((model) => model.endpoints.includes("responses")).length;
140
+ const chat = models.filter((model) => model.endpoints.includes("chat")).length;
141
+ const claude = models.filter((model) => model.id.toLowerCase().startsWith("claude-")
142
+ || model.vendor.toLowerCase() === "anthropic").length;
143
+ const lines = [
144
+ `${commandName} models`,
145
+ status("ok", `Live catalog from ${safeText(catalog?.upstreamHost, "GitHub Copilot")}: ${models.length} selectable of ${Number(catalog?.advertised) || 0} advertised`),
146
+ status("info", `Responses: ${responses}; Chat: ${chat}; Claude/Anthropic: ${claude}`),
147
+ ];
148
+
149
+ if (!models.length) {
150
+ lines.push(status("warn", "No selectable models were advertised for this account"));
151
+ return lines.join("\n");
152
+ }
153
+
154
+ let vendor = "";
155
+ for (const model of models) {
156
+ if (model.vendor !== vendor) {
157
+ vendor = model.vendor;
158
+ lines.push("", `${vendor}:`);
159
+ }
160
+ const flags = [...model.endpoints, ...(model.preview ? ["preview"] : [])];
161
+ lines.push(` ${model.id} [${flags.join(", ")}]`);
162
+ }
163
+ return lines.join("\n");
164
+ }
@@ -26,6 +26,10 @@ export function parseCliArgs(args = []) {
26
26
  if (rest.length) unexpectedArgs(rest);
27
27
  return { command: "usage", configureClaudeDesktop: false, showRequestId: false, online: false, compat: false };
28
28
  }
29
+ if (command === "models") {
30
+ if (rest.length) unexpectedArgs(rest);
31
+ return { command: "models", configureClaudeDesktop: false, showRequestId: false, online: false, compat: false };
32
+ }
29
33
  if (command === "auto-review-model") {
30
34
  if (rest.length) unexpectedArgs(rest);
31
35
  return { command: "auto-review-model", configureClaudeDesktop: false, showRequestId: false, online: false, compat: false };
@@ -125,6 +129,7 @@ export function cliHelp(commandName = "ccdx") {
125
129
  ${name} [--configure-claude-desktop] [--show-request-id]
126
130
  ${name} doctor [--online] [--compat]
127
131
  ${name} status
132
+ ${name} models
128
133
  ${name} usage
129
134
  ${name} auto-review-model
130
135
  ${name} update [npm|github]