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.
@@ -1,5 +1,18 @@
1
1
  (() => {
2
2
  const translations = {
3
+ "CLIENT FEATURES": ["CLIENT FEATURES", "接入能力"],
4
+ "OBSERVED STATE": ["OBSERVED STATE", "实际状态"],
5
+ "Native predictions": ["Native predictions", "输入建议"],
6
+ "Local bridge": ["Local bridge", "本地接入"],
7
+ "Client route": ["Client route", "配置请求"],
8
+ "Visible candidate": ["Visible candidate", "显示结果"],
9
+ "Configured does not mean displayed. Use ccdx prediction status for details.": ["Enabled is not proof of a visible suggestion. Check ccdx prediction status.", "开启不等于已显示。\n终端命令可查看详情。"],
10
+ "Enabled": ["Enabled", "已开启"],
11
+ "Disabled": ["Disabled", "已关闭"],
12
+ "Running": ["Running", "运行中"],
13
+ "Stopped": ["Stopped", "未运行"],
14
+ "Unavailable": ["Unavailable", "不可用"],
15
+ "Observed": ["Observed", "已收到配置请求"],
3
16
  "First output": ["First output", "首次输出"],
4
17
  "Token time": ["Token time", "输出耗时"],
5
18
  "Gateway timing. Token time is estimated.": ["Observed at the gateway. Token time is an estimate.", "网关观测时间。\n每Token耗时为估算值。"],
@@ -168,6 +168,15 @@ function render(data) {
168
168
 
169
169
  renderFailures(data.response_failures?.recent, data.stream_performance?.recent_requests);
170
170
  renderContext(data.stream_performance);
171
+ if (element("prediction-preference")) {
172
+ const prediction = data.client_integrations?.prediction;
173
+ text("prediction-preference", prediction ? prediction.configured ? "Enabled" : "Disabled" : "Unknown");
174
+ text("prediction-bridge", ({ running: "Running", stopped: "Stopped", unavailable: "Unavailable" })[prediction?.bridge] || "Unknown");
175
+ text("prediction-route", prediction?.client_route === "observed" ? "Observed" : "Unknown");
176
+ text("prediction-candidate", "Unknown");
177
+ const note = element("prediction-note");
178
+ if (note) note.textContent = prediction?.reason || "";
179
+ }
171
180
  text("updated", "Snapshot {time}", { time: new Date().toLocaleTimeString() });
172
181
  }
173
182
 
package/src/lock.mjs CHANGED
@@ -6,6 +6,35 @@ function sleep(ms) {
6
6
  return new Promise((resolve) => setTimeout(resolve, ms));
7
7
  }
8
8
 
9
+ // Synchronous configuration writers share the same cross-process lock format.
10
+ // Readers never take the lock. A live lock is never stolen on timeout.
11
+ export function withFileLockSync(lockPath, fn, { timeoutMs = 1000, staleMs = 60_000 } = {}) {
12
+ fs.mkdirSync(path.dirname(lockPath), { recursive: true, mode: 0o700 });
13
+ const started = Date.now(), owner = randomUUID();
14
+ const sleeper = new Int32Array(new SharedArrayBuffer(4));
15
+ while (true) {
16
+ if (lockPathExists(reclaimGuardPath(lockPath), fs)) {
17
+ if (Date.now() - started >= timeoutMs) throw new Error("Configuration lock recovery is in progress; retry shortly");
18
+ Atomics.wait(sleeper, 0, 0, 10);
19
+ continue;
20
+ }
21
+ try {
22
+ const fd = writeLockRecord(lockPath, { pid: process.pid, owner, created_at: new Date().toISOString() }, fs);
23
+ fs.closeSync(fd);
24
+ if (lockPathExists(reclaimGuardPath(lockPath), fs)) { removeOwnedLock(lockPath, owner, fs); continue; }
25
+ break;
26
+ } catch (error) {
27
+ if (error.code !== "EEXIST") throw error;
28
+ if (maybeRemoveStaleLock(lockPath, staleMs, Date.now(), fs, processIsAlive)) continue;
29
+ let sameProcess = false;
30
+ try { sameProcess = readLockSnapshot(lockPath).pid === process.pid; } catch {}
31
+ if (sameProcess || Date.now() - started >= timeoutMs) throw new Error("Configuration is being updated by another operation; retry shortly");
32
+ Atomics.wait(sleeper, 0, 0, 10);
33
+ }
34
+ }
35
+ try { return fn(); } finally { removeOwnedLock(lockPath, owner, fs); }
36
+ }
37
+
9
38
  function readLockSnapshot(lockPath) {
10
39
  const stat = fs.statSync(lockPath);
11
40
  let record = {};
@@ -5,13 +5,14 @@ import { execFileSync } from "node:child_process";
5
5
  import { createHash, X509Certificate, createPrivateKey, createPublicKey } from "node:crypto";
6
6
  import { atomicWriteFileSync } from "./atomic-file.mjs";
7
7
  import { userSettingsPath } from "./user-settings.mjs";
8
+ import { resolveNetworkRoute } from "./network-route.mjs";
9
+ import { codexPaths } from "./codex-paths.mjs";
8
10
 
9
11
  export function predictionPaths({ env = process.env, home = os.homedir() } = {}) {
10
12
  const directory = path.join(path.dirname(userSettingsPath({ env, home })), "native-prediction");
11
- const profile = env.CODEX_ELECTRON_USER_DATA_PATH?.trim()
12
- || path.join(home, "Library", "Application Support", "Codex");
13
+ const { profile, certificateStore } = codexPaths({ env, home });
13
14
  return { directory, settings: path.join(directory, "settings.json"), profile,
14
- store: path.join(profile, "Default", "ServerCertificate") };
15
+ store: certificateStore };
15
16
  }
16
17
 
17
18
  export function readPredictionSettings(options = {}) {
@@ -31,11 +32,7 @@ export function writePredictionSettings(value, options = {}) {
31
32
  }
32
33
 
33
34
  export function assertPredictionProxyCompatible({ env = process.env, systemProxy = "" } = {}) {
34
- if (["HTTPS_PROXY", "HTTP_PROXY", "ALL_PROXY", "https_proxy", "http_proxy", "all_proxy", "CODEX_API_BASE_URL"]
35
- .some((key) => env[key]?.trim())
36
- || /(?:HTTPEnable|HTTPSEnable|SOCKSEnable|ProxyAutoConfigEnable|ProxyAutoDiscoveryEnable)\s*:\s*1/.test(systemProxy)) {
37
- throw new Error("Native prediction setup skipped: an existing proxy or API route is configured; it was not replaced");
38
- }
35
+ return resolveNetworkRoute({ env, systemProxy });
39
36
  }
40
37
 
41
38
  export function assertCodexClosed({ exec = execFileSync } = {}) {
@@ -0,0 +1,29 @@
1
+ import { redactDiagnosticText } from "./diagnostic-text.mjs";
2
+ import { operationErrorDetails } from "./operation-error.mjs";
3
+
4
+ export function createPredictionState({ now = () => new Date().toISOString() } = {}) {
5
+ let state = { configured: false, bridge: "stopped", client_route: "unknown", candidate: "unknown", reason: null,
6
+ configuration: {}, counters: { configuration_responses: 0, modified: 0, tls_errors: 0, upstream_errors: 0 }, last_event_at: null };
7
+ return {
8
+ configure(enabled) { state.configured = enabled === true; },
9
+ running(proxy) { state.bridge = "running"; state.reason = null; state.proxy = proxy; },
10
+ stopped() { state.bridge = "stopped"; },
11
+ unavailable(error) {
12
+ state.bridge = "unavailable";
13
+ state.reason = redactDiagnosticText(String(error?.message || "Prediction setup failed")).slice(0, 400);
14
+ state.error = operationErrorDetails(error);
15
+ },
16
+ event(event) {
17
+ state.last_event_at = now();
18
+ if (event.type === "configuration") {
19
+ state.client_route = "observed";
20
+ state.counters.configuration_responses++;
21
+ if (event.changed) state.counters.modified++;
22
+ state.configuration[event.route] = { status: event.status, modified: event.changed === true, official: event.official === true };
23
+ } else if (event.type === "tls_error") state.counters.tls_errors++;
24
+ else if (event.type === "upstream_error") state.counters.upstream_errors++;
25
+ if (event.error) state.last_error = { code: event.error.code, source: event.error.source, message: event.error.message, status: event.error.status };
26
+ },
27
+ snapshot() { return structuredClone(state); },
28
+ };
29
+ }
@@ -2,6 +2,8 @@ import http from "node:http";
2
2
  import https from "node:https";
3
3
  import { promisify } from "node:util";
4
4
  import { gunzip, inflate, brotliDecompress } from "node:zlib";
5
+ import { networkPredictionPac } from "./network-route.mjs";
6
+ import { operationErrorDetails } from "./operation-error.mjs";
5
7
 
6
8
  export const PREDICTION_HOSTS = Object.freeze(["chatgpt.com", "ab.chatgpt.com"]);
7
9
  const CONFIG_LIMIT = 16 * 1024 * 1024;
@@ -15,8 +17,9 @@ export function predictionRoute(host, pathname) {
15
17
  return null;
16
18
  }
17
19
 
18
- export function predictionPac(port) {
20
+ export function predictionPac(port, networkRoute) {
19
21
  if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error("Invalid prediction proxy port");
22
+ if (networkRoute) return networkPredictionPac(port, networkRoute);
20
23
  return `function FindProxyForURL(url, host) { if (url.indexOf('https:') === 0 && (host === 'chatgpt.com' || host === 'ab.chatgpt.com')) return 'PROXY 127.0.0.1:${port}; DIRECT'; return 'DIRECT'; }\n`;
21
24
  }
22
25
 
@@ -56,6 +59,7 @@ function listen(server) {
56
59
  export async function startPredictionTransport({
57
60
  key, cert, overlay, normalizeRequest = (value) => value,
58
61
  enabled = () => true, requestImpl = https.request,
62
+ agent, networkRoute, onEvent = () => {},
59
63
  timeoutMs = 30_000, maxConnections = 128,
60
64
  }) {
61
65
  const sockets = new Set();
@@ -76,7 +80,8 @@ export async function startPredictionTransport({
76
80
  // No decoding/re-encoding on pass-through paths.
77
81
  if (route && enabled()) headers["accept-encoding"] = "identity";
78
82
  let upstream;
79
- const fail = () => {
83
+ const fail = (error) => {
84
+ onEvent({ type: "upstream_error", error: operationErrorDetails(error) });
80
85
  if (!res.headersSent) { res.writeHead(502); res.end(); } else res.destroy();
81
86
  };
82
87
  const send = (body, streamRemainder = false) => {
@@ -86,6 +91,7 @@ export async function startPredictionTransport({
86
91
  servername: target.hostname, method: req.method,
87
92
  path: req.url, headers,
88
93
  rejectUnauthorized: true,
94
+ ...(agent ? { agent } : {}),
89
95
  }, (reply) => {
90
96
  const pass = () => {
91
97
  res.writeHead(reply.statusCode, cleanHeaders(reply.headers));
@@ -95,7 +101,10 @@ export async function startPredictionTransport({
95
101
  // Error and non-JSON responses are forwarded as received. The overlay
96
102
  // decides whether the narrowly allowed missing-config response applies.
97
103
  const mediaType = String(reply.headers["content-type"] || "").split(";", 1)[0].trim();
98
- if (!route || !enabled() || mediaType !== "application/json") { pass(); return; }
104
+ if (!route || !enabled() || mediaType !== "application/json") {
105
+ if (route) onEvent({ type: "configuration", route, status: reply.statusCode, changed: false });
106
+ pass(); return;
107
+ }
99
108
  let size = 0;
100
109
  const parts = [];
101
110
  let streaming = false;
@@ -124,10 +133,12 @@ export async function startPredictionTransport({
124
133
  });
125
134
  } catch { /* Preserve unexpected upstream payloads byte-for-byte. */ }
126
135
  if (next?.body !== undefined && (next.body !== original.toString("utf8") || next.status !== reply.statusCode)) {
136
+ onEvent({ type: "configuration", route, status: next.status ?? reply.statusCode, changed: true });
127
137
  const bytes = Buffer.from(next.body);
128
138
  res.writeHead(next.status ?? reply.statusCode, changedHeaders(reply.headers, bytes.length));
129
139
  res.end(bytes);
130
140
  } else {
141
+ onEvent({ type: "configuration", route, status: reply.statusCode, changed: false, official: next?.official });
131
142
  res.writeHead(reply.statusCode, cleanHeaders(reply.headers)); res.end(original);
132
143
  }
133
144
  })().catch(fail));
@@ -179,6 +190,7 @@ export async function startPredictionTransport({
179
190
  const upstream = requestImpl({
180
191
  protocol: "https:", hostname: host, port: 443, servername: host,
181
192
  method: req.method, path: req.url, rejectUnauthorized: true,
193
+ ...(agent ? { agent } : {}),
182
194
  headers: { ...cleanHeaders(req.headers), connection: "Upgrade", upgrade: req.headers.upgrade },
183
195
  });
184
196
  upstreams.add(upstream);
@@ -204,13 +216,13 @@ export async function startPredictionTransport({
204
216
  socket.once("close", () => { upstreams.delete(upstream); upstream.destroy(); });
205
217
  upstream.end();
206
218
  });
207
- gateway.on("tlsClientError", (_error, socket) => socket.destroy());
219
+ gateway.on("tlsClientError", (error, socket) => { onEvent({ type: "tls_error", error: operationErrorDetails(error) }); socket.destroy(); });
208
220
  gateway.on("clientError", (_error, socket) => socket.destroy());
209
221
  const proxy = http.createServer((req, res) => {
210
222
  if (req.method === "GET" && req.url === "/proxy.pac" && !req.headers.origin
211
223
  && ["127.0.0.1", "localhost"].includes(String(req.headers.host || "").split(":")[0])) {
212
224
  res.writeHead(200, { "Content-Type": "application/x-ns-proxy-autoconfig", "Cache-Control": "no-store" });
213
- res.end(predictionPac(proxy.address().port)); return;
225
+ res.end(predictionPac(proxy.address().port, networkRoute)); return;
214
226
  }
215
227
  res.writeHead(403); res.end();
216
228
  });
@@ -234,6 +246,7 @@ export async function startPredictionTransport({
234
246
  async close() {
235
247
  closing = true;
236
248
  for (const upstream of upstreams) upstream.destroy();
249
+ agent?.destroy();
237
250
  for (const socket of sockets) socket.destroy();
238
251
  await new Promise((resolve) => proxy.close(resolve));
239
252
  gateway.close();
@@ -5,6 +5,8 @@ import { predictionPaths, readPredictionSettings, writePredictionSettings, asser
5
5
  assertCodexClosed, installedPredictionApp, predictionAppMatches, readPredictionCertificate,
6
6
  createPredictionCertificate, updatePredictionTrust } from "./native-prediction-profile.mjs";
7
7
  import { retirePredictionIntegration } from "./prediction-retirement.mjs";
8
+ import { networkRouteSnapshot } from "./network-route.mjs";
9
+ import { withFileLock } from "./lock.mjs";
8
10
 
9
11
  function systemProxy(exec = execFileSync) {
10
12
  return exec("/usr/sbin/scutil", ["--proxy"], { encoding: "utf8", timeout: 2000, maxBuffer: 64 * 1024 });
@@ -19,15 +21,32 @@ function certificateDirectory(settings, paths) {
19
21
  return directory;
20
22
  }
21
23
 
22
- export async function runPredictionCommand({ action = "status", env = process.env,
24
+ export async function runPredictionCommand(options = {}) {
25
+ if (!options.action || options.action === "status") return runPredictionCommandUnlocked(options);
26
+ if ((options.platform || process.platform) !== "darwin") return runPredictionCommandUnlocked(options);
27
+ const { settings } = predictionPaths(options);
28
+ return withFileLock(`${settings}.lock`, () => runPredictionCommandUnlocked(options), { timeoutMs: 2000 });
29
+ }
30
+
31
+ async function runPredictionCommandUnlocked({ action = "status", env = process.env,
23
32
  platform = process.platform, output = console.log, resolveApp = installedPredictionApp,
24
- writeSettings = writePredictionSettings, ...options } = {}) {
33
+ writeSettings = writePredictionSettings, runtimeSnapshot, ...options } = {}) {
25
34
  const opts = { ...options, env };
26
35
  const settings = readPredictionSettings(opts);
27
36
  if (action === "status") {
28
37
  output(`Native prediction preference: ${settings.enabled ? "on" : "off"} (default: off)`);
29
38
  output("Uses the conversation's model. No prediction MCP, Hook, or panel.");
30
39
  if (settings.enabled && !predictionAppMatches(settings.app)) output("Codex App changed; quit it and run ccdx prediction on to recheck compatibility.");
40
+ if (runtimeSnapshot) {
41
+ try {
42
+ const runtime = (await runtimeSnapshot())?.client_integrations?.prediction;
43
+ if (runtime) {
44
+ output(`Bridge: ${runtime.bridge}; client route: ${runtime.client_route}; visible candidate: ${runtime.candidate}`);
45
+ if (runtime.reason) output(runtime.reason);
46
+ for (const [name, event] of Object.entries(runtime.configuration || {})) output(`${name}: HTTP ${event.status}${event.modified ? " (local compatibility)" : " (upstream)"}`);
47
+ } else output("Runtime prediction state: unavailable; status is a saved preference, not proof of a visible suggestion.");
48
+ } catch { output("Runtime prediction state: unavailable; start ccdx and check again."); }
49
+ }
31
50
  return settings;
32
51
  }
33
52
  if (!["on", "off"].includes(action)) throw new Error("Prediction action must be on, off, or status");
@@ -100,34 +119,38 @@ export async function runPredictionCommand({ action = "status", env = process.en
100
119
  }
101
120
 
102
121
  export async function startNativePrediction({ env = process.env, platform = process.platform,
103
- warning = () => {}, ...options } = {}) {
122
+ warning = () => {}, state, ...options } = {}) {
104
123
  const opts = { ...options, env };
105
124
  const settings = readPredictionSettings(opts);
125
+ state?.configure(settings.enabled);
106
126
  if (!settings.enabled) return null;
107
127
  if (platform !== "darwin") throw new Error("Native predictions were skipped: unsupported platform");
108
- assertPredictionProxyCompatible({ env, systemProxy: systemProxy(options.exec) });
128
+ const networkRoute = assertPredictionProxyCompatible({ env, systemProxy: systemProxy(options.exec) });
109
129
  if (!predictionAppMatches(settings.app)) throw new Error("Codex App changed; run ccdx prediction on after quitting it to recheck compatibility");
110
130
  const paths = predictionPaths(opts);
111
131
  if (settings.store !== paths.store || !fs.existsSync(paths.store)) throw new Error("Prediction profile trust is unavailable; run ccdx prediction on after quitting Codex");
112
132
  const certificate = readPredictionCertificate(certificateDirectory(settings, paths));
113
133
  if (certificate.hash !== settings.certificateHash) throw new Error("Prediction certificate changed; setup must be checked again");
114
- const [{ startPredictionTransport }, { applyNativePredictionOverlay, normalizeStatsigRequest }] = await Promise.all([
134
+ const [{ startPredictionTransport }, { applyNativePredictionOverlay, normalizeStatsigRequest }, { createProxyAgent }] = await Promise.all([
115
135
  import("./native-prediction-transport.mjs"), import("./native-prediction-overlay.mjs"),
136
+ import("./proxy-tunnel.mjs"),
116
137
  ]);
117
138
  // An in-memory switch is refreshed independently of request bodies and never
118
139
  // touches the model path. A failed read turns off this optional overlay.
119
140
  let enabled = true;
120
141
  const timer = setInterval(() => {
121
- try { enabled = readPredictionSettings(opts).enabled; }
142
+ try { enabled = readPredictionSettings(opts).enabled; state?.configure(enabled); }
122
143
  catch { enabled = false; }
123
144
  }, 1000);
124
145
  timer.unref();
125
146
  let transport;
126
147
  try {
127
148
  transport = await startPredictionTransport({ ...certificate, enabled: () => enabled,
149
+ networkRoute, agent: networkRoute.https ? createProxyAgent(networkRoute) : undefined,
150
+ onEvent: (event) => state?.event(event),
128
151
  overlay: ({ host, pathname, status, body, enabled }) => {
129
152
  const result = applyNativePredictionOverlay({ hostname: host, pathname, statusCode: status, body, enabled });
130
- return result.changed ? { body: result.body, status: result.statusCode } : null;
153
+ return result.changed ? { body: result.body, status: result.statusCode } : { official: result.official };
131
154
  },
132
155
  normalizeRequest: (parsed) => {
133
156
  const result = normalizeStatsigRequest({ hostname: "ab.chatgpt.com", pathname: "/v1/initialize", body: JSON.stringify(parsed), enabled });
@@ -135,12 +158,13 @@ export async function startNativePrediction({ env = process.env, platform = proc
135
158
  },
136
159
  });
137
160
  } catch (error) { clearInterval(timer); throw error; }
161
+ state?.running(networkRouteSnapshot(networkRoute));
138
162
  warning("Native prediction bridge enabled; only the two configuration hosts use its App-only route.");
139
163
  warning("If Codex is already running, quit it and run ccdx again to apply the route.");
140
164
  return {
141
165
  app: settings.app.app,
142
166
  pacUrl: transport.pacUrl,
143
167
  args: [`--proxy-pac-url=${transport.pacUrl}`],
144
- async close() { clearInterval(timer); await transport.close(); },
168
+ async close() { clearInterval(timer); await transport.close(); state?.stopped(); },
145
169
  };
146
170
  }
@@ -0,0 +1,102 @@
1
+ import net from "node:net";
2
+
3
+ const OFFICIAL_API = "https://chatgpt.com/backend-api";
4
+
5
+ function proxy(value, source) {
6
+ if (!value) return null;
7
+ let url;
8
+ try { url = new URL(value); } catch { throw new Error(`Invalid proxy address in ${source}`); }
9
+ if (!["http:", "https:", "socks5:", "socks5h:"].includes(url.protocol)
10
+ || !url.hostname || !["", "/"].includes(url.pathname) || url.search || url.hash) {
11
+ throw new Error(`Unsupported proxy address in ${source}; expected HTTP, HTTPS or SOCKS5`);
12
+ }
13
+ return { url, source };
14
+ }
15
+
16
+ function variable(env, lower, upper) {
17
+ const key = String(env[lower] || "").trim() ? lower : upper;
18
+ return { value: String(env[key] || "").trim(), source: key };
19
+ }
20
+
21
+ export function resolveNetworkRoute({ env = process.env, systemProxy = "", checkApi = true } = {}) {
22
+ const api = String(env.CODEX_API_BASE_URL || "").trim().replace(/\/+$/, "");
23
+ if (checkApi && api && api !== OFFICIAL_API) {
24
+ throw new Error("Native prediction setup skipped: CODEX_API_BASE_URL selects a custom API route; it was preserved");
25
+ }
26
+ const settings = Object.fromEntries([...systemProxy.matchAll(/^\s*([A-Za-z]+)\s*:\s*(.+?)\s*$/gm)].map((m) => [m[1], m[2]]));
27
+ for (const key of ["ProxyAutoConfigEnable", "ProxyAutoDiscoveryEnable"]) {
28
+ if (settings[key] === "1") throw new Error(`Native prediction setup skipped: system ${key === "ProxyAutoConfigEnable" ? "PAC" : "WPAD"} (${key}) needs dynamic routing; its rules were preserved`);
29
+ }
30
+ const system = (prefix, scheme) => {
31
+ if (settings[`${prefix}Enable`] !== "1") return null;
32
+ const host = settings[`${prefix}Proxy`], port = settings[`${prefix}Port`];
33
+ if (!host || !/^\d+$/.test(port || "") || Number(port) < 1 || Number(port) > 65535) {
34
+ throw new Error(`Invalid system ${prefix} proxy settings`);
35
+ }
36
+ return proxy(`${scheme}://${net.isIP(host) === 6 ? `[${host}]` : host}:${port}`, `system ${prefix}`);
37
+ };
38
+ const httpEnv = variable(env, "http_proxy", "HTTP_PROXY"), httpsEnv = variable(env, "https_proxy", "HTTPS_PROXY");
39
+ const allEnv = variable(env, "all_proxy", "ALL_PROXY");
40
+ const all = proxy(allEnv.value, allEnv.source);
41
+ const systemSocks = system("SOCKS", "socks5");
42
+ const systemHttp = system("HTTP", "http"), systemHttps = system("HTTPS", "http");
43
+ const http = proxy(httpEnv.value, httpEnv.source) || all || systemHttp || systemSocks;
44
+ const https = proxy(httpsEnv.value, httpsEnv.source) || all || systemHttps || systemSocks;
45
+ const noProxy = variable(env, "no_proxy", "NO_PROXY").value;
46
+ const exceptions = /ExceptionsList\s*:\s*<array>\s*\{([^}]+)\}/.exec(systemProxy)?.[1] || "";
47
+ const bypass = [...new Set([...noProxy.split(","), ...[...exceptions.matchAll(/\d+\s*:\s*([^\n]+)/g)].map((m) => m[1])]
48
+ .map((s) => s.trim().toLowerCase()).filter(Boolean))];
49
+ if (bypass.some((rule) => rule.includes("/"))) throw new Error("Unsupported proxy bypass CIDR rule in NO_PROXY or system exceptions; existing routes were preserved");
50
+ return { http, https, bypass, excludeSimple: settings.ExcludeSimpleHostnames === "1" };
51
+ }
52
+
53
+ export function bypassNetworkProxy(hostname, port, plan) {
54
+ const host = hostname.replace(/^\[|\]$/g, "").toLowerCase();
55
+ if (["localhost", "127.0.0.1", "::1"].includes(host) || (plan.excludeSimple && !host.includes("."))) return true;
56
+ return plan.bypass.some((raw) => {
57
+ if (raw === "*") return true;
58
+ const match = /^(.*):(\d+)$/.exec(raw);
59
+ const rule = match ? match[1] : raw;
60
+ if (match && Number(match[2]) !== Number(port)) return false;
61
+ const suffix = rule.replace(/^\*?\./, "");
62
+ if (rule.includes("*")) {
63
+ const expression = rule.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replaceAll("*", ".*");
64
+ return new RegExp(`^${expression}$`).test(host);
65
+ }
66
+ return host === suffix || host.endsWith(`.${suffix}`);
67
+ });
68
+ }
69
+
70
+ export function networkProxyFor(url, plan) {
71
+ if (bypassNetworkProxy(url.hostname, url.port || (url.protocol === "https:" ? 443 : 80), plan)) return null;
72
+ return url.protocol === "https:" ? plan.https : plan.http;
73
+ }
74
+
75
+ export function networkRouteSnapshot(plan) {
76
+ const route = plan.https;
77
+ return { source: route?.source || "direct", type: route?.url.protocol.replace(":", "") || "direct",
78
+ bypass_rules: plan.bypass.length, proxy_credentials: Boolean(route?.url.username || route?.url.password) };
79
+ }
80
+
81
+ function pacProxy(route) {
82
+ if (!route) return "DIRECT";
83
+ const type = route.url.protocol.startsWith("socks5") ? "SOCKS5" : route.url.protocol === "https:" ? "HTTPS" : "PROXY";
84
+ const port = route.url.port || (type === "HTTPS" ? 443 : type === "SOCKS5" ? 1080 : 80);
85
+ return `${type} ${route.url.hostname}:${port}`;
86
+ }
87
+
88
+ export function networkPredictionPac(port, plan) {
89
+ if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error("Invalid prediction proxy port");
90
+ // Embed only routing rules, never proxy passwords or API credentials.
91
+ return `var ccdxRoute=${JSON.stringify({ http: pacProxy(plan.http), https: pacProxy(plan.https), bypass: plan.bypass, excludeSimple: plan.excludeSimple })};
92
+ function FindProxyForURL(url,host) {
93
+ var secure=url.indexOf('https:')===0, p=secure?443:80;
94
+ var authority=url.split('/')[2]||'', explicit=/:([0-9]+)$/.exec(authority);if(explicit)p=Number(explicit[1]);
95
+ var skip=host==='localhost'||host==='127.0.0.1'||host==='::1'||(ccdxRoute.excludeSimple&&host.indexOf('.')<0);
96
+ for(var i=0;i<ccdxRoute.bypass.length&&!skip;i++){var raw=ccdxRoute.bypass[i],m=/^(.*):([0-9]+)$/.exec(raw),r=m?m[1]:raw;if(m&&Number(m[2])!==p)continue;
97
+ var suffix=r.indexOf('*.')===0?r.slice(2):r[0]==='.'?r.slice(1):r;skip=r==='*'||(r.indexOf('*')>=0?shExpMatch(host,r):host===suffix||dnsDomainIs(host,'.'+suffix));}
98
+ var original=skip?'DIRECT':secure?ccdxRoute.https:ccdxRoute.http;
99
+ if(secure&&(host==='chatgpt.com'||host==='ab.chatgpt.com'))return 'PROXY 127.0.0.1:${port}; '+original;
100
+ return original;
101
+ }\n`;
102
+ }
@@ -159,6 +159,7 @@ export function runtimeStatusPayload({
159
159
  codexClient,
160
160
  codexModelRegistry,
161
161
  codexModelCatalog,
162
+ clientIntegrations,
162
163
  } = {}) {
163
164
  const memory = process.memoryUsage();
164
165
  const codexRuntime = safeClientRuntimeStatus(codexClient);
@@ -196,6 +197,7 @@ export function runtimeStatusPayload({
196
197
  response_history: history,
197
198
  image_optimization: imageOptimizationStats(),
198
199
  image_generation: imageGeneration || null,
200
+ client_integrations: clientIntegrations || null,
199
201
  logging: { usage: usageLoggingStats(), debug: debugLoggingStats() },
200
202
  image_history_pressure: imagePressure?.snapshot?.() || null,
201
203
  response_failures: responseFailures?.snapshot?.() || null,
@@ -0,0 +1,37 @@
1
+ import { redactDiagnosticText } from "./diagnostic-text.mjs";
2
+
3
+ export function operationError(code, message, { status = null, retryAt = null } = {}) {
4
+ return Object.assign(new Error(message), { code, status, retryAt });
5
+ }
6
+
7
+ export function operationErrorDetails(error) {
8
+ const code = String(error?.code || error?.cause?.code || "unknown");
9
+ let source = "unknown";
10
+ if (/proxy/i.test(code)) source = "proxy";
11
+ else if (/cert|tls|ssl/i.test(code)) source = "tls";
12
+ else if (/timeout|abort/i.test(code) || error?.name === "AbortError" || error?.name === "TimeoutError") source = "timeout";
13
+ else if (/limit|429/.test(code)) source = "rate_limit";
14
+ else if (/config|format|route|redirect/.test(code)) source = "configuration";
15
+ else if (/HTTP|http/.test(code)) source = "upstream";
16
+ else if (/ENOTFOUND|ECONN|EAI_/.test(code)) source = "network";
17
+ return { code: /^[A-Za-z0-9_-]{1,80}$/.test(code) ? code : "unknown", source,
18
+ message: redactDiagnosticText(String(error?.message || "Operation failed")).slice(0, 400),
19
+ status: Number.isInteger(error?.status) ? error.status : null };
20
+ }
21
+
22
+ // Error classifiers consume decoded string fields; responses retain their
23
+ // original bytes. Only escaped, bounded JSON errors need this extra work.
24
+ export function errorEvidenceText(value) {
25
+ const text = String(value || "");
26
+ if (!text.includes("\\u") || text.length > 16 * 1024) return text;
27
+ try {
28
+ const strings = [];
29
+ const visit = (node, depth) => {
30
+ if (depth > 4 || strings.length >= 32) return;
31
+ if (typeof node === "string") strings.push(node);
32
+ else if (node && typeof node === "object") for (const child of Object.values(node)) visit(child, depth + 1);
33
+ };
34
+ visit(JSON.parse(text), 0);
35
+ return `${text}\n${strings.join("\n")}`;
36
+ } catch { return text; }
37
+ }
@@ -1,6 +1,6 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { createInterface } from "node:readline/promises";
3
- import { fetchLatestRelease, githubReleaseVersion } from "./version.mjs";
3
+ import { lookupLatestRelease, githubReleaseVersion } from "./version.mjs";
4
4
 
5
5
  const PACKAGE_NAME = "codex-copilot-dx";
6
6
  const UPDATE_SPECS = {
@@ -95,8 +95,7 @@ export async function runPackageUpdateCommand({
95
95
  let release;
96
96
  if (selectedSource === "github") {
97
97
  output.write("Checking the latest stable GitHub release...\n");
98
- release = await fetchLatestRelease({ fetchImpl, timeoutMs: 10000 });
99
- if (!release) throw new Error("Could not read the latest stable GitHub release. No update was installed.");
98
+ release = await lookupLatestRelease({ fetchImpl, timeoutMs: 10000 });
100
99
  }
101
100
  const update = globalUpdateCommand(selectedSource, { platform, releaseTag: release?.tagName });
102
101
  const sourceLabel = selectedSource === "npm" ? "npm registry" : `GitHub release ${release.tagName}`;
@@ -6,6 +6,8 @@ import { isDeepStrictEqual } from "node:util";
6
6
  import { atomicWriteFileIfChangedSync, atomicWriteFilePairSync } from "./atomic-file.mjs";
7
7
  import { codexTomlStatements, parseCodexToml, validateManagedConfigEdit } from "./config-toml.mjs";
8
8
  import { userSettingsPath } from "./user-settings.mjs";
9
+ import { codexPaths } from "./codex-paths.mjs";
10
+ import { withFileLockSync } from "./lock.mjs";
9
11
 
10
12
  const SERVER_PATH = ["mcp_servers", "ccdx_suggestion"];
11
13
  const same = (left, right) => isDeepStrictEqual(left, right);
@@ -52,9 +54,18 @@ export function computeRetiredPredictionConfig(content, filePath) {
52
54
  return { content: updated, changed: updated !== content, preserved: Boolean(existing && !owned) || (content.includes("# ccdx:suggestion-auto:start") && !hookOwned) };
53
55
  }
54
56
 
55
- export function retirePredictionIntegration({ env = process.env, home = os.homedir(), dryRun = false } = {}) {
57
+ export function retirePredictionIntegration(options = {}) {
58
+ if (options.dryRun) return retirePredictionIntegrationUnlocked(options);
59
+ const { config } = codexPaths(options);
60
+ // No registration/settings means no lock directory or other write on startup.
61
+ const env = options.env || process.env;
62
+ const settings = path.resolve(env.CCDX_SUGGESTION_CONFIG || path.join(path.dirname(userSettingsPath(options)), "suggestion.json"));
63
+ if (!fs.existsSync(config) && !fs.existsSync(settings)) return { changed: false, preserved: false };
64
+ return withFileLockSync(`${config}.ccdx.lock`, () => retirePredictionIntegrationUnlocked(options));
65
+ }
66
+ function retirePredictionIntegrationUnlocked({ env = process.env, home = os.homedir(), dryRun = false } = {}) {
56
67
  const settings = path.resolve(env.CCDX_SUGGESTION_CONFIG || path.join(path.dirname(userSettingsPath({ env, home })), "suggestion.json"));
57
- const codex = path.join(env.CODEX_HOME || path.join(home, ".codex"), "config.toml");
68
+ const codex = codexPaths({ env, home }).config;
58
69
  let content;
59
70
  try { content = new TextDecoder("utf-8", { fatal: true }).decode(fs.readFileSync(codex)); }
60
71
  catch (error) { if (error.code !== "ENOENT") throw new Error("Cannot safely read prediction configuration"); }