codex-copilot-dx 0.6.1 → 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 +29 -6
- package/bin/cli.mjs +12 -4
- package/package.json +1 -1
- package/scripts/benchmark.mjs +34 -0
- package/src/auth.mjs +5 -0
- package/src/cli-auth.mjs +43 -12
- package/src/cli-options.mjs +139 -51
- package/src/cli-status.mjs +35 -2
- package/src/doctor.mjs +56 -3
- package/src/observability.mjs +2 -4
- package/src/pm-studio-relay.mjs +14 -48
- package/src/pm-studio-status.mjs +130 -0
- package/src/profile-model-runtime.mjs +3 -2
- package/src/profile-routing.mjs +74 -0
- package/src/profile-runtime.mjs +13 -0
package/README.md
CHANGED
|
@@ -34,6 +34,8 @@ command remains an equivalent compatibility alias. For a one-off run without a
|
|
|
34
34
|
global install, use `npx codex-copilot-dx@latest`.
|
|
35
35
|
Both installed commands expose the same options and subcommands; help, version,
|
|
36
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.
|
|
37
39
|
|
|
38
40
|
To query the models currently advertised as selectable for the saved Codex
|
|
39
41
|
Copilot account, run:
|
|
@@ -60,11 +62,14 @@ while using a different personal account that has Claude models enabled, run:
|
|
|
60
62
|
ccdx auth login claude --github-login <personal-github-login>
|
|
61
63
|
```
|
|
62
64
|
|
|
63
|
-
This is the only command that
|
|
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
|
|
64
68
|
the selected GitHub identity, Copilot entitlement, and an enabled Claude model
|
|
65
69
|
before saving anything. It refuses the current Codex account and never changes,
|
|
66
70
|
migrates, or deletes the existing Codex credential. The optional login pin is
|
|
67
|
-
recommended so
|
|
71
|
+
recommended so a machine with multiple accounts selects only the requested
|
|
72
|
+
identity. Use `--reauth` to bypass local discovery and force Device Flow.
|
|
68
73
|
|
|
69
74
|
The existing Codex token remains at
|
|
70
75
|
`~/.local/share/copilot-api/github_token`. The isolated Claude credential is
|
|
@@ -80,7 +85,8 @@ Once enabled, routing is fixed by API surface rather than by model name:
|
|
|
80
85
|
- `/v1/messages/count_tokens` remains local and uses neither account.
|
|
81
86
|
|
|
82
87
|
The two profiles keep separate service tokens, API bases, refresh/backoff state,
|
|
83
|
-
model endpoint metadata, and model caches.
|
|
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
|
|
84
90
|
becomes invalid, Claude requests fail with a reauthentication instruction while
|
|
85
91
|
Codex remains available; CCDX never silently falls back to the enterprise
|
|
86
92
|
account. Reauthorize it explicitly with:
|
|
@@ -114,6 +120,15 @@ ccdx auth login claude --reauth --github-login <personal-github-login>
|
|
|
114
120
|
ccdx pms setup
|
|
115
121
|
```
|
|
116
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
|
+
|
|
117
132
|
`ccdx pms setup` performs only local preflight, backup, staging, integrity,
|
|
118
133
|
signing, and replacement work. It never starts Device Flow, the adapter,
|
|
119
134
|
Codex, or PM Studio, and it does not modify PM profiles, cached model lists, or
|
|
@@ -232,7 +247,11 @@ ccdx doctor
|
|
|
232
247
|
|
|
233
248
|
The command exits with status `1` when it finds an invalid configuration and `0` when checks contain only OK or warning results.
|
|
234
249
|
|
|
235
|
-
The doctor checks the GitHub token, Codex config, Claude Code settings, Claude
|
|
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.
|
|
236
255
|
|
|
237
256
|
For a read-only live check of the saved GitHub token, Copilot entitlement, and models endpoint, run:
|
|
238
257
|
|
|
@@ -267,7 +286,7 @@ loopback endpoint directly:
|
|
|
267
286
|
curl -s http://127.0.0.1:2026/_ccdx/status | jq
|
|
268
287
|
```
|
|
269
288
|
|
|
270
|
-
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:
|
|
271
290
|
|
|
272
291
|
```bash
|
|
273
292
|
ccdx --show-request-id
|
|
@@ -287,9 +306,13 @@ ccdx
|
|
|
287
306
|
Claude App support is opt-in so the default Codex Desktop and Claude Code setup stays unchanged:
|
|
288
307
|
|
|
289
308
|
```bash
|
|
290
|
-
npx codex-copilot-dx@latest --configure-claude-
|
|
309
|
+
npx codex-copilot-dx@latest start --configure-claude-app
|
|
291
310
|
```
|
|
292
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
|
+
|
|
293
316
|
Or set:
|
|
294
317
|
|
|
295
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") {
|
|
@@ -125,6 +125,11 @@ if (CLI.command === "update") {
|
|
|
125
125
|
}
|
|
126
126
|
if (CLI.command === "pms") {
|
|
127
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
|
+
}
|
|
128
133
|
const { runPmStudioSetup } = await import("../src/pm-studio-setup.mjs");
|
|
129
134
|
await runPmStudioSetup({ commandName: CLI_NAME });
|
|
130
135
|
process.exit(0);
|
|
@@ -206,7 +211,7 @@ async function reuseRunningAdapterIfAvailable() {
|
|
|
206
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"));
|
|
207
212
|
}
|
|
208
213
|
} else {
|
|
209
|
-
console.log(status("ok", "Claude App support available with --configure-claude-
|
|
214
|
+
console.log(status("ok", "Claude App support available with --configure-claude-app"));
|
|
210
215
|
}
|
|
211
216
|
|
|
212
217
|
await openCodex();
|
|
@@ -270,6 +275,7 @@ try {
|
|
|
270
275
|
codexClient: profileRuntime.codexClient,
|
|
271
276
|
claudeClient: profileRuntime.claudeClient,
|
|
272
277
|
claudeMode: profileRuntime.claudeMode,
|
|
278
|
+
codexCredentialFingerprint: profileRuntime.codexCredentialFingerprint,
|
|
273
279
|
claudeCredentialFingerprint: profileRuntime.claudeCredentialFingerprint,
|
|
274
280
|
timeoutMs: MODEL_REFRESH_TIMEOUT_MS,
|
|
275
281
|
commandName: CLI_NAME,
|
|
@@ -329,7 +335,7 @@ try {
|
|
|
329
335
|
console.log(status("ok", `Configured Claude App gateway profile at ${result.baseUrl}`));
|
|
330
336
|
console.log(formatClaudeDesktopApplyResult(result));
|
|
331
337
|
} else {
|
|
332
|
-
console.log(status("ok", "Claude App support available with --configure-claude-
|
|
338
|
+
console.log(status("ok", "Claude App support available with --configure-claude-app"));
|
|
333
339
|
}
|
|
334
340
|
|
|
335
341
|
// Launch Codex when available.
|
|
@@ -345,8 +351,10 @@ try {
|
|
|
345
351
|
modelRefreshTimer.unref?.();
|
|
346
352
|
}
|
|
347
353
|
|
|
354
|
+
const readyClients = ["Codex App", "Claude Code"];
|
|
355
|
+
if (claudeDesktopApiKey) readyClients.splice(1, 0, "Claude App");
|
|
348
356
|
console.log(`
|
|
349
|
-
${status("ok",
|
|
357
|
+
${status("ok", `Ready, ${readyClients.join(", ")} ${readyClients.length === 1 ? "is" : "are"} ready to use`)}
|
|
350
358
|
|
|
351
359
|
Adapter: ${adapterBaseUrl(ADAPTER_HOST, ADAPTER_PORT)}
|
|
352
360
|
|
package/package.json
CHANGED
package/scripts/benchmark.mjs
CHANGED
|
@@ -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/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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
|
package/src/cli-options.mjs
CHANGED
|
@@ -1,7 +1,29 @@
|
|
|
1
1
|
const HELP_COMMANDS = new Set(["help", "--help", "-h"]);
|
|
2
2
|
const VERSION_COMMANDS = new Set(["version", "--version", "-v"]);
|
|
3
|
-
const START_OPTIONS = new
|
|
3
|
+
const START_OPTIONS = new Map([
|
|
4
|
+
["--configure-claude-desktop", "configure-claude-app"],
|
|
5
|
+
["--configure-claude-app", "configure-claude-app"],
|
|
6
|
+
["--show-request-id", "show-request-id"],
|
|
7
|
+
]);
|
|
4
8
|
const CLI_COMMANDS = new Set(["ccdx", "codex-copilot-dx"]);
|
|
9
|
+
const PM_COMMANDS = new Set(["pms", "pm-studio"]);
|
|
10
|
+
const HELP_TOPICS = new Set([
|
|
11
|
+
"",
|
|
12
|
+
"start",
|
|
13
|
+
"auth",
|
|
14
|
+
"auth status",
|
|
15
|
+
"auth login",
|
|
16
|
+
"doctor",
|
|
17
|
+
"status",
|
|
18
|
+
"models",
|
|
19
|
+
"pms",
|
|
20
|
+
"pms setup",
|
|
21
|
+
"pms status",
|
|
22
|
+
"usage",
|
|
23
|
+
"auto-review-model",
|
|
24
|
+
"update",
|
|
25
|
+
"version",
|
|
26
|
+
]);
|
|
5
27
|
|
|
6
28
|
function unexpectedArgs(args) {
|
|
7
29
|
throw new Error(`Unexpected argument${args.length === 1 ? "" : "s"}: ${args.join(" ")}`);
|
|
@@ -39,40 +61,97 @@ function checkedProfile(value, { allowAll = false } = {}) {
|
|
|
39
61
|
return profile;
|
|
40
62
|
}
|
|
41
63
|
|
|
64
|
+
function baseCommand(command, extra = {}) {
|
|
65
|
+
return {
|
|
66
|
+
command,
|
|
67
|
+
configureClaudeDesktop: false,
|
|
68
|
+
showRequestId: false,
|
|
69
|
+
online: false,
|
|
70
|
+
compat: false,
|
|
71
|
+
...extra,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function checkedHelpTopic(parts = []) {
|
|
76
|
+
const normalized = parts.map((part, index) => {
|
|
77
|
+
if (index === 0 && part === "pm-studio") return "pms";
|
|
78
|
+
return part;
|
|
79
|
+
}).join(" ");
|
|
80
|
+
if (!HELP_TOPICS.has(normalized)) throw new Error(`Unknown help topic: ${parts.join(" ")}`);
|
|
81
|
+
return normalized;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function helpCommand(parts = []) {
|
|
85
|
+
return baseCommand("help", { helpTopic: checkedHelpTopic(parts) });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function nestedHelp(command, rest) {
|
|
89
|
+
if (!rest.length || !HELP_COMMANDS.has(rest.at(-1))) return null;
|
|
90
|
+
const args = rest.slice(0, -1);
|
|
91
|
+
if (PM_COMMANDS.has(command)) {
|
|
92
|
+
if (args.length > 1) unexpectedArgs(args.slice(1));
|
|
93
|
+
return helpCommand(["pms", ...args]);
|
|
94
|
+
}
|
|
95
|
+
if (command === "auth") {
|
|
96
|
+
if (args[0] === "login" && args[1] === "claude" && args.length === 2) {
|
|
97
|
+
return helpCommand(["auth", "login"]);
|
|
98
|
+
}
|
|
99
|
+
if (args.length > 1) unexpectedArgs(args.slice(1));
|
|
100
|
+
return helpCommand(["auth", ...args]);
|
|
101
|
+
}
|
|
102
|
+
if (args.length) unexpectedArgs(args);
|
|
103
|
+
return helpCommand([command]);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function parseStartOptions(args) {
|
|
107
|
+
const seen = new Set();
|
|
108
|
+
for (const option of args) {
|
|
109
|
+
const kind = START_OPTIONS.get(option);
|
|
110
|
+
if (!kind || seen.has(kind)) unexpectedArgs([option]);
|
|
111
|
+
seen.add(kind);
|
|
112
|
+
}
|
|
113
|
+
return baseCommand("start", {
|
|
114
|
+
configureClaudeDesktop: seen.has("configure-claude-app"),
|
|
115
|
+
showRequestId: seen.has("show-request-id"),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
42
119
|
export function parseCliArgs(args = []) {
|
|
43
120
|
const [command, ...rest] = args;
|
|
44
|
-
if (!command) return
|
|
121
|
+
if (!command) return baseCommand("start");
|
|
45
122
|
if (HELP_COMMANDS.has(command)) {
|
|
46
|
-
|
|
47
|
-
|
|
123
|
+
const topic = rest[0] === "auth" && rest[1] === "login" && rest[2] === "claude"
|
|
124
|
+
? ["auth", "login"]
|
|
125
|
+
: rest;
|
|
126
|
+
if (topic.length > 2) unexpectedArgs(topic.slice(2));
|
|
127
|
+
return helpCommand(topic);
|
|
48
128
|
}
|
|
129
|
+
const requestedHelp = nestedHelp(command, rest);
|
|
130
|
+
if (requestedHelp) return requestedHelp;
|
|
49
131
|
if (VERSION_COMMANDS.has(command)) {
|
|
50
132
|
if (rest.length) unexpectedArgs(rest);
|
|
51
|
-
return
|
|
133
|
+
return baseCommand("version");
|
|
52
134
|
}
|
|
135
|
+
if (command === "start") return parseStartOptions(rest);
|
|
53
136
|
if (command === "status" || command === "--status") {
|
|
54
137
|
if (rest.length) unexpectedArgs(rest);
|
|
55
|
-
return
|
|
138
|
+
return baseCommand("status");
|
|
56
139
|
}
|
|
57
140
|
if (command === "usage") {
|
|
58
141
|
if (rest.length) unexpectedArgs(rest);
|
|
59
|
-
return
|
|
142
|
+
return baseCommand("usage");
|
|
60
143
|
}
|
|
61
|
-
if (command
|
|
144
|
+
if (PM_COMMANDS.has(command)) {
|
|
62
145
|
const [action, ...pmsArgs] = rest;
|
|
63
|
-
if (!action) throw new Error("Missing pms action: expected setup");
|
|
64
|
-
if (
|
|
146
|
+
if (!action) throw new Error("Missing pms action: expected setup or status");
|
|
147
|
+
if (!new Set(["setup", "status"]).has(action)) throw new Error(`Unknown pms action: ${action}`);
|
|
65
148
|
if (pmsArgs.length) unexpectedArgs(pmsArgs);
|
|
66
|
-
return
|
|
149
|
+
return baseCommand("pms", { action });
|
|
67
150
|
}
|
|
68
151
|
if (command === "models") {
|
|
69
152
|
const options = parseOptions(rest, new Map([["--profile", "value"]]));
|
|
70
153
|
return {
|
|
71
|
-
|
|
72
|
-
configureClaudeDesktop: false,
|
|
73
|
-
showRequestId: false,
|
|
74
|
-
online: false,
|
|
75
|
-
compat: false,
|
|
154
|
+
...baseCommand("models"),
|
|
76
155
|
profile: checkedProfile(options["--profile"]),
|
|
77
156
|
};
|
|
78
157
|
}
|
|
@@ -82,13 +161,10 @@ export function parseCliArgs(args = []) {
|
|
|
82
161
|
if (action === "status") {
|
|
83
162
|
const options = parseOptions(authArgs, new Map([["--online", "flag"]]));
|
|
84
163
|
return {
|
|
85
|
-
|
|
164
|
+
...baseCommand("auth"),
|
|
86
165
|
action: "status",
|
|
87
166
|
profile: "",
|
|
88
|
-
configureClaudeDesktop: false,
|
|
89
|
-
showRequestId: false,
|
|
90
167
|
online: Boolean(options["--online"]),
|
|
91
|
-
compat: false,
|
|
92
168
|
expectedLogin: "",
|
|
93
169
|
reauth: false,
|
|
94
170
|
};
|
|
@@ -102,13 +178,9 @@ export function parseCliArgs(args = []) {
|
|
|
102
178
|
["--reauth", "flag"],
|
|
103
179
|
]));
|
|
104
180
|
return {
|
|
105
|
-
|
|
181
|
+
...baseCommand("auth"),
|
|
106
182
|
action: "login",
|
|
107
183
|
profile: "claude",
|
|
108
|
-
configureClaudeDesktop: false,
|
|
109
|
-
showRequestId: false,
|
|
110
|
-
online: false,
|
|
111
|
-
compat: false,
|
|
112
184
|
expectedLogin: options["--github-login"] || "",
|
|
113
185
|
reauth: Boolean(options["--reauth"]),
|
|
114
186
|
};
|
|
@@ -117,7 +189,7 @@ export function parseCliArgs(args = []) {
|
|
|
117
189
|
}
|
|
118
190
|
if (command === "auto-review-model") {
|
|
119
191
|
if (rest.length) unexpectedArgs(rest);
|
|
120
|
-
return
|
|
192
|
+
return baseCommand("auto-review-model");
|
|
121
193
|
}
|
|
122
194
|
if (command === "update") {
|
|
123
195
|
if (rest.length > 1) unexpectedArgs(rest.slice(1));
|
|
@@ -126,11 +198,7 @@ export function parseCliArgs(args = []) {
|
|
|
126
198
|
throw new Error(`Update source must be npm or github: ${source}`);
|
|
127
199
|
}
|
|
128
200
|
return {
|
|
129
|
-
|
|
130
|
-
configureClaudeDesktop: false,
|
|
131
|
-
showRequestId: false,
|
|
132
|
-
online: false,
|
|
133
|
-
compat: false,
|
|
201
|
+
...baseCommand("update"),
|
|
134
202
|
updateSource: source === "gh" ? "github" : source,
|
|
135
203
|
};
|
|
136
204
|
}
|
|
@@ -141,29 +209,13 @@ export function parseCliArgs(args = []) {
|
|
|
141
209
|
["--profile", "value"],
|
|
142
210
|
]));
|
|
143
211
|
return {
|
|
144
|
-
|
|
145
|
-
configureClaudeDesktop: false,
|
|
146
|
-
showRequestId: false,
|
|
212
|
+
...baseCommand("doctor"),
|
|
147
213
|
online: Boolean(options["--online"]),
|
|
148
214
|
compat: Boolean(options["--compat"]),
|
|
149
215
|
profile: checkedProfile(options["--profile"], { allowAll: true }),
|
|
150
216
|
};
|
|
151
217
|
}
|
|
152
|
-
if (START_OPTIONS.has(command))
|
|
153
|
-
const options = [command, ...rest];
|
|
154
|
-
const invalid = options.filter((arg) => !START_OPTIONS.has(arg));
|
|
155
|
-
if (invalid.length) unexpectedArgs(invalid);
|
|
156
|
-
for (const option of START_OPTIONS) {
|
|
157
|
-
if (options.filter((arg) => arg === option).length > 1) unexpectedArgs([option]);
|
|
158
|
-
}
|
|
159
|
-
return {
|
|
160
|
-
command: "start",
|
|
161
|
-
configureClaudeDesktop: options.includes("--configure-claude-desktop"),
|
|
162
|
-
showRequestId: options.includes("--show-request-id"),
|
|
163
|
-
online: false,
|
|
164
|
-
compat: false,
|
|
165
|
-
};
|
|
166
|
-
}
|
|
218
|
+
if (START_OPTIONS.has(command)) return parseStartOptions([command, ...rest]);
|
|
167
219
|
throw new Error(`Unknown command or option: ${command}`);
|
|
168
220
|
}
|
|
169
221
|
|
|
@@ -207,16 +259,39 @@ export function cliCommandName(executable = process.argv[1]) {
|
|
|
207
259
|
return CLI_COMMANDS.has(name) ? name : "ccdx";
|
|
208
260
|
}
|
|
209
261
|
|
|
210
|
-
|
|
262
|
+
function topicHelp(name, topic) {
|
|
263
|
+
const sections = {
|
|
264
|
+
start: `Usage:\n ${name} [start] [--configure-claude-app] [--show-request-id]\n\nStarts or reuses the local adapter, updates Codex and Claude Code configuration, and opens Codex.\n--configure-claude-app also creates or updates the managed Claude App gateway profile.`,
|
|
265
|
+
auth: `Usage:\n ${name} auth status [--online]\n ${name} auth login claude [--github-login <login>] [--reauth]\n\nShows the two account profiles or configures the isolated Claude account. Device login starts only when no reusable local Copilot credential is available or --reauth is used.`,
|
|
266
|
+
"auth status": `Usage:\n ${name} auth status [--online]\n\nShows account routing without exposing credentials. --online also verifies both configured Copilot entitlements and model catalogs.`,
|
|
267
|
+
"auth login": `Usage:\n ${name} auth login claude [--github-login <login>] [--reauth]\n\nReuses a compatible local Copilot credential when possible. --reauth forces GitHub Device Flow.`,
|
|
268
|
+
doctor: `Usage:\n ${name} doctor [--online] [--compat] [--profile codex|claude|all]\n\nChecks local credentials, client configuration, PM Studio state, and the adapter. --online validates account entitlement; --compat sends minimal inference requests and consumes a small amount of Copilot usage.`,
|
|
269
|
+
status: `Usage:\n ${name} status\n\nShows bounded runtime, routing, performance, cache, queue, and PM relay metrics from a running adapter.`,
|
|
270
|
+
models: `Usage:\n ${name} models [--profile codex|claude]\n\nPerforms a fresh, read-only Copilot model-directory lookup for one saved profile.`,
|
|
271
|
+
pms: `Usage:\n ${name} pms status\n ${name} pms setup\n ${name} pm-studio status\n ${name} pm-studio setup\n\nInspects or installs the version-locked PM Studio loopback patch. setup modifies the installed app only after all preflight checks pass.`,
|
|
272
|
+
"pms setup": `Usage:\n ${name} pms setup\n\nBacks up, patches, verifies, and ad-hoc signs an exactly supported PM Studio bundle. Unknown versions and drift are never modified.`,
|
|
273
|
+
"pms status": `Usage:\n ${name} pms status\n\nRead-only inspection of the installed PM Studio version, patch integrity, Claude profile, and relay availability.`,
|
|
274
|
+
usage: `Usage:\n ${name} usage\n\nSummarizes local token usage metadata without reading prompt or completion content.`,
|
|
275
|
+
"auto-review-model": `Usage:\n ${name} auto-review-model\n\nInteractively selects an enabled Responses model for Codex Auto-review.`,
|
|
276
|
+
update: `Usage:\n ${name} update [npm|github]\n\nUpdates the global package from the configured npm registry or GitHub main. With no source, an interactive terminal prompts for one.`,
|
|
277
|
+
version: `Usage:\n ${name} --version`,
|
|
278
|
+
};
|
|
279
|
+
return sections[topic];
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export function cliHelp(commandName = "ccdx", topic = "") {
|
|
211
283
|
const name = CLI_COMMANDS.has(commandName) ? commandName : "ccdx";
|
|
212
284
|
const alias = name === "ccdx" ? "codex-copilot-dx" : "ccdx";
|
|
285
|
+
const normalizedTopic = checkedHelpTopic(topic ? topic.split(" ") : []);
|
|
286
|
+
if (normalizedTopic) return `${topicHelp(name, normalizedTopic)}\n\nEquivalent command: ${alias}`;
|
|
213
287
|
return `Usage:
|
|
214
|
-
${name} [--configure-claude-
|
|
288
|
+
${name} [start] [--configure-claude-app] [--show-request-id]
|
|
215
289
|
${name} auth status [--online]
|
|
216
290
|
${name} auth login claude [--github-login <login>] [--reauth]
|
|
217
291
|
${name} doctor [--online] [--compat] [--profile codex|claude|all]
|
|
218
292
|
${name} status
|
|
219
293
|
${name} models [--profile codex|claude]
|
|
294
|
+
${name} pms status
|
|
220
295
|
${name} pms setup
|
|
221
296
|
${name} usage
|
|
222
297
|
${name} auto-review-model
|
|
@@ -224,5 +299,18 @@ export function cliHelp(commandName = "ccdx") {
|
|
|
224
299
|
${name} --version
|
|
225
300
|
${name} --help
|
|
226
301
|
|
|
302
|
+
Commands:
|
|
303
|
+
start Start or reuse the adapter and configure local clients
|
|
304
|
+
auth Inspect or configure the Codex and Claude accounts
|
|
305
|
+
doctor Diagnose local config; optional live and inference checks
|
|
306
|
+
status Show runtime routing, performance, queue, and cache health
|
|
307
|
+
models Query a saved account's live Copilot model catalog
|
|
308
|
+
pms, pm-studio Inspect or install the PM Studio patch
|
|
309
|
+
usage Summarize locally recorded token usage
|
|
310
|
+
auto-review-model Select the Codex Auto-review Responses model
|
|
311
|
+
update Update the global package from npm or GitHub
|
|
312
|
+
|
|
313
|
+
Run ${name} <command> --help for command details.
|
|
314
|
+
|
|
227
315
|
Equivalent command: ${alias}`;
|
|
228
316
|
}
|
package/src/cli-status.mjs
CHANGED
|
@@ -70,6 +70,33 @@ function routingTarget(value) {
|
|
|
70
70
|
return "unknown";
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
function pmRouteSummary(requests = {}) {
|
|
74
|
+
const routes = requests.by_route;
|
|
75
|
+
if (!routes || typeof routes !== "object") return null;
|
|
76
|
+
const names = ["pm_models", "pm_chat_completions", "pm_responses", "pm_embeddings"];
|
|
77
|
+
const totals = names.reduce((summary, name) => {
|
|
78
|
+
const route = routes[name] || {};
|
|
79
|
+
summary.total += finiteNumber(route.total) || 0;
|
|
80
|
+
summary.active += finiteNumber(route.active) || 0;
|
|
81
|
+
summary.errors += finiteNumber(route.errors) || 0;
|
|
82
|
+
return summary;
|
|
83
|
+
}, { total: 0, active: 0, errors: 0 });
|
|
84
|
+
return `PM relay: ${count(totals.total)} requests, ${count(totals.active)} active, ${count(totals.errors)} errors; models ${count(routes.pm_models?.total)}, chat ${count(routes.pm_chat_completions?.total)}, responses ${count(routes.pm_responses?.total)}, embeddings ${count(routes.pm_embeddings?.total)}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function errorCode(error) {
|
|
88
|
+
const seen = new Set();
|
|
89
|
+
const pending = [error];
|
|
90
|
+
while (pending.length) {
|
|
91
|
+
const current = pending.shift();
|
|
92
|
+
if (!current || typeof current !== "object" || seen.has(current)) continue;
|
|
93
|
+
seen.add(current);
|
|
94
|
+
if (typeof current.code === "string") return current.code;
|
|
95
|
+
pending.push(current.cause, ...(Array.isArray(current.errors) ? current.errors : []));
|
|
96
|
+
}
|
|
97
|
+
return "";
|
|
98
|
+
}
|
|
99
|
+
|
|
73
100
|
export async function readAdapterStatus({
|
|
74
101
|
host = "127.0.0.1",
|
|
75
102
|
port = 2026,
|
|
@@ -101,12 +128,16 @@ export async function readAdapterStatus({
|
|
|
101
128
|
}
|
|
102
129
|
return { baseUrl, data };
|
|
103
130
|
} catch (error) {
|
|
131
|
+
const code = errorCode(error);
|
|
104
132
|
const reason = ctrl.signal.aborted
|
|
105
133
|
? `timed out after ${timeoutMs}ms`
|
|
106
|
-
:
|
|
134
|
+
: code === "ECONNREFUSED" || code === "ENETUNREACH"
|
|
107
135
|
? "adapter is not running"
|
|
108
136
|
: error?.message || String(error);
|
|
109
|
-
|
|
137
|
+
const nextStep = reason === "adapter is not running"
|
|
138
|
+
? `. Run ccdx start, then retry ccdx status`
|
|
139
|
+
: "";
|
|
140
|
+
throw new Error(`Could not read adapter status at ${baseUrl}: ${reason}${nextStep}`);
|
|
110
141
|
} finally {
|
|
111
142
|
clearTimeout(timer);
|
|
112
143
|
}
|
|
@@ -143,6 +174,8 @@ export function formatAdapterStatus({ baseUrl, data }, { commandName = "ccdx", c
|
|
|
143
174
|
if (data.routing) {
|
|
144
175
|
lines.push(status("info", `Routing: /v1/responses -> ${routingTarget(data.routing.responses)}; /v1/messages -> ${routingTarget(data.routing.messages)}`));
|
|
145
176
|
}
|
|
177
|
+
const pmSummary = pmRouteSummary(requests);
|
|
178
|
+
if (pmSummary) lines.push(status("info", pmSummary));
|
|
146
179
|
lines.push(status("info", `Limits: request body ${mebibytes(limits.max_body_bytes)}, decoded body ${mebibytes(limits.max_decoded_body_bytes)}`));
|
|
147
180
|
return lines.join("\n");
|
|
148
181
|
}
|
package/src/doctor.mjs
CHANGED
|
@@ -251,7 +251,11 @@ export function inspectAuthProfiles({ home = os.homedir() } = {}) {
|
|
|
251
251
|
if (!claude.configured) {
|
|
252
252
|
checks.push({ kind: "ok", message: "Claude authentication profile is not isolated and inherits Codex" });
|
|
253
253
|
} else if (!claude.valid) {
|
|
254
|
-
checks.push({
|
|
254
|
+
checks.push({
|
|
255
|
+
kind: "err",
|
|
256
|
+
message: `Claude isolated authentication profile is invalid: ${claude.reason}`,
|
|
257
|
+
fix: "ccdx auth login claude --reauth",
|
|
258
|
+
});
|
|
255
259
|
} else {
|
|
256
260
|
const account = claude.identity?.login ? ` for ${claude.identity.login}` : "";
|
|
257
261
|
checks.push({ kind: "ok", message: `Claude isolated authentication profile is configured${account}` });
|
|
@@ -455,7 +459,11 @@ export function inspectClaudeAppConfig({
|
|
|
455
459
|
const meta = readJson(paths.metaPath);
|
|
456
460
|
const appliedId = meta.ok ? String(meta.json?.appliedId || "").trim() : "";
|
|
457
461
|
if (!appliedId) {
|
|
458
|
-
checks.push({
|
|
462
|
+
checks.push({
|
|
463
|
+
kind: "warn",
|
|
464
|
+
message: "Claude App gateway profile is not configured",
|
|
465
|
+
fix: "ccdx start --configure-claude-app",
|
|
466
|
+
});
|
|
459
467
|
return checks;
|
|
460
468
|
}
|
|
461
469
|
|
|
@@ -508,6 +516,7 @@ export async function collectDoctorChecks({
|
|
|
508
516
|
host = "127.0.0.1",
|
|
509
517
|
port = 2026,
|
|
510
518
|
checkAdapter = true,
|
|
519
|
+
checkPmStudio = checkAdapter,
|
|
511
520
|
checkAdapterListeningFn = checkAdapterListening,
|
|
512
521
|
checkRunningAdapterFn = checkRunningAdapter,
|
|
513
522
|
online = false,
|
|
@@ -517,6 +526,7 @@ export async function collectDoctorChecks({
|
|
|
517
526
|
onlineTimeoutMs = 10000,
|
|
518
527
|
compatTimeoutMs = 120000,
|
|
519
528
|
inspectAdapterCompatibilityFn = inspectAdapterCompatibility,
|
|
529
|
+
inspectPmStudioHealthFn,
|
|
520
530
|
} = {}) {
|
|
521
531
|
const checks = [
|
|
522
532
|
...inspectAuthProfiles({ home }),
|
|
@@ -552,7 +562,43 @@ export async function collectDoctorChecks({
|
|
|
552
562
|
const listening = await checkAdapterListeningFn({ host, port });
|
|
553
563
|
checks.push(listening
|
|
554
564
|
? { kind: "warn", message: `A service is listening on ${localGatewayBaseUrl(host, port)}, but it is not a compatible codex-copilot-dx adapter` }
|
|
555
|
-
: {
|
|
565
|
+
: {
|
|
566
|
+
kind: "warn",
|
|
567
|
+
message: `Adapter is not listening on ${localGatewayBaseUrl(host, port)}`,
|
|
568
|
+
fix: "ccdx start",
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
if (checkPmStudio && platform === "darwin") {
|
|
574
|
+
const inspect = inspectPmStudioHealthFn || (async (options) => {
|
|
575
|
+
const module = await import("./pm-studio-status.mjs");
|
|
576
|
+
return module.inspectPmStudioStatus(options);
|
|
577
|
+
});
|
|
578
|
+
const pm = await inspect({ home });
|
|
579
|
+
const version = pm.app.metadata ? `${pm.app.metadata.version} build ${pm.app.metadata.build}` : "";
|
|
580
|
+
if (pm.app.state === "patched") {
|
|
581
|
+
checks.push({ kind: "ok", message: `PM Studio ${version} patch is verified` });
|
|
582
|
+
if (!pm.claude.valid) {
|
|
583
|
+
checks.push({
|
|
584
|
+
kind: "warn",
|
|
585
|
+
message: "PM Studio Claude routing requires a valid isolated Claude profile",
|
|
586
|
+
fix: "ccdx auth login claude --reauth",
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
if (!pm.adapter?.ok) {
|
|
590
|
+
checks.push({ kind: "warn", message: "PM Studio relay is not currently available", fix: "ccdx start" });
|
|
591
|
+
}
|
|
592
|
+
} else if (pm.app.state === "clean") {
|
|
593
|
+
checks.push({
|
|
594
|
+
kind: "warn",
|
|
595
|
+
message: `PM Studio ${version} is supported but not patched`,
|
|
596
|
+
fix: "ccdx pms setup",
|
|
597
|
+
});
|
|
598
|
+
} else if (pm.app.state === "unsupported") {
|
|
599
|
+
checks.push({ kind: "warn", message: `PM Studio ${version} has no exact patch recipe; no files will be changed` });
|
|
600
|
+
} else if (pm.app.state === "drift" || pm.app.state === "error") {
|
|
601
|
+
checks.push({ kind: "err", message: `PM Studio integrity check failed: ${pm.app.issues.join("; ")}` });
|
|
556
602
|
}
|
|
557
603
|
}
|
|
558
604
|
|
|
@@ -604,5 +650,12 @@ export async function runDoctor(options = {}) {
|
|
|
604
650
|
log(`${options.commandName || "ccdx"} doctor${flags.length ? ` ${flags.join(" ")}` : ""}`);
|
|
605
651
|
const checks = await collectDoctorChecks(options);
|
|
606
652
|
for (const check of checks) log(status(check.kind, check.message));
|
|
653
|
+
const totals = { ok: 0, warn: 0, err: 0 };
|
|
654
|
+
for (const check of checks) totals[check.kind] = (totals[check.kind] || 0) + 1;
|
|
655
|
+
log(status(totals.err ? "err" : totals.warn ? "warn" : "ok",
|
|
656
|
+
`Summary: ${totals.ok} passed, ${totals.warn} warning(s), ${totals.err} error(s)`));
|
|
657
|
+
for (const fix of new Set(checks.map((check) => check.fix).filter(Boolean))) {
|
|
658
|
+
log(status("info", `Next step: ${fix}`));
|
|
659
|
+
}
|
|
607
660
|
return checks;
|
|
608
661
|
}
|
package/src/observability.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { copilotRuntimeStatus } from "./copilot.mjs";
|
|
|
4
4
|
import { imageOptimizationStats } from "./image-optimization.mjs";
|
|
5
5
|
import { responseHistoryStats } from "./response-history.mjs";
|
|
6
6
|
import { loadRuntimeConfig } from "./runtime-config.mjs";
|
|
7
|
+
import { profileRouting } from "./profile-routing.mjs";
|
|
7
8
|
|
|
8
9
|
export { ADAPTER_STATUS_PATH };
|
|
9
10
|
const OBSERVABILITY_RUNTIME_CONFIG = loadRuntimeConfig();
|
|
@@ -191,10 +192,7 @@ export function runtimeStatusPayload({
|
|
|
191
192
|
codex: { mode: "legacy", client: codexRuntime, models: codexModels },
|
|
192
193
|
claude: { mode: resolvedClaudeMode, client: claudeRuntime, models: claudeModels },
|
|
193
194
|
},
|
|
194
|
-
routing: {
|
|
195
|
-
responses: "codex",
|
|
196
|
-
messages: resolvedClaudeMode === "isolated" ? "claude" : "codex",
|
|
197
|
-
},
|
|
195
|
+
routing: profileRouting({ claudeMode: resolvedClaudeMode }),
|
|
198
196
|
limits: {
|
|
199
197
|
max_body_bytes: OBSERVABILITY_RUNTIME_CONFIG.maxBodyBytes,
|
|
200
198
|
max_decoded_body_bytes: OBSERVABILITY_RUNTIME_CONFIG.maxDecodedBodyBytes,
|
package/src/pm-studio-relay.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
writeOrDrain,
|
|
11
11
|
} from "./http-transport.mjs";
|
|
12
12
|
import { isLoopbackAddress } from "./observability.mjs";
|
|
13
|
+
import { createPmStudioModelRouter } from "./profile-routing.mjs";
|
|
13
14
|
import { loadRuntimeConfig, parsePositiveInteger } from "./runtime-config.mjs";
|
|
14
15
|
import { requireUpstreamEventStream } from "./stream-contract.mjs";
|
|
15
16
|
import { safeUpstreamResponseHeaders } from "./upstream-headers.mjs";
|
|
@@ -147,48 +148,6 @@ function catalogData(catalog) {
|
|
|
147
148
|
return Array.isArray(catalog?.data) ? catalog.data : null;
|
|
148
149
|
}
|
|
149
150
|
|
|
150
|
-
function originalClaudeModels(registry) {
|
|
151
|
-
return catalogData(registry?.models) || [];
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function isClaudeCatalogEntry(model) {
|
|
155
|
-
const id = String(model?.id || "").trim();
|
|
156
|
-
const vendor = String(model?.vendor || "").trim().toLowerCase();
|
|
157
|
-
return Boolean(id) && (vendor === "anthropic" || id.toLowerCase().startsWith("claude-"));
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function isAllowedClaudeModel(model) {
|
|
161
|
-
return isClaudeCatalogEntry(model)
|
|
162
|
-
&& String(model?.vendor || "").trim().toLowerCase() === "anthropic"
|
|
163
|
-
&& model?.model_picker_enabled !== false
|
|
164
|
-
&& Array.isArray(model?.supported_endpoints)
|
|
165
|
-
&& model.supported_endpoints.includes("/chat/completions");
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function claudeAvailability(options) {
|
|
169
|
-
const enabled = options.claudeMode === "isolated"
|
|
170
|
-
&& options.claudeProfile?.valid === true
|
|
171
|
-
&& typeof options.claudeClient?.chatCompletions === "function";
|
|
172
|
-
const catalog = originalClaudeModels(options.claudeModelRegistry);
|
|
173
|
-
const allowed = new Map();
|
|
174
|
-
const known = new Set();
|
|
175
|
-
for (const model of catalog) {
|
|
176
|
-
const id = String(model?.id || "").trim();
|
|
177
|
-
if (!id || !isClaudeCatalogEntry(model)) continue;
|
|
178
|
-
known.add(id);
|
|
179
|
-
if (enabled && isAllowedClaudeModel(model) && !allowed.has(id)) allowed.set(id, model);
|
|
180
|
-
}
|
|
181
|
-
return { allowed, known };
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function classifyModel(modelId, options) {
|
|
185
|
-
const id = String(modelId || "").trim();
|
|
186
|
-
const { allowed, known } = claudeAvailability(options);
|
|
187
|
-
if (allowed.has(id)) return "claude";
|
|
188
|
-
if (known.has(id) || id.toLowerCase().startsWith("claude-")) return "unsupported_claude";
|
|
189
|
-
return "enterprise";
|
|
190
|
-
}
|
|
191
|
-
|
|
192
151
|
async function responseSnapshot(response) {
|
|
193
152
|
return {
|
|
194
153
|
status: response.status,
|
|
@@ -218,10 +177,10 @@ function rejectUpstreamRedirect(response) {
|
|
|
218
177
|
return response;
|
|
219
178
|
}
|
|
220
179
|
|
|
221
|
-
function mergeModelCatalog(snapshot,
|
|
180
|
+
function mergeModelCatalog(snapshot, modelRouter) {
|
|
222
181
|
if (!snapshot.ok) return null;
|
|
223
|
-
const
|
|
224
|
-
if (!allowed.
|
|
182
|
+
const allowed = modelRouter.allowedModels();
|
|
183
|
+
if (!allowed.length) return null;
|
|
225
184
|
let enterprise;
|
|
226
185
|
try {
|
|
227
186
|
enterprise = JSON.parse(snapshot.body.toString("utf8"));
|
|
@@ -232,7 +191,8 @@ function mergeModelCatalog(snapshot, options) {
|
|
|
232
191
|
if (!data) return null;
|
|
233
192
|
const seen = new Set(data.map((model) => String(model?.id || "").trim()).filter(Boolean));
|
|
234
193
|
const additions = [];
|
|
235
|
-
for (const
|
|
194
|
+
for (const model of allowed) {
|
|
195
|
+
const id = String(model?.id || "").trim();
|
|
236
196
|
if (seen.has(id)) continue;
|
|
237
197
|
seen.add(id);
|
|
238
198
|
additions.push(model);
|
|
@@ -332,6 +292,12 @@ export function createPmStudioRelayHandler(options = {}) {
|
|
|
332
292
|
const validated = new Map();
|
|
333
293
|
const validationFlights = new Map();
|
|
334
294
|
let activeModelRequests = 0;
|
|
295
|
+
const modelRouter = createPmStudioModelRouter({
|
|
296
|
+
getCatalog: () => options.claudeModelRegistry?.models,
|
|
297
|
+
isClaudeEnabled: () => options.claudeMode === "isolated"
|
|
298
|
+
&& options.claudeProfile?.valid === true
|
|
299
|
+
&& typeof options.claudeClient?.chatCompletions === "function",
|
|
300
|
+
});
|
|
335
301
|
|
|
336
302
|
function isValidated(fingerprint) {
|
|
337
303
|
const expiresAt = validated.get(fingerprint);
|
|
@@ -452,7 +418,7 @@ export function createPmStudioRelayHandler(options = {}) {
|
|
|
452
418
|
sendSnapshot(res, snapshot);
|
|
453
419
|
return;
|
|
454
420
|
}
|
|
455
|
-
const merged = mergeModelCatalog(snapshot,
|
|
421
|
+
const merged = mergeModelCatalog(snapshot, modelRouter);
|
|
456
422
|
if (!merged) {
|
|
457
423
|
sendSnapshot(res, snapshot);
|
|
458
424
|
return;
|
|
@@ -480,7 +446,7 @@ export function createPmStudioRelayHandler(options = {}) {
|
|
|
480
446
|
}
|
|
481
447
|
abort.setTimeout(requestTimeoutMs, timeoutReason);
|
|
482
448
|
|
|
483
|
-
const modelType =
|
|
449
|
+
const modelType = modelRouter.classify(body?.model);
|
|
484
450
|
if (modelType === "unsupported_claude" || (modelType === "claude" && pathname !== "/chat/completions")) {
|
|
485
451
|
throw localError(400, "model_not_supported", "The requested Claude model is not available for this endpoint");
|
|
486
452
|
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
AUTH_PROFILE_CLAUDE,
|
|
6
|
+
readAuthProfileCredentials,
|
|
7
|
+
} from "./auth-profile.mjs";
|
|
8
|
+
import { PM_STUDIO_RECIPES } from "./pm-studio-asar.mjs";
|
|
9
|
+
import {
|
|
10
|
+
createPmStudioSetupOperations,
|
|
11
|
+
DEFAULT_PM_STUDIO_APP_PATH,
|
|
12
|
+
inspectPmStudioApp,
|
|
13
|
+
PM_STUDIO_CLAUDE_AUTH_COMMAND,
|
|
14
|
+
} from "./pm-studio-setup.mjs";
|
|
15
|
+
import { checkRunningAdapter } from "./running-adapter.mjs";
|
|
16
|
+
import { status } from "./status.mjs";
|
|
17
|
+
|
|
18
|
+
const PM_RELAY_HOST = "127.0.0.1";
|
|
19
|
+
const PM_RELAY_PORT = 2026;
|
|
20
|
+
|
|
21
|
+
function selectRecipe(metadata, recipes) {
|
|
22
|
+
return recipes.find((recipe) => recipe.version === String(metadata.version)
|
|
23
|
+
&& recipe.build === String(metadata.build)
|
|
24
|
+
&& recipe.bundleIdentifier === String(metadata.bundleIdentifier));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function safeMessage(error) {
|
|
28
|
+
return String(error?.message || "inspection failed").replace(/\s+/g, " ").trim();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function inspectClaudeProfile(home, readCredentials) {
|
|
32
|
+
try {
|
|
33
|
+
const credentials = readCredentials(AUTH_PROFILE_CLAUDE, { home });
|
|
34
|
+
return {
|
|
35
|
+
configured: credentials.configured === true,
|
|
36
|
+
valid: credentials.valid === true,
|
|
37
|
+
reason: credentials.reason || "",
|
|
38
|
+
login: credentials.identity?.login || "",
|
|
39
|
+
};
|
|
40
|
+
} catch (error) {
|
|
41
|
+
return { configured: true, valid: false, reason: safeMessage(error), login: "" };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function inspectPmStudioStatus({
|
|
46
|
+
appPath = DEFAULT_PM_STUDIO_APP_PATH,
|
|
47
|
+
home = os.homedir(),
|
|
48
|
+
recipes = PM_STUDIO_RECIPES,
|
|
49
|
+
operationOverrides = {},
|
|
50
|
+
existsSync = fs.existsSync,
|
|
51
|
+
inspectApp = inspectPmStudioApp,
|
|
52
|
+
readClaudeCredentials = readAuthProfileCredentials,
|
|
53
|
+
checkRunningAdapterFn = checkRunningAdapter,
|
|
54
|
+
} = {}) {
|
|
55
|
+
const operations = createPmStudioSetupOperations(operationOverrides);
|
|
56
|
+
let app;
|
|
57
|
+
if (!existsSync(appPath)) {
|
|
58
|
+
app = { state: "not_installed", metadata: null, issues: [] };
|
|
59
|
+
} else {
|
|
60
|
+
try {
|
|
61
|
+
const metadata = operations.readBundleMetadata({
|
|
62
|
+
appPath,
|
|
63
|
+
infoPlistPath: path.join(appPath, "Contents/Info.plist"),
|
|
64
|
+
processRunner: operations.processRunner,
|
|
65
|
+
});
|
|
66
|
+
const normalized = {
|
|
67
|
+
version: String(metadata.version),
|
|
68
|
+
build: String(metadata.build),
|
|
69
|
+
bundleIdentifier: String(metadata.bundleIdentifier),
|
|
70
|
+
};
|
|
71
|
+
const recipe = selectRecipe(normalized, recipes);
|
|
72
|
+
app = recipe
|
|
73
|
+
? { ...inspectApp({ appPath, recipe, operations }), recipe: recipe.id }
|
|
74
|
+
: { state: "unsupported", metadata: normalized, issues: [], recipe: "" };
|
|
75
|
+
} catch (error) {
|
|
76
|
+
app = { state: "error", metadata: null, issues: [safeMessage(error)], recipe: "" };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const claude = inspectClaudeProfile(home, readClaudeCredentials);
|
|
81
|
+
let adapter;
|
|
82
|
+
try {
|
|
83
|
+
adapter = await checkRunningAdapterFn({ host: PM_RELAY_HOST, port: PM_RELAY_PORT });
|
|
84
|
+
} catch (error) {
|
|
85
|
+
adapter = { ok: false, error };
|
|
86
|
+
}
|
|
87
|
+
const operational = app.state === "patched" && claude.valid && adapter?.ok === true;
|
|
88
|
+
return { ok: operational, appPath, app, claude, adapter };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function appStatusLine(result, commandName) {
|
|
92
|
+
const { app } = result;
|
|
93
|
+
const version = app.metadata ? `${app.metadata.version} build ${app.metadata.build}` : "";
|
|
94
|
+
if (app.state === "patched") return status("ok", `PM Studio ${version} is patched and verified`);
|
|
95
|
+
if (app.state === "clean") return status("warn", `PM Studio ${version} is supported but not patched; run ${commandName} pms setup`);
|
|
96
|
+
if (app.state === "unsupported") return status("err", `PM Studio ${version} has no exact patch recipe; no files will be changed`);
|
|
97
|
+
if (app.state === "drift") return status("err", `PM Studio ${version} does not match the clean or patched recipe: ${app.issues.join("; ")}`);
|
|
98
|
+
if (app.state === "not_installed") return status("warn", `PM Studio is not installed at ${result.appPath}`);
|
|
99
|
+
return status("err", `PM Studio could not be inspected: ${app.issues.join("; ")}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function formatPmStudioStatus(result, { commandName = "ccdx" } = {}) {
|
|
103
|
+
const lines = [
|
|
104
|
+
`${commandName} pms status`,
|
|
105
|
+
appStatusLine(result, commandName),
|
|
106
|
+
];
|
|
107
|
+
if (result.claude.valid) {
|
|
108
|
+
const account = result.claude.login ? ` for ${result.claude.login}` : "";
|
|
109
|
+
lines.push(status("ok", `Isolated Claude profile is valid${account}`));
|
|
110
|
+
} else {
|
|
111
|
+
const reason = result.claude.configured && result.claude.reason ? ` (${result.claude.reason})` : "";
|
|
112
|
+
lines.push(status("err", `Isolated Claude profile is not ready${reason}; run ${PM_STUDIO_CLAUDE_AUTH_COMMAND}`));
|
|
113
|
+
}
|
|
114
|
+
if (result.adapter?.ok) {
|
|
115
|
+
lines.push(status("ok", `PM relay is available at ${result.adapter.baseUrl}`));
|
|
116
|
+
} else if (result.adapter?.incompatible) {
|
|
117
|
+
lines.push(status("err", `The adapter at ${result.adapter.baseUrl} is incompatible; stop it and run ${commandName} start`));
|
|
118
|
+
} else {
|
|
119
|
+
lines.push(status("warn", `PM relay is not running at http://${PM_RELAY_HOST}:${PM_RELAY_PORT}; run ${commandName} start`));
|
|
120
|
+
}
|
|
121
|
+
lines.push(status("info", "Routing: PM GPT uses the PM Studio bearer; eligible Claude chat uses the isolated Claude profile"));
|
|
122
|
+
return lines.join("\n");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function runPmStudioStatus(options = {}) {
|
|
126
|
+
const result = await inspectPmStudioStatus(options);
|
|
127
|
+
const output = formatPmStudioStatus(result, options);
|
|
128
|
+
(options.log || console.log)(output);
|
|
129
|
+
return { ...result, output };
|
|
130
|
+
}
|
|
@@ -33,6 +33,7 @@ export function createProfileModelRuntime({
|
|
|
33
33
|
codexClient,
|
|
34
34
|
claudeClient = codexClient,
|
|
35
35
|
claudeMode = "inherited",
|
|
36
|
+
codexCredentialFingerprint = "",
|
|
36
37
|
claudeCredentialFingerprint = "",
|
|
37
38
|
home,
|
|
38
39
|
env = process.env,
|
|
@@ -93,7 +94,7 @@ export function createProfileModelRuntime({
|
|
|
93
94
|
function loadCached({ client, profile, registry, ownsClaudeModels }) {
|
|
94
95
|
const credentialFingerprint = profile === "claude"
|
|
95
96
|
? String(claudeCredentialFingerprint || "").trim()
|
|
96
|
-
: "";
|
|
97
|
+
: String(codexCredentialFingerprint || "").trim();
|
|
97
98
|
if (profile === "claude" && !credentialFingerprint) return false;
|
|
98
99
|
const cached = loadModelCache({ home, profile, credentialFingerprint });
|
|
99
100
|
if (!cached) return false;
|
|
@@ -133,7 +134,7 @@ export function createProfileModelRuntime({
|
|
|
133
134
|
function persist(models, profile) {
|
|
134
135
|
const credentialFingerprint = profile === "claude"
|
|
135
136
|
? String(claudeCredentialFingerprint || "").trim()
|
|
136
|
-
: "";
|
|
137
|
+
: String(codexCredentialFingerprint || "").trim();
|
|
137
138
|
if (profile === "claude" && !credentialFingerprint) return;
|
|
138
139
|
try {
|
|
139
140
|
saveModelCache(models, { home, profile, credentialFingerprint });
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
function catalogData(catalog) {
|
|
2
|
+
if (Array.isArray(catalog)) return catalog;
|
|
3
|
+
return Array.isArray(catalog?.data) ? catalog.data : [];
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function isClaudeCatalogEntry(model) {
|
|
7
|
+
const id = String(model?.id || "").trim();
|
|
8
|
+
const vendor = String(model?.vendor || "").trim().toLowerCase();
|
|
9
|
+
return Boolean(id) && (vendor === "anthropic" || id.toLowerCase().startsWith("claude-"));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function isAllowedClaudeModel(model) {
|
|
13
|
+
return isClaudeCatalogEntry(model)
|
|
14
|
+
&& String(model?.vendor || "").trim().toLowerCase() === "anthropic"
|
|
15
|
+
&& model?.model_picker_enabled !== false
|
|
16
|
+
&& Array.isArray(model?.supported_endpoints)
|
|
17
|
+
&& model.supported_endpoints.includes("/chat/completions");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function profileRouting({ claudeMode = "inherited", claudeConfigured = false } = {}) {
|
|
21
|
+
const isolated = claudeMode === "isolated" || claudeConfigured === true;
|
|
22
|
+
return Object.freeze({ responses: "codex", messages: isolated ? "claude" : "codex" });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function createPmStudioModelRouter({ getCatalog, isClaudeEnabled } = {}) {
|
|
26
|
+
if (typeof getCatalog !== "function") throw new Error("getCatalog is required");
|
|
27
|
+
if (typeof isClaudeEnabled !== "function") throw new Error("isClaudeEnabled is required");
|
|
28
|
+
|
|
29
|
+
const unset = Symbol("unset");
|
|
30
|
+
let cachedCatalog = unset;
|
|
31
|
+
let cachedEnabled = false;
|
|
32
|
+
let cachedAllowed = new Map();
|
|
33
|
+
let cachedKnown = new Set();
|
|
34
|
+
let rebuilds = 0;
|
|
35
|
+
|
|
36
|
+
function availability() {
|
|
37
|
+
const catalog = getCatalog();
|
|
38
|
+
const enabled = isClaudeEnabled() === true;
|
|
39
|
+
if (catalog === cachedCatalog && enabled === cachedEnabled) {
|
|
40
|
+
return { allowed: cachedAllowed, known: cachedKnown };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const allowed = new Map();
|
|
44
|
+
const known = new Set();
|
|
45
|
+
for (const model of catalogData(catalog)) {
|
|
46
|
+
const id = String(model?.id || "").trim();
|
|
47
|
+
if (!id || !isClaudeCatalogEntry(model)) continue;
|
|
48
|
+
known.add(id);
|
|
49
|
+
if (enabled && isAllowedClaudeModel(model) && !allowed.has(id)) allowed.set(id, model);
|
|
50
|
+
}
|
|
51
|
+
cachedCatalog = catalog;
|
|
52
|
+
cachedEnabled = enabled;
|
|
53
|
+
cachedAllowed = allowed;
|
|
54
|
+
cachedKnown = known;
|
|
55
|
+
rebuilds += 1;
|
|
56
|
+
return { allowed, known };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return Object.freeze({
|
|
60
|
+
classify(modelId) {
|
|
61
|
+
const id = String(modelId || "").trim();
|
|
62
|
+
const { allowed, known } = availability();
|
|
63
|
+
if (allowed.has(id)) return "claude";
|
|
64
|
+
if (known.has(id) || id.toLowerCase().startsWith("claude-")) return "unsupported_claude";
|
|
65
|
+
return "enterprise";
|
|
66
|
+
},
|
|
67
|
+
allowedModels() {
|
|
68
|
+
return [...availability().allowed.values()];
|
|
69
|
+
},
|
|
70
|
+
diagnostics() {
|
|
71
|
+
return { rebuilds, allowed: cachedAllowed.size, known: cachedKnown.size };
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
}
|
package/src/profile-runtime.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
AUTH_PROFILE_CLAUDE,
|
|
3
|
+
AUTH_PROFILE_CODEX,
|
|
3
4
|
authProfilePaths,
|
|
4
5
|
profileReauthMessage,
|
|
5
6
|
readAuthProfileCredentials,
|
|
@@ -45,6 +46,16 @@ export function createProfileRuntime({
|
|
|
45
46
|
};
|
|
46
47
|
const credentials = readCredentials();
|
|
47
48
|
const claudeProfile = safeProfileStatus(credentials);
|
|
49
|
+
let codexCredentialFingerprint = "";
|
|
50
|
+
try {
|
|
51
|
+
const codexCredentials = readAuthProfileCredentials(AUTH_PROFILE_CODEX, { home });
|
|
52
|
+
if (codexCredentials.valid) {
|
|
53
|
+
codexCredentialFingerprint = githubTokenFingerprint(codexCredentials.token);
|
|
54
|
+
}
|
|
55
|
+
} catch {
|
|
56
|
+
// The Codex client remains authoritative if its optional metadata cannot
|
|
57
|
+
// be inspected; only credential-bound cache reuse is disabled.
|
|
58
|
+
}
|
|
48
59
|
|
|
49
60
|
if (!credentials.configured) {
|
|
50
61
|
return Object.freeze({
|
|
@@ -52,6 +63,7 @@ export function createProfileRuntime({
|
|
|
52
63
|
claudeClient: codexClient,
|
|
53
64
|
claudeMode: "inherited",
|
|
54
65
|
claudeProfile,
|
|
66
|
+
codexCredentialFingerprint,
|
|
55
67
|
claudeCredentialFingerprint: "",
|
|
56
68
|
});
|
|
57
69
|
}
|
|
@@ -73,6 +85,7 @@ export function createProfileRuntime({
|
|
|
73
85
|
claudeClient,
|
|
74
86
|
claudeMode: "isolated",
|
|
75
87
|
claudeProfile,
|
|
88
|
+
codexCredentialFingerprint,
|
|
76
89
|
claudeCredentialFingerprint: credentials.valid
|
|
77
90
|
? githubTokenFingerprint(credentials.token)
|
|
78
91
|
: "",
|