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 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
- ? (process.env.CCDX_CLAUDE_DESKTOP_API_KEY || process.env.CCDX_PROXY_API_KEY || generatedClaudeDesktopApiKey())
99
- : (process.env.CCDX_CLAUDE_DESKTOP_API_KEY || process.env.CCDX_PROXY_API_KEY || "");
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-copilot-dx",
3
- "version": "0.4.16",
3
+ "version": "0.4.17",
4
4
  "description": "Use Codex Desktop, Claude Code, and Claude App with GitHub Copilot",
5
5
  "type": "module",
6
6
  "bin": {
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 discovered = await discoverGithubToken({ home, env, fetchImpl, signal, excludeTokens });
288
- if (!discovered.ok) return null;
289
- writeToken(discovered.token, home);
290
- const login = discovered.validation.login ? ` for ${discovered.validation.login}` : "";
291
- log(status("ok", `Imported GitHub token from ${sourceDescription(discovered.source)}${login}`));
292
- return discovered;
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
- const imported = await importDiscoveredGithubToken({ home, env, fetchImpl, signal, log });
333
- if (imported) return;
334
-
335
- log(status("wait", "No usable GitHub token found. Starting device login..."));
336
-
337
- // Request a device code.
338
- const codeResp = await fetchImpl("https://github.com/login/device/code", {
339
- method: "POST",
340
- headers: { "Content-Type": "application/json", Accept: "application/json" },
341
- body: JSON.stringify({ client_id: CLIENT_ID, scope: SCOPE }),
342
- signal,
343
- });
344
- if (!codeResp.ok) throw new Error(`device code request failed: ${codeResp.status}`);
345
- const { device_code, user_code, verification_uri, interval } = await codeResp.json();
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 (!pollResp.ok) {
366
- // Treat transient network/server errors as pending and retry.
367
- continue;
368
- }
369
- const data = await pollResp.json();
370
- const r = interpretPoll(data);
371
- if (r.state === "done") {
372
- writeToken(r.token, home);
373
- log(status("ok", "Login successful"));
374
- return;
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
- if (r.state === "slow") { waitMs += 5000; continue; }
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
- export async function getCopilotToken({
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
+ }