codex-copilot-dx 0.4.23 → 0.4.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -1
- package/bin/cli.mjs +52 -12
- package/package.json +1 -1
- package/src/adapter.mjs +468 -82
- package/src/auth.mjs +148 -7
- package/src/copilot.mjs +9 -4
- package/src/doctor.mjs +82 -7
- package/src/model-cache.mjs +45 -0
- package/src/running-adapter.mjs +22 -0
package/src/auth.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
5
6
|
import { status } from "./status.mjs";
|
|
6
7
|
import { withFileLock } from "./lock.mjs";
|
|
7
8
|
|
|
@@ -22,6 +23,10 @@ export function githubTokenLockPath(home = os.homedir()) {
|
|
|
22
23
|
return `${githubTokenPath(home)}.lock`;
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
export function githubTokenMetadataPath(home = os.homedir()) {
|
|
27
|
+
return `${githubTokenPath(home)}.account.json`;
|
|
28
|
+
}
|
|
29
|
+
|
|
25
30
|
export function githubReauthMessage(reason, home = os.homedir()) {
|
|
26
31
|
const tokenPath = githubTokenPath(home);
|
|
27
32
|
return `${reason}
|
|
@@ -210,6 +215,73 @@ function readSavedGithubToken(home = os.homedir()) {
|
|
|
210
215
|
return fs.readFileSync(filePath, "utf8").trim();
|
|
211
216
|
}
|
|
212
217
|
|
|
218
|
+
function normalizeGithubIdentity(identity) {
|
|
219
|
+
if (!identity || typeof identity !== "object") return null;
|
|
220
|
+
const login = typeof identity.login === "string" ? identity.login.trim() : "";
|
|
221
|
+
const id = identity.id === undefined || identity.id === null ? "" : String(identity.id).trim();
|
|
222
|
+
if (!login && !id) return null;
|
|
223
|
+
return { login, id };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function tokenFingerprint(token) {
|
|
227
|
+
return createHash("sha256").update(String(token || "")).digest("hex").slice(0, 24);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function readGithubTokenMetadata(home = os.homedir(), token = null) {
|
|
231
|
+
try {
|
|
232
|
+
const parsed = JSON.parse(fs.readFileSync(githubTokenMetadataPath(home), "utf8"));
|
|
233
|
+
if (token && parsed.token_fingerprint !== tokenFingerprint(token)) return null;
|
|
234
|
+
const identity = normalizeGithubIdentity(parsed);
|
|
235
|
+
return identity ? { ...identity, token_fingerprint: parsed.token_fingerprint || "" } : null;
|
|
236
|
+
} catch {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function githubIdentityMatches(identity, expected) {
|
|
242
|
+
const actual = normalizeGithubIdentity(identity);
|
|
243
|
+
const wanted = normalizeGithubIdentity(expected);
|
|
244
|
+
if (!wanted) return true;
|
|
245
|
+
if (!actual) return false;
|
|
246
|
+
if (wanted.id && actual.id) return wanted.id === actual.id;
|
|
247
|
+
return Boolean(wanted.login && actual.login && wanted.login.toLowerCase() === actual.login.toLowerCase());
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function expectedGithubIdentity(home, env, token) {
|
|
251
|
+
const configuredLogin = String(env.CCDX_GITHUB_LOGIN || "").trim();
|
|
252
|
+
if (configuredLogin) return { login: configuredLogin };
|
|
253
|
+
return token ? readGithubTokenMetadata(home, token) : null;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export async function fetchGithubIdentity(token, { fetchImpl = fetch, signal } = {}) {
|
|
257
|
+
if (typeof token !== "string" || !token.trim()) return { ok: false, reason: "empty_token" };
|
|
258
|
+
try {
|
|
259
|
+
const resp = await fetchImpl(`${GITHUB_API}/user`, {
|
|
260
|
+
headers: { Authorization: `token ${token.trim()}`, Accept: "application/json" },
|
|
261
|
+
signal,
|
|
262
|
+
});
|
|
263
|
+
if (!resp.ok) return { ok: false, status: resp.status, reason: "github_user_failed" };
|
|
264
|
+
const data = await resp.json();
|
|
265
|
+
const identity = normalizeGithubIdentity(data);
|
|
266
|
+
return identity ? { ok: true, ...identity } : { ok: false, reason: "github_identity_missing" };
|
|
267
|
+
} catch (error) {
|
|
268
|
+
return { ok: false, transient: true, reason: "github_user_request_failed", error };
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export async function ensureGithubTokenMetadata(token, {
|
|
273
|
+
home = os.homedir(),
|
|
274
|
+
fetchImpl = fetch,
|
|
275
|
+
signal,
|
|
276
|
+
} = {}) {
|
|
277
|
+
const existing = readGithubTokenMetadata(home, token);
|
|
278
|
+
if (existing) return existing;
|
|
279
|
+
const identity = await fetchGithubIdentity(token, { fetchImpl, signal });
|
|
280
|
+
if (!identity.ok) return null;
|
|
281
|
+
writeGithubTokenMetadata(identity, home, token);
|
|
282
|
+
return normalizeGithubIdentity(identity);
|
|
283
|
+
}
|
|
284
|
+
|
|
213
285
|
export async function validateGithubToken(token, {
|
|
214
286
|
fetchImpl = fetch,
|
|
215
287
|
signal,
|
|
@@ -239,6 +311,7 @@ export async function validateGithubToken(token, {
|
|
|
239
311
|
userData = await userResp.json();
|
|
240
312
|
} catch {}
|
|
241
313
|
const login = typeof userData.login === "string" ? userData.login : "";
|
|
314
|
+
const id = userData.id === undefined || userData.id === null ? "" : String(userData.id);
|
|
242
315
|
|
|
243
316
|
let copilotResp;
|
|
244
317
|
try {
|
|
@@ -267,7 +340,7 @@ export async function validateGithubToken(token, {
|
|
|
267
340
|
return { ok: false, reason: "copilot_token_missing", login };
|
|
268
341
|
}
|
|
269
342
|
|
|
270
|
-
return { ok: true, login, copilotTokenData };
|
|
343
|
+
return { ok: true, login, id, copilotTokenData };
|
|
271
344
|
}
|
|
272
345
|
|
|
273
346
|
export async function discoverGithubToken({
|
|
@@ -276,10 +349,13 @@ export async function discoverGithubToken({
|
|
|
276
349
|
fetchImpl = fetch,
|
|
277
350
|
signal,
|
|
278
351
|
excludeTokens = [],
|
|
352
|
+
expectedIdentity,
|
|
353
|
+
strictExpectedIdentity = false,
|
|
279
354
|
} = {}) {
|
|
280
355
|
const excluded = new Set(excludeTokens);
|
|
281
356
|
const seen = new Set(excluded);
|
|
282
357
|
const failures = [];
|
|
358
|
+
const candidates = [];
|
|
283
359
|
|
|
284
360
|
for (const source of githubTokenSources({ home, env })) {
|
|
285
361
|
let token = "";
|
|
@@ -294,10 +370,40 @@ export async function discoverGithubToken({
|
|
|
294
370
|
seen.add(token);
|
|
295
371
|
|
|
296
372
|
const validation = await validateGithubToken(token, { fetchImpl, signal });
|
|
297
|
-
if (validation.ok)
|
|
373
|
+
if (validation.ok) {
|
|
374
|
+
const candidate = { ok: true, token, source, validation };
|
|
375
|
+
const explicitSource = source.type === "env" || source.type === "token-file";
|
|
376
|
+
if (expectedIdentity && (strictExpectedIdentity || !explicitSource)) {
|
|
377
|
+
if (githubIdentityMatches(validation, expectedIdentity)) return candidate;
|
|
378
|
+
failures.push({ source, validation, reason: "github_account_mismatch" });
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
if (explicitSource) return candidate;
|
|
382
|
+
candidates.push(candidate);
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
298
385
|
failures.push({ source, validation });
|
|
299
386
|
}
|
|
300
387
|
|
|
388
|
+
if (candidates.length) {
|
|
389
|
+
const identities = new Set(candidates.map(({ validation, token }) => {
|
|
390
|
+
if (validation.id) return `id:${validation.id}`;
|
|
391
|
+
if (validation.login) return `login:${validation.login.toLowerCase()}`;
|
|
392
|
+
return `token:${token}`;
|
|
393
|
+
}));
|
|
394
|
+
if (identities.size === 1) return candidates[0];
|
|
395
|
+
return {
|
|
396
|
+
ok: false,
|
|
397
|
+
ambiguous: true,
|
|
398
|
+
candidates: candidates.map(({ source, validation }) => ({
|
|
399
|
+
source,
|
|
400
|
+
login: validation.login,
|
|
401
|
+
id: validation.id,
|
|
402
|
+
})),
|
|
403
|
+
failures,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
301
407
|
return { ok: false, failures };
|
|
302
408
|
}
|
|
303
409
|
|
|
@@ -314,19 +420,34 @@ export async function importDiscoveredGithubToken({
|
|
|
314
420
|
const run = async () => {
|
|
315
421
|
const excluded = new Set(excludeTokens);
|
|
316
422
|
const savedToken = readSavedGithubToken(home);
|
|
423
|
+
const expectedIdentity = expectedGithubIdentity(home, env, savedToken);
|
|
424
|
+
const strictExpectedIdentity = Boolean(String(env.CCDX_GITHUB_LOGIN || "").trim());
|
|
317
425
|
if (savedToken && !excluded.has(savedToken)) {
|
|
318
426
|
if (!validateSavedToken) {
|
|
319
427
|
return { ok: true, token: savedToken, source: { type: "saved-token" }, imported: false };
|
|
320
428
|
}
|
|
321
429
|
const validation = await validateGithubToken(savedToken, { fetchImpl, signal });
|
|
322
|
-
if (validation.ok) {
|
|
430
|
+
if (validation.ok && githubIdentityMatches(validation, expectedIdentity)) {
|
|
431
|
+
writeGithubTokenMetadata(validation, home, savedToken);
|
|
323
432
|
return { ok: true, token: savedToken, source: { type: "saved-token" }, validation, imported: false };
|
|
324
433
|
}
|
|
325
434
|
}
|
|
326
435
|
|
|
327
|
-
const discovered = await discoverGithubToken({
|
|
436
|
+
const discovered = await discoverGithubToken({
|
|
437
|
+
home,
|
|
438
|
+
env,
|
|
439
|
+
fetchImpl,
|
|
440
|
+
signal,
|
|
441
|
+
excludeTokens,
|
|
442
|
+
expectedIdentity,
|
|
443
|
+
strictExpectedIdentity,
|
|
444
|
+
});
|
|
445
|
+
if (discovered.ambiguous) {
|
|
446
|
+
const logins = [...new Set(discovered.candidates.map((candidate) => candidate.login || candidate.id || "unknown"))];
|
|
447
|
+
throw new Error(`Multiple GitHub Copilot accounts were found (${logins.join(", ")}). Set CCDX_GITHUB_LOGIN or CCDX_GITHUB_TOKEN_PATH to select one explicitly.`);
|
|
448
|
+
}
|
|
328
449
|
if (!discovered.ok) return null;
|
|
329
|
-
writeToken(discovered.token, home);
|
|
450
|
+
writeToken(discovered.token, home, discovered.validation);
|
|
330
451
|
const login = discovered.validation.login ? ` for ${discovered.validation.login}` : "";
|
|
331
452
|
log(status("ok", `Imported GitHub token from ${sourceDescription(discovered.source)}${login}`));
|
|
332
453
|
return { ...discovered, imported: true };
|
|
@@ -421,7 +542,8 @@ export async function ensureAuth({
|
|
|
421
542
|
const data = await pollResp.json();
|
|
422
543
|
const r = interpretPoll(data);
|
|
423
544
|
if (r.state === "done") {
|
|
424
|
-
|
|
545
|
+
const identity = await fetchGithubIdentity(r.token, { fetchImpl, signal });
|
|
546
|
+
writeToken(r.token, home, identity.ok ? identity : null);
|
|
425
547
|
log(status("ok", "Login successful"));
|
|
426
548
|
return;
|
|
427
549
|
}
|
|
@@ -432,9 +554,28 @@ export async function ensureAuth({
|
|
|
432
554
|
}, tokenLockOptions(env));
|
|
433
555
|
}
|
|
434
556
|
|
|
435
|
-
export function
|
|
557
|
+
export function writeGithubTokenMetadata(identity, home = os.homedir(), token = "") {
|
|
558
|
+
const normalized = normalizeGithubIdentity(identity);
|
|
559
|
+
if (!normalized) return false;
|
|
560
|
+
const filePath = githubTokenMetadataPath(home);
|
|
561
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
562
|
+
fs.writeFileSync(filePath, `${JSON.stringify({
|
|
563
|
+
...normalized,
|
|
564
|
+
token_fingerprint: tokenFingerprint(token),
|
|
565
|
+
updated_at: new Date().toISOString(),
|
|
566
|
+
}, null, 2)}\n`, { mode: 0o600 });
|
|
567
|
+
fs.chmodSync(filePath, 0o600);
|
|
568
|
+
return true;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
export function writeToken(token, home = os.homedir(), identity = null) {
|
|
436
572
|
const GITHUB_TOKEN_PATH = githubTokenPath(home);
|
|
437
573
|
fs.mkdirSync(path.dirname(GITHUB_TOKEN_PATH), { recursive: true });
|
|
438
574
|
fs.writeFileSync(GITHUB_TOKEN_PATH, token, { mode: 0o600 });
|
|
439
575
|
fs.chmodSync(GITHUB_TOKEN_PATH, 0o600);
|
|
576
|
+
if (normalizeGithubIdentity(identity)) {
|
|
577
|
+
writeGithubTokenMetadata(identity, home, token);
|
|
578
|
+
} else {
|
|
579
|
+
try { fs.unlinkSync(githubTokenMetadataPath(home)); } catch (e) { if (e?.code !== "ENOENT") throw e; }
|
|
580
|
+
}
|
|
440
581
|
}
|
package/src/copilot.mjs
CHANGED
|
@@ -3,24 +3,28 @@ import os from "node:os";
|
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import { status } from "./status.mjs";
|
|
5
5
|
import { debugLog } from "./log.mjs";
|
|
6
|
-
import { githubReauthMessage, githubTokenPath, importDiscoveredGithubToken } from "./auth.mjs";
|
|
6
|
+
import { ensureGithubTokenMetadata, githubReauthMessage, githubTokenPath, importDiscoveredGithubToken } from "./auth.mjs";
|
|
7
7
|
|
|
8
8
|
export const DEFAULT_API_BASE = "https://api.githubcopilot.com";
|
|
9
9
|
let apiBase = DEFAULT_API_BASE;
|
|
10
10
|
|
|
11
11
|
// Cache of model id -> supported_endpoints, populated from listModels(). Used to
|
|
12
12
|
// route requests to /responses vs /chat/completions based on real model metadata.
|
|
13
|
-
|
|
13
|
+
let modelEndpointCache = new Map();
|
|
14
14
|
|
|
15
15
|
export function cacheModelEndpoints(models) {
|
|
16
16
|
const data = Array.isArray(models) ? models : models?.data;
|
|
17
|
-
if (!Array.isArray(data)) return;
|
|
17
|
+
if (!Array.isArray(data)) return false;
|
|
18
|
+
const next = new Map();
|
|
18
19
|
for (const model of data) {
|
|
19
20
|
const id = String(model?.id || "").trim();
|
|
20
21
|
if (id && Array.isArray(model?.supported_endpoints)) {
|
|
21
|
-
|
|
22
|
+
next.set(id, [...model.supported_endpoints]);
|
|
22
23
|
}
|
|
23
24
|
}
|
|
25
|
+
if (!next.size) return false;
|
|
26
|
+
modelEndpointCache = next;
|
|
27
|
+
return true;
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
// Returns the cached supported_endpoints for a model id, or null if unknown.
|
|
@@ -463,6 +467,7 @@ async function refreshCopilotToken({
|
|
|
463
467
|
throw new Error(`Failed to get Copilot token: ${resp.status}`);
|
|
464
468
|
}
|
|
465
469
|
const data = await resp.json();
|
|
470
|
+
await ensureGithubTokenMetadata(ghToken, { home, fetchImpl, signal });
|
|
466
471
|
return cacheCopilotTokenData(data);
|
|
467
472
|
}
|
|
468
473
|
|
package/src/doctor.mjs
CHANGED
|
@@ -2,9 +2,11 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import net from "node:net";
|
|
5
|
-
import { githubTokenPath } from "./auth.mjs";
|
|
5
|
+
import { githubTokenPath, readGithubTokenMetadata, validateGithubToken } from "./auth.mjs";
|
|
6
6
|
import { claudeDesktopPaths } from "./claude-desktop-config.mjs";
|
|
7
7
|
import { status } from "./status.mjs";
|
|
8
|
+
import { buildHeaders, DEFAULT_API_BASE, FALLBACK_VSCODE_VERSION } from "./copilot.mjs";
|
|
9
|
+
import { checkRunningAdapter } from "./running-adapter.mjs";
|
|
8
10
|
|
|
9
11
|
function localGatewayBaseUrl(host, port) {
|
|
10
12
|
const safeHost = String(host || "127.0.0.1");
|
|
@@ -55,7 +57,60 @@ export function inspectGitHubToken({ home = os.homedir() } = {}) {
|
|
|
55
57
|
if (!token.text.trim()) {
|
|
56
58
|
return [{ kind: "err", message: `GitHub token file is empty at ${displayPath(home, filePath)}` }];
|
|
57
59
|
}
|
|
58
|
-
|
|
60
|
+
const metadata = readGithubTokenMetadata(home, token.text.trim());
|
|
61
|
+
const account = metadata?.login ? ` for ${metadata.login}` : "";
|
|
62
|
+
return [{ kind: "ok", message: `GitHub token found${account}` }];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function inspectGitHubTokenOnline({
|
|
66
|
+
home = os.homedir(),
|
|
67
|
+
fetchImpl = fetch,
|
|
68
|
+
timeoutMs = 10000,
|
|
69
|
+
} = {}) {
|
|
70
|
+
const token = readText(githubTokenPath(home));
|
|
71
|
+
if (!token.ok || !token.text.trim()) {
|
|
72
|
+
return [{ kind: "warn", message: "Online Copilot check skipped because the GitHub token is missing" }];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const controller = new AbortController();
|
|
76
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
77
|
+
try {
|
|
78
|
+
const validation = await validateGithubToken(token.text.trim(), {
|
|
79
|
+
fetchImpl,
|
|
80
|
+
signal: controller.signal,
|
|
81
|
+
});
|
|
82
|
+
if (!validation.ok) {
|
|
83
|
+
const statusText = validation.status ? ` (HTTP ${validation.status})` : "";
|
|
84
|
+
return [{ kind: validation.transient ? "warn" : "err", message: `GitHub Copilot authentication failed: ${validation.reason}${statusText}` }];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const tokenData = validation.copilotTokenData;
|
|
88
|
+
const apiBase = tokenData.endpoints?.api || DEFAULT_API_BASE;
|
|
89
|
+
const headers = buildHeaders({
|
|
90
|
+
token: tokenData.token,
|
|
91
|
+
version: FALLBACK_VSCODE_VERSION,
|
|
92
|
+
initiator: "user",
|
|
93
|
+
vision: false,
|
|
94
|
+
});
|
|
95
|
+
const modelResp = await fetchImpl(`${apiBase}/models`, { headers, signal: controller.signal });
|
|
96
|
+
if (!modelResp.ok) {
|
|
97
|
+
return [
|
|
98
|
+
{ kind: "ok", message: `GitHub Copilot access verified for ${validation.login || "current account"}` },
|
|
99
|
+
{ kind: "err", message: `Copilot models endpoint returned HTTP ${modelResp.status}` },
|
|
100
|
+
];
|
|
101
|
+
}
|
|
102
|
+
const models = await modelResp.json();
|
|
103
|
+
const data = Array.isArray(models) ? models : models?.data;
|
|
104
|
+
return [
|
|
105
|
+
{ kind: "ok", message: `GitHub Copilot access verified for ${validation.login || "current account"}` },
|
|
106
|
+
{ kind: "ok", message: `Copilot models endpoint returned ${Array.isArray(data) ? data.length : 0} models` },
|
|
107
|
+
];
|
|
108
|
+
} catch (e) {
|
|
109
|
+
const reason = e?.name === "AbortError" ? `timed out after ${timeoutMs}ms` : e.message;
|
|
110
|
+
return [{ kind: "warn", message: `Online Copilot check failed: ${reason}` }];
|
|
111
|
+
} finally {
|
|
112
|
+
clearTimeout(timer);
|
|
113
|
+
}
|
|
59
114
|
}
|
|
60
115
|
|
|
61
116
|
export function inspectCodexConfig({ home = os.homedir(), port = 2026 } = {}) {
|
|
@@ -180,6 +235,10 @@ export async function collectDoctorChecks({
|
|
|
180
235
|
port = 2026,
|
|
181
236
|
checkAdapter = true,
|
|
182
237
|
checkAdapterListeningFn = checkAdapterListening,
|
|
238
|
+
checkRunningAdapterFn = checkRunningAdapter,
|
|
239
|
+
online = false,
|
|
240
|
+
fetchImpl = fetch,
|
|
241
|
+
onlineTimeoutMs = 10000,
|
|
183
242
|
} = {}) {
|
|
184
243
|
const checks = [
|
|
185
244
|
...inspectGitHubToken({ home }),
|
|
@@ -188,11 +247,27 @@ export async function collectDoctorChecks({
|
|
|
188
247
|
...inspectClaudeAppConfig({ home, platform, env, host, port }),
|
|
189
248
|
];
|
|
190
249
|
|
|
250
|
+
if (online) {
|
|
251
|
+
checks.push(...await inspectGitHubTokenOnline({ home, fetchImpl, timeoutMs: onlineTimeoutMs }));
|
|
252
|
+
}
|
|
253
|
+
|
|
191
254
|
if (checkAdapter) {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
255
|
+
let running;
|
|
256
|
+
try {
|
|
257
|
+
running = await checkRunningAdapterFn({ host, port, fetchImpl });
|
|
258
|
+
} catch {
|
|
259
|
+
running = null;
|
|
260
|
+
}
|
|
261
|
+
if (running?.ok) {
|
|
262
|
+
checks.push({ kind: "ok", message: `Adapter ${running.data.version} is listening on ${running.baseUrl}` });
|
|
263
|
+
} else if (running?.incompatible) {
|
|
264
|
+
checks.push({ kind: "warn", message: `Adapter ${running.data?.version || "legacy"} is running at ${running.baseUrl}, but it is incompatible with this CLI` });
|
|
265
|
+
} else {
|
|
266
|
+
const listening = await checkAdapterListeningFn({ host, port });
|
|
267
|
+
checks.push(listening
|
|
268
|
+
? { kind: "warn", message: `A service is listening on ${localGatewayBaseUrl(host, port)}, but it is not a compatible codex-copilot-dx adapter` }
|
|
269
|
+
: { kind: "warn", message: `Adapter is not listening on ${localGatewayBaseUrl(host, port)}` });
|
|
270
|
+
}
|
|
196
271
|
}
|
|
197
272
|
|
|
198
273
|
return checks;
|
|
@@ -200,7 +275,7 @@ export async function collectDoctorChecks({
|
|
|
200
275
|
|
|
201
276
|
export async function runDoctor(options = {}) {
|
|
202
277
|
const log = options.log || console.log;
|
|
203
|
-
log(
|
|
278
|
+
log(`codex-copilot-dx doctor${options.online ? " --online" : ""}`);
|
|
204
279
|
const checks = await collectDoctorChecks(options);
|
|
205
280
|
for (const check of checks) log(status(check.kind, check.message));
|
|
206
281
|
return checks;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
6
|
+
|
|
7
|
+
export function modelCachePath(home = os.homedir()) {
|
|
8
|
+
return path.join(home, ".local", "share", "codex-copilot-dx", "models.json");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function modelData(models) {
|
|
12
|
+
const data = Array.isArray(models) ? models : models?.data;
|
|
13
|
+
return Array.isArray(data) ? data : null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function loadModelCache({
|
|
17
|
+
home = os.homedir(),
|
|
18
|
+
maxAgeMs = DEFAULT_MAX_AGE_MS,
|
|
19
|
+
now = Date.now,
|
|
20
|
+
} = {}) {
|
|
21
|
+
try {
|
|
22
|
+
const parsed = JSON.parse(fs.readFileSync(modelCachePath(home), "utf8"));
|
|
23
|
+
const savedAt = Date.parse(parsed.saved_at);
|
|
24
|
+
if (!Number.isFinite(savedAt) || now() - savedAt > maxAgeMs) return null;
|
|
25
|
+
if (!modelData(parsed.models)) return null;
|
|
26
|
+
return parsed.models;
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function saveModelCache(models, { home = os.homedir() } = {}) {
|
|
33
|
+
if (!modelData(models)) return false;
|
|
34
|
+
const filePath = modelCachePath(home);
|
|
35
|
+
const tempPath = `${filePath}.${process.pid}.tmp`;
|
|
36
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
37
|
+
try {
|
|
38
|
+
fs.writeFileSync(tempPath, `${JSON.stringify({ saved_at: new Date().toISOString(), models })}\n`, { mode: 0o600 });
|
|
39
|
+
fs.renameSync(tempPath, filePath);
|
|
40
|
+
fs.chmodSync(filePath, 0o600);
|
|
41
|
+
return true;
|
|
42
|
+
} finally {
|
|
43
|
+
try { fs.unlinkSync(tempPath); } catch (e) { if (e?.code !== "ENOENT") throw e; }
|
|
44
|
+
}
|
|
45
|
+
}
|
package/src/running-adapter.mjs
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { localPackageVersion } from "./version.mjs";
|
|
3
|
+
|
|
1
4
|
export const ADAPTER_HEALTH_PATH = "/_ccdx/health";
|
|
5
|
+
export const ADAPTER_PROTOCOL_VERSION = 2;
|
|
6
|
+
export const ADAPTER_VERSION = localPackageVersion();
|
|
7
|
+
const ADAPTER_INSTANCE_ID = randomUUID();
|
|
2
8
|
|
|
3
9
|
export function adapterHealthPayload() {
|
|
4
10
|
return {
|
|
5
11
|
ok: true,
|
|
6
12
|
name: "codex-copilot-dx",
|
|
7
13
|
pid: process.pid,
|
|
14
|
+
version: ADAPTER_VERSION,
|
|
15
|
+
protocol_version: ADAPTER_PROTOCOL_VERSION,
|
|
16
|
+
instance_id: ADAPTER_INSTANCE_ID,
|
|
8
17
|
};
|
|
9
18
|
}
|
|
10
19
|
|
|
@@ -25,6 +34,8 @@ export async function checkRunningAdapter({
|
|
|
25
34
|
port = 2026,
|
|
26
35
|
fetchImpl = fetch,
|
|
27
36
|
timeoutMs = 500,
|
|
37
|
+
expectedVersion = ADAPTER_VERSION,
|
|
38
|
+
expectedProtocolVersion = ADAPTER_PROTOCOL_VERSION,
|
|
28
39
|
} = {}) {
|
|
29
40
|
const baseUrl = adapterBaseUrl(host, port);
|
|
30
41
|
const ctrl = new AbortController();
|
|
@@ -39,6 +50,17 @@ export async function checkRunningAdapter({
|
|
|
39
50
|
if (data?.name !== "codex-copilot-dx" || data?.ok !== true) {
|
|
40
51
|
return { ok: false, baseUrl, status: resp.status, data };
|
|
41
52
|
}
|
|
53
|
+
if (data.version !== expectedVersion || data.protocol_version !== expectedProtocolVersion) {
|
|
54
|
+
return {
|
|
55
|
+
ok: false,
|
|
56
|
+
incompatible: true,
|
|
57
|
+
baseUrl,
|
|
58
|
+
status: resp.status,
|
|
59
|
+
data,
|
|
60
|
+
expectedVersion,
|
|
61
|
+
expectedProtocolVersion,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
42
64
|
return { ok: true, baseUrl, data };
|
|
43
65
|
} catch (e) {
|
|
44
66
|
return { ok: false, baseUrl, error: e };
|