codex-copilot-dx 0.6.20 → 0.7.0
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 +123 -326
- package/bin/cli.mjs +13 -129
- package/package.json +2 -8
- package/scripts/benchmark.mjs +0 -114
- package/src/adapter.mjs +19 -106
- package/src/auth-profile.mjs +18 -130
- package/src/cli-auth.mjs +44 -398
- package/src/cli-models.mjs +21 -49
- package/src/cli-options.mjs +45 -75
- package/src/cli-status.mjs +6 -23
- package/src/config.mjs +2 -8
- package/src/copilot.mjs +2 -2
- package/src/doctor.mjs +44 -291
- package/src/model-cache.mjs +4 -11
- package/src/models.mjs +0 -184
- package/src/observability.mjs +1 -38
- package/src/profile-model-runtime.mjs +61 -189
- package/src/profile-routing.mjs +2 -102
- package/src/profile-runtime.mjs +15 -68
- package/src/responses-handler.mjs +3 -1
- package/src/responses-proxy.mjs +1 -1
- package/src/running-adapter.mjs +2 -4
- package/src/stream-errors.mjs +3 -6
- package/src/stream-performance.mjs +1 -1
- package/src/usage.mjs +0 -31
- package/src/anthropic-handler.mjs +0 -195
- package/src/anthropic.mjs +0 -387
- package/src/claude-config.mjs +0 -57
- package/src/claude-desktop-config.mjs +0 -317
- package/src/cli-error.mjs +0 -50
- package/src/pm-studio-asar.mjs +0 -1244
- package/src/pm-studio-bundle-content.mjs +0 -165
- package/src/pm-studio-relay.mjs +0 -563
- package/src/pm-studio-setup-lock.mjs +0 -336
- package/src/pm-studio-setup.mjs +0 -1581
- package/src/pm-studio-status.mjs +0 -369
- package/src/token-counter-service.mjs +0 -211
- package/src/token-counter-worker.mjs +0 -16
package/src/anthropic.mjs
DELETED
|
@@ -1,387 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
-
import {
|
|
3
|
-
incompleteUpstreamStream,
|
|
4
|
-
invalidUpstreamStream,
|
|
5
|
-
ToolArgumentDeltaGuard,
|
|
6
|
-
upstreamChatStreamError,
|
|
7
|
-
} from "./stream-contract.mjs";
|
|
8
|
-
import { isChatOutputDelta, markFirstOutput, markOutputTokens } from "./stream-performance.mjs";
|
|
9
|
-
|
|
10
|
-
let tokenizerModulePromise = null;
|
|
11
|
-
|
|
12
|
-
async function tokenCounter() {
|
|
13
|
-
tokenizerModulePromise ||= import("gpt-tokenizer");
|
|
14
|
-
return (await tokenizerModulePromise).countTokens;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
// Anthropic Messages API to OpenAI chat/completions translation.
|
|
18
|
-
// Pure functions only; network calls stay in the protocol handler and copilot.mjs.
|
|
19
|
-
|
|
20
|
-
export function mapStopReason(finishReason) {
|
|
21
|
-
switch (finishReason) {
|
|
22
|
-
case "stop": return "end_turn";
|
|
23
|
-
case "tool_calls": return "tool_use";
|
|
24
|
-
case "length": return "max_tokens";
|
|
25
|
-
default: return "end_turn";
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// Convert a string or text-block system field into one system message.
|
|
30
|
-
function systemToText(system) {
|
|
31
|
-
if (!system) return null;
|
|
32
|
-
if (typeof system === "string") return system;
|
|
33
|
-
if (Array.isArray(system)) return system.map((b) => (typeof b === "string" ? b : b.text || "")).join("\n");
|
|
34
|
-
return null;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// Convert tool_result content into the string payload expected by OpenAI tool messages.
|
|
38
|
-
function toolResultContent(content) {
|
|
39
|
-
if (typeof content === "string") return content;
|
|
40
|
-
if (Array.isArray(content)) {
|
|
41
|
-
return content.map((b) => (b.type === "text" ? b.text : typeof b === "string" ? b : JSON.stringify(b))).join("");
|
|
42
|
-
}
|
|
43
|
-
return JSON.stringify(content);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Convert one Anthropic message into zero or more OpenAI messages.
|
|
47
|
-
function convertContentBlocks(role, blocks) {
|
|
48
|
-
const textImageParts = [];
|
|
49
|
-
const toolCalls = [];
|
|
50
|
-
for (const b of blocks) {
|
|
51
|
-
if (b.type === "text") {
|
|
52
|
-
textImageParts.push({ type: "text", text: b.text });
|
|
53
|
-
} else if (b.type === "image") {
|
|
54
|
-
const url = b.source?.type === "url"
|
|
55
|
-
? b.source.url
|
|
56
|
-
: `data:${b.source.media_type};base64,${b.source.data}`;
|
|
57
|
-
textImageParts.push({ type: "image_url", image_url: { url } });
|
|
58
|
-
} else if (b.type === "tool_use") {
|
|
59
|
-
toolCalls.push({ id: b.id, type: "function", function: { name: b.name, arguments: JSON.stringify(b.input ?? {}) } });
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (toolCalls.length > 0) {
|
|
64
|
-
const m = { role, content: null, tool_calls: toolCalls };
|
|
65
|
-
if (textImageParts.length === 1 && textImageParts[0].type === "text") m.content = textImageParts[0].text;
|
|
66
|
-
else if (textImageParts.length > 0) m.content = textImageParts;
|
|
67
|
-
return m;
|
|
68
|
-
} else if (textImageParts.length > 0) {
|
|
69
|
-
if (textImageParts.length === 1 && textImageParts[0].type === "text") {
|
|
70
|
-
return { role, content: textImageParts[0].text };
|
|
71
|
-
} else {
|
|
72
|
-
return { role, content: textImageParts };
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
return null;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function convertMessage(msg) {
|
|
79
|
-
const out = [];
|
|
80
|
-
const role = msg.role; // "user" | "assistant"
|
|
81
|
-
|
|
82
|
-
if (typeof msg.content === "string") {
|
|
83
|
-
out.push({ role, content: msg.content });
|
|
84
|
-
return out;
|
|
85
|
-
}
|
|
86
|
-
if (!Array.isArray(msg.content)) return out;
|
|
87
|
-
|
|
88
|
-
if (!msg.content.some((block) => block.type === "tool_result")) {
|
|
89
|
-
const converted = convertContentBlocks(role, msg.content);
|
|
90
|
-
if (converted) out.push(converted);
|
|
91
|
-
return out;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
let contentBlocks = [];
|
|
95
|
-
const flushContent = () => {
|
|
96
|
-
const converted = convertContentBlocks(role, contentBlocks);
|
|
97
|
-
if (converted) out.push(converted);
|
|
98
|
-
contentBlocks = [];
|
|
99
|
-
};
|
|
100
|
-
for (const block of msg.content) {
|
|
101
|
-
if (block.type !== "tool_result") {
|
|
102
|
-
contentBlocks.push(block);
|
|
103
|
-
continue;
|
|
104
|
-
}
|
|
105
|
-
flushContent();
|
|
106
|
-
out.push({
|
|
107
|
-
role: "tool",
|
|
108
|
-
tool_call_id: block.tool_use_id,
|
|
109
|
-
content: toolResultContent(block.content),
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
flushContent();
|
|
113
|
-
return out;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function toolInputSchema(inputSchema) {
|
|
117
|
-
if (inputSchema
|
|
118
|
-
&& typeof inputSchema === "object"
|
|
119
|
-
&& !Array.isArray(inputSchema)
|
|
120
|
-
&& inputSchema.type === "object"
|
|
121
|
-
&& Object.keys(inputSchema).length === 1) {
|
|
122
|
-
return { type: "object", properties: {} };
|
|
123
|
-
}
|
|
124
|
-
return inputSchema;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function mapToolChoice(tc) {
|
|
128
|
-
if (!tc) return undefined;
|
|
129
|
-
switch (tc.type) {
|
|
130
|
-
case "auto": return "auto";
|
|
131
|
-
case "none": return "none";
|
|
132
|
-
case "any": return "required";
|
|
133
|
-
case "tool": return { type: "function", function: { name: tc.name } };
|
|
134
|
-
default: return undefined;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
export function anthropicToChat(body, options = {}) {
|
|
139
|
-
const messages = [];
|
|
140
|
-
const sys = systemToText(body.system);
|
|
141
|
-
if (sys) messages.push({ role: "system", content: sys });
|
|
142
|
-
for (const m of body.messages || []) {
|
|
143
|
-
for (const converted of convertMessage(m)) messages.push(converted);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const chatReq = { model: options.upstreamModel || body.model, messages };
|
|
147
|
-
if (body.max_tokens !== undefined) chatReq.max_tokens = body.max_tokens;
|
|
148
|
-
if (body.temperature !== undefined) chatReq.temperature = body.temperature;
|
|
149
|
-
if (body.top_p !== undefined) chatReq.top_p = body.top_p;
|
|
150
|
-
if (Array.isArray(body.stop_sequences) && body.stop_sequences.length) chatReq.stop = body.stop_sequences;
|
|
151
|
-
|
|
152
|
-
if (Array.isArray(body.tools) && body.tools.length) {
|
|
153
|
-
chatReq.tools = body.tools.map((t) => ({
|
|
154
|
-
type: "function",
|
|
155
|
-
function: { name: t.name, description: t.description, parameters: toolInputSchema(t.input_schema) },
|
|
156
|
-
}));
|
|
157
|
-
}
|
|
158
|
-
const tc = mapToolChoice(body.tool_choice);
|
|
159
|
-
if (tc !== undefined) chatReq.tool_choice = tc;
|
|
160
|
-
|
|
161
|
-
return chatReq;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function uid() { return randomUUID().replace(/-/g, ""); }
|
|
165
|
-
|
|
166
|
-
function nonNegativeTokenCount(value) {
|
|
167
|
-
return Number.isFinite(value) ? Math.max(0, value) : 0;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function invalidToolArguments(toolName) {
|
|
171
|
-
const name = String(toolName || "unknown").slice(0, 100);
|
|
172
|
-
return invalidUpstreamStream(
|
|
173
|
-
`Upstream Chat Completions tool "${name}" did not return valid JSON object arguments`,
|
|
174
|
-
"upstream_tool_arguments_json_invalid",
|
|
175
|
-
);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
export function chatToAnthropic(openaiResp, model, options = {}) {
|
|
179
|
-
const choice = openaiResp.choices?.[0];
|
|
180
|
-
const msg = choice?.message || {};
|
|
181
|
-
const content = [];
|
|
182
|
-
|
|
183
|
-
if (msg.content) content.push({ type: "text", text: msg.content });
|
|
184
|
-
if (Array.isArray(msg.tool_calls)) {
|
|
185
|
-
for (const tc of msg.tool_calls) {
|
|
186
|
-
const rawArguments = tc.function?.arguments;
|
|
187
|
-
let input;
|
|
188
|
-
if (rawArguments === undefined || rawArguments === null || rawArguments === "") {
|
|
189
|
-
input = {};
|
|
190
|
-
} else {
|
|
191
|
-
try {
|
|
192
|
-
input = JSON.parse(rawArguments);
|
|
193
|
-
} catch {
|
|
194
|
-
throw invalidToolArguments(tc.function?.name);
|
|
195
|
-
}
|
|
196
|
-
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
197
|
-
throw invalidToolArguments(tc.function?.name);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
content.push({ type: "tool_use", id: tc.id, name: tc.function.name, input });
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const u = openaiResp.usage || {};
|
|
205
|
-
const prompt = nonNegativeTokenCount(u.prompt_tokens);
|
|
206
|
-
const cached = Math.min(prompt, nonNegativeTokenCount(u.prompt_tokens_details?.cached_tokens));
|
|
207
|
-
const usage = {
|
|
208
|
-
input_tokens: prompt - cached,
|
|
209
|
-
output_tokens: nonNegativeTokenCount(u.completion_tokens),
|
|
210
|
-
};
|
|
211
|
-
if (cached > 0) usage.cache_read_input_tokens = cached;
|
|
212
|
-
|
|
213
|
-
return {
|
|
214
|
-
id: `msg_${uid()}`,
|
|
215
|
-
type: "message",
|
|
216
|
-
role: "assistant",
|
|
217
|
-
model: options.forceModel ? model : (openaiResp.model || model),
|
|
218
|
-
content,
|
|
219
|
-
stop_reason: mapStopReason(choice?.finish_reason),
|
|
220
|
-
stop_sequence: null,
|
|
221
|
-
usage,
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// Consume OpenAI chat SSE lines and emit Anthropic SSE events.
|
|
226
|
-
export async function streamAnthropicFromLines(lineIterator, emit, model, options = {}) {
|
|
227
|
-
const msgId = `msg_${uid()}`;
|
|
228
|
-
let started = false;
|
|
229
|
-
let blockIndex = -1;
|
|
230
|
-
let textOpen = false;
|
|
231
|
-
let actualModel = model;
|
|
232
|
-
let finishReason = null;
|
|
233
|
-
const toolBlocks = {}; // openaiIndex -> { anthropicIndex }
|
|
234
|
-
let sawToolUse = false;
|
|
235
|
-
const trailingTextChunks = [];
|
|
236
|
-
let outputTokens = 0;
|
|
237
|
-
let sawDone = false;
|
|
238
|
-
const toolArgumentGuard = new ToolArgumentDeltaGuard();
|
|
239
|
-
let performanceOutputSeen = false;
|
|
240
|
-
const markPerformanceOutput = () => {
|
|
241
|
-
if (performanceOutputSeen) return;
|
|
242
|
-
performanceOutputSeen = true;
|
|
243
|
-
markFirstOutput();
|
|
244
|
-
};
|
|
245
|
-
|
|
246
|
-
const ensureStart = async () => {
|
|
247
|
-
if (started) return;
|
|
248
|
-
started = true;
|
|
249
|
-
await emit("message_start", { type: "message_start", message: {
|
|
250
|
-
id: msgId, type: "message", role: "assistant", model: actualModel,
|
|
251
|
-
content: [], stop_reason: null, stop_sequence: null,
|
|
252
|
-
usage: { input_tokens: 0, output_tokens: 0 },
|
|
253
|
-
} });
|
|
254
|
-
};
|
|
255
|
-
|
|
256
|
-
const openText = async () => {
|
|
257
|
-
if (textOpen) return;
|
|
258
|
-
blockIndex += 1;
|
|
259
|
-
textOpen = true;
|
|
260
|
-
await emit("content_block_start", { type: "content_block_start", index: blockIndex,
|
|
261
|
-
content_block: { type: "text", text: "" } });
|
|
262
|
-
};
|
|
263
|
-
const closeText = async () => {
|
|
264
|
-
if (!textOpen) return;
|
|
265
|
-
await emit("content_block_stop", { type: "content_block_stop", index: blockIndex });
|
|
266
|
-
textOpen = false;
|
|
267
|
-
};
|
|
268
|
-
|
|
269
|
-
for await (const line of lineIterator) {
|
|
270
|
-
if (!line.startsWith("data: ")) continue;
|
|
271
|
-
const data = line.slice(6).trim();
|
|
272
|
-
if (data === "[DONE]") {
|
|
273
|
-
sawDone = true;
|
|
274
|
-
break;
|
|
275
|
-
}
|
|
276
|
-
let parsed;
|
|
277
|
-
try {
|
|
278
|
-
parsed = JSON.parse(data);
|
|
279
|
-
} catch {
|
|
280
|
-
throw invalidUpstreamStream(
|
|
281
|
-
"Upstream Chat Completions SSE event contained invalid JSON",
|
|
282
|
-
"upstream_stream_json_invalid",
|
|
283
|
-
);
|
|
284
|
-
}
|
|
285
|
-
const upstreamError = upstreamChatStreamError(parsed);
|
|
286
|
-
if (upstreamError) throw upstreamError;
|
|
287
|
-
if (!options.forceModel && parsed.model) actualModel = parsed.model;
|
|
288
|
-
if (parsed.usage?.completion_tokens != null) {
|
|
289
|
-
outputTokens = nonNegativeTokenCount(parsed.usage.completion_tokens);
|
|
290
|
-
markOutputTokens(outputTokens);
|
|
291
|
-
}
|
|
292
|
-
const choice = parsed.choices?.[0];
|
|
293
|
-
if (!choice) continue;
|
|
294
|
-
const delta = choice.delta || {};
|
|
295
|
-
if (isChatOutputDelta(delta)) markPerformanceOutput();
|
|
296
|
-
await ensureStart();
|
|
297
|
-
|
|
298
|
-
if (delta.content) {
|
|
299
|
-
if (sawToolUse) {
|
|
300
|
-
trailingTextChunks.push(delta.content);
|
|
301
|
-
} else {
|
|
302
|
-
await openText();
|
|
303
|
-
await emit("content_block_delta", { type: "content_block_delta", index: blockIndex,
|
|
304
|
-
delta: { type: "text_delta", text: delta.content } });
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
if (Array.isArray(delta.tool_calls)) {
|
|
309
|
-
sawToolUse = true;
|
|
310
|
-
await closeText();
|
|
311
|
-
for (const tc of delta.tool_calls) {
|
|
312
|
-
const oi = tc.index ?? 0;
|
|
313
|
-
if (!toolBlocks[oi]) {
|
|
314
|
-
blockIndex += 1;
|
|
315
|
-
toolBlocks[oi] = { anthropicIndex: blockIndex };
|
|
316
|
-
await emit("content_block_start", { type: "content_block_start", index: blockIndex,
|
|
317
|
-
content_block: { type: "tool_use", id: tc.id || `tu_${uid()}`, name: tc.function?.name || "", input: {} } });
|
|
318
|
-
}
|
|
319
|
-
const argumentDelta = tc.function?.arguments;
|
|
320
|
-
toolArgumentGuard.observe(oi, argumentDelta);
|
|
321
|
-
if (argumentDelta) {
|
|
322
|
-
await emit("content_block_delta", { type: "content_block_delta", index: toolBlocks[oi].anthropicIndex,
|
|
323
|
-
delta: { type: "input_json_delta", partial_json: argumentDelta } });
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
if (choice.finish_reason) finishReason = choice.finish_reason;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
if (!sawDone) throw incompleteUpstreamStream("[DONE]");
|
|
332
|
-
|
|
333
|
-
await ensureStart();
|
|
334
|
-
if (textOpen) await closeText();
|
|
335
|
-
for (const oi of Object.keys(toolBlocks)) {
|
|
336
|
-
await emit("content_block_stop", { type: "content_block_stop", index: toolBlocks[oi].anthropicIndex });
|
|
337
|
-
}
|
|
338
|
-
if (trailingTextChunks.length) {
|
|
339
|
-
await openText();
|
|
340
|
-
for (const text of trailingTextChunks) {
|
|
341
|
-
await emit("content_block_delta", { type: "content_block_delta", index: blockIndex,
|
|
342
|
-
delta: { type: "text_delta", text } });
|
|
343
|
-
}
|
|
344
|
-
await closeText();
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
const stop_reason = finishReason ? mapStopReason(finishReason) : (sawToolUse ? "tool_use" : "end_turn");
|
|
348
|
-
await emit("message_delta", { type: "message_delta", delta: { stop_reason, stop_sequence: null },
|
|
349
|
-
usage: { output_tokens: outputTokens } });
|
|
350
|
-
await emit("message_stop", { type: "message_stop" });
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
export function anthropicTokenText(body) {
|
|
354
|
-
const parts = [];
|
|
355
|
-
const sys = systemToText(body.system);
|
|
356
|
-
if (sys) parts.push(sys);
|
|
357
|
-
|
|
358
|
-
for (const m of body.messages || []) {
|
|
359
|
-
if (typeof m.content === "string") {
|
|
360
|
-
parts.push(m.content);
|
|
361
|
-
} else if (Array.isArray(m.content)) {
|
|
362
|
-
for (const b of m.content) {
|
|
363
|
-
if (b.type === "text") parts.push(b.text);
|
|
364
|
-
else if (b.type === "tool_use") parts.push(b.name + JSON.stringify(b.input ?? {}));
|
|
365
|
-
else if (b.type === "tool_result") parts.push(toolResultContent(b.content));
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
if (Array.isArray(body.tools)) {
|
|
371
|
-
for (const t of body.tools) {
|
|
372
|
-
parts.push(t.name + (t.description || "") + JSON.stringify(t.input_schema || {}));
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
return parts.join("\n");
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
export async function countTokenText(text) {
|
|
380
|
-
const count = await tokenCounter();
|
|
381
|
-
return { input_tokens: count(text) };
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
// Copilot has no count_tokens endpoint; estimate locally with gpt-tokenizer.
|
|
385
|
-
export async function countTokens(body) {
|
|
386
|
-
return countTokenText(anthropicTokenText(body));
|
|
387
|
-
}
|
package/src/claude-config.mjs
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import os from "node:os";
|
|
4
|
-
import { status } from "./status.mjs";
|
|
5
|
-
import { atomicWriteFileIfChangedSync } from "./atomic-file.mjs";
|
|
6
|
-
import { adapterBaseUrl } from "./running-adapter.mjs";
|
|
7
|
-
|
|
8
|
-
const SETTINGS_PATH = path.join(os.homedir(), ".claude", "settings.json");
|
|
9
|
-
|
|
10
|
-
// Return an updated settings object without mutating the input.
|
|
11
|
-
export function computeUpdatedSettings(settings, port, host = "127.0.0.1") {
|
|
12
|
-
const target = adapterBaseUrl(host, port);
|
|
13
|
-
const current = settings?.env?.ANTHROPIC_BASE_URL;
|
|
14
|
-
const currentToken = settings?.env?.ANTHROPIC_AUTH_TOKEN;
|
|
15
|
-
const changed = current !== target || currentToken !== "dummy";
|
|
16
|
-
const json = {
|
|
17
|
-
...settings,
|
|
18
|
-
env: {
|
|
19
|
-
...(settings?.env || {}),
|
|
20
|
-
ANTHROPIC_BASE_URL: target,
|
|
21
|
-
ANTHROPIC_AUTH_TOKEN: "dummy",
|
|
22
|
-
},
|
|
23
|
-
};
|
|
24
|
-
return { json, changed };
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// Point Claude Code at the adapter by updating ~/.claude/settings.json.
|
|
28
|
-
export function ensureClaudeConfig(port = 2026, { filePath = SETTINGS_PATH, host = "127.0.0.1" } = {}) {
|
|
29
|
-
const target = adapterBaseUrl(host, port);
|
|
30
|
-
|
|
31
|
-
if (!fs.existsSync(filePath)) {
|
|
32
|
-
const { json } = computeUpdatedSettings({}, port, host);
|
|
33
|
-
atomicWriteFileIfChangedSync(filePath, JSON.stringify(json, null, 2) + "\n");
|
|
34
|
-
console.log(status("ok", `Created ~/.claude/settings.json for Claude Code at ${target}`));
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
let raw, settings;
|
|
39
|
-
try {
|
|
40
|
-
raw = fs.readFileSync(filePath, "utf-8");
|
|
41
|
-
settings = JSON.parse(raw);
|
|
42
|
-
} catch (e) {
|
|
43
|
-
console.log(status("warn", `Could not parse ~/.claude/settings.json: ${e.message}`));
|
|
44
|
-
console.log(status("info", "Fix the JSON file and rerun ccdx to configure Claude Code automatically."));
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const { json, changed } = computeUpdatedSettings(settings, port, host);
|
|
49
|
-
if (!changed) {
|
|
50
|
-
console.log(status("ok", `Claude Code already points to ${target}`));
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
atomicWriteFileIfChangedSync(`${filePath}.bak`, raw);
|
|
55
|
-
atomicWriteFileIfChangedSync(filePath, JSON.stringify(json, null, 2) + "\n");
|
|
56
|
-
console.log(status("ok", `Configured Claude Code ANTHROPIC_BASE_URL="${target}" and backed up settings.json.bak`));
|
|
57
|
-
}
|