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 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
- await printUsageSummary({
79
- format: CLI.outputFormat || "auto",
80
- output: process.stdout,
81
- });
82
- process.exit(0);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-copilot-dx",
3
- "version": "0.9.16",
3
+ "version": "0.9.18",
4
4
  "description": "Use Codex App with GitHub Copilot",
5
5
  "author": "Dale Xiao",
6
6
  "type": "module",
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 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
- 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
- const DEFAULT_COPILOT_CATALOG_TIMEOUT_MS = 30 * 1000;
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";
@@ -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
- const DEFAULT_MODELS_TIMEOUT_MS = 10000;
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 { stripVTControlCharacters } from "node:util";
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,
@@ -1,9 +1,9 @@
1
1
  import os from "node:os";
2
- import { fetchLiveCopilotModels } from "./cli-models.mjs";
3
- import { authStatus } from "./cli-auth.mjs";
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 "./observability.mjs";
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