codex-copilot-dx 0.7.5 → 0.7.7
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 +65 -27
- package/bin/cli.mjs +18 -1
- package/bin/codex-copilot-dx.mjs +2 -1
- package/package.json +3 -2
- package/src/cli-options.mjs +19 -6
- package/src/codex-config-doctor.mjs +162 -0
- package/src/config.mjs +1 -1
- package/src/doctor.mjs +6 -34
- package/src/terminal-animation.mjs +94 -6
- package/src/user-settings.mjs +2 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# codex-copilot-dx
|
|
2
2
|
|
|
3
|
-
Use Codex App with GPT models
|
|
3
|
+
Use Codex App with GPT models available to your **GitHub Copilot** account.
|
|
4
4
|
|
|
5
5
|
CCDX is a local compatibility adapter for Codex App. It configures Codex to use an OpenAI Responses API on loopback while GitHub authentication, model discovery, and inference stay backed by GitHub Copilot.
|
|
6
6
|
|
|
@@ -14,7 +14,7 @@ One in-process adapter listens on `127.0.0.1:2026` by default and exposes:
|
|
|
14
14
|
|
|
15
15
|
Responses-only models and compaction use Copilot's Responses transport directly. For GPT models that advertise only Chat Completions, CCDX converts the supported text, image, and function-tool subset and rejects incompatible request shapes explicitly. Both streaming SSE and non-streaming responses are supported.
|
|
16
16
|
|
|
17
|
-
The adapter
|
|
17
|
+
The adapter handles response history, encrypted reasoning state, function/custom tools, images, compaction, cancellation, retries, and usage metadata within the compatibility boundaries below. Routing prefers endpoint metadata from Copilot's live or cached catalog; when metadata is unavailable, a small built-in set identifies known Responses-only GPT models.
|
|
18
18
|
|
|
19
19
|
### Fast and Standard modes
|
|
20
20
|
|
|
@@ -24,13 +24,13 @@ Codex Auto-review uses the hidden `codex-auto-review` model ID. CCDX maps it to
|
|
|
24
24
|
|
|
25
25
|
### GPT-6 model catalog
|
|
26
26
|
|
|
27
|
-
Codex App requests a versioned, capability-rich model catalog that is different from Copilot's raw model list. CCDX reads the complete catalog bundled with the installed Codex App and preserves every other model entry and capability. It
|
|
27
|
+
Codex App requests a versioned, capability-rich model catalog that is different from Copilot's raw model list. CCDX reads the complete catalog bundled with the installed Codex App and preserves every other model entry and capability. It exposes the bundled `gpt-6-astra` entry only when Copilot's available catalog advertises that exact model as enabled, selectable, OpenAI-owned, and Responses-capable.
|
|
28
28
|
|
|
29
|
-
CCDX
|
|
29
|
+
CCDX exposes the GPT-6 Standard path and removes its unverified speed-tier metadata. It leaves the bundled reasoning-effort options unchanged, including Ultra when supplied by the installed catalog. GPT-5.6 Sol Fast follows the separate rule above. Versioned model discovery matches the bundled catalog to the client's `client_version`, caches it by application binary identity, and reloads it after an App update. If a matching usable catalog cannot be loaded, the versioned models request returns `503`; an unversioned request returns the Copilot-style list.
|
|
30
30
|
|
|
31
31
|
## Prerequisites
|
|
32
32
|
|
|
33
|
-
- GitHub Copilot
|
|
33
|
+
- A GitHub account with Copilot access to the desired models
|
|
34
34
|
- Node.js 22.15 or newer, for built-in Zstandard request decompression
|
|
35
35
|
- Codex App on macOS
|
|
36
36
|
|
|
@@ -41,7 +41,7 @@ npm install -g codex-copilot-dx@latest
|
|
|
41
41
|
ccdx
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
-
`ccdx` is the primary launcher. The old `codex-copilot-dx` executable remains temporarily as a compatibility shim: it prints a deprecation warning at most once every seven days in interactive terminals, runs `ccdx`, and remains silent in scripts.
|
|
44
|
+
`ccdx` is the primary launcher. The old `codex-copilot-dx` executable remains temporarily as a compatibility shim: it prints a deprecation warning at most once every seven days in interactive terminals, runs `ccdx`, and remains silent in scripts. The warning timestamp is stored under `XDG_CACHE_HOME` or `~/.cache`; `doctor config` bypasses that warning cache to remain read-only. The alias will be removed in a future breaking release.
|
|
45
45
|
|
|
46
46
|
For a one-off run without a global install:
|
|
47
47
|
|
|
@@ -53,15 +53,29 @@ On a normal launch, CCDX:
|
|
|
53
53
|
|
|
54
54
|
1. Prints the installed package version and checks for a newer npm release in the background.
|
|
55
55
|
2. Reuses a compatible running adapter when available; otherwise it reuses a compatible local Copilot credential or starts GitHub Device Flow when authentication is required.
|
|
56
|
-
3.
|
|
57
|
-
4.
|
|
56
|
+
3. Loads a usable model cache or attempts a live refresh, then starts listening. A cache older than two hours refreshes in the background; a fresh cache can be used immediately. A failed refresh retains the last-known-good list when one is available.
|
|
57
|
+
4. Checks the startup-managed settings in `~/.codex/config.toml`, including the local `/v1` endpoint.
|
|
58
58
|
5. Attempts to open Codex App on macOS unless `CCDX_AUTO_LAUNCH` disables it.
|
|
59
59
|
|
|
60
|
-
The `OPENAI_API_KEY=dummy` value
|
|
60
|
+
The `OPENAI_API_KEY=dummy` value used in the Codex shell environment is only a client-side placeholder. It is not a GitHub credential. CCDX exchanges the saved GitHub OAuth credential for short-lived Copilot service tokens internally.
|
|
61
61
|
|
|
62
|
-
On startup, CCDX also adds `context_management = true` under `[features]` when the key is missing, creating the section if needed. Existing values, including `false`, are preserved. Existing inline or dotted `features` declarations and nested context-management settings are left intact to avoid conflicting TOML definitions. This
|
|
62
|
+
On startup, CCDX also adds `context_management = true` under `[features]` when the key is missing, creating the section if needed. Existing values, including `false`, are preserved. Existing inline or dotted `features` declarations and nested context-management settings are left intact to avoid conflicting TOML definitions. This opts unconfigured users into Codex context management; support and behavior depend on the installed Codex version. A configuration that already matches is not rewritten.
|
|
63
63
|
|
|
64
|
-
If a compatible adapter is already running, a later launch reuses it and
|
|
64
|
+
If a compatible adapter is already running, a later launch reuses it and checks the local Codex configuration. Reuse requires the same package version, protocol version, and required capabilities. After updating the package, stop the old adapter before starting the new version. Installing or updating the package alone does not restart the adapter or update Codex configuration.
|
|
65
|
+
|
|
66
|
+
### Codex configuration writes
|
|
67
|
+
|
|
68
|
+
Normal startup and reuse of an existing adapter check `~/.codex/config.toml` once, after startup prerequisites succeed. There is no file watcher or periodic configuration rewrite. The path is based on the OS home directory; CCDX does not redirect it through `CODEX_HOME` or edit project-level Codex files.
|
|
69
|
+
|
|
70
|
+
| Setting | Existing file | New file |
|
|
71
|
+
| --- | --- | --- |
|
|
72
|
+
| Top-level `openai_base_url` | Add or update to the adapter's `/v1` address | Set to the adapter address |
|
|
73
|
+
| `model_context_window` | Add `1000000` only when missing | `1000000` |
|
|
74
|
+
| `model_auto_compact_token_limit` | Add `900000` only when missing | `900000` |
|
|
75
|
+
| `[shell_environment_policy.set]` OpenAI URL/key | Update `OPENAI_BASE_URL` and set `OPENAI_API_KEY="dummy"` when this section is recognized; leave an absent section absent | Create the section and set both keys; set `shell_environment_policy.inherit="core"` |
|
|
76
|
+
| `[features]` `context_management` | Add `true` when missing; preserve explicit values and alternative declarations as described above | `true` |
|
|
77
|
+
|
|
78
|
+
The two token limits are fixed defaults, not detected model capabilities. Existing custom values are retained. Writes target the listed keys with line-based edits and atomic replacement; this is not a general TOML formatter. The shell environment updater recognizes the literal `[shell_environment_policy.set]` header. Use `ccdx doctor config` to inspect parsed values and preview what this writer would do with the current file. No actual change means no file rewrite.
|
|
65
79
|
|
|
66
80
|
## Version 0.7.0 migration
|
|
67
81
|
|
|
@@ -91,7 +105,7 @@ Interactive terminals use aligned tables for `models`, `auth status`, and `usage
|
|
|
91
105
|
ccdx auto-review-model
|
|
92
106
|
```
|
|
93
107
|
|
|
94
|
-
The selector first queries the running adapter, including its last-known-good model list when live refresh is unavailable, then falls back to
|
|
108
|
+
The selector first queries the running adapter, including its last-known-good model list when live refresh is unavailable, then falls back to a local model cache no older than seven days. It offers model IDs that advertise a Responses endpoint and are not explicitly hidden from the model picker. This selection does not itself verify policy or inference access.
|
|
95
109
|
|
|
96
110
|
The selection is saved in `~/.config/codex-copilot-dx/config.json`, or under `XDG_CONFIG_HOME` when set. Choosing `gpt-5.5` clears the override and restores the package default. A running adapter reads the setting on the next Auto-review request.
|
|
97
111
|
|
|
@@ -111,10 +125,14 @@ The selector is ordered numerically:
|
|
|
111
125
|
4. Chase
|
|
112
126
|
5. Mirror
|
|
113
127
|
6. Pulse
|
|
114
|
-
7.
|
|
128
|
+
7. Stack — short light trails arrive, accumulate, then leave as one bundle
|
|
129
|
+
8. Relay — fixed nodes receive and pass a short light trail
|
|
130
|
+
9. Split — one incoming light trail divides into two outgoing trails
|
|
115
131
|
|
|
116
132
|
Enter the corresponding number to select an animation. The Comet row includes a static preview, and after a selection CCDX plays one cycle of the chosen animation as confirmation when terminal animation is enabled. The setting is saved in `~/.config/codex-copilot-dx/config.json`, or under `XDG_CONFIG_HOME` when set.
|
|
117
133
|
|
|
134
|
+
All nine animations use the same 20-column colon track and cyan/blue palette. They indicate activity, not completion percentage. Braille was removed in 0.7.7. A saved `terminal_animation: "braille"` falls back to Comet without rewriting the settings file; choosing another animation replaces it, and explicitly choosing Comet clears it. Other settings remain intact.
|
|
135
|
+
|
|
118
136
|
The selected theme is loaded once when a new adapter starts, so changing it does not restart or alter an adapter that is already running. Stop and start CCDX to apply the new theme. `CCDX_TERMINAL_ANIMATION=0` (also `false`, `no`, or `off`) remains the boolean gate that disables terminal animation regardless of the selected theme.
|
|
119
137
|
|
|
120
138
|
### Update
|
|
@@ -138,13 +156,25 @@ If a configured npm mirror has not synchronized the current release yet, use the
|
|
|
138
156
|
|
|
139
157
|
## Diagnostics
|
|
140
158
|
|
|
141
|
-
|
|
159
|
+
Inspect only the local Codex configuration:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
ccdx doctor config
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
This command reads `~/.codex/config.toml` and checks UTF-8/TOML syntax, correctly scoped CCDX-managed URL/environment settings, positive integer context limits, their relative ordering, and `features.context_management`. Explicit `false` is valid. A missing optional shell environment section is valid for an existing file. Structured context-management settings are retained and reported as unverified for the installed Codex version.
|
|
166
|
+
|
|
167
|
+
It also runs the existing startup writer in memory and lists the managed keys that would change. A missing file produces a creation preview. A preview that yields invalid TOML or changes unowned settings is reported as an error rather than applied. No files are written, including debug logs; it does not read credentials, contact the adapter or upstream, start Device Flow, or open Codex App. It uses `ADAPTER_HOST` and `ADAPTER_PORT` for the expected URL. Unrelated startup/probe timeout settings do not affect this config-only command.
|
|
168
|
+
|
|
169
|
+
The syntax check uses a TOML 1.1 parser and validates only the listed managed fields, not the full installed Codex schema. It does not merge project files, profiles, environment overrides, or command-line overrides into an effective Codex configuration. Syntax errors report line/column without source snippets; configured URL and key values are not printed. The command accepts no `--online`, `--compat`, or write/fix options.
|
|
170
|
+
|
|
171
|
+
For the broader credential/configuration/adapter diagnostic:
|
|
142
172
|
|
|
143
173
|
```bash
|
|
144
174
|
ccdx doctor
|
|
145
175
|
```
|
|
146
176
|
|
|
147
|
-
The doctor checks the saved GitHub credential,
|
|
177
|
+
The ordinary doctor reuses the same configuration checks, inspects the saved GitHub credential, and probes the local adapter. It does not start the adapter or modify credentials or Codex configuration; when `CCDX_LOG_PATH` is set, this broader command can write its diagnostics to that log. Both doctor modes exit `1` when any check is an error, and `0` for OK/warning-only results, including missing configuration or an explicit feature opt-out. Unsupported arguments or invalid relevant CLI options exit `2`.
|
|
148
178
|
|
|
149
179
|
To verify the saved GitHub token, Copilot entitlement, and live models endpoint without starting Device Flow or changing the credential:
|
|
150
180
|
|
|
@@ -188,19 +218,21 @@ When the saved token is missing, `ccdx` looks for compatible local Copilot GitHu
|
|
|
188
218
|
|
|
189
219
|
After an account is selected, automatic `401`/`403` recovery and in-process token rotation accept only that same GitHub account. Concurrent callers share token refresh work without sharing cancellation, and a still-valid Copilot token may be used briefly after a transient refresh failure. Explicit token variables remain the intentional way to select another account.
|
|
190
220
|
|
|
191
|
-
If refresh still fails with `401` or `403`, the saved GitHub OAuth credential may be expired, revoked, or missing Copilot access.
|
|
221
|
+
If refresh still fails with `401` or `403`, the saved GitHub OAuth credential may be expired, revoked, or missing Copilot access. After stopping the adapter, remove that exact saved token and start again to rerun authentication recovery:
|
|
192
222
|
|
|
193
223
|
```bash
|
|
194
224
|
rm ~/.local/share/copilot-api/github_token
|
|
195
225
|
ccdx
|
|
196
226
|
```
|
|
197
227
|
|
|
228
|
+
Recovery first tries compatible local credentials and starts Device Flow only when none can be imported. Set `CCDX_DISABLE_TOKEN_DISCOVERY=1` for that launch if a fresh Device Flow is intended after removing the saved token.
|
|
229
|
+
|
|
198
230
|
## Security
|
|
199
231
|
|
|
200
232
|
- CCDX binds to loopback by default. Non-loopback hosts are rejected unless `CCDX_ALLOW_LAN=1` is set explicitly.
|
|
201
233
|
- Enabling LAN binding exposes a Copilot-backed API to other reachable machines. Provide your own host firewall and network isolation; do not expose the listener to an untrusted network.
|
|
202
234
|
- The GitHub OAuth credential is stored locally at `~/.local/share/copilot-api/github_token`. Short-lived Copilot service tokens remain in process memory and are refreshed as needed.
|
|
203
|
-
- Runtime status
|
|
235
|
+
- Runtime status and usage records retain metadata rather than request/response content. Diagnostic logging records status and error messages; it is not a general redaction layer for arbitrary error text. See Debug logging before sharing logs.
|
|
204
236
|
- Request and response bodies are bounded. Queues, history, image work, SSE parsing, retries, and shutdown all have explicit resource or time limits.
|
|
205
237
|
|
|
206
238
|
## Configuration
|
|
@@ -232,9 +264,9 @@ ccdx
|
|
|
232
264
|
| `CCDX_TERMINAL_ANIMATION` | auto | Show the selected transient animation while an interactive terminal has active requests and no output for 800 ms; set to `0`, `false`, `no`, or `off` to disable |
|
|
233
265
|
| `CCDX_IMG_MAX_DIM` | `2048` | Maximum long edge in pixels for image downscaling |
|
|
234
266
|
| `CCDX_IMG_QUALITY` | `82` | Initial WebP quality for image re-encoding |
|
|
235
|
-
| `CCDX_IMG_MIN_BYTES` | `100000` | In-bounds images smaller than this
|
|
267
|
+
| `CCDX_IMG_MIN_BYTES` | `100000` | In-bounds images smaller than this skip re-encoding; oversized dimensions still trigger an optimization attempt |
|
|
236
268
|
| `CCDX_IMG_CONCURRENCY` | `2` | Global concurrent image-optimization tasks; values above `12` are capped |
|
|
237
|
-
| `CCDX_IMG_MAX_INPUT_PIXELS` | `40000000` |
|
|
269
|
+
| `CCDX_IMG_MAX_INPUT_PIXELS` | `40000000` | Sharp's per-image decode limit; an optimization failure retains the original image, subject to the overall forwarded-body limit |
|
|
238
270
|
| `CCDX_IMG_CACHE_MAX_BYTES` | `67108864` | Process-local byte ceiling for cached image transforms; set to `0` to disable the cache |
|
|
239
271
|
| `CCDX_DISABLE_IMG_OPT` | unset | Set to `1` to disable image optimization |
|
|
240
272
|
| `CCDX_AUTO_LAUNCH` | enabled | Set to `0`, `false`, `no`, or `off` to start without opening Codex App |
|
|
@@ -244,10 +276,10 @@ ccdx
|
|
|
244
276
|
| `CCDX_GITHUB_LOGIN` | saved account | Require automatic discovery and recovery to use this GitHub login |
|
|
245
277
|
| `CCDX_DISABLE_TOKEN_DISCOVERY` | unset | Set to `1` to skip local token discovery and use the saved token or Device Flow |
|
|
246
278
|
| `CCDX_TOKEN_LOCK_TIMEOUT_MS` | `600000` | Maximum time to wait for another process to finish GitHub token login or import |
|
|
247
|
-
| `CCDX_TOKEN_LOCK_STALE_MS` | `900000` |
|
|
279
|
+
| `CCDX_TOKEN_LOCK_STALE_MS` | `900000` | Minimum age for stale-lock recovery; a lock with a live owner is not removed |
|
|
248
280
|
| `CCDX_EXISTING_ADAPTER_TIMEOUT_MS` | `500` | Timeout for detecting an already-running local adapter during startup |
|
|
249
281
|
| `CCDX_MODEL_REFRESH_TIMEOUT_MS` | `5000` | Timeout for Copilot model refresh or live Auto-review model lookup |
|
|
250
|
-
| `CCDX_MODEL_REFRESH_INTERVAL_MS` | `7200000` |
|
|
282
|
+
| `CCDX_MODEL_REFRESH_INTERVAL_MS` | `7200000` | Periodic model refresh interval; `0` disables periodic refresh, while startup cache/refresh behavior still applies |
|
|
251
283
|
| `CCDX_RESPONSE_HISTORY_MAX_BYTES` | `67108864` | Total in-memory byte budget for locally expanded Responses history |
|
|
252
284
|
| `CCDX_RESPONSE_HISTORY_MAX_ENTRIES` | `4096` | Maximum stored incremental Responses history nodes |
|
|
253
285
|
| `CCDX_USAGE_PATH` | `~/.local/share/codex-copilot-dx/usage.jsonl` | Local JSONL token-usage log |
|
|
@@ -267,27 +299,31 @@ The summary covers the current log and its single rotated backup, not an unbound
|
|
|
267
299
|
|
|
268
300
|
## Debug logging
|
|
269
301
|
|
|
270
|
-
Set `CCDX_LOG_PATH=1` to mirror terminal logs to `~/.local/share/codex-copilot-dx/debug.log`, or provide a custom path. Add `CCDX_LOG_LEVEL=debug` for upstream attempts, retry causes, status codes, and timings.
|
|
302
|
+
Set `CCDX_LOG_PATH=1` to mirror terminal logs to `~/.local/share/codex-copilot-dx/debug.log`, or provide a custom path. Add `CCDX_LOG_LEVEL=debug` for upstream attempts, retry causes, status codes, and timings. The adapter does not deliberately dump request/response bodies or authorization headers. The log mirror writes console text verbatim, including diagnostic/error messages that can originate in external components, so review logs before sharing them. `ccdx doctor config` bypasses file logging entirely.
|
|
271
303
|
|
|
272
304
|
## Large histories and images
|
|
273
305
|
|
|
274
306
|
Long computer-use sessions can accumulate screenshots that are sent again on later turns. This can increase local preparation time, upstream handshake latency, and the chance of a `413 Payload Too Large` response.
|
|
275
307
|
|
|
276
|
-
|
|
308
|
+
The response-history cache is process-local and byte/entry bounded. Restarting the adapter discards it; a `previous_response_id` stored only in that process becomes unavailable. CCDX does not persist conversation history to disk. Clients can still submit explicit history in the request body, subject to the same limits.
|
|
309
|
+
|
|
310
|
+
CCDX attempts to downsample inline base64 images to model-appropriate pixel bounds, with a 2048 px long-edge fallback, and initially encodes them as WebP quality 82. It never replaces an image with a larger encoding and uses one global concurrency limit across direct images and function/custom-tool outputs. GIFs and non-inline references are not re-encoded. An encoding/decode failure keeps the original image; the final request-byte limit still applies.
|
|
277
311
|
|
|
278
312
|
The final serialized UTF-8 body is measured before forwarding. Above the configured limit, unique source images are processed largest-first from their originals at quality 75 / 1600 px and then quality 65 / 1280 px, stopping when the body fits. If necessary, the forwarded view omits older duplicate images, historical tool images, other historical images, and finally old tool outputs while preserving current input and tool-call skeletons. Requests that still do not fit receive a structured local `413`.
|
|
279
313
|
|
|
280
314
|
Completed image transforms are reused from a byte-bounded process-local LRU cache, and concurrent requests for the same transform share one encode. Failures are not cached and request cancellation remains isolated.
|
|
281
315
|
|
|
282
|
-
|
|
316
|
+
The visual-history pressure pass does not omit images while there are at most 24 historical images and the expanded body is at most 18 MiB; normal image encoding and other limits still apply. Above either threshold, this pass preserves current-turn images and keeps up to 16 recent historical images, targeting 16 MiB. The separate 50-image and total-body limits remain in force. After an eligible large-visual-history preparation, handshake, upstream timeout, or HTTP `408`, the next retry uses an 8-image, 10 MiB recovery window for ten minutes. Two successful requests or a successful compaction clear recovery mode. CCDX never transparently repeats an ambiguous timed-out POST, and it does not delete local history.
|
|
283
317
|
|
|
284
318
|
When expanded history exceeds Copilot's 50-image request limit, CCDX removes older duplicate occurrences first and then keeps the 50 most recent images. This affects only the forwarded request; local history remains complete and current images are preferred.
|
|
285
319
|
|
|
286
320
|
For `/v1/responses/compact`, CCDX sends one terminal `compaction_trigger`, validates completed compaction state, and preserves the complete returned window as the canonical next context. Only a validated window becomes a new local history root. Existing older branches remain available until normal history eviction.
|
|
287
321
|
|
|
288
|
-
|
|
322
|
+
Changing the model or transport of a continuation can require removing incompatible encrypted history from the forwarded request. A specific upstream encrypted-state verification failure can also trigger one sanitized retry. CCDX does not decrypt that state locally.
|
|
289
323
|
|
|
290
|
-
|
|
324
|
+
Successful streaming responses are accepted only when the upstream body is SSE and reaches its protocol terminal event. Unexpected EOF becomes a protocol-native stream error, and repeated blank tool-argument deltas are stopped per tool call. Allowlisted rate-limit, retry, model, trace, and upstream request-ID metadata are forwarded; cookies, authorization, body-length, and encoding headers are not.
|
|
325
|
+
|
|
326
|
+
CCDX keeps each native Responses message on its first upstream ID when Copilot changes that ID during streaming. Message deltas, completion events, and cached history stay consistent, addressing this cause of duplicate live messages in Codex App. Tool identities, reasoning/encrypted content, message phases, and speed-tier routing are unchanged by this ID normalization. Stable events retain their original SSE bytes; changed events are rewritten with bounded, backpressured streaming.
|
|
291
327
|
|
|
292
328
|
Newer Codex App builds can advertise an `image_gen` namespace that already exists upstream. CCDX removes that exact conflicting client tool before forwarding and retries once only when Copilot explicitly reports an image namespace collision. Image input and screenshot optimization remain enabled.
|
|
293
329
|
|
|
@@ -301,7 +337,9 @@ npm run bench:payload
|
|
|
301
337
|
|
|
302
338
|
`bench:check` covers both SSE parsers, including native Responses identity rewriting, 1/4/8 MiB fragmented events, write batching, and slow-client backpressure. It also repeats four concurrent image preparations across six rounds using a desktop-sized synthetic screenshot and a public-domain photograph. Checks enforce content preservation, single-pass JSON parsing, bounded copying, cache reuse, settled work, and bounded retained heap/ArrayBuffers after warm-up and GC. Sampled RSS and absolute timings remain report-only because allocators, runtimes, and machines differ; the fixtures do not establish production latency or visual model accuracy.
|
|
303
339
|
|
|
304
|
-
`npm test` runs unit and
|
|
340
|
+
`npm test` runs unit, handler, CLI, and documentation-contract tests. `npm run test:smoke` starts a real local adapter with fully injected offline upstreams. `npm run bench:check` enforces linear SSE scanning, image/tool processing, and request-admission resource limits. `npm run pack:check` verifies npm tarball contents without publishing. `npm run bench:payload` is a report-only isolated-process benchmark for 5–60 MiB synthetic padded-image payloads and does not contact Copilot. CI runs the complete `verify` gate on Node 22.15.0 and 24.x.
|
|
341
|
+
|
|
342
|
+
The [documentation index](https://github.com/DaleXiao/codex-copilot-dx/blob/main/docs/README.md) distinguishes current guidance from historical release evidence. Release reports retain the measurements and limits of their specific versions; they do not establish current live-provider compatibility.
|
|
305
343
|
|
|
306
344
|
From a source checkout, `node scripts/codex-message-id-replay.mjs --codex /absolute/path/to/codex` checks message identity against an installed Codex runtime using only synthetic SSE, a local mock server, and automatically cleaned temporary Codex directories. It does not use the real client configuration or call Copilot.
|
|
307
345
|
|
package/bin/cli.mjs
CHANGED
|
@@ -12,7 +12,7 @@ import { runDoctor } from "../src/doctor.mjs";
|
|
|
12
12
|
import { adapterBaseUrl, checkRunningAdapter } from "../src/running-adapter.mjs";
|
|
13
13
|
import { assertSafeAdapterHost, isLoopbackHost } from "../src/security.mjs";
|
|
14
14
|
import { runInBackground } from "../src/startup.mjs";
|
|
15
|
-
import { cliCommandName, cliHelp, parseAdapterProbeOptions, parseCliArgs, parseRuntimeOptions } from "../src/cli-options.mjs";
|
|
15
|
+
import { cliCommandName, cliHelp, parseAdapterAddressOptions, parseAdapterProbeOptions, parseCliArgs, parseRuntimeOptions } from "../src/cli-options.mjs";
|
|
16
16
|
import { formatAdapterStatus, readAdapterStatus } from "../src/cli-status.mjs";
|
|
17
17
|
import { closeHttpServer } from "../src/shutdown.mjs";
|
|
18
18
|
import { runAutoReviewModelCommand } from "../src/auto-review-model.mjs";
|
|
@@ -143,6 +143,23 @@ if (CLI.command === "update") {
|
|
|
143
143
|
process.exit(1);
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
|
+
if (CLI.command === "doctor" && CLI.configOnly) {
|
|
147
|
+
let probe;
|
|
148
|
+
try {
|
|
149
|
+
probe = parseAdapterAddressOptions(process.env);
|
|
150
|
+
} catch (e) {
|
|
151
|
+
console.error(e.message);
|
|
152
|
+
process.exit(2);
|
|
153
|
+
}
|
|
154
|
+
const checks = await runDoctor({
|
|
155
|
+
commandName: CLI_NAME,
|
|
156
|
+
configOnly: true,
|
|
157
|
+
host: probe.adapterHost,
|
|
158
|
+
port: probe.adapterPort,
|
|
159
|
+
});
|
|
160
|
+
process.exit(checks.some((check) => check.kind === "err") ? 1 : 0);
|
|
161
|
+
}
|
|
162
|
+
|
|
146
163
|
let RUNTIME;
|
|
147
164
|
try {
|
|
148
165
|
RUNTIME = parseRuntimeOptions(process.env);
|
package/bin/codex-copilot-dx.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
const configDoctor = ["doctor", "--doctor"].includes(process.argv[2]) && process.argv[3] === "config";
|
|
4
|
+
if (process.stderr.isTTY === true && !configDoctor) {
|
|
4
5
|
const {
|
|
5
6
|
LEGACY_COMMAND_WARNING,
|
|
6
7
|
shouldShowLegacyCommandWarning,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codex-copilot-dx",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.7",
|
|
4
4
|
"description": "Use Codex App with GitHub Copilot",
|
|
5
5
|
"author": "Dale Xiao",
|
|
6
6
|
"type": "module",
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
"prepublishOnly": "npm run verify"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"sharp": "^0.35.3"
|
|
51
|
+
"sharp": "^0.35.3",
|
|
52
|
+
"smol-toml": "1.8.0"
|
|
52
53
|
}
|
|
53
54
|
}
|
package/src/cli-options.mjs
CHANGED
|
@@ -16,6 +16,7 @@ const HELP_TOPICS = new Set([
|
|
|
16
16
|
"auth",
|
|
17
17
|
"auth status",
|
|
18
18
|
"doctor",
|
|
19
|
+
"doctor config",
|
|
19
20
|
"status",
|
|
20
21
|
"models",
|
|
21
22
|
"usage",
|
|
@@ -126,11 +127,16 @@ export function parseCliArgs(args = []) {
|
|
|
126
127
|
|| (command === "start" && rest.some((option) => RETIRED_CLAUDE_OPTIONS.has(option)))
|
|
127
128
|
|| (command === "auth" && rest[0] === "login" && rest[1] === "claude")
|
|
128
129
|
|| (command === "models" && requestedProfile === "claude")
|
|
129
|
-
|| (command === "doctor" && ["claude", "all"].includes(requestedProfile));
|
|
130
|
+
|| (command === "doctor" && rest[0] !== "config" && ["claude", "all"].includes(requestedProfile));
|
|
130
131
|
if (PM_COMMANDS.has(command)) return baseCommand("retired", { integration: "PM Studio" });
|
|
131
132
|
if (retiredClaudeInvocation) {
|
|
132
133
|
return baseCommand("retired", { integration: "Claude App and Claude Code" });
|
|
133
134
|
}
|
|
135
|
+
if ((command === "doctor" || command === "--doctor") && rest[0] === "config") {
|
|
136
|
+
if (rest.length === 2 && HELP_COMMANDS.has(rest[1])) return helpCommand(["doctor", "config"]);
|
|
137
|
+
if (rest.length > 1) unexpectedArgs(rest.slice(1));
|
|
138
|
+
return baseCommand("doctor", { configOnly: true });
|
|
139
|
+
}
|
|
134
140
|
const requestedHelp = nestedHelp(command, rest);
|
|
135
141
|
if (requestedHelp) return requestedHelp;
|
|
136
142
|
if (VERSION_COMMANDS.has(command)) {
|
|
@@ -245,10 +251,16 @@ function integerEnv(env, name, fallback, { min, max = Number.MAX_SAFE_INTEGER })
|
|
|
245
251
|
return value;
|
|
246
252
|
}
|
|
247
253
|
|
|
248
|
-
export function
|
|
254
|
+
export function parseAdapterAddressOptions(env = process.env) {
|
|
249
255
|
return {
|
|
250
256
|
adapterPort: integerEnv(env, "ADAPTER_PORT", 2026, { min: 1, max: 65535 }),
|
|
251
257
|
adapterHost: String(env.ADAPTER_HOST || "127.0.0.1").trim() || "127.0.0.1",
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function parseRuntimeOptions(env = process.env) {
|
|
262
|
+
return {
|
|
263
|
+
...parseAdapterAddressOptions(env),
|
|
252
264
|
modelRefreshTimeoutMs: integerEnv(env, "CCDX_MODEL_REFRESH_TIMEOUT_MS", 5000, { min: 1, max: MAX_TIMER_DELAY_MS }),
|
|
253
265
|
existingAdapterTimeoutMs: integerEnv(env, "CCDX_EXISTING_ADAPTER_TIMEOUT_MS", 500, { min: 1, max: MAX_TIMER_DELAY_MS }),
|
|
254
266
|
modelRefreshIntervalMs: integerEnv(env, "CCDX_MODEL_REFRESH_INTERVAL_MS", 2 * 60 * 60 * 1000, { min: 0, max: MAX_TIMER_DELAY_MS }),
|
|
@@ -261,8 +273,7 @@ export function parseRuntimeOptions(env = process.env) {
|
|
|
261
273
|
|
|
262
274
|
export function parseAdapterProbeOptions(env = process.env) {
|
|
263
275
|
return {
|
|
264
|
-
|
|
265
|
-
adapterHost: String(env.ADAPTER_HOST || "127.0.0.1").trim() || "127.0.0.1",
|
|
276
|
+
...parseAdapterAddressOptions(env),
|
|
266
277
|
existingAdapterTimeoutMs: integerEnv(env, "CCDX_EXISTING_ADAPTER_TIMEOUT_MS", 500, { min: 1, max: MAX_TIMER_DELAY_MS }),
|
|
267
278
|
};
|
|
268
279
|
}
|
|
@@ -276,12 +287,13 @@ function topicHelp(name, topic) {
|
|
|
276
287
|
start: `Usage:\n ${name} [start] [--show-request-id]\n\nStarts or reuses the local adapter, updates Codex configuration, and attempts to open Codex App when auto-launch is enabled and supported.`,
|
|
277
288
|
auth: `Usage:\n ${name} auth status [--online] [--format table|plain]\n\nShows the saved GitHub Copilot account without exposing credentials. --online also verifies its entitlement and model catalog.`,
|
|
278
289
|
"auth status": `Usage:\n ${name} auth status [--online] [--format table|plain]\n\nShows the saved GitHub Copilot account without exposing credentials. --online also verifies its entitlement and model catalog. Interactive terminals use a table by default.`,
|
|
279
|
-
doctor: `Usage:\n ${name} doctor [--online] [--compat]\n\nChecks the local credential, Codex configuration, and adapter. --online validates account entitlement; --compat sends minimal inference requests and consumes a small amount of Copilot usage.`,
|
|
290
|
+
doctor: `Usage:\n ${name} doctor [--online] [--compat]\n ${name} doctor config\n\nChecks the local credential, Codex configuration, and adapter. --online validates account entitlement; --compat sends minimal inference requests and consumes a small amount of Copilot usage. The config subcommand checks only the local file and previews startup changes, without network, authentication, or file writes.`,
|
|
291
|
+
"doctor config": `Usage:\n ${name} doctor config\n\nReads ~/.codex/config.toml, checks TOML syntax and CCDX-managed settings, and previews the existing startup writer's changes. Does not merge Codex profiles or validate the complete installed Codex schema. No authentication, network requests, file writes, or adapter/GUI startup. Existing context_management=false is valid. Exit 1 for errors, 0 for OK/warnings; unsupported arguments exit 2.`,
|
|
280
292
|
status: `Usage:\n ${name} status\n\nShows bounded runtime, routing, performance, queue, and cache metrics from a running adapter.`,
|
|
281
293
|
models: `Usage:\n ${name} models [--format table|plain]\n\nPerforms a fresh, read-only Copilot model-directory lookup for the saved account. Interactive terminals use a table by default.`,
|
|
282
294
|
usage: `Usage:\n ${name} usage [--format table|plain]\n\nSummarizes local token usage metadata without reading prompt or completion content. Interactive terminals use a table by default.`,
|
|
283
295
|
animation: `Usage:\n ${name} animation\n\nInteractively selects the terminal activity animation used the next time the adapter starts.`,
|
|
284
|
-
"auto-review-model": `Usage:\n ${name} auto-review-model\n\nInteractively selects an
|
|
296
|
+
"auto-review-model": `Usage:\n ${name} auto-review-model\n\nInteractively selects an advertised Responses model for Codex Auto-review.`,
|
|
285
297
|
update: `Usage:\n ${name} update [npm|github]\n\nUpdates the global package from the configured npm registry or GitHub main. With no source, an interactive terminal prompts for one.`,
|
|
286
298
|
version: `Usage:\n ${name} --version`,
|
|
287
299
|
};
|
|
@@ -296,6 +308,7 @@ export function cliHelp(commandName = "ccdx", topic = "") {
|
|
|
296
308
|
${name} [start] [--show-request-id]
|
|
297
309
|
${name} auth status [--online] [--format table|plain]
|
|
298
310
|
${name} doctor [--online] [--compat]
|
|
311
|
+
${name} doctor config
|
|
299
312
|
${name} status
|
|
300
313
|
${name} models [--format table|plain]
|
|
301
314
|
${name} usage [--format table|plain]
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { isDeepStrictEqual } from "node:util";
|
|
5
|
+
import { parse } from "smol-toml";
|
|
6
|
+
import { computeUpdatedCodexConfig, initialCodexConfig } from "./config.mjs";
|
|
7
|
+
import { adapterBaseUrl } from "./running-adapter.mjs";
|
|
8
|
+
|
|
9
|
+
const MANAGED_KEYS = [
|
|
10
|
+
"openai_base_url",
|
|
11
|
+
"model_context_window",
|
|
12
|
+
"model_auto_compact_token_limit",
|
|
13
|
+
"shell_environment_policy.inherit",
|
|
14
|
+
"shell_environment_policy.set.OPENAI_BASE_URL",
|
|
15
|
+
"shell_environment_policy.set.OPENAI_API_KEY",
|
|
16
|
+
"features.context_management",
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
function field(config, key) {
|
|
20
|
+
return key.split(".").reduce((value, part) => (
|
|
21
|
+
value && Object.hasOwn(value, part) ? value[part] : undefined
|
|
22
|
+
), config);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function table(value) {
|
|
26
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function parseConfig(text) {
|
|
30
|
+
// Keep integer and floating-point TOML values distinct, including large ints.
|
|
31
|
+
return parse(text, { integersAsBigInt: true });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function unmanagedSettings(config) {
|
|
35
|
+
const remaining = structuredClone(config);
|
|
36
|
+
for (const key of MANAGED_KEYS) {
|
|
37
|
+
const parts = key.split(".");
|
|
38
|
+
const leaf = parts.pop();
|
|
39
|
+
const parent = parts.length ? field(remaining, parts.join(".")) : remaining;
|
|
40
|
+
if (table(parent)) delete parent[leaf];
|
|
41
|
+
}
|
|
42
|
+
for (const key of ["shell_environment_policy.set", "shell_environment_policy", "features"]) {
|
|
43
|
+
const value = field(remaining, key);
|
|
44
|
+
if (!table(value) || Object.keys(value).length) continue;
|
|
45
|
+
const parts = key.split(".");
|
|
46
|
+
const leaf = parts.pop();
|
|
47
|
+
const parent = parts.length ? field(remaining, parts.join(".")) : remaining;
|
|
48
|
+
delete parent[leaf];
|
|
49
|
+
}
|
|
50
|
+
return remaining;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function startupPreview(content, config, host, port) {
|
|
54
|
+
const updated = content === null
|
|
55
|
+
? { content: initialCodexConfig(port, host), changed: true }
|
|
56
|
+
: computeUpdatedCodexConfig(content, port, host);
|
|
57
|
+
if (!updated.changed) return { kind: "ok", message: "No startup configuration changes required" };
|
|
58
|
+
let next;
|
|
59
|
+
try {
|
|
60
|
+
next = parseConfig(updated.content);
|
|
61
|
+
} catch {
|
|
62
|
+
return { kind: "err", message: "Startup configuration preview is not valid TOML; review the file before starting ccdx" };
|
|
63
|
+
}
|
|
64
|
+
if (!isDeepStrictEqual(unmanagedSettings(config), unmanagedSettings(next))) {
|
|
65
|
+
return { kind: "err", message: "Startup preview would change settings outside CCDX's managed keys; review the file before starting ccdx" };
|
|
66
|
+
}
|
|
67
|
+
const changed = MANAGED_KEYS.filter((key) => !isDeepStrictEqual(field(config, key), field(next, key)));
|
|
68
|
+
return {
|
|
69
|
+
kind: "warn",
|
|
70
|
+
message: changed.length
|
|
71
|
+
? `Next startup would ${content === null ? "create the file and set" : "set"}: ${changed.join(", ")}`
|
|
72
|
+
: "Next startup would rewrite the file; managed key values are unchanged",
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function checkUrl(value, expected, label) {
|
|
77
|
+
if (value === undefined) return { kind: "warn", message: `${label} is missing` };
|
|
78
|
+
if (typeof value !== "string") return { kind: "err", message: `${label} must be a string` };
|
|
79
|
+
// Do not echo configured URLs: userinfo and query strings can contain secrets.
|
|
80
|
+
return value === expected
|
|
81
|
+
? { kind: "ok", message: `${label} points to ${expected}` }
|
|
82
|
+
: { kind: "warn", message: `${label} differs from the CCDX address ${expected}` };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function inspectCodexConfig({ home = os.homedir(), host = "127.0.0.1", port = 2026 } = {}) {
|
|
86
|
+
const filePath = path.join(home, ".codex", "config.toml");
|
|
87
|
+
let content;
|
|
88
|
+
try {
|
|
89
|
+
content = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(fs.readFileSync(filePath));
|
|
90
|
+
} catch (error) {
|
|
91
|
+
if (error?.code === "ENOENT") {
|
|
92
|
+
return [
|
|
93
|
+
{ kind: "warn", message: `Codex config not found at ${filePath}` },
|
|
94
|
+
startupPreview(null, {}, host, port),
|
|
95
|
+
];
|
|
96
|
+
}
|
|
97
|
+
return [{ kind: "err", message: `Cannot read UTF-8 Codex config at ${filePath}` }];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let config;
|
|
101
|
+
try {
|
|
102
|
+
config = parseConfig(content);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
const location = Number.isInteger(error?.line) && Number.isInteger(error?.column)
|
|
105
|
+
? ` at line ${error.line}, column ${error.column}`
|
|
106
|
+
: "";
|
|
107
|
+
// Parser messages include source snippets. Report only numeric locations.
|
|
108
|
+
return [{ kind: "err", message: `Invalid TOML in ${filePath}${location}` }];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const expectedBaseUrl = `${adapterBaseUrl(host, port)}/v1`;
|
|
112
|
+
const checks = [
|
|
113
|
+
{ kind: "ok", message: `Codex config is readable TOML: ${filePath}` },
|
|
114
|
+
checkUrl(config.openai_base_url, expectedBaseUrl, "Codex base URL"),
|
|
115
|
+
];
|
|
116
|
+
for (const key of ["model_context_window", "model_auto_compact_token_limit"]) {
|
|
117
|
+
const value = field(config, key);
|
|
118
|
+
checks.push(value === undefined
|
|
119
|
+
? { kind: "warn", message: `${key} is missing` }
|
|
120
|
+
: typeof value === "bigint" && value > 0n
|
|
121
|
+
? { kind: "ok", message: `${key} = ${value} (existing value retained)` }
|
|
122
|
+
: { kind: "err", message: `${key} must be a positive TOML integer` });
|
|
123
|
+
}
|
|
124
|
+
if (typeof config.model_context_window === "bigint" && config.model_context_window > 0n
|
|
125
|
+
&& typeof config.model_auto_compact_token_limit === "bigint" && config.model_auto_compact_token_limit > 0n
|
|
126
|
+
&& config.model_auto_compact_token_limit >= config.model_context_window) {
|
|
127
|
+
checks.push({ kind: "warn", message: "model_auto_compact_token_limit is not below model_context_window; review the custom limits" });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const policy = field(config, "shell_environment_policy");
|
|
131
|
+
const env = field(config, "shell_environment_policy.set");
|
|
132
|
+
if ((policy !== undefined && !table(policy)) || (env !== undefined && !table(env))) {
|
|
133
|
+
checks.push({ kind: "err", message: "shell_environment_policy and its set value must be TOML tables" });
|
|
134
|
+
} else if (env === undefined) {
|
|
135
|
+
checks.push({ kind: "ok", message: "Optional shell_environment_policy.set table is absent; existing files do not require it" });
|
|
136
|
+
} else {
|
|
137
|
+
checks.push(checkUrl(env.OPENAI_BASE_URL, expectedBaseUrl, "Codex shell OPENAI_BASE_URL"));
|
|
138
|
+
checks.push(env.OPENAI_API_KEY === undefined
|
|
139
|
+
? { kind: "warn", message: "Codex shell OPENAI_API_KEY placeholder is missing" }
|
|
140
|
+
: typeof env.OPENAI_API_KEY !== "string"
|
|
141
|
+
? { kind: "err", message: "Codex shell OPENAI_API_KEY must be a string (value hidden)" }
|
|
142
|
+
: env.OPENAI_API_KEY === "dummy"
|
|
143
|
+
? { kind: "ok", message: "Codex shell OPENAI_API_KEY uses the dummy placeholder" }
|
|
144
|
+
: { kind: "warn", message: "Codex shell OPENAI_API_KEY differs from the dummy placeholder (value hidden)" });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const features = field(config, "features");
|
|
148
|
+
const management = field(config, "features.context_management");
|
|
149
|
+
if (features !== undefined && !table(features)) {
|
|
150
|
+
checks.push({ kind: "err", message: "features must be a TOML table" });
|
|
151
|
+
} else if (management === undefined) {
|
|
152
|
+
checks.push({ kind: "warn", message: "features.context_management is missing; see the startup preview for whether it will be added" });
|
|
153
|
+
} else if (typeof management === "boolean") {
|
|
154
|
+
checks.push({ kind: "ok", message: `features.context_management = ${management} (${management ? "enabled" : "explicitly disabled"}; retained)` });
|
|
155
|
+
} else if (table(management)) {
|
|
156
|
+
checks.push({ kind: "warn", message: "Structured context-management settings are retained; installed Codex support is not verified" });
|
|
157
|
+
} else {
|
|
158
|
+
checks.push({ kind: "err", message: "features.context_management must be a boolean for the basic feature flag" });
|
|
159
|
+
}
|
|
160
|
+
checks.push(startupPreview(content, config, host, port));
|
|
161
|
+
return checks;
|
|
162
|
+
}
|
package/src/config.mjs
CHANGED
|
@@ -135,7 +135,7 @@ export function computeUpdatedCodexConfig(content, adapterPort = 2026, adapterHo
|
|
|
135
135
|
return { content: lines.join("\n") + (hadTrailingNewline ? "\n" : ""), changed };
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
function initialCodexConfig(adapterPort, adapterHost) {
|
|
138
|
+
export function initialCodexConfig(adapterPort, adapterHost) {
|
|
139
139
|
const baseUrl = `${adapterBaseUrl(adapterHost, adapterPort)}/v1`;
|
|
140
140
|
return `openai_base_url = "${baseUrl}"
|
|
141
141
|
model_context_window = ${MODEL_CONTEXT_WINDOW}
|
package/src/doctor.mjs
CHANGED
|
@@ -11,6 +11,9 @@ import { status } from "./status.mjs";
|
|
|
11
11
|
import { buildHeaders, FALLBACK_VSCODE_VERSION, parseApiBase } from "./copilot.mjs";
|
|
12
12
|
import { adapterBaseUrl, checkRunningAdapter } from "./running-adapter.mjs";
|
|
13
13
|
import { CODEX_AUTO_REVIEW_MODEL } from "./models.mjs";
|
|
14
|
+
import { inspectCodexConfig } from "./codex-config-doctor.mjs";
|
|
15
|
+
|
|
16
|
+
export { inspectCodexConfig };
|
|
14
17
|
|
|
15
18
|
function localGatewayBaseUrl(host, port) {
|
|
16
19
|
const safeHost = String(host || "127.0.0.1");
|
|
@@ -26,22 +29,12 @@ function readText(filePath) {
|
|
|
26
29
|
}
|
|
27
30
|
}
|
|
28
31
|
|
|
29
|
-
function tomlString(content, key) {
|
|
30
|
-
const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
31
|
-
const match = new RegExp(`^\\s*${escaped}\\s*=\\s*"([^"]*)"`, "m").exec(content);
|
|
32
|
-
return match?.[1] || "";
|
|
33
|
-
}
|
|
34
|
-
|
|
35
32
|
function displayPath(home, filePath) {
|
|
36
33
|
const rel = path.relative(home, filePath);
|
|
37
34
|
if (!rel.startsWith("..") && !path.isAbsolute(rel)) return `~/${rel}`;
|
|
38
35
|
return filePath;
|
|
39
36
|
}
|
|
40
37
|
|
|
41
|
-
function valueLabel(value) {
|
|
42
|
-
return value ? `"${value}"` : "missing";
|
|
43
|
-
}
|
|
44
|
-
|
|
45
38
|
function copilotModelData(models) {
|
|
46
39
|
const data = Array.isArray(models) ? models : models?.data;
|
|
47
40
|
return Array.isArray(data) ? data : [];
|
|
@@ -282,29 +275,6 @@ export async function inspectAuthProfilesOnline(options = {}) {
|
|
|
282
275
|
return inspectGitHubTokenOnline(options);
|
|
283
276
|
}
|
|
284
277
|
|
|
285
|
-
export function inspectCodexConfig({ home = os.homedir(), host = "127.0.0.1", port = 2026 } = {}) {
|
|
286
|
-
const filePath = path.join(home, ".codex", "config.toml");
|
|
287
|
-
const expectedBaseUrl = `${adapterBaseUrl(host, port)}/v1`;
|
|
288
|
-
const config = readText(filePath);
|
|
289
|
-
if (!config.ok) {
|
|
290
|
-
return [{ kind: "warn", message: `Codex config not found at ${displayPath(home, filePath)}` }];
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
const checks = [];
|
|
294
|
-
const baseUrl = tomlString(config.text, "openai_base_url");
|
|
295
|
-
checks.push(baseUrl === expectedBaseUrl
|
|
296
|
-
? { kind: "ok", message: `Codex base URL points to ${expectedBaseUrl}` }
|
|
297
|
-
: { kind: "warn", message: `Codex base URL is ${valueLabel(baseUrl)}; expected "${expectedBaseUrl}"` });
|
|
298
|
-
|
|
299
|
-
const missing = [];
|
|
300
|
-
if (tomlString(config.text, "OPENAI_BASE_URL") !== expectedBaseUrl) missing.push("OPENAI_BASE_URL");
|
|
301
|
-
if (tomlString(config.text, "OPENAI_API_KEY") !== "dummy") missing.push("OPENAI_API_KEY");
|
|
302
|
-
checks.push(missing.length === 0
|
|
303
|
-
? { kind: "ok", message: "Codex shell env local API keys are configured" }
|
|
304
|
-
: { kind: "warn", message: `Codex shell env local API keys need update: ${missing.join(", ")}` });
|
|
305
|
-
return checks;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
278
|
function connectHost(host) {
|
|
309
279
|
const normalized = String(host || "127.0.0.1").replace(/^\[(.*)\]$/, "$1");
|
|
310
280
|
if (normalized === "0.0.0.0") return "127.0.0.1";
|
|
@@ -332,6 +302,7 @@ export async function collectDoctorChecks({
|
|
|
332
302
|
host = "127.0.0.1",
|
|
333
303
|
port = 2026,
|
|
334
304
|
checkAdapter = true,
|
|
305
|
+
configOnly = false,
|
|
335
306
|
checkAdapterListeningFn = checkAdapterListening,
|
|
336
307
|
checkRunningAdapterFn = checkRunningAdapter,
|
|
337
308
|
online = false,
|
|
@@ -342,6 +313,7 @@ export async function collectDoctorChecks({
|
|
|
342
313
|
compatTimeoutMs = 120000,
|
|
343
314
|
inspectAdapterCompatibilityFn = inspectAdapterCompatibility,
|
|
344
315
|
} = {}) {
|
|
316
|
+
if (configOnly) return inspectCodexConfig({ home, host, port });
|
|
345
317
|
checkedDoctorProfile(profile);
|
|
346
318
|
const checks = [
|
|
347
319
|
...inspectAuthProfiles({ home }),
|
|
@@ -406,7 +378,7 @@ export async function runDoctor(options = {}) {
|
|
|
406
378
|
options.online ? "--online" : "",
|
|
407
379
|
options.compat ? "--compat" : "",
|
|
408
380
|
].filter(Boolean);
|
|
409
|
-
log(`${options.commandName || "ccdx"} doctor${flags.length ? ` ${flags.join(" ")}` : ""}`);
|
|
381
|
+
log(`${options.commandName || "ccdx"} doctor${options.configOnly ? " config" : flags.length ? ` ${flags.join(" ")}` : ""}`);
|
|
410
382
|
const checks = await collectDoctorChecks(options);
|
|
411
383
|
for (const check of checks) log(status(check.kind, check.message));
|
|
412
384
|
const totals = { ok: 0, warn: 0, err: 0 };
|
|
@@ -5,7 +5,7 @@ export const DEFAULT_TERMINAL_ANIMATION_THEME = "comet";
|
|
|
5
5
|
|
|
6
6
|
const TAIL_LENGTH = 7;
|
|
7
7
|
const TAIL_COLORS = Object.freeze([159, 123, 87, 51, 45, 39, 33]);
|
|
8
|
-
const
|
|
8
|
+
const RELAY_ROUTE = Object.freeze([2, 9, 16, 9, 2]);
|
|
9
9
|
|
|
10
10
|
function colorCell(glyph, color) {
|
|
11
11
|
return `${ESC}[${color}m${glyph}${ESC}[0m`;
|
|
@@ -39,6 +39,25 @@ function pingPongState(index) {
|
|
|
39
39
|
: { position: period - offset, direction: -1 };
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
function setGlowCell(cells, index, level) {
|
|
43
|
+
const color = level === 0 ? "97" : `38;5;${TAIL_COLORS[Math.min(level - 1, TAIL_LENGTH - 1)]}`;
|
|
44
|
+
setCell(cells, index, ":", color);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function stackState(index) {
|
|
48
|
+
// Three incoming packets each leave three cells parked on the left.
|
|
49
|
+
for (let parked = 0; parked < 9; parked += 3) {
|
|
50
|
+
const travelFrames = TERMINAL_ANIMATION_TRACK_WIDTH - parked;
|
|
51
|
+
if (index < travelFrames) {
|
|
52
|
+
return { parked, head: TERMINAL_ANIMATION_TRACK_WIDTH - 1 - index, delayMs: 55 };
|
|
53
|
+
}
|
|
54
|
+
if (index === travelFrames) return { parked: parked + 3, delayMs: 180 };
|
|
55
|
+
index -= travelFrames + 1;
|
|
56
|
+
}
|
|
57
|
+
// Release the accumulated bundle, then leave a blank frame before restarting.
|
|
58
|
+
return { shift: index, delayMs: index === 0 ? 130 : index === 21 ? 180 : 50 };
|
|
59
|
+
}
|
|
60
|
+
|
|
42
61
|
const THEME_DEFINITIONS = Object.freeze([
|
|
43
62
|
Object.freeze({
|
|
44
63
|
id: "comet",
|
|
@@ -137,15 +156,84 @@ const THEME_DEFINITIONS = Object.freeze([
|
|
|
137
156
|
},
|
|
138
157
|
}),
|
|
139
158
|
Object.freeze({
|
|
140
|
-
id: "
|
|
141
|
-
label: "
|
|
142
|
-
frameCount:
|
|
143
|
-
frameDelayMs:
|
|
159
|
+
id: "stack",
|
|
160
|
+
label: "Stack",
|
|
161
|
+
frameCount: 76,
|
|
162
|
+
frameDelayMs: 55,
|
|
163
|
+
loopPauseMs: 0,
|
|
164
|
+
startFrame: 0,
|
|
165
|
+
frameDelay(index) {
|
|
166
|
+
return stackState(index).delayMs;
|
|
167
|
+
},
|
|
168
|
+
render(index) {
|
|
169
|
+
const cells = blankTrack();
|
|
170
|
+
const state = stackState(index);
|
|
171
|
+
if (state.shift !== undefined) {
|
|
172
|
+
for (let column = 0; column < 9; column += 1) {
|
|
173
|
+
setGlowCell(cells, state.shift + column, 8 - column);
|
|
174
|
+
}
|
|
175
|
+
} else {
|
|
176
|
+
for (let column = 0; column < state.parked; column += 1) setGlowCell(cells, column, 5);
|
|
177
|
+
if (state.head !== undefined) drawComet(cells, state.head, -1, 2);
|
|
178
|
+
}
|
|
179
|
+
return wrapTrack(cells);
|
|
180
|
+
},
|
|
181
|
+
}),
|
|
182
|
+
Object.freeze({
|
|
183
|
+
id: "relay",
|
|
184
|
+
label: "Relay",
|
|
185
|
+
frameCount: 40,
|
|
186
|
+
frameDelayMs: 85,
|
|
144
187
|
loopPauseMs: 0,
|
|
145
188
|
startFrame: 0,
|
|
189
|
+
frameDelay(index) {
|
|
190
|
+
const phase = index % 10;
|
|
191
|
+
return phase === 0 ? 220 : phase < 7 ? 85 : 70;
|
|
192
|
+
},
|
|
146
193
|
render(index) {
|
|
147
194
|
const cells = blankTrack();
|
|
148
|
-
|
|
195
|
+
const leg = Math.floor(index / 10);
|
|
196
|
+
const from = RELAY_ROUTE[leg];
|
|
197
|
+
const to = RELAY_ROUTE[leg + 1];
|
|
198
|
+
const phase = index % 10;
|
|
199
|
+
const active = phase === 0 ? from : phase >= 7 ? to : -1;
|
|
200
|
+
const level = phase >= 7 ? [0, 1, 3][phase - 7] : 0;
|
|
201
|
+
for (let center = 2; center <= 16; center += 7) {
|
|
202
|
+
for (let offset = -1; offset <= 1; offset += 1) {
|
|
203
|
+
setGlowCell(cells, center + offset, center === active ? level + Math.abs(offset) * 2 : 7);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (phase > 0 && phase < 7) {
|
|
207
|
+
const direction = Math.sign(to - from);
|
|
208
|
+
drawComet(cells, from + direction * phase, direction, 2);
|
|
209
|
+
}
|
|
210
|
+
return wrapTrack(cells);
|
|
211
|
+
},
|
|
212
|
+
}),
|
|
213
|
+
Object.freeze({
|
|
214
|
+
id: "split",
|
|
215
|
+
label: "Split",
|
|
216
|
+
frameCount: 29,
|
|
217
|
+
frameDelayMs: 65,
|
|
218
|
+
loopPauseMs: 0,
|
|
219
|
+
startFrame: 0,
|
|
220
|
+
frameDelay(index) {
|
|
221
|
+
return index < 10 ? 70 : index < 13 ? 80 : index < 28 ? 65 : 220;
|
|
222
|
+
},
|
|
223
|
+
render(index) {
|
|
224
|
+
const cells = blankTrack();
|
|
225
|
+
if (index < 10) {
|
|
226
|
+
drawComet(cells, index, 1, 4);
|
|
227
|
+
} else if (index < 13) {
|
|
228
|
+
const level = [2, 0, 1][index - 10];
|
|
229
|
+
for (let offset = -2; offset <= 2; offset += 1) {
|
|
230
|
+
setGlowCell(cells, 9 + offset, level + Math.abs(offset));
|
|
231
|
+
}
|
|
232
|
+
} else if (index < 28) {
|
|
233
|
+
const distance = index - 13;
|
|
234
|
+
drawComet(cells, 9 - distance, -1, 4);
|
|
235
|
+
drawComet(cells, 10 + distance, 1, 4);
|
|
236
|
+
}
|
|
149
237
|
return wrapTrack(cells);
|
|
150
238
|
},
|
|
151
239
|
}),
|
package/src/user-settings.mjs
CHANGED
|
@@ -37,7 +37,8 @@ export function readUserSettings({ env = process.env, home = os.homedir(), stric
|
|
|
37
37
|
}
|
|
38
38
|
if (Object.hasOwn(parsed, TERMINAL_ANIMATION_KEY)) {
|
|
39
39
|
const value = parsed[TERMINAL_ANIMATION_KEY];
|
|
40
|
-
|
|
40
|
+
// Accept the retired value on read so upgrades can fall back and select a replacement.
|
|
41
|
+
if (value !== "braille" && !isTerminalAnimationTheme(value)) {
|
|
41
42
|
const choices = TERMINAL_ANIMATION_THEMES.map(({ id }) => id).join(", ");
|
|
42
43
|
if (strict) {
|
|
43
44
|
throw invalidSettings(filePath, `${TERMINAL_ANIMATION_KEY} must be one of: ${choices}`);
|