bridgerapi 1.7.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 +276 -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,24 +90,131 @@ 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`);
88
218
  }
89
219
  async models() {
90
220
  return [
@@ -96,10 +226,9 @@ var ClaudeBackend = class {
96
226
  ];
97
227
  }
98
228
  async runBlocking(prompt, model2) {
99
- const bin = which("claude") || this.bin;
100
229
  let out;
101
230
  try {
102
- 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], {
103
232
  input: prompt,
104
233
  encoding: "utf8",
105
234
  timeout: 3e5
@@ -107,12 +236,15 @@ var ClaudeBackend = class {
107
236
  } catch (e) {
108
237
  throw new Error(e.stderr?.trim() || `claude exited non-zero`);
109
238
  }
110
- const data = JSON.parse(out.trim() || "{}");
111
- 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
+ }
112
245
  }
113
246
  async *stream(prompt, model2) {
114
- const bin = which("claude") || this.bin;
115
- yield* spawnStream(bin, ["-p", "--output-format", "text", "--model", model2], prompt);
247
+ yield* spawnStream(this.bin, ["-p", "--output-format", "text", "--model", model2], prompt);
116
248
  }
117
249
  };
118
250
  var GeminiBackend = class {
@@ -124,7 +256,7 @@ var GeminiBackend = class {
124
256
  return process.env.GEMINI_BIN ?? which("gemini") ?? "/opt/homebrew/bin/gemini";
125
257
  }
126
258
  available() {
127
- return Boolean(which("gemini")) || (0, import_fs.existsSync)(this.bin);
259
+ return Boolean(which("gemini")) || (0, import_fs2.existsSync)(this.bin);
128
260
  }
129
261
  async models() {
130
262
  return [
@@ -138,10 +270,9 @@ var GeminiBackend = class {
138
270
  ];
139
271
  }
140
272
  async runBlocking(prompt, model2) {
141
- const bin = which("gemini") || this.bin;
142
273
  let out;
143
274
  try {
144
- 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"], {
145
276
  input: prompt,
146
277
  encoding: "utf8",
147
278
  timeout: 3e5,
@@ -162,8 +293,11 @@ var GeminiBackend = class {
162
293
  }
163
294
  }
164
295
  async *stream(prompt, model2) {
165
- const bin = which("gemini") || this.bin;
166
- 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
+ );
167
301
  }
168
302
  };
169
303
  var CodexBackend = class {
@@ -249,9 +383,8 @@ var DroidBackend = class _DroidBackend {
249
383
  this.prefixes = ["droid", "glm", "kimi", "minimax"];
250
384
  }
251
385
  static {
252
- // Up-to-date as of March 2026 — source: droid exec --help + Factory docs
386
+ // Up-to-date as of March 2026 — source: Factory docs + droid exec --help
253
387
  this.KNOWN_MODELS = [
254
- // OpenAI via Droid
255
388
  "gpt-5-codex",
256
389
  "gpt-5.1-codex",
257
390
  "gpt-5.1-codex-max",
@@ -260,17 +393,14 @@ var DroidBackend = class _DroidBackend {
260
393
  "gpt-5.2-codex",
261
394
  "gpt-5.3-codex",
262
395
  "gpt-5-2025-08-07",
263
- // Anthropic via Droid
264
396
  "claude-opus-4-6",
265
397
  "claude-opus-4-6-fast",
266
398
  "claude-opus-4-1-20250805",
267
399
  "claude-sonnet-4-5-20250929",
268
400
  "claude-haiku-4-5-20251001",
269
- // Google via Droid
270
401
  "gemini-3.1-pro-preview",
271
402
  "gemini-3-pro-preview",
272
403
  "gemini-3-flash-preview",
273
- // Droid-native (GLM / Kimi / MiniMax)
274
404
  "glm-4.6",
275
405
  "glm-4.7",
276
406
  "glm-5",
@@ -282,7 +412,7 @@ var DroidBackend = class _DroidBackend {
282
412
  return process.env.DROID_BIN ?? which("droid") ?? `${HOME}/.local/bin/droid`;
283
413
  }
284
414
  available() {
285
- return (0, import_fs.existsSync)(this.bin) || Boolean(which("droid"));
415
+ return Boolean(which("droid")) || (0, import_fs2.existsSync)(`${HOME}/.local/bin/droid`);
286
416
  }
287
417
  async models() {
288
418
  try {
@@ -291,44 +421,49 @@ var DroidBackend = class _DroidBackend {
291
421
  timeout: 5e3,
292
422
  stdio: ["ignore", "pipe", "pipe"]
293
423
  });
294
- const MODEL_RE = /\b([a-z][a-z0-9]+(?:[.\-][a-z0-9]+){1,})\b/g;
295
- const found = /* @__PURE__ */ new Set();
296
- for (const [, id] of help.matchAll(MODEL_RE)) {
297
- if (id.length >= 5 && !["help", "exec", "droid", "text", "json", "output", "format", "model", "usage"].includes(id)) {
298
- found.add(id);
299
- }
300
- }
301
- if (found.size > 0) return [...found];
424
+ const found = extractModelIds(help);
425
+ if (found.length > 0) return found;
302
426
  } catch {
303
427
  }
304
428
  return _DroidBackend.KNOWN_MODELS;
305
429
  }
306
430
  async runBlocking(prompt, model2) {
307
- const bin = which("droid") || this.bin;
308
431
  let out;
309
432
  try {
310
- out = (0, import_child_process.execFileSync)(bin, ["exec", "--output-format", "text", "--model", model2, "-"], {
311
- input: prompt,
312
- encoding: "utf8",
313
- timeout: 3e5
314
- });
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
+ );
315
438
  } catch (e) {
316
439
  throw new Error(e.stderr?.trim() || `droid exited non-zero`);
317
440
  }
318
441
  return [out.trim(), null];
319
442
  }
320
443
  async *stream(prompt, model2) {
321
- const bin = which("droid") || this.bin;
322
- 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
+ );
323
449
  }
324
450
  };
325
- var BACKENDS = [
451
+ var BUILTIN = [
326
452
  new ClaudeBackend(),
327
453
  new GeminiBackend(),
328
454
  new CodexBackend(),
329
455
  new CopilotBackend(),
330
456
  new DroidBackend()
331
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()];
332
467
  function pickBackend(model2) {
333
468
  const override = process.env.BRIDGERAPI_BACKEND?.toLowerCase();
334
469
  if (override) {
@@ -448,7 +583,11 @@ async function handleChat(req, res) {
448
583
  res.write(chunk(id, ts, model2, { content: raw.toString("utf8") }));
449
584
  }
450
585
  } catch (err) {
451
- 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}` }));
452
591
  }
