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.
Files changed (2) hide show
  1. package/dist/cli.js +701 -0
  2. 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
+ }