@yhong91/cpac 0.1.24 → 0.1.26
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 +28 -47
- package/dist/agents.js +784 -0
- package/dist/claude.js +373 -0
- package/dist/codex.js +378 -0
- package/dist/config.js +440 -0
- package/dist/cpac.js +157 -2252
- package/dist/proxy.js +341 -0
- package/dist/util.js +192 -0
- package/package.json +2 -3
package/dist/proxy.js
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { createServer, request as httpRequest, } from "node:http";
|
|
5
|
+
import { request as httpsRequest } from "node:https";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { apiBase, originalBytes, proxyFingerprint, readState, stateBytes, stateProxy, } from "./config.js";
|
|
9
|
+
import { CPACError, atomicWrite, objectValue, resolveApiKey } from "./util.js";
|
|
10
|
+
const HOP_BY_HOP_HEADERS = new Set([
|
|
11
|
+
"connection",
|
|
12
|
+
"keep-alive",
|
|
13
|
+
"proxy-authenticate",
|
|
14
|
+
"proxy-authorization",
|
|
15
|
+
"proxy-connection",
|
|
16
|
+
"te",
|
|
17
|
+
"trailer",
|
|
18
|
+
"transfer-encoding",
|
|
19
|
+
"upgrade",
|
|
20
|
+
]);
|
|
21
|
+
const CLIENT_CREDENTIAL_HEADERS = new Set([
|
|
22
|
+
"authorization",
|
|
23
|
+
"chatgpt-account-id",
|
|
24
|
+
"cookie",
|
|
25
|
+
"openai-organization",
|
|
26
|
+
"openai-project",
|
|
27
|
+
"x-api-key",
|
|
28
|
+
"x-opencodex-api-key",
|
|
29
|
+
]);
|
|
30
|
+
function localBrowserOrigin(value) {
|
|
31
|
+
if (!value)
|
|
32
|
+
return true;
|
|
33
|
+
try {
|
|
34
|
+
const hostname = new URL(value).hostname.toLowerCase();
|
|
35
|
+
return (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1");
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export function proxyHeaders(headers, apiKey) {
|
|
42
|
+
const forwarded = {};
|
|
43
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
44
|
+
if (value === undefined ||
|
|
45
|
+
name === "host" ||
|
|
46
|
+
CLIENT_CREDENTIAL_HEADERS.has(name) ||
|
|
47
|
+
HOP_BY_HOP_HEADERS.has(name)) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
forwarded[name] = value;
|
|
51
|
+
}
|
|
52
|
+
forwarded.authorization = `Bearer ${apiKey}`;
|
|
53
|
+
return forwarded;
|
|
54
|
+
}
|
|
55
|
+
export function responseHeaders(headers) {
|
|
56
|
+
const forwarded = {};
|
|
57
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
58
|
+
if (value === undefined || HOP_BY_HOP_HEADERS.has(name))
|
|
59
|
+
continue;
|
|
60
|
+
forwarded[name] = value;
|
|
61
|
+
}
|
|
62
|
+
return forwarded;
|
|
63
|
+
}
|
|
64
|
+
export function upstreamUrl(cpaUrl, requestUrl) {
|
|
65
|
+
try {
|
|
66
|
+
const incoming = new URL(requestUrl, "http://127.0.0.1");
|
|
67
|
+
const suffix = incoming.pathname === "/v1"
|
|
68
|
+
? ""
|
|
69
|
+
: incoming.pathname.startsWith("/v1/")
|
|
70
|
+
? incoming.pathname.slice(3)
|
|
71
|
+
: incoming.pathname;
|
|
72
|
+
return new URL(`${apiBase(cpaUrl)}${suffix}${incoming.search}`);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
throw new CPACError(`invalid upstream request URL: ${requestUrl}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export async function createLoopbackProxy(cpaUrl, apiKey, proxyId, port) {
|
|
79
|
+
const server = createServer((request, response) => {
|
|
80
|
+
const requestUrl = request.url ?? "/";
|
|
81
|
+
if (requestUrl === "/_cpac/health") {
|
|
82
|
+
if (request.headers["x-cpac-proxy-id"] !== proxyId) {
|
|
83
|
+
response.writeHead(404).end();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
response.writeHead(204).end();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (requestUrl === "/_cpac/shutdown" && request.method === "POST") {
|
|
90
|
+
if (request.headers["x-cpac-proxy-id"] !== proxyId) {
|
|
91
|
+
response.writeHead(404).end();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
response.writeHead(204).end();
|
|
95
|
+
setImmediate(() => {
|
|
96
|
+
server.closeAllConnections?.();
|
|
97
|
+
server.close();
|
|
98
|
+
});
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (!localBrowserOrigin(request.headers.origin)) {
|
|
102
|
+
response.writeHead(403, { "content-type": "application/json" });
|
|
103
|
+
response.end(JSON.stringify({ error: "non-local origin rejected" }));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
let target;
|
|
107
|
+
try {
|
|
108
|
+
target = upstreamUrl(cpaUrl, requestUrl);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
response.writeHead(400).end("invalid request URL");
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const send = target.protocol === "https:" ? httpsRequest : httpRequest;
|
|
115
|
+
const upstream = send(target, {
|
|
116
|
+
method: request.method,
|
|
117
|
+
headers: proxyHeaders(request.headers, apiKey),
|
|
118
|
+
}, (upstreamResponse) => {
|
|
119
|
+
response.writeHead(upstreamResponse.statusCode ?? 502, responseHeaders(upstreamResponse.headers));
|
|
120
|
+
upstreamResponse.pipe(response);
|
|
121
|
+
});
|
|
122
|
+
upstream.once("error", () => {
|
|
123
|
+
if (!response.headersSent) {
|
|
124
|
+
response.writeHead(502, { "content-type": "application/json" });
|
|
125
|
+
}
|
|
126
|
+
if (!response.writableEnded)
|
|
127
|
+
response.end(JSON.stringify({ error: "CPA upstream request failed" }));
|
|
128
|
+
});
|
|
129
|
+
request.once("aborted", () => upstream.destroy());
|
|
130
|
+
response.once("close", () => {
|
|
131
|
+
if (!response.writableEnded)
|
|
132
|
+
upstream.destroy();
|
|
133
|
+
});
|
|
134
|
+
request.pipe(upstream);
|
|
135
|
+
});
|
|
136
|
+
server.on("upgrade", (_request, socket) => {
|
|
137
|
+
socket.end("HTTP/1.1 426 Upgrade Required\r\nConnection: close\r\nContent-Length: 0\r\n\r\n");
|
|
138
|
+
});
|
|
139
|
+
server.on("clientError", (_error, socket) => socket.destroy());
|
|
140
|
+
await new Promise((resolveListen, rejectListen) => {
|
|
141
|
+
server.once("error", rejectListen);
|
|
142
|
+
server.listen(port, "127.0.0.1", () => {
|
|
143
|
+
server.off("error", rejectListen);
|
|
144
|
+
resolveListen();
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
const address = server.address();
|
|
148
|
+
if (!address || typeof address === "string") {
|
|
149
|
+
server.close();
|
|
150
|
+
throw new CPACError("cannot determine loopback proxy port");
|
|
151
|
+
}
|
|
152
|
+
return { server, port: address.port };
|
|
153
|
+
}
|
|
154
|
+
export async function runProxyChild(port) {
|
|
155
|
+
const cpaUrl = process.env.CPAC_PROXY_UPSTREAM;
|
|
156
|
+
const apiKey = process.env.CPAC_PROXY_API_KEY;
|
|
157
|
+
const proxyId = process.env.CPAC_PROXY_ID;
|
|
158
|
+
if (!cpaUrl || !apiKey || !proxyId)
|
|
159
|
+
return 1;
|
|
160
|
+
try {
|
|
161
|
+
const proxy = await createLoopbackProxy(cpaUrl, apiKey, proxyId, port);
|
|
162
|
+
process.send?.({ ready: true, port: proxy.port, pid: process.pid });
|
|
163
|
+
const close = () => {
|
|
164
|
+
proxy.server.closeAllConnections?.();
|
|
165
|
+
proxy.server.close();
|
|
166
|
+
};
|
|
167
|
+
process.once("SIGINT", close);
|
|
168
|
+
process.once("SIGTERM", close);
|
|
169
|
+
await new Promise((resolveClose) => proxy.server.once("close", resolveClose));
|
|
170
|
+
return 0;
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
process.send?.({
|
|
174
|
+
ready: false,
|
|
175
|
+
error: error instanceof Error ? error.message : String(error),
|
|
176
|
+
});
|
|
177
|
+
return 1;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
export async function proxyIsHealthy(proxy) {
|
|
181
|
+
try {
|
|
182
|
+
const response = await fetch(`http://127.0.0.1:${proxy.port}/_cpac/health`, {
|
|
183
|
+
headers: { "x-cpac-proxy-id": proxy.id },
|
|
184
|
+
signal: AbortSignal.timeout(1_000),
|
|
185
|
+
});
|
|
186
|
+
return response.status === 204;
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
export async function startProxyProcess(config, apiKey) {
|
|
193
|
+
const id = randomBytes(24).toString("hex");
|
|
194
|
+
const child = spawn(process.execPath, [
|
|
195
|
+
fileURLToPath(new URL("./cpac.js", import.meta.url)),
|
|
196
|
+
"_proxy",
|
|
197
|
+
String(config.codex_proxy_port),
|
|
198
|
+
], {
|
|
199
|
+
detached: true,
|
|
200
|
+
env: {
|
|
201
|
+
...process.env,
|
|
202
|
+
CPAC_PROXY_API_KEY: apiKey,
|
|
203
|
+
CPAC_PROXY_ID: id,
|
|
204
|
+
CPAC_PROXY_UPSTREAM: config.cpa_url,
|
|
205
|
+
},
|
|
206
|
+
stdio: ["ignore", "ignore", "ignore", "ipc"],
|
|
207
|
+
});
|
|
208
|
+
return await new Promise((resolveStart, rejectStart) => {
|
|
209
|
+
let settled = false;
|
|
210
|
+
let timeout;
|
|
211
|
+
const finish = (error, value) => {
|
|
212
|
+
if (settled)
|
|
213
|
+
return;
|
|
214
|
+
settled = true;
|
|
215
|
+
clearTimeout(timeout);
|
|
216
|
+
child.removeAllListeners();
|
|
217
|
+
if (child.connected)
|
|
218
|
+
child.disconnect();
|
|
219
|
+
child.unref();
|
|
220
|
+
if (error)
|
|
221
|
+
rejectStart(error);
|
|
222
|
+
else
|
|
223
|
+
resolveStart(value);
|
|
224
|
+
};
|
|
225
|
+
timeout = setTimeout(() => {
|
|
226
|
+
child.kill("SIGKILL");
|
|
227
|
+
finish(new CPACError("loopback proxy did not become ready"));
|
|
228
|
+
}, 5_000);
|
|
229
|
+
child.once("error", (error) => finish(new CPACError(`cannot start loopback proxy: ${error.message}`)));
|
|
230
|
+
child.once("exit", (code) => finish(new CPACError(`loopback proxy exited before ready (${code ?? 1})`)));
|
|
231
|
+
child.once("message", (message) => {
|
|
232
|
+
if (objectValue(message) &&
|
|
233
|
+
message.ready === true &&
|
|
234
|
+
typeof message.pid === "number" &&
|
|
235
|
+
typeof message.port === "number") {
|
|
236
|
+
finish(undefined, { id, pid: message.pid, port: message.port });
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const detail = objectValue(message) && typeof message.error === "string"
|
|
240
|
+
? `: ${message.error}`
|
|
241
|
+
: "";
|
|
242
|
+
finish(new CPACError(`cannot start loopback proxy${detail}`));
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
function processCommandArgs(pid) {
|
|
247
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
248
|
+
return undefined;
|
|
249
|
+
try {
|
|
250
|
+
const proc = `/proc/${pid}/cmdline`;
|
|
251
|
+
if (existsSync(proc)) {
|
|
252
|
+
const raw = readFileSync(proc, "utf8");
|
|
253
|
+
return raw ? raw.split("\0").filter(Boolean) : undefined;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
return undefined;
|
|
258
|
+
}
|
|
259
|
+
try {
|
|
260
|
+
const result = spawnSync("ps", ["-p", String(pid), "-www", "-o", "args="], {
|
|
261
|
+
encoding: "utf8",
|
|
262
|
+
timeout: 1_000,
|
|
263
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
264
|
+
});
|
|
265
|
+
if (result.status !== 0)
|
|
266
|
+
return undefined;
|
|
267
|
+
const line = result.stdout.trim();
|
|
268
|
+
return line ? line.split(/\s+/) : undefined;
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
return undefined;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
function isCpacProxyProcess(pid) {
|
|
275
|
+
const args = processCommandArgs(pid);
|
|
276
|
+
if (!args)
|
|
277
|
+
return false;
|
|
278
|
+
return (args.includes("_proxy") &&
|
|
279
|
+
args.some((arg) => arg === "cpac.js" || arg.endsWith("/cpac.js") || arg.endsWith("\\cpac.js")));
|
|
280
|
+
}
|
|
281
|
+
function killCpacProxy(pid, signal) {
|
|
282
|
+
if (!isCpacProxyProcess(pid))
|
|
283
|
+
return false;
|
|
284
|
+
try {
|
|
285
|
+
process.kill(pid, signal);
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
export async function stopProxyProcess(proxy) {
|
|
293
|
+
if (await proxyIsHealthy(proxy)) {
|
|
294
|
+
try {
|
|
295
|
+
await fetch(`http://127.0.0.1:${proxy.port}/_cpac/shutdown`, {
|
|
296
|
+
method: "POST",
|
|
297
|
+
headers: { "x-cpac-proxy-id": proxy.id },
|
|
298
|
+
signal: AbortSignal.timeout(1_000),
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
// The server may close the connection while shutting down.
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (!killCpacProxy(proxy.pid, "SIGTERM"))
|
|
306
|
+
return;
|
|
307
|
+
await new Promise((resolveWait) => setTimeout(resolveWait, 200));
|
|
308
|
+
killCpacProxy(proxy.pid, "SIGKILL");
|
|
309
|
+
}
|
|
310
|
+
export async function runProxy(config) {
|
|
311
|
+
const apiKey = await resolveApiKey(config.api_key_env);
|
|
312
|
+
const state = readState(config.state_dir);
|
|
313
|
+
if (!state)
|
|
314
|
+
throw new CPACError("no active CPAC injection; run cpac inject first");
|
|
315
|
+
originalBytes(config.state_dir, state, config.codex_config);
|
|
316
|
+
const recorded = stateProxy(state);
|
|
317
|
+
if (!recorded) {
|
|
318
|
+
throw new CPACError("injection has no loopback proxy state; run cpac inject to migrate it");
|
|
319
|
+
}
|
|
320
|
+
if (await proxyIsHealthy(recorded)) {
|
|
321
|
+
throw new CPACError(`loopback proxy is already running on 127.0.0.1:${recorded.port}`);
|
|
322
|
+
}
|
|
323
|
+
const proxy = await createLoopbackProxy(config.cpa_url, apiKey, recorded.id, recorded.port);
|
|
324
|
+
try {
|
|
325
|
+
atomicWrite(join(config.state_dir, "state.json"), stateBytes(config, state.config_existed, state.config_mode, { id: recorded.id, pid: process.pid, port: proxy.port }, proxyFingerprint(config, apiKey)));
|
|
326
|
+
}
|
|
327
|
+
catch (error) {
|
|
328
|
+
proxy.server.closeAllConnections?.();
|
|
329
|
+
proxy.server.close();
|
|
330
|
+
throw error;
|
|
331
|
+
}
|
|
332
|
+
console.log(`CPAC loopback proxy listening on http://127.0.0.1:${proxy.port}/v1`);
|
|
333
|
+
const close = () => {
|
|
334
|
+
proxy.server.closeAllConnections?.();
|
|
335
|
+
proxy.server.close();
|
|
336
|
+
};
|
|
337
|
+
process.once("SIGINT", close);
|
|
338
|
+
process.once("SIGTERM", close);
|
|
339
|
+
await new Promise((resolveClose) => proxy.server.once("close", resolveClose));
|
|
340
|
+
return 0;
|
|
341
|
+
}
|
package/dist/util.js
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { chmodSync, closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join, parse } from "node:path";
|
|
4
|
+
import { createInterface } from "node:readline/promises";
|
|
5
|
+
import { Writable } from "node:stream";
|
|
6
|
+
let atomicSequence = 0;
|
|
7
|
+
export class CPACError extends Error {
|
|
8
|
+
}
|
|
9
|
+
export function objectValue(value) {
|
|
10
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
11
|
+
}
|
|
12
|
+
export function expandUserPath(value) {
|
|
13
|
+
if (value === "~")
|
|
14
|
+
return homedir();
|
|
15
|
+
if (value.startsWith("~/") || value.startsWith("~\\"))
|
|
16
|
+
return join(homedir(), value.slice(2));
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
export async function checkboxPicker(title, items, max) {
|
|
20
|
+
if (!process.stdin.isTTY || !process.stderr.isTTY) {
|
|
21
|
+
throw new CPACError("model selection requires an interactive terminal");
|
|
22
|
+
}
|
|
23
|
+
const stdin = process.stdin;
|
|
24
|
+
const out = process.stderr;
|
|
25
|
+
const selected = new Set();
|
|
26
|
+
let cursor = 0;
|
|
27
|
+
const lines = items.length + 1;
|
|
28
|
+
const render = () => {
|
|
29
|
+
out.write(`${title} [space: toggle, up/down: move, enter: confirm, q: cancel, max ${max}]\n`);
|
|
30
|
+
items.forEach((item, index) => {
|
|
31
|
+
const box = selected.has(index) ? "[x]" : "[ ]";
|
|
32
|
+
out.write(`${index === cursor ? ">" : " "} ${box} ${item}\n`);
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
const redraw = () => {
|
|
36
|
+
out.write(`\x1b[${lines}F`);
|
|
37
|
+
for (let line = 0; line < lines; line += 1)
|
|
38
|
+
out.write("\x1b[2K\x1b[1E");
|
|
39
|
+
out.write(`\x1b[${lines}F`);
|
|
40
|
+
render();
|
|
41
|
+
};
|
|
42
|
+
return await new Promise((resolvePromise, rejectPromise) => {
|
|
43
|
+
const finish = (error) => {
|
|
44
|
+
stdin.removeListener("data", onData);
|
|
45
|
+
try {
|
|
46
|
+
stdin.setRawMode(false);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// Terminal already gone; nothing to restore.
|
|
50
|
+
}
|
|
51
|
+
stdin.pause();
|
|
52
|
+
out.write("\n");
|
|
53
|
+
if (error)
|
|
54
|
+
rejectPromise(error);
|
|
55
|
+
else
|
|
56
|
+
resolvePromise([...selected].map((index) => items[index]));
|
|
57
|
+
};
|
|
58
|
+
const onData = (chunk) => {
|
|
59
|
+
const key = chunk.toString("utf8");
|
|
60
|
+
if (key === "\x03" || key === "q" || key === "\x1b") {
|
|
61
|
+
finish(new CPACError("model selection cancelled"));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (key === "\r" || key === "\n") {
|
|
65
|
+
if (max === 1 && selected.size === 0) {
|
|
66
|
+
selected.add(cursor);
|
|
67
|
+
}
|
|
68
|
+
finish();
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (key === "\x1b[A")
|
|
72
|
+
cursor = (cursor + items.length - 1) % items.length;
|
|
73
|
+
else if (key === "\x1b[B")
|
|
74
|
+
cursor = (cursor + 1) % items.length;
|
|
75
|
+
else if (key === " ") {
|
|
76
|
+
if (max === 1) {
|
|
77
|
+
selected.clear();
|
|
78
|
+
selected.add(cursor);
|
|
79
|
+
finish();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (selected.has(cursor))
|
|
83
|
+
selected.delete(cursor);
|
|
84
|
+
else if (selected.size < max)
|
|
85
|
+
selected.add(cursor);
|
|
86
|
+
}
|
|
87
|
+
redraw();
|
|
88
|
+
};
|
|
89
|
+
stdin.setRawMode(true);
|
|
90
|
+
stdin.resume();
|
|
91
|
+
stdin.on("data", onData);
|
|
92
|
+
render();
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
export function tomlString(value) {
|
|
96
|
+
return JSON.stringify(value);
|
|
97
|
+
}
|
|
98
|
+
export function tomlStringArray(values) {
|
|
99
|
+
return `[ ${values.map(tomlString).join(", ")} ]`;
|
|
100
|
+
}
|
|
101
|
+
export function dominantEol(content) {
|
|
102
|
+
const crlf = (content.match(/\r\n/g) ?? []).length;
|
|
103
|
+
if (crlf === 0)
|
|
104
|
+
return "\n";
|
|
105
|
+
const bareLf = (content.match(/\n/g) ?? []).length - crlf;
|
|
106
|
+
return crlf >= bareLf ? "\r\n" : "\n";
|
|
107
|
+
}
|
|
108
|
+
export function atomicWrite(path, data, mode = 0o600) {
|
|
109
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
110
|
+
const temporary = `${path}.cpac.${process.pid}.${Date.now()}.${++atomicSequence}.tmp`;
|
|
111
|
+
let descriptor;
|
|
112
|
+
try {
|
|
113
|
+
descriptor = openSync(temporary, "wx", 0o600);
|
|
114
|
+
writeFileSync(descriptor, data);
|
|
115
|
+
fsyncSync(descriptor);
|
|
116
|
+
closeSync(descriptor);
|
|
117
|
+
descriptor = undefined;
|
|
118
|
+
chmodSync(temporary, mode);
|
|
119
|
+
renameSync(temporary, path);
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
if (descriptor !== undefined) {
|
|
123
|
+
try {
|
|
124
|
+
closeSync(descriptor);
|
|
125
|
+
}
|
|
126
|
+
catch (closeError) {
|
|
127
|
+
void closeError;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
unlinkSync(temporary);
|
|
132
|
+
}
|
|
133
|
+
catch (unlinkError) {
|
|
134
|
+
void unlinkError;
|
|
135
|
+
}
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
export async function promptSecret(name) {
|
|
140
|
+
if (!process.stdin.isTTY || !process.stderr.isTTY) {
|
|
141
|
+
throw new CPACError(`environment variable ${name} is not set; run: export ${name}="..."`);
|
|
142
|
+
}
|
|
143
|
+
process.stderr.write(`Enter ${name}: `);
|
|
144
|
+
const silent = new Writable({
|
|
145
|
+
write(_chunk, _encoding, callback) {
|
|
146
|
+
callback();
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
const prompt = createInterface({
|
|
150
|
+
input: process.stdin,
|
|
151
|
+
output: silent,
|
|
152
|
+
terminal: true,
|
|
153
|
+
});
|
|
154
|
+
try {
|
|
155
|
+
const value = (await prompt.question("")).trim();
|
|
156
|
+
process.stderr.write("\n");
|
|
157
|
+
if (!value)
|
|
158
|
+
throw new CPACError(`${name} must not be empty`);
|
|
159
|
+
return value;
|
|
160
|
+
}
|
|
161
|
+
finally {
|
|
162
|
+
prompt.close();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
export async function resolveApiKey(name) {
|
|
166
|
+
return process.env[name]?.trim() || (await promptSecret(name));
|
|
167
|
+
}
|
|
168
|
+
export function shellProfile() {
|
|
169
|
+
const shell = parse(process.env.SHELL || "").base;
|
|
170
|
+
if (shell === "zsh")
|
|
171
|
+
return join(homedir(), ".zshrc");
|
|
172
|
+
if (shell === "bash")
|
|
173
|
+
return join(homedir(), process.platform === "darwin" ? ".bash_profile" : ".bashrc");
|
|
174
|
+
if (["sh", "dash", "ksh"].includes(shell))
|
|
175
|
+
return join(homedir(), ".profile");
|
|
176
|
+
throw new CPACError(`unsupported shell ${shell || "unknown"}; run: export CPA_API_KEY="..."`);
|
|
177
|
+
}
|
|
178
|
+
function shellQuote(value) {
|
|
179
|
+
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
180
|
+
}
|
|
181
|
+
export function saveApiKeyExport(profile, name, apiKey) {
|
|
182
|
+
const start = `# >>> CPAC ${name} >>>`;
|
|
183
|
+
const end = `# <<< CPAC ${name} <<<`;
|
|
184
|
+
const block = `${start}\nexport ${name}=${shellQuote(apiKey)}\n${end}`;
|
|
185
|
+
let content = existsSync(profile) ? readFileSync(profile, "utf8") : "";
|
|
186
|
+
const managed = new RegExp(`${start.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?${end.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`);
|
|
187
|
+
content = managed.test(content)
|
|
188
|
+
? content.replace(managed, block)
|
|
189
|
+
: `${content}${content && !content.endsWith("\n") ? "\n" : ""}${block}\n`;
|
|
190
|
+
const mode = existsSync(profile) ? statSync(profile).mode & 0o7777 : 0o600;
|
|
191
|
+
atomicWrite(profile, Buffer.from(content), mode);
|
|
192
|
+
}
|
package/package.json
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yhong91/cpac",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.26",
|
|
4
4
|
"description": "Connect Codex and Claude Code to a remote CLIProxyAPI gateway",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"cpac": "dist/cpac.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
|
-
"dist
|
|
11
|
-
"dist/pi-extension.template",
|
|
10
|
+
"dist",
|
|
12
11
|
"cpac.example.json",
|
|
13
12
|
"README.md"
|
|
14
13
|
],
|