codex-copilot-dx 0.9.24 → 0.9.25
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 +9 -6
- package/bin/cli.mjs +19 -29
- package/docs/ARCHITECTURE.md +50 -0
- package/docs/README.md +4 -0
- package/docs/REFERENCE.md +53 -8
- package/package.json +3 -2
- package/src/adapter.mjs +1 -0
- package/src/cli-image.mjs +13 -10
- package/src/cli-options.mjs +1 -1
- package/src/cli-status.mjs +5 -0
- package/src/client-integration.mjs +41 -0
- package/src/codex-config-doctor.mjs +3 -3
- package/src/codex-paths.mjs +10 -0
- package/src/codex-thread-compression.mjs +11 -6
- package/src/config.mjs +10 -6
- package/src/copilot-responses-policy.mjs +4 -2
- package/src/dashboard/index.html +7 -0
- package/src/dashboard/language.js +13 -0
- package/src/dashboard/ui.js +9 -0
- package/src/lock.mjs +29 -0
- package/src/native-prediction-profile.mjs +5 -8
- package/src/native-prediction-state.mjs +29 -0
- package/src/native-prediction-transport.mjs +18 -5
- package/src/native-prediction.mjs +32 -8
- package/src/network-route.mjs +102 -0
- package/src/observability.mjs +2 -0
- package/src/operation-error.mjs +37 -0
- package/src/package-update.mjs +2 -3
- package/src/prediction-retirement.mjs +13 -2
- package/src/proxy-tunnel.mjs +102 -0
- package/src/responses-request.mjs +5 -2
- package/src/user-settings.mjs +17 -4
- package/src/version.mjs +23 -46
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ The dashboard does not poll in the background; refresh when needed.
|
|
|
30
30
|
| Command | Purpose |
|
|
31
31
|
| --- | --- |
|
|
32
32
|
| `ccdx help` | Command help; use `ccdx help <command>` for details |
|
|
33
|
-
| `ccdx start` | Start
|
|
33
|
+
| `ccdx start` | Start or reuse the adapter and open Codex when auto-launch is enabled |
|
|
34
34
|
| `ccdx status` | Runtime, limits and bounded request diagnostics |
|
|
35
35
|
| `ccdx doctor` | Configuration, adapter and read-only App/catalog diagnostics |
|
|
36
36
|
| `ccdx doctor config` | Read-only configuration check |
|
|
@@ -73,8 +73,8 @@ The dashboard does not poll in the background; refresh when needed.
|
|
|
73
73
|
trust and an App-only route for `chatgpt.com` and `ab.chatgpt.com`; HTTPS on those
|
|
74
74
|
hosts is decrypted locally, but only prediction configuration is changed.
|
|
75
75
|
Other domains and the CCDX model endpoint stay on their existing routes.
|
|
76
|
-
No program files or OS trust are changed.
|
|
77
|
-
|
|
76
|
+
No program files or OS trust are changed. Static HTTP/HTTPS/SOCKS5 proxies and bypass rules are retained.
|
|
77
|
+
Dynamic PAC/WPAD routes are reported as unsupported; see the reference for routing, rollback and client-version limits.
|
|
78
78
|
- CCDX adds experimental local history compression only when installed App
|
|
79
79
|
runtimes support it and report it off. Existing choices are preserved; an
|
|
80
80
|
already-enabled default, unavailable runtime or failed check is skipped.
|
|
@@ -84,11 +84,14 @@ The dashboard does not poll in the background; refresh when needed.
|
|
|
84
84
|
expose it to the public internet. The dashboard is not an authentication wall
|
|
85
85
|
for the API. Diagnostic errors may contain sensitive upstream text.
|
|
86
86
|
|
|
87
|
-
Startup checks GitHub
|
|
87
|
+
Startup checks GitHub's latest-release web redirect, not npm or the anonymous REST quota, and suggests `ccdx update github`.
|
|
88
|
+
Updates install the exact stable tag; dependencies still use npm's configured registry. Lookup errors identify HTTP refusal, timeout, network failure or invalid redirects.
|
|
89
|
+
|
|
90
|
+
The dashboard and CLI separate prediction preference, bridge state and observed config traffic; candidate visibility stays unknown.
|
|
91
|
+
Inference, client integration, optional images and management have separate ownership. See [architecture boundaries](docs/ARCHITECTURE.md).
|
|
88
92
|
|
|
89
93
|
See the [complete command/configuration reference](https://github.com/DaleXiao/codex-copilot-dx/blob/main/docs/REFERENCE.md)
|
|
90
|
-
for authentication recovery, configuration
|
|
91
|
-
logging, image setup and client compatibility details.
|
|
94
|
+
for authentication recovery, configuration safety, limits, logging, images and client compatibility.
|
|
92
95
|
The same reference is included as `docs/REFERENCE.md` in the installed npm package.
|
|
93
96
|
|
|
94
97
|
## Development
|
package/bin/cli.mjs
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { ensureAuth, openCodex } from "../src/launcher.mjs";
|
|
4
|
-
import { ensureCodexConfig } from "../src/config.mjs";
|
|
5
4
|
import { startAdapter } from "../src/adapter.mjs";
|
|
6
5
|
import { defaultCopilotClient, refreshVSCodeVersion } from "../src/copilot.mjs";
|
|
7
6
|
import { status } from "../src/status.mjs";
|
|
@@ -22,9 +21,7 @@ import { runAuthCommand } from "../src/cli-auth.mjs";
|
|
|
22
21
|
import { createProfileRuntime } from "../src/profile-runtime.mjs";
|
|
23
22
|
import { createProfileModelRuntime } from "../src/profile-model-runtime.mjs";
|
|
24
23
|
import { createCodexModelCatalog } from "../src/codex-model-catalog.mjs";
|
|
25
|
-
import { readImageProviderConfig } from "../src/image-provider-config.mjs";
|
|
26
24
|
import { setResponseHistoryMaxBytes } from "../src/response-history.mjs";
|
|
27
|
-
import { ensureCodexThreadCompression } from "../src/codex-thread-compression.mjs";
|
|
28
25
|
|
|
29
26
|
const LOCAL_VERSION = localPackageVersion();
|
|
30
27
|
const CLI_NAME = cliCommandName();
|
|
@@ -91,7 +88,10 @@ if (CLI.command === "suggestion") {
|
|
|
91
88
|
if (CLI.command === "prediction") {
|
|
92
89
|
try {
|
|
93
90
|
const { runPredictionCommand } = await import("../src/native-prediction.mjs");
|
|
94
|
-
await runPredictionCommand({ action: CLI.action
|
|
91
|
+
await runPredictionCommand({ action: CLI.action, runtimeSnapshot: () => {
|
|
92
|
+
const probe = parseAdapterProbeOptions(process.env);
|
|
93
|
+
return readAdapterStatus({ host: probe.adapterHost, port: probe.adapterPort, timeoutMs: probe.existingAdapterTimeoutMs }).then(({ data }) => data);
|
|
94
|
+
} });
|
|
95
95
|
process.exit(0);
|
|
96
96
|
} catch (error) {
|
|
97
97
|
console.error(status("err", error.message));
|
|
@@ -265,6 +265,16 @@ let activeServer = null;
|
|
|
265
265
|
let modelRefreshTimer = null;
|
|
266
266
|
let shuttingDown = false;
|
|
267
267
|
let nativePrediction = null;
|
|
268
|
+
let clientIntegration = null;
|
|
269
|
+
|
|
270
|
+
async function clientIntegrations() {
|
|
271
|
+
if (!clientIntegration) {
|
|
272
|
+
const { createClientIntegration } = await import("../src/client-integration.mjs");
|
|
273
|
+
clientIntegration = createClientIntegration({ port: ADAPTER_PORT, host: ADAPTER_HOST,
|
|
274
|
+
info: (message) => console.log(status("info", message)), warning: (message) => console.warn(status("warn", message)) });
|
|
275
|
+
}
|
|
276
|
+
return clientIntegration;
|
|
277
|
+
}
|
|
268
278
|
|
|
269
279
|
if (LOGGING.filePath) {
|
|
270
280
|
console.log(status("info", `Debug log: ${LOGGING.filePath}`));
|
|
@@ -310,25 +320,7 @@ async function reuseRunningAdapterIfAvailable() {
|
|
|
310
320
|
}
|
|
311
321
|
|
|
312
322
|
async function configureCodexClient() {
|
|
313
|
-
|
|
314
|
-
const { retirePredictionIntegration } = await import("../src/prediction-retirement.mjs");
|
|
315
|
-
const result = retirePredictionIntegration();
|
|
316
|
-
if (result.changed) console.log(status("info", "Removed the retired CCDX prediction integration"));
|
|
317
|
-
if (result.preserved) console.warn(status("warn", "Modified prediction settings were preserved"));
|
|
318
|
-
} catch (error) {
|
|
319
|
-
console.warn(status("warn", `${error.message}. Core startup will continue.`));
|
|
320
|
-
}
|
|
321
|
-
const imageProviderEnabled = Boolean(readImageProviderConfig());
|
|
322
|
-
const { imageMcpSkipped } = ensureCodexConfig(ADAPTER_PORT, { host: ADAPTER_HOST, imageProviderEnabled });
|
|
323
|
-
if (imageProviderEnabled && !imageMcpSkipped) {
|
|
324
|
-
try {
|
|
325
|
-
const { syncEnabledImageSkill } = await import("../src/cli-image.mjs");
|
|
326
|
-
syncEnabledImageSkill({ adapterPort: ADAPTER_PORT, adapterHost: ADAPTER_HOST });
|
|
327
|
-
} catch (error) {
|
|
328
|
-
console.warn(status("warn", `Image guidance could not be updated: ${error.message}`));
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
return { compressionSetup: runInBackground(ensureCodexThreadCompression) };
|
|
323
|
+
return (await clientIntegrations()).configure();
|
|
332
324
|
}
|
|
333
325
|
|
|
334
326
|
if (CLI.command === "doctor") {
|
|
@@ -403,16 +395,14 @@ try {
|
|
|
403
395
|
streamIdleTimeoutMs: RUNTIME.streamIdleTimeoutMs,
|
|
404
396
|
terminalAnimationTheme,
|
|
405
397
|
nativePredictionPac: () => nativePrediction?.pacUrl || null,
|
|
398
|
+
clientIntegrationSnapshot: () => clientIntegration?.snapshot() || null,
|
|
406
399
|
});
|
|
407
400
|
|
|
408
401
|
// Point Codex App at the adapter.
|
|
409
402
|
const { compressionSetup } = await configureCodexClient();
|
|
410
403
|
|
|
411
404
|
// Optional native prediction setup must never prevent the core adapter from running.
|
|
412
|
-
|
|
413
|
-
const { startNativePrediction } = await import("../src/native-prediction.mjs");
|
|
414
|
-
nativePrediction = await startNativePrediction({ warning: (message) => console.log(status("info", message)) });
|
|
415
|
-
} catch (error) { console.warn(status("warn", `${error.message}. Core startup will continue.`)); }
|
|
405
|
+
nativePrediction = await (await clientIntegrations()).start();
|
|
416
406
|
|
|
417
407
|
// Launch Codex when available. A running App is never forcibly restarted.
|
|
418
408
|
await openCodex({ predictionPacUrl: nativePrediction?.pacUrl, predictionApp: nativePrediction?.app });
|
|
@@ -444,7 +434,7 @@ try {
|
|
|
444
434
|
let exitCode = 0;
|
|
445
435
|
try {
|
|
446
436
|
if (modelRefreshTimer) clearInterval(modelRefreshTimer);
|
|
447
|
-
await
|
|
437
|
+
await clientIntegration?.close();
|
|
448
438
|
const result = await closeHttpServer(activeServer, { timeoutMs: RUNTIME.shutdownTimeoutMs });
|
|
449
439
|
if (result.forced) console.warn(status("warn", "Forced remaining adapter connections closed"));
|
|
450
440
|
await flushUsageWrites();
|
|
@@ -461,7 +451,7 @@ try {
|
|
|
461
451
|
await compressionSetup;
|
|
462
452
|
} catch (e) {
|
|
463
453
|
console.error(status("err", e.message));
|
|
464
|
-
await
|
|
454
|
+
await clientIntegration?.close().catch(() => {});
|
|
465
455
|
await closeHttpServer(activeServer, { timeoutMs: RUNTIME.shutdownTimeoutMs }).catch(() => {});
|
|
466
456
|
await flushUsageWrites();
|
|
467
457
|
await LOGGING.cleanup();
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Architecture boundaries
|
|
2
|
+
|
|
3
|
+
CCDX is one local Node service for Codex App. Client integration, image tools and
|
|
4
|
+
the dashboard are optional parts of that service, not a general plugin framework.
|
|
5
|
+
|
|
6
|
+
| Responsibility | Ownership | Boundary |
|
|
7
|
+
| --- | --- | --- |
|
|
8
|
+
| Core inference | `adapter.mjs`, Responses modules, model catalog, history | Copilot requests, streaming and recovery |
|
|
9
|
+
| Client integration | `client-integration.mjs`, Codex config, native prediction | Client setup, optional start/stop, diagnostic snapshots |
|
|
10
|
+
| Optional images | Image provider, CLI setup, image MCP and skill | Opt-in API access and image delivery; failures do not block core startup |
|
|
11
|
+
| Management | CLI, dashboard, observability, package updates | Bounded metadata and explicit user actions |
|
|
12
|
+
| Shared foundations | Codex paths, file locks, atomic files, network routes | No dependency on inference or dashboard rendering |
|
|
13
|
+
|
|
14
|
+
## Composition and lifecycle
|
|
15
|
+
|
|
16
|
+
The CLI composes the service. Client integration owns its `configure`, `start`,
|
|
17
|
+
`snapshot` and `close` operations. It does not forward inference requests or
|
|
18
|
+
render the dashboard. The adapter exposes integration snapshots and keeps
|
|
19
|
+
management/MCP routes separate from the Responses path.
|
|
20
|
+
|
|
21
|
+
Native prediction has a separate loopback transport and an isolated HTTPS agent.
|
|
22
|
+
Only its two selected hosts use the App-specific route. Non-configuration requests
|
|
23
|
+
on those hosts are forwarded unchanged. The bridge does not replace Copilot or
|
|
24
|
+
image-provider networking, modify client program files, or install system trust.
|
|
25
|
+
Static upstream proxy routes and bypass rules are preserved. Dynamic PAC/WPAD
|
|
26
|
+
and CIDR bypass rules are reported as unsupported, not silently discarded.
|
|
27
|
+
|
|
28
|
+
## Configuration ownership
|
|
29
|
+
|
|
30
|
+
`CODEX_HOME` selects Codex configuration. App profile/trust paths have their own
|
|
31
|
+
selection and are not inferred from that directory. CCDX writers use a shared
|
|
32
|
+
per-file lock, reread current contents and preserve unrelated values. Locks do
|
|
33
|
+
not coordinate editors or other applications that ignore them. Read-only status
|
|
34
|
+
and doctor commands do not write configuration.
|
|
35
|
+
|
|
36
|
+
Legacy public module exports remain available. This change adds a concrete
|
|
37
|
+
composition boundary; it does not relocate every file or add speculative APIs.
|
|
38
|
+
|
|
39
|
+
## Evidence and verification
|
|
40
|
+
|
|
41
|
+
Prediction preference, bridge readiness and observed configuration responses are
|
|
42
|
+
different states. None proves that a candidate appeared in the App. The dashboard
|
|
43
|
+
keeps that result unknown. Integration diagnostics retain bounded, redacted
|
|
44
|
+
metadata, not conversation or image contents.
|
|
45
|
+
|
|
46
|
+
Tests cover module dependency direction, default-off behavior, concurrent writers,
|
|
47
|
+
proxy handshakes, TLS validation, streaming, history isolation and release lookup.
|
|
48
|
+
Performance measurements distinguish request preparation from model latency.
|
|
49
|
+
Release reports describe a specific environment, not an SLA or a guarantee of
|
|
50
|
+
compatibility with every future client or corporate network policy.
|
package/docs/README.md
CHANGED
|
@@ -4,6 +4,8 @@ The [root README](../README.md) is the quick start; the [command/configuration
|
|
|
4
4
|
reference](REFERENCE.md) preserves complete current behavior and limits. `ccdx --help` and
|
|
5
5
|
`ccdx <command> --help` describe the installed package's CLI. Code and tests
|
|
6
6
|
resolve any discrepancy; CLI version and running-adapter version can differ.
|
|
7
|
+
The [architecture boundaries](ARCHITECTURE.md) explain module ownership,
|
|
8
|
+
optional integration lifecycle and configuration safety.
|
|
7
9
|
|
|
8
10
|
`ccdx doctor config` performs local, read-only configuration diagnostics and
|
|
9
11
|
previews the current startup writer. It does not claim to resolve the complete
|
|
@@ -15,6 +17,8 @@ These reports record a specific release and test environment. Their version,
|
|
|
15
17
|
test counts, timings, live-provider observations, and limitations remain
|
|
16
18
|
historical facts rather than current guarantees.
|
|
17
19
|
|
|
20
|
+
- [0.9.25: static proxy compatibility, integration boundaries and release diagnostics](RELEASE_0.9.25.md)
|
|
21
|
+
|
|
18
22
|
- [0.9.19: GitHub startup checks and stable-release updates](RELEASE_0.9.19.md)
|
|
19
23
|
- [0.9.21: compatibility-gated local history compression](RELEASE_0.9.21.md)
|
|
20
24
|
- [0.9.20: timing evidence, real-client checks and bounded analytics reuse](RELEASE_0.9.20.md)
|
package/docs/REFERENCE.md
CHANGED
|
@@ -53,8 +53,25 @@ account identity or account-permission value is rewritten. Credentials are not
|
|
|
53
53
|
logged or persisted by the bridge. The local private key has owner-only permissions;
|
|
54
54
|
its signing key is discarded after certificate creation.
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
Static proxy settings are composed with the local bridge. For HTTPS targets,
|
|
57
|
+
`https_proxy` takes precedence over `HTTPS_PROXY`, then `all_proxy`/`ALL_PROXY`,
|
|
58
|
+
then the macOS HTTPS proxy or SOCKS proxy. HTTP targets follow their corresponding
|
|
59
|
+
HTTP setting. An HTTP-only environment proxy is not used for HTTPS. HTTP CONNECT,
|
|
60
|
+
TLS to an HTTPS proxy, SOCKS5 (including username/password), `NO_PROXY`/`no_proxy`,
|
|
61
|
+
system exceptions and simple-host exclusions are supported. Proxy credentials
|
|
62
|
+
remain in memory, are never included in PAC responses or diagnostics, and are
|
|
63
|
+
sent only to the proxy. TLS verification remains enabled for both proxy and target.
|
|
64
|
+
Other App domains keep the resolved original route; if a bridge fails, its PAC
|
|
65
|
+
fallback uses that route rather than adding an unrequested direct connection.
|
|
66
|
+
Route selection is a startup snapshot; restart CCDX and Codex after changing it.
|
|
67
|
+
Bypass lists support host/domain patterns and optional ports. Unsupported CIDR
|
|
68
|
+
notation is reported explicitly rather than silently changing the route.
|
|
69
|
+
|
|
70
|
+
Dynamic PAC/WPAD is not executed or overwritten. Setup identifies the exact
|
|
71
|
+
system setting and preserves its rules. A non-default `CODEX_API_BASE_URL` is
|
|
72
|
+
also preserved and reported as a custom-route conflict; setting it to the normal
|
|
73
|
+
`https://chatgpt.com/backend-api` does not cause a conflict. No automatic direct
|
|
74
|
+
fallback after upstream-proxy rejection is added. The default App profile
|
|
58
75
|
is `~/Library/Application Support/Codex`; an explicit
|
|
59
76
|
`CODEX_ELECTRON_USER_DATA_PATH` is respected. Quit the App before trust changes;
|
|
60
77
|
CCDX never kills it. The launcher uses the exact App that passed the compatibility
|
|
@@ -79,14 +96,35 @@ trust removal is deferred; quit it and run `prediction off` again. Only the exac
|
|
|
79
96
|
owned certificate record is removed, never the whole store. Failed activation
|
|
80
97
|
rolls back newly added trust; certificate replacement keeps old trust until the
|
|
81
98
|
new preference is committed. If the bridge stops, new connections can fall back
|
|
82
|
-
to
|
|
99
|
+
to the original route; already-interrupted requests are not replayed. Run `ccdx`
|
|
83
100
|
after quitting Codex to attach a newly started bridge. `CCDX_AUTO_LAUNCH=0` keeps
|
|
84
101
|
automatic launch disabled and does not attach the route to a manually opened App.
|
|
85
102
|
Optional prediction failures never prevent the core adapter or image service from
|
|
86
|
-
starting. `prediction status` reports the saved preference,
|
|
87
|
-
|
|
103
|
+
starting. `prediction status` reports the saved preference and, when a compatible
|
|
104
|
+
running adapter responds, its bridge state, configuration-response observations
|
|
105
|
+
and bounded TLS/upstream errors. The dashboard uses the same snapshot and manual
|
|
106
|
+
refresh. Configuration traffic is evidence of a request reaching the bridge, not
|
|
107
|
+
proof of a generated/displayed candidate. Candidate visibility remains unknown;
|
|
108
|
+
no prompt matching, UI injection, transcript scanning or extra inference is used
|
|
109
|
+
to invent that status. Prediction settings are stored separately in
|
|
88
110
|
`~/.config/codex-copilot-dx/native-prediction/` (or under `XDG_CONFIG_HOME`).
|
|
89
111
|
|
|
112
|
+
### Configuration ownership
|
|
113
|
+
|
|
114
|
+
Core setup, image setup/guidance, config doctor, optional history compression and
|
|
115
|
+
legacy prediction cleanup resolve the same `CODEX_HOME/config.toml`; absent
|
|
116
|
+
`CODEX_HOME`, they use `~/.codex/config.toml`. An explicit function-level file path
|
|
117
|
+
still takes precedence for embedders. App-profile trust is a separate location
|
|
118
|
+
selected by `CODEX_ELECTRON_USER_DATA_PATH`, not by `CODEX_HOME`.
|
|
119
|
+
|
|
120
|
+
CCDX-owned config mutations share an owner-recorded cross-process lock, reread
|
|
121
|
+
under the lock, and retain existing atomic replacement/rollback checks. User
|
|
122
|
+
settings updates use a separate lock for their shared JSON file; prediction
|
|
123
|
+
on/off uses its own settings lock. Readers take no lock and write no lock file.
|
|
124
|
+
Contention fails with a retry message instead of stealing a live lock. Other
|
|
125
|
+
applications do not participate in these locks, so snapshot conflict checks still
|
|
126
|
+
apply; the locks do not claim a global filesystem transaction.
|
|
127
|
+
|
|
90
128
|
### Core adapter
|
|
91
129
|
|
|
92
130
|
One in-process adapter listens on `127.0.0.1:2026` by default and exposes:
|
|
@@ -141,7 +179,7 @@ On a normal launch, CCDX:
|
|
|
141
179
|
1. Prints the installed package version and checks for a newer npm release in the background.
|
|
142
180
|
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.
|
|
143
181
|
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.
|
|
144
|
-
4. Checks the startup-managed settings in `~/.codex/config.toml
|
|
182
|
+
4. Checks the startup-managed settings in `CODEX_HOME/config.toml` (default `~/.codex/config.toml`), including the local `/v1` endpoint.
|
|
145
183
|
5. Attempts to open Codex App on macOS unless `CCDX_AUTO_LAUNCH` disables it.
|
|
146
184
|
|
|
147
185
|
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.
|
|
@@ -156,7 +194,7 @@ If a compatible adapter is already running, a later launch reuses it and checks
|
|
|
156
194
|
|
|
157
195
|
### Codex configuration writes
|
|
158
196
|
|
|
159
|
-
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.
|
|
197
|
+
Normal startup and reuse of an existing adapter check `CODEX_HOME/config.toml` (default `~/.codex/config.toml`) once, after startup prerequisites succeed. There is no file watcher or periodic configuration rewrite. Startup, image setup, config doctor, compression and legacy prediction cleanup share this path. CCDX does not edit project-level Codex files.
|
|
160
198
|
|
|
161
199
|
| Setting | Existing file | New file |
|
|
162
200
|
| --- | --- | --- |
|
|
@@ -318,7 +356,14 @@ ccdx update npm
|
|
|
318
356
|
ccdx update github
|
|
319
357
|
```
|
|
320
358
|
|
|
321
|
-
Normal startup checks the
|
|
359
|
+
Normal startup checks the official GitHub `releases/latest` web endpoint once in
|
|
360
|
+
the background, without accessing the npm registry or REST API. The anonymous
|
|
361
|
+
check requires no PAT or Copilot credentials, has a two-second deadline, validates
|
|
362
|
+
only the redirect header, cancels the response body without reading HTML, and
|
|
363
|
+
silently skips errors. Redirects must use HTTPS, the exact GitHub repository and
|
|
364
|
+
a stable version tag with no credentials, query or fragment. It never follows
|
|
365
|
+
the redirect, retries, polls, installs or restarts. On adapter reuse, Ready is
|
|
366
|
+
displayed before waiting for any remaining check budget.
|
|
322
367
|
|
|
323
368
|
`ccdx update gh` is accepted as shorthand for the GitHub source. The npm source installs `codex-copilot-dx@latest` through the registry already configured for npm, including a company mirror. The GitHub source queries the latest stable published Release (ten-second deadline), validates its version tag, and installs that exact tag from `DaleXiao/codex-copilot-dx`, not `main`. Lookup failure stops before installation, without an npm fallback. It opts in to Git fetching only for that command and does not change npm's persistent `allow-git` setting.
|
|
324
369
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codex-copilot-dx",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.25",
|
|
4
4
|
"description": "Use Codex App with GitHub Copilot",
|
|
5
5
|
"author": "Dale Xiao",
|
|
6
6
|
"type": "module",
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
"scripts/benchmark-probes.mjs",
|
|
16
16
|
"scripts/fixtures/",
|
|
17
17
|
"README.md",
|
|
18
|
-
"docs/REFERENCE.md"
|
|
18
|
+
"docs/REFERENCE.md",
|
|
19
|
+
"docs/ARCHITECTURE.md"
|
|
19
20
|
],
|
|
20
21
|
"keywords": [
|
|
21
22
|
"codex",
|
package/src/adapter.mjs
CHANGED
package/src/cli-image.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import os from "node:os";
|
|
3
|
-
import path from "node:path";
|
|
4
3
|
import readline from "node:readline";
|
|
5
4
|
import { createInterface } from "node:readline/promises";
|
|
5
|
+
import { codexPaths } from "./codex-paths.mjs";
|
|
6
|
+
import { withFileLockSync } from "./lock.mjs";
|
|
6
7
|
import { atomicWriteFileIfChangedSync, atomicWriteFilePairSync } from "./atomic-file.mjs";
|
|
7
8
|
import { terminalCell } from "./cli-table.mjs";
|
|
8
9
|
import { computeImageMcpCodexConfig } from "./config.mjs";
|
|
@@ -15,11 +16,6 @@ import {
|
|
|
15
16
|
readImageProviderConfig,
|
|
16
17
|
validateImageProviderConfig,
|
|
17
18
|
} from "./image-provider-config.mjs";
|
|
18
|
-
|
|
19
|
-
function defaultCodexConfigPath(home) {
|
|
20
|
-
return path.join(home, ".codex", "config.toml");
|
|
21
|
-
}
|
|
22
|
-
|
|
23
19
|
async function visibleQuestion(input, output, question) {
|
|
24
20
|
const prompt = createInterface({ input, output });
|
|
25
21
|
try { return await prompt.question(question); }
|
|
@@ -102,7 +98,10 @@ function configSummary(config) {
|
|
|
102
98
|
};
|
|
103
99
|
}
|
|
104
100
|
|
|
105
|
-
function enableImageProvider(
|
|
101
|
+
function enableImageProvider(options) {
|
|
102
|
+
return withFileLockSync(`${options.codexPath}.ccdx.lock`, () => enableImageProviderUnlocked(options));
|
|
103
|
+
}
|
|
104
|
+
function enableImageProviderUnlocked({ providerConfig, providerPath, codexPath, codexContent, adapterPort, adapterHost }) {
|
|
106
105
|
const codex = computeImageMcpCodexConfig(codexContent, {
|
|
107
106
|
enabled: true,
|
|
108
107
|
adapterPort,
|
|
@@ -128,7 +127,11 @@ function enableImageProvider({ providerConfig, providerPath, codexPath, codexCon
|
|
|
128
127
|
return { codexChanged: codex.changed, config: normalized };
|
|
129
128
|
}
|
|
130
129
|
|
|
131
|
-
function disableImageProvider(
|
|
130
|
+
function disableImageProvider(options) {
|
|
131
|
+
if (!fs.existsSync(options.codexPath)) return disableImageProviderUnlocked(options);
|
|
132
|
+
return withFileLockSync(`${options.codexPath}.ccdx.lock`, () => disableImageProviderUnlocked(options));
|
|
133
|
+
}
|
|
134
|
+
function disableImageProviderUnlocked({ providerPath, codexPath, codexContent, adapterPort, adapterHost }) {
|
|
132
135
|
const codex = computeImageMcpCodexConfig(codexContent, {
|
|
133
136
|
enabled: false,
|
|
134
137
|
adapterPort,
|
|
@@ -154,7 +157,7 @@ function disableImageProvider({ providerPath, codexPath, codexContent, adapterPo
|
|
|
154
157
|
return { changed: providerExists || codex.changed || skill.changed, preservedGuidance: Boolean(skill.preserved) };
|
|
155
158
|
}
|
|
156
159
|
|
|
157
|
-
export function syncEnabledImageSkill({ home = os.homedir(), codexPath =
|
|
160
|
+
export function syncEnabledImageSkill({ home = os.homedir(), env = process.env, codexPath = codexPaths({ env, home }).config, ...options } = {}) {
|
|
158
161
|
return updateImageSkill({ ...options, enabled: true, codexPath });
|
|
159
162
|
}
|
|
160
163
|
|
|
@@ -179,7 +182,7 @@ export async function runImageCommand({
|
|
|
179
182
|
probeFetchImpl = fetch,
|
|
180
183
|
adapterPort = 2026,
|
|
181
184
|
adapterHost = "127.0.0.1",
|
|
182
|
-
codexPath =
|
|
185
|
+
codexPath = codexPaths({ env, home }).config,
|
|
183
186
|
} = {}) {
|
|
184
187
|
const providerPath = imageProviderConfigPath({ env, home });
|
|
185
188
|
if (action === "status") {
|
package/src/cli-options.mjs
CHANGED
|
@@ -372,7 +372,7 @@ function topicHelp(name, topic) {
|
|
|
372
372
|
"image-status": `Usage:\n ${name} image-status\n\nShows provider configuration and locally advertised generation/editing tools without generating an image or exposing its API key.`,
|
|
373
373
|
"auto-review-model": `Usage:\n ${name} auto-review-model\n\nSelect a Responses model, then an advertised reasoning effort. Enter keeps the default/current choice; q cancels both steps without saving. Effort 0 follows the client (low when omitted). Settings apply only to Auto-review on the next request; no client patch or restart is required for adapters running 0.9.13 or newer.`,
|
|
374
374
|
"suggestion": `The CCDX prediction panel has been retired. Run ${name} suggestion off to safely remove unchanged CCDX registrations. This cleanup does not change native predictions; see ${name} prediction --help for that separate opt-in feature.`,
|
|
375
|
-
prediction: `Usage:\n ${name} prediction [status|on|off]\n\nOptional native next-message predictions; off by default. macOS only. Quit Codex before on/off, then restart ccdx. On adds private App-profile certificate trust and an App-only route for chatgpt.com and ab.chatgpt.com. HTTPS on those hosts is decrypted locally; only prediction configuration is changed. No client program files, OS trust, account permissions, MCP or Hooks are changed.
|
|
375
|
+
prediction: `Usage:\n ${name} prediction [status|on|off]\n\nOptional native next-message predictions; off by default. macOS only. Quit Codex before on/off, then restart ccdx. On adds private App-profile certificate trust and an App-only route for chatgpt.com and ab.chatgpt.com. HTTPS on those hosts is decrypted locally; only prediction configuration is changed. No client program files, OS trust, account permissions, MCP or Hooks are changed. Static HTTP/HTTPS/SOCKS5 proxies and bypass rules are preserved; dynamic PAC/WPAD and CIDR bypass rules are reported as unsupported. Official prediction configuration is preserved; the conversation's model is used. App updates require another compatibility check. Status distinguishes saved preference and observed config traffic from unverified candidate visibility.`,
|
|
376
376
|
update: `Usage:\n ${name} update [npm|github]\n\nUpdates the global package from the configured npm registry or the latest stable GitHub release. With no source, an interactive terminal prompts for one.`,
|
|
377
377
|
version: `Usage:\n ${name} --version`,
|
|
378
378
|
};
|
package/src/cli-status.mjs
CHANGED
|
@@ -188,6 +188,11 @@ export function formatAdapterStatus({ baseUrl, data }, { commandName = "ccdx", c
|
|
|
188
188
|
}
|
|
189
189
|
}
|
|
190
190
|
lines.push(status("info", `Models: ${count(models.models)} total; Copilot token ${copilot.token_cached ? `cached (${duration(copilot.token_expires_in_ms)} remaining)` : "not cached"}`));
|
|
191
|
+
const prediction = data.client_integrations?.prediction;
|
|
192
|
+
if (prediction) {
|
|
193
|
+
lines.push(status("info", `Native predictions: ${prediction.configured ? "on" : "off"}; bridge ${prediction.bridge}; client route ${prediction.client_route}; visible candidate unknown`));
|
|
194
|
+
if (prediction.reason) lines.push(status("warn", terminalCell(prediction.reason).slice(0, 400)));
|
|
195
|
+
}
|
|
191
196
|
if (data.profiles?.codex) {
|
|
192
197
|
lines.push(status("info", `Profile: ${profileSummary("Codex", data.profiles.codex)}`));
|
|
193
198
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { ensureCodexConfig } from "./config.mjs";
|
|
2
|
+
import { readImageProviderConfig } from "./image-provider-config.mjs";
|
|
3
|
+
import { ensureCodexThreadCompression } from "./codex-thread-compression.mjs";
|
|
4
|
+
import { runInBackground } from "./startup.mjs";
|
|
5
|
+
import { createPredictionState } from "./native-prediction-state.mjs";
|
|
6
|
+
|
|
7
|
+
// The CLI composes the services; this module owns optional client integration
|
|
8
|
+
// lifecycle. It never forwards model requests or depends on dashboard rendering.
|
|
9
|
+
export function createClientIntegration({ port, host, info = () => {}, warning = () => {} } = {}) {
|
|
10
|
+
const prediction = createPredictionState();
|
|
11
|
+
let native = null;
|
|
12
|
+
return {
|
|
13
|
+
async configure() {
|
|
14
|
+
try {
|
|
15
|
+
const { retirePredictionIntegration } = await import("./prediction-retirement.mjs");
|
|
16
|
+
const result = retirePredictionIntegration();
|
|
17
|
+
if (result.changed) info("Removed the retired CCDX prediction integration");
|
|
18
|
+
if (result.preserved) warning("Modified prediction settings were preserved");
|
|
19
|
+
} catch (error) { warning(`${error.message}. Core startup will continue.`); }
|
|
20
|
+
const enabled = Boolean(readImageProviderConfig());
|
|
21
|
+
const { imageMcpSkipped } = ensureCodexConfig(port, { host, imageProviderEnabled: enabled });
|
|
22
|
+
if (enabled && !imageMcpSkipped) {
|
|
23
|
+
try {
|
|
24
|
+
const { syncEnabledImageSkill } = await import("./cli-image.mjs");
|
|
25
|
+
syncEnabledImageSkill({ adapterPort: port, adapterHost: host });
|
|
26
|
+
} catch (error) { warning(`Image guidance could not be updated: ${error.message}`); }
|
|
27
|
+
}
|
|
28
|
+
return { compressionSetup: runInBackground(ensureCodexThreadCompression) };
|
|
29
|
+
},
|
|
30
|
+
async start() {
|
|
31
|
+
try {
|
|
32
|
+
const { startNativePrediction } = await import("./native-prediction.mjs");
|
|
33
|
+
native = await startNativePrediction({ state: prediction, warning: info });
|
|
34
|
+
} catch (error) { prediction.unavailable(error); warning(`${error.message}. Core startup will continue.`); }
|
|
35
|
+
return native;
|
|
36
|
+
},
|
|
37
|
+
pacUrl() { return native?.pacUrl || null; },
|
|
38
|
+
snapshot() { return { prediction: prediction.snapshot() }; },
|
|
39
|
+
async close() { await native?.close(); native = null; },
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import os from "node:os";
|
|
3
|
-
import
|
|
3
|
+
import { codexPaths } from "./codex-paths.mjs";
|
|
4
4
|
import { isDeepStrictEqual } from "node:util";
|
|
5
5
|
import { parse } from "smol-toml";
|
|
6
6
|
import { computeUpdatedCodexConfig, initialCodexConfig } from "./config.mjs";
|
|
@@ -87,8 +87,8 @@ function checkUrl(value, expected, label) {
|
|
|
87
87
|
: { kind: "warn", message: `${label} differs from the CCDX address ${expected}` };
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
export function inspectCodexConfig({ home = os.homedir(), host = "127.0.0.1", port = 2026 } = {}) {
|
|
91
|
-
const filePath =
|
|
90
|
+
export function inspectCodexConfig({ home = os.homedir(), env: processEnv = process.env, host = "127.0.0.1", port = 2026 } = {}) {
|
|
91
|
+
const filePath = codexPaths({ env: processEnv, home }).config;
|
|
92
92
|
let content;
|
|
93
93
|
try {
|
|
94
94
|
content = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(fs.readFileSync(filePath));
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export function codexPaths({ env = process.env, home = os.homedir() } = {}) {
|
|
5
|
+
const directory = path.resolve(String(env.CODEX_HOME || "").trim() || path.join(home, ".codex"));
|
|
6
|
+
const profile = path.resolve(String(env.CODEX_ELECTRON_USER_DATA_PATH || "").trim()
|
|
7
|
+
|| path.join(home, "Library", "Application Support", "Codex"));
|
|
8
|
+
return { directory, config: path.join(directory, "config.toml"), profile,
|
|
9
|
+
certificateStore: path.join(profile, "Default", "ServerCertificate") };
|
|
10
|
+
}
|
|
@@ -6,6 +6,8 @@ import { promisify } from "node:util";
|
|
|
6
6
|
import { CODEX_APP_BINARY_PATHS } from "./codex-model-catalog.mjs";
|
|
7
7
|
import { parseCodexToml, codexTomlStatements, validateManagedConfigEdit } from "./config-toml.mjs";
|
|
8
8
|
import { atomicWriteFileIfChangedSync } from "./atomic-file.mjs";
|
|
9
|
+
import { codexPaths } from "./codex-paths.mjs";
|
|
10
|
+
import { withFileLockSync } from "./lock.mjs";
|
|
9
11
|
import { status } from "./status.mjs";
|
|
10
12
|
|
|
11
13
|
const FEATURE = "local_thread_store_compression";
|
|
@@ -70,7 +72,8 @@ export async function codexNeedsThreadCompressionDefault({
|
|
|
70
72
|
}
|
|
71
73
|
|
|
72
74
|
export async function ensureCodexThreadCompression({
|
|
73
|
-
|
|
75
|
+
env = process.env, home = os.homedir(),
|
|
76
|
+
filePath = codexPaths({ env, home }).config,
|
|
74
77
|
probe = codexNeedsThreadCompressionDefault,
|
|
75
78
|
} = {}) {
|
|
76
79
|
try {
|
|
@@ -79,11 +82,13 @@ export async function ensureCodexThreadCompression({
|
|
|
79
82
|
if (!await probe()) return;
|
|
80
83
|
// The client/user may edit the shared file while the asynchronous probe runs.
|
|
81
84
|
// Recompute against its current bytes; explicit opt-outs always win.
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
withFileLockSync(`${filePath}.ccdx.lock`, () => {
|
|
86
|
+
const updated = computeThreadCompressionDefault(read());
|
|
87
|
+
if (updated.changed) {
|
|
88
|
+
atomicWriteFileIfChangedSync(filePath, updated.content);
|
|
89
|
+
console.log(status("ok", "Enabled Codex local history compression (experimental)"));
|
|
90
|
+
}
|
|
91
|
+
});
|
|
87
92
|
} catch {
|
|
88
93
|
console.warn(status("warn", "Optional Codex history compression setup was skipped; core proxy remains available"));
|
|
89
94
|
}
|
package/src/config.mjs
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import os from "node:os";
|
|
4
2
|
import { status } from "./status.mjs";
|
|
5
3
|
import { atomicWriteFileIfChangedSync } from "./atomic-file.mjs";
|
|
6
4
|
import { adapterBaseUrl } from "./running-adapter.mjs";
|
|
7
5
|
import { isLoopbackHost } from "./security.mjs";
|
|
8
6
|
import { codexTomlStatements, configValue, parseCodexToml, validateManagedConfigEdit } from "./config-toml.mjs";
|
|
7
|
+
import { codexPaths } from "./codex-paths.mjs";
|
|
8
|
+
import { withFileLockSync } from "./lock.mjs";
|
|
9
9
|
|
|
10
|
-
const CONFIG_PATH = path.join(os.homedir(), ".codex", "config.toml");
|
|
11
10
|
const MODEL_CONTEXT_WINDOW = 1_000_000;
|
|
12
11
|
const MODEL_AUTO_COMPACT_TOKEN_LIMIT = 900_000;
|
|
13
12
|
const IMAGE_MCP_START = "# ccdx:image-mcp:start";
|
|
@@ -278,8 +277,13 @@ export function computeImageMcpCodexConfig(content, options = {}) {
|
|
|
278
277
|
return computeImageConfig(content, options);
|
|
279
278
|
}
|
|
280
279
|
|
|
281
|
-
export function ensureCodexConfig(adapterPort = 2026, {
|
|
282
|
-
filePath =
|
|
280
|
+
export function ensureCodexConfig(adapterPort = 2026, options = {}) {
|
|
281
|
+
const filePath = options.filePath || codexPaths(options).config;
|
|
282
|
+
return withFileLockSync(`${filePath}.ccdx.lock`, () => ensureCodexConfigUnlocked(adapterPort, { ...options, filePath }));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function ensureCodexConfigUnlocked(adapterPort = 2026, {
|
|
286
|
+
filePath,
|
|
283
287
|
host = "127.0.0.1",
|
|
284
288
|
imageProviderEnabled = false,
|
|
285
289
|
} = {}) {
|
|
@@ -303,7 +307,7 @@ export function ensureCodexConfig(adapterPort = 2026, {
|
|
|
303
307
|
console.log(status("ok", `Codex already points to ${baseUrl}`));
|
|
304
308
|
} else {
|
|
305
309
|
atomicWriteFileIfChangedSync(filePath, core.content);
|
|
306
|
-
console.log(status("ok", content === undefined ?
|
|
310
|
+
console.log(status("ok", content === undefined ? `Created Codex configuration: ${filePath}` : `Configured Codex base URL: ${baseUrl}`));
|
|
307
311
|
}
|
|
308
312
|
// Disabled means no image maintenance, including no implicit removal of old
|
|
309
313
|
// or user-owned entries. Explicit disable-image owns the cleanup operation.
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import { errorEvidenceText } from "./operation-error.mjs";
|
|
2
|
+
|
|
1
3
|
function cloneJson(value) {
|
|
2
4
|
return value === undefined ? undefined : structuredClone(value);
|
|
3
5
|
}
|
|
4
6
|
|
|
5
7
|
export function isEncryptedContentVerificationError(statusCode, text) {
|
|
6
8
|
if (statusCode < 400 || !text) return false;
|
|
7
|
-
const lower =
|
|
9
|
+
const lower = errorEvidenceText(text).toLowerCase();
|
|
8
10
|
const reasoningFailure = lower.includes("encrypted content")
|
|
9
11
|
&& lower.includes("could not be verified")
|
|
10
12
|
&& (lower.includes("could not be decrypted") || lower.includes("could not be parsed"));
|
|
@@ -17,7 +19,7 @@ export function isEncryptedContentVerificationError(statusCode, text) {
|
|
|
17
19
|
|
|
18
20
|
export function isImageNamespaceCollisionError(statusCode, text) {
|
|
19
21
|
if (statusCode < 400 || !text) return false;
|
|
20
|
-
const lower =
|
|
22
|
+
const lower = errorEvidenceText(text).toLowerCase();
|
|
21
23
|
return lower.includes("namespace")
|
|
22
24
|
&& lower.includes("image_gen")
|
|
23
25
|
&& lower.includes("collid");
|
package/src/dashboard/index.html
CHANGED
|
@@ -57,6 +57,13 @@
|
|
|
57
57
|
<p class="hint" data-i18n>Recent upstream request metadata, not exact live session occupancy. Missing values are unknown. No prompt or image content is retained.</p>
|
|
58
58
|
</details>
|
|
59
59
|
|
|
60
|
+
<section class="panel dashboard-section" aria-label="CLIENT FEATURES" data-i18n-label>
|
|
61
|
+
<div class="panel-heading"><h2 data-i18n>CLIENT FEATURES</h2><span data-i18n>OBSERVED STATE</span></div>
|
|
62
|
+
<dl class="stats"><div><dt data-i18n>Native predictions</dt><dd id="prediction-preference">—</dd></div><div><dt data-i18n>Local bridge</dt><dd id="prediction-bridge">—</dd></div><div><dt data-i18n>Client route</dt><dd id="prediction-route">—</dd></div><div><dt data-i18n>Visible candidate</dt><dd id="prediction-candidate">—</dd></div></dl>
|
|
63
|
+
<p id="prediction-note" class="hint">—</p>
|
|
64
|
+
<p class="hint" data-i18n>Configured does not mean displayed. Use ccdx prediction status for details.</p>
|
|
65
|
+
</section>
|
|
66
|
+
|
|
60
67
|
<section class="panel dashboard-section" aria-label="Live upstream models" data-i18n-label>
|
|
61
68
|
<div class="panel-heading"><h2 data-i18n>UPSTREAM GPT MODELS</h2><div class="heading-actions"><span id="models-state" data-i18n>CHECKING LIVE</span><button id="refresh-models" type="button" data-i18n>Refresh</button></div></div>
|
|
62
69
|
<p id="models-note" class="table-note" data-i18n>Querying GitHub Copilot…</p>
|