codex-copilot-dx 0.4.6 → 0.4.7

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) **and** [Claude Code](https://claude.com/claude-code) with your **GitHub Copilot** subscription.
3
+ Use [Codex Desktop](https://openai.com/codex), [Claude Code](https://claude.com/claude-code), and optionally Claude Desktop with your **GitHub Copilot** subscription.
4
4
 
5
5
  ## How it works
6
6
 
@@ -8,6 +8,7 @@ A single in-process adapter (port `2026`) exposes both APIs over your Copilot su
8
8
 
9
9
  - **Codex** -> OpenAI Responses API (`/v1/responses`, `/v1/responses/compact`); Responses-only models and compaction proxy directly, chat models convert to Chat Completions.
10
10
  - **Claude Code** -> Anthropic Messages API (`/v1/messages`, `/v1/messages/count_tokens`), translated to/from Chat Completions.
11
+ - **Claude Desktop** -> optional Anthropic gateway profile using the same local Messages API plus local model discovery for the configured gateway key.
11
12
 
12
13
  Supports both HTTP SSE streaming and non-streaming.
13
14
 
@@ -15,7 +16,7 @@ Supports both HTTP SSE streaming and non-streaming.
15
16
 
16
17
  - GitHub Copilot subscription (Individual, Business, or Enterprise)
17
18
  - Node.js 18+
18
- - [Codex Desktop](https://openai.com/codex) and/or [Claude Code](https://claude.com/claude-code) installed
19
+ - [Codex Desktop](https://openai.com/codex), [Claude Code](https://claude.com/claude-code), and/or Claude Desktop installed
19
20
 
20
21
  ## Usage
21
22
 
@@ -37,6 +38,22 @@ Claude Code picks up the new `ANTHROPIC_BASE_URL` on its next launch.
37
38
 
38
39
  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.
39
40
 
41
+ ### Claude Desktop opt-in
42
+
43
+ Claude Desktop support is opt-in so the default Codex Desktop and Claude Code setup stays unchanged:
44
+
45
+ ```bash
46
+ npx codex-copilot-dx@latest --configure-claude-desktop
47
+ ```
48
+
49
+ Or set:
50
+
51
+ ```bash
52
+ CCDX_CONFIGURE_CLAUDE_DESKTOP=1 npx codex-copilot-dx@latest
53
+ ```
54
+
55
+ This writes a local Claude Desktop 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 Desktop after running the command.
56
+
40
57
  ## Configuration
41
58
 
42
59
  Environment variables:
@@ -53,6 +70,9 @@ Environment variables:
53
70
  | `CCDX_IMG_MIN_BYTES` | `100000` | Images smaller than this are left untouched |
54
71
  | `CCDX_IMG_CONCURRENCY` | `4` | Concurrent image optimization tasks; values above `12` are capped at `12` |
55
72
  | `CCDX_DISABLE_IMG_OPT` | unset | Set to `1` to disable image optimization |
73
+ | `CCDX_CONFIGURE_CLAUDE_DESKTOP` | unset | Set to `1` to write the Claude Desktop 3P gateway profile during startup |
74
+ | `CCDX_CLAUDE_DESKTOP_API_KEY` | generated for opt-in setup | Bearer key written into the Claude Desktop profile and recognized by the adapter for model discovery |
75
+ | `CCDX_CLAUDE_MODEL_ALIASES` | built-in Claude aliases | Comma-separated Desktop-to-upstream aliases, for example `claude-sonnet-4-6=claude-sonnet-4.6` |
56
76
  | `CCDX_USAGE_PATH` | `~/.local/share/codex-copilot-dx/usage.jsonl` | Local JSONL token usage log |
57
77
  | `CCDX_DISABLE_USAGE` | unset | Set to `1` to disable usage logging |
58
78
 
package/bin/cli.mjs CHANGED
@@ -3,6 +3,7 @@
3
3
  import { ensureAuth, openCodex } from "../src/launcher.mjs";
4
4
  import { ensureCodexConfig } from "../src/config.mjs";
5
5
  import { ensureClaudeConfig } from "../src/claude-config.mjs";
6
+ import { applyClaudeDesktopConfig, formatClaudeDesktopApplyResult, generatedClaudeDesktopApiKey } from "../src/claude-desktop-config.mjs";
6
7
  import { startAdapter } from "../src/adapter.mjs";
7
8
  import { refreshVSCodeVersion } from "../src/copilot.mjs";
8
9
  import { status } from "../src/status.mjs";
@@ -13,6 +14,7 @@ const ADAPTER_PORT = parseInt(process.env.ADAPTER_PORT || "2026");
13
14
  const ADAPTER_HOST = process.env.ADAPTER_HOST || "127.0.0.1";
14
15
  const LOCAL_VERSION = localPackageVersion();
15
16
  const command = process.argv[2];
17
+ const CONFIGURE_CLAUDE_DESKTOP = command === "--configure-claude-desktop" || process.env.CCDX_CONFIGURE_CLAUDE_DESKTOP === "1";
16
18
 
17
19
  function isLoopbackHost(host) {
18
20
  return ["127.0.0.1", "localhost", "::1"].includes(String(host).toLowerCase());
@@ -50,18 +52,33 @@ try {
50
52
  console.log(status("warn", `ADAPTER_HOST=${ADAPTER_HOST} exposes the adapter beyond loopback. Use only on trusted networks.`));
51
53
  }
52
54
 
55
+ const claudeDesktopApiKey = CONFIGURE_CLAUDE_DESKTOP
56
+ ? (process.env.CCDX_CLAUDE_DESKTOP_API_KEY || process.env.CCDX_PROXY_API_KEY || generatedClaudeDesktopApiKey())
57
+ : (process.env.CCDX_CLAUDE_DESKTOP_API_KEY || process.env.CCDX_PROXY_API_KEY || "");
58
+
53
59
  // Start the in-process adapter.
54
- await startAdapter(ADAPTER_PORT, ADAPTER_HOST);
60
+ await startAdapter(ADAPTER_PORT, ADAPTER_HOST, { claudeDesktopApiKey });
55
61
 
56
62
  // Point Codex and Claude Code at the adapter.
57
63
  ensureCodexConfig(ADAPTER_PORT);
58
64
  ensureClaudeConfig(ADAPTER_PORT);
65
+ if (CONFIGURE_CLAUDE_DESKTOP) {
66
+ const result = applyClaudeDesktopConfig({
67
+ port: ADAPTER_PORT,
68
+ host: ADAPTER_HOST,
69
+ gatewayApiKey: claudeDesktopApiKey,
70
+ });
71
+ console.log(status("ok", `Configured Claude Desktop gateway profile at ${result.baseUrl}`));
72
+ console.log(formatClaudeDesktopApplyResult(result));
73
+ }
59
74
 
60
75
  // Launch Codex when available.
61
76
  openCodex();
62
77
 
63
78
  console.log(`
64
- ${status("ok", "Ready. Codex and Claude Code are using your GitHub Copilot subscription.")}
79
+ ${status("ok", CONFIGURE_CLAUDE_DESKTOP
80
+ ? "Ready. Codex, Claude Code, and Claude Desktop are using your GitHub Copilot subscription."
81
+ : "Ready. Codex and Claude Code are using your GitHub Copilot subscription.")}
65
82
 
66
83
  Adapter: http://${ADAPTER_HOST}:${ADAPTER_PORT}
67
84
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-copilot-dx",
3
- "version": "0.4.6",
3
+ "version": "0.4.7",
4
4
  "description": "Use Codex Desktop app with your GitHub Copilot subscription",
5
5
  "type": "module",
6
6
  "bin": {
package/src/adapter.mjs CHANGED
@@ -5,6 +5,7 @@ import * as zlib from "node:zlib";
5
5
  import { chatCompletions, listModels, responses as copilotResponses, responsesCompact as copilotResponsesCompact } from "./copilot.mjs";
6
6
  import { webStreamLines } from "./stream.mjs";
7
7
  import { anthropicToChat, chatToAnthropic, streamAnthropicFromLines, countTokens } from "./anthropic.mjs";
8
+ import { claudeDesktopModelsResponse, resolveAnthropicModel } from "./models.mjs";
8
9
  import { status } from "./status.mjs";
9
10
  import { recordAnthropicUsage, recordResponsesUsage } from "./usage.mjs";
10
11
 
@@ -132,6 +133,19 @@ function sendUpstreamError(res, resp, text) {
132
133
  res.end(text || JSON.stringify({ error: "Upstream request failed" }));
133
134
  }
134
135
 
136
+ function bearerToken(headers = {}) {
137
+ const value = headers.authorization || headers.Authorization || "";
138
+ const match = /^Bearer\s+(.+)$/i.exec(String(value));
139
+ return match?.[1]?.trim() || "";
140
+ }
141
+
142
+ export function shouldServeClaudeDesktopModels(req, claudeDesktopApiKey) {
143
+ if (!claudeDesktopApiKey || claudeDesktopApiKey === "dummy") return false;
144
+ const token = bearerToken(req.headers);
145
+ const xApiKey = req.headers?.["x-api-key"] || req.headers?.["X-Api-Key"] || req.headers?.["X-API-Key"] || "";
146
+ return token === claudeDesktopApiKey || xApiKey === claudeDesktopApiKey;
147
+ }
148
+
135
149
  export function writeOrDrain(res, chunk) {
136
150
  if (res.destroyed || res.writableEnded) return Promise.resolve(false);
137
151
  if (res.write(chunk)) return Promise.resolve(true);
@@ -504,7 +518,12 @@ async function forwardToChat(chatReq, emitEvent, onDone, onError, options = {})
504
518
 
505
519
  // Public server entry point.
506
520
 
507
- export function startAdapter(port = 2026, host = "127.0.0.1") {
521
+ export function startAdapter(port = 2026, host = "127.0.0.1", options = {}) {
522
+ const claudeDesktopApiKey = options.claudeDesktopApiKey
523
+ || process.env.CCDX_CLAUDE_DESKTOP_API_KEY
524
+ || process.env.CCDX_PROXY_API_KEY
525
+ || "";
526
+
508
527
  const server = http.createServer((req, res) => {
509
528
  const pathname = requestPath(req.url);
510
529
 
@@ -580,6 +599,11 @@ export function startAdapter(port = 2026, host = "127.0.0.1") {
580
599
  }
581
600
 
582
601
  if (req.method === "GET" && pathname === "/v1/models") {
602
+ if (shouldServeClaudeDesktopModels(req, claudeDesktopApiKey)) {
603
+ res.writeHead(200, { "Content-Type": "application/json" });
604
+ res.end(JSON.stringify(claudeDesktopModelsResponse()));
605
+ return;
606
+ }
583
607
  const abort = createRequestAbort(req, res);
584
608
  abort.setTimeout(UPSTREAM_TIMEOUT_MS);
585
609
  listModels({ signal: abort.signal })
@@ -611,9 +635,11 @@ export function startAdapter(port = 2026, host = "127.0.0.1") {
611
635
  try {
612
636
  const parsed = await readJsonBody(req);
613
637
  if (!parsed.stream) abort.setTimeout(UPSTREAM_TIMEOUT_MS);
614
- const model = parsed.model || "unknown";
615
- console.log(status("info", `messages model=${model} stream=${!!parsed.stream}`));
616
- const chatReq = anthropicToChat(parsed);
638
+ const { requestedModel, upstreamModel } = resolveAnthropicModel(parsed.model || "unknown");
639
+ const modelNote = upstreamModel === requestedModel ? requestedModel : `${requestedModel} -> ${upstreamModel}`;
640
+ console.log(status("info", `messages model=${modelNote} stream=${!!parsed.stream}`));
641
+ const chatReq = anthropicToChat(parsed, { upstreamModel });
642
+ const forceRequestedModel = upstreamModel !== requestedModel;
617
643
  if (parsed.stream) {
618
644
  const upstream = await chatCompletions({ ...chatReq, stream: true }, { signal: abort.signal });
619
645
  if (!upstream.ok) {
@@ -631,14 +657,15 @@ export function startAdapter(port = 2026, host = "127.0.0.1") {
631
657
  if (event === "message_delta") usage = data.usage;
632
658
  await writeOrDrain(res, `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
633
659
  },
634
- model,
660
+ requestedModel,
661
+ { forceModel: forceRequestedModel },
635
662
  );
636
- recordAnthropicUsage({ surface: "messages", mode: "stream", model, responseId: messageId, usage });
663
+ recordAnthropicUsage({ surface: "messages", mode: "stream", model: requestedModel, responseId: messageId, usage });
637
664
  if (!res.writableEnded) res.end();
638
665
  } else {
639
666
  const upstream = await chatCompletions({ ...chatReq, stream: false }, { signal: abort.signal });
640
667
  const data = await upstream.text();
641
- const anthropicMsg = chatToAnthropic(JSON.parse(data), model);
668
+ const anthropicMsg = chatToAnthropic(JSON.parse(data), requestedModel, { forceModel: forceRequestedModel });
642
669
  recordAnthropicUsage({ surface: "messages", mode: "json", model: anthropicMsg.model, responseId: anthropicMsg.id, usage: anthropicMsg.usage });
643
670
  res.writeHead(200, { "Content-Type": "application/json" });
644
671
  res.end(JSON.stringify(anthropicMsg));
package/src/anthropic.mjs CHANGED
@@ -89,7 +89,7 @@ function mapToolChoice(tc) {
89
89
  }
90
90
  }
91
91
 
92
- export function anthropicToChat(body) {
92
+ export function anthropicToChat(body, options = {}) {
93
93
  const messages = [];
94
94
  const sys = systemToText(body.system);
95
95
  if (sys) messages.push({ role: "system", content: sys });
@@ -97,7 +97,7 @@ export function anthropicToChat(body) {
97
97
  for (const converted of convertMessage(m)) messages.push(converted);
98
98
  }
99
99
 
100
- const chatReq = { model: body.model, messages };
100
+ const chatReq = { model: options.upstreamModel || body.model, messages };
101
101
  if (body.max_tokens !== undefined) chatReq.max_tokens = body.max_tokens;
102
102
  if (body.temperature !== undefined) chatReq.temperature = body.temperature;
103
103
  if (body.top_p !== undefined) chatReq.top_p = body.top_p;
@@ -117,7 +117,7 @@ export function anthropicToChat(body) {
117
117
 
118
118
  function uid() { return randomUUID().replace(/-/g, ""); }
119
119
 
120
- export function chatToAnthropic(openaiResp, model) {
120
+ export function chatToAnthropic(openaiResp, model, options = {}) {
121
121
  const choice = openaiResp.choices?.[0];
122
122
  const msg = choice?.message || {};
123
123
  const content = [];
@@ -143,7 +143,7 @@ export function chatToAnthropic(openaiResp, model) {
143
143
  id: `msg_${uid()}`,
144
144
  type: "message",
145
145
  role: "assistant",
146
- model: openaiResp.model || model,
146
+ model: options.forceModel ? model : (openaiResp.model || model),
147
147
  content,
148
148
  stop_reason: mapStopReason(choice?.finish_reason),
149
149
  stop_sequence: null,
@@ -152,7 +152,7 @@ export function chatToAnthropic(openaiResp, model) {
152
152
  }
153
153
 
154
154
  // Consume OpenAI chat SSE lines and emit Anthropic SSE events.
155
- export async function streamAnthropicFromLines(lineIterator, emit, model) {
155
+ export async function streamAnthropicFromLines(lineIterator, emit, model, options = {}) {
156
156
  const msgId = `msg_${uid()}`;
157
157
  let started = false;
158
158
  let blockIndex = -1;
@@ -192,7 +192,7 @@ export async function streamAnthropicFromLines(lineIterator, emit, model) {
192
192
  if (data === "[DONE]") break;
193
193
  let parsed;
194
194
  try { parsed = JSON.parse(data); } catch { continue; }
195
- if (parsed.model) actualModel = parsed.model;
195
+ if (!options.forceModel && parsed.model) actualModel = parsed.model;
196
196
  if (parsed.usage?.completion_tokens != null) outputTokens = parsed.usage.completion_tokens;
197
197
  const choice = parsed.choices?.[0];
198
198
  if (!choice) continue;
@@ -0,0 +1,232 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import os from "node:os";
4
+ import { randomUUID } from "node:crypto";
5
+ import { claudeDesktopModelIds } from "./models.mjs";
6
+ import { status } from "./status.mjs";
7
+
8
+ const CONFIG_FILE = "claude_desktop_config.json";
9
+ const CONFIG_LIBRARY_DIR = "configLibrary";
10
+ export const DEFAULT_CLAUDE_DESKTOP_PROFILE_ID = "00000000-0000-4000-8000-000000202600";
11
+ export const DEFAULT_CLAUDE_DESKTOP_PROFILE_NAME = "Codex Copilot DX";
12
+
13
+ function jsonClone(value) {
14
+ return value === undefined ? undefined : JSON.parse(JSON.stringify(value));
15
+ }
16
+
17
+ function valueOrDefault(value, fallback) {
18
+ const trimmed = String(value || "").trim();
19
+ return trimmed || fallback;
20
+ }
21
+
22
+ function isLoopbackHost(host) {
23
+ const normalized = String(host || "").toLowerCase().replace(/^\[(.*)\]$/, "$1");
24
+ return ["127.0.0.1", "localhost", "::1"].includes(normalized);
25
+ }
26
+
27
+ function localGatewayBaseUrl(host, port) {
28
+ const safeHost = String(host || "127.0.0.1");
29
+ const urlHost = safeHost.includes(":") && !safeHost.startsWith("[") ? `[${safeHost}]` : safeHost;
30
+ return `http://${urlHost}:${port}`;
31
+ }
32
+
33
+ export function claudeDesktopPaths(home = os.homedir(), platform = process.platform, env = process.env) {
34
+ if (platform === "win32") {
35
+ const localAppData = valueOrDefault(env.LOCALAPPDATA, path.join(home, "AppData", "Local"));
36
+ return pathsFromDirs(path.join(localAppData, "Claude"), path.join(localAppData, "Claude-3p"));
37
+ }
38
+ const appSupport = path.join(home, "Library", "Application Support");
39
+ return pathsFromDirs(path.join(appSupport, "Claude"), path.join(appSupport, "Claude-3p"));
40
+ }
41
+
42
+ function pathsFromDirs(normalDir, threepDir) {
43
+ const configLibraryPath = path.join(threepDir, CONFIG_LIBRARY_DIR);
44
+ return {
45
+ normalConfigPath: path.join(normalDir, CONFIG_FILE),
46
+ threepConfigPath: path.join(threepDir, CONFIG_FILE),
47
+ configLibraryPath,
48
+ metaPath: path.join(configLibraryPath, "_meta.json"),
49
+ };
50
+ }
51
+
52
+ export function validateClaudeDesktopBaseUrl(raw) {
53
+ let parsed;
54
+ try {
55
+ parsed = new URL(raw);
56
+ } catch {
57
+ throw new Error(`Claude Desktop gateway base URL is not valid: ${raw}`);
58
+ }
59
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
60
+ throw new Error("Claude Desktop gateway base URL must use http or https");
61
+ }
62
+ if (parsed.protocol === "http:" && !isLoopbackHost(parsed.hostname)) {
63
+ throw new Error("Claude Desktop allows http only for loopback; use https for LAN or remote gateways");
64
+ }
65
+ if (parsed.pathname !== "/" || parsed.search || parsed.hash) {
66
+ throw new Error("Claude Desktop gateway base URL must be the root URL without /v1, query, or hash");
67
+ }
68
+ }
69
+
70
+ function isGatewayProfileField(key) {
71
+ return [
72
+ "inferenceProvider",
73
+ "inferenceGatewayBaseUrl",
74
+ "inferenceGatewayApiKey",
75
+ "inferenceGatewayAuthScheme",
76
+ "inferenceModels",
77
+ ].includes(key);
78
+ }
79
+
80
+ export function computeClaudeDesktopProfile(sourceProfile = {}, {
81
+ baseUrl,
82
+ gatewayApiKey,
83
+ modelIds = claudeDesktopModelIds(),
84
+ } = {}) {
85
+ validateClaudeDesktopBaseUrl(baseUrl);
86
+ if (!String(gatewayApiKey || "").trim()) {
87
+ throw new Error("Claude Desktop gateway API key is required");
88
+ }
89
+
90
+ const profile = {};
91
+ for (const [key, value] of Object.entries(sourceProfile || {})) {
92
+ if (key === "enterpriseConfig" || isGatewayProfileField(key)) continue;
93
+ profile[key] = jsonClone(value);
94
+ }
95
+
96
+ profile.inferenceProvider = "gateway";
97
+ profile.inferenceGatewayBaseUrl = baseUrl;
98
+ profile.inferenceGatewayApiKey = gatewayApiKey;
99
+ profile.inferenceGatewayAuthScheme = "bearer";
100
+ profile.inferenceModels = JSON.stringify(modelIds);
101
+ if (!profile.disableDeploymentModeChooser) profile.disableDeploymentModeChooser = "true";
102
+ return profile;
103
+ }
104
+
105
+ export function computeClaudeDesktopMeta(meta = {}, profileId = DEFAULT_CLAUDE_DESKTOP_PROFILE_ID, profileName = DEFAULT_CLAUDE_DESKTOP_PROFILE_NAME) {
106
+ const out = { ...(meta || {}) };
107
+ const entries = Array.isArray(out.entries) ? out.entries : [];
108
+ out.entries = [
109
+ ...entries.filter((entry) => entry?.id !== profileId),
110
+ { id: profileId, name: profileName },
111
+ ];
112
+ out.appliedId = profileId;
113
+ return out;
114
+ }
115
+
116
+ export function computeDeploymentConfig(config = {}) {
117
+ return { ...(config || {}), deploymentMode: "3p" };
118
+ }
119
+
120
+ function readJsonFile(filePath, fallback = {}) {
121
+ try {
122
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
123
+ } catch (e) {
124
+ if (e?.code === "ENOENT") return fallback;
125
+ throw e;
126
+ }
127
+ }
128
+
129
+ function writeJsonFile(filePath, data) {
130
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
131
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", { mode: 0o600 });
132
+ }
133
+
134
+ function snapshotFiles(files) {
135
+ return files.map((filePath) => {
136
+ try {
137
+ return { filePath, exists: true, data: fs.readFileSync(filePath) };
138
+ } catch (e) {
139
+ if (e?.code === "ENOENT") return { filePath, exists: false };
140
+ throw e;
141
+ }
142
+ });
143
+ }
144
+
145
+ function restoreSnapshots(snapshots) {
146
+ for (const snapshot of snapshots) {
147
+ if (!snapshot.exists) {
148
+ fs.rmSync(snapshot.filePath, { force: true });
149
+ continue;
150
+ }
151
+ fs.mkdirSync(path.dirname(snapshot.filePath), { recursive: true });
152
+ fs.writeFileSync(snapshot.filePath, snapshot.data, { mode: 0o600 });
153
+ }
154
+ }
155
+
156
+ function readActiveProfile(paths) {
157
+ const meta = readJsonFile(paths.metaPath, {});
158
+ const appliedId = String(meta.appliedId || "").trim();
159
+ if (!appliedId) return {};
160
+ return readJsonFile(path.join(paths.configLibraryPath, `${appliedId}.json`), {});
161
+ }
162
+
163
+ export function applyClaudeDesktopConfig({
164
+ port = 2026,
165
+ host = "127.0.0.1",
166
+ home = os.homedir(),
167
+ platform = process.platform,
168
+ env = process.env,
169
+ baseUrl = localGatewayBaseUrl(host, port),
170
+ gatewayApiKey = env.CCDX_CLAUDE_DESKTOP_API_KEY || env.CCDX_PROXY_API_KEY || "dummy",
171
+ modelIds = claudeDesktopModelIds(env),
172
+ profileId = DEFAULT_CLAUDE_DESKTOP_PROFILE_ID,
173
+ profileName = DEFAULT_CLAUDE_DESKTOP_PROFILE_NAME,
174
+ } = {}) {
175
+ const paths = claudeDesktopPaths(home, platform, env);
176
+ const profilePath = path.join(paths.configLibraryPath, `${profileId}.json`);
177
+ const snapshots = snapshotFiles([
178
+ paths.normalConfigPath,
179
+ paths.threepConfigPath,
180
+ paths.metaPath,
181
+ profilePath,
182
+ ]);
183
+
184
+ try {
185
+ const sourceProfile = readActiveProfile(paths);
186
+ const normalConfig = readJsonFile(paths.normalConfigPath, {});
187
+ const threepConfig = readJsonFile(paths.threepConfigPath, {});
188
+ const meta = readJsonFile(paths.metaPath, {});
189
+
190
+ writeJsonFile(paths.normalConfigPath, computeDeploymentConfig(normalConfig));
191
+ writeJsonFile(paths.threepConfigPath, computeDeploymentConfig(threepConfig));
192
+ writeJsonFile(profilePath, computeClaudeDesktopProfile(sourceProfile, { baseUrl, gatewayApiKey, modelIds }));
193
+ writeJsonFile(paths.metaPath, computeClaudeDesktopMeta(meta, profileId, profileName));
194
+ } catch (e) {
195
+ restoreSnapshots(snapshots);
196
+ throw e;
197
+ }
198
+
199
+ return {
200
+ paths,
201
+ profileId,
202
+ profilePath,
203
+ baseUrl,
204
+ modelIds,
205
+ };
206
+ }
207
+
208
+ export function formatClaudeDesktopApplyResult(result) {
209
+ return [
210
+ "Claude Desktop local gateway profile",
211
+ `profile id: ${result.profileId}`,
212
+ `profile path: ${result.profilePath}`,
213
+ `base url: ${result.baseUrl}`,
214
+ `models: ${result.modelIds.join(", ")}`,
215
+ "gateway api key: <redacted>",
216
+ ].join("\n");
217
+ }
218
+
219
+ export function ensureClaudeDesktopConfig(port = 2026, host = "127.0.0.1") {
220
+ const result = applyClaudeDesktopConfig({
221
+ port,
222
+ host,
223
+ gatewayApiKey: process.env.CCDX_CLAUDE_DESKTOP_API_KEY || process.env.CCDX_PROXY_API_KEY || generatedClaudeDesktopApiKey(),
224
+ });
225
+ console.log(status("ok", `Configured Claude Desktop gateway profile at ${result.baseUrl}`));
226
+ console.log(formatClaudeDesktopApplyResult(result));
227
+ return result;
228
+ }
229
+
230
+ export function generatedClaudeDesktopApiKey() {
231
+ return `ccdx_${randomUUID().replaceAll("-", "")}`;
232
+ }
package/src/models.mjs ADDED
@@ -0,0 +1,110 @@
1
+ const DEFAULT_CLAUDE_DESKTOP_MODEL_DEFS = [
2
+ {
3
+ id: "claude-sonnet-4.6",
4
+ upstream: "claude-sonnet-4.6",
5
+ displayName: "Claude Sonnet 4.6",
6
+ createdAt: "2026-02-17T00:00:00Z",
7
+ maxInputTokens: 200000,
8
+ maxOutputTokens: 8192,
9
+ },
10
+ {
11
+ id: "claude-opus-4.8",
12
+ upstream: "claude-opus-4.8",
13
+ displayName: "Claude Opus 4.8",
14
+ createdAt: "2026-05-28T00:00:00Z",
15
+ maxInputTokens: 200000,
16
+ maxOutputTokens: 8192,
17
+ },
18
+ {
19
+ id: "claude-haiku-4.5",
20
+ upstream: "claude-haiku-4.5",
21
+ displayName: "Claude Haiku 4.5",
22
+ createdAt: "2025-10-01T00:00:00Z",
23
+ maxInputTokens: 200000,
24
+ maxOutputTokens: 8192,
25
+ },
26
+ {
27
+ id: "claude-sonnet-4-6",
28
+ upstream: "claude-sonnet-4.6",
29
+ displayName: "Claude Sonnet 4.6",
30
+ createdAt: "2026-02-17T00:00:00Z",
31
+ maxInputTokens: 200000,
32
+ maxOutputTokens: 8192,
33
+ },
34
+ {
35
+ id: "claude-opus-4-8",
36
+ upstream: "claude-opus-4.8",
37
+ displayName: "Claude Opus 4.8",
38
+ createdAt: "2026-05-28T00:00:00Z",
39
+ maxInputTokens: 200000,
40
+ maxOutputTokens: 8192,
41
+ },
42
+ {
43
+ id: "claude-haiku-4-5",
44
+ upstream: "claude-haiku-4.5",
45
+ displayName: "Claude Haiku 4.5",
46
+ createdAt: "2025-10-01T00:00:00Z",
47
+ maxInputTokens: 200000,
48
+ maxOutputTokens: 8192,
49
+ },
50
+ ];
51
+
52
+ function cleanList(value) {
53
+ return String(value || "")
54
+ .split(",")
55
+ .map((item) => item.trim())
56
+ .filter(Boolean);
57
+ }
58
+
59
+ export function parseModelAliasEnv(value) {
60
+ const aliases = [];
61
+ for (const entry of cleanList(value)) {
62
+ const [id, upstream] = entry.split("=", 2).map((part) => part?.trim());
63
+ if (!id || !upstream) continue;
64
+ aliases.push({
65
+ id,
66
+ upstream,
67
+ displayName: id,
68
+ createdAt: "2025-01-01T00:00:00Z",
69
+ maxInputTokens: 200000,
70
+ maxOutputTokens: 8192,
71
+ });
72
+ }
73
+ return aliases;
74
+ }
75
+
76
+ export function claudeDesktopModelDefs(env = process.env) {
77
+ const custom = parseModelAliasEnv(env.CCDX_CLAUDE_MODEL_ALIASES);
78
+ return custom.length ? custom : DEFAULT_CLAUDE_DESKTOP_MODEL_DEFS.map((model) => ({ ...model }));
79
+ }
80
+
81
+ export function claudeDesktopModelIds(env = process.env) {
82
+ return claudeDesktopModelDefs(env).map((model) => model.id);
83
+ }
84
+
85
+ export function resolveAnthropicModel(model, env = process.env) {
86
+ const requestedModel = String(model || "");
87
+ const match = claudeDesktopModelDefs(env).find((entry) => entry.id === requestedModel);
88
+ return {
89
+ requestedModel,
90
+ upstreamModel: match?.upstream || requestedModel,
91
+ };
92
+ }
93
+
94
+ export function anthropicModelInfo(model) {
95
+ return {
96
+ id: model.id,
97
+ type: "model",
98
+ display_name: model.displayName || model.id,
99
+ created_at: model.createdAt || "2025-01-01T00:00:00Z",
100
+ max_input_tokens: model.maxInputTokens || 200000,
101
+ max_tokens: model.maxOutputTokens || 8192,
102
+ };
103
+ }
104
+
105
+ export function claudeDesktopModelsResponse(env = process.env) {
106
+ return {
107
+ object: "list",
108
+ data: claudeDesktopModelDefs(env).map(anthropicModelInfo),
109
+ };
110
+ }