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 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 without launching the App |
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. Existing proxies are not replaced.
77
- See the reference for restart, rollback and client-version boundaries.
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 Releases, not npm, and suggests `ccdx update github` when a newer stable version is available. GitHub updates install the exact release tag; dependency downloads still use npm's configured registry.
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-write boundaries, advanced limits,
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
- try {
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
- try {
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 nativePrediction?.close();
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 nativePrediction?.close().catch(() => {});
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
- Existing App proxy/PAC/environment routes cause setup to skip with an explanation.
57
- This initial version does not compose enterprise proxies. The default App profile
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 direct routing; already-interrupted requests are not replayed. Run `ccdx`
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, not proof of live UI
87
- availability. Prediction settings are stored separately in
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`, including the local `/v1` endpoint.
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. The path is based on the OS home directory; CCDX does not redirect it through `CODEX_HOME` or edit project-level Codex files.
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 latest stable GitHub Release once in the background, without accessing the npm registry. The anonymous check requires no PAT or Copilot credentials, has a two-second deadline and a bounded response body, and silently skips network errors, rate limits and invalid metadata. It never falls back to npm, retries, polls, installs an update or restarts the service. When a newer release is found, it prints the versions and `ccdx update github`. On adapter reuse, Ready is displayed before waiting for any remaining check budget so the short-lived command does not lose its notification.
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.24",
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
@@ -236,6 +236,7 @@ export function createAdapterHandler(options = {}) {
236
236
  codexClient,
237
237
  codexModelRegistry,
238
238
  codexModelCatalog,
239
+ clientIntegrations: options.clientIntegrationSnapshot?.(),
239
240
  })));
240
241
  return;
241
242
  }
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({ providerConfig, providerPath, codexPath, codexContent, adapterPort, adapterHost }) {
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({ providerPath, codexPath, codexContent, adapterPort, adapterHost }) {
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 = defaultCodexConfigPath(home), ...options } = {}) {
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 = defaultCodexConfigPath(home),
185
+ codexPath = codexPaths({ env, home }).config,
183
186
  } = {}) {
184
187
  const providerPath = imageProviderConfigPath({ env, home });
185
188
  if (action === "status") {
@@ -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. Existing proxies are not replaced. Official prediction configuration is preserved; the conversation's model is used. App updates require another compatibility check.`,
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
  };
@@ -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 path from "node:path";
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 = path.join(home, ".codex", "config.toml");
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
- filePath = path.join(os.homedir(), ".codex", "config.toml"),
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
- const updated = computeThreadCompressionDefault(read());
83
- if (updated.changed) {
84
- atomicWriteFileIfChangedSync(filePath, updated.content);
85
- console.log(status("ok", "Enabled Codex local history compression (experimental)"));
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 = CONFIG_PATH,
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 ? "Created ~/.codex/config.toml" : `Configured Codex base URL: ${baseUrl}`));
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 = String(text).toLowerCase();
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 = String(text).toLowerCase();
22
+ const lower = errorEvidenceText(text).toLowerCase();
21
23
  return lower.includes("namespace")
22
24
  && lower.includes("image_gen")
23
25
  && lower.includes("collid");
@@ -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>