codex-copilot-dx 0.9.22 → 0.9.24

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
@@ -5,6 +5,7 @@ Use Codex App with GitHub Copilot through a local adapter.
5
5
  CCDX targets Codex App. It discovers Copilot models, adapts the client's model
6
6
  catalog, and preserves Responses streaming, tool calls and conversation history.
7
7
  Default Auto-review: **GPT-6.1 Sol / low**. Image generation is **off by default**.
8
+ Native predictions are also **off by default**.
8
9
 
9
10
  ## Install and run
10
11
 
@@ -38,14 +39,13 @@ The dashboard does not poll in the background; refresh when needed.
38
39
  | `ccdx models` | Fresh upstream model list |
39
40
  | `ccdx usage` | Recorded tokens and upstream prompt-cache hit rate |
40
41
  | `ccdx auto-review-model` | Select model, then reasoning effort; `0` follows client |
41
- | `ccdx suggestion on` / `ccdx suggestion off` | Enable or disable the optional next-message panel |
42
- | `ccdx suggestion model` / `ccdx suggestion status` | Choose its model by number, or check settings |
43
42
  | `ccdx animation` | Preview and select terminal animation |
44
43
  | `ccdx cache --limit 128` | Set the history-cache budget in MiB |
45
44
  | `ccdx cache --clean` | Clear the rebuildable image-transform cache |
46
45
  | `ccdx limits --decoded 256` | Set the decoded request-body budget; restart to apply |
47
46
  | `ccdx enable-image` | Opt in to image generation/editing with your own API |
48
47
  | `ccdx image-status` / `ccdx disable-image` | Check or disable optional images |
48
+ | `ccdx prediction on` / `off` / `status` | Opt in to native next-message predictions (macOS) |
49
49
  | `ccdx update` | Update the global package |
50
50
 
51
51
  ## Compatibility and safety
@@ -64,18 +64,17 @@ The dashboard does not poll in the background; refresh when needed.
64
64
  break continuations. Generated images and Codex transcripts are not deleted.
65
65
  - Optional image setup/maintenance never blocks the primary service. Disabled
66
66
  users get no CCDX image skill or MCP installation.
67
- - Next-message suggestions are **off by default**. Opt in with `ccdx suggestion on`,
68
- then open **Next message / 下一句** in a local conversation panel or ask Codex for
69
- a next-message suggestion. **Add to draft / 加入草稿** appends ordinary text; it
70
- never sends, replaces your draft, or patches the client. Default model:
71
- `gpt-6-luna`, with `low` reasoning when advertised. Requires a running local
72
- adapter and a recent Codex with paginated thread reads and MCP draft support.
73
- - Suggestions read only a bounded page from the host-identified current thread.
74
- Images, reasoning items and tool outputs are excluded. There is no background
75
- prediction or startup hook; selecting a model alone does not enable the feature.
76
- Existing user-managed MCP/profile configuration is preserved. Native prediction
77
- settings are untouched; account/remote defaults in all future clients cannot be
78
- reliably detected. Use `ccdx suggestion off` when preferring an official feature.
67
+ - The manual prediction MCP, background hook and dashboard panel are retired.
68
+ Startup removes only unchanged CCDX registrations and disables saved prediction
69
+ settings. The optional image MCP remains unchanged. Modified old registrations
70
+ can use inert compatibility entrypoints; they no longer generate suggestions.
71
+ - Native predictions are a separate, opt-in compatibility feature. Quit Codex,
72
+ run `ccdx prediction on`, then restart CCDX. This adds App-profile certificate
73
+ trust and an App-only route for `chatgpt.com` and `ab.chatgpt.com`; HTTPS on those
74
+ hosts is decrypted locally, but only prediction configuration is changed.
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.
79
78
  - CCDX adds experimental local history compression only when installed App
80
79
  runtimes support it and report it off. Existing choices are preserved; an
81
80
  already-enabled default, unavailable runtime or failed check is skipped.
package/bin/cli.mjs CHANGED
@@ -77,8 +77,21 @@ if (CLI.command === "image") {
77
77
  }
