codex-copilot-dx 0.5.3 → 0.5.5

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
@@ -60,6 +60,8 @@ ccdx update npm
60
60
  ccdx update github
61
61
  ```
62
62
 
63
+ `ccdx update gh` is accepted as a shorthand for the GitHub source.
64
+
63
65
  The npm source installs `codex-copilot-dx@latest` through the registry already
64
66
  configured for npm, including a company mirror. The GitHub source installs the
65
67
  latest commit from `DaleXiao/codex-copilot-dx` `main` and opts in to Git fetching
@@ -68,6 +70,8 @@ for that command, as required by npm 12. It does not change npm's persistent
68
70
  interpolation. A currently running adapter keeps its loaded version until it is
69
71
  stopped and started again. The
70
72
  `codex-copilot-dx update` compatibility command behaves identically.
73
+ If a configured npm mirror has not synchronized the current release yet, use
74
+ the GitHub source to install the current `main` revision.
71
75
 
72
76
  On first run, it will:
73
77
  1. Authenticate with GitHub via device flow (if needed), after first trying compatible local Copilot token sources
@@ -185,6 +189,7 @@ Environment variables:
185
189
  | `CCDX_LOG_PATH` | unset | Mirror terminal logs to a file; set to `1` for `~/.local/share/codex-copilot-dx/debug.log` |
186
190
  | `CCDX_LOG_LEVEL` | `info` | Set to `debug` to include upstream request attempts, status codes, retry causes, and timings |
187
191
  | `CCDX_LOG_MAX_BYTES` | `16777216` | Rotate the debug log at this size, retaining one `.1` backup; set to `0` to disable rotation |
192
+ | `CCDX_TERMINAL_ANIMATION` | auto | Show a transient comet while an interactive terminal has active requests and no new output for 800ms; set to `0` to disable |
188
193
  | `CCDX_IMG_MAX_DIM` | `2048` | Max long edge in pixels for image downscaling |
189
194
  | `CCDX_IMG_QUALITY` | `82` | Initial WebP quality used when re-encoding images |
190
195
  | `CCDX_IMG_MIN_BYTES` | `100000` | In-bounds images smaller than this are left untouched; oversized images are still downscaled |
@@ -193,7 +198,9 @@ Environment variables:
193
198
  | `CCDX_IMG_CACHE_MAX_BYTES` | `67108864` | Process-local byte ceiling for cached image optimization results; set to `0` to disable result caching |
194
199
  | `CCDX_DISABLE_IMG_OPT` | unset | Set to `1` to disable image optimization |
195
200
  | `CCDX_CONFIGURE_CLAUDE_DESKTOP` | unset | Set to `1` to write the Claude App 3P gateway profile during startup |
201
+ | `CCDX_AUTO_LAUNCH` | enabled | Set to `0`, `false`, `no`, or `off` to start the adapter without opening Codex or ChatGPT |
196
202
  | `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 |
203
+ | `CCDX_PROXY_API_KEY` | unset | Backward-compatible alias for `CCDX_CLAUDE_DESKTOP_API_KEY`; the Claude-specific variable takes precedence |
197
204
  | `CCDX_CLAUDE_MODEL_ALIASES` | built-in Claude aliases | Comma-separated Desktop-to-upstream aliases, for example `claude-sonnet-4-6=claude-sonnet-4.6` |
198
205
  | `CCDX_GITHUB_TOKEN` | unset | Explicit GitHub Copilot OAuth token to validate and import before device login |
199
206
  | `CCDX_GITHUB_TOKEN_PATH` | unset | Explicit file containing a GitHub Copilot OAuth token to validate and import before device login |
@@ -203,6 +210,7 @@ Environment variables:
203
210
  | `CCDX_TOKEN_LOCK_TIMEOUT_MS` | `600000` | Maximum time to wait for another local process to finish GitHub token login/import |
204
211
  | `CCDX_TOKEN_LOCK_STALE_MS` | `900000` | Age after which a stale GitHub token lock file can be removed |
205
212
  | `CCDX_EXISTING_ADAPTER_TIMEOUT_MS` | `500` | Timeout for detecting an already-running local adapter during startup |
213
+ | `CCDX_MODEL_REFRESH_TIMEOUT_MS` | `5000` | Timeout for a Copilot model metadata refresh or live Auto-review model lookup |
206
214
  | `CCDX_MODEL_REFRESH_INTERVAL_MS` | `7200000` | Interval for refreshing Copilot model metadata; successful lists are cached locally as last-known-good data |
207
215
  | `CCDX_RESPONSE_HISTORY_MAX_BYTES` | `67108864` | Total in-memory byte budget for locally expanded Responses history |
208
216
  | `CCDX_RESPONSE_HISTORY_MAX_ENTRIES` | `4096` | Maximum stored incremental Responses history nodes |
@@ -215,14 +223,14 @@ Environment variables:
215
223
 
216
224
  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.
217
225
 
218
- ### Debug logging
219
-
220
- 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.
221
-
222
226
  ```bash
223
227
  ccdx usage
224
228
  ```
225
229
 
230
+ ### Debug logging
231
+
232
+ 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.
233
+
226
234
  ### Image optimization
227
235
 
228
236
  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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-copilot-dx",
3
- "version": "0.5.3",
3
+ "version": "0.5.5",
4
4
  "description": "Use Codex Desktop, Claude Code, and Claude App with GitHub Copilot",
5
5
  "author": "Dale Xiao",
6
6
  "type": "module",
package/src/adapter.mjs CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  import { createRequestId, runWithRequestContext } from "./request-context.mjs";
14
14
  import { createStreamPerformanceMetrics } from "./stream-performance.mjs";
15
15
  import { status } from "./status.mjs";
16
+ import { createTerminalActivityIndicator } from "./terminal-activity.mjs";
16
17
  import { ADAPTER_HEALTH_PATH, adapterHealthPayload } from "./running-adapter.mjs";
17
18
  import { createResponsesCompactHandler, createResponsesHandler } from "./responses-handler.mjs";
18
19
  import { loadRuntimeConfig, parsePositiveInteger } from "./runtime-config.mjs";
@@ -232,9 +233,20 @@ export function createAdapterHandler(options = {}) {
232
233
  const routeName = classifyAdapterRoute(req.method, pathname);
233
234
  const complete = trackRequest ? requestMetrics.begin(routeName) : () => {};
234
235
  const streamPerformance = trackRequest ? streamPerformanceMetrics.begin(routeName) : null;
236
+ let finishTerminalActivity = () => {};
237
+ if (trackRequest) {
238
+ try {
239
+ const finish = options.terminalActivity?.beginRequest?.();
240
+ if (typeof finish === "function") finishTerminalActivity = finish;
241
+ } catch {}
242
+ }
243
+ let requestFinished = false;
235
244
  const finishRequest = ({ statusCode = 0, aborted = false } = {}) => {
245
+ if (requestFinished) return;
246
+ requestFinished = true;
236
247
  complete({ statusCode, aborted });
237
248
  streamPerformance?.finish({ failed: aborted || statusCode >= 400 });
249
+ try { finishTerminalActivity(); } catch {}
238
250
  };
239
251
  if (trackRequest && typeof res.once === "function") {
240
252
  res.once("finish", () => finishRequest({ statusCode: res.statusCode }));
@@ -268,7 +280,13 @@ export function createAdapterHandler(options = {}) {
268
280
  // Public server entry point.
269
281
 
270
282
  export function startAdapter(port = 2026, host = "127.0.0.1", options = {}) {
271
- const server = http.createServer(createAdapterHandler(options));
283
+ const ownsTerminalActivity = !Object.hasOwn(options, "terminalActivity");
284
+ const terminalActivity = ownsTerminalActivity ? createTerminalActivityIndicator() : options.terminalActivity;
285
+ const server = http.createServer(createAdapterHandler({ ...options, terminalActivity }));
286
+ const cleanupTerminalActivity = () => {
287
+ if (ownsTerminalActivity) terminalActivity?.cleanup?.();
288
+ };
289
+ server.once("close", cleanupTerminalActivity);
272
290
 
273
291
  server.on("upgrade", (req, socket) => {
274
292
  // Codex Desktop 0.130+ negotiates a "responses_websockets" server-push
@@ -282,6 +300,7 @@ export function startAdapter(port = 2026, host = "127.0.0.1", options = {}) {
282
300
 
283
301
  return new Promise((resolve, reject) => {
284
302
  const onListenError = (e) => {
303
+ cleanupTerminalActivity();
285
304
  if (e?.code === "EADDRINUSE") {
286
305
  reject(new Error(`Adapter address http://${host}:${port} is already in use. Stop the existing ccdx process or set ADAPTER_PORT to another port.`));
287
306
  return;
@@ -0,0 +1,170 @@
1
+ const ESC = "\u001b";
2
+ const ERASE_LINE = `\r${ESC}[2K`;
3
+ const HIDE_CURSOR = `${ESC}[?25l`;
4
+ const SHOW_CURSOR = `${ESC}[?25h`;
5
+
6
+ const TRACK_WIDTH = 20;
7
+ const TAIL_LENGTH = 7;
8
+ const IDLE_DELAY_MS = 800;
9
+ const FRAME_DELAY_MS = 45;
10
+ const LOOP_PAUSE_MS = 200;
11
+ const TAIL_COLORS = [159, 123, 87, 51, 45, 39, 33];
12
+
13
+ function envFlagEnabled(value) {
14
+ const normalized = String(value || "").trim().toLowerCase();
15
+ return normalized !== "" && !["0", "false", "no", "off"].includes(normalized);
16
+ }
17
+
18
+ function terminalActivityEnabled({ env, output, errorOutput }) {
19
+ const animationSetting = String(env.CCDX_TERMINAL_ANIMATION || "").trim().toLowerCase();
20
+ if (["0", "false", "no", "off"].includes(animationSetting)) return false;
21
+ if (envFlagEnabled(env.CI) || String(env.TERM || "").toLowerCase() === "dumb") return false;
22
+ if (output?.isTTY !== true || errorOutput?.isTTY !== true) return false;
23
+ const columns = Number(output.columns);
24
+ return !Number.isFinite(columns) || columns <= 0 || columns >= TRACK_WIDTH + 2;
25
+ }
26
+
27
+ export function renderCometFrame(position) {
28
+ let track = "";
29
+ for (let column = 0; column < TRACK_WIDTH; column += 1) {
30
+ const distance = position - column;
31
+ if (distance === 0) {
32
+ track += `${ESC}[97m:${ESC}[0m`;
33
+ } else if (distance > 0 && distance <= TAIL_LENGTH) {
34
+ track += `${ESC}[38;5;${TAIL_COLORS[distance - 1]}m:${ESC}[0m`;
35
+ } else {
36
+ track += " ";
37
+ }
38
+ }
39
+ return `[${track}]`;
40
+ }
41
+
42
+ function disabledIndicator() {
43
+ return {
44
+ enabled: false,
45
+ beginRequest() { return () => {}; },
46
+ cleanup() {},
47
+ };
48
+ }
49
+
50
+ export function createTerminalActivityIndicator({
51
+ env = process.env,
52
+ output = process.stdout,
53
+ errorOutput = process.stderr,
54
+ consoleObj = console,
55
+ timers = { setTimeout, clearTimeout },
56
+ } = {}) {
57
+ if (!terminalActivityEnabled({ env, output, errorOutput })) return disabledIndicator();
58
+
59
+ const methods = ["log", "warn", "error", "debug"];
60
+ const originals = Object.fromEntries(methods.map((method) => [method, consoleObj[method]]));
61
+ const wrappers = {};
62
+ let activeRequests = 0;
63
+ let idleTimer = null;
64
+ let frameTimer = null;
65
+ let framePosition = 0;
66
+ let cursorHidden = false;
67
+ let frameVisible = false;
68
+ let cleaned = false;
69
+
70
+ const schedule = (callback, delay) => {
71
+ const timer = timers.setTimeout(callback, delay);
72
+ timer?.unref?.();
73
+ return timer;
74
+ };
75
+
76
+ const clearTimer = (timer) => {
77
+ if (timer) timers.clearTimeout(timer);
78
+ };
79
+
80
+ const clearAnimation = () => {
81
+ clearTimer(frameTimer);
82
+ frameTimer = null;
83
+ framePosition = 0;
84
+ if (cursorHidden || frameVisible) output.write(`${ERASE_LINE}${SHOW_CURSOR}`);
85
+ cursorHidden = false;
86
+ frameVisible = false;
87
+ };
88
+
89
+ const cancelPendingOutput = () => {
90
+ clearTimer(idleTimer);
91
+ idleTimer = null;
92
+ clearAnimation();
93
+ };
94
+
95
+ const drawNextFrame = () => {
96
+ frameTimer = null;
97
+ if (cleaned || activeRequests === 0) {
98
+ clearAnimation();
99
+ return;
100
+ }
101
+ if (framePosition >= TRACK_WIDTH + TAIL_LENGTH) {
102
+ output.write(ERASE_LINE);
103
+ frameVisible = false;
104
+ framePosition = 0;
105
+ frameTimer = schedule(drawNextFrame, LOOP_PAUSE_MS);
106
+ return;
107
+ }
108
+
109
+ const prefix = cursorHidden ? "" : HIDE_CURSOR;
110
+ cursorHidden = true;
111
+ frameVisible = true;
112
+ output.write(`${prefix}${ERASE_LINE}${renderCometFrame(framePosition)}`);
113
+ framePosition += 1;
114
+ frameTimer = schedule(drawNextFrame, FRAME_DELAY_MS);
115
+ };
116
+
117
+ const scheduleAfterIdle = () => {
118
+ clearTimer(idleTimer);
119
+ idleTimer = null;
120
+ if (cleaned || activeRequests === 0) return;
121
+ idleTimer = schedule(() => {
122
+ idleTimer = null;
123
+ drawNextFrame();
124
+ }, IDLE_DELAY_MS);
125
+ };
126
+
127
+ for (const method of methods) {
128
+ const original = typeof originals[method] === "function" ? originals[method] : () => {};
129
+ wrappers[method] = (...args) => {
130
+ cancelPendingOutput();
131
+ try {
132
+ return original.apply(consoleObj, args);
133
+ } finally {
134
+ scheduleAfterIdle();
135
+ }
136
+ };
137
+ consoleObj[method] = wrappers[method];
138
+ }
139
+
140
+ return {
141
+ enabled: true,
142
+ beginRequest() {
143
+ if (cleaned) return () => {};
144
+ activeRequests += 1;
145
+ if (activeRequests === 1 && !idleTimer && !frameTimer) scheduleAfterIdle();
146
+ let finished = false;
147
+ return () => {
148
+ if (finished) return;
149
+ finished = true;
150
+ activeRequests = Math.max(0, activeRequests - 1);
151
+ if (activeRequests === 0) {
152
+ clearTimer(idleTimer);
153
+ idleTimer = null;
154
+ clearAnimation();
155
+ }
156
+ };
157
+ },
158
+ cleanup() {
159
+ if (cleaned) return;
160
+ cleaned = true;
161
+ activeRequests = 0;
162
+ clearTimer(idleTimer);
163
+ idleTimer = null;
164
+ clearAnimation();
165
+ for (const method of methods) {
166
+ if (consoleObj[method] === wrappers[method]) consoleObj[method] = originals[method];
167
+ }
168
+ },
169
+ };
170
+ }