bridgerapi 1.6.0 → 1.8.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 +368 -89
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -57,9 +57,32 @@ function messagesToPrompt(messages) {
57
57
 
58
58
  // src/backends.ts
59
59
  var import_child_process = require("child_process");
60
+ var import_fs2 = require("fs");
61
+ var import_os2 = require("os");
62
+ var import_path2 = require("path");
63
+
64
+ // src/config.ts
60
65
  var import_fs = require("fs");
61
66
  var import_os = require("os");
62
- var HOME = (0, import_os.homedir)();
67
+ var import_path = require("path");
68
+ var CONFIG_DIR = (0, import_path.join)((0, import_os.homedir)(), ".bridgerapi");
69
+ var CONFIG_FILE = (0, import_path.join)(CONFIG_DIR, "config.json");
70
+ function loadConfig() {
71
+ try {
72
+ if ((0, import_fs.existsSync)(CONFIG_FILE)) {
73
+ return JSON.parse((0, import_fs.readFileSync)(CONFIG_FILE, "utf8"));
74
+ }
75
+ } catch {
76
+ }
77
+ return {};
78
+ }
79
+ function saveConfig(cfg) {
80
+ (0, import_fs.mkdirSync)(CONFIG_DIR, { recursive: true });
81
+ (0, import_fs.writeFileSync)(CONFIG_FILE, JSON.stringify(cfg, null, 2) + "\n");
82
+ }
83
+
84
+ // src/backends.ts
85
+ var HOME = (0, import_os2.homedir)();
63
86
  function which(cmd2) {
64
87
  try {
65
88
  return (0, import_child_process.execFileSync)("which", [cmd2], { encoding: "utf8" }).trim();
@@ -67,30 +90,145 @@ function which(cmd2) {
67
90
  return "";
68
91
  }
69
92
  }
93
+ var SKIP_TOKENS = /* @__PURE__ */ new Set([
94
+ "output",
95
+ "format",
96
+ "text",
97
+ "json",
98
+ "model",
99
+ "usage",
100
+ "help",
101
+ "exec",
102
+ "run",
103
+ "list",
104
+ "command",
105
+ "option",
106
+ "default",
107
+ "true",
108
+ "false",
109
+ "stdin",
110
+ "stdout",
111
+ "stderr",
112
+ "path",
113
+ "file",
114
+ "config",
115
+ "error",
116
+ "warn",
117
+ "approval",
118
+ "mode",
119
+ "yolo",
120
+ "version",
121
+ "verbose",
122
+ "debug",
123
+ "quiet"
124
+ ]);
125
+ function extractModelIds(text) {
126
+ const seen = /* @__PURE__ */ new Set();
127
+ const re = /\b([a-z][a-z0-9]*(?:[.\-][a-z0-9]+)+)\b/g;
128
+ for (const [, id] of text.matchAll(re)) {
129
+ if (id.length >= 5 && !SKIP_TOKENS.has(id)) seen.add(id);
130
+ }
131
+ return [...seen];
132
+ }
70
133
  async function* spawnStream(cmd2, args, stdin) {
71
- const proc = (0, import_child_process.spawn)(cmd2, args, {
72
- env: process.env,
73
- stdio: ["pipe", "pipe", "pipe"]
134
+ const proc = (0, import_child_process.spawn)(cmd2, args, { env: process.env, stdio: ["pipe", "pipe", "pipe"] });
135
+ if (stdin !== void 0) proc.stdin.end(stdin);
136
+ const stderrBufs = [];
137
+ proc.stderr.on("data", (c) => stderrBufs.push(c));
138
+ let hasOutput = false;
139
+ try {
140
+ for await (const chunk2 of proc.stdout) {
141
+ hasOutput = true;
142
+ yield chunk2;
143
+ }
144
+ } finally {
145
+ if (proc.exitCode === null) proc.kill();
146
+ }
147
+ const exitCode = await new Promise((resolve) => {
148
+ proc.exitCode !== null ? resolve(proc.exitCode) : proc.on("close", (c) => resolve(c ?? 0));
74
149
  });
75
- if (stdin) proc.stdin.end(stdin);
76
- for await (const chunk2 of proc.stdout) yield chunk2;
150
+ if (!hasOutput && exitCode !== 0) {
151
+ const stderr = Buffer.concat(stderrBufs).toString().trim();
152
+ throw new Error(stderr || `${(0, import_path2.basename)(cmd2)} exited with code ${exitCode}`);
153
+ }
77
154
  }
155
+ var GenericBackend = class {
156
+ constructor(def) {
157
+ this.name = def.name;
158
+ this.prefixes = def.prefixes;
159
+ this.def = def;
160
+ }
161
+ get bin() {
162
+ return which(this.def.bin) || `${HOME}/.local/bin/${this.def.bin}`;
163
+ }
164
+ available() {
165
+ return Boolean(which(this.def.bin)) || (0, import_fs2.existsSync)(`${HOME}/.local/bin/${this.def.bin}`);
166
+ }
167
+ async models() {
168
+ if (this.def.modelsCmd) {
169
+ try {
170
+ const out = (0, import_child_process.execFileSync)(this.bin, this.def.modelsCmd, {
171
+ encoding: "utf8",
172
+ timeout: 5e3,
173
+ stdio: ["ignore", "pipe", "pipe"]
174
+ });
175
+ const found = extractModelIds(out);
176
+ if (found.length > 0) return found;
177
+ } catch {
178
+ }
179
+ }
180
+ return this.def.models ?? [];
181
+ }
182
+ buildArgs(model2) {
183
+ return this.def.args.map((a) => a === "{model}" ? model2 : a);
184
+ }
185
+ async runBlocking(prompt, model2) {
186
+ const args = this.buildArgs(model2);
187
+ let out;
188
+ try {
189
+ if (this.def.promptMode === "stdin") {
190
+ out = (0, import_child_process.execFileSync)(this.bin, args, { input: prompt, encoding: "utf8", timeout: 3e5 });
191
+ } else {
192
+ out = (0, import_child_process.execFileSync)(this.bin, [...args, prompt], { encoding: "utf8", timeout: 3e5 });
193
+ }
194
+ } catch (e) {
195
+ throw new Error(e.stderr?.trim() || `${this.def.bin} exited non-zero`);
196
+ }
197
+ return [out.trim(), null];
198
+ }
199
+ async *stream(prompt, model2) {
200
+ const args = this.buildArgs(model2);
201
+ if (this.def.promptMode === "stdin") {
202
+ yield* spawnStream(this.bin, args, prompt);
203
+ } else {
204
+ yield* spawnStream(this.bin, [...args, prompt]);
205
+ }
206
+ }
207
+ };
78
208
  var ClaudeBackend = class {
79
209
  constructor() {
80
210
  this.name = "claude";
81
211
  this.prefixes = ["claude"];
82
212
  }
83
213
  get bin() {
84
- return process.env.CLAUDE_BIN ?? `${HOME}/.local/bin/claude`;
214
+ return process.env.CLAUDE_BIN ?? which("claude") ?? `${HOME}/.local/bin/claude`;
85
215
  }
86
216
  available() {
87
- return (0, import_fs.existsSync)(this.bin) || Boolean(which("claude"));
217
+ return Boolean(which("claude")) || (0, import_fs2.existsSync)(`${HOME}/.local/bin/claude`);
218
+ }
219
+ async models() {
220
+ return [
221
+ "claude-opus-4-6",
222
+ "claude-opus-4-6-fast",
223
+ "claude-sonnet-4-6",
224
+ "claude-sonnet-4-5-20250929",
225
+ "claude-haiku-4-5-20251001"
226
+ ];
88
227
  }
89
228
  async runBlocking(prompt, model2) {
90
- const bin = which("claude") || this.bin;
91
229
  let out;
92
230
  try {
93
- out = (0, import_child_process.execFileSync)(bin, ["-p", "--output-format", "json", "--model", model2], {
231
+ out = (0, import_child_process.execFileSync)(this.bin, ["-p", "--output-format", "json", "--model", model2], {
94
232
  input: prompt,
95
233
  encoding: "utf8",
96
234
  timeout: 3e5
@@ -98,12 +236,15 @@ var ClaudeBackend = class {
98
236
  } catch (e) {
99
237
  throw new Error(e.stderr?.trim() || `claude exited non-zero`);
100
238
  }
101
- const data = JSON.parse(out.trim() || "{}");
102
- return [data.result ?? "", data.usage ?? null];
239
+ try {
240
+ const data = JSON.parse(out.trim() || "{}");
241
+ return [data.result ?? "", data.usage ?? null];
242
+ } catch {
243
+ return [out.trim(), null];
244
+ }
103
245
  }
104
246
  async *stream(prompt, model2) {
105
- const bin = which("claude") || this.bin;
106
- yield* spawnStream(bin, ["-p", "--output-format", "text", "--model", model2], prompt);
247
+ yield* spawnStream(this.bin, ["-p", "--output-format", "text", "--model", model2], prompt);
107
248
  }
108
249
  };
109
250
  var GeminiBackend = class {
@@ -115,13 +256,23 @@ var GeminiBackend = class {
115
256
  return process.env.GEMINI_BIN ?? which("gemini") ?? "/opt/homebrew/bin/gemini";
116
257
  }
117
258
  available() {
118
- return Boolean(which("gemini")) || (0, import_fs.existsSync)(this.bin);
259
+ return Boolean(which("gemini")) || (0, import_fs2.existsSync)(this.bin);
260
+ }
261
+ async models() {
262
+ return [
263
+ "gemini-3.1-pro-preview",
264
+ "gemini-3-pro-preview",
265
+ "gemini-3-flash-preview",
266
+ "gemini-2.5-pro",
267
+ "gemini-2.5-flash",
268
+ "gemini-2.0-flash",
269
+ "gemini-1.5-pro"
270
+ ];
119
271
  }
120
272
  async runBlocking(prompt, model2) {
121
- const bin = which("gemini") || this.bin;
122
273
  let out;
123
274
  try {
124
- out = (0, import_child_process.execFileSync)(bin, ["--output-format", "json", "--model", model2, "--approval-mode", "yolo"], {
275
+ out = (0, import_child_process.execFileSync)(this.bin, ["--output-format", "json", "--model", model2, "--approval-mode", "yolo"], {
125
276
  input: prompt,
126
277
  encoding: "utf8",
127
278
  timeout: 3e5,
@@ -142,8 +293,11 @@ var GeminiBackend = class {
142
293
  }
143
294
  }
144
295
  async *stream(prompt, model2) {
145
- const bin = which("gemini") || this.bin;
146
- yield* spawnStream(bin, ["--output-format", "text", "--model", model2, "--approval-mode", "yolo"], prompt);
296
+ yield* spawnStream(
297
+ this.bin,
298
+ ["--output-format", "text", "--model", model2, "--approval-mode", "yolo"],
299
+ prompt
300
+ );
147
301
  }
148
302
  };
149
303
  var CodexBackend = class {
@@ -157,6 +311,20 @@ var CodexBackend = class {
157
311
  available() {
158
312
  return Boolean(which("codex"));
159
313
  }
314
+ async models() {
315
+ return [
316
+ "gpt-5-codex",
317
+ "gpt-5.1-codex",
318
+ "gpt-5.1-codex-max",
319
+ "gpt-5.1",
320
+ "gpt-5.2",
321
+ "gpt-5.2-codex",
322
+ "gpt-5.3-codex",
323
+ "gpt-5-2025-08-07",
324
+ "o4-mini",
325
+ "o3"
326
+ ];
327
+ }
160
328
  async runBlocking(prompt, model2) {
161
329
  let out;
162
330
  try {
@@ -190,6 +358,9 @@ var CopilotBackend = class {
190
358
  return false;
191
359
  }
192
360
  }
361
+ async models() {
362
+ return ["copilot"];
363
+ }
193
364
  async runBlocking(prompt, model2) {
194
365
  let out;
195
366
  try {
@@ -206,44 +377,93 @@ var CopilotBackend = class {
206
377
  yield* spawnStream(this.bin, ["copilot", "suggest", "-t", "general", prompt]);
207
378
  }
208
379
  };
209
- var DroidBackend = class {
380
+ var DroidBackend = class _DroidBackend {
210
381
  constructor() {
211
382
  this.name = "droid";
212
- // Route Droid-exclusive model families + explicit "droid" prefix
213
383
  this.prefixes = ["droid", "glm", "kimi", "minimax"];
214
384
  }
385
+ static {
386
+ // Up-to-date as of March 2026 — source: Factory docs + droid exec --help
387
+ this.KNOWN_MODELS = [
388
+ "gpt-5-codex",
389
+ "gpt-5.1-codex",
390
+ "gpt-5.1-codex-max",
391
+ "gpt-5.1",
392
+ "gpt-5.2",
393
+ "gpt-5.2-codex",
394
+ "gpt-5.3-codex",
395
+ "gpt-5-2025-08-07",
396
+ "claude-opus-4-6",
397
+ "claude-opus-4-6-fast",
398
+ "claude-opus-4-1-20250805",
399
+ "claude-sonnet-4-5-20250929",
400
+ "claude-haiku-4-5-20251001",
401
+ "gemini-3.1-pro-preview",
402
+ "gemini-3-pro-preview",
403
+ "gemini-3-flash-preview",
404
+ "glm-4.6",
405
+ "glm-4.7",
406
+ "glm-5",
407
+ "kimi-k2.5",
408
+ "minimax-m2.5"
409
+ ];
410
+ }
215
411
  get bin() {
216
412
  return process.env.DROID_BIN ?? which("droid") ?? `${HOME}/.local/bin/droid`;
217
413
  }
218
414
  available() {
219
- return (0, import_fs.existsSync)(this.bin) || Boolean(which("droid"));
415
+ return Boolean(which("droid")) || (0, import_fs2.existsSync)(`${HOME}/.local/bin/droid`);
220
416
  }
221
- async runBlocking(prompt, model2) {
222
- const bin = which("droid") || this.bin;
223
- let out;
417
+ async models() {
224
418
  try {
225
- out = (0, import_child_process.execFileSync)(bin, ["exec", "--output-format", "text", "--model", model2, "-"], {
226
- input: prompt,
419
+ const help = (0, import_child_process.execFileSync)(which("droid") || this.bin, ["exec", "--help"], {
227
420
  encoding: "utf8",
228
- timeout: 3e5
421
+ timeout: 5e3,
422
+ stdio: ["ignore", "pipe", "pipe"]
229
423
  });
424
+ const found = extractModelIds(help);
425
+ if (found.length > 0) return found;
426
+ } catch {
427
+ }
428
+ return _DroidBackend.KNOWN_MODELS;
429
+ }
430
+ async runBlocking(prompt, model2) {
431
+ let out;
432
+ try {
433
+ out = (0, import_child_process.execFileSync)(
434
+ which("droid") || this.bin,
435
+ ["exec", "--output-format", "text", "--model", model2, "-"],
436
+ { input: prompt, encoding: "utf8", timeout: 3e5 }
437
+ );
230
438
  } catch (e) {
231
439
  throw new Error(e.stderr?.trim() || `droid exited non-zero`);
232
440
  }
233
441
  return [out.trim(), null];
234
442
  }
235
443
  async *stream(prompt, model2) {
236
- const bin = which("droid") || this.bin;
237
- yield* spawnStream(bin, ["exec", "--output-format", "text", "--model", model2, "-"], prompt);
444
+ yield* spawnStream(
445
+ which("droid") || this.bin,
446
+ ["exec", "--output-format", "text", "--model", model2, "-"],
447
+ prompt
448
+ );
238
449
  }
239
450
  };
240
- var BACKENDS = [
451
+ var BUILTIN = [
241
452
  new ClaudeBackend(),
242
453
  new GeminiBackend(),
243
454
  new CodexBackend(),
244
455
  new CopilotBackend(),
245
456
  new DroidBackend()
246
457
  ];
458
+ function loadUserBackends() {
459
+ try {
460
+ const cfg = loadConfig();
461
+ return (cfg.customBackends ?? []).map((def) => new GenericBackend(def));
462
+ } catch {
463
+ return [];
464
+ }
465
+ }
466
+ var BACKENDS = [...BUILTIN, ...loadUserBackends()];
247
467
  function pickBackend(model2) {
248
468
  const override = process.env.BRIDGERAPI_BACKEND?.toLowerCase();
249
469
  if (override) {
@@ -306,18 +526,25 @@ async function readBody(req) {
306
526
  req.on("error", reject);
307
527
  });
308
528
  }
309
- function handleModels(res) {
529
+ async function handleModels(res) {
310
530
  const ts = Math.floor(Date.now() / 1e3);
531
+ const override = process.env.BRIDGERAPI_BACKEND?.toLowerCase();
532
+ const pinned = override ? BACKENDS.find((b) => b.name === override && b.available()) : null;
533
+ if (pinned) {
534
+ const ids = await pinned.models();
535
+ sendJson(res, 200, {
536
+ object: "list",
537
+ data: ids.map((id) => ({ id, object: "model", created: ts, owned_by: pinned.name }))
538
+ });
539
+ return;
540
+ }
311
541
  const available = BACKENDS.filter((b) => b.available());
312
- sendJson(res, 200, {
313
- object: "list",
314
- data: available.map((b) => ({
315
- id: b.name,
316
- object: "model",
317
- created: ts,
318
- owned_by: "bridgerapi"
319
- }))
320
- });
542
+ const allModels = [];
543
+ for (const b of available) {
544
+ const ids = await b.models();
545
+ for (const id of ids) allModels.push({ id, object: "model", created: ts, owned_by: b.name });
546
+ }
547
+ sendJson(res, 200, { object: "list", data: allModels });
321
548
  }
322
549
  function handleHealth(res, port2) {
323
550
  const backends = {};
@@ -356,7 +583,11 @@ async function handleChat(req, res) {
356
583
  res.write(chunk(id, ts, model2, { content: raw.toString("utf8") }));
357
584
  }
358
585
  } catch (err) {
359
- console.error(` stream error: ${err.message}`);
586
+ const msg = err.message ?? String(err);
587
+ console.error(` stream error [${backend2.name}]: ${msg}`);
588
+ res.write(chunk(id, ts, model2, { content: `
589
+
590
+ \u26A0\uFE0F ${backend2.name} error: ${msg}` }));
360
591
  }
361
592
  res.write(chunk(id, ts, model2, {}, "stop"));
362
593
  res.write("data: [DONE]\n\n");
@@ -381,7 +612,7 @@ function createBridgeServer(port2) {
381
612
  return;
382
613
  }
383
614
  if (method === "GET" && (path === "/v1/models" || path === "/models")) {
384
- handleModels(res);
615
+ await handleModels(res);
385
616
  return;
386
617
  }
387
618
  if (method === "GET" && path === "/health") {
@@ -399,18 +630,18 @@ function createBridgeServer(port2) {
399
630
 
400
631
  // src/service.ts
401
632
  var import_child_process2 = require("child_process");
402
- var import_fs2 = require("fs");
403
- var import_os2 = require("os");
404
- var import_path = require("path");
405
- var HOME2 = (0, import_os2.homedir)();
633
+ var import_fs3 = require("fs");
634
+ var import_os3 = require("os");
635
+ var import_path3 = require("path");
636
+ var HOME2 = (0, import_os3.homedir)();
406
637
  var LABEL = "com.bridgerapi.server";
407
638
  function plistPath() {
408
- return (0, import_path.join)(HOME2, "Library/LaunchAgents", `${LABEL}.plist`);
639
+ return (0, import_path3.join)(HOME2, "Library/LaunchAgents", `${LABEL}.plist`);
409
640
  }
410
641
  function writePlist(port2, scriptPath, nodePath, backend2) {
411
- const logDir = (0, import_path.join)(HOME2, ".bridgerapi");
412
- (0, import_fs2.mkdirSync)(logDir, { recursive: true });
413
- (0, import_fs2.mkdirSync)((0, import_path.join)(HOME2, "Library/LaunchAgents"), { recursive: true });
642
+ const logDir = (0, import_path3.join)(HOME2, ".bridgerapi");
643
+ (0, import_fs3.mkdirSync)(logDir, { recursive: true });
644
+ (0, import_fs3.mkdirSync)((0, import_path3.join)(HOME2, "Library/LaunchAgents"), { recursive: true });
414
645
  const backendEntry = backend2 ? `
415
646
  <key>BRIDGERAPI_BACKEND</key>
416
647
  <string>${backend2}</string>` : "";
@@ -453,16 +684,16 @@ function writePlist(port2, scriptPath, nodePath, backend2) {
453
684
  <true/>
454
685
  </dict>
455
686
  </plist>`;
456
- (0, import_fs2.writeFileSync)(plistPath(), plist);
687
+ (0, import_fs3.writeFileSync)(plistPath(), plist);
457
688
  }
458
689
  function unitPath() {
459
- const configHome = process.env.XDG_CONFIG_HOME ?? (0, import_path.join)(HOME2, ".config");
460
- return (0, import_path.join)(configHome, "systemd/user/bridgerapi.service");
690
+ const configHome = process.env.XDG_CONFIG_HOME ?? (0, import_path3.join)(HOME2, ".config");
691
+ return (0, import_path3.join)(configHome, "systemd/user/bridgerapi.service");
461
692
  }
462
693
  function writeUnit(port2, scriptPath, nodePath, backend2) {
463
- const logDir = (0, import_path.join)(HOME2, ".bridgerapi");
464
- (0, import_fs2.mkdirSync)(logDir, { recursive: true });
465
- (0, import_fs2.mkdirSync)((0, import_path.join)(HOME2, ".config/systemd/user"), { recursive: true });
694
+ const logDir = (0, import_path3.join)(HOME2, ".bridgerapi");
695
+ (0, import_fs3.mkdirSync)(logDir, { recursive: true });
696
+ (0, import_fs3.mkdirSync)((0, import_path3.join)(HOME2, ".config/systemd/user"), { recursive: true });
466
697
  const backendLine = backend2 ? `
467
698
  Environment=BRIDGERAPI_BACKEND=${backend2}` : "";
468
699
  const unit = `[Unit]
@@ -483,12 +714,12 @@ StandardError=append:${logDir}/server.log
483
714
  [Install]
484
715
  WantedBy=default.target
485
716
  `;
486
- (0, import_fs2.writeFileSync)(unitPath(), unit);
717
+ (0, import_fs3.writeFileSync)(unitPath(), unit);
487
718
  }
488
719
  function installService(port2, backend2) {
489
720
  const scriptPath = process.argv[1];
490
721
  const nodePath = process.execPath;
491
- const os = (0, import_os2.platform)();
722
+ const os = (0, import_os3.platform)();
492
723
  if (os === "darwin") {
493
724
  try {
494
725
  (0, import_child_process2.execSync)(`launchctl unload "${plistPath()}" 2>/dev/null`, { stdio: "ignore" });
@@ -510,15 +741,15 @@ function installService(port2, backend2) {
510
741
  }
511
742
  }
512
743
  function uninstallService() {
513
- const os = (0, import_os2.platform)();
744
+ const os = (0, import_os3.platform)();
514
745
  if (os === "darwin") {
515
746
  const p = plistPath();
516
- if ((0, import_fs2.existsSync)(p)) {
747
+ if ((0, import_fs3.existsSync)(p)) {
517
748
  try {
518
749
  (0, import_child_process2.execSync)(`launchctl unload "${p}"`);
519
750
  } catch {
520
751
  }
521
- (0, import_fs2.unlinkSync)(p);
752
+ (0, import_fs3.unlinkSync)(p);
522
753
  console.log("\u2713 LaunchAgent removed");
523
754
  } else {
524
755
  console.log(" bridgerapi service is not installed");
@@ -529,8 +760,8 @@ function uninstallService() {
529
760
  (0, import_child_process2.execSync)("systemctl --user disable --now bridgerapi");
530
761
  } catch {
531
762
  }
532
- if ((0, import_fs2.existsSync)(p)) {
533
- (0, import_fs2.unlinkSync)(p);
763
+ if ((0, import_fs3.existsSync)(p)) {
764
+ (0, import_fs3.unlinkSync)(p);
534
765
  try {
535
766
  (0, import_child_process2.execSync)("systemctl --user daemon-reload");
536
767
  } catch {
@@ -542,7 +773,7 @@ function uninstallService() {
542
773
  }
543
774
  }
544
775
  function serviceStatus() {
545
- const os = (0, import_os2.platform)();
776
+ const os = (0, import_os3.platform)();
546
777
  try {
547
778
  if (os === "darwin") {
548
779
  const out = (0, import_child_process2.execSync)(`launchctl list ${LABEL} 2>/dev/null`, { encoding: "utf8" });
@@ -557,33 +788,13 @@ function serviceStatus() {
557
788
  return { running: false };
558
789
  }
559
790
 
560
- // src/config.ts
561
- var import_fs3 = require("fs");
562
- var import_os3 = require("os");
563
- var import_path2 = require("path");
564
- var CONFIG_DIR = (0, import_path2.join)((0, import_os3.homedir)(), ".bridgerapi");
565
- var CONFIG_FILE = (0, import_path2.join)(CONFIG_DIR, "config.json");
566
- function loadConfig() {
567
- try {
568
- if ((0, import_fs3.existsSync)(CONFIG_FILE)) {
569
- return JSON.parse((0, import_fs3.readFileSync)(CONFIG_FILE, "utf8"));
570
- }
571
- } catch {
572
- }
573
- return {};
574
- }
575
- function saveConfig(cfg) {
576
- (0, import_fs3.mkdirSync)(CONFIG_DIR, { recursive: true });
577
- (0, import_fs3.writeFileSync)(CONFIG_FILE, JSON.stringify(cfg, null, 2) + "\n");
578
- }
579
-
580
791
  // src/cli.ts
581
792
  var import_fs4 = require("fs");
582
793
  var import_os4 = require("os");
583
- var import_path3 = require("path");
794
+ var import_path4 = require("path");
584
795
  var import_readline = require("readline");
585
796
  var DEFAULT_PORT = parseInt(process.env.BRIDGERAPI_PORT ?? "8082");
586
- var LOG_DIR = (0, import_path3.join)((0, import_os4.homedir)(), ".bridgerapi");
797
+ var LOG_DIR = (0, import_path4.join)((0, import_os4.homedir)(), ".bridgerapi");
587
798
  var INSTALL_HINTS = {
588
799
  claude: "claude login (Claude Code \u2014 claude.ai/download)",
589
800
  gemini: "gemini auth (Gemini CLI \u2014 npm i -g @google/gemini-cli)",
@@ -799,7 +1010,7 @@ function cmdConfig(args) {
799
1010
  console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
800
1011
  console.log(` backend : ${cfg.backend ?? "(auto \u2014 routed by model prefix)"}`);
801
1012
  console.log(` port : ${cfg.port ?? `${DEFAULT_PORT} (default)`}`);
802
- console.log(` file : ${(0, import_path3.join)((0, import_os4.homedir)(), ".bridgerapi/config.json")}`);
1013
+ console.log(` file : ${(0, import_path4.join)((0, import_os4.homedir)(), ".bridgerapi/config.json")}`);
803
1014
  console.log();
804
1015
  console.log(" To change:");
805
1016
  console.log(` bridgerapi config set backend=claude`);
@@ -807,6 +1018,67 @@ function cmdConfig(args) {
807
1018
  console.log(` bridgerapi config reset`);
808
1019
  console.log();
809
1020
  }
1021
+ async function cmdBackendAdd() {
1022
+ console.log();
1023
+ console.log(" Add a custom backend CLI");
1024
+ console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1025
+ console.log(" This lets bridgerapi drive any AI CLI tool.");
1026
+ console.log();
1027
+ const name = await ask(" Backend name (e.g. opencode): ");
1028
+ if (!name) {
1029
+ console.log(" Cancelled.");
1030
+ return;
1031
+ }
1032
+ const bin = await ask(` Binary name in PATH [${name}]: `) || name;
1033
+ const prefixRaw = await ask(` Model prefixes that route here, comma-separated [${name}]: `);
1034
+ const prefixes = prefixRaw ? prefixRaw.split(",").map((s) => s.trim()).filter(Boolean) : [name];
1035
+ console.log();
1036
+ console.log(" How is the prompt delivered to the CLI?");
1037
+ console.log(" 1 stdin (piped to process.stdin)");
1038
+ console.log(" 2 arg (appended as last argument)");
1039
+ const modeChoice = await ask(" Choose [1/2]: ");
1040
+ const promptMode = modeChoice === "2" ? "arg" : "stdin";
1041
+ console.log();
1042
+ console.log(` Command arguments template. Use {model} as placeholder for the model name.`);
1043
+ console.log(` Example: exec --output-format text --model {model} -`);
1044
+ const argsRaw = await ask(" Args: ");
1045
+ const args = argsRaw.trim().split(/\s+/).filter(Boolean);
1046
+ const modelsCmdRaw = await ask(" Args to list models (leave blank to skip): ");
1047
+ const modelsCmd = modelsCmdRaw.trim() ? modelsCmdRaw.trim().split(/\s+/) : void 0;
1048
+ const modelsRaw = await ask(" Fallback model list, comma-separated (leave blank to skip): ");
1049
+ const models = modelsRaw.trim() ? modelsRaw.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
1050
+ const def = { name, bin, prefixes, promptMode, args, modelsCmd, models };
1051
+ const cfg = loadConfig();
1052
+ const existing = cfg.customBackends ?? [];
1053
+ const idx = existing.findIndex((b) => b.name === name);
1054
+ if (idx >= 0) existing[idx] = def;
1055
+ else existing.push(def);
1056
+ saveConfig({ ...cfg, customBackends: existing });
1057
+ console.log();
1058
+ console.log(` \u2713 ${name} backend saved.`);
1059
+ console.log(` Restart bridgerapi for it to take effect.`);
1060
+ console.log();
1061
+ console.log(" Example JSON entry in ~/.bridgerapi/config.json:");
1062
+ console.log(` ${JSON.stringify(def, null, 2).split("\n").join("\n ")}`);
1063
+ console.log();
1064
+ }
1065
+ function cmdBackendList() {
1066
+ const cfg = loadConfig();
1067
+ console.log();
1068
+ console.log(" Built-in backends:");
1069
+ for (const b of BACKENDS) {
1070
+ const ok = b.available();
1071
+ console.log(` ${ok ? "\u2713" : "\u2717"} ${b.name} [${b.prefixes.join(", ")}]`);
1072
+ }
1073
+ if (cfg.customBackends && cfg.customBackends.length > 0) {
1074
+ console.log();
1075
+ console.log(" Custom backends (from ~/.bridgerapi/config.json):");
1076
+ for (const b of cfg.customBackends) {
1077
+ console.log(` ? ${b.name} [${b.prefixes.join(", ")}] (bin: ${b.bin})`);
1078
+ }
1079
+ }
1080
+ console.log();
1081
+ }
810
1082
  async function cmdChat(model2, backendFlag) {
811
1083
  const cfg = loadConfig();
812
1084
  const activeBackend = backendFlag ?? (model2 && BACKENDS.find((b) => b.name === model2?.toLowerCase())?.name) ?? cfg.backend;
@@ -875,8 +1147,11 @@ function showHelp() {
875
1147
  bridgerapi config set backend=<b> Set default backend (claude|gemini|codex|copilot|droid)
876
1148
  bridgerapi config set port=<n> Set default port
877
1149
  bridgerapi config reset Clear saved configuration
1150
+ bridgerapi backend List all backends (built-in + custom)
1151
+ bridgerapi backend add Add a custom CLI backend interactively
878
1152
 
879
- Backends: claude, gemini, codex, copilot, droid
1153
+ Built-in backends: claude, gemini, codex, copilot, droid
1154
+ Custom backends: add any AI CLI via "bridgerapi backend add"
880
1155
  `.trim());
881
1156
  }
882
1157
  function parseArgs() {
@@ -924,6 +1199,10 @@ switch (cmd) {
924
1199
  case "config":
925
1200
  cmdConfig(rest);
926
1201
  break;
1202
+ case "backend":
1203
+ if (rest[0] === "add") cmdBackendAdd();
1204
+ else cmdBackendList();
1205
+ break;
927
1206
  case "help":
928
1207
  case "--help":
929
1208
  case "-h":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bridgerapi",
3
- "version": "1.6.0",
3
+ "version": "1.8.0",
4
4
  "description": "Turn any AI CLI (Claude Code, Gemini, Codex, GitHub Copilot) into an OpenAI-compatible API — no API keys needed",
5
5
  "keywords": [
6
6
  "claude",