codex-copilot-dx 0.6.0 → 0.6.2

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
@@ -1,6 +1,6 @@
1
1
  # codex-copilot-dx
2
2
 
3
- Use [Codex Desktop](https://openai.com/codex), [Claude Code](https://claude.com/claude-code), and optionally Claude App with your **GitHub Copilot** subscription.
3
+ Use [Codex Desktop](https://openai.com/codex), [Claude Code](https://claude.com/claude-code), and optionally Claude App or PM Studio with your **GitHub Copilot** subscription.
4
4
 
5
5
  ## How it works
6
6
 
@@ -10,6 +10,7 @@ account by default, or over isolated Codex and Claude accounts when configured:
10
10
  - **Codex** -> OpenAI Responses API (`/v1/responses`, `/v1/responses/compact`); Responses-only models and compaction proxy directly, chat models convert to Chat Completions.
11
11
  - **Claude Code** -> Anthropic Messages API (`/v1/messages`, `/v1/messages/count_tokens`), translated to/from Chat Completions.
12
12
  - **Claude App** -> optional Claude Desktop App gateway profile using the same local Messages API plus local model discovery for the configured gateway key.
13
+ - **PM Studio** -> optional macOS app patch that keeps the active PM profile for GPT models while routing enabled Claude models through an isolated CCDX Claude account.
13
14
 
14
15
  Supports both HTTP SSE streaming and non-streaming.
15
16
 
@@ -19,7 +20,7 @@ Codex Auto-review requests use the hidden `codex-auto-review` model ID. The adap
19
20
 
20
21
  - GitHub Copilot subscription (Individual, Business, or Enterprise)
21
22
  - Node.js 22.15+ (required for built-in Zstandard request decompression)
22
- - [Codex Desktop](https://openai.com/codex), [Claude Code](https://claude.com/claude-code), and/or Claude App installed
23
+ - [Codex Desktop](https://openai.com/codex), [Claude Code](https://claude.com/claude-code), Claude App, and/or a supported PM Studio installation
23
24
 
24
25
  ## Usage
25
26
 
@@ -33,6 +34,8 @@ command remains an equivalent compatibility alias. For a one-off run without a
33
34
  global install, use `npx codex-copilot-dx@latest`.
34
35
  Both installed commands expose the same options and subcommands; help, version,
35
36
  doctor, status, and argument-error output use the command name that was invoked.
37
+ Run `ccdx <command> --help` (including `auth login claude --help` and
38
+ `pms status --help`) for command-specific behavior and side-effect details.
36
39
 
37
40
  To query the models currently advertised as selectable for the saved Codex
38
41
  Copilot account, run:
@@ -59,11 +62,14 @@ while using a different personal account that has Claude models enabled, run:
59
62
  ccdx auth login claude --github-login <personal-github-login>
60
63
  ```
61
64
 
62
- This is the only command that starts the additional Device Flow. It verifies
65
+ This is the only command that may start the additional Device Flow. Before
66
+ opening a browser, it checks compatible local Copilot credentials, excludes the
67
+ current Codex identity, and reuses an unambiguous valid account. It then verifies
63
68
  the selected GitHub identity, Copilot entitlement, and an enabled Claude model
64
69
  before saving anything. It refuses the current Codex account and never changes,
65
70
  migrates, or deletes the existing Codex credential. The optional login pin is
66
- recommended so authorizing the wrong browser account fails before activation.
71
+ recommended so a machine with multiple accounts selects only the requested
72
+ identity. Use `--reauth` to bypass local discovery and force Device Flow.
67
73
 
68
74
  The existing Codex token remains at
69
75
  `~/.local/share/copilot-api/github_token`. The isolated Claude credential is
@@ -79,7 +85,8 @@ Once enabled, routing is fixed by API surface rather than by model name:
79
85
  - `/v1/messages/count_tokens` remains local and uses neither account.
80
86
 
81
87
  The two profiles keep separate service tokens, API bases, refresh/backoff state,
82
- model endpoint metadata, and model caches. If the isolated Claude profile later
88
+ model endpoint metadata, and credential-bound model caches. Switching either
89
+ GitHub account invalidates that profile's old cache instead of borrowing it. If the isolated Claude profile later
83
90
  becomes invalid, Claude requests fail with a reauthentication instruction while
84
91
  Codex remains available; CCDX never silently falls back to the enterprise
85
92
  account. Reauthorize it explicitly with:
@@ -97,6 +104,83 @@ ccdx models --profile claude
97
104
  ccdx doctor --online --profile all
98
105
  ```
99
106
 
107
+ ### PM Studio opt-in patch
108
+
109
+ CCDX can add the isolated Claude catalog to PM Studio without switching its
110
+ active account or profile. GPT requests continue to use the bearer supplied by
111
+ the current PM profile; exact enabled Anthropic model IDs use the isolated
112
+ CCDX Claude profile. Neither credential is copied into the other application.
113
+
114
+ The first recipe supports only an exact, unmodified PM Studio 2.9.7 bundle on
115
+ macOS. Authorize the isolated Claude profile first, quit PM Studio and its
116
+ updater, then run the one-time setup command:
117
+
118
+ ```bash
119
+ ccdx auth login claude --reauth --github-login <personal-github-login>
120
+ ccdx pms setup
121
+ ```
122
+
123
+ Inspect the installed version, exact patch integrity, isolated Claude profile,
124
+ and relay without changing files:
125
+
126
+ ```bash
127
+ ccdx pms status
128
+ ```
129
+
130
+ `pm-studio status` and `pm-studio setup` are equivalent long-form aliases.
131
+
132
+ `ccdx pms setup` performs only local preflight, backup, staging, integrity,
133
+ signing, and replacement work. It never starts Device Flow, the adapter,
134
+ Codex, or PM Studio, and it does not modify PM profiles, cached model lists, or
135
+ saved PM tokens. Unknown versions, hashes, signatures, partial patches, a
136
+ running PM process, insufficient space, and every pre-replacement verification
137
+ failure leave the installed App unchanged. If the final read-only verification
138
+ after an atomic replacement detects an exceptional filesystem drift, setup
139
+ does not attempt a second automatic write: it preserves the verified backup,
140
+ retains any diagnostic stage still present, and prints recovery paths. Repeating
141
+ setup for the exact installed recipe is idempotent.
142
+
143
+ The patch is App-wide: every PM profile sends its Copilot API traffic through
144
+ the loopback-only `/pm-ccdx/*` relay. Run ordinary `ccdx` before opening PM
145
+ Studio; if port 2026 is unavailable, PM Copilot requests cannot connect. The
146
+ relay explicitly supports only the PM 2.9.7 Copilot paths discovered in the
147
+ validated bundle (`GET /models` and `POST /chat/completions`, `/responses`, and
148
+ `/embeddings`) and is not a general-purpose proxy.
149
+
150
+ Patching replaces the vendor signature with an ad-hoc signature. PM Studio's
151
+ updater is not disabled; an official update may overwrite the patch or may
152
+ require reinstalling the official App. Setup prints the verified backup and
153
+ manifest paths plus the version-matched restore procedure. Never restore a
154
+ 2.9.7 backup over a newer PM Studio installation.
155
+
156
+ To restore, first quit PM Studio and its updater. Use the exact verified backup
157
+ path printed by setup as `BACKUP_APP`, then confirm that both the installed App
158
+ and backup still report version/build `2.9.7/2.9.7`:
159
+
160
+ ```bash
161
+ BACKUP_APP="<verified-backup-path-from-ccdx-pms-setup>"
162
+ /usr/bin/plutil -extract CFBundleShortVersionString raw -o - "/Applications/PM Studio.app/Contents/Info.plist"
163
+ /usr/bin/plutil -extract CFBundleVersion raw -o - "/Applications/PM Studio.app/Contents/Info.plist"
164
+ /usr/bin/plutil -extract CFBundleShortVersionString raw -o - "$BACKUP_APP/Contents/Info.plist"
165
+ /usr/bin/plutil -extract CFBundleVersion raw -o - "$BACKUP_APP/Contents/Info.plist"
166
+ ```
167
+
168
+ Only when all four values match `2.9.7`, move the patched App aside, restore the
169
+ complete vendor-signed backup, and verify it before launch:
170
+
171
+ ```bash
172
+ RECOVERY_DIR="$(/usr/bin/mktemp -d '/Applications/CCDX-PM-Studio-recovery.XXXXXX')"
173
+ PATCHED_APP="$RECOVERY_DIR/PM Studio.app"
174
+ mv "/Applications/PM Studio.app" "$PATCHED_APP"
175
+ /usr/bin/ditto --rsrc --extattr --acl "$BACKUP_APP" "/Applications/PM Studio.app"
176
+ /usr/bin/codesign --verify --deep --strict --verbose=2 "/Applications/PM Studio.app"
177
+ ```
178
+
179
+ If the version check, copy, or signature verification fails, do not launch the
180
+ restored App. Keep the moved patched bundle at `$PATCHED_APP` and reinstall the official PM Studio
181
+ instead of applying a 2.9.7 backup to unknown content. CCDX never runs these
182
+ restore commands or `sudo` automatically.
183
+
100
184
  To change the model used by Codex Auto-review, run:
101
185
 
102
186
  ```bash
@@ -163,7 +247,11 @@ ccdx doctor
163
247
 
164
248
  The command exits with status `1` when it finds an invalid configuration and `0` when checks contain only OK or warning results.
165
249
 
166
- The doctor checks the GitHub token, Codex config, Claude Code settings, Claude App gateway profile, and whether the local adapter port is listening.
250
+ The doctor checks the GitHub token, Codex config, Claude Code settings, Claude
251
+ App gateway profile, adapter availability, and an installed PM Studio bundle.
252
+ It ends with passed/warning/error totals and deduplicated next-step commands.
253
+ An absent PM Studio installation is ignored; unsupported versions are reported
254
+ without modification, while integrity drift remains an error.
167
255
 
168
256
  For a read-only live check of the saved GitHub token, Copilot entitlement, and models endpoint, run:
169
257
 
@@ -198,7 +286,7 @@ loopback endpoint directly:
198
286
  curl -s http://127.0.0.1:2026/_ccdx/status | jq
199
287
  ```
200
288
 
201
- The status endpoint is restricted to the socket's real loopback address, even when LAN binding is explicitly enabled. It reports fixed-size request counters, bounded TTFT/TPOT histograms for streaming routes, admission pressure, memory use, response-history size, image queue/cache state, model cache counts, and token expiry state. It never retains metric samples or includes prompts, completions, tool arguments, image content, account names, or token values. API responses always include a safe `X-Request-Id` for correlation. To include the same ID as `request_id` in related terminal and file log lines, start the adapter with:
289
+ The status endpoint is restricted to the socket's real loopback address, even when LAN binding is explicitly enabled. It reports fixed-size request counters, bounded TTFT/TPOT histograms for streaming routes, admission pressure, memory use, response-history size, image queue/cache state, profile model cache counts, PM relay route totals, and token expiry state. It never retains metric samples or includes prompts, completions, tool arguments, image content, account names, or token values. API responses always include a safe `X-Request-Id` for correlation. If the adapter is not running, the CLI prints the exact start-and-retry command. To include the same ID as `request_id` in related terminal and file log lines, start the adapter with:
202
290
 
203
291
  ```bash
204
292
  ccdx --show-request-id
@@ -218,9 +306,13 @@ ccdx
218
306
  Claude App support is opt-in so the default Codex Desktop and Claude Code setup stays unchanged:
219
307
 
220
308
  ```bash
221
- npx codex-copilot-dx@latest --configure-claude-desktop
309
+ npx codex-copilot-dx@latest start --configure-claude-app
222
310
  ```
223
311
 
312
+ The previous `--configure-claude-desktop` spelling remains a compatibility
313
+ alias. Startup reports Claude App as ready only when a matching managed gateway
314
+ key is restored or the profile is configured in that run.
315
+
224
316
  Or set:
225
317
 
226
318
  ```bash
package/bin/cli.mjs CHANGED
@@ -39,7 +39,7 @@ try {
39
39
  }
40
40
 
41
41
  if (CLI.command === "help") {
42
- console.log(cliHelp(CLI_NAME));
42
+ console.log(cliHelp(CLI_NAME, CLI.helpTopic));
43
43
  process.exit(0);
44
44
  }
45
45
  if (CLI.command === "version") {
@@ -123,6 +123,21 @@ if (CLI.command === "update") {
123
123
  process.exit(1);
124
124
  }
125
125
  }
126
+ if (CLI.command === "pms") {
127
+ try {
128
+ if (CLI.action === "status") {
129
+ const { runPmStudioStatus } = await import("../src/pm-studio-status.mjs");
130
+ const result = await runPmStudioStatus({ commandName: CLI_NAME });
131
+ process.exit(result.ok ? 0 : 1);
132
+ }
133
+ const { runPmStudioSetup } = await import("../src/pm-studio-setup.mjs");
134
+ await runPmStudioSetup({ commandName: CLI_NAME });
135
+ process.exit(0);
136
+ } catch (e) {
137
+ console.error(status("err", e.message));
138
+ process.exit(1);
139
+ }
140
+ }
126
141
 
127
142
  let RUNTIME;
128
143
  try {
@@ -196,7 +211,7 @@ async function reuseRunningAdapterIfAvailable() {
196
211
  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"));
197
212
  }
198
213
  } else {
199
- console.log(status("ok", "Claude App support available with --configure-claude-desktop"));
214
+ console.log(status("ok", "Claude App support available with --configure-claude-app"));
200
215
  }
201
216
 
202
217
  await openCodex();
@@ -260,6 +275,7 @@ try {
260
275
  codexClient: profileRuntime.codexClient,
261
276
  claudeClient: profileRuntime.claudeClient,
262
277
  claudeMode: profileRuntime.claudeMode,
278
+ codexCredentialFingerprint: profileRuntime.codexCredentialFingerprint,
263
279
  claudeCredentialFingerprint: profileRuntime.claudeCredentialFingerprint,
264
280
  timeoutMs: MODEL_REFRESH_TIMEOUT_MS,
265
281
  commandName: CLI_NAME,
@@ -296,6 +312,7 @@ try {
296
312
  autoReviewModelResolver: () => autoReviewModelPreference().model,
297
313
  codexClient: profileRuntime.codexClient,
298
314
  claudeClient: profileRuntime.claudeClient,
315
+ claudeProfile: profileRuntime.claudeProfile,
299
316
  codexModelRegistry: profileModels.codexRegistry,
300
317
  claudeModelRegistry: profileModels.claudeRegistry,
301
318
  claudeMode: profileRuntime.claudeMode,
@@ -318,7 +335,7 @@ try {
318
335
  console.log(status("ok", `Configured Claude App gateway profile at ${result.baseUrl}`));
319
336
  console.log(formatClaudeDesktopApplyResult(result));
320
337
  } else {
321
- console.log(status("ok", "Claude App support available with --configure-claude-desktop"));
338
+ console.log(status("ok", "Claude App support available with --configure-claude-app"));
322
339
  }
323
340
 
324
341
  // Launch Codex when available.
@@ -334,8 +351,10 @@ try {
334
351
  modelRefreshTimer.unref?.();
335
352
  }
336
353
 
354
+ const readyClients = ["Codex App", "Claude Code"];
355
+ if (claudeDesktopApiKey) readyClients.splice(1, 0, "Claude App");
337
356
  console.log(`
338
- ${status("ok", "Ready, Claude Code, Claude App and Codex App are now ready to use")}
357
+ ${status("ok", `Ready, ${readyClients.join(", ")} ${readyClients.length === 1 ? "is" : "are"} ready to use`)}
339
358
 
340
359
  Adapter: ${adapterBaseUrl(ADAPTER_HOST, ADAPTER_PORT)}
341
360
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-copilot-dx",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "Use Codex Desktop, Claude Code, and Claude App with GitHub Copilot",
5
5
  "author": "Dale Xiao",
6
6
  "type": "module",
@@ -8,6 +8,7 @@ import { setTimeout as delay } from "node:timers/promises";
8
8
  import { fileURLToPath } from "node:url";
9
9
  import sharp from "sharp";
10
10
  import { optimizeImagesInBody, prepareResponsesPayload } from "../src/image-optimization.mjs";
11
+ import { createPmStudioModelRouter } from "../src/profile-routing.mjs";
11
12
 
12
13
  const packageRoot = fileURLToPath(new URL("../", import.meta.url));
13
14
 
@@ -235,6 +236,34 @@ const duplicateImages = {
235
236
  optimizer_calls: duplicateCalls,
236
237
  };
237
238
 
239
+ const routingCatalog = {
240
+ data: Array.from({ length: 1000 }, (_, index) => index % 5 === 0
241
+ ? {
242
+ id: `claude-benchmark-${index}`,
243
+ vendor: "Anthropic",
244
+ model_picker_enabled: true,
245
+ supported_endpoints: ["/chat/completions"],
246
+ }
247
+ : { id: `gpt-benchmark-${index}`, vendor: "OpenAI", supported_endpoints: ["/responses"] }),
248
+ };
249
+ const modelRouter = createPmStudioModelRouter({
250
+ getCatalog: () => routingCatalog,
251
+ isClaudeEnabled: () => true,
252
+ });
253
+ const routingIterations = 100000;
254
+ const routingStarted = performance.now();
255
+ let claudeClassifications = 0;
256
+ for (let index = 0; index < routingIterations; index += 1) {
257
+ if (modelRouter.classify("claude-benchmark-0") === "claude") claudeClassifications += 1;
258
+ }
259
+ const pmModelRouting = {
260
+ catalog_models: routingCatalog.data.length,
261
+ iterations: routingIterations,
262
+ elapsed_ms: +(performance.now() - routingStarted).toFixed(1),
263
+ claude_classifications: claudeClassifications,
264
+ availability_rebuilds: modelRouter.diagnostics().rebuilds,
265
+ };
266
+
238
267
  const pixels = deterministicPixels(1024 * 1024 * 3, 0x87654321);
239
268
  const webp = await sharp(pixels, { raw: { width: 1024, height: 1024, channels: 3 } })
240
269
  .webp({ quality: 100 })
@@ -281,6 +310,7 @@ const report = {
281
310
  adapter_import: adapterImport,
282
311
  token_count_100kb: tokenCount,
283
312
  duplicate_images: duplicateImages,
313
+ pm_model_routing: pmModelRouting,
284
314
  oversized_payload: oversizedPayload,
285
315
  };
286
316
 
@@ -308,6 +338,10 @@ console.log(JSON.stringify(report, null, 2));
308
338
 
309
339
  if (checkMode) {
310
340
  const failures = [];
341
+ if (report.pm_model_routing.availability_rebuilds !== 1
342
+ || report.pm_model_routing.claude_classifications !== report.pm_model_routing.iterations) {
343
+ failures.push("PM Studio model routing did not reuse the memoized catalog classification");
344
+ }
311
345
  const proxy = report.large_token_count.proxy;
312
346
  const array = report.large_token_count.materialized_array;
313
347
  if (proxy.tokens !== array.tokens) failures.push(`token counts differ: proxy=${proxy.tokens} array=${array.tokens}`);
package/src/adapter.mjs CHANGED
@@ -11,6 +11,7 @@ import {
11
11
  runtimeStatusPayload,
12
12
  } from "./observability.mjs";
13
13
  import { createRequestId, runWithRequestContext } from "./request-context.mjs";
14
+ import { createPmStudioRelayHandler, PM_STUDIO_RELAY_PREFIX } from "./pm-studio-relay.mjs";
14
15
  import { createStreamPerformanceMetrics } from "./stream-performance.mjs";
15
16
  import { status } from "./status.mjs";
16
17
  import { createTerminalActivityIndicator } from "./terminal-activity.mjs";
@@ -172,6 +173,17 @@ export function createAdapterHandler(options = {}) {
172
173
  streamIdleTimeoutMs,
173
174
  upstreamTimeoutMs,
174
175
  });
176
+ const pmStudioRelayHandler = createPmStudioRelayHandler({
177
+ acquireRequest,
178
+ claudeClient,
179
+ claudeMode,
180
+ claudeProfile: options.claudeProfile,
181
+ claudeModelRegistry,
182
+ fetchImpl: options.pmStudioFetchImpl,
183
+ streamHandshakeTimeoutMs,
184
+ streamIdleTimeoutMs,
185
+ upstreamTimeoutMs,
186
+ });
175
187
 
176
188
  const dispatch = (req, res, pathname) => {
177
189
  if (req.method === "GET" && pathname === ADAPTER_STATUS_PATH) {
@@ -201,6 +213,10 @@ export function createAdapterHandler(options = {}) {
201
213
  return;
202
214
  }
203
215
 
216
+ if (pathname.startsWith(`${PM_STUDIO_RELAY_PREFIX}/`)) {
217
+ return pmStudioRelayHandler(req, res, pathname);
218
+ }
219
+
204
220
  if (req.method === "POST" && pathname === "/v1/responses") {
205
221
  return responsesHandler(req, res);
206
222
  }
package/src/auth.mjs CHANGED
@@ -326,6 +326,7 @@ export async function discoverGithubToken({
326
326
  fetchImpl = fetch,
327
327
  signal,
328
328
  excludeTokens = [],
329
+ excludeIdentities = [],
329
330
  expectedIdentity,
330
331
  strictExpectedIdentity = false,
331
332
  } = {}) {
@@ -348,6 +349,10 @@ export async function discoverGithubToken({
348
349
 
349
350
  const validation = await validateGithubToken(token, { fetchImpl, signal });
350
351
  if (validation.ok) {
352
+ if (excludeIdentities.some((identity) => githubIdentityMatchesExpected(validation, identity))) {
353
+ failures.push({ source, validation, reason: "github_account_excluded" });
354
+ continue;
355
+ }
351
356
  const candidate = { ok: true, token, source, validation };
352
357
  const explicitSource = source.type === "env" || source.type === "token-file";
353
358
  if (expectedIdentity && (strictExpectedIdentity || !explicitSource)) {
package/src/cli-auth.mjs CHANGED
@@ -1,8 +1,10 @@
1
1
  import os from "node:os";
2
2
  import { spawn } from "node:child_process";
3
3
  import {
4
+ discoverGithubToken,
4
5
  fetchGithubIdentity,
5
6
  interpretPoll,
7
+ sourceDescription,
6
8
  validateGithubToken,
7
9
  } from "./auth.mjs";
8
10
  import {
@@ -19,6 +21,7 @@ import {
19
21
  normalizeGithubIdentity,
20
22
  } from "./github-identity.mjs";
21
23
  import { saveModelCache } from "./model-cache.mjs";
24
+ import { profileRouting } from "./profile-routing.mjs";
22
25
  import { status } from "./status.mjs";
23
26
 
24
27
  const CLIENT_ID = "Iv1.b507a08c87ecfe98";
@@ -267,14 +270,45 @@ export async function authorizeClaudeProfile({
267
270
  throw new Error(`Claude profile is configured but invalid (${current.reason}); run ccdx auth login claude --reauth`);
268
271
  }
269
272
 
270
- const githubToken = await requestDeviceFlowToken({
271
- fetchImpl,
272
- signal,
273
- log,
274
- openAndCopyFn,
275
- sleepImpl,
276
- now,
277
- });
273
+ let githubToken = "";
274
+ if (!reauth) {
275
+ const codex = readAuthProfileCredentials(AUTH_PROFILE_CODEX, { home });
276
+ if (!codex.valid) {
277
+ throw new Error("The existing Codex GitHub account is not configured; refusing to create an unbound Claude profile");
278
+ }
279
+ const existingCodex = await codexIdentity({ home, fetchImpl, signal });
280
+ const pinnedLogin = String(expectedLogin || "").trim();
281
+ const discovered = await discoverGithubToken({
282
+ home,
283
+ env,
284
+ fetchImpl,
285
+ signal,
286
+ excludeTokens: [codex.token],
287
+ excludeIdentities: [existingCodex.identity],
288
+ expectedIdentity: pinnedLogin ? { login: pinnedLogin } : undefined,
289
+ strictExpectedIdentity: Boolean(pinnedLogin),
290
+ });
291
+ if (discovered.ambiguous) {
292
+ const accounts = [...new Set(discovered.candidates
293
+ .map((candidate) => candidate.login || candidate.id || "unknown"))];
294
+ throw new Error(`Multiple reusable GitHub Copilot accounts were found (${accounts.join(", ")}). Use --github-login or CCDX_GITHUB_TOKEN_PATH to select the Claude account.`);
295
+ }
296
+ if (discovered.ok) {
297
+ githubToken = discovered.token;
298
+ const login = discovered.validation?.login ? ` for ${discovered.validation.login}` : "";
299
+ log(status("info", `Reusing a local Copilot credential from ${sourceDescription(discovered.source)}${login}`));
300
+ }
301
+ }
302
+ if (!githubToken) {
303
+ githubToken = await requestDeviceFlowToken({
304
+ fetchImpl,
305
+ signal,
306
+ log,
307
+ openAndCopyFn,
308
+ sleepImpl,
309
+ now,
310
+ });
311
+ }
278
312
  const candidate = await validateClaudeCandidate(githubToken, {
279
313
  home,
280
314
  expectedLogin,
@@ -350,10 +384,7 @@ export function authStatus({ home = os.homedir() } = {}) {
350
384
  codex: publicProfile(codex, "legacy"),
351
385
  claude: publicProfile(claude, "isolated"),
352
386
  },
353
- routing: {
354
- responses: AUTH_PROFILE_CODEX,
355
- messages: claude.configured ? AUTH_PROFILE_CLAUDE : AUTH_PROFILE_CODEX,
356
- },
387
+ routing: profileRouting({ claudeConfigured: claude.configured }),
357
388
  };
358
389
  }
359
390