453
592
  res.write(chunk(id, ts, model2, {}, "stop"));
454
593
  res.write("data: [DONE]\n\n");
@@ -491,18 +630,18 @@ function createBridgeServer(port2) {
491
630
 
492
631
  // src/service.ts
493
632
  var import_child_process2 = require("child_process");
494
- var import_fs2 = require("fs");
495
- var import_os2 = require("os");
496
- var import_path = require("path");
497
- 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)();
498
637
  var LABEL = "com.bridgerapi.server";
499
638
  function plistPath() {
500
- return (0, import_path.join)(HOME2, "Library/LaunchAgents", `${LABEL}.plist`);
639
+ return (0, import_path3.join)(HOME2, "Library/LaunchAgents", `${LABEL}.plist`);
501
640
  }
502
641
  function writePlist(port2, scriptPath, nodePath, backend2) {
503
- const logDir = (0, import_path.join)(HOME2, ".bridgerapi");
504
- (0, import_fs2.mkdirSync)(logDir, { recursive: true });
505
- (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 });
506
645
  const backendEntry = backend2 ? `
507
646
  <key>BRIDGERAPI_BACKEND</key>
508
647
  <string>${backend2}</string>` : "";
@@ -545,16 +684,16 @@ function writePlist(port2, scriptPath, nodePath, backend2) {
545
684
  <true/>
546
685
  </dict>
547
686
  </plist>`;
548
- (0, import_fs2.writeFileSync)(plistPath(), plist);
687
+ (0, import_fs3.writeFileSync)(plistPath(), plist);
549
688
  }
550
689
  function unitPath() {
551
- const configHome = process.env.XDG_CONFIG_HOME ?? (0, import_path.join)(HOME2, ".config");
552
- 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");
553
692
  }
554
693
  function writeUnit(port2, scriptPath, nodePath, backend2) {
555
- const logDir = (0, import_path.join)(HOME2, ".bridgerapi");
556
- (0, import_fs2.mkdirSync)(logDir, { recursive: true });
557
- (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 });
558
697
  const backendLine = backend2 ? `
559
698
  Environment=BRIDGERAPI_BACKEND=${backend2}` : "";
560
699
  const unit = `[Unit]
@@ -575,12 +714,12 @@ StandardError=append:${logDir}/server.log
575
714
  [Install]
576
715
  WantedBy=default.target
577
716
  `;
578
- (0, import_fs2.writeFileSync)(unitPath(), unit);
717
+ (0, import_fs3.writeFileSync)(unitPath(), unit);
579
718
  }
580
719
  function installService(port2, backend2) {
581
720
  const scriptPath = process.argv[1];
582
721
  const nodePath = process.execPath;
583
- const os = (0, import_os2.platform)();
722
+ const os = (0, import_os3.platform)();
584
723
  if (os === "darwin") {
585
724
  try {
586
725
  (0, import_child_process2.execSync)(`launchctl unload "${plistPath()}" 2>/dev/null`, { stdio: "ignore" });
@@ -602,15 +741,15 @@ function installService(port2, backend2) {
602
741
  }
603
742
  }
604
743
  function uninstallService() {
605
- const os = (0, import_os2.platform)();
744
+ const os = (0, import_os3.platform)();
606
745
  if (os === "darwin") {
607
746
  const p = plistPath();
608
- if ((0, import_fs2.existsSync)(p)) {
747
+ if ((0, import_fs3.existsSync)(p)) {
609
748
  try {
610
749
  (0, import_child_process2.execSync)(`launchctl unload "${p}"`);
611
750
  } catch {
612
751
  }
613
- (0, import_fs2.unlinkSync)(p);
752
+ (0, import_fs3.unlinkSync)(p);
614
753
  console.log("\u2713 LaunchAgent removed");
615
754
  } else {
616
755
  console.log(" bridgerapi service is not installed");
@@ -621,8 +760,8 @@ function uninstallService() {
621
760
  (0, import_child_process2.execSync)("systemctl --user disable --now bridgerapi");
622
761
  } catch {
623
762
  }
624
- if ((0, import_fs2.existsSync)(p)) {
625
- (0, import_fs2.unlinkSync)(p);
763
+ if ((0, import_fs3.existsSync)(p)) {
764
+ (0, import_fs3.unlinkSync)(p);
626
765
  try {
627
766
  (0, import_child_process2.execSync)("systemctl --user daemon-reload");
628
767
  } catch {
@@ -634,7 +773,7 @@ function uninstallService() {
634
773
  }
635
774
  }
636
775
  function serviceStatus() {
637
- const os = (0, import_os2.platform)();
776
+ const os = (0, import_os3.platform)();
638
777
  try {
639
778
  if (os === "darwin") {
640
779
  const out = (0, import_child_process2.execSync)(`launchctl list ${LABEL} 2>/dev/null`, { encoding: "utf8" });
@@ -649,33 +788,13 @@ function serviceStatus() {
649
788
  return { running: false };
650
789
  }
651
790
 
652
- // src/config.ts
653
- var import_fs3 = require("fs");
654
- var import_os3 = require("os");
655
- var import_path2 = require("path");
656
- var CONFIG_DIR = (0, import_path2.join)((0, import_os3.homedir)(), ".bridgerapi");
657
- var CONFIG_FILE = (0, import_path2.join)(CONFIG_DIR, "config.json");
658
- function loadConfig() {
659
- try {
660
- if ((0, import_fs3.existsSync)(CONFIG_FILE)) {
661
- return JSON.parse((0, import_fs3.readFileSync)(CONFIG_FILE, "utf8"));
662
- }
663
- } catch {
664
- }
665
- return {};
666
- }
667
- function saveConfig(cfg) {
668
- (0, import_fs3.mkdirSync)(CONFIG_DIR, { recursive: true });
669
- (0, import_fs3.writeFileSync)(CONFIG_FILE, JSON.stringify(cfg, null, 2) + "\n");
670
- }
671
-
672
791
  // src/cli.ts
673
792
  var import_fs4 = require("fs");
674
793
  var import_os4 = require("os");
675
- var import_path3 = require("path");
794
+ var import_path4 = require("path");
676
795
  var import_readline = require("readline");
677
796
  var DEFAULT_PORT = parseInt(process.env.BRIDGERAPI_PORT ?? "8082");
678
- 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");
679
798
  var INSTALL_HINTS = {
680
799
  claude: "claude login (Claude Code \u2014 claude.ai/download)",
681
800
  gemini: "gemini auth (Gemini CLI \u2014 npm i -g @google/gemini-cli)",
@@ -891,7 +1010,7 @@ function cmdConfig(args) {
891
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");
892
1011
  console.log(` backend : ${cfg.backend ?? "(auto \u2014 routed by model prefix)"}`);
893
1012
  console.log(` port : ${cfg.port ?? `${DEFAULT_PORT} (default)`}`);
894
- 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")}`);
895
1014
  console.log();
