codex-copilot-dx 0.9.17 → 0.9.19
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 +4 -2
- package/docs/README.md +1 -0
- package/docs/REFERENCE.md +5 -1
- package/package.json +1 -1
- package/src/adapter.mjs +3 -4
- package/src/auth-status.mjs +175 -0
- package/src/cli-auth.mjs +2 -180
- package/src/cli-models.mjs +1 -131
- package/src/cli-options.mjs +1 -1
- 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 +1 -1
- package/src/live-models.mjs +137 -0
- package/src/observability.mjs +2 -22
- package/src/package-update.mjs +19 -7
- package/src/request-admission.mjs +348 -0
- package/src/response-failures.mjs +1 -1
- package/src/response-history.mjs +1 -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/src/version.mjs +57 -7
package/README.md
CHANGED
|
@@ -68,6 +68,8 @@ The dashboard does not poll in the background; refresh when needed.
|
|
|
68
68
|
expose it to the public internet. The dashboard is not an authentication wall
|
|
69
69
|
for the API. Diagnostic errors may contain sensitive upstream text.
|
|
70
70
|
|
|
71
|
+
Startup checks GitHub Releases, not npm, and suggests `ccdx update github` when a newer stable version is available. GitHub updates install the exact release tag; dependency downloads still use npm's configured registry.
|
|
72
|
+
|
|
71
73
|
See the [complete command/configuration reference](https://github.com/DaleXiao/codex-copilot-dx/blob/main/docs/REFERENCE.md)
|
|
72
74
|
for authentication recovery, configuration-write boundaries, advanced limits,
|
|
73
75
|
logging, image setup and client compatibility details.
|
package/bin/cli.mjs
CHANGED
|
@@ -307,7 +307,7 @@ async function printUpdateNotice() {
|
|
|
307
307
|
const { latestVersion, updateAvailable } = await checkForUpdate({ currentVersion: LOCAL_VERSION });
|
|
308
308
|
if (!updateAvailable) return;
|
|
309
309
|
console.log(`\n ${status("warn", `Update available: ${LOCAL_VERSION} -> ${latestVersion}`)}`);
|
|
310
|
-
console.log(
|
|
310
|
+
console.log(` Run ${CLI_NAME} update github to update, then restart ${CLI_NAME}.\n`);
|
|
311
311
|
} catch {
|
|
312
312
|
// Never block startup on the update check.
|
|
313
313
|
}
|
|
@@ -315,8 +315,10 @@ async function printUpdateNotice() {
|
|
|
315
315
|
|
|
316
316
|
try {
|
|
317
317
|
assertSafeAdapterHost(ADAPTER_HOST, process.env);
|
|
318
|
-
|
|
318
|
+
const updateNotice = runInBackground(printUpdateNotice);
|
|
319
319
|
if (await reuseRunningAdapterIfAvailable()) {
|
|
320
|
+
// Ready is already displayed; finish the bounded check before this short-lived CLI exits.
|
|
321
|
+
await updateNotice;
|
|
320
322
|
await LOGGING.cleanup();
|
|
321
323
|
process.exit(0);
|
|
322
324
|
}
|
package/docs/README.md
CHANGED
|
@@ -15,6 +15,7 @@ These reports record a specific release and test environment. Their version,
|
|
|
15
15
|
test counts, timings, live-provider observations, and limitations remain
|
|
16
16
|
historical facts rather than current guarantees.
|
|
17
17
|
|
|
18
|
+
- [0.9.19: GitHub startup checks and stable-release updates](RELEASE_0.9.19.md)
|
|
18
19
|
- [0.9.13: model-bound Auto-review reasoning selection](RELEASE_0.9.13.md)
|
|
19
20
|
- [0.9.12: GPT-6.1 Sol / low Auto-review default](RELEASE_0.9.12.md)
|
|
20
21
|
- [0.9.11: bounded usage-summary cache and diagnostic redaction](RELEASE_0.9.11.md)
|
package/docs/REFERENCE.md
CHANGED
|
@@ -230,7 +230,11 @@ ccdx update npm
|
|
|
230
230
|
ccdx update github
|
|
231
231
|
```
|
|
232
232
|
|
|
233
|
-
|
|
233
|
+
Normal startup checks the latest stable GitHub Release once in the background, without accessing the npm registry. The anonymous check requires no PAT or Copilot credentials, has a two-second deadline and a bounded response body, and silently skips network errors, rate limits and invalid metadata. It never falls back to npm, retries, polls, installs an update or restarts the service. When a newer release is found, it prints the versions and `ccdx update github`. On adapter reuse, Ready is displayed before waiting for any remaining check budget so the short-lived command does not lose its notification.
|
|
234
|
+
|
|
235
|
+
`ccdx update gh` is accepted as shorthand for the GitHub source. The npm source installs `codex-copilot-dx@latest` through the registry already configured for npm, including a company mirror. The GitHub source queries the latest stable published Release (ten-second deadline), validates its version tag, and installs that exact tag from `DaleXiao/codex-copilot-dx`, not `main`. Lookup failure stops before installation, without an npm fallback. It opts in to Git fetching only for that command and does not change npm's persistent `allow-git` setting.
|
|
236
|
+
|
|
237
|
+
GitHub is the package source, not a replacement for the npm installer. Dependencies such as `sharp` may still need the configured npm registry during an explicit update; use a company-approved mirror/cache where required. GitHub updates do not promise an entirely registry-free installation.
|
|
234
238
|
|
|
235
239
|
If a configured npm mirror has not synchronized the current release yet, use the GitHub source. A running adapter keeps its loaded version until it is stopped and restarted.
|
|
236
240
|
|
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,
|
|
@@ -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-options.mjs
CHANGED
|
@@ -356,7 +356,7 @@ function topicHelp(name, topic) {
|
|
|
356
356
|
"disable-image": `Usage:\n ${name} disable-image\n\nDisables the local CCDX image tool and removes its stored API credential and unmodified CCDX image guidance.`,
|
|
357
357
|
"image-status": `Usage:\n ${name} image-status\n\nShows provider configuration and probes local image-tool readiness without generating an image or exposing its API key.`,
|
|
358
358
|
"auto-review-model": `Usage:\n ${name} auto-review-model\n\nSelect a Responses model, then an advertised reasoning effort. Enter keeps the default/current choice; q cancels both steps without saving. Effort 0 follows the client (low when omitted). Settings apply only to Auto-review on the next request; no client patch or restart is required for adapters running 0.9.13 or newer.`,
|
|
359
|
-
update: `Usage:\n ${name} update [npm|github]\n\nUpdates the global package from the configured npm registry or GitHub
|
|
359
|
+
update: `Usage:\n ${name} update [npm|github]\n\nUpdates the global package from the configured npm registry or the latest stable GitHub release. With no source, an interactive terminal prompts for one.`,
|
|
360
360
|
version: `Usage:\n ${name} --version`,
|
|
361
361
|
};
|
|
362
362
|
return sections[topic];
|
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
|
|