codex-copilot-dx 0.9.16 → 0.9.18
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 +2 -0
- package/bin/cli.mjs +10 -5
- package/docs/REFERENCE.md +7 -1
- package/package.json +1 -1
- package/src/adapter.mjs +5 -6
- package/src/auth-status.mjs +175 -0
- package/src/cli-auth.mjs +2 -180
- package/src/cli-models.mjs +1 -131
- package/src/cli-table.mjs +3 -11
- package/src/copilot-responses-compat.mjs +1 -1
- package/src/dashboard-api.mjs +4 -4
- package/src/dashboard.mjs +1 -1
- package/src/http-errors.mjs +5 -0
- package/src/http-transport.mjs +6 -350
- package/src/image-mcp.mjs +33 -1
- package/src/image-provider.mjs +34 -17
- package/src/live-models.mjs +137 -0
- package/src/observability.mjs +2 -22
- package/src/request-admission.mjs +348 -0
- package/src/response-failures.mjs +1 -1
- package/src/response-history.mjs +1 -1
- package/src/responses-bridge.mjs +14 -1
- package/src/responses-compaction.mjs +1 -1
- package/src/responses-handler.mjs +1 -1
- package/src/responses-proxy.mjs +2 -2
- package/src/security.mjs +22 -0
- package/src/terminal-text.mjs +11 -0
- package/src/usage-analytics.mjs +1 -1
- package/src/usage-store.mjs +369 -0
- package/src/usage.mjs +17 -368
package/README.md
CHANGED
|
@@ -62,6 +62,8 @@ The dashboard does not poll in the background; refresh when needed.
|
|
|
62
62
|
break continuations. Generated images and Codex transcripts are not deleted.
|
|
63
63
|
- Optional image setup/maintenance never blocks the primary service. Disabled
|
|
64
64
|
users get no CCDX image skill or MCP installation.
|
|
65
|
+
- Image API redirects must stay on the same HTTPS origin. Returned images must
|
|
66
|
+
use public HTTPS URLs; well-known NAT64 mappings follow IPv4 address rules.
|
|
65
67
|
- Keep the adapter on loopback. LAN mode is only for trusted networks; do not
|
|
66
68
|
expose it to the public internet. The dashboard is not an authentication wall
|
|
67
69
|
for the API. Diagnostic errors may contain sensitive upstream text.
|
package/bin/cli.mjs
CHANGED
|
@@ -75,11 +75,16 @@ if (CLI.command === "image") {
|
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
if (CLI.command === "usage") {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
78
|
+
try {
|
|
79
|
+
await printUsageSummary({
|
|
80
|
+
format: CLI.outputFormat || "auto",
|
|
81
|
+
output: process.stdout,
|
|
82
|
+
});
|
|
83
|
+
process.exit(0);
|
|
84
|
+
} catch (e) {
|
|
85
|
+
console.error(status("err", e.message));
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
83
88
|
}
|
|
84
89
|
if (CLI.command === "cache") {
|
|
85
90
|
try {
|
package/docs/REFERENCE.md
CHANGED
|
@@ -203,7 +203,13 @@ ccdx disable-image
|
|
|
203
203
|
|
|
204
204
|
`image-status` never prints the API key and performs only a local, non-generating tool-readiness check. Disabling removes the stored provider credential, the CCDX-owned MCP block, and unmodified skill, reference, and helper files; user-modified files are preserved. Cached tool calls then fail closed. The Node helper creates a new file under the task's `output/imagegen` directory by default and reports its absolute path; it never overwrites an existing file or automatically repeats a failed generation. The image endpoint is contacted only by `enable-image` validation and by explicit `generate_image` or `edit_image` tool calls. Ordinary Responses, models, authentication, Fast, Auto-review, compaction, and image-input paths remain on their existing routes.
|
|
205
205
|
|
|
206
|
-
Generated image downloads must use public HTTPS addresses, do not follow redirects, are DNS-checked against private and reserved addresses, and are capped at 32 MiB. DNS resolution and downloading share one deadline and respond to cancellation; a late DNS result cannot start a cancelled download. The local MCP route accepts calls only from the same device and permits at most two concurrent image operations. It requires `application/json` and rejects browser `Origin` headers; native Codex and Node clients do not send an Origin. The tool supports new-image generation at `1024x1024`, `1536x1024`, or `1024x1536`.
|
|
206
|
+
Generated image downloads must use public HTTPS addresses, do not follow redirects, are DNS-checked against private and reserved addresses, and are capped at 32 MiB. Well-known NAT64 `64:ff9b::/96` addresses apply the same IPv4 public/private rules to the embedded address, without rejecting legitimate public translations. DNS resolution and downloading share one deadline and respond to cancellation; a late DNS result cannot start a cancelled download. The local MCP route accepts calls only from the same device and permits at most two concurrent image operations. Host must name loopback, the actual local socket address (including equivalent IPv6/IPv4-mapped forms), or the explicitly configured bind hostname. Missing Host remains compatible with HTTP/1.0 and in-process clients. It requires `application/json` and rejects browser `Origin` headers; native Codex and Node clients do not send an Origin. The tool supports new-image generation at `1024x1024`, `1536x1024`, or `1024x1536`.
|
|
207
|
+
|
|
208
|
+
Image API generation/editing requests retain same-origin HTTPS redirects, with Fetch's existing POST/GET rules, a 20-hop limit and one shared timeout/cancellation signal. Cross-origin, HTTPS-to-HTTP, credential-bearing or invalid redirect targets are refused before another request is sent. Use the final API base URL when a service redirects to another origin. Redirect bodies are cancelled; generation failures are not automatically retried. The provider's returned image-download URLs still never follow redirects.
|
|
209
|
+
|
|
210
|
+
The Chat compatibility bridge ignores repeated complete tool names only when the name is declared and concatenation cannot be a fragment of another declared name. Overlapping fragments, delayed names and ambiguous/undeclared streams retain their existing assembly rules; argument deltas and tool-call IDs are not deduplicated. Native Responses routing and payloads are unchanged.
|
|
211
|
+
|
|
212
|
+
`ccdx usage` reports genuine log-read failures with the normal error line and exit code 1, without an uncaught exception dump. Missing logs remain a successful empty summary; malformed records are skipped with a warning rather than making usage unavailable.
|
|
207
213
|
|
|
208
214
|
With `qwen-image-3.0-pro` or `qwen-image-3.0` using either supported protocol, CCDX also exposes `edit_image`. Ask Codex to modify a specific CCDX-generated image; it passes that image's returned `image_id` and the edit instruction to the same configured provider. Each edit returns a new image and ID while preserving the original; omitting `size` retains the source's requested dimensions. The Qwen request contains the source image again on every edit, as required by its [single-turn image editing API](https://help.aliyun.com/zh/model-studio/qwen-image-generation-and-editing-api-reference).
|
|
209
215
|
|
package/package.json
CHANGED
package/src/adapter.mjs
CHANGED
|
@@ -6,8 +6,6 @@ import {
|
|
|
6
6
|
ADAPTER_STATUS_PATH,
|
|
7
7
|
classifyAdapterRoute,
|
|
8
8
|
createRequestMetrics,
|
|
9
|
-
isLoopbackAddress,
|
|
10
|
-
isLoopbackHostHeader,
|
|
11
9
|
runtimeStatusPayload,
|
|
12
10
|
} from "./observability.mjs";
|
|
13
11
|
import { createRequestId, runWithRequestContext } from "./request-context.mjs";
|
|
@@ -26,13 +24,14 @@ import {
|
|
|
26
24
|
} from "./cache-control.mjs";
|
|
27
25
|
import { loadRuntimeConfig, parsePositiveInteger } from "./runtime-config.mjs";
|
|
28
26
|
import {
|
|
29
|
-
createRequestAdmission,
|
|
30
27
|
createRequestAbort,
|
|
31
|
-
httpError,
|
|
32
28
|
logRequestFailure,
|
|
33
29
|
readJsonBody,
|
|
34
30
|
sendJsonError,
|
|
35
31
|
} from "./http-transport.mjs";
|
|
32
|
+
import { httpError } from "./http-errors.mjs";
|
|
33
|
+
import { createRequestAdmission } from "./request-admission.mjs";
|
|
34
|
+
import { isLoopbackAddress, isLoopbackHostHeader } from "./security.mjs";
|
|
36
35
|
|
|
37
36
|
export {
|
|
38
37
|
abortErrorStatusCode,
|
|
@@ -215,7 +214,7 @@ export function createAdapterHandler(options = {}) {
|
|
|
215
214
|
if (pathname === "/mcp/image") {
|
|
216
215
|
if (imageMcpHandler) return imageMcpHandler(req, res);
|
|
217
216
|
return import("./image-mcp.mjs").then(({ createImageMcpHandler }) => {
|
|
218
|
-
imageMcpHandler ||= createImageMcpHandler();
|
|
217
|
+
imageMcpHandler ||= createImageMcpHandler({ localHostname: options.imageMcpHostname });
|
|
219
218
|
return imageMcpHandler(req, res);
|
|
220
219
|
});
|
|
221
220
|
}
|
|
@@ -460,7 +459,7 @@ export function startAdapter(port = 2026, host = "127.0.0.1", options = {}) {
|
|
|
460
459
|
const terminalActivity = ownsTerminalActivity
|
|
461
460
|
? createTerminalActivityIndicator({ theme: options.terminalAnimationTheme })
|
|
462
461
|
: options.terminalActivity;
|
|
463
|
-
const handler = createAdapterHandler({ ...options, terminalActivity });
|
|
462
|
+
const handler = createAdapterHandler({ ...options, terminalActivity, imageMcpHostname: host });
|
|
464
463
|
const server = http.createServer(handler);
|
|
465
464
|
const cleanupTerminalActivity = () => {
|
|
466
465
|
if (ownsTerminalActivity) terminalActivity?.cleanup?.();
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import { validateGithubToken } from "./auth.mjs";
|
|
3
|
+
import { AUTH_PROFILE_CODEX, readAuthProfileCredentials } from "./auth-profile.mjs";
|
|
4
|
+
import { buildHeaders, FALLBACK_VSCODE_VERSION, parseApiBase } from "./copilot.mjs";
|
|
5
|
+
import { MAX_UPSTREAM_MODEL_CATALOG_BYTES, readBoundedResponseText } from "./http-transport.mjs";
|
|
6
|
+
import { profileRouting } from "./profile-routing.mjs";
|
|
7
|
+
|
|
8
|
+
const DEFAULT_COPILOT_CATALOG_TIMEOUT_MS = 30 * 1000;
|
|
9
|
+
|
|
10
|
+
function responseDetail(value) {
|
|
11
|
+
return String(value || "").replace(/\s+/g, " ").trim().slice(0, 240);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function abortError(signal) {
|
|
15
|
+
if (signal?.reason instanceof Error) return signal.reason;
|
|
16
|
+
const error = new Error("The operation was aborted");
|
|
17
|
+
error.name = "AbortError";
|
|
18
|
+
return error;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function catalogModels(payload) {
|
|
22
|
+
const models = Array.isArray(payload) ? payload : payload?.data;
|
|
23
|
+
if (!Array.isArray(models)) throw new Error("Copilot model catalog contained no model list");
|
|
24
|
+
return models;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function fetchCopilotCatalog(tokenData, {
|
|
28
|
+
fetchImpl,
|
|
29
|
+
signal,
|
|
30
|
+
vscodeVersion = FALLBACK_VSCODE_VERSION,
|
|
31
|
+
timeoutMs = DEFAULT_COPILOT_CATALOG_TIMEOUT_MS,
|
|
32
|
+
} = {}) {
|
|
33
|
+
const parsedTimeout = Number(timeoutMs);
|
|
34
|
+
const deadlineMs = Number.isFinite(parsedTimeout) && parsedTimeout > 0
|
|
35
|
+
? parsedTimeout
|
|
36
|
+
: DEFAULT_COPILOT_CATALOG_TIMEOUT_MS;
|
|
37
|
+
const controller = new AbortController();
|
|
38
|
+
const onCallerAbort = () => controller.abort(signal?.reason);
|
|
39
|
+
signal?.addEventListener("abort", onCallerAbort, { once: true });
|
|
40
|
+
if (signal?.aborted) onCallerAbort();
|
|
41
|
+
const timer = setTimeout(() => controller.abort(new Error(`Copilot model catalog timed out after ${deadlineMs}ms`)), deadlineMs);
|
|
42
|
+
try {
|
|
43
|
+
const apiBase = parseApiBase(tokenData);
|
|
44
|
+
const headers = buildHeaders({
|
|
45
|
+
token: tokenData.token,
|
|
46
|
+
version: vscodeVersion,
|
|
47
|
+
initiator: "user",
|
|
48
|
+
vision: false,
|
|
49
|
+
});
|
|
50
|
+
const response = await fetchImpl(`${apiBase}/models`, {
|
|
51
|
+
headers,
|
|
52
|
+
redirect: "error",
|
|
53
|
+
signal: controller.signal,
|
|
54
|
+
});
|
|
55
|
+
const modelText = await readBoundedResponseText(response, {
|
|
56
|
+
maxBytes: MAX_UPSTREAM_MODEL_CATALOG_BYTES,
|
|
57
|
+
label: "Copilot model catalog",
|
|
58
|
+
signal: controller.signal,
|
|
59
|
+
});
|
|
60
|
+
if (!response.ok) {
|
|
61
|
+
const detail = responseDetail(modelText);
|
|
62
|
+
throw new Error(`Copilot model catalog failed with HTTP ${response.status}${detail ? `: ${detail}` : ""}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let catalog;
|
|
66
|
+
try {
|
|
67
|
+
catalog = JSON.parse(modelText);
|
|
68
|
+
} catch {
|
|
69
|
+
throw new Error("Copilot model catalog returned invalid JSON");
|
|
70
|
+
}
|
|
71
|
+
catalogModels(catalog);
|
|
72
|
+
return { catalog, apiBase };
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if (signal?.aborted) throw abortError(signal);
|
|
75
|
+
if (controller.signal.aborted) throw abortError(controller.signal);
|
|
76
|
+
throw error;
|
|
77
|
+
} finally {
|
|
78
|
+
clearTimeout(timer);
|
|
79
|
+
signal?.removeEventListener("abort", onCallerAbort);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function publicProfile(credentials) {
|
|
84
|
+
return {
|
|
85
|
+
configured: credentials.configured,
|
|
86
|
+
valid: credentials.valid,
|
|
87
|
+
reason: credentials.reason,
|
|
88
|
+
login: credentials.identity?.login || "",
|
|
89
|
+
id: credentials.identity?.id || "",
|
|
90
|
+
source: "legacy",
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function readStatusCredentials(home) {
|
|
95
|
+
try {
|
|
96
|
+
return readAuthProfileCredentials(AUTH_PROFILE_CODEX, { home });
|
|
97
|
+
} catch {
|
|
98
|
+
return {
|
|
99
|
+
profile: AUTH_PROFILE_CODEX,
|
|
100
|
+
configured: true,
|
|
101
|
+
valid: false,
|
|
102
|
+
reason: "credential_read_failed",
|
|
103
|
+
token: "",
|
|
104
|
+
identity: null,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function authStatus({ home = os.homedir() } = {}) {
|
|
110
|
+
const codex = readStatusCredentials(home);
|
|
111
|
+
return {
|
|
112
|
+
profiles: { codex: publicProfile(codex) },
|
|
113
|
+
routing: profileRouting(),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function inspectProfileOnline(credentials, { fetchImpl, timeoutMs } = {}) {
|
|
118
|
+
if (!credentials.valid) {
|
|
119
|
+
return { checked: false, ok: false, reason: credentials.reason || "unconfigured" };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const controller = new AbortController();
|
|
123
|
+
const timer = setTimeout(() => controller.abort(new Error(`timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
124
|
+
try {
|
|
125
|
+
const validation = await validateGithubToken(credentials.token, {
|
|
126
|
+
fetchImpl,
|
|
127
|
+
signal: controller.signal,
|
|
128
|
+
});
|
|
129
|
+
if (controller.signal.aborted) throw abortError(controller.signal);
|
|
130
|
+
if (!validation.ok) {
|
|
131
|
+
return {
|
|
132
|
+
checked: true,
|
|
133
|
+
ok: false,
|
|
134
|
+
reason: validation.reason,
|
|
135
|
+
httpStatus: validation.status || null,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
const { catalog, apiBase } = await fetchCopilotCatalog(validation.copilotTokenData, {
|
|
139
|
+
fetchImpl,
|
|
140
|
+
signal: controller.signal,
|
|
141
|
+
});
|
|
142
|
+
let upstreamHost = "GitHub Copilot";
|
|
143
|
+
try { upstreamHost = new URL(apiBase).hostname; } catch {}
|
|
144
|
+
return {
|
|
145
|
+
checked: true,
|
|
146
|
+
ok: true,
|
|
147
|
+
login: validation.login || credentials.identity?.login || "",
|
|
148
|
+
models: catalogModels(catalog).length,
|
|
149
|
+
upstreamHost,
|
|
150
|
+
};
|
|
151
|
+
} catch (error) {
|
|
152
|
+
return {
|
|
153
|
+
checked: true,
|
|
154
|
+
ok: false,
|
|
155
|
+
reason: controller.signal.aborted ? `timed out after ${timeoutMs}ms` : error.message,
|
|
156
|
+
};
|
|
157
|
+
} finally {
|
|
158
|
+
clearTimeout(timer);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export async function authStatusOnline({
|
|
163
|
+
home = os.homedir(),
|
|
164
|
+
fetchImpl = fetch,
|
|
165
|
+
timeoutMs = 10000,
|
|
166
|
+
} = {}) {
|
|
167
|
+
const local = authStatus({ home });
|
|
168
|
+
const online = await inspectProfileOnline(readStatusCredentials(home), { fetchImpl, timeoutMs });
|
|
169
|
+
return {
|
|
170
|
+
...local,
|
|
171
|
+
profiles: {
|
|
172
|
+
codex: { ...local.profiles.codex, online },
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
}
|
package/src/cli-auth.mjs
CHANGED
|
@@ -1,15 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { validateGithubToken } from "./auth.mjs";
|
|
3
|
-
import {
|
|
4
|
-
AUTH_PROFILE_CODEX,
|
|
5
|
-
readAuthProfileCredentials,
|
|
6
|
-
} from "./auth-profile.mjs";
|
|
7
|
-
import { buildHeaders, FALLBACK_VSCODE_VERSION, parseApiBase } from "./copilot.mjs";
|
|
8
|
-
import {
|
|
9
|
-
MAX_UPSTREAM_MODEL_CATALOG_BYTES,
|
|
10
|
-
readBoundedResponseText,
|
|
11
|
-
} from "./http-transport.mjs";
|
|
12
|
-
import { profileRouting } from "./profile-routing.mjs";
|
|
1
|
+
import { authStatus, authStatusOnline } from "./auth-status.mjs";
|
|
13
2
|
import { status } from "./status.mjs";
|
|
14
3
|
import {
|
|
15
4
|
cliOutputFormat,
|
|
@@ -18,174 +7,7 @@ import {
|
|
|
18
7
|
terminalCell,
|
|
19
8
|
} from "./cli-table.mjs";
|
|
20
9
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
function responseDetail(value) {
|
|
24
|
-
return String(value || "").replace(/\s+/g, " ").trim().slice(0, 240);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function abortError(signal) {
|
|
28
|
-
if (signal?.reason instanceof Error) return signal.reason;
|
|
29
|
-
const error = new Error("The operation was aborted");
|
|
30
|
-
error.name = "AbortError";
|
|
31
|
-
return error;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function catalogModels(payload) {
|
|
35
|
-
const models = Array.isArray(payload) ? payload : payload?.data;
|
|
36
|
-
if (!Array.isArray(models)) throw new Error("Copilot model catalog contained no model list");
|
|
37
|
-
return models;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
async function fetchCopilotCatalog(tokenData, {
|
|
41
|
-
fetchImpl,
|
|
42
|
-
signal,
|
|
43
|
-
vscodeVersion = FALLBACK_VSCODE_VERSION,
|
|
44
|
-
timeoutMs = DEFAULT_COPILOT_CATALOG_TIMEOUT_MS,
|
|
45
|
-
} = {}) {
|
|
46
|
-
const parsedTimeout = Number(timeoutMs);
|
|
47
|
-
const deadlineMs = Number.isFinite(parsedTimeout) && parsedTimeout > 0
|
|
48
|
-
? parsedTimeout
|
|
49
|
-
: DEFAULT_COPILOT_CATALOG_TIMEOUT_MS;
|
|
50
|
-
const controller = new AbortController();
|
|
51
|
-
const onCallerAbort = () => controller.abort(signal?.reason);
|
|
52
|
-
signal?.addEventListener("abort", onCallerAbort, { once: true });
|
|
53
|
-
if (signal?.aborted) onCallerAbort();
|
|
54
|
-
const timer = setTimeout(() => controller.abort(new Error(`Copilot model catalog timed out after ${deadlineMs}ms`)), deadlineMs);
|
|
55
|
-
try {
|
|
56
|
-
const apiBase = parseApiBase(tokenData);
|
|
57
|
-
const headers = buildHeaders({
|
|
58
|
-
token: tokenData.token,
|
|
59
|
-
version: vscodeVersion,
|
|
60
|
-
initiator: "user",
|
|
61
|
-
vision: false,
|
|
62
|
-
});
|
|
63
|
-
const response = await fetchImpl(`${apiBase}/models`, {
|
|
64
|
-
headers,
|
|
65
|
-
redirect: "error",
|
|
66
|
-
signal: controller.signal,
|
|
67
|
-
});
|
|
68
|
-
const modelText = await readBoundedResponseText(response, {
|
|
69
|
-
maxBytes: MAX_UPSTREAM_MODEL_CATALOG_BYTES,
|
|
70
|
-
label: "Copilot model catalog",
|
|
71
|
-
signal: controller.signal,
|
|
72
|
-
});
|
|
73
|
-
if (!response.ok) {
|
|
74
|
-
const detail = responseDetail(modelText);
|
|
75
|
-
throw new Error(`Copilot model catalog failed with HTTP ${response.status}${detail ? `: ${detail}` : ""}`);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
let catalog;
|
|
79
|
-
try {
|
|
80
|
-
catalog = JSON.parse(modelText);
|
|
81
|
-
} catch {
|
|
82
|
-
throw new Error("Copilot model catalog returned invalid JSON");
|
|
83
|
-
}
|
|
84
|
-
catalogModels(catalog);
|
|
85
|
-
return { catalog, apiBase };
|
|
86
|
-
} catch (error) {
|
|
87
|
-
if (signal?.aborted) throw abortError(signal);
|
|
88
|
-
if (controller.signal.aborted) throw abortError(controller.signal);
|
|
89
|
-
throw error;
|
|
90
|
-
} finally {
|
|
91
|
-
clearTimeout(timer);
|
|
92
|
-
signal?.removeEventListener("abort", onCallerAbort);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function publicProfile(credentials) {
|
|
97
|
-
return {
|
|
98
|
-
configured: credentials.configured,
|
|
99
|
-
valid: credentials.valid,
|
|
100
|
-
reason: credentials.reason,
|
|
101
|
-
login: credentials.identity?.login || "",
|
|
102
|
-
id: credentials.identity?.id || "",
|
|
103
|
-
source: "legacy",
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function readStatusCredentials(home) {
|
|
108
|
-
try {
|
|
109
|
-
return readAuthProfileCredentials(AUTH_PROFILE_CODEX, { home });
|
|
110
|
-
} catch {
|
|
111
|
-
return {
|
|
112
|
-
profile: AUTH_PROFILE_CODEX,
|
|
113
|
-
configured: true,
|
|
114
|
-
valid: false,
|
|
115
|
-
reason: "credential_read_failed",
|
|
116
|
-
token: "",
|
|
117
|
-
identity: null,
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export function authStatus({ home = os.homedir() } = {}) {
|
|
123
|
-
const codex = readStatusCredentials(home);
|
|
124
|
-
return {
|
|
125
|
-
profiles: { codex: publicProfile(codex) },
|
|
126
|
-
routing: profileRouting(),
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
async function inspectProfileOnline(credentials, { fetchImpl, timeoutMs } = {}) {
|
|
131
|
-
if (!credentials.valid) {
|
|
132
|
-
return { checked: false, ok: false, reason: credentials.reason || "unconfigured" };
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const controller = new AbortController();
|
|
136
|
-
const timer = setTimeout(() => controller.abort(new Error(`timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
137
|
-
try {
|
|
138
|
-
const validation = await validateGithubToken(credentials.token, {
|
|
139
|
-
fetchImpl,
|
|
140
|
-
signal: controller.signal,
|
|
141
|
-
});
|
|
142
|
-
if (controller.signal.aborted) throw abortError(controller.signal);
|
|
143
|
-
if (!validation.ok) {
|
|
144
|
-
return {
|
|
145
|
-
checked: true,
|
|
146
|
-
ok: false,
|
|
147
|
-
reason: validation.reason,
|
|
148
|
-
httpStatus: validation.status || null,
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
const { catalog, apiBase } = await fetchCopilotCatalog(validation.copilotTokenData, {
|
|
152
|
-
fetchImpl,
|
|
153
|
-
signal: controller.signal,
|
|
154
|
-
});
|
|
155
|
-
let upstreamHost = "GitHub Copilot";
|
|
156
|
-
try { upstreamHost = new URL(apiBase).hostname; } catch {}
|
|
157
|
-
return {
|
|
158
|
-
checked: true,
|
|
159
|
-
ok: true,
|
|
160
|
-
login: validation.login || credentials.identity?.login || "",
|
|
161
|
-
models: catalogModels(catalog).length,
|
|
162
|
-
upstreamHost,
|
|
163
|
-
};
|
|
164
|
-
} catch (error) {
|
|
165
|
-
return {
|
|
166
|
-
checked: true,
|
|
167
|
-
ok: false,
|
|
168
|
-
reason: controller.signal.aborted ? `timed out after ${timeoutMs}ms` : error.message,
|
|
169
|
-
};
|
|
170
|
-
} finally {
|
|
171
|
-
clearTimeout(timer);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
export async function authStatusOnline({
|
|
176
|
-
home = os.homedir(),
|
|
177
|
-
fetchImpl = fetch,
|
|
178
|
-
timeoutMs = 10000,
|
|
179
|
-
} = {}) {
|
|
180
|
-
const local = authStatus({ home });
|
|
181
|
-
const online = await inspectProfileOnline(readStatusCredentials(home), { fetchImpl, timeoutMs });
|
|
182
|
-
return {
|
|
183
|
-
...local,
|
|
184
|
-
profiles: {
|
|
185
|
-
codex: { ...local.profiles.codex, online },
|
|
186
|
-
},
|
|
187
|
-
};
|
|
188
|
-
}
|
|
10
|
+
export { authStatus, authStatusOnline } from "./auth-status.mjs";
|
|
189
11
|
|
|
190
12
|
function accountLabel(profile) {
|
|
191
13
|
if (!profile.configured) return "not configured";
|
package/src/cli-models.mjs
CHANGED
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
import os from "node:os";
|
|
2
|
-
import { validateGithubToken } from "./auth.mjs";
|
|
3
|
-
import {
|
|
4
|
-
AUTH_PROFILE_CODEX,
|
|
5
|
-
readAuthProfileCredentials,
|
|
6
|
-
} from "./auth-profile.mjs";
|
|
7
|
-
import { buildHeaders, FALLBACK_VSCODE_VERSION, parseApiBase } from "./copilot.mjs";
|
|
8
1
|
import { status } from "./status.mjs";
|
|
9
2
|
import {
|
|
10
3
|
cliOutputFormat,
|
|
@@ -13,11 +6,7 @@ import {
|
|
|
13
6
|
terminalCell,
|
|
14
7
|
} from "./cli-table.mjs";
|
|
15
8
|
|
|
16
|
-
|
|
17
|
-
const SUPPORTED_ENDPOINTS = [
|
|
18
|
-
["responses", new Set(["/responses", "/v1/responses"])],
|
|
19
|
-
["chat", new Set(["/chat/completions"])],
|
|
20
|
-
];
|
|
9
|
+
export { fetchLiveCopilotModels, selectableCopilotModels } from "./live-models.mjs";
|
|
21
10
|
|
|
22
11
|
function safeText(value, fallback = "") {
|
|
23
12
|
const text = String(value ?? "")
|
|
@@ -27,125 +16,6 @@ function safeText(value, fallback = "") {
|
|
|
27
16
|
return text || fallback;
|
|
28
17
|
}
|
|
29
18
|
|
|
30
|
-
function endpointLabels(model) {
|
|
31
|
-
const advertised = new Set(Array.isArray(model?.supported_endpoints) ? model.supported_endpoints : []);
|
|
32
|
-
return SUPPORTED_ENDPOINTS
|
|
33
|
-
.filter(([, values]) => [...values].some((value) => advertised.has(value)))
|
|
34
|
-
.map(([label]) => label);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function selectableCopilotModels(payload) {
|
|
38
|
-
const data = Array.isArray(payload) ? payload : payload?.data;
|
|
39
|
-
if (!Array.isArray(data)) throw new Error("Copilot models response contained no model list");
|
|
40
|
-
|
|
41
|
-
const models = [];
|
|
42
|
-
const seen = new Set();
|
|
43
|
-
for (const model of data) {
|
|
44
|
-
if (model?.model_picker_enabled !== true) continue;
|
|
45
|
-
const policyState = safeText(model?.policy?.state).toLowerCase();
|
|
46
|
-
if (policyState && policyState !== "enabled") continue;
|
|
47
|
-
const capabilityType = safeText(model?.capabilities?.type).toLowerCase();
|
|
48
|
-
if (capabilityType && capabilityType !== "chat") continue;
|
|
49
|
-
|
|
50
|
-
const id = safeText(model?.id);
|
|
51
|
-
const endpoints = endpointLabels(model);
|
|
52
|
-
if (!id.startsWith("gpt-") || id === "codex-auto-review" || !endpoints.length || seen.has(id)) continue;
|
|
53
|
-
seen.add(id);
|
|
54
|
-
models.push({
|
|
55
|
-
id,
|
|
56
|
-
vendor: safeText(model?.vendor || model?.owned_by, "Unknown"),
|
|
57
|
-
endpoints,
|
|
58
|
-
preview: model?.preview === true,
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
models.sort((left, right) => left.vendor.localeCompare(right.vendor, "en")
|
|
63
|
-
|| left.id.localeCompare(right.id, "en"));
|
|
64
|
-
return { advertised: data.length, models };
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function validationFailure(validation) {
|
|
68
|
-
const httpStatus = validation?.status ? ` (HTTP ${validation.status})` : "";
|
|
69
|
-
return `GitHub Copilot authentication failed: ${validation?.reason || "unknown error"}${httpStatus}`;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
async function responseText(response) {
|
|
73
|
-
try {
|
|
74
|
-
return await response.text();
|
|
75
|
-
} catch {
|
|
76
|
-
return "";
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function readModelCredentials(home) {
|
|
81
|
-
let credentials;
|
|
82
|
-
try {
|
|
83
|
-
credentials = readAuthProfileCredentials(AUTH_PROFILE_CODEX, { home });
|
|
84
|
-
} catch (error) {
|
|
85
|
-
throw new Error(`Could not read the GitHub token: ${error.message}`);
|
|
86
|
-
}
|
|
87
|
-
if (credentials.valid) return credentials;
|
|
88
|
-
if (!credentials.configured) throw new Error("GitHub token not found. Start ccdx once to log in.");
|
|
89
|
-
if (credentials.reason === "empty_token") throw new Error("GitHub token file is empty. Start ccdx again to log in.");
|
|
90
|
-
throw new Error(`GitHub token is unavailable (${credentials.reason || "unknown"}). Start ccdx again to log in.`);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export async function fetchLiveCopilotModels({
|
|
94
|
-
home = os.homedir(),
|
|
95
|
-
fetchImpl = fetch,
|
|
96
|
-
timeoutMs = DEFAULT_MODELS_TIMEOUT_MS,
|
|
97
|
-
vscodeVersion = FALLBACK_VSCODE_VERSION,
|
|
98
|
-
} = {}) {
|
|
99
|
-
const credentials = readModelCredentials(home);
|
|
100
|
-
const controller = new AbortController();
|
|
101
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
102
|
-
try {
|
|
103
|
-
const validation = await validateGithubToken(credentials.token, {
|
|
104
|
-
fetchImpl,
|
|
105
|
-
signal: controller.signal,
|
|
106
|
-
});
|
|
107
|
-
if (controller.signal.aborted) throw new Error(`Live model lookup timed out after ${timeoutMs}ms`);
|
|
108
|
-
if (!validation.ok) throw new Error(validationFailure(validation));
|
|
109
|
-
|
|
110
|
-
const tokenData = validation.copilotTokenData;
|
|
111
|
-
const apiBase = parseApiBase(tokenData);
|
|
112
|
-
const headers = buildHeaders({
|
|
113
|
-
token: tokenData.token,
|
|
114
|
-
version: vscodeVersion,
|
|
115
|
-
initiator: "user",
|
|
116
|
-
vision: false,
|
|
117
|
-
});
|
|
118
|
-
const response = await fetchImpl(`${apiBase}/models`, {
|
|
119
|
-
headers,
|
|
120
|
-
redirect: "error",
|
|
121
|
-
signal: controller.signal,
|
|
122
|
-
});
|
|
123
|
-
const body = await responseText(response);
|
|
124
|
-
if (!response.ok) {
|
|
125
|
-
const detail = safeText(body).slice(0, 240);
|
|
126
|
-
throw new Error(`Copilot models endpoint returned HTTP ${response.status}${detail ? `: ${detail}` : ""}`);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
let payload;
|
|
130
|
-
try {
|
|
131
|
-
payload = JSON.parse(body);
|
|
132
|
-
} catch {
|
|
133
|
-
throw new Error("Copilot models endpoint returned invalid JSON");
|
|
134
|
-
}
|
|
135
|
-
const catalog = selectableCopilotModels(payload);
|
|
136
|
-
let upstreamHost = "GitHub Copilot";
|
|
137
|
-
try { upstreamHost = new URL(apiBase).hostname; } catch {}
|
|
138
|
-
return { ...catalog, profile: AUTH_PROFILE_CODEX, upstreamHost: safeText(upstreamHost, "GitHub Copilot") };
|
|
139
|
-
} catch (error) {
|
|
140
|
-
if (controller.signal.aborted && !/timed out/.test(error?.message || "")) {
|
|
141
|
-
throw new Error(`Live model lookup timed out after ${timeoutMs}ms`);
|
|
142
|
-
}
|
|
143
|
-
throw error;
|
|
144
|
-
} finally {
|
|
145
|
-
clearTimeout(timer);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
19
|
function liveCatalogHeader(catalog, { commandName, models }) {
|
|
150
20
|
const responses = models.filter((model) => model.endpoints.includes("responses")).length;
|
|
151
21
|
const chat = models.filter((model) => model.endpoints.includes("chat")).length;
|
package/src/cli-table.mjs
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { terminalCell } from "./terminal-text.mjs";
|
|
2
|
+
|
|
3
|
+
export { terminalCell } from "./terminal-text.mjs";
|
|
2
4
|
|
|
3
5
|
const DEFAULT_TERMINAL_COLUMNS = 120;
|
|
4
6
|
const COMBINING_MARK = /\p{Mark}/u;
|
|
@@ -50,16 +52,6 @@ function padded(value, width, align) {
|
|
|
50
52
|
return align === "right" ? `${spaces}${value}` : `${value}${spaces}`;
|
|
51
53
|
}
|
|
52
54
|
|
|
53
|
-
export function terminalCell(value, { fallback = "—" } = {}) {
|
|
54
|
-
if (value === undefined || value === null) return fallback;
|
|
55
|
-
const text = stripVTControlCharacters(String(value))
|
|
56
|
-
.replace(/[\u0000-\u001f\u007f-\u009f]/g, " ")
|
|
57
|
-
.replace(/[\u061c\u200b\u200e\u200f\u202a-\u202e\u2060\u2066-\u2069\ufeff]/g, "")
|
|
58
|
-
.replace(/\s+/g, " ")
|
|
59
|
-
.trim();
|
|
60
|
-
return text || fallback;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
55
|
export function cliOutputFormat(format = "auto", output = process.stdout) {
|
|
64
56
|
if (format === "table" || format === "plain") return format;
|
|
65
57
|
return output?.isTTY === true ? "table" : "plain";
|
|
@@ -5,10 +5,10 @@ import {
|
|
|
5
5
|
sanitizeImageNamespaceCollisionRequest,
|
|
6
6
|
} from "./copilot-responses-policy.mjs";
|
|
7
7
|
import {
|
|
8
|
-
httpError,
|
|
9
8
|
MAX_UPSTREAM_ERROR_BODY_BYTES,
|
|
10
9
|
readBoundedResponseText,
|
|
11
10
|
} from "./http-transport.mjs";
|
|
11
|
+
import { httpError } from "./http-errors.mjs";
|
|
12
12
|
import {
|
|
13
13
|
finalizeEncryptedHistoryRebase,
|
|
14
14
|
sanitizeEncryptedReasoningRequest,
|
package/src/dashboard-api.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import os from "node:os";
|
|
2
|
-
import { fetchLiveCopilotModels } from "./
|
|
3
|
-
import { authStatus } from "./
|
|
2
|
+
import { fetchLiveCopilotModels } from "./live-models.mjs";
|
|
3
|
+
import { authStatus } from "./auth-status.mjs";
|
|
4
4
|
import { redactDiagnosticText } from "./diagnostic-text.mjs";
|
|
5
5
|
import { createRequestAbort, readJsonBody } from "./http-transport.mjs";
|
|
6
|
-
import { isLoopbackAddress, isLoopbackHostHeader } from "./
|
|
6
|
+
import { isLoopbackAddress, isLoopbackHostHeader } from "./security.mjs";
|
|
7
7
|
import {
|
|
8
8
|
DEFAULT_TERMINAL_ANIMATION_THEME,
|
|
9
9
|
getTerminalAnimationFrameDelay,
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
renderTerminalAnimationFrame,
|
|
12
12
|
TERMINAL_ANIMATION_THEMES,
|
|
13
13
|
} from "./terminal-animation.mjs";
|
|
14
|
-
import { cacheReadTokens, summarizeUsageLogs, usageCacheHitRate } from "./usage.mjs";
|
|
14
|
+
import { cacheReadTokens, summarizeUsageLogs, usageCacheHitRate } from "./usage-store.mjs";
|
|
15
15
|
import { createUsageAnalytics } from "./usage-analytics.mjs";
|
|
16
16
|
import { readUserSettings, terminalAnimationPreference, writeTerminalAnimationTheme } from "./user-settings.mjs";
|
|
17
17
|
|