claude-code-relay 0.0.1 → 0.0.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 +199 -0
- package/dist/cjs/cli-wrapper.js +186 -0
- package/dist/cjs/cli.js +400 -0
- package/dist/cjs/index.js +351 -0
- package/dist/cjs/server.js +347 -0
- package/dist/cjs/types.js +18 -0
- package/dist/cjs/utils.js +53 -0
- package/dist/esm/cli-wrapper.d.ts.map +1 -0
- package/dist/esm/cli-wrapper.js +161 -0
- package/dist/esm/cli.d.ts.map +1 -0
- package/dist/esm/cli.js +399 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +322 -0
- package/dist/esm/server.d.ts.map +1 -0
- package/dist/esm/server.js +321 -0
- package/dist/esm/types.d.ts.map +1 -0
- package/dist/esm/types.js +0 -0
- package/dist/esm/utils.d.ts.map +1 -0
- package/dist/esm/utils.js +27 -0
- package/package.json +17 -8
- package/dist/cli-wrapper.d.ts.map +0 -1
- package/dist/cli-wrapper.js +0 -149
- package/dist/cli-wrapper.js.map +0 -1
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -87
- package/dist/cli.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -6
- package/dist/index.js.map +0 -1
- package/dist/server.d.ts.map +0 -1
- package/dist/server.js +0 -168
- package/dist/server.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -5
- package/dist/types.js.map +0 -1
- package/dist/utils.d.ts.map +0 -1
- package/dist/utils.js +0 -34
- package/dist/utils.js.map +0 -1
- /package/dist/{cli-wrapper.d.ts → esm/cli-wrapper.d.ts} +0 -0
- /package/dist/{cli.d.ts → esm/cli.d.ts} +0 -0
- /package/dist/{index.d.ts → esm/index.d.ts} +0 -0
- /package/dist/{server.d.ts → esm/server.d.ts} +0 -0
- /package/dist/{types.d.ts → esm/types.d.ts} +0 -0
- /package/dist/{utils.d.ts → esm/utils.d.ts} +0 -0
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
ClaudeCLI: () => ClaudeCLI,
|
|
24
|
+
createApp: () => createApp,
|
|
25
|
+
runServer: () => runServer
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(index_exports);
|
|
28
|
+
|
|
29
|
+
// src/server.ts
|
|
30
|
+
var import_node_http = require("node:http");
|
|
31
|
+
|
|
32
|
+
// src/cli-wrapper.ts
|
|
33
|
+
var import_node_child_process2 = require("node:child_process");
|
|
34
|
+
|
|
35
|
+
// src/utils.ts
|
|
36
|
+
var import_node_child_process = require("node:child_process");
|
|
37
|
+
var import_node_fs = require("node:fs");
|
|
38
|
+
var import_node_os = require("node:os");
|
|
39
|
+
function which(command) {
|
|
40
|
+
if (command.startsWith("/") || command.startsWith("~")) {
|
|
41
|
+
return (0, import_node_fs.existsSync)(command) ? command : null;
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
const cmd = (0, import_node_os.platform)() === "win32" ? "where" : "which";
|
|
45
|
+
const result = (0, import_node_child_process.execSync)(`${cmd} ${command}`, {
|
|
46
|
+
encoding: "utf-8",
|
|
47
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
48
|
+
});
|
|
49
|
+
return result.trim().split("\n")[0] ?? null;
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function generateId(prefix = "") {
|
|
55
|
+
const random = Math.random().toString(36).substring(2, 14);
|
|
56
|
+
return prefix ? `${prefix}-${random}` : random;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/cli-wrapper.ts
|
|
60
|
+
var MODEL_MAP = {
|
|
61
|
+
sonnet: "sonnet",
|
|
62
|
+
opus: "opus",
|
|
63
|
+
haiku: "haiku",
|
|
64
|
+
"claude-3-sonnet": "sonnet",
|
|
65
|
+
"claude-3-opus": "opus",
|
|
66
|
+
"claude-3-haiku": "haiku",
|
|
67
|
+
"claude-sonnet-4": "sonnet",
|
|
68
|
+
"claude-opus-4": "opus"
|
|
69
|
+
};
|
|
70
|
+
var ClaudeCLI = class {
|
|
71
|
+
config;
|
|
72
|
+
constructor(config) {
|
|
73
|
+
this.config = {
|
|
74
|
+
cliPath: config?.cliPath ?? process.env.CLAUDE_CLI_PATH ?? "claude",
|
|
75
|
+
timeout: config?.timeout ?? parseInt(process.env.CLAUDE_CODE_RELAY_TIMEOUT ?? "300", 10),
|
|
76
|
+
verbose: config?.verbose ?? process.env.CLAUDE_CODE_RELAY_VERBOSE === "1"
|
|
77
|
+
};
|
|
78
|
+
this.validateCLI();
|
|
79
|
+
}
|
|
80
|
+
validateCLI() {
|
|
81
|
+
const cliPath = which(this.config.cliPath);
|
|
82
|
+
if (!cliPath) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
`Claude CLI not found at '${this.config.cliPath}'. Please install it or set CLAUDE_CLI_PATH.`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
if (this.config.verbose) {
|
|
88
|
+
console.log(`Using Claude CLI at: ${cliPath}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
normalizeModel(model) {
|
|
92
|
+
return MODEL_MAP[model.toLowerCase()] ?? "sonnet";
|
|
93
|
+
}
|
|
94
|
+
buildPrompt(messages, systemPrompt) {
|
|
95
|
+
const parts = [];
|
|
96
|
+
for (const msg of messages) {
|
|
97
|
+
if (msg.role === "system") {
|
|
98
|
+
systemPrompt = msg.content;
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (systemPrompt) {
|
|
103
|
+
parts.push(`System: ${systemPrompt}
|
|
104
|
+
`);
|
|
105
|
+
}
|
|
106
|
+
for (const msg of messages) {
|
|
107
|
+
if (msg.role === "system") continue;
|
|
108
|
+
if (msg.role === "user") {
|
|
109
|
+
parts.push(`Human: ${msg.content}
|
|
110
|
+
`);
|
|
111
|
+
} else if (msg.role === "assistant") {
|
|
112
|
+
parts.push(`Assistant: ${msg.content}
|
|
113
|
+
`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
parts.push("Assistant:");
|
|
117
|
+
return parts.join("\n");
|
|
118
|
+
}
|
|
119
|
+
async complete(messages, model = "sonnet", systemPrompt) {
|
|
120
|
+
const prompt = this.buildPrompt(messages, systemPrompt);
|
|
121
|
+
const normalizedModel = this.normalizeModel(model);
|
|
122
|
+
return new Promise((resolve, reject) => {
|
|
123
|
+
const args = ["-p", "--model", normalizedModel, "--output-format", "text"];
|
|
124
|
+
if (this.config.verbose) {
|
|
125
|
+
console.log(`Running: ${this.config.cliPath} ${args.join(" ")}`);
|
|
126
|
+
}
|
|
127
|
+
const proc = (0, import_node_child_process2.spawn)(this.config.cliPath, args, {
|
|
128
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
129
|
+
});
|
|
130
|
+
let stdout = "";
|
|
131
|
+
let stderr = "";
|
|
132
|
+
proc.stdout.on("data", (data) => {
|
|
133
|
+
stdout += data.toString();
|
|
134
|
+
});
|
|
135
|
+
proc.stderr.on("data", (data) => {
|
|
136
|
+
stderr += data.toString();
|
|
137
|
+
});
|
|
138
|
+
proc.on("close", (code) => {
|
|
139
|
+
if (code !== 0) {
|
|
140
|
+
reject(new Error(`Claude CLI failed: ${stderr}`));
|
|
141
|
+
} else {
|
|
142
|
+
resolve(stdout.trim());
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
proc.on("error", (err) => {
|
|
146
|
+
reject(err);
|
|
147
|
+
});
|
|
148
|
+
proc.stdin.write(prompt);
|
|
149
|
+
proc.stdin.end();
|
|
150
|
+
setTimeout(() => {
|
|
151
|
+
proc.kill();
|
|
152
|
+
reject(new Error(`Claude CLI timeout after ${this.config.timeout}s`));
|
|
153
|
+
}, this.config.timeout * 1e3);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
async *stream(messages, model = "sonnet", systemPrompt) {
|
|
157
|
+
const prompt = this.buildPrompt(messages, systemPrompt);
|
|
158
|
+
const normalizedModel = this.normalizeModel(model);
|
|
159
|
+
const args = ["-p", "--model", normalizedModel, "--output-format", "stream-json"];
|
|
160
|
+
if (this.config.verbose) {
|
|
161
|
+
console.log(`Running: ${this.config.cliPath} ${args.join(" ")}`);
|
|
162
|
+
}
|
|
163
|
+
const proc = (0, import_node_child_process2.spawn)(this.config.cliPath, args, {
|
|
164
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
165
|
+
});
|
|
166
|
+
proc.stdin.write(prompt);
|
|
167
|
+
proc.stdin.end();
|
|
168
|
+
let buffer = "";
|
|
169
|
+
for await (const chunk of proc.stdout) {
|
|
170
|
+
buffer += chunk.toString();
|
|
171
|
+
while (buffer.includes("\n")) {
|
|
172
|
+
const [line, rest] = buffer.split("\n", 2);
|
|
173
|
+
buffer = rest ?? "";
|
|
174
|
+
const trimmed = line.trim();
|
|
175
|
+
if (!trimmed) continue;
|
|
176
|
+
try {
|
|
177
|
+
const data = JSON.parse(trimmed);
|
|
178
|
+
if (data.content) {
|
|
179
|
+
yield data.content;
|
|
180
|
+
} else if (data.text) {
|
|
181
|
+
yield data.text;
|
|
182
|
+
} else if (data.delta?.text) {
|
|
183
|
+
yield data.delta.text;
|
|
184
|
+
}
|
|
185
|
+
} catch {
|
|
186
|
+
if (!trimmed.startsWith("{")) {
|
|
187
|
+
yield trimmed;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
// src/server.ts
|
|
196
|
+
var _cli = null;
|
|
197
|
+
function sendJson(res, data, status = 200) {
|
|
198
|
+
const body = JSON.stringify(data);
|
|
199
|
+
res.writeHead(status, {
|
|
200
|
+
"Content-Type": "application/json",
|
|
201
|
+
"Content-Length": Buffer.byteLength(body),
|
|
202
|
+
"Access-Control-Allow-Origin": "*"
|
|
203
|
+
});
|
|
204
|
+
res.end(body);
|
|
205
|
+
}
|
|
206
|
+
function sendError(res, message, status = 500) {
|
|
207
|
+
sendJson(res, { error: { message, type: "server_error" } }, status);
|
|
208
|
+
}
|
|
209
|
+
function sendSSEChunk(res, data) {
|
|
210
|
+
res.write(`data: ${data}
|
|
211
|
+
|
|
212
|
+
`);
|
|
213
|
+
}
|
|
214
|
+
async function readBody(req) {
|
|
215
|
+
return new Promise((resolve, reject) => {
|
|
216
|
+
let body = "";
|
|
217
|
+
req.on("data", (chunk) => body += chunk);
|
|
218
|
+
req.on("end", () => resolve(body));
|
|
219
|
+
req.on("error", reject);
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
async function handleRequest(req, res) {
|
|
223
|
+
const url = req.url ?? "/";
|
|
224
|
+
const method = req.method ?? "GET";
|
|
225
|
+
if (method === "OPTIONS") {
|
|
226
|
+
res.writeHead(204, {
|
|
227
|
+
"Access-Control-Allow-Origin": "*",
|
|
228
|
+
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
229
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization"
|
|
230
|
+
});
|
|
231
|
+
res.end();
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (method === "GET" && url === "/health") {
|
|
235
|
+
sendJson(res, { status: _cli ? "ok" : "degraded", cli_available: _cli !== null });
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (method === "GET" && url === "/v1/models") {
|
|
239
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
240
|
+
const models = {
|
|
241
|
+
object: "list",
|
|
242
|
+
data: [
|
|
243
|
+
{ id: "sonnet", object: "model", created: now, owned_by: "anthropic" },
|
|
244
|
+
{ id: "opus", object: "model", created: now, owned_by: "anthropic" },
|
|
245
|
+
{ id: "haiku", object: "model", created: now, owned_by: "anthropic" }
|
|
246
|
+
]
|
|
247
|
+
};
|
|
248
|
+
sendJson(res, models);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (method === "POST" && url === "/v1/chat/completions") {
|
|
252
|
+
if (!_cli) {
|
|
253
|
+
sendError(res, "Claude CLI not available", 503);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
let request;
|
|
257
|
+
try {
|
|
258
|
+
const body = await readBody(req);
|
|
259
|
+
request = JSON.parse(body);
|
|
260
|
+
} catch {
|
|
261
|
+
sendError(res, "Invalid JSON", 400);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const chatId = generateId("chatcmpl");
|
|
265
|
+
const created = Math.floor(Date.now() / 1e3);
|
|
266
|
+
if (request.stream) {
|
|
267
|
+
res.writeHead(200, {
|
|
268
|
+
"Content-Type": "text/event-stream",
|
|
269
|
+
"Cache-Control": "no-cache",
|
|
270
|
+
Connection: "keep-alive",
|
|
271
|
+
"Access-Control-Allow-Origin": "*",
|
|
272
|
+
"X-Accel-Buffering": "no"
|
|
273
|
+
});
|
|
274
|
+
const initial = {
|
|
275
|
+
id: chatId,
|
|
276
|
+
object: "chat.completion.chunk",
|
|
277
|
+
created,
|
|
278
|
+
model: request.model,
|
|
279
|
+
choices: [{ index: 0, delta: { role: "assistant", content: "" }, finish_reason: null }]
|
|
280
|
+
};
|
|
281
|
+
sendSSEChunk(res, JSON.stringify(initial));
|
|
282
|
+
try {
|
|
283
|
+
for await (const text of _cli.stream(request.messages, request.model)) {
|
|
284
|
+
const chunk = {
|
|
285
|
+
id: chatId,
|
|
286
|
+
object: "chat.completion.chunk",
|
|
287
|
+
created,
|
|
288
|
+
model: request.model,
|
|
289
|
+
choices: [{ index: 0, delta: { content: text }, finish_reason: null }]
|
|
290
|
+
};
|
|
291
|
+
sendSSEChunk(res, JSON.stringify(chunk));
|
|
292
|
+
}
|
|
293
|
+
} catch (err) {
|
|
294
|
+
sendSSEChunk(res, JSON.stringify({ error: { message: String(err), type: "server_error" } }));
|
|
295
|
+
}
|
|
296
|
+
const final = {
|
|
297
|
+
id: chatId,
|
|
298
|
+
object: "chat.completion.chunk",
|
|
299
|
+
created,
|
|
300
|
+
model: request.model,
|
|
301
|
+
choices: [{ index: 0, delta: {}, finish_reason: "stop" }]
|
|
302
|
+
};
|
|
303
|
+
sendSSEChunk(res, JSON.stringify(final));
|
|
304
|
+
sendSSEChunk(res, "[DONE]");
|
|
305
|
+
res.end();
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
try {
|
|
309
|
+
const content = await _cli.complete(request.messages, request.model);
|
|
310
|
+
const response = {
|
|
311
|
+
id: chatId,
|
|
312
|
+
object: "chat.completion",
|
|
313
|
+
created,
|
|
314
|
+
model: request.model,
|
|
315
|
+
choices: [{ index: 0, message: { role: "assistant", content }, finish_reason: "stop" }],
|
|
316
|
+
usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
|
|
317
|
+
};
|
|
318
|
+
sendJson(res, response);
|
|
319
|
+
} catch (err) {
|
|
320
|
+
sendError(res, String(err));
|
|
321
|
+
}
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
sendError(res, "Not found", 404);
|
|
325
|
+
}
|
|
326
|
+
function createApp(config) {
|
|
327
|
+
try {
|
|
328
|
+
_cli = new ClaudeCLI(config?.cli);
|
|
329
|
+
} catch (err) {
|
|
330
|
+
console.error(`Failed to initialize Claude CLI: ${err}`);
|
|
331
|
+
_cli = null;
|
|
332
|
+
}
|
|
333
|
+
return (0, import_node_http.createServer)((req, res) => {
|
|
334
|
+
handleRequest(req, res).catch((err) => {
|
|
335
|
+
console.error("Request error:", err);
|
|
336
|
+
sendError(res, "Internal server error");
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
function runServer(host = "127.0.0.1", port = 52014, config) {
|
|
341
|
+
const server = createApp(config);
|
|
342
|
+
server.listen(port, host, () => {
|
|
343
|
+
console.log(`Server running at http://${host}:${port}`);
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
347
|
+
0 && (module.exports = {
|
|
348
|
+
ClaudeCLI,
|
|
349
|
+
createApp,
|
|
350
|
+
runServer
|
|
351
|
+
});
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/server.ts
|
|
21
|
+
var server_exports = {};
|
|
22
|
+
__export(server_exports, {
|
|
23
|
+
createApp: () => createApp,
|
|
24
|
+
runServer: () => runServer
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(server_exports);
|
|
27
|
+
var import_node_http = require("node:http");
|
|
28
|
+
|
|
29
|
+
// src/cli-wrapper.ts
|
|
30
|
+
var import_node_child_process2 = require("node:child_process");
|
|
31
|
+
|
|
32
|
+
// src/utils.ts
|
|
33
|
+
var import_node_child_process = require("node:child_process");
|
|
34
|
+
var import_node_fs = require("node:fs");
|
|
35
|
+
var import_node_os = require("node:os");
|
|
36
|
+
function which(command) {
|
|
37
|
+
if (command.startsWith("/") || command.startsWith("~")) {
|
|
38
|
+
return (0, import_node_fs.existsSync)(command) ? command : null;
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
const cmd = (0, import_node_os.platform)() === "win32" ? "where" : "which";
|
|
42
|
+
const result = (0, import_node_child_process.execSync)(`${cmd} ${command}`, {
|
|
43
|
+
encoding: "utf-8",
|
|
44
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
45
|
+
});
|
|
46
|
+
return result.trim().split("\n")[0] ?? null;
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function generateId(prefix = "") {
|
|
52
|
+
const random = Math.random().toString(36).substring(2, 14);
|
|
53
|
+
return prefix ? `${prefix}-${random}` : random;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// src/cli-wrapper.ts
|
|
57
|
+
var MODEL_MAP = {
|
|
58
|
+
sonnet: "sonnet",
|
|
59
|
+
opus: "opus",
|
|
60
|
+
haiku: "haiku",
|
|
61
|
+
"claude-3-sonnet": "sonnet",
|
|
62
|
+
"claude-3-opus": "opus",
|
|
63
|
+
"claude-3-haiku": "haiku",
|
|
64
|
+
"claude-sonnet-4": "sonnet",
|
|
65
|
+
"claude-opus-4": "opus"
|
|
66
|
+
};
|
|
67
|
+
var ClaudeCLI = class {
|
|
68
|
+
config;
|
|
69
|
+
constructor(config) {
|
|
70
|
+
this.config = {
|
|
71
|
+
cliPath: config?.cliPath ?? process.env.CLAUDE_CLI_PATH ?? "claude",
|
|
72
|
+
timeout: config?.timeout ?? parseInt(process.env.CLAUDE_CODE_RELAY_TIMEOUT ?? "300", 10),
|
|
73
|
+
verbose: config?.verbose ?? process.env.CLAUDE_CODE_RELAY_VERBOSE === "1"
|
|
74
|
+
};
|
|
75
|
+
this.validateCLI();
|
|
76
|
+
}
|
|
77
|
+
validateCLI() {
|
|
78
|
+
const cliPath = which(this.config.cliPath);
|
|
79
|
+
if (!cliPath) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`Claude CLI not found at '${this.config.cliPath}'. Please install it or set CLAUDE_CLI_PATH.`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
if (this.config.verbose) {
|
|
85
|
+
console.log(`Using Claude CLI at: ${cliPath}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
normalizeModel(model) {
|
|
89
|
+
return MODEL_MAP[model.toLowerCase()] ?? "sonnet";
|
|
90
|
+
}
|
|
91
|
+
buildPrompt(messages, systemPrompt) {
|
|
92
|
+
const parts = [];
|
|
93
|
+
for (const msg of messages) {
|
|
94
|
+
if (msg.role === "system") {
|
|
95
|
+
systemPrompt = msg.content;
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (systemPrompt) {
|
|
100
|
+
parts.push(`System: ${systemPrompt}
|
|
101
|
+
`);
|
|
102
|
+
}
|
|
103
|
+
for (const msg of messages) {
|
|
104
|
+
if (msg.role === "system") continue;
|
|
105
|
+
if (msg.role === "user") {
|
|
106
|
+
parts.push(`Human: ${msg.content}
|
|
107
|
+
`);
|
|
108
|
+
} else if (msg.role === "assistant") {
|
|
109
|
+
parts.push(`Assistant: ${msg.content}
|
|
110
|
+
`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
parts.push("Assistant:");
|
|
114
|
+
return parts.join("\n");
|
|
115
|
+
}
|
|
116
|
+
async complete(messages, model = "sonnet", systemPrompt) {
|
|
117
|
+
const prompt = this.buildPrompt(messages, systemPrompt);
|
|
118
|
+
const normalizedModel = this.normalizeModel(model);
|
|
119
|
+
return new Promise((resolve, reject) => {
|
|
120
|
+
const args = ["-p", "--model", normalizedModel, "--output-format", "text"];
|
|
121
|
+
if (this.config.verbose) {
|
|
122
|
+
console.log(`Running: ${this.config.cliPath} ${args.join(" ")}`);
|
|
123
|
+
}
|
|
124
|
+
const proc = (0, import_node_child_process2.spawn)(this.config.cliPath, args, {
|
|
125
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
126
|
+
});
|
|
127
|
+
let stdout = "";
|
|
128
|
+
let stderr = "";
|
|
129
|
+
proc.stdout.on("data", (data) => {
|
|
130
|
+
stdout += data.toString();
|
|
131
|
+
});
|
|
132
|
+
proc.stderr.on("data", (data) => {
|
|
133
|
+
stderr += data.toString();
|
|
134
|
+
});
|
|
135
|
+
proc.on("close", (code) => {
|
|
136
|
+
if (code !== 0) {
|
|
137
|
+
reject(new Error(`Claude CLI failed: ${stderr}`));
|
|
138
|
+
} else {
|
|
139
|
+
resolve(stdout.trim());
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
proc.on("error", (err) => {
|
|
143
|
+
reject(err);
|
|
144
|
+
});
|
|
145
|
+
proc.stdin.write(prompt);
|
|
146
|
+
proc.stdin.end();
|
|
147
|
+
setTimeout(() => {
|
|
148
|
+
proc.kill();
|
|
149
|
+
reject(new Error(`Claude CLI timeout after ${this.config.timeout}s`));
|
|
150
|
+
}, this.config.timeout * 1e3);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
async *stream(messages, model = "sonnet", systemPrompt) {
|
|
154
|
+
const prompt = this.buildPrompt(messages, systemPrompt);
|
|
155
|
+
const normalizedModel = this.normalizeModel(model);
|
|
156
|
+
const args = ["-p", "--model", normalizedModel, "--output-format", "stream-json"];
|
|
157
|
+
if (this.config.verbose) {
|
|
158
|
+
console.log(`Running: ${this.config.cliPath} ${args.join(" ")}`);
|
|
159
|
+
}
|
|
160
|
+
const proc = (0, import_node_child_process2.spawn)(this.config.cliPath, args, {
|
|
161
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
162
|
+
});
|
|
163
|
+
proc.stdin.write(prompt);
|
|
164
|
+
proc.stdin.end();
|
|
165
|
+
let buffer = "";
|
|
166
|
+
for await (const chunk of proc.stdout) {
|
|
167
|
+
buffer += chunk.toString();
|
|
168
|
+
while (buffer.includes("\n")) {
|
|
169
|
+
const [line, rest] = buffer.split("\n", 2);
|
|
170
|
+
buffer = rest ?? "";
|
|
171
|
+
const trimmed = line.trim();
|
|
172
|
+
if (!trimmed) continue;
|
|
173
|
+
try {
|
|
174
|
+
const data = JSON.parse(trimmed);
|
|
175
|
+
if (data.content) {
|
|
176
|
+
yield data.content;
|
|
177
|
+
} else if (data.text) {
|
|
178
|
+
yield data.text;
|
|
179
|
+
} else if (data.delta?.text) {
|
|
180
|
+
yield data.delta.text;
|
|
181
|
+
}
|
|
182
|
+
} catch {
|
|
183
|
+
if (!trimmed.startsWith("{")) {
|
|
184
|
+
yield trimmed;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// src/server.ts
|
|
193
|
+
var _cli = null;
|
|
194
|
+
function sendJson(res, data, status = 200) {
|
|
195
|
+
const body = JSON.stringify(data);
|
|
196
|
+
res.writeHead(status, {
|
|
197
|
+
"Content-Type": "application/json",
|
|
198
|
+
"Content-Length": Buffer.byteLength(body),
|
|
199
|
+
"Access-Control-Allow-Origin": "*"
|
|
200
|
+
});
|
|
201
|
+
res.end(body);
|
|
202
|
+
}
|
|
203
|
+
function sendError(res, message, status = 500) {
|
|
204
|
+
sendJson(res, { error: { message, type: "server_error" } }, status);
|
|
205
|
+
}
|
|
206
|
+
function sendSSEChunk(res, data) {
|
|
207
|
+
res.write(`data: ${data}
|
|
208
|
+
|
|
209
|
+
`);
|
|
210
|
+
}
|
|
211
|
+
async function readBody(req) {
|
|
212
|
+
return new Promise((resolve, reject) => {
|
|
213
|
+
let body = "";
|
|
214
|
+
req.on("data", (chunk) => body += chunk);
|
|
215
|
+
req.on("end", () => resolve(body));
|
|
216
|
+
req.on("error", reject);
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
async function handleRequest(req, res) {
|
|
220
|
+
const url = req.url ?? "/";
|
|
221
|
+
const method = req.method ?? "GET";
|
|
222
|
+
if (method === "OPTIONS") {
|
|
223
|
+
res.writeHead(204, {
|
|
224
|
+
"Access-Control-Allow-Origin": "*",
|
|
225
|
+
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
226
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization"
|
|
227
|
+
});
|
|
228
|
+
res.end();
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (method === "GET" && url === "/health") {
|
|
232
|
+
sendJson(res, { status: _cli ? "ok" : "degraded", cli_available: _cli !== null });
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (method === "GET" && url === "/v1/models") {
|
|
236
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
237
|
+
const models = {
|
|
238
|
+
object: "list",
|
|
239
|
+
data: [
|
|
240
|
+
{ id: "sonnet", object: "model", created: now, owned_by: "anthropic" },
|
|
241
|
+
{ id: "opus", object: "model", created: now, owned_by: "anthropic" },
|
|
242
|
+
{ id: "haiku", object: "model", created: now, owned_by: "anthropic" }
|
|
243
|
+
]
|
|
244
|
+
};
|
|
245
|
+
sendJson(res, models);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (method === "POST" && url === "/v1/chat/completions") {
|
|
249
|
+
if (!_cli) {
|
|
250
|
+
sendError(res, "Claude CLI not available", 503);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
let request;
|
|
254
|
+
try {
|
|
255
|
+
const body = await readBody(req);
|
|
256
|
+
request = JSON.parse(body);
|
|
257
|
+
} catch {
|
|
258
|
+
sendError(res, "Invalid JSON", 400);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
const chatId = generateId("chatcmpl");
|
|
262
|
+
const created = Math.floor(Date.now() / 1e3);
|
|
263
|
+
if (request.stream) {
|
|
264
|
+
res.writeHead(200, {
|
|
265
|
+
"Content-Type": "text/event-stream",
|
|
266
|
+
"Cache-Control": "no-cache",
|
|
267
|
+
Connection: "keep-alive",
|
|
268
|
+
"Access-Control-Allow-Origin": "*",
|
|
269
|
+
"X-Accel-Buffering": "no"
|
|
270
|
+
});
|
|
271
|
+
const initial = {
|
|
272
|
+
id: chatId,
|
|
273
|
+
object: "chat.completion.chunk",
|
|
274
|
+
created,
|
|
275
|
+
model: request.model,
|
|
276
|
+
choices: [{ index: 0, delta: { role: "assistant", content: "" }, finish_reason: null }]
|
|
277
|
+
};
|
|
278
|
+
sendSSEChunk(res, JSON.stringify(initial));
|
|
279
|
+
try {
|
|
280
|
+
for await (const text of _cli.stream(request.messages, request.model)) {
|
|
281
|
+
const chunk = {
|
|
282
|
+
id: chatId,
|
|
283
|
+
object: "chat.completion.chunk",
|
|
284
|
+
created,
|
|
285
|
+
model: request.model,
|
|
286
|
+
choices: [{ index: 0, delta: { content: text }, finish_reason: null }]
|
|
287
|
+
};
|
|
288
|
+
sendSSEChunk(res, JSON.stringify(chunk));
|
|
289
|
+
}
|
|
290
|
+
} catch (err) {
|
|
291
|
+
sendSSEChunk(res, JSON.stringify({ error: { message: String(err), type: "server_error" } }));
|
|
292
|
+
}
|
|
293
|
+
const final = {
|
|
294
|
+
id: chatId,
|
|
295
|
+
object: "chat.completion.chunk",
|
|
296
|
+
created,
|
|
297
|
+
model: request.model,
|
|
298
|
+
choices: [{ index: 0, delta: {}, finish_reason: "stop" }]
|
|
299
|
+
};
|
|
300
|
+
sendSSEChunk(res, JSON.stringify(final));
|
|
301
|
+
sendSSEChunk(res, "[DONE]");
|
|
302
|
+
res.end();
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
try {
|
|
306
|
+
const content = await _cli.complete(request.messages, request.model);
|
|
307
|
+
const response = {
|
|
308
|
+
id: chatId,
|
|
309
|
+
object: "chat.completion",
|
|
310
|
+
created,
|
|
311
|
+
model: request.model,
|
|
312
|
+
choices: [{ index: 0, message: { role: "assistant", content }, finish_reason: "stop" }],
|
|
313
|
+
usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
|
|
314
|
+
};
|
|
315
|
+
sendJson(res, response);
|
|
316
|
+
} catch (err) {
|
|
317
|
+
sendError(res, String(err));
|
|
318
|
+
}
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
sendError(res, "Not found", 404);
|
|
322
|
+
}
|
|
323
|
+
function createApp(config) {
|
|
324
|
+
try {
|
|
325
|
+
_cli = new ClaudeCLI(config?.cli);
|
|
326
|
+
} catch (err) {
|
|
327
|
+
console.error(`Failed to initialize Claude CLI: ${err}`);
|
|
328
|
+
_cli = null;
|
|
329
|
+
}
|
|
330
|
+
return (0, import_node_http.createServer)((req, res) => {
|
|
331
|
+
handleRequest(req, res).catch((err) => {
|
|
332
|
+
console.error("Request error:", err);
|
|
333
|
+
sendError(res, "Internal server error");
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
function runServer(host = "127.0.0.1", port = 52014, config) {
|
|
338
|
+
const server = createApp(config);
|
|
339
|
+
server.listen(port, host, () => {
|
|
340
|
+
console.log(`Server running at http://${host}:${port}`);
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
344
|
+
0 && (module.exports = {
|
|
345
|
+
createApp,
|
|
346
|
+
runServer
|
|
347
|
+
});
|