78
78
  if (CLI.command === "suggestion") {
79
79
  try {
80
- const { runSuggestionCommand } = await import("../src/cli-suggestion.mjs");
81
- await runSuggestionCommand({ action: CLI.action, model: CLI.model, commandName: CLI_NAME });
80
+ const { retirePredictionIntegration } = await import("../src/prediction-retirement.mjs");
81
+ const result = retirePredictionIntegration({ dryRun: CLI.action === "status" });
82
+ console.log("The CCDX prediction panel has been retired and cannot be enabled.");
83
+ if (CLI.action === "status" && result.changed) console.log(`Legacy settings remain; run ${CLI_NAME} suggestion off to clean them up.`);
84
+ if (result.preserved) console.warn("Modified prediction settings were preserved.");
85
+ process.exit(0);
86
+ } catch (error) {
87
+ console.error(status("err", error.message));
88
+ process.exit(1);
89
+ }
90
+ }
91
+ if (CLI.command === "prediction") {
92
+ try {
93
+ const { runPredictionCommand } = await import("../src/native-prediction.mjs");
94
+ await runPredictionCommand({ action: CLI.action });
82
95
  process.exit(0);
83
96
  } catch (error) {
84
97
  console.error(status("err", error.message));
@@ -251,6 +264,7 @@ const LOGGING = configureLogging();
251
264
  let activeServer = null;
252
265
  let modelRefreshTimer = null;
253
266
  let shuttingDown = false;
267
+ let nativePrediction = null;
254
268
 
255
269
  if (LOGGING.filePath) {
256
270
  console.log(status("info", `Debug log: ${LOGGING.filePath}`));
@@ -272,7 +286,18 @@ async function reuseRunningAdapterIfAvailable() {
272
286
  console.log(status("ok", `Using existing adapter at ${running.baseUrl}`));
273
287
  const { compressionSetup } = await configureCodexClient();
274
288
 
275
- await openCodex();
289
+ let predictionPacUrl = null;
290
+ let predictionApp = null;
291
+ try {
292
+ const { readPredictionSettings } = await import("../src/native-prediction-profile.mjs");
293
+ const preference = readPredictionSettings();
294
+ if (preference.enabled) {
295
+ predictionPacUrl = running.data?.native_prediction_pac || null;
296
+ predictionApp = preference.app?.app;
297
+ if (!predictionPacUrl) console.warn(status("warn", "Restart ccdx to activate the newly enabled prediction bridge"));
298
+ }
299
+ } catch (error) { console.warn(status("warn", error.message)); }
300
+ await openCodex({ predictionPacUrl, predictionApp });
276
301
  console.log(`
277
302
  ${status("ok", "Ready, using the existing ccdx adapter")}
278
303
 
@@ -285,6 +310,14 @@ async function reuseRunningAdapterIfAvailable() {
285
310
  }
286
311
 
287
312
  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
+ }
288
321
  const imageProviderEnabled = Boolean(readImageProviderConfig());
289
322
  const { imageMcpSkipped } = ensureCodexConfig(ADAPTER_PORT, { host: ADAPTER_HOST, imageProviderEnabled });
290
323
  if (imageProviderEnabled && !imageMcpSkipped) {
@@ -369,13 +402,20 @@ try {
369
402
  streamHandshakeTimeoutMs: RUNTIME.streamHandshakeTimeoutMs,
370
403
  streamIdleTimeoutMs: RUNTIME.streamIdleTimeoutMs,
371
404
  terminalAnimationTheme,
405
+ nativePredictionPac: () => nativePrediction?.pacUrl || null,
372
406
  });
373
407
 
374
408
  // Point Codex App at the adapter.
375
409
  const { compressionSetup } = await configureCodexClient();
376
410
 
377
- // Launch Codex when available.
378
- await openCodex();
411
+ // 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.`)); }
416
+
417
+ // Launch Codex when available. A running App is never forcibly restarted.
418
+ await openCodex({ predictionPacUrl: nativePrediction?.pacUrl, predictionApp: nativePrediction?.app });
379
419
 
380
420
  // Periodically refresh the model list + endpoint cache so a long-running
381
421
  // adapter picks up newly released models without a restart.
@@ -404,6 +444,7 @@ try {
404
444
  let exitCode = 0;
405
445
  try {
406
446
  if (modelRefreshTimer) clearInterval(modelRefreshTimer);
447
+ await nativePrediction?.close();
407
448
  const result = await closeHttpServer(activeServer, { timeoutMs: RUNTIME.shutdownTimeoutMs });
408
449
  if (result.forced) console.warn(status("warn", "Forced remaining adapter connections closed"));
409
450
  await flushUsageWrites();
@@ -420,6 +461,7 @@ try {
420
461
  await compressionSetup;
421
462
  } catch (e) {
422
463
  console.error(status("err", e.message));
464
+ await nativePrediction?.close().catch(() => {});
423
465
  await closeHttpServer(activeServer, { timeoutMs: RUNTIME.shutdownTimeoutMs }).catch(() => {});
424
466
  await flushUsageWrites();
425
467
  await LOGGING.cleanup();
package/docs/REFERENCE.md CHANGED
@@ -6,52 +6,86 @@ CCDX is a local compatibility adapter for Codex App. It configures Codex to use
6
6
 
7
7
  ## How it works
8
8
 
9
- ### Optional next-message suggestions
10
-
11
- Suggestions are disabled by default. No suggestion MCP or skill is installed at
12
- startup, and the primary adapter does not import or start a predictor.
9
+ ### Retired prediction integration
10
+
11
+ The manual prediction MCP, automatic Stop hook and dashboard panel are removed.
12
+ CCDX does not install a prediction skill or replacement predictor. Startup disables
13
+ the old `suggestion.json` preference and removes only unchanged CCDX declarations
14
+ from the active `CODEX_HOME/config.toml` (or `~/.codex/config.toml`).
15
+ `CCDX_SUGGESTION_CONFIG` still identifies the legacy settings path for cleanup.
16
+ For an immediate cleanup without starting the adapter, run `ccdx suggestion off`.
17
+ The old `suggestion on` and `suggestion model` commands cannot re-enable it.
18
+
19
+ Modified, inline and profile-specific declarations are preserved. Cleanup failures
20
+ do not block the core adapter. Saved model preferences and old candidate files are
21
+ not deleted; they are inactive. Native Codex preference settings, hook trust records,
22
+ other hooks and the optional image MCP are untouched. Two inert compatibility
23
+ entrypoints prevent user-modified registrations from invoking a removed module:
24
+ the old Hook exits with an empty result, and the old MCP exposes no tools or panel.
25
+
26
+ ### Optional native predictions
27
+
28
+ Native next-message predictions are **off by default** and use no MCP, Hook, skill,
29
+ panel, renderer injection, or client-program modification. This macOS-only
30
+ compatibility feature uses the App's existing composer UI and the conversation's
31
+ selected model through CCDX. Suggestions are not sent automatically. Native
32
+ eligibility rules still apply; availability is not promised for every thread type.
13
33
 
14
34
  ```bash
15
- ccdx suggestion on
16
- ccdx suggestion off
17
- ccdx suggestion model
18
- ccdx suggestion model gpt-6.1-sol
19
- ccdx suggestion status
35
+ # Fully quit Codex App first.
36
+ ccdx prediction on
37
+ # Restart the running CCDX process, then launch Codex through ccdx.
38
+ ccdx
39
+ ccdx prediction status
40
+
41
+ # To remove the optional setup, fully quit Codex first:
42
+ ccdx prediction off
20
43
  ```
21
44
 
22
- Start CCDX, enable suggestions, then open **Next message / 下一句** from a local
23
- conversation panel, or ask Codex for a next-message suggestion. **Add to draft /
24
- 加入草稿** appends ordinary text and never sends. The user still presses Send.
25
- If the MCP has not appeared in an open App, restart the App once. There is no
26
- client patch, grey ghost text, automatic background prediction, or auto-approval.
27
- Model selection alone keeps the feature disabled. The initial model is
28
- `gpt-6-luna`; `low` reasoning is used when advertised. No service tier is forced.
29
-
30
- Settings are stored in `suggestion.json` beside CCDX's `config.json`.
31
- `CCDX_SUGGESTION_CONFIG` overrides this file path; the managed MCP sets it to
32
- the exact saved file, so normal users do not need to set it. The managed
33
- MCP is `mcp_servers.ccdx_suggestion` in the active `CODEX_HOME/config.toml` (or
34
- `~/.codex/config.toml`). Existing user-managed, inline or profile-specific entries
35
- are preserved rather than overwritten; identical enabled setup is a no-op.
36
- Native prediction settings are never changed. Official account/remote defaults
37
- currently have no reliable generic local signal, so CCDX does not promise to
38
- detect every future client default. Use `suggestion off` when preferring native
39
- predictions. An unavailable capability reports a local error, not a fallback
40
- that edits the client or sends a chat message.
41
-
42
- The MCP uses the host-supplied thread ID and the installed Codex's paginated
43
- `thread/items/list` API. It reads at most 24 recent items, retaining up to eight
44
- user/assistant text excerpts and 16,000 characters. Images, reasoning items and
45
- tool outputs are excluded; there is no transcript scan, fork or resumed turn.
46
- Recent text is sent through the local CCDX adapter to the selected Copilot model
47
- only when the user requests a suggestion. Provider data policies still apply.
48
- Context lookup is capped at 5 seconds/2 MiB, the operation at 30 seconds, output
49
- at 64 KiB/300 characters, concurrent work at two, and in-memory candidates at 16.
50
- Candidates expire for adoption after 10 minutes and are bound to their thread
51
- and selected model. Failure/HTTP 200 incomplete responses never become drafts.
52
- No additional retry is added by the suggestion feature. Existing adapter policies
53
- remain unchanged. App support requires MCP draft insertion; the executable can
54
- be selected explicitly with `CCDX_CODEX_BINARY` when automatic discovery fails.
45
+ Enabling explicitly adds a private certificate trust record to the selected App
46
+ profile and a process-scoped PAC route for `chatgpt.com` and `ab.chatgpt.com` only.
47
+ HTTPS traffic for **all paths on those two hosts** is decrypted and forwarded
48
+ locally; PAC cannot select HTTPS paths before TLS. Only the bootstrap prediction
49
+ flag, prediction configuration, and corresponding Statsig refresh values are
50
+ overlaid. Other paths preserve upstream status, cookies, streaming and WebSocket
51
+ traffic. Upstream TLS verification stays enabled; no OS trust, global proxy, login,
52
+ account identity or account-permission value is rewritten. Credentials are not
53
+ logged or persisted by the bridge. The local private key has owner-only permissions;
54
+ its signing key is discarded after certificate creation.
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
58
+ is `~/Library/Application Support/Codex`; an explicit
59
+ `CODEX_ELECTRON_USER_DATA_PATH` is respected. Quit the App before trust changes;
60
+ CCDX never kills it. The launcher uses the exact App that passed the compatibility
61
+ check. Updating the App invalidates that check: quit it and run `prediction on`
62
+ again. Unknown client/store/configuration formats are not forcibly rewritten.
63
+ These are checked compatibility boundaries, not a promise for every future App.
64
+
65
+ The bridge changes only its own prediction fields and preserves already-enabled
66
+ official prediction configuration. A missing prediction-config route can receive
67
+ CCDX's own short JSON-suggestion prompt using low reasoning effort. Authentication,
68
+ permission, rate-limit and server errors are returned unchanged. Full and live
69
+ refresh schemas are supported; delta requests ask for complete values to avoid
70
+ merging upstream checksums with locally changed fields. Configuration bodies are
71
+ bounded at 16 MiB; larger or unknown bodies pass through without an overlay.
72
+ No extra inference or bridge exists for users who leave the feature disabled.
73
+ When enabled, prediction requests consume Copilot usage; the setting does not
74
+ provide a separate prediction-model picker.
75
+
76
+ `prediction off` immediately disables new local overlays. Quit/reopen Codex to
77
+ clear its cached predictions and temporary route. If the App is still running,
78
+ trust removal is deferred; quit it and run `prediction off` again. Only the exact
79
+ owned certificate record is removed, never the whole store. Failed activation
80
+ rolls back newly added trust; certificate replacement keeps old trust until the
81
+ 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`
83
+ after quitting Codex to attach a newly started bridge. `CCDX_AUTO_LAUNCH=0` keeps
84
+ automatic launch disabled and does not attach the route to a manually opened App.
85
+ 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
88
+ `~/.config/codex-copilot-dx/native-prediction/` (or under `XDG_CONFIG_HOME`).
55
89
 
56
90
  ### Core adapter
57
91
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-copilot-dx",
3
- "version": "0.9.22",
3
+ "version": "0.9.24",
4
4
  "description": "Use Codex App with GitHub Copilot",
5
5
  "author": "Dale Xiao",
6
6
  "type": "module",
package/src/adapter.mjs CHANGED
@@ -242,7 +242,9 @@ export function createAdapterHandler(options = {}) {
242
242
 
243
243
  if (req.method === "GET" && pathname === ADAPTER_HEALTH_PATH) {
244
244
  res.writeHead(200, { "Content-Type": "application/json" });
245
- res.end(JSON.stringify(adapterHealthPayload()));
245
+ const predictionPac = isLoopbackAddress(req.socket?.remoteAddress) && isLoopbackHostHeader(req.headers?.host)
246
+ ? options.nativePredictionPac?.() : null;
247
+ res.end(JSON.stringify({ ...adapterHealthPayload(), ...(predictionPac ? { native_prediction_pac: predictionPac } : {}) }));
246
248
  return;
247
249
  }
248
250
 
@@ -28,6 +28,7 @@ const HELP_TOPICS = new Set([
28
28
  "image-status",
29
29
  "auto-review-model",
30
30
  "suggestion",
31
+ "prediction",
31
32
  "update",
32
33
  "version",
33
34
  ]);
@@ -252,11 +253,18 @@ export function parseCliArgs(args = []) {
252
253
  if (rest.length) unexpectedArgs(rest);
253
254
  return baseCommand("animation");
254
255
  }
256
+ if (command === "prediction") {
257
+ const [action = "status", ...extra] = rest;
258
+ if (!["on", "off", "status"].includes(action)) throw new Error("Prediction action must be on, off, or status");
259
+ if (extra.length) unexpectedArgs(extra);
260
+ return baseCommand("prediction", { action });
261
+ }
255
262
  if (command === "suggestion") {
256
263
  const [action = "status", model, ...extra] = rest;
257
264
  if (!["on", "off", "model", "status"].includes(action)) throw new Error("Suggestion action must be on, off, model, or status");
258
265
  if (extra.length || (model !== undefined && action !== "model")) unexpectedArgs(rest.slice(1));
259
- return baseCommand("suggestion", { action, ...(model !== undefined ? { model } : {}) });
266
+ if (["on", "model"].includes(action)) throw new Error("The CCDX prediction panel has been retired; use ccdx prediction for the separate optional native feature");
267
+ return baseCommand("suggestion", { action });
260
268
  }
261
269
  if (command === "enable-image" || command === "disable-image" || command === "image-status") {
262
270
  if (rest.length) unexpectedArgs(rest);
@@ -363,7 +371,8 @@ function topicHelp(name, topic) {
363
371
  "disable-image": `Usage:\n ${name} disable-image\n\nDisables the local CCDX image tool and removes its stored API credential and unmodified CCDX image guidance.`,
364
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.`,
365
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.`,
366
- "suggestion": `Usage:\n ${name} suggestion on\n ${name} suggestion off\n ${name} suggestion model [MODEL_ID]\n ${name} suggestion status\n\nOptional next-message panel; disabled by default. Open it in a local Codex conversation to generate a suggestion, then choose Add to draft. Ordinary text only: no client patch, background prediction, automatic insertion or sending. Model selection alone does not enable the feature. Requires a running local adapter and recent Codex. Existing user-owned MCP configuration is preserved; native prediction settings are never modified. Future account/remote defaults cannot be universally detected.`,
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.`,
367
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.`,
368
377
  version: `Usage:\n ${name} --version`,
369
378
  };
@@ -389,7 +398,7 @@ export function cliHelp(commandName = "ccdx", topic = "") {
389
398
  ${name} disable-image
390
399
  ${name} image-status
391
400
  ${name} auto-review-model
392
- ${name} suggestion on|off|model|status
401
+ ${name} prediction [status|on|off]
393
402
  ${name} update [npm|github]
394
403
  ${name} --version
395
404
  ${name} --help
@@ -408,7 +417,7 @@ Commands:
408
417
  disable-image Disable the image provider and remove its credential
409
418
  image-status Show the optional image provider status
410
419
  auto-review-model Select the Codex Auto-review model and reasoning effort
411
- suggestion Optional next-message suggestions (off by default)
420
+ prediction Toggle optional native next-message predictions
412
421
  update Update the global package from npm or GitHub
413
422
 
414
423
  Run ${name} <command> --help for command details.`;
package/src/launcher.mjs CHANGED
@@ -36,7 +36,7 @@ export function launchAttempts() {
36
36
  return [["-b", CODEX_BUNDLE_ID], ...APP_PATH_CANDIDATES.map((path) => [path])];
37
37
  }
38
38
 
39
- export async function openCodex({ env = process.env, spawnImpl = spawn, platform = process.platform } = {}) {
39
+ export async function openCodex({ env = process.env, spawnImpl = spawn, platform = process.platform, predictionPacUrl = null, predictionApp = null } = {}) {
40
40
  if (platform !== "darwin") {
41
41
  console.log(status("warn", "Auto-launch is only supported on macOS"));
42
42
  console.log(status("info", "Open Codex App manually; it will connect to the adapter."));
@@ -48,7 +48,12 @@ export async function openCodex({ env = process.env, spawnImpl = spawn, platform
48
48
  return false;
49
49
  }
50
50
 
51
- for (const args of launchAttempts()) {
51
+ const extraArgs = typeof predictionPacUrl === "string" && /^http:\/\/127\.0\.0\.1:\d{1,5}\/proxy\.pac$/.test(predictionPacUrl)
52
+ && APP_PATH_CANDIDATES.includes(predictionApp)
53
+ ? ["--args", `--proxy-pac-url=${predictionPacUrl}`] : [];
54
+ const attempts = extraArgs.length ? [[predictionApp]] : launchAttempts();
55
+ for (const attempt of attempts) {
56
+ const args = [...attempt, ...extraArgs];
52
57
  if (await tryOpen(args, { spawnImpl })) {
53
58
  console.log(status("ok", "Codex app launched"));
54
59
  return true;
@@ -0,0 +1,92 @@
1
+ export const NATIVE_PREDICTION_GATE = "4290031608";
2
+
3
+ const LOCAL_PREDICTION_CONFIG = Object.freeze({
4
+ is_enabled: true,
5
+ prompt: 'Predict one useful next message the user may send, based on the conversation. Write from the user\'s perspective and use the user\'s language. Do not answer the message, call tools, or invent new requests, consent, or permission. Prefer a short, natural follow-up. If no useful follow-up is apparent, use an empty string. Return only a JSON object with exactly one key: {"suggestion":"the predicted user message"}.',
6
+ prompt_version: "ccdx-native-prediction-v1",
7
+ prediction_reasoning_effort: "low",
8
+ });
9
+
10
+ function isObject(value) {
11
+ return value !== null && typeof value === "object" && !Array.isArray(value);
12
+ }
13
+
14
+ function parseBody(body) {
15
+ if (typeof body !== "string" && !Buffer.isBuffer(body)) return null;
16
+ try { return JSON.parse(body.toString()); } catch { return null; }
17
+ }
18
+
19
+ function encodeBody(value, original) {
20
+ const text = JSON.stringify(value);
21
+ return Buffer.isBuffer(original) ? Buffer.from(text) : text;
22
+ }
23
+
24
+ export function classifyNativePredictionPath({ hostname, pathname } = {}) {
25
+ const host = typeof hostname === "string" ? hostname.toLowerCase() : "";
26
+ if (host === "chatgpt.com") {
27
+ if (pathname === "/backend-api/wham/statsig/bootstrap") return "bootstrap";
28
+ if (pathname === "/backend-api/wham/predictions/config") return "config";
29
+ }
30
+ if (host === "ab.chatgpt.com" && pathname === "/v1/initialize") return "refresh";
31
+ return null;
32
+ }
33
+
34
+ function enablePredictionGate(payload) {
35
+ if (!isObject(payload) || (payload.response_mode === "live_overlay" ? payload.has_updates === false : payload.has_updates !== true) ||
36
+ (payload.is_delta !== undefined && payload.is_delta !== false) ||
37
+ (payload.response_mode !== undefined && !["full", "live_overlay"].includes(payload.response_mode)) ||
38
+ !isObject(payload.feature_gates)) return false;
39
+ // Statsig V1 first resolves the literal name, then its djb2 lookup key.
40
+ let hash = 0;
41
+ for (const char of NATIVE_PREDICTION_GATE) hash = ((hash << 5) - hash + char.charCodeAt(0)) | 0;
42
+ const hashedKey = String(hash >>> 0);
43
+ const key = Object.hasOwn(payload.feature_gates, NATIVE_PREDICTION_GATE) ? NATIVE_PREDICTION_GATE
44
+ : Object.hasOwn(payload.feature_gates, hashedKey) ? hashedKey : NATIVE_PREDICTION_GATE;
45
+ const gate = payload.feature_gates[key];
46
+ if (gate !== undefined && (!isObject(gate) || typeof gate.value !== "boolean" || gate.value === true)) return false;
47
+ if (payload.response_mode === "live_overlay" && !Array.isArray(payload.live_entity_names?.feature_gates)) return false;
48
+ if (gate) gate.value = true;
49
+ else payload.feature_gates[key] = { name: key, value: true, rule_id: "ccdx-local-compatibility", secondary_exposures: [] };
50
+ if (payload.response_mode === "live_overlay" && !payload.live_entity_names.feature_gates.includes(key)) {
51
+ payload.live_entity_names.feature_gates.push(key);
52
+ }
53
+ return true;
54
+ }
55
+
56
+ // No-op results retain the original bytes; callers own HTTP headers and decoding.
57
+ export function applyNativePredictionOverlay({ hostname, pathname, statusCode, body, enabled = false } = {}) {
58
+ const kind = classifyNativePredictionPath({ hostname, pathname });
59
+ const result = { body, statusCode, changed: false, official: false, kind };
60
+ if (!enabled || !kind) return result;
61
+ if (kind === "config" && statusCode === 404) {
62
+ return { ...result, statusCode: 200, body: encodeBody(LOCAL_PREDICTION_CONFIG, body), changed: true };
63
+ }
64
+ if (statusCode !== 200) return result;
65
+ const parsed = parseBody(body);
66
+ if (!isObject(parsed)) return result;
67
+ if (kind === "config") {
68
+ if (parsed.is_enabled === true) return { ...result, official: true };
69
+ if (parsed.is_enabled !== false || typeof parsed.prompt !== "string" || !parsed.prompt.trim()) return result;
70
+ parsed.is_enabled = true;
71
+ } else if (kind === "bootstrap") {
72
+ const stringPayload = typeof parsed.statsigPayload === "string";
73
+ const payload = stringPayload ? parseBody(parsed.statsigPayload) : parsed.statsigPayload;
74
+ if (!enablePredictionGate(payload)) return result;
75
+ if (stringPayload) parsed.statsigPayload = JSON.stringify(payload);
76
+ } else if (!enablePredictionGate(parsed)) return result;
77
+ return { ...result, body: encodeBody(parsed, body), changed: true };
78
+ }
79
+
80
+ // Overlaying a delta would invalidate its checksum. Ask for a full response instead.
81
+ export function normalizeStatsigRequest({ hostname, pathname, body, enabled = false } = {}) {
82
+ const result = { body, changed: false };
83
+ if (!enabled || classifyNativePredictionPath({ hostname, pathname }) !== "refresh") return result;
84
+ const parsed = parseBody(body);
85
+ if (!isObject(parsed) || !isObject(parsed.user) ||
86
+ (parsed.responseMode !== undefined && !["full", "live_overlay"].includes(parsed.responseMode))) return result;
87
+ if (parsed.deltasResponseRequested === false && parsed.full_checksum === null && parsed.sinceTime === 0) return result;
88
+ parsed.deltasResponseRequested = false;
89
+ parsed.full_checksum = null;
90
+ parsed.sinceTime = 0;
91
+ return { body: encodeBody(parsed, body), changed: true };
92
+ }