bridgerapi 1.0.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/dist/cli.js +701 -0
- package/package.json +43 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,701 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
25
|
+
|
|
26
|
+
// src/server.ts
|
|
27
|
+
var import_http = require("http");
|
|
28
|
+
var import_crypto = require("crypto");
|
|
29
|
+
|
|
30
|
+
// src/messages.ts
|
|
31
|
+
function extractText(content) {
|
|
32
|
+
if (typeof content === "string") return content;
|
|
33
|
+
if (Array.isArray(content)) {
|
|
34
|
+
return content.filter((p) => p.type === "text").map((p) => p.text ?? "").join(" ");
|
|
35
|
+
}
|
|
36
|
+
return String(content ?? "");
|
|
37
|
+
}
|
|
38
|
+
function messagesToPrompt(messages) {
|
|
39
|
+
const system = [];
|
|
40
|
+
const turns = [];
|
|
41
|
+
for (const m of messages) {
|
|
42
|
+
const text = extractText(m.content).trim();
|
|
43
|
+
if (!text) continue;
|
|
44
|
+
if (m.role === "system") {
|
|
45
|
+
system.push(text);
|
|
46
|
+
} else if (m.role === "assistant") {
|
|
47
|
+
turns.push(`Assistant: ${text}`);
|
|
48
|
+
} else {
|
|
49
|
+
turns.push(`User: ${text}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const parts = [];
|
|
53
|
+
if (system.length) parts.push("System instructions:\n" + system.join("\n"));
|
|
54
|
+
if (turns.length) parts.push(turns.join("\n"));
|
|
55
|
+
return parts.join("\n\n");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// src/backends.ts
|
|
59
|
+
var import_child_process = require("child_process");
|
|
60
|
+
var import_fs = require("fs");
|
|
61
|
+
var import_os = require("os");
|
|
62
|
+
var HOME = (0, import_os.homedir)();
|
|
63
|
+
function which(cmd2) {
|
|
64
|
+
try {
|
|
65
|
+
return (0, import_child_process.execFileSync)("which", [cmd2], { encoding: "utf8" }).trim();
|
|
66
|
+
} catch {
|
|
67
|
+
return "";
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async function* spawnStream(cmd2, args, stdin, env) {
|
|
71
|
+
const proc = (0, import_child_process.spawn)(cmd2, args, {
|
|
72
|
+
env: env ?? process.env,
|
|
73
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
74
|
+
});
|
|
75
|
+
if (stdin) {
|
|
76
|
+
proc.stdin.end(stdin);
|
|
77
|
+
}
|
|
78
|
+
for await (const chunk2 of proc.stdout) {
|
|
79
|
+
yield chunk2;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
var ClaudeBackend = class {
|
|
83
|
+
constructor() {
|
|
84
|
+
this.name = "claude";
|
|
85
|
+
this.models = ["claude-opus-4-6", "claude-sonnet-4-6", "claude-haiku-4-5"];
|
|
86
|
+
this.prefixes = ["claude"];
|
|
87
|
+
}
|
|
88
|
+
get bin() {
|
|
89
|
+
return process.env.CLAUDE_BIN ?? `${HOME}/.local/bin/claude`;
|
|
90
|
+
}
|
|
91
|
+
available() {
|
|
92
|
+
return (0, import_fs.existsSync)(this.bin) || Boolean(which("claude"));
|
|
93
|
+
}
|
|
94
|
+
async runBlocking(prompt, model) {
|
|
95
|
+
const bin = which("claude") || this.bin;
|
|
96
|
+
let out;
|
|
97
|
+
try {
|
|
98
|
+
out = (0, import_child_process.execFileSync)(bin, ["-p", "--output-format", "json", "--model", model], {
|
|
99
|
+
input: prompt,
|
|
100
|
+
encoding: "utf8",
|
|
101
|
+
timeout: 3e5
|
|
102
|
+
});
|
|
103
|
+
} catch (e) {
|
|
104
|
+
throw new Error(e.stderr?.trim() || `claude exited non-zero`);
|
|
105
|
+
}
|
|
106
|
+
const data = JSON.parse(out.trim() || "{}");
|
|
107
|
+
return [data.result ?? "", data.usage ?? null];
|
|
108
|
+
}
|
|
109
|
+
async *stream(prompt, model) {
|
|
110
|
+
const bin = which("claude") || this.bin;
|
|
111
|
+
yield* spawnStream(bin, ["-p", "--output-format", "text", "--model", model], prompt);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
var GeminiBackend = class {
|
|
115
|
+
constructor() {
|
|
116
|
+
this.name = "gemini";
|
|
117
|
+
this.models = ["gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.0-flash", "gemini-1.5-pro"];
|
|
118
|
+
this.prefixes = ["gemini"];
|
|
119
|
+
}
|
|
120
|
+
get bin() {
|
|
121
|
+
return process.env.GEMINI_BIN ?? which("gemini") ?? "/opt/homebrew/bin/gemini";
|
|
122
|
+
}
|
|
123
|
+
available() {
|
|
124
|
+
return Boolean(which("gemini")) || (0, import_fs.existsSync)(this.bin);
|
|
125
|
+
}
|
|
126
|
+
async runBlocking(prompt, model) {
|
|
127
|
+
const bin = which("gemini") || this.bin;
|
|
128
|
+
let out;
|
|
129
|
+
try {
|
|
130
|
+
out = (0, import_child_process.execFileSync)(
|
|
131
|
+
bin,
|
|
132
|
+
["--output-format", "json", "--model", model, "--approval-mode", "yolo"],
|
|
133
|
+
{ input: prompt, encoding: "utf8", timeout: 3e5, env: process.env }
|
|
134
|
+
);
|
|
135
|
+
} catch (e) {
|
|
136
|
+
const err = e.stderr?.trim() ?? "";
|
|
137
|
+
if (/auth|login|sign.?in/i.test(err)) {
|
|
138
|
+
throw new Error(`Gemini not authenticated. Run: gemini auth OR export GEMINI_API_KEY=<key>`);
|
|
139
|
+
}
|
|
140
|
+
throw new Error(err || `gemini exited non-zero`);
|
|
141
|
+
}
|
|
142
|
+
const raw = out.trim();
|
|
143
|
+
try {
|
|
144
|
+
const data = JSON.parse(raw);
|
|
145
|
+
const text = String(data.response ?? data.result ?? data.text ?? raw);
|
|
146
|
+
const usage = data.tokenCount ?? data.usage ?? null;
|
|
147
|
+
return [text, usage];
|
|
148
|
+
} catch {
|
|
149
|
+
return [raw, null];
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
async *stream(prompt, model) {
|
|
153
|
+
const bin = which("gemini") || this.bin;
|
|
154
|
+
yield* spawnStream(
|
|
155
|
+
bin,
|
|
156
|
+
["--output-format", "text", "--model", model, "--approval-mode", "yolo"],
|
|
157
|
+
prompt
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
var CodexBackend = class {
|
|
162
|
+
constructor() {
|
|
163
|
+
this.name = "codex";
|
|
164
|
+
this.models = ["o3", "o4-mini", "gpt-4.1", "gpt-4o"];
|
|
165
|
+
this.prefixes = ["gpt", "o3", "o4", "o1"];
|
|
166
|
+
}
|
|
167
|
+
get bin() {
|
|
168
|
+
return process.env.CODEX_BIN ?? which("codex") ?? "codex";
|
|
169
|
+
}
|
|
170
|
+
available() {
|
|
171
|
+
return Boolean(which("codex"));
|
|
172
|
+
}
|
|
173
|
+
async runBlocking(prompt, model) {
|
|
174
|
+
let out;
|
|
175
|
+
try {
|
|
176
|
+
out = (0, import_child_process.execFileSync)(this.bin, ["-q", "--model", model, prompt], {
|
|
177
|
+
encoding: "utf8",
|
|
178
|
+
timeout: 3e5
|
|
179
|
+
});
|
|
180
|
+
} catch (e) {
|
|
181
|
+
throw new Error(e.stderr?.trim() || `codex exited non-zero`);
|
|
182
|
+
}
|
|
183
|
+
return [out.trim(), null];
|
|
184
|
+
}
|
|
185
|
+
async *stream(prompt, model) {
|
|
186
|
+
yield* spawnStream(this.bin, ["-q", "--model", model, prompt]);
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
var CopilotBackend = class {
|
|
190
|
+
constructor() {
|
|
191
|
+
this.name = "copilot";
|
|
192
|
+
this.models = ["copilot", "github-copilot"];
|
|
193
|
+
this.prefixes = ["copilot", "github-copilot"];
|
|
194
|
+
}
|
|
195
|
+
get bin() {
|
|
196
|
+
return process.env.GH_BIN ?? which("gh") ?? "gh";
|
|
197
|
+
}
|
|
198
|
+
available() {
|
|
199
|
+
if (!which("gh")) return false;
|
|
200
|
+
try {
|
|
201
|
+
(0, import_child_process.execFileSync)("gh", ["copilot", "--version"], { encoding: "utf8", stdio: "pipe" });
|
|
202
|
+
return true;
|
|
203
|
+
} catch {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
async runBlocking(prompt, model) {
|
|
208
|
+
let out;
|
|
209
|
+
try {
|
|
210
|
+
out = (0, import_child_process.execFileSync)(this.bin, ["copilot", "suggest", "-t", "general", prompt], {
|
|
211
|
+
encoding: "utf8",
|
|
212
|
+
timeout: 12e4
|
|
213
|
+
});
|
|
214
|
+
} catch (e) {
|
|
215
|
+
throw new Error(e.stderr?.trim() || `gh copilot exited non-zero`);
|
|
216
|
+
}
|
|
217
|
+
return [out.trim(), null];
|
|
218
|
+
}
|
|
219
|
+
async *stream(prompt, model) {
|
|
220
|
+
yield* spawnStream(this.bin, ["copilot", "suggest", "-t", "general", prompt]);
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
var BACKENDS = [
|
|
224
|
+
new ClaudeBackend(),
|
|
225
|
+
new GeminiBackend(),
|
|
226
|
+
new CodexBackend(),
|
|
227
|
+
new CopilotBackend()
|
|
228
|
+
];
|
|
229
|
+
function pickBackend(model) {
|
|
230
|
+
const m = model.toLowerCase();
|
|
231
|
+
for (const b of BACKENDS) {
|
|
232
|
+
if (b.prefixes.some((p) => m.startsWith(p))) {
|
|
233
|
+
if (b.available()) return b;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return BACKENDS.find((b) => b.available()) ?? BACKENDS[0];
|
|
237
|
+
}
|
|
238
|
+
function allModels() {
|
|
239
|
+
return BACKENDS.filter((b) => b.available()).flatMap((b) => [...b.models]);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// src/server.ts
|
|
243
|
+
function sse(data) {
|
|
244
|
+
return `data: ${JSON.stringify(data)}
|
|
245
|
+
|
|
246
|
+
`;
|
|
247
|
+
}
|
|
248
|
+
function chunk(id, ts, model, delta, finish) {
|
|
249
|
+
return sse({
|
|
250
|
+
id,
|
|
251
|
+
object: "chat.completion.chunk",
|
|
252
|
+
created: ts,
|
|
253
|
+
model,
|
|
254
|
+
choices: [{ index: 0, delta, finish_reason: finish ?? null }]
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
function completion(id, ts, model, text, usage) {
|
|
258
|
+
const pt = usage ? (usage.input_tokens ?? 0) + (usage.cache_creation_input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0) + (usage.promptTokenCount ?? 0) : 0;
|
|
259
|
+
const ct = usage ? (usage.output_tokens ?? 0) + (usage.candidatesTokenCount ?? 0) : 0;
|
|
260
|
+
return {
|
|
261
|
+
id,
|
|
262
|
+
object: "chat.completion",
|
|
263
|
+
created: ts,
|
|
264
|
+
model,
|
|
265
|
+
choices: [{ index: 0, message: { role: "assistant", content: text }, finish_reason: "stop" }],
|
|
266
|
+
usage: { prompt_tokens: pt, completion_tokens: ct, total_tokens: pt + ct }
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
function cors(res, statusCode) {
|
|
270
|
+
res.writeHead(statusCode, {
|
|
271
|
+
"Access-Control-Allow-Origin": "*",
|
|
272
|
+
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
273
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization"
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
function sendJson(res, statusCode, body) {
|
|
277
|
+
const payload = Buffer.from(JSON.stringify(body));
|
|
278
|
+
cors(res, statusCode);
|
|
279
|
+
res.setHeader("Content-Type", "application/json");
|
|
280
|
+
res.setHeader("Content-Length", payload.byteLength);
|
|
281
|
+
res.end(payload);
|
|
282
|
+
}
|
|
283
|
+
async function readBody(req) {
|
|
284
|
+
return new Promise((resolve, reject) => {
|
|
285
|
+
const chunks = [];
|
|
286
|
+
req.on("data", (c) => chunks.push(c));
|
|
287
|
+
req.on("end", () => resolve(Buffer.concat(chunks).toString()));
|
|
288
|
+
req.on("error", reject);
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
function handleModels(res) {
|
|
292
|
+
const ts = Math.floor(Date.now() / 1e3);
|
|
293
|
+
sendJson(res, 200, {
|
|
294
|
+
object: "list",
|
|
295
|
+
data: allModels().map((id) => ({ id, object: "model", created: ts, owned_by: "bridge" }))
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
function handleHealth(res, port2) {
|
|
299
|
+
const backends = {};
|
|
300
|
+
for (const b of BACKENDS) backends[b.name] = b.available();
|
|
301
|
+
sendJson(res, 200, { status: "ok", port: port2, backends });
|
|
302
|
+
}
|
|
303
|
+
async function handleChat(req, res) {
|
|
304
|
+
let body;
|
|
305
|
+
try {
|
|
306
|
+
body = JSON.parse(await readBody(req));
|
|
307
|
+
} catch {
|
|
308
|
+
sendJson(res, 400, { error: { message: "invalid JSON", type: "invalid_request_error" } });
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
const messages = body?.messages;
|
|
312
|
+
if (!Array.isArray(messages) || messages.length === 0) {
|
|
313
|
+
sendJson(res, 400, { error: { message: "messages required", type: "invalid_request_error" } });
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
const model = body.model ?? "claude-sonnet-4-6";
|
|
317
|
+
const streaming = Boolean(body.stream);
|
|
318
|
+
const prompt = messagesToPrompt(messages);
|
|
319
|
+
const backend = pickBackend(model);
|
|
320
|
+
const id = `chatcmpl-${(0, import_crypto.randomUUID)().replace(/-/g, "").slice(0, 20)}`;
|
|
321
|
+
const ts = Math.floor(Date.now() / 1e3);
|
|
322
|
+
console.log(` ${backend.name} model=${model} stream=${streaming} turns=${messages.length}`);
|
|
323
|
+
if (streaming) {
|
|
324
|
+
cors(res, 200);
|
|
325
|
+
res.setHeader("Content-Type", "text/event-stream");
|
|
326
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
327
|
+
res.setHeader("X-Accel-Buffering", "no");
|
|
328
|
+
res.flushHeaders();
|
|
329
|
+
res.write(chunk(id, ts, model, { role: "assistant" }));
|
|
330
|
+
try {
|
|
331
|
+
for await (const raw of backend.stream(prompt, model)) {
|
|
332
|
+
res.write(chunk(id, ts, model, { content: raw.toString("utf8") }));
|
|
333
|
+
}
|
|
334
|
+
} catch (err) {
|
|
335
|
+
console.error(` stream error: ${err.message}`);
|
|
336
|
+
}
|
|
337
|
+
res.write(chunk(id, ts, model, {}, "stop"));
|
|
338
|
+
res.write("data: [DONE]\n\n");
|
|
339
|
+
res.end();
|
|
340
|
+
} else {
|
|
341
|
+
try {
|
|
342
|
+
const [text, usage] = await backend.runBlocking(prompt, model);
|
|
343
|
+
sendJson(res, 200, completion(id, ts, model, text, usage));
|
|
344
|
+
} catch (err) {
|
|
345
|
+
console.error(` error: ${err.message}`);
|
|
346
|
+
sendJson(res, 500, { error: { message: err.message, type: "server_error" } });
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
function createBridgeServer(port2) {
|
|
351
|
+
const server = (0, import_http.createServer)(async (req, res) => {
|
|
352
|
+
const path = (req.url ?? "/").split("?")[0];
|
|
353
|
+
const method = req.method ?? "GET";
|
|
354
|
+
if (method === "OPTIONS") {
|
|
355
|
+
cors(res, 200);
|
|
356
|
+
res.end();
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
if (method === "GET" && (path === "/v1/models" || path === "/models")) {
|
|
360
|
+
handleModels(res);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
if (method === "GET" && path === "/health") {
|
|
364
|
+
handleHealth(res, port2);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
if (method === "POST" && (path === "/v1/chat/completions" || path === "/chat/completions")) {
|
|
368
|
+
await handleChat(req, res);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
sendJson(res, 404, { error: { message: "not found", type: "not_found_error" } });
|
|
372
|
+
});
|
|
373
|
+
return server;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// src/service.ts
|
|
377
|
+
var import_child_process2 = require("child_process");
|
|
378
|
+
var import_fs2 = require("fs");
|
|
379
|
+
var import_os2 = require("os");
|
|
380
|
+
var import_path = require("path");
|
|
381
|
+
var HOME2 = (0, import_os2.homedir)();
|
|
382
|
+
var LABEL = "com.bridgerapi.server";
|
|
383
|
+
function plistPath() {
|
|
384
|
+
return (0, import_path.join)(HOME2, "Library/LaunchAgents", `${LABEL}.plist`);
|
|
385
|
+
}
|
|
386
|
+
function writePlist(port2, scriptPath, nodePath) {
|
|
387
|
+
const logDir = (0, import_path.join)(HOME2, ".bridgerapi");
|
|
388
|
+
(0, import_fs2.mkdirSync)(logDir, { recursive: true });
|
|
389
|
+
(0, import_fs2.mkdirSync)((0, import_path.join)(HOME2, "Library/LaunchAgents"), { recursive: true });
|
|
390
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
391
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
|
392
|
+
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
393
|
+
<plist version="1.0">
|
|
394
|
+
<dict>
|
|
395
|
+
<key>Label</key>
|
|
396
|
+
<string>${LABEL}</string>
|
|
397
|
+
|
|
398
|
+
<key>ProgramArguments</key>
|
|
399
|
+
<array>
|
|
400
|
+
<string>${nodePath}</string>
|
|
401
|
+
<string>${scriptPath}</string>
|
|
402
|
+
<string>start</string>
|
|
403
|
+
</array>
|
|
404
|
+
|
|
405
|
+
<key>EnvironmentVariables</key>
|
|
406
|
+
<dict>
|
|
407
|
+
<key>BRIDGERAPI_PORT</key>
|
|
408
|
+
<string>${port2}</string>
|
|
409
|
+
<key>PATH</key>
|
|
410
|
+
<string>${HOME2}/.local/bin:/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin</string>
|
|
411
|
+
<key>HOME</key>
|
|
412
|
+
<string>${HOME2}</string>
|
|
413
|
+
<key>CUSTOM_OKBRIDGER_API_KEY</key>
|
|
414
|
+
<string>local</string>
|
|
415
|
+
</dict>
|
|
416
|
+
|
|
417
|
+
<key>StandardOutPath</key>
|
|
418
|
+
<string>${logDir}/server.log</string>
|
|
419
|
+
<key>StandardErrorPath</key>
|
|
420
|
+
<string>${logDir}/server.log</string>
|
|
421
|
+
<key>WorkingDirectory</key>
|
|
422
|
+
<string>${HOME2}</string>
|
|
423
|
+
<key>RunAtLoad</key>
|
|
424
|
+
<true/>
|
|
425
|
+
<key>KeepAlive</key>
|
|
426
|
+
<true/>
|
|
427
|
+
</dict>
|
|
428
|
+
</plist>`;
|
|
429
|
+
(0, import_fs2.writeFileSync)(plistPath(), plist);
|
|
430
|
+
}
|
|
431
|
+
function unitPath() {
|
|
432
|
+
const configHome = process.env.XDG_CONFIG_HOME ?? (0, import_path.join)(HOME2, ".config");
|
|
433
|
+
return (0, import_path.join)(configHome, "systemd/user/bridgerapi.service");
|
|
434
|
+
}
|
|
435
|
+
function writeUnit(port2, scriptPath, nodePath) {
|
|
436
|
+
const logDir = (0, import_path.join)(HOME2, ".bridgerapi");
|
|
437
|
+
(0, import_fs2.mkdirSync)(logDir, { recursive: true });
|
|
438
|
+
(0, import_fs2.mkdirSync)((0, import_path.join)(HOME2, ".config/systemd/user"), { recursive: true });
|
|
439
|
+
const unit = `[Unit]
|
|
440
|
+
Description=bridgerapi \u2014 OpenAI-compatible bridge for AI CLIs
|
|
441
|
+
After=network.target
|
|
442
|
+
|
|
443
|
+
[Service]
|
|
444
|
+
Type=simple
|
|
445
|
+
ExecStart=${nodePath} ${scriptPath} start
|
|
446
|
+
Environment=BRIDGERAPI_PORT=${port2}
|
|
447
|
+
Environment=HOME=${HOME2}
|
|
448
|
+
Environment=CUSTOM_OKBRIDGER_API_KEY=local
|
|
449
|
+
Environment=PATH=${HOME2}/.local/bin:/usr/local/bin:/usr/bin:/bin
|
|
450
|
+
Restart=always
|
|
451
|
+
StandardOutput=append:${logDir}/server.log
|
|
452
|
+
StandardError=append:${logDir}/server.log
|
|
453
|
+
|
|
454
|
+
[Install]
|
|
455
|
+
WantedBy=default.target
|
|
456
|
+
`;
|
|
457
|
+
(0, import_fs2.writeFileSync)(unitPath(), unit);
|
|
458
|
+
}
|
|
459
|
+
function installService(port2) {
|
|
460
|
+
const scriptPath = process.argv[1];
|
|
461
|
+
const nodePath = process.execPath;
|
|
462
|
+
const os = (0, import_os2.platform)();
|
|
463
|
+
if (os === "darwin") {
|
|
464
|
+
try {
|
|
465
|
+
(0, import_child_process2.execSync)(`launchctl unload "${plistPath()}" 2>/dev/null`, { stdio: "ignore" });
|
|
466
|
+
} catch {
|
|
467
|
+
}
|
|
468
|
+
writePlist(port2, scriptPath, nodePath);
|
|
469
|
+
(0, import_child_process2.execSync)(`launchctl load -w "${plistPath()}"`);
|
|
470
|
+
console.log(`\u2713 LaunchAgent installed \u2192 ${plistPath()}`);
|
|
471
|
+
} else if (os === "linux") {
|
|
472
|
+
writeUnit(port2, scriptPath, nodePath);
|
|
473
|
+
try {
|
|
474
|
+
(0, import_child_process2.execSync)("systemctl --user daemon-reload");
|
|
475
|
+
} catch {
|
|
476
|
+
}
|
|
477
|
+
(0, import_child_process2.execSync)("systemctl --user enable --now bridgerapi");
|
|
478
|
+
console.log(`\u2713 systemd user service installed`);
|
|
479
|
+
} else {
|
|
480
|
+
throw new Error(`Auto-install not supported on ${os}. Run 'bridgerapi start' manually.`);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
function uninstallService() {
|
|
484
|
+
const os = (0, import_os2.platform)();
|
|
485
|
+
if (os === "darwin") {
|
|
486
|
+
const p = plistPath();
|
|
487
|
+
if ((0, import_fs2.existsSync)(p)) {
|
|
488
|
+
try {
|
|
489
|
+
(0, import_child_process2.execSync)(`launchctl unload "${p}"`);
|
|
490
|
+
} catch {
|
|
491
|
+
}
|
|
492
|
+
(0, import_fs2.unlinkSync)(p);
|
|
493
|
+
console.log("\u2713 LaunchAgent removed");
|
|
494
|
+
} else {
|
|
495
|
+
console.log(" bridgerapi service is not installed");
|
|
496
|
+
}
|
|
497
|
+
} else if (os === "linux") {
|
|
498
|
+
const p = unitPath();
|
|
499
|
+
try {
|
|
500
|
+
(0, import_child_process2.execSync)("systemctl --user disable --now bridgerapi");
|
|
501
|
+
} catch {
|
|
502
|
+
}
|
|
503
|
+
if ((0, import_fs2.existsSync)(p)) {
|
|
504
|
+
(0, import_fs2.unlinkSync)(p);
|
|
505
|
+
try {
|
|
506
|
+
(0, import_child_process2.execSync)("systemctl --user daemon-reload");
|
|
507
|
+
} catch {
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
console.log("\u2713 systemd service removed");
|
|
511
|
+
} else {
|
|
512
|
+
throw new Error(`Not supported on ${os}`);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
function serviceStatus() {
|
|
516
|
+
const os = (0, import_os2.platform)();
|
|
517
|
+
try {
|
|
518
|
+
if (os === "darwin") {
|
|
519
|
+
const out = (0, import_child_process2.execSync)(`launchctl list ${LABEL} 2>/dev/null`, { encoding: "utf8" });
|
|
520
|
+
const pid = parseInt(out.match(/"PID"\s*=\s*(\d+)/)?.[1] ?? "0");
|
|
521
|
+
return { running: pid > 0, pid: pid || void 0 };
|
|
522
|
+
} else if (os === "linux") {
|
|
523
|
+
(0, import_child_process2.execSync)("systemctl --user is-active bridgerapi", { stdio: "ignore" });
|
|
524
|
+
return { running: true };
|
|
525
|
+
}
|
|
526
|
+
} catch {
|
|
527
|
+
}
|
|
528
|
+
return { running: false };
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// src/cli.ts
|
|
532
|
+
var import_fs3 = require("fs");
|
|
533
|
+
var import_os3 = require("os");
|
|
534
|
+
var import_path2 = require("path");
|
|
535
|
+
var PORT = parseInt(process.env.BRIDGERAPI_PORT ?? "8082");
|
|
536
|
+
var LOG_DIR = (0, import_path2.join)((0, import_os3.homedir)(), ".bridgerapi");
|
|
537
|
+
var USAGE = `
|
|
538
|
+
bridgerapi \u2014 OpenAI-compatible API bridge for AI CLI tools
|
|
539
|
+
|
|
540
|
+
Commands:
|
|
541
|
+
start [--port <n>] Start server in the foreground (default port: 8082)
|
|
542
|
+
install [--port <n>] Install as a background service (auto-starts on login)
|
|
543
|
+
uninstall Remove background service
|
|
544
|
+
status Show whether the service is running
|
|
545
|
+
backends List detected CLI backends and their status
|
|
546
|
+
|
|
547
|
+
Examples:
|
|
548
|
+
bridgerapi start
|
|
549
|
+
bridgerapi start --port 9000
|
|
550
|
+
bridgerapi install
|
|
551
|
+
bridgerapi backends
|
|
552
|
+
|
|
553
|
+
Supported backends (auto-detected):
|
|
554
|
+
claude-* \u2192 Claude Code CLI (oauth via Claude Code login)
|
|
555
|
+
gemini-* \u2192 Gemini CLI (run: gemini auth)
|
|
556
|
+
gpt-*, o3 \u2192 Codex CLI (run: codex auth)
|
|
557
|
+
copilot \u2192 GitHub Copilot (run: gh auth login)
|
|
558
|
+
|
|
559
|
+
Logs: ${LOG_DIR}/server.log
|
|
560
|
+
`.trim();
|
|
561
|
+
function parseArgs() {
|
|
562
|
+
const args = process.argv.slice(2);
|
|
563
|
+
const cmd2 = args[0] ?? "help";
|
|
564
|
+
let port2 = PORT;
|
|
565
|
+
for (let i = 1; i < args.length; i++) {
|
|
566
|
+
if ((args[i] === "--port" || args[i] === "-p") && args[i + 1]) {
|
|
567
|
+
port2 = parseInt(args[++i]);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
return { cmd: cmd2, port: port2 };
|
|
571
|
+
}
|
|
572
|
+
function cmdStart(port2) {
|
|
573
|
+
(0, import_fs3.mkdirSync)(LOG_DIR, { recursive: true });
|
|
574
|
+
const available = BACKENDS.filter((b) => b.available());
|
|
575
|
+
const unavailable = BACKENDS.filter((b) => !b.available());
|
|
576
|
+
console.log(`bridgerapi http://127.0.0.1:${port2}`);
|
|
577
|
+
console.log(` backends ready : ${available.map((b) => b.name).join(", ") || "none!"}`);
|
|
578
|
+
if (unavailable.length) {
|
|
579
|
+
console.log(` backends missing : ${unavailable.map((b) => b.name).join(", ")}`);
|
|
580
|
+
}
|
|
581
|
+
console.log(` logs : ${LOG_DIR}/server.log`);
|
|
582
|
+
console.log();
|
|
583
|
+
if (available.length === 0) {
|
|
584
|
+
console.error("Error: no CLI backends found. Install claude / gemini / codex first.");
|
|
585
|
+
process.exit(1);
|
|
586
|
+
}
|
|
587
|
+
const server = createBridgeServer(port2);
|
|
588
|
+
server.listen(port2, "127.0.0.1", () => {
|
|
589
|
+
console.log(` GET /v1/models`);
|
|
590
|
+
console.log(` POST /v1/chat/completions (stream + blocking)`);
|
|
591
|
+
console.log(` GET /health`);
|
|
592
|
+
console.log();
|
|
593
|
+
console.log(` Goose provider config:`);
|
|
594
|
+
console.log(` Base URL : http://127.0.0.1:${port2}/v1`);
|
|
595
|
+
console.log(` API Key : local`);
|
|
596
|
+
console.log(` Model : claude-sonnet-4-6`);
|
|
597
|
+
});
|
|
598
|
+
server.on("error", (err) => {
|
|
599
|
+
if (err.code === "EADDRINUSE") {
|
|
600
|
+
console.error(`Port ${port2} is already in use. Try: bridgerapi start --port 9000`);
|
|
601
|
+
} else {
|
|
602
|
+
console.error("Server error:", err.message);
|
|
603
|
+
}
|
|
604
|
+
process.exit(1);
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
function cmdInstall(port2) {
|
|
608
|
+
try {
|
|
609
|
+
installService(port2);
|
|
610
|
+
console.log();
|
|
611
|
+
console.log(" Waiting for server to start\u2026");
|
|
612
|
+
let attempts = 0;
|
|
613
|
+
const poll = setInterval(async () => {
|
|
614
|
+
attempts++;
|
|
615
|
+
try {
|
|
616
|
+
const http = await import("http");
|
|
617
|
+
http.get(`http://127.0.0.1:${port2}/health`, (res) => {
|
|
618
|
+
if (res.statusCode === 200) {
|
|
619
|
+
clearInterval(poll);
|
|
620
|
+
console.log(`
|
|
621
|
+
\u2713 bridgerapi running on http://127.0.0.1:${port2}`);
|
|
622
|
+
console.log();
|
|
623
|
+
console.log(" Goose config:");
|
|
624
|
+
console.log(` Provider : OpenAI Compatible`);
|
|
625
|
+
console.log(` Base URL : http://127.0.0.1:${port2}/v1`);
|
|
626
|
+
console.log(` API Key : local`);
|
|
627
|
+
console.log(` Model : claude-sonnet-4-6`);
|
|
628
|
+
console.log();
|
|
629
|
+
console.log(` Logs : tail -f ${LOG_DIR}/server.log`);
|
|
630
|
+
console.log(` Stop : bridgerapi uninstall`);
|
|
631
|
+
process.exit(0);
|
|
632
|
+
}
|
|
633
|
+
}).on("error", () => {
|
|
634
|
+
});
|
|
635
|
+
} catch {
|
|
636
|
+
}
|
|
637
|
+
if (attempts >= 10) {
|
|
638
|
+
clearInterval(poll);
|
|
639
|
+
console.log(`
|
|
640
|
+
Server did not start. Check: tail -f ${LOG_DIR}/server.log`);
|
|
641
|
+
process.exit(1);
|
|
642
|
+
}
|
|
643
|
+
}, 600);
|
|
644
|
+
} catch (err) {
|
|
645
|
+
console.error("Install failed:", err.message);
|
|
646
|
+
process.exit(1);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
function cmdUninstall() {
|
|
650
|
+
try {
|
|
651
|
+
uninstallService();
|
|
652
|
+
} catch (err) {
|
|
653
|
+
console.error("Uninstall failed:", err.message);
|
|
654
|
+
process.exit(1);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
function cmdStatus(port2) {
|
|
658
|
+
const { running, pid } = serviceStatus();
|
|
659
|
+
if (running) {
|
|
660
|
+
console.log(`\u2713 bridgerapi is running${pid ? ` (pid ${pid})` : ""} on port ${port2}`);
|
|
661
|
+
} else {
|
|
662
|
+
console.log(" bridgerapi is not running");
|
|
663
|
+
console.log(" Start with: bridgerapi start or bridgerapi install");
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
function cmdBackends() {
|
|
667
|
+
console.log("CLI backends:\n");
|
|
668
|
+
for (const b of BACKENDS) {
|
|
669
|
+
const ok = b.available();
|
|
670
|
+
const icon = ok ? "\u2713" : "\u2717";
|
|
671
|
+
const models = ok ? b.models.join(", ") : "(not installed)";
|
|
672
|
+
console.log(` ${icon} ${b.name.padEnd(10)} ${models}`);
|
|
673
|
+
}
|
|
674
|
+
console.log();
|
|
675
|
+
const available = BACKENDS.filter((b) => b.available());
|
|
676
|
+
if (available.length) {
|
|
677
|
+
console.log(`All available models:
|
|
678
|
+
${allModels().join(", ")}`);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
var { cmd, port } = parseArgs();
|
|
682
|
+
switch (cmd) {
|
|
683
|
+
case "start":
|
|
684
|
+
cmdStart(port);
|
|
685
|
+
break;
|
|
686
|
+
case "install":
|
|
687
|
+
cmdInstall(port);
|
|
688
|
+
break;
|
|
689
|
+
case "uninstall":
|
|
690
|
+
cmdUninstall();
|
|
691
|
+
break;
|
|
692
|
+
case "status":
|
|
693
|
+
cmdStatus(port);
|
|
694
|
+
break;
|
|
695
|
+
case "backends":
|
|
696
|
+
cmdBackends();
|
|
697
|
+
break;
|
|
698
|
+
default:
|
|
699
|
+
console.log(USAGE);
|
|
700
|
+
if (cmd !== "help") process.exit(1);
|
|
701
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bridgerapi",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Turn any AI CLI (Claude Code, Gemini, Codex, GitHub Copilot) into an OpenAI-compatible API — no API keys needed",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"claude",
|
|
7
|
+
"gemini",
|
|
8
|
+
"codex",
|
|
9
|
+
"copilot",
|
|
10
|
+
"openai",
|
|
11
|
+
"api",
|
|
12
|
+
"proxy",
|
|
13
|
+
"bridge",
|
|
14
|
+
"goose",
|
|
15
|
+
"oauth",
|
|
16
|
+
"llm"
|
|
17
|
+
],
|
|
18
|
+
"homepage": "https://github.com/teodorwaltervido/bridgerapi#readme",
|
|
19
|
+
"bugs": "https://github.com/teodorwaltervido/bridgerapi/issues",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"author": "teodorwaltervido",
|
|
22
|
+
"main": "dist/cli.js",
|
|
23
|
+
"bin": {
|
|
24
|
+
"bridgerapi": "./dist/cli.js"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist"
|
|
28
|
+
],
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=18"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsup",
|
|
34
|
+
"dev": "tsx src/cli.ts start",
|
|
35
|
+
"prepublishOnly": "npm run build"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/node": "^22.0.0",
|
|
39
|
+
"tsup": "^8.3.5",
|
|
40
|
+
"tsx": "^4.19.2",
|
|
41
|
+
"typescript": "^5.7.2"
|
|
42
|
+
}
|
|
43
|
+
}
|