codex-copilot-dx 0.9.24 → 0.9.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -6
- package/bin/cli.mjs +19 -29
- package/docs/ARCHITECTURE.md +50 -0
- package/docs/README.md +4 -0
- package/docs/REFERENCE.md +53 -8
- package/package.json +3 -2
- package/src/adapter.mjs +1 -0
- package/src/cli-image.mjs +13 -10
- package/src/cli-options.mjs +1 -1
- package/src/cli-status.mjs +5 -0
- package/src/client-integration.mjs +41 -0
- package/src/codex-config-doctor.mjs +3 -3
- package/src/codex-paths.mjs +10 -0
- package/src/codex-thread-compression.mjs +11 -6
- package/src/config.mjs +10 -6
- package/src/copilot-responses-policy.mjs +4 -2
- package/src/dashboard/index.html +7 -0
- package/src/dashboard/language.js +13 -0
- package/src/dashboard/ui.js +9 -0
- package/src/lock.mjs +29 -0
- package/src/native-prediction-profile.mjs +5 -8
- package/src/native-prediction-state.mjs +29 -0
- package/src/native-prediction-transport.mjs +18 -5
- package/src/native-prediction.mjs +32 -8
- package/src/network-route.mjs +102 -0
- package/src/observability.mjs +2 -0
- package/src/operation-error.mjs +37 -0
- package/src/package-update.mjs +2 -3
- package/src/prediction-retirement.mjs +13 -2
- package/src/proxy-tunnel.mjs +102 -0
- package/src/responses-request.mjs +5 -2
- package/src/user-settings.mjs +17 -4
- package/src/version.mjs +23 -46
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import net from "node:net";
|
|
2
|
+
import tls from "node:tls";
|
|
3
|
+
import https from "node:https";
|
|
4
|
+
import { networkProxyFor } from "./network-route.mjs";
|
|
5
|
+
|
|
6
|
+
// A bounded read preserves bytes following the handshake for the TLS parser.
|
|
7
|
+
function readSocket(socket, ready, maxBytes = 16 * 1024) {
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
let chunks = [], size = 0;
|
|
10
|
+
const cleanup = () => { socket.off("data", data); socket.off("error", error); socket.off("close", closed); };
|
|
11
|
+
const error = (e) => { cleanup(); reject(e); };
|
|
12
|
+
const closed = () => error(new Error("Proxy closed the connection"));
|
|
13
|
+
const data = (chunk) => {
|
|
14
|
+
chunks.push(chunk); size += chunk.length;
|
|
15
|
+
if (size > maxBytes) { error(new Error("Proxy handshake exceeds its limit")); return; }
|
|
16
|
+
const bytes = Buffer.concat(chunks, size), end = ready(bytes);
|
|
17
|
+
if (end !== null) {
|
|
18
|
+
socket.pause(); cleanup();
|
|
19
|
+
if (bytes.length > end) socket.unshift(bytes.subarray(end));
|
|
20
|
+
resolve(bytes.subarray(0, end));
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
socket.on("data", data); socket.once("error", error); socket.once("close", closed); socket.resume();
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function connectProxy(target, route, timeoutMs, pending) {
|
|
28
|
+
const proxy = route.url;
|
|
29
|
+
const proxyHost = proxy.hostname.replace(/^\[|\]$/g, "");
|
|
30
|
+
const socket = proxy.protocol === "https:"
|
|
31
|
+
? tls.connect({ host: proxyHost, port: Number(proxy.port || 443), servername: net.isIP(proxyHost) ? undefined : proxyHost, rejectUnauthorized: true })
|
|
32
|
+
: net.connect({ host: proxyHost, port: Number(proxy.port || (proxy.protocol.startsWith("socks") ? 1080 : 80)) });
|
|
33
|
+
pending.add(socket);
|
|
34
|
+
const timer = setTimeout(() => socket.destroy(new Error("Proxy connection timed out")), timeoutMs);
|
|
35
|
+
socket.once("close", () => pending.delete(socket));
|
|
36
|
+
try {
|
|
37
|
+
await new Promise((resolve, reject) => {
|
|
38
|
+
const event = proxy.protocol === "https:" ? "secureConnect" : "connect";
|
|
39
|
+
const failed = (error) => { socket.off(event, connected); reject(error); };
|
|
40
|
+
const connected = () => { socket.off("error", failed); resolve(); };
|
|
41
|
+
socket.once(event, connected); socket.once("error", failed);
|
|
42
|
+
});
|
|
43
|
+
if (proxy.protocol.startsWith("socks5")) {
|
|
44
|
+
const user = Buffer.from(decodeURIComponent(proxy.username)), password = Buffer.from(decodeURIComponent(proxy.password));
|
|
45
|
+
if (user.length > 255 || password.length > 255) throw new Error("SOCKS5 proxy credentials are too long");
|
|
46
|
+
const greeting = readSocket(socket, b => b.length >= 2 ? 2 : null);
|
|
47
|
+
socket.write(user.length ? Buffer.from([5, 2, 0, 2]) : Buffer.from([5, 1, 0]));
|
|
48
|
+
const choice = await greeting;
|
|
49
|
+
if (choice[0] !== 5 || ![0, 2].includes(choice[1]) || (choice[1] === 2 && !user.length)) throw new Error("SOCKS5 proxy authentication is unsupported");
|
|
50
|
+
if (choice[1] === 2) {
|
|
51
|
+
const auth = readSocket(socket, b => b.length >= 2 ? 2 : null);
|
|
52
|
+
socket.write(Buffer.concat([Buffer.from([1, user.length]), user, Buffer.from([password.length]), password]));
|
|
53
|
+
const result = await auth; if (result[0] !== 1 || result[1] !== 0) throw new Error("SOCKS5 proxy authentication failed");
|
|
54
|
+
}
|
|
55
|
+
const host = Buffer.from(target.hostname);
|
|
56
|
+
if (host.length > 255) throw new Error("Proxy target hostname is too long");
|
|
57
|
+
const response = readSocket(socket, b => {
|
|
58
|
+
if (b.length < 5) return null;
|
|
59
|
+
const size = b[3] === 1 ? 10 : b[3] === 4 ? 22 : b[3] === 3 ? 7 + b[4] : 5;
|
|
60
|
+
return b.length >= size ? size : null;
|
|
61
|
+
});
|
|
62
|
+
socket.write(Buffer.concat([Buffer.from([5, 1, 0, 3, host.length]), host, Buffer.from([1, 187])]));
|
|
63
|
+
const result = await response;
|
|
64
|
+
if (result[0] !== 5 || result[1] !== 0 || ![1, 3, 4].includes(result[3])) throw new Error("SOCKS5 proxy rejected the connection");
|
|
65
|
+
} else {
|
|
66
|
+
const response = readSocket(socket, b => { const end = b.indexOf("\r\n\r\n"); return end < 0 ? null : end + 4; });
|
|
67
|
+
const credentials = proxy.username || proxy.password
|
|
68
|
+
? `Proxy-Authorization: Basic ${Buffer.from(`${decodeURIComponent(proxy.username)}:${decodeURIComponent(proxy.password)}`).toString("base64")}\r\n` : "";
|
|
69
|
+
socket.write(`CONNECT ${target.hostname}:443 HTTP/1.1\r\nHost: ${target.hostname}:443\r\n${credentials}\r\n`);
|
|
70
|
+
const headers = (await response).toString("latin1");
|
|
71
|
+
const status = /^HTTP\/1\.[01] (\d{3})/.exec(headers)?.[1];
|
|
72
|
+
if (status !== "200") throw new Error(`HTTP proxy rejected CONNECT (${status || "invalid response"})`);
|
|
73
|
+
}
|
|
74
|
+
clearTimeout(timer); pending.delete(socket);
|
|
75
|
+
return socket;
|
|
76
|
+
} catch (error) { clearTimeout(timer); socket.destroy(); throw error; }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function createProxyAgent(plan, { timeoutMs = 30_000 } = {}) {
|
|
80
|
+
const pending = new Set();
|
|
81
|
+
let closed = false;
|
|
82
|
+
const agent = new https.Agent({ keepAlive: true, maxSockets: 32, maxFreeSockets: 4 });
|
|
83
|
+
const direct = agent.createConnection.bind(agent);
|
|
84
|
+
agent.createConnection = (options, callback) => {
|
|
85
|
+
if (closed) { queueMicrotask(() => callback(new Error("Proxy agent is closed"))); return undefined; }
|
|
86
|
+
const target = new URL(`https://${options.host}`), route = networkProxyFor(target, plan);
|
|
87
|
+
if (!route) return direct(options, callback);
|
|
88
|
+
void connectProxy(target, route, timeoutMs, pending).then(socket => {
|
|
89
|
+
if (closed) { socket.destroy(); callback(new Error("Proxy agent is closed")); return; }
|
|
90
|
+
const secure = tls.connect({ ...options, socket, rejectUnauthorized: true });
|
|
91
|
+
pending.add(secure);
|
|
92
|
+
const timer = setTimeout(() => secure.destroy(new Error("Proxy target TLS timed out")), timeoutMs);
|
|
93
|
+
const fail = (error) => { clearTimeout(timer); pending.delete(secure); secure.off("secureConnect", done); callback(error); };
|
|
94
|
+
const done = () => { clearTimeout(timer); pending.delete(secure); secure.off("error", fail); callback(null, secure); };
|
|
95
|
+
secure.once("error", fail); secure.once("secureConnect", done); secure.resume();
|
|
96
|
+
}, callback);
|
|
97
|
+
return undefined;
|
|
98
|
+
};
|
|
99
|
+
const destroy = agent.destroy.bind(agent);
|
|
100
|
+
agent.destroy = () => { closed = true; for (const socket of pending) socket.destroy(new Error("Proxy agent closed")); pending.clear(); destroy(); };
|
|
101
|
+
return agent;
|
|
102
|
+
}
|
|
@@ -309,7 +309,8 @@ export function prepareResponsesRequest(reqBody, {
|
|
|
309
309
|
assertActive?.();
|
|
310
310
|
const body = mutate ? reqBody : cloneJson(reqBody);
|
|
311
311
|
assertActive?.();
|
|
312
|
-
|
|
312
|
+
// body is already an owned clone unless the caller explicitly allows mutation.
|
|
313
|
+
const currentInputItems = responsesInputItems(body.input, { assertActive, clone: false });
|
|
313
314
|
const currentInputRefs = new Set(
|
|
314
315
|
currentInputItems.filter((item) => item && typeof item === "object"),
|
|
315
316
|
);
|
|
@@ -359,7 +360,9 @@ export function prepareResponsesRequest(reqBody, {
|
|
|
359
360
|
// concrete Copilot RoutePlan.
|
|
360
361
|
if (copilotBoundary) applyCopilotResponsesRequestPolicies(body);
|
|
361
362
|
stripInternalResponsesInputFields(body.input, { assertActive });
|
|
362
|
-
|
|
363
|
+
// The first pass already covers shared current items. Only an independent
|
|
364
|
+
// pre-adaptation snapshot needs another pass.
|
|
365
|
+
if (historyInputItems !== currentInputItems) stripInternalResponsesInputFields(historyInputItems, { assertActive });
|
|
363
366
|
assertActive?.();
|
|
364
367
|
const retainedCurrentInputStart = body.input.findIndex((item) => currentInputRefs.has(item));
|
|
365
368
|
|
package/src/user-settings.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { atomicWriteFileIfChangedSync } from "./atomic-file.mjs";
|
|
5
|
+
import { withFileLockSync } from "./lock.mjs";
|
|
5
6
|
import { AUTO_REVIEW_REASONING_EFFORTS, DEFAULT_CODEX_AUTO_REVIEW_MODEL } from "./models.mjs";
|
|
6
7
|
import { loadRuntimeConfig, RUNTIME_DEFAULTS } from "./runtime-config.mjs";
|
|
7
8
|
import {
|
|
@@ -155,7 +156,10 @@ export function decodedBodyLimitPreference({ env = process.env, home = os.homedi
|
|
|
155
156
|
return { bytes: RUNTIME_DEFAULTS.maxDecodedBodyBytes, mib: DECODED_BODY_LIMIT_MIN_MIB, source: "default" };
|
|
156
157
|
}
|
|
157
158
|
|
|
158
|
-
export function writeDecodedBodyLimitMib(mib,
|
|
159
|
+
export function writeDecodedBodyLimitMib(mib, options = {}) {
|
|
160
|
+
return withFileLockSync(`${userSettingsPath(options)}.lock`, () => writeDecodedBodyLimitMibUnlocked(mib, options));
|
|
161
|
+
}
|
|
162
|
+
function writeDecodedBodyLimitMibUnlocked(mib, { env = process.env, home = os.homedir() } = {}) {
|
|
159
163
|
const filePath = userSettingsPath({ env, home });
|
|
160
164
|
const settings = readUserSettings({ env, home, strict: true });
|
|
161
165
|
const next = { ...settings };
|
|
@@ -177,7 +181,10 @@ export function writeDecodedBodyLimitMib(mib, { env = process.env, home = os.hom
|
|
|
177
181
|
return { changed, filePath, ...decodedBodyLimitPreference({ env, home }) };
|
|
178
182
|
}
|
|
179
183
|
|
|
180
|
-
export function writeResponseHistoryLimitMib(mib,
|
|
184
|
+
export function writeResponseHistoryLimitMib(mib, options = {}) {
|
|
185
|
+
return withFileLockSync(`${userSettingsPath(options)}.lock`, () => writeResponseHistoryLimitMibUnlocked(mib, options));
|
|
186
|
+
}
|
|
187
|
+
function writeResponseHistoryLimitMibUnlocked(mib, { env = process.env, home = os.homedir() } = {}) {
|
|
181
188
|
const filePath = userSettingsPath({ env, home });
|
|
182
189
|
const settings = readUserSettings({ env, home, strict: true });
|
|
183
190
|
const next = { ...settings };
|
|
@@ -199,7 +206,10 @@ export function writeResponseHistoryLimitMib(mib, { env = process.env, home = os
|
|
|
199
206
|
return { changed, filePath, ...responseHistoryLimitPreference({ env, home }) };
|
|
200
207
|
}
|
|
201
208
|
|
|
202
|
-
export function writeAutoReviewModel(model,
|
|
209
|
+
export function writeAutoReviewModel(model, options = {}) {
|
|
210
|
+
return withFileLockSync(`${userSettingsPath(options)}.lock`, () => writeAutoReviewModelUnlocked(model, options));
|
|
211
|
+
}
|
|
212
|
+
function writeAutoReviewModelUnlocked(model, { env = process.env, home = os.homedir(), reasoningEffort } = {}) {
|
|
203
213
|
if (reasoningEffort !== undefined && reasoningEffort !== null
|
|
204
214
|
&& !AUTO_REVIEW_REASONING_EFFORTS.includes(reasoningEffort)) {
|
|
205
215
|
throw new Error("Auto-review reasoning effort is invalid");
|
|
@@ -220,7 +230,10 @@ export function writeAutoReviewModel(model, { env = process.env, home = os.homed
|
|
|
220
230
|
return { changed, filePath, model: value || DEFAULT_CODEX_AUTO_REVIEW_MODEL };
|
|
221
231
|
}
|
|
222
232
|
|
|
223
|
-
export function writeTerminalAnimationTheme(theme,
|
|
233
|
+
export function writeTerminalAnimationTheme(theme, options = {}) {
|
|
234
|
+
return withFileLockSync(`${userSettingsPath(options)}.lock`, () => writeTerminalAnimationThemeUnlocked(theme, options));
|
|
235
|
+
}
|
|
236
|
+
function writeTerminalAnimationThemeUnlocked(theme, { env = process.env, home = os.homedir() } = {}) {
|
|
224
237
|
if (!isTerminalAnimationTheme(theme)) {
|
|
225
238
|
const choices = TERMINAL_ANIMATION_THEMES.map(({ id }) => id).join(", ");
|
|
226
239
|
throw new Error(`Terminal animation must be one of: ${choices}`);
|
package/src/version.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { operationError } from "./operation-error.mjs";
|
|
4
5
|
|
|
5
|
-
const GITHUB_LATEST_RELEASE_URL = "https://
|
|
6
|
-
const MAX_RELEASE_BYTES = 256 * 1024;
|
|
6
|
+
const GITHUB_LATEST_RELEASE_URL = "https://github.com/DaleXiao/codex-copilot-dx/releases/latest";
|
|
7
7
|
|
|
8
8
|
export function localPackageVersion() {
|
|
9
9
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -37,62 +37,39 @@ export function githubReleaseVersion(tag) {
|
|
|
37
37
|
return match.slice(1).join(".");
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
async function
|
|
41
|
-
const reader = response.body?.getReader?.();
|
|
42
|
-
if (!reader) throw new Error("GitHub release response has no readable body");
|
|
43
|
-
const cancel = () => { void reader.cancel().catch(() => {}); };
|
|
44
|
-
signal.addEventListener("abort", cancel, { once: true });
|
|
45
|
-
const chunks = [];
|
|
46
|
-
let bytes = 0;
|
|
47
|
-
let complete = false;
|
|
48
|
-
try {
|
|
49
|
-
if (Number(response.headers?.get?.("content-length")) > MAX_RELEASE_BYTES) {
|
|
50
|
-
throw new Error("GitHub release response is too large");
|
|
51
|
-
}
|
|
52
|
-
while (!signal.aborted) {
|
|
53
|
-
const { done, value } = await reader.read();
|
|
54
|
-
if (done) {
|
|
55
|
-
complete = true;
|
|
56
|
-
break;
|
|
57
|
-
}
|
|
58
|
-
bytes += value.byteLength;
|
|
59
|
-
if (bytes > MAX_RELEASE_BYTES) throw new Error("GitHub release response is too large");
|
|
60
|
-
chunks.push(Buffer.from(value));
|
|
61
|
-
}
|
|
62
|
-
if (signal.aborted) throw new Error("GitHub release request timed out");
|
|
63
|
-
return JSON.parse(Buffer.concat(chunks, bytes).toString("utf8"));
|
|
64
|
-
} finally {
|
|
65
|
-
signal.removeEventListener("abort", cancel);
|
|
66
|
-
if (!complete) cancel();
|
|
67
|
-
reader.releaseLock();
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export async function fetchLatestRelease({ fetchImpl = fetch, timeoutMs = 2000 } = {}) {
|
|
40
|
+
export async function lookupLatestRelease({ fetchImpl = fetch, timeoutMs = 2000 } = {}) {
|
|
72
41
|
const controller = new AbortController();
|
|
73
42
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
74
43
|
try {
|
|
75
44
|
const resp = await fetchImpl(GITHUB_LATEST_RELEASE_URL, {
|
|
76
|
-
headers: { Accept: "
|
|
77
|
-
redirect: "
|
|
45
|
+
headers: { Accept: "text/html", "User-Agent": "codex-copilot-dx" },
|
|
46
|
+
redirect: "manual",
|
|
78
47
|
signal: controller.signal,
|
|
79
48
|
});
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
49
|
+
void resp.body?.cancel?.().catch(() => {});
|
|
50
|
+
if ([403, 429].includes(resp.status)) throw operationError("github_rate_limit", `GitHub release lookup was refused (HTTP ${resp.status}); retry later. No update was installed.`, { status: resp.status });
|
|
51
|
+
if (![301, 302, 303, 307, 308].includes(resp.status)) throw operationError("github_http_error", `GitHub release lookup returned HTTP ${resp.status}; no stable release redirect was received. No update was installed.`, { status: resp.status });
|
|
52
|
+
let url;
|
|
53
|
+
try { url = new URL(resp.headers.get("location"), GITHUB_LATEST_RELEASE_URL); } catch {}
|
|
54
|
+
const match = url?.pathname.match(/^\/DaleXiao\/codex-copilot-dx\/releases\/tag\/([^/]+)$/i);
|
|
55
|
+
const tagName = match?.[1], version = githubReleaseVersion(tagName);
|
|
56
|
+
if (!url || url.origin !== "https://github.com" || url.username || url.password || url.search || url.hash || !version) {
|
|
57
|
+
throw operationError("github_invalid_redirect", "GitHub did not return a valid stable release tag for this repository. No update was installed.");
|
|
83
58
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
} catch {
|
|
90
|
-
return null;
|
|
59
|
+
return { version, tagName };
|
|
60
|
+
} catch (error) {
|
|
61
|
+
if (error.code?.startsWith("github_")) throw error;
|
|
62
|
+
if (controller.signal.aborted) throw operationError("github_timeout", "GitHub release lookup timed out. No update was installed.");
|
|
63
|
+
throw operationError(error.cause?.code || "github_network_error", "Could not connect to the GitHub release page. Check your network or proxy. No update was installed.");
|
|
91
64
|
} finally {
|
|
92
65
|
clearTimeout(timer);
|
|
93
66
|
}
|
|
94
67
|
}
|
|
95
68
|
|
|
69
|
+
export async function fetchLatestRelease(options = {}) {
|
|
70
|
+
try { return await lookupLatestRelease(options); } catch { return null; }
|
|
71
|
+
}
|
|
72
|
+
|
|
96
73
|
export async function fetchLatestVersion(options) {
|
|
97
74
|
return (await fetchLatestRelease(options))?.version || null;
|
|
98
75
|
}
|