896
1015
  console.log(" To change:");
897
1016
  console.log(` bridgerapi config set backend=claude`);
@@ -899,6 +1018,67 @@ function cmdConfig(args) {
899
1018
  console.log(` bridgerapi config reset`);
900
1019
  console.log();
901
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
+ }
902
1082
  async function cmdChat(model2, backendFlag) {
903
1083
  const cfg = loadConfig();
904
1084
  const activeBackend = backendFlag ?? (model2 && BACKENDS.find((b) => b.name === model2?.toLowerCase())?.name) ?? cfg.backend;
@@ -967,8 +1147,11 @@ function showHelp() {
967
1147
  bridgerapi config set backend=<b> Set default backend (claude|gemini|codex|copilot|droid)
968
1148
  bridgerapi config set port=<n> Set default port
969
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
970
1152
 
971
- 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"
972
1155
  `.trim());
973
1156
  }
974
1157
  function parseArgs() {
@@ -1016,6 +1199,10 @@ switch (cmd) {
1016
1199
  case "config":
1017
1200
  cmdConfig(rest);
1018
1201
  break;
1202
+ case "backend":
1203
+ if (rest[0] === "add") cmdBackendAdd();
1204
+ else cmdBackendList();
1205
+ break;
1019
1206
  case "help":
1020
1207
  case "--help":
1021
1208
  case "-h":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bridgerapi",
3
- "version": "1.7.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",