codex-copilot-dx 0.1.6 → 0.4.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 +17 -15
- package/bin/cli.mjs +16 -16
- package/package.json +7 -5
- package/src/adapter.mjs +235 -417
- package/src/anthropic.mjs +267 -0
- package/src/auth.mjs +95 -0
- package/src/claude-config.mjs +56 -0
- package/src/config.mjs +77 -17
- package/src/copilot.mjs +316 -0
- package/src/launcher.mjs +8 -79
- package/src/status.mjs +11 -0
- package/src/stream.mjs +22 -0
- package/CHANGELOG.md +0 -19
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { encode } from "gpt-tokenizer";
|
|
3
|
+
|
|
4
|
+
// Anthropic Messages API to OpenAI chat/completions translation.
|
|
5
|
+
// Pure functions only; network calls stay in adapter.mjs and copilot.mjs.
|
|
6
|
+
|
|
7
|
+
export function mapStopReason(finishReason) {
|
|
8
|
+
switch (finishReason) {
|
|
9
|
+
case "stop": return "end_turn";
|
|
10
|
+
case "tool_calls": return "tool_use";
|
|
11
|
+
case "length": return "max_tokens";
|
|
12
|
+
default: return "end_turn";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Convert a string or text-block system field into one system message.
|
|
17
|
+
function systemToText(system) {
|
|
18
|
+
if (!system) return null;
|
|
19
|
+
if (typeof system === "string") return system;
|
|
20
|
+
if (Array.isArray(system)) return system.map((b) => (typeof b === "string" ? b : b.text || "")).join("\n");
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Convert tool_result content into the string payload expected by OpenAI tool messages.
|
|
25
|
+
function toolResultContent(content) {
|
|
26
|
+
if (typeof content === "string") return content;
|
|
27
|
+
if (Array.isArray(content)) {
|
|
28
|
+
return content.map((b) => (b.type === "text" ? b.text : typeof b === "string" ? b : JSON.stringify(b))).join("");
|
|
29
|
+
}
|
|
30
|
+
return JSON.stringify(content);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Convert one Anthropic message into zero or more OpenAI messages.
|
|
34
|
+
function convertMessage(msg) {
|
|
35
|
+
const out = [];
|
|
36
|
+
const role = msg.role; // "user" | "assistant"
|
|
37
|
+
|
|
38
|
+
if (typeof msg.content === "string") {
|
|
39
|
+
out.push({ role, content: msg.content });
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
if (!Array.isArray(msg.content)) return out;
|
|
43
|
+
|
|
44
|
+
// tool_result blocks must become independent role:"tool" messages.
|
|
45
|
+
const toolResults = msg.content.filter((b) => b.type === "tool_result");
|
|
46
|
+
for (const tr of toolResults) {
|
|
47
|
+
out.push({ role: "tool", tool_call_id: tr.tool_use_id, content: toolResultContent(tr.content) });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Convert remaining text, image, and tool_use blocks.
|
|
51
|
+
const textImageParts = [];
|
|
52
|
+
const toolCalls = [];
|
|
53
|
+
for (const b of msg.content) {
|
|
54
|
+
if (b.type === "text") {
|
|
55
|
+
textImageParts.push({ type: "text", text: b.text });
|
|
56
|
+
} else if (b.type === "image") {
|
|
57
|
+
const url = b.source?.type === "url"
|
|
58
|
+
? b.source.url
|
|
59
|
+
: `data:${b.source.media_type};base64,${b.source.data}`;
|
|
60
|
+
textImageParts.push({ type: "image_url", image_url: { url } });
|
|
61
|
+
} else if (b.type === "tool_use") {
|
|
62
|
+
toolCalls.push({ id: b.id, type: "function", function: { name: b.name, arguments: JSON.stringify(b.input ?? {}) } });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (toolCalls.length > 0) {
|
|
67
|
+
const m = { role, content: null, tool_calls: toolCalls };
|
|
68
|
+
if (textImageParts.length === 1 && textImageParts[0].type === "text") m.content = textImageParts[0].text;
|
|
69
|
+
else if (textImageParts.length > 0) m.content = textImageParts;
|
|
70
|
+
out.push(m);
|
|
71
|
+
} else if (textImageParts.length > 0) {
|
|
72
|
+
if (textImageParts.length === 1 && textImageParts[0].type === "text") {
|
|
73
|
+
out.push({ role, content: textImageParts[0].text });
|
|
74
|
+
} else {
|
|
75
|
+
out.push({ role, content: textImageParts });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function mapToolChoice(tc) {
|
|
82
|
+
if (!tc) return undefined;
|
|
83
|
+
switch (tc.type) {
|
|
84
|
+
case "auto": return "auto";
|
|
85
|
+
case "none": return "none";
|
|
86
|
+
case "any": return "required";
|
|
87
|
+
case "tool": return { type: "function", function: { name: tc.name } };
|
|
88
|
+
default: return undefined;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function anthropicToChat(body) {
|
|
93
|
+
const messages = [];
|
|
94
|
+
const sys = systemToText(body.system);
|
|
95
|
+
if (sys) messages.push({ role: "system", content: sys });
|
|
96
|
+
for (const m of body.messages || []) {
|
|
97
|
+
for (const converted of convertMessage(m)) messages.push(converted);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const chatReq = { model: body.model, messages };
|
|
101
|
+
if (body.max_tokens !== undefined) chatReq.max_tokens = body.max_tokens;
|
|
102
|
+
if (body.temperature !== undefined) chatReq.temperature = body.temperature;
|
|
103
|
+
if (body.top_p !== undefined) chatReq.top_p = body.top_p;
|
|
104
|
+
if (Array.isArray(body.stop_sequences) && body.stop_sequences.length) chatReq.stop = body.stop_sequences;
|
|
105
|
+
|
|
106
|
+
if (Array.isArray(body.tools) && body.tools.length) {
|
|
107
|
+
chatReq.tools = body.tools.map((t) => ({
|
|
108
|
+
type: "function",
|
|
109
|
+
function: { name: t.name, description: t.description, parameters: t.input_schema },
|
|
110
|
+
}));
|
|
111
|
+
}
|
|
112
|
+
const tc = mapToolChoice(body.tool_choice);
|
|
113
|
+
if (tc !== undefined) chatReq.tool_choice = tc;
|
|
114
|
+
|
|
115
|
+
return chatReq;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function uid() { return randomUUID().replace(/-/g, ""); }
|
|
119
|
+
|
|
120
|
+
export function chatToAnthropic(openaiResp, model) {
|
|
121
|
+
const choice = openaiResp.choices?.[0];
|
|
122
|
+
const msg = choice?.message || {};
|
|
123
|
+
const content = [];
|
|
124
|
+
|
|
125
|
+
if (msg.content) content.push({ type: "text", text: msg.content });
|
|
126
|
+
if (Array.isArray(msg.tool_calls)) {
|
|
127
|
+
for (const tc of msg.tool_calls) {
|
|
128
|
+
let input = {};
|
|
129
|
+
try { input = JSON.parse(tc.function.arguments || "{}"); } catch { input = {}; }
|
|
130
|
+
content.push({ type: "tool_use", id: tc.id, name: tc.function.name, input });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const u = openaiResp.usage || {};
|
|
135
|
+
const cached = u.prompt_tokens_details?.cached_tokens ?? 0;
|
|
136
|
+
const usage = {
|
|
137
|
+
input_tokens: (u.prompt_tokens ?? 0) - cached,
|
|
138
|
+
output_tokens: u.completion_tokens ?? 0,
|
|
139
|
+
};
|
|
140
|
+
if (cached > 0) usage.cache_read_input_tokens = cached;
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
id: `msg_${uid()}`,
|
|
144
|
+
type: "message",
|
|
145
|
+
role: "assistant",
|
|
146
|
+
model: openaiResp.model || model,
|
|
147
|
+
content,
|
|
148
|
+
stop_reason: mapStopReason(choice?.finish_reason),
|
|
149
|
+
stop_sequence: null,
|
|
150
|
+
usage,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Consume OpenAI chat SSE lines and emit Anthropic SSE events.
|
|
155
|
+
export async function streamAnthropicFromLines(lineIterator, emit, model) {
|
|
156
|
+
const msgId = `msg_${uid()}`;
|
|
157
|
+
let started = false;
|
|
158
|
+
let blockIndex = -1;
|
|
159
|
+
let textOpen = false;
|
|
160
|
+
let actualModel = model;
|
|
161
|
+
let finishReason = null;
|
|
162
|
+
const toolBlocks = {}; // openaiIndex -> { anthropicIndex }
|
|
163
|
+
let sawToolUse = false;
|
|
164
|
+
let outputTokens = 0;
|
|
165
|
+
|
|
166
|
+
const ensureStart = () => {
|
|
167
|
+
if (started) return;
|
|
168
|
+
started = true;
|
|
169
|
+
emit("message_start", { type: "message_start", message: {
|
|
170
|
+
id: msgId, type: "message", role: "assistant", model: actualModel,
|
|
171
|
+
content: [], stop_reason: null, stop_sequence: null,
|
|
172
|
+
usage: { input_tokens: 0, output_tokens: 0 },
|
|
173
|
+
} });
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const openText = () => {
|
|
177
|
+
if (textOpen) return;
|
|
178
|
+
blockIndex += 1;
|
|
179
|
+
textOpen = true;
|
|
180
|
+
emit("content_block_start", { type: "content_block_start", index: blockIndex,
|
|
181
|
+
content_block: { type: "text", text: "" } });
|
|
182
|
+
};
|
|
183
|
+
const closeText = () => {
|
|
184
|
+
if (!textOpen) return;
|
|
185
|
+
emit("content_block_stop", { type: "content_block_stop", index: blockIndex });
|
|
186
|
+
textOpen = false;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
for await (const line of lineIterator) {
|
|
190
|
+
if (!line.startsWith("data: ")) continue;
|
|
191
|
+
const data = line.slice(6).trim();
|
|
192
|
+
if (data === "[DONE]") break;
|
|
193
|
+
let parsed;
|
|
194
|
+
try { parsed = JSON.parse(data); } catch { continue; }
|
|
195
|
+
if (parsed.model) actualModel = parsed.model;
|
|
196
|
+
if (parsed.usage?.completion_tokens != null) outputTokens = parsed.usage.completion_tokens;
|
|
197
|
+
const choice = parsed.choices?.[0];
|
|
198
|
+
if (!choice) continue;
|
|
199
|
+
const delta = choice.delta || {};
|
|
200
|
+
ensureStart();
|
|
201
|
+
|
|
202
|
+
if (delta.content && !sawToolUse) {
|
|
203
|
+
openText();
|
|
204
|
+
emit("content_block_delta", { type: "content_block_delta", index: blockIndex,
|
|
205
|
+
delta: { type: "text_delta", text: delta.content } });
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (Array.isArray(delta.tool_calls)) {
|
|
209
|
+
sawToolUse = true;
|
|
210
|
+
closeText();
|
|
211
|
+
for (const tc of delta.tool_calls) {
|
|
212
|
+
const oi = tc.index ?? 0;
|
|
213
|
+
if (!toolBlocks[oi]) {
|
|
214
|
+
blockIndex += 1;
|
|
215
|
+
toolBlocks[oi] = { anthropicIndex: blockIndex };
|
|
216
|
+
emit("content_block_start", { type: "content_block_start", index: blockIndex,
|
|
217
|
+
content_block: { type: "tool_use", id: tc.id || `tu_${uid()}`, name: tc.function?.name || "", input: {} } });
|
|
218
|
+
}
|
|
219
|
+
if (tc.function?.arguments) {
|
|
220
|
+
emit("content_block_delta", { type: "content_block_delta", index: toolBlocks[oi].anthropicIndex,
|
|
221
|
+
delta: { type: "input_json_delta", partial_json: tc.function.arguments } });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (choice.finish_reason) finishReason = choice.finish_reason;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
ensureStart();
|
|
230
|
+
if (textOpen) closeText();
|
|
231
|
+
for (const oi of Object.keys(toolBlocks)) {
|
|
232
|
+
emit("content_block_stop", { type: "content_block_stop", index: toolBlocks[oi].anthropicIndex });
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const stop_reason = finishReason ? mapStopReason(finishReason) : (sawToolUse ? "tool_use" : "end_turn");
|
|
236
|
+
emit("message_delta", { type: "message_delta", delta: { stop_reason, stop_sequence: null },
|
|
237
|
+
usage: { output_tokens: outputTokens } });
|
|
238
|
+
emit("message_stop", { type: "message_stop" });
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Copilot has no count_tokens endpoint; estimate locally with gpt-tokenizer.
|
|
242
|
+
export function countTokens(body) {
|
|
243
|
+
const parts = [];
|
|
244
|
+
const sys = systemToText(body.system);
|
|
245
|
+
if (sys) parts.push(sys);
|
|
246
|
+
|
|
247
|
+
for (const m of body.messages || []) {
|
|
248
|
+
if (typeof m.content === "string") {
|
|
249
|
+
parts.push(m.content);
|
|
250
|
+
} else if (Array.isArray(m.content)) {
|
|
251
|
+
for (const b of m.content) {
|
|
252
|
+
if (b.type === "text") parts.push(b.text);
|
|
253
|
+
else if (b.type === "tool_use") parts.push(b.name + JSON.stringify(b.input ?? {}));
|
|
254
|
+
else if (b.type === "tool_result") parts.push(toolResultContent(b.content));
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (Array.isArray(body.tools)) {
|
|
260
|
+
for (const t of body.tools) {
|
|
261
|
+
parts.push(t.name + (t.description || "") + JSON.stringify(t.input_schema || {}));
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const text = parts.join("\n");
|
|
266
|
+
return { input_tokens: encode(text).length };
|
|
267
|
+
}
|
package/src/auth.mjs
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { status } from "./status.mjs";
|
|
6
|
+
|
|
7
|
+
const CLIENT_ID = "Iv1.b507a08c87ecfe98"; // Public GitHub Copilot client ID.
|
|
8
|
+
const SCOPE = "read:user";
|
|
9
|
+
const GITHUB_TOKEN_PATH = path.join(os.homedir(), ".local", "share", "copilot-api", "github_token");
|
|
10
|
+
|
|
11
|
+
// Map GitHub polling responses to a small local state machine.
|
|
12
|
+
export function interpretPoll(data) {
|
|
13
|
+
if (typeof data.access_token === "string" && data.access_token) return { state: "done", token: data.access_token };
|
|
14
|
+
switch (data.error) {
|
|
15
|
+
case "authorization_pending": return { state: "wait" };
|
|
16
|
+
case "slow_down": return { state: "slow" };
|
|
17
|
+
default: return { state: "fail", error: data.error || "unknown" };
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
|
|
22
|
+
|
|
23
|
+
// On macOS, copy the user code and open the verification page. Fail quietly.
|
|
24
|
+
function openAndCopy(userCode, verificationUri) {
|
|
25
|
+
if (process.platform !== "darwin") return;
|
|
26
|
+
try {
|
|
27
|
+
const pb = spawn("pbcopy");
|
|
28
|
+
pb.on("error", () => {});
|
|
29
|
+
pb.stdin.on("error", () => {});
|
|
30
|
+
pb.on("close", (code) => {
|
|
31
|
+
if (code === 0) console.log(status("ok", "Device code copied to the clipboard"));
|
|
32
|
+
});
|
|
33
|
+
pb.stdin.write(userCode);
|
|
34
|
+
pb.stdin.end();
|
|
35
|
+
} catch {}
|
|
36
|
+
try {
|
|
37
|
+
const op = spawn("open", [verificationUri], { detached: true, stdio: "ignore" });
|
|
38
|
+
op.on("error", () => {});
|
|
39
|
+
} catch {}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function ensureAuth() {
|
|
43
|
+
if (fs.existsSync(GITHUB_TOKEN_PATH)) {
|
|
44
|
+
console.log(status("ok", "GitHub token found"));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
console.log(status("wait", "No GitHub token found. Starting device login..."));
|
|
48
|
+
|
|
49
|
+
// Request a device code.
|
|
50
|
+
const codeResp = await fetch("https://github.com/login/device/code", {
|
|
51
|
+
method: "POST",
|
|
52
|
+
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
53
|
+
body: JSON.stringify({ client_id: CLIENT_ID, scope: SCOPE }),
|
|
54
|
+
});
|
|
55
|
+
if (!codeResp.ok) throw new Error(`device code request failed: ${codeResp.status}`);
|
|
56
|
+
const { device_code, user_code, verification_uri, interval } = await codeResp.json();
|
|
57
|
+
|
|
58
|
+
// Prompt the user.
|
|
59
|
+
console.log(`\n${status("info", `Open ${verification_uri}`)}\n${status("info", `Enter code: ${user_code}`)}\n`);
|
|
60
|
+
openAndCopy(user_code, verification_uri);
|
|
61
|
+
|
|
62
|
+
// Poll until GitHub completes the device flow.
|
|
63
|
+
let waitMs = (interval || 5) * 1000;
|
|
64
|
+
while (true) {
|
|
65
|
+
await sleep(waitMs);
|
|
66
|
+
const pollResp = await fetch("https://github.com/login/oauth/access_token", {
|
|
67
|
+
method: "POST",
|
|
68
|
+
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
69
|
+
body: JSON.stringify({
|
|
70
|
+
client_id: CLIENT_ID,
|
|
71
|
+
device_code,
|
|
72
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
73
|
+
}),
|
|
74
|
+
});
|
|
75
|
+
if (!pollResp.ok) {
|
|
76
|
+
// Treat transient network/server errors as pending and retry.
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const data = await pollResp.json();
|
|
80
|
+
const r = interpretPoll(data);
|
|
81
|
+
if (r.state === "done") {
|
|
82
|
+
writeToken(r.token);
|
|
83
|
+
console.log(status("ok", "Login successful"));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (r.state === "slow") { waitMs += 5000; continue; }
|
|
87
|
+
if (r.state === "fail") throw new Error(`Login failed: ${r.error}`);
|
|
88
|
+
// wait: continue polling.
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function writeToken(token) {
|
|
93
|
+
fs.mkdirSync(path.dirname(GITHUB_TOKEN_PATH), { recursive: true });
|
|
94
|
+
fs.writeFileSync(GITHUB_TOKEN_PATH, token, { mode: 0o600 });
|
|
95
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
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
|
+
|
|
6
|
+
const SETTINGS_PATH = path.join(os.homedir(), ".claude", "settings.json");
|
|
7
|
+
|
|
8
|
+
// Return an updated settings object without mutating the input.
|
|
9
|
+
export function computeUpdatedSettings(settings, port) {
|
|
10
|
+
const target = `http://localhost:${port}`;
|
|
11
|
+
const current = settings?.env?.ANTHROPIC_BASE_URL;
|
|
12
|
+
const currentToken = settings?.env?.ANTHROPIC_AUTH_TOKEN;
|
|
13
|
+
const changed = current !== target || currentToken !== "dummy";
|
|
14
|
+
const json = {
|
|
15
|
+
...settings,
|
|
16
|
+
env: {
|
|
17
|
+
...(settings?.env || {}),
|
|
18
|
+
ANTHROPIC_BASE_URL: target,
|
|
19
|
+
ANTHROPIC_AUTH_TOKEN: "dummy",
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
return { json, changed };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Point Claude Code at the adapter by updating ~/.claude/settings.json.
|
|
26
|
+
export function ensureClaudeConfig(port = 2026) {
|
|
27
|
+
const target = `http://localhost:${port}`;
|
|
28
|
+
|
|
29
|
+
if (!fs.existsSync(SETTINGS_PATH)) {
|
|
30
|
+
fs.mkdirSync(path.dirname(SETTINGS_PATH), { recursive: true });
|
|
31
|
+
const { json } = computeUpdatedSettings({}, port);
|
|
32
|
+
fs.writeFileSync(SETTINGS_PATH, JSON.stringify(json, null, 2) + "\n");
|
|
33
|
+
console.log(status("ok", `Created ~/.claude/settings.json for Claude Code at ${target}`));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let raw, settings;
|
|
38
|
+
try {
|
|
39
|
+
raw = fs.readFileSync(SETTINGS_PATH, "utf-8");
|
|
40
|
+
settings = JSON.parse(raw);
|
|
41
|
+
} catch (e) {
|
|
42
|
+
console.log(status("warn", `Could not parse ~/.claude/settings.json: ${e.message}`));
|
|
43
|
+
console.log(status("info", "Fix the JSON file and rerun codex-copilot-dx to configure Claude Code automatically."));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const { json, changed } = computeUpdatedSettings(settings, port);
|
|
48
|
+
if (!changed) {
|
|
49
|
+
console.log(status("ok", `Claude Code already points to ${target}`));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
fs.writeFileSync(`${SETTINGS_PATH}.bak`, raw);
|
|
54
|
+
fs.writeFileSync(SETTINGS_PATH, JSON.stringify(json, null, 2) + "\n");
|
|
55
|
+
console.log(status("ok", `Configured Claude Code ANTHROPIC_BASE_URL="${target}" and backed up settings.json.bak`));
|
|
56
|
+
}
|
package/src/config.mjs
CHANGED
|
@@ -1,33 +1,93 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import os from "node:os";
|
|
4
|
+
import { status } from "./status.mjs";
|
|
4
5
|
|
|
5
6
|
const CONFIG_PATH = path.join(os.homedir(), ".codex", "config.toml");
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
function setTomlKey(lines, sectionName, key, value) {
|
|
9
|
+
const sectionLine = `[${sectionName}]`;
|
|
10
|
+
const start = lines.findIndex((line) => line.trim() === sectionLine);
|
|
11
|
+
if (start === -1) return false;
|
|
12
|
+
|
|
13
|
+
let end = lines.length;
|
|
14
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
15
|
+
if (/^\s*\[[^\]]+\]\s*$/.test(lines[i])) {
|
|
16
|
+
end = i;
|
|
17
|
+
break;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const nextLine = `${key} = "${value}"`;
|
|
22
|
+
const keyRe = new RegExp(`^\\s*${key}\\s*=`);
|
|
23
|
+
for (let i = start + 1; i < end; i++) {
|
|
24
|
+
if (keyRe.test(lines[i])) {
|
|
25
|
+
const changed = lines[i] !== nextLine;
|
|
26
|
+
lines[i] = nextLine;
|
|
27
|
+
return changed;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
lines.splice(end, 0, nextLine);
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function computeUpdatedCodexConfig(content, adapterPort = 2026) {
|
|
36
|
+
const baseUrl = `http://localhost:${adapterPort}/v1`;
|
|
37
|
+
const anthropicBaseUrl = `http://localhost:${adapterPort}`;
|
|
38
|
+
const hadTrailingNewline = content.endsWith("\n");
|
|
39
|
+
const lines = content.split("\n");
|
|
40
|
+
if (hadTrailingNewline) lines.pop();
|
|
41
|
+
|
|
42
|
+
let changed = false;
|
|
43
|
+
const openaiLine = `openai_base_url = "${baseUrl}"`;
|
|
44
|
+
const openaiIndex = lines.findIndex((line) => /^openai_base_url\s*=/.test(line));
|
|
45
|
+
if (openaiIndex === -1) {
|
|
46
|
+
lines.unshift(openaiLine);
|
|
47
|
+
changed = true;
|
|
48
|
+
} else if (lines[openaiIndex] !== openaiLine) {
|
|
49
|
+
lines[openaiIndex] = openaiLine;
|
|
50
|
+
changed = true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
changed = setTomlKey(lines, "shell_environment_policy.set", "ANTHROPIC_AUTH_TOKEN", "dummy") || changed;
|
|
54
|
+
changed = setTomlKey(lines, "shell_environment_policy.set", "ANTHROPIC_BASE_URL", anthropicBaseUrl) || changed;
|
|
55
|
+
changed = setTomlKey(lines, "shell_environment_policy.set", "OPENAI_BASE_URL", baseUrl) || changed;
|
|
56
|
+
changed = setTomlKey(lines, "shell_environment_policy.set", "OPENAI_API_KEY", "dummy") || changed;
|
|
57
|
+
|
|
58
|
+
return { content: lines.join("\n") + (hadTrailingNewline ? "\n" : ""), changed };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function initialCodexConfig(adapterPort) {
|
|
62
|
+
const baseUrl = `http://localhost:${adapterPort}/v1`;
|
|
63
|
+
const anthropicBaseUrl = `http://localhost:${adapterPort}`;
|
|
64
|
+
return `openai_base_url = "${baseUrl}"
|
|
65
|
+
|
|
66
|
+
[shell_environment_policy]
|
|
67
|
+
inherit = "core"
|
|
68
|
+
|
|
69
|
+
[shell_environment_policy.set]
|
|
70
|
+
ANTHROPIC_AUTH_TOKEN = "dummy"
|
|
71
|
+
ANTHROPIC_BASE_URL = "${anthropicBaseUrl}"
|
|
72
|
+
OPENAI_BASE_URL = "${baseUrl}"
|
|
73
|
+
OPENAI_API_KEY = "dummy"
|
|
74
|
+
`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function ensureCodexConfig(adapterPort = 2026) {
|
|
8
78
|
const baseUrl = `http://localhost:${adapterPort}/v1`;
|
|
9
79
|
|
|
10
80
|
if (!fs.existsSync(CONFIG_PATH)) {
|
|
11
|
-
// Codex not
|
|
81
|
+
// Codex config does not exist yet; create the local proxy defaults.
|
|
12
82
|
fs.mkdirSync(path.dirname(CONFIG_PATH), { recursive: true });
|
|
13
|
-
fs.writeFileSync(CONFIG_PATH,
|
|
14
|
-
console.log("
|
|
83
|
+
fs.writeFileSync(CONFIG_PATH, initialCodexConfig(adapterPort));
|
|
84
|
+
console.log(status("ok", "Created ~/.codex/config.toml"));
|
|
15
85
|
return;
|
|
16
86
|
}
|
|
17
87
|
|
|
18
88
|
let content = fs.readFileSync(CONFIG_PATH, "utf-8");
|
|
89
|
+
const updated = computeUpdatedCodexConfig(content, adapterPort);
|
|
19
90
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
content = content.replace(
|
|
23
|
-
/openai_base_url\s*=\s*"[^"]*"/,
|
|
24
|
-
`openai_base_url = "${baseUrl}"`,
|
|
25
|
-
);
|
|
26
|
-
} else {
|
|
27
|
-
// Add at top of file
|
|
28
|
-
content = `openai_base_url = "${baseUrl}"\n` + content;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
fs.writeFileSync(CONFIG_PATH, content);
|
|
32
|
-
console.log(`[codex-copilot-dx] Configured openai_base_url = "${baseUrl}"`);
|
|
91
|
+
fs.writeFileSync(CONFIG_PATH, updated.content);
|
|
92
|
+
console.log(status("ok", `Configured Codex base URL: ${baseUrl}`));
|
|
33
93
|
}
|