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 +12 -4
- package/package.json +1 -1
- package/src/adapter.mjs +20 -1
- package/src/terminal-activity.mjs +170 -0
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
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
|
|
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
|
+
}
|