codex-copilot-dx 0.4.16 → 0.4.17
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 +7 -0
- package/bin/cli.mjs +49 -2
- package/package.json +1 -1
- package/src/adapter.mjs +7 -0
- package/src/auth.mjs +102 -49
- package/src/copilot.mjs +23 -2
- package/src/lock.mjs +60 -0
- package/src/running-adapter.mjs +48 -0
package/README.md
CHANGED
|
@@ -36,6 +36,8 @@ On first run, it will:
|
|
|
36
36
|
|
|
37
37
|
Claude Code picks up the new `ANTHROPIC_BASE_URL` on its next launch.
|
|
38
38
|
|
|
39
|
+
If an existing `codex-copilot-dx` adapter is already running on the configured host and port, a second launch reuses it instead of starting another proxy. The second launch still refreshes Codex and Claude Code config, then exits.
|
|
40
|
+
|
|
39
41
|
Do not set Claude Code by manually exporting `ANTHROPIC_BASE_URL` or `ANTHROPIC_AUTH_TOKEN` in your shell. Let `codex-copilot-dx` write the local config files instead. If you previously exported those variables, remove them from shell startup files and restart the terminal before launching Claude Code.
|
|
40
42
|
|
|
41
43
|
### Diagnostics
|
|
@@ -73,6 +75,8 @@ CCDX_CONFIGURE_CLAUDE_DESKTOP=1 npx codex-copilot-dx@latest
|
|
|
73
75
|
|
|
74
76
|
This writes a local Claude App 3P gateway profile that points to the adapter root URL, such as `http://127.0.0.1:2026`. The profile uses a generated local bearer key unless `CCDX_CLAUDE_DESKTOP_API_KEY` is set. Restart Claude App after running the command.
|
|
75
77
|
|
|
78
|
+
When reusing an already-running adapter, Claude App profile updates require `CCDX_CLAUDE_DESKTOP_API_KEY` or `CCDX_PROXY_API_KEY` so the profile key matches the running process. Otherwise the existing adapter is left untouched.
|
|
79
|
+
|
|
76
80
|
## Configuration
|
|
77
81
|
|
|
78
82
|
Environment variables:
|
|
@@ -96,6 +100,9 @@ Environment variables:
|
|
|
96
100
|
| `CCDX_GITHUB_TOKEN_PATH` | unset | Explicit file containing a GitHub Copilot OAuth token to validate and import before device login |
|
|
97
101
|
| `CCDX_GITHUB_TOKEN_PATHS` | unset | Multiple token files separated by the platform path delimiter (`:` on macOS/Linux, `;` on Windows) |
|
|
98
102
|
| `CCDX_DISABLE_TOKEN_DISCOVERY` | unset | Set to `1` to skip local token discovery and go straight to the saved token or device flow |
|
|
103
|
+
| `CCDX_TOKEN_LOCK_TIMEOUT_MS` | `600000` | Maximum time to wait for another local process to finish GitHub token login/import |
|
|
104
|
+
| `CCDX_TOKEN_LOCK_STALE_MS` | `900000` | Age after which a stale GitHub token lock file can be removed |
|
|
105
|
+
| `CCDX_EXISTING_ADAPTER_TIMEOUT_MS` | `500` | Timeout for detecting an already-running local adapter during startup |
|
|
99
106
|
| `CCDX_USAGE_PATH` | `~/.local/share/codex-copilot-dx/usage.jsonl` | Local JSONL token usage log |
|
|
100
107
|
| `CCDX_DISABLE_USAGE` | unset | Set to `1` to disable usage logging |
|
|
101
108
|
|
package/bin/cli.mjs
CHANGED
|
@@ -11,10 +11,12 @@ import { status } from "../src/status.mjs";
|
|
|
11
11
|
import { printUsageSummary } from "../src/usage.mjs";
|
|
12
12
|
import { checkForUpdate, localPackageVersion } from "../src/version.mjs";
|
|
13
13
|
import { runDoctor } from "../src/doctor.mjs";
|
|
14
|
+
import { checkRunningAdapter } from "../src/running-adapter.mjs";
|
|
14
15
|
|
|
15
16
|
const ADAPTER_PORT = parseInt(process.env.ADAPTER_PORT || "2026");
|
|
16
17
|
const ADAPTER_HOST = process.env.ADAPTER_HOST || "127.0.0.1";
|
|
17
18
|
const MODEL_REFRESH_TIMEOUT_MS = parseInt(process.env.CCDX_MODEL_REFRESH_TIMEOUT_MS || "5000", 10);
|
|
19
|
+
const EXISTING_ADAPTER_TIMEOUT_MS = parseInt(process.env.CCDX_EXISTING_ADAPTER_TIMEOUT_MS || "500", 10);
|
|
18
20
|
const LOCAL_VERSION = localPackageVersion();
|
|
19
21
|
const command = process.argv[2];
|
|
20
22
|
const CONFIGURE_CLAUDE_DESKTOP = command === "--configure-claude-desktop" || process.env.CCDX_CONFIGURE_CLAUDE_DESKTOP === "1";
|
|
@@ -56,6 +58,49 @@ async function refreshClaudeDesktopModelDefs() {
|
|
|
56
58
|
}
|
|
57
59
|
}
|
|
58
60
|
|
|
61
|
+
function currentClaudeDesktopApiKey() {
|
|
62
|
+
return process.env.CCDX_CLAUDE_DESKTOP_API_KEY || process.env.CCDX_PROXY_API_KEY || "";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function reuseRunningAdapterIfAvailable() {
|
|
66
|
+
const running = await checkRunningAdapter({
|
|
67
|
+
port: ADAPTER_PORT,
|
|
68
|
+
host: ADAPTER_HOST,
|
|
69
|
+
timeoutMs: EXISTING_ADAPTER_TIMEOUT_MS,
|
|
70
|
+
});
|
|
71
|
+
if (!running.ok) return false;
|
|
72
|
+
|
|
73
|
+
console.log(status("ok", `Using existing adapter at ${running.baseUrl}`));
|
|
74
|
+
ensureCodexConfig(ADAPTER_PORT);
|
|
75
|
+
ensureClaudeConfig(ADAPTER_PORT);
|
|
76
|
+
|
|
77
|
+
if (CONFIGURE_CLAUDE_DESKTOP) {
|
|
78
|
+
const claudeDesktopApiKey = currentClaudeDesktopApiKey();
|
|
79
|
+
if (claudeDesktopApiKey) {
|
|
80
|
+
const result = applyClaudeDesktopConfig({
|
|
81
|
+
port: ADAPTER_PORT,
|
|
82
|
+
host: ADAPTER_HOST,
|
|
83
|
+
gatewayApiKey: claudeDesktopApiKey,
|
|
84
|
+
modelIds: claudeDesktopModelIds(process.env),
|
|
85
|
+
});
|
|
86
|
+
console.log(status("ok", `Configured Claude App gateway profile at ${result.baseUrl}`));
|
|
87
|
+
console.log(formatClaudeDesktopApplyResult(result));
|
|
88
|
+
} else {
|
|
89
|
+
console.log(status("warn", "Existing adapter is running; skip Claude App profile update unless CCDX_CLAUDE_DESKTOP_API_KEY or CCDX_PROXY_API_KEY is set"));
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
console.log(status("ok", "Claude App support available with --configure-claude-desktop"));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
openCodex();
|
|
96
|
+
console.log(`
|
|
97
|
+
${status("ok", "Ready, using the existing codex-copilot-dx adapter")}
|
|
98
|
+
|
|
99
|
+
Adapter: ${running.baseUrl}
|
|
100
|
+
`);
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
|
|
59
104
|
if (command === "--version" || command === "-v" || command === "version") {
|
|
60
105
|
console.log(`codex-copilot-dx v${LOCAL_VERSION}`);
|
|
61
106
|
process.exit(0);
|
|
@@ -83,6 +128,8 @@ checkForUpdate({ currentVersion: LOCAL_VERSION }).then(({ latestVersion, updateA
|
|
|
83
128
|
});
|
|
84
129
|
|
|
85
130
|
try {
|
|
131
|
+
if (await reuseRunningAdapterIfAvailable()) process.exit(0);
|
|
132
|
+
|
|
86
133
|
// Ensure GitHub login, using device flow if no token exists.
|
|
87
134
|
await ensureAuth();
|
|
88
135
|
|
|
@@ -95,8 +142,8 @@ try {
|
|
|
95
142
|
}
|
|
96
143
|
|
|
97
144
|
const claudeDesktopApiKey = CONFIGURE_CLAUDE_DESKTOP
|
|
98
|
-
? (
|
|
99
|
-
: (
|
|
145
|
+
? (currentClaudeDesktopApiKey() || generatedClaudeDesktopApiKey())
|
|
146
|
+
: currentClaudeDesktopApiKey();
|
|
100
147
|
|
|
101
148
|
// Start the in-process adapter.
|
|
102
149
|
await startAdapter(ADAPTER_PORT, ADAPTER_HOST, { claudeDesktopApiKey, claudeDesktopModelDefs });
|
package/package.json
CHANGED
package/src/adapter.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import { anthropicToChat, chatToAnthropic, streamAnthropicFromLines, countTokens
|
|
|
8
8
|
import { claudeDesktopModelsResponse, resolveAnthropicModel } from "./models.mjs";
|
|
9
9
|
import { status } from "./status.mjs";
|
|
10
10
|
import { recordAnthropicUsage, recordResponsesUsage } from "./usage.mjs";
|
|
11
|
+
import { ADAPTER_HEALTH_PATH, adapterHealthPayload } from "./running-adapter.mjs";
|
|
11
12
|
|
|
12
13
|
// Models that only support Responses API (not chat/completions)
|
|
13
14
|
const RESPONSES_ONLY = new Set([
|
|
@@ -574,6 +575,12 @@ export function startAdapter(port = 2026, host = "127.0.0.1", options = {}) {
|
|
|
574
575
|
const server = http.createServer((req, res) => {
|
|
575
576
|
const pathname = requestPath(req.url);
|
|
576
577
|
|
|
578
|
+
if (req.method === "GET" && pathname === ADAPTER_HEALTH_PATH) {
|
|
579
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
580
|
+
res.end(JSON.stringify(adapterHealthPayload()));
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
|
|
577
584
|
if (req.method === "POST" && pathname === "/v1/responses") {
|
|
578
585
|
(async () => {
|
|
579
586
|
const abort = createRequestAbort(req, res);
|
package/src/auth.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
5
|
import { status } from "./status.mjs";
|
|
6
|
+
import { withFileLock } from "./lock.mjs";
|
|
6
7
|
|
|
7
8
|
const CLIENT_ID = "Iv1.b507a08c87ecfe98"; // Public GitHub Copilot client ID.
|
|
8
9
|
const SCOPE = "read:user";
|
|
@@ -10,11 +11,17 @@ const GITHUB_API = "https://api.github.com";
|
|
|
10
11
|
const COPILOT_TOKEN_URL = `${GITHUB_API}/copilot_internal/v2/token`;
|
|
11
12
|
const DISABLE_TOKEN_DISCOVERY_VALUES = new Set(["1", "true", "yes"]);
|
|
12
13
|
const MAX_AUTH_JSON_BYTES = 1024 * 1024;
|
|
14
|
+
const DEFAULT_TOKEN_LOCK_TIMEOUT_MS = 10 * 60 * 1000;
|
|
15
|
+
const DEFAULT_TOKEN_LOCK_STALE_MS = 15 * 60 * 1000;
|
|
13
16
|
|
|
14
17
|
export function githubTokenPath(home = os.homedir()) {
|
|
15
18
|
return path.join(home, ".local", "share", "copilot-api", "github_token");
|
|
16
19
|
}
|
|
17
20
|
|
|
21
|
+
export function githubTokenLockPath(home = os.homedir()) {
|
|
22
|
+
return `${githubTokenPath(home)}.lock`;
|
|
23
|
+
}
|
|
24
|
+
|
|
18
25
|
export function githubReauthMessage(reason, home = os.homedir()) {
|
|
19
26
|
const tokenPath = githubTokenPath(home);
|
|
20
27
|
return `${reason}
|
|
@@ -46,6 +53,18 @@ function isTokenDiscoveryDisabled(env = process.env) {
|
|
|
46
53
|
return DISABLE_TOKEN_DISCOVERY_VALUES.has(String(env.CCDX_DISABLE_TOKEN_DISCOVERY || "").toLowerCase());
|
|
47
54
|
}
|
|
48
55
|
|
|
56
|
+
function positiveInt(value, fallback) {
|
|
57
|
+
const n = Number.parseInt(value, 10);
|
|
58
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function tokenLockOptions(env = process.env) {
|
|
62
|
+
return {
|
|
63
|
+
timeoutMs: positiveInt(env.CCDX_TOKEN_LOCK_TIMEOUT_MS, DEFAULT_TOKEN_LOCK_TIMEOUT_MS),
|
|
64
|
+
staleMs: positiveInt(env.CCDX_TOKEN_LOCK_STALE_MS, DEFAULT_TOKEN_LOCK_STALE_MS),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
49
68
|
function splitPathList(value) {
|
|
50
69
|
return String(value || "")
|
|
51
70
|
.split(path.delimiter)
|
|
@@ -185,6 +204,12 @@ export function readGithubTokenSource(source) {
|
|
|
185
204
|
return "";
|
|
186
205
|
}
|
|
187
206
|
|
|
207
|
+
function readSavedGithubToken(home = os.homedir()) {
|
|
208
|
+
const filePath = githubTokenPath(home);
|
|
209
|
+
if (!fs.existsSync(filePath)) return "";
|
|
210
|
+
return fs.readFileSync(filePath, "utf8").trim();
|
|
211
|
+
}
|
|
212
|
+
|
|
188
213
|
export async function validateGithubToken(token, {
|
|
189
214
|
fetchImpl = fetch,
|
|
190
215
|
signal,
|
|
@@ -283,13 +308,32 @@ export async function importDiscoveredGithubToken({
|
|
|
283
308
|
signal,
|
|
284
309
|
excludeTokens = [],
|
|
285
310
|
log = console.log,
|
|
311
|
+
lock = true,
|
|
312
|
+
validateSavedToken = false,
|
|
286
313
|
} = {}) {
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
314
|
+
const run = async () => {
|
|
315
|
+
const excluded = new Set(excludeTokens);
|
|
316
|
+
const savedToken = readSavedGithubToken(home);
|
|
317
|
+
if (savedToken && !excluded.has(savedToken)) {
|
|
318
|
+
if (!validateSavedToken) {
|
|
319
|
+
return { ok: true, token: savedToken, source: { type: "saved-token" }, imported: false };
|
|
320
|
+
}
|
|
321
|
+
const validation = await validateGithubToken(savedToken, { fetchImpl, signal });
|
|
322
|
+
if (validation.ok) {
|
|
323
|
+
return { ok: true, token: savedToken, source: { type: "saved-token" }, validation, imported: false };
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const discovered = await discoverGithubToken({ home, env, fetchImpl, signal, excludeTokens });
|
|
328
|
+
if (!discovered.ok) return null;
|
|
329
|
+
writeToken(discovered.token, home);
|
|
330
|
+
const login = discovered.validation.login ? ` for ${discovered.validation.login}` : "";
|
|
331
|
+
log(status("ok", `Imported GitHub token from ${sourceDescription(discovered.source)}${login}`));
|
|
332
|
+
return { ...discovered, imported: true };
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
if (!lock) return run();
|
|
336
|
+
return withFileLock(githubTokenLockPath(home), run, tokenLockOptions(env));
|
|
293
337
|
}
|
|
294
338
|
|
|
295
339
|
// On macOS, copy the user code and open the verification page. Fail quietly.
|
|
@@ -329,54 +373,63 @@ export async function ensureAuth({
|
|
|
329
373
|
log(status("warn", "GitHub token file is empty"));
|
|
330
374
|
}
|
|
331
375
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
// Prompt the user.
|
|
348
|
-
log(`\n${status("info", `Open ${verification_uri}`)}\n${status("info", `Enter code: ${user_code}`)}\n`);
|
|
349
|
-
openAndCopyFn(user_code, verification_uri);
|
|
350
|
-
|
|
351
|
-
// Poll until GitHub completes the device flow.
|
|
352
|
-
let waitMs = (interval || 5) * 1000;
|
|
353
|
-
while (true) {
|
|
354
|
-
await sleep(waitMs);
|
|
355
|
-
const pollResp = await fetchImpl("https://github.com/login/oauth/access_token", {
|
|
376
|
+
await withFileLock(githubTokenLockPath(home), async () => {
|
|
377
|
+
const savedToken = readSavedGithubToken(home);
|
|
378
|
+
if (savedToken) {
|
|
379
|
+
log(status("ok", "GitHub token found"));
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const imported = await importDiscoveredGithubToken({ home, env, fetchImpl, signal, log, lock: false });
|
|
384
|
+
if (imported) return;
|
|
385
|
+
|
|
386
|
+
log(status("wait", "No usable GitHub token found. Starting device login..."));
|
|
387
|
+
|
|
388
|
+
// Request a device code while holding the auth lock so concurrent starts do not
|
|
389
|
+
// trigger multiple browser/device-flow sessions for the same local token.
|
|
390
|
+
const codeResp = await fetchImpl("https://github.com/login/device/code", {
|
|
356
391
|
method: "POST",
|
|
357
392
|
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
358
|
-
body: JSON.stringify({
|
|
359
|
-
client_id: CLIENT_ID,
|
|
360
|
-
device_code,
|
|
361
|
-
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
362
|
-
}),
|
|
393
|
+
body: JSON.stringify({ client_id: CLIENT_ID, scope: SCOPE }),
|
|
363
394
|
signal,
|
|
364
395
|
});
|
|
365
|
-
if (!
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
396
|
+
if (!codeResp.ok) throw new Error(`device code request failed: ${codeResp.status}`);
|
|
397
|
+
const { device_code, user_code, verification_uri, interval } = await codeResp.json();
|
|
398
|
+
|
|
399
|
+
// Prompt the user.
|
|
400
|
+
log(`\n${status("info", `Open ${verification_uri}`)}\n${status("info", `Enter code: ${user_code}`)}\n`);
|
|
401
|
+
openAndCopyFn(user_code, verification_uri);
|
|
402
|
+
|
|
403
|
+
// Poll until GitHub completes the device flow.
|
|
404
|
+
let waitMs = (interval || 5) * 1000;
|
|
405
|
+
while (true) {
|
|
406
|
+
await sleep(waitMs);
|
|
407
|
+
const pollResp = await fetchImpl("https://github.com/login/oauth/access_token", {
|
|
408
|
+
method: "POST",
|
|
409
|
+
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
410
|
+
body: JSON.stringify({
|
|
411
|
+
client_id: CLIENT_ID,
|
|
412
|
+
device_code,
|
|
413
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
414
|
+
}),
|
|
415
|
+
signal,
|
|
416
|
+
});
|
|
417
|
+
if (!pollResp.ok) {
|
|
418
|
+
// Treat transient network/server errors as pending and retry.
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
const data = await pollResp.json();
|
|
422
|
+
const r = interpretPoll(data);
|
|
423
|
+
if (r.state === "done") {
|
|
424
|
+
writeToken(r.token, home);
|
|
425
|
+
log(status("ok", "Login successful"));
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
if (r.state === "slow") { waitMs += 5000; continue; }
|
|
429
|
+
if (r.state === "fail") throw new Error(`Login failed: ${r.error}`);
|
|
430
|
+
// wait: continue polling.
|
|
375
431
|
}
|
|
376
|
-
|
|
377
|
-
if (r.state === "fail") throw new Error(`Login failed: ${r.error}`);
|
|
378
|
-
// wait: continue polling.
|
|
379
|
-
}
|
|
432
|
+
}, tokenLockOptions(env));
|
|
380
433
|
}
|
|
381
434
|
|
|
382
435
|
export function writeToken(token, home = os.homedir()) {
|
package/src/copilot.mjs
CHANGED
|
@@ -246,6 +246,7 @@ export async function refreshVSCodeVersion() {
|
|
|
246
246
|
|
|
247
247
|
let copilotToken = null;
|
|
248
248
|
let copilotTokenExpiry = 0;
|
|
249
|
+
let copilotTokenRefresh = null;
|
|
249
250
|
|
|
250
251
|
function getGithubToken({ home = os.homedir() } = {}) {
|
|
251
252
|
const GITHUB_TOKEN_PATH = githubTokenPath(home);
|
|
@@ -275,13 +276,12 @@ function cacheCopilotTokenData(data) {
|
|
|
275
276
|
return copilotToken;
|
|
276
277
|
}
|
|
277
278
|
|
|
278
|
-
|
|
279
|
+
async function refreshCopilotToken({
|
|
279
280
|
signal,
|
|
280
281
|
home = os.homedir(),
|
|
281
282
|
env = process.env,
|
|
282
283
|
fetchImpl = fetch,
|
|
283
284
|
} = {}) {
|
|
284
|
-
if (copilotToken && Date.now() < copilotTokenExpiry - 60000) return copilotToken;
|
|
285
285
|
const ghToken = getGithubToken({ home });
|
|
286
286
|
const resp = await requestCopilotToken(ghToken, { fetchImpl, signal });
|
|
287
287
|
if (!resp.ok) {
|
|
@@ -292,6 +292,7 @@ export async function getCopilotToken({
|
|
|
292
292
|
fetchImpl,
|
|
293
293
|
signal,
|
|
294
294
|
excludeTokens: [ghToken],
|
|
295
|
+
validateSavedToken: true,
|
|
295
296
|
});
|
|
296
297
|
if (imported?.validation?.copilotTokenData?.token) {
|
|
297
298
|
return cacheCopilotTokenData(imported.validation.copilotTokenData);
|
|
@@ -304,6 +305,26 @@ export async function getCopilotToken({
|
|
|
304
305
|
return cacheCopilotTokenData(data);
|
|
305
306
|
}
|
|
306
307
|
|
|
308
|
+
export async function getCopilotToken(options = {}) {
|
|
309
|
+
if (copilotToken && Date.now() < copilotTokenExpiry - 60000) return copilotToken;
|
|
310
|
+
const home = options.home || os.homedir();
|
|
311
|
+
const refreshKey = githubTokenPath(home);
|
|
312
|
+
if (copilotTokenRefresh?.key === refreshKey) return copilotTokenRefresh.promise;
|
|
313
|
+
|
|
314
|
+
const promise = refreshCopilotToken(options).finally(() => {
|
|
315
|
+
if (copilotTokenRefresh?.promise === promise) copilotTokenRefresh = null;
|
|
316
|
+
});
|
|
317
|
+
copilotTokenRefresh = { key: refreshKey, promise };
|
|
318
|
+
return promise;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export function resetCopilotTokenForTests() {
|
|
322
|
+
copilotToken = null;
|
|
323
|
+
copilotTokenExpiry = 0;
|
|
324
|
+
copilotTokenRefresh = null;
|
|
325
|
+
apiBase = DEFAULT_API_BASE;
|
|
326
|
+
}
|
|
327
|
+
|
|
307
328
|
// chatReq is already converted by the adapter. The caller parses the raw Response.
|
|
308
329
|
export async function chatCompletions(chatReq, { signal } = {}) {
|
|
309
330
|
const token = await getCopilotToken({ signal });
|
package/src/lock.mjs
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
function sleep(ms) {
|
|
5
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function maybeRemoveStaleLock(lockPath, staleMs, nowMs) {
|
|
9
|
+
if (!Number.isFinite(staleMs) || staleMs <= 0) return false;
|
|
10
|
+
try {
|
|
11
|
+
const stat = fs.statSync(lockPath);
|
|
12
|
+
if (nowMs - stat.mtimeMs <= staleMs) return false;
|
|
13
|
+
fs.unlinkSync(lockPath);
|
|
14
|
+
return true;
|
|
15
|
+
} catch (e) {
|
|
16
|
+
if (e?.code === "ENOENT") return true;
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function withFileLock(lockPath, fn, {
|
|
22
|
+
timeoutMs = 5000,
|
|
23
|
+
staleMs = 15 * 60 * 1000,
|
|
24
|
+
pollMs = 50,
|
|
25
|
+
now = Date.now,
|
|
26
|
+
} = {}) {
|
|
27
|
+
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
28
|
+
const started = now();
|
|
29
|
+
let fd = null;
|
|
30
|
+
|
|
31
|
+
while (fd === null) {
|
|
32
|
+
try {
|
|
33
|
+
fd = fs.openSync(lockPath, "wx", 0o600);
|
|
34
|
+
fs.writeFileSync(fd, JSON.stringify({ pid: process.pid, created_at: new Date().toISOString() }));
|
|
35
|
+
break;
|
|
36
|
+
} catch (e) {
|
|
37
|
+
if (e?.code !== "EEXIST") throw e;
|
|
38
|
+
const nowMs = now();
|
|
39
|
+
if (maybeRemoveStaleLock(lockPath, staleMs, nowMs)) continue;
|
|
40
|
+
if (nowMs - started >= timeoutMs) {
|
|
41
|
+
throw new Error(`Timed out waiting for lock: ${lockPath}`);
|
|
42
|
+
}
|
|
43
|
+
await sleep(Math.min(pollMs, Math.max(1, timeoutMs - (nowMs - started))));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
return await fn();
|
|
49
|
+
} finally {
|
|
50
|
+
try {
|
|
51
|
+
if (fd !== null) fs.closeSync(fd);
|
|
52
|
+
} finally {
|
|
53
|
+
try {
|
|
54
|
+
fs.unlinkSync(lockPath);
|
|
55
|
+
} catch (e) {
|
|
56
|
+
if (e?.code !== "ENOENT") throw e;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export const ADAPTER_HEALTH_PATH = "/_ccdx/health";
|
|
2
|
+
|
|
3
|
+
export function adapterHealthPayload() {
|
|
4
|
+
return {
|
|
5
|
+
ok: true,
|
|
6
|
+
name: "codex-copilot-dx",
|
|
7
|
+
pid: process.pid,
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function adapterProbeHost(host = "127.0.0.1") {
|
|
12
|
+
const normalized = String(host || "127.0.0.1").toLowerCase();
|
|
13
|
+
if (normalized === "0.0.0.0" || normalized === "::" || normalized === "[::]") return "127.0.0.1";
|
|
14
|
+
return host || "127.0.0.1";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function adapterBaseUrl(host = "127.0.0.1", port = 2026) {
|
|
18
|
+
const probeHost = String(adapterProbeHost(host));
|
|
19
|
+
const urlHost = probeHost.includes(":") && !probeHost.startsWith("[") ? `[${probeHost}]` : probeHost;
|
|
20
|
+
return `http://${urlHost}:${port}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function checkRunningAdapter({
|
|
24
|
+
host = "127.0.0.1",
|
|
25
|
+
port = 2026,
|
|
26
|
+
fetchImpl = fetch,
|
|
27
|
+
timeoutMs = 500,
|
|
28
|
+
} = {}) {
|
|
29
|
+
const baseUrl = adapterBaseUrl(host, port);
|
|
30
|
+
const ctrl = new AbortController();
|
|
31
|
+
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
32
|
+
try {
|
|
33
|
+
const resp = await fetchImpl(`${baseUrl}${ADAPTER_HEALTH_PATH}`, {
|
|
34
|
+
headers: { Accept: "application/json" },
|
|
35
|
+
signal: ctrl.signal,
|
|
36
|
+
});
|
|
37
|
+
if (!resp.ok) return { ok: false, baseUrl, status: resp.status };
|
|
38
|
+
const data = await resp.json();
|
|
39
|
+
if (data?.name !== "codex-copilot-dx" || data?.ok !== true) {
|
|
40
|
+
return { ok: false, baseUrl, status: resp.status, data };
|
|
41
|
+
}
|
|
42
|
+
return { ok: true, baseUrl, data };
|
|
43
|
+
} catch (e) {
|
|
44
|
+
return { ok: false, baseUrl, error: e };
|
|
45
|
+
} finally {
|
|
46
|
+
clearTimeout(timer);
|
|
47
|
+
}
|
|
48
|
+
}
|