bridgerapi 1.7.0 → 1.9.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 +328 -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,26 +90,161 @@ 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
+ "output-format",
125
+ "approval-mode",
126
+ "list-models",
127
+ "api-key",
128
+ "base-url"
129
+ ]);
130
+ function extractModelIds(text) {
131
+ const seen = /* @__PURE__ */ new Set();
132
+ const re = /\b([a-z][a-z0-9]*(?:[.\-][a-z0-9]+)+)\b/g;
133
+ for (const [, id] of text.matchAll(re)) {
134
+ if (id.length >= 5 && !SKIP_TOKENS.has(id)) seen.add(id);
135
+ }
136
+ return [...seen];
137
+ }
138
+ async function tryDiscover(bin, strategies) {
139
+ for (const args of strategies) {
140
+ try {
141
+ const out = (0, import_child_process.execFileSync)(bin, args, {
142
+ encoding: "utf8",
143
+ timeout: 5e3,
144
+ stdio: ["ignore", "pipe", "pipe"]
145
+ });
146
+ const found = extractModelIds(out);
147
+ if (found.length > 0) return found;
148
+ } catch {
149
+ }
150
+ }
151
+ return [];
152
+ }
70
153
  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"]
154
+ const proc = (0, import_child_process.spawn)(cmd2, args, { env: process.env, stdio: ["pipe", "pipe", "pipe"] });
155
+ if (stdin !== void 0) proc.stdin.end(stdin);
156
+ const stderrBufs = [];
157
+ proc.stderr.on("data", (c) => stderrBufs.push(c));
158
+ let hasOutput = false;
159
+ try {
160
+ for await (const chunk2 of proc.stdout) {
161
+ hasOutput = true;
162
+ yield chunk2;
163
+ }
164
+ } finally {
165
+ if (proc.exitCode === null) proc.kill();
166
+ }
167
+ const exitCode = await new Promise((resolve) => {
168
+ proc.exitCode !== null ? resolve(proc.exitCode) : proc.on("close", (c) => resolve(c ?? 0));
74
169
  });
75
- if (stdin) proc.stdin.end(stdin);
76
- for await (const chunk2 of proc.stdout) yield chunk2;
170
+ if (!hasOutput && exitCode !== 0) {
171
+ const stderr = Buffer.concat(stderrBufs).toString().trim();
172
+ throw new Error(stderr || `${(0, import_path2.basename)(cmd2)} exited with code ${exitCode}`);
173
+ }
77
174
  }
175
+ var GenericBackend = class {
176
+ constructor(def) {
177
+ this.name = def.name;
178
+ this.prefixes = def.prefixes;
179
+ this.def = def;
180
+ }
181
+ get bin() {
182
+ return which(this.def.bin) || `${HOME}/.local/bin/${this.def.bin}`;
183
+ }
184
+ available() {
185
+ return Boolean(which(this.def.bin)) || (0, import_fs2.existsSync)(`${HOME}/.local/bin/${this.def.bin}`);
186
+ }
187
+ async models() {
188
+ if (this.def.modelsCmd) {
189
+ try {
190
+ const out = (0, import_child_process.execFileSync)(this.bin, this.def.modelsCmd, {
191
+ encoding: "utf8",
192
+ timeout: 5e3,
193
+ stdio: ["ignore", "pipe", "pipe"]
194
+ });
195
+ const found = extractModelIds(out);
196
+ if (found.length > 0) return found;
197
+ } catch {
198
+ }
199
+ }
200
+ return this.def.models ?? [];
201
+ }
202
+ buildArgs(model2) {
203
+ return this.def.args.map((a) => a === "{model}" ? model2 : a);
204
+ }
205
+ async runBlocking(prompt, model2) {
206
+ const args = this.buildArgs(model2);
207
+ let out;
208
+ try {
209
+ if (this.def.promptMode === "stdin") {
210
+ out = (0, import_child_process.execFileSync)(this.bin, args, { input: prompt, encoding: "utf8", timeout: 3e5 });
211
+ } else {
212
+ out = (0, import_child_process.execFileSync)(this.bin, [...args, prompt], { encoding: "utf8", timeout: 3e5 });
213
+ }
214
+ } catch (e) {
215
+ throw new Error(e.stderr?.trim() || `${this.def.bin} exited non-zero`);
216
+ }
217
+ return [out.trim(), null];
218
+ }
219
+ async *stream(prompt, model2) {
220
+ const args = this.buildArgs(model2);
221
+ if (this.def.promptMode === "stdin") {
222
+ yield* spawnStream(this.bin, args, prompt);
223
+ } else {
224
+ yield* spawnStream(this.bin, [...args, prompt]);
225
+ }
226
+ }
227
+ };
78
228
  var ClaudeBackend = class {
79
229
  constructor() {
80
230
  this.name = "claude";
81
231
  this.prefixes = ["claude"];
82
232
  }
83
233
  get bin() {
84
- return process.env.CLAUDE_BIN ?? `${HOME}/.local/bin/claude`;
234
+ return process.env.CLAUDE_BIN ?? which("claude") ?? `${HOME}/.local/bin/claude`;
85
235
  }
86
236
  available() {
87
- return (0, import_fs.existsSync)(this.bin) || Boolean(which("claude"));
237
+ return Boolean(which("claude")) || (0, import_fs2.existsSync)(`${HOME}/.local/bin/claude`);
88
238
  }
89
239
  async models() {
240
+ const bin = which("claude") || this.bin;
241
+ const found = await tryDiscover(bin, [
242
+ ["models"],
243
+ ["models", "list"],
244
+ ["model", "list"],
245
+ ["--list-models"]
246
+ ]);
247
+ if (found.length > 0) return found;
90
248
  return [
91
249
  "claude-opus-4-6",
92
250
  "claude-opus-4-6-fast",
@@ -96,10 +254,9 @@ var ClaudeBackend = class {
96
254
  ];
97
255
  }
98
256
  async runBlocking(prompt, model2) {
99
- const bin = which("claude") || this.bin;
100
257
  let out;
101
258
  try {
102
- out = (0, import_child_process.execFileSync)(bin, ["-p", "--output-format", "json", "--model", model2], {
259
+ out = (0, import_child_process.execFileSync)(this.bin, ["-p", "--output-format", "json", "--model", model2], {
103
260
  input: prompt,
104
261
  encoding: "utf8",
105
262
  timeout: 3e5
@@ -107,12 +264,15 @@ var ClaudeBackend = class {
107
264
  } catch (e) {
108
265
  throw new Error(e.stderr?.trim() || `claude exited non-zero`);
109
266
  }
110
- const data = JSON.parse(out.trim() || "{}");
111
- return [data.result ?? "", data.usage ?? null];
267
+ try {
268
+ const data = JSON.parse(out.trim() || "{}");
269
+ return [data.result ?? "", data.usage ?? null];
270
+ } catch {
271
+ return [out.trim(), null];
272
+ }
112
273
  }
113
274
  async *stream(prompt, model2) {
114
- const bin = which("claude") || this.bin;
115
- yield* spawnStream(bin, ["-p", "--output-format", "text", "--model", model2], prompt);
275
+ yield* spawnStream(this.bin, ["-p", "--output-format", "text", "--model", model2], prompt);
116
276
  }
117
277
  };
118
278
  var GeminiBackend = class {
@@ -124,9 +284,17 @@ var GeminiBackend = class {
124
284
  return process.env.GEMINI_BIN ?? which("gemini") ?? "/opt/homebrew/bin/gemini";
125
285
  }
126
286
  available() {
127
- return Boolean(which("gemini")) || (0, import_fs.existsSync)(this.bin);
287
+ return Boolean(which("gemini")) || (0, import_fs2.existsSync)(this.bin);
128
288
  }
129
289
  async models() {
290
+ const bin = which("gemini") || this.bin;
291
+ const found = await tryDiscover(bin, [
292
+ ["models"],
293
+ ["models", "list"],
294
+ ["--list-models"],
295
+ ["list-models"]
296
+ ]);
297
+ if (found.length > 0) return found;
130
298
  return [
131
299
  "gemini-3.1-pro-preview",
132
300
  "gemini-3-pro-preview",
@@ -138,10 +306,9 @@ var GeminiBackend = class {
138
306
  ];
139
307
  }
140
308
  async runBlocking(prompt, model2) {
141
- const bin = which("gemini") || this.bin;
142
309
  let out;
143
310
  try {
144
- out = (0, import_child_process.execFileSync)(bin, ["--output-format", "json", "--model", model2, "--approval-mode", "yolo"], {
311
+ out = (0, import_child_process.execFileSync)(this.bin, ["--output-format", "json", "--model", model2, "--approval-mode", "yolo"], {
145
312
  input: prompt,
146
313
  encoding: "utf8",
147
314
  timeout: 3e5,
@@ -162,8 +329,11 @@ var GeminiBackend = class {
162
329
  }
163
330
  }
164
331
  async *stream(prompt, model2) {
165
- const bin = which("gemini") || this.bin;
166
- yield* spawnStream(bin, ["--output-format", "text", "--model", model2, "--approval-mode", "yolo"], prompt);
332
+ yield* spawnStream(
333
+ this.bin,
334
+ ["--output-format", "text", "--model", model2, "--approval-mode", "yolo"],
335
+ prompt
336
+ );
167
337
  }
168
338
  };
169
339
  var CodexBackend = class {
@@ -178,6 +348,13 @@ var CodexBackend = class {
178
348
  return Boolean(which("codex"));
179
349
  }
180
350
  async models() {
351
+ const found = await tryDiscover(this.bin, [
352
+ ["models"],
353
+ ["models", "list"],
354
+ ["--list-models"],
355
+ ["list-models"]
356
+ ]);
357
+ if (found.length > 0) return found;
181
358
  return [
182
359
  "gpt-5-codex",
183
360
  "gpt-5.1-codex",
@@ -225,6 +402,12 @@ var CopilotBackend = class {
225
402
  }
226
403
  }
227
404
  async models() {
405
+ const found = await tryDiscover(this.bin, [
406
+ ["copilot", "models"],
407
+ ["copilot", "models", "list"],
408
+ ["copilot", "--list-models"]
409
+ ]);
410
+ if (found.length > 0) return found;
228
411
  return ["copilot"];
229
412
  }
230
413
  async runBlocking(prompt, model2) {
@@ -249,9 +432,8 @@ var DroidBackend = class _DroidBackend {
249
432
  this.prefixes = ["droid", "glm", "kimi", "minimax"];
250
433
  }
251
434
  static {
252
- // Up-to-date as of March 2026 — source: droid exec --help + Factory docs
435
+ // Up-to-date as of March 2026 — source: Factory docs + droid exec --help
253
436
  this.KNOWN_MODELS = [
254
- // OpenAI via Droid
255
437
  "gpt-5-codex",
256
438
  "gpt-5.1-codex",
257
439
  "gpt-5.1-codex-max",
@@ -260,17 +442,14 @@ var DroidBackend = class _DroidBackend {
260
442
  "gpt-5.2-codex",
261
443
  "gpt-5.3-codex",
262
444
  "gpt-5-2025-08-07",
263
- // Anthropic via Droid
264
445
  "claude-opus-4-6",
265
446
  "claude-opus-4-6-fast",
266
447
  "claude-opus-4-1-20250805",
267
448
  "claude-sonnet-4-5-20250929",
268
449
  "claude-haiku-4-5-20251001",
269
- // Google via Droid
270
450
  "gemini-3.1-pro-preview",
271
451
  "gemini-3-pro-preview",
272
452
  "gemini-3-flash-preview",
273
- // Droid-native (GLM / Kimi / MiniMax)
274
453
  "glm-4.6",
275
454
  "glm-4.7",
276
455
  "glm-5",
@@ -282,7 +461,7 @@ var DroidBackend = class _DroidBackend {
282
461
  return process.env.DROID_BIN ?? which("droid") ?? `${HOME}/.local/bin/droid`;
283
462
  }
284
463
  available() {
285
- return (0, import_fs.existsSync)(this.bin) || Boolean(which("droid"));
464
+ return Boolean(which("droid")) || (0, import_fs2.existsSync)(`${HOME}/.local/bin/droid`);
286
465
  }
287
466
  async models() {
288
467
  try {
@@ -291,44 +470,49 @@ var DroidBackend = class _DroidBackend {
291
470
  timeout: 5e3,
292
471
  stdio: ["ignore", "pipe", "pipe"]
293
472
  });
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];
473
+ const found = extractModelIds(help);
474
+ if (found.length > 0) return found;
302
475
  } catch {
303
476
  }
304
477
  return _DroidBackend.KNOWN_MODELS;
305
478
  }
306
479
  async runBlocking(prompt, model2) {
307
- const bin = which("droid") || this.bin;
308
480
  let out;
309
481
  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
- });
482
+ out = (0, import_child_process.execFileSync)(
483
+ which("droid") || this.bin,
484
+ ["exec", "--output-format", "text", "--model", model2, "-"],
485
+ { input: prompt, encoding: "utf8", timeout: 3e5 }
486
+ );
315
487
  } catch (e) {
316
488
  throw new Error(e.stderr?.trim() || `droid exited non-zero`);
317
489
  }
318
490
  return [out.trim(), null];
319
491
  }
320
492
  async *stream(prompt, model2) {
321
- const bin = which("droid") || this.bin;
322
- yield* spawnStream(bin, ["exec", "--output-format", "text", "--model", model2, "-"], prompt);
493
+ yield* spawnStream(
494
+ which("droid") || this.bin,
495
+ ["exec", "--output-format", "text", "--model", model2, "-"],
496
+ prompt
497
+ );
323
498
  }
324
499
  };
325
- var BACKENDS = [
500
+ var BUILTIN = [
326
501
  new ClaudeBackend(),
327
502
  new GeminiBackend(),
328
503
  new CodexBackend(),
329
504
  new CopilotBackend(),
330
505
  new DroidBackend()
331
506
  ];
507
+ function loadUserBackends() {
508
+ try {
509
+ const cfg = loadConfig();
510
+ return (cfg.customBackends ?? []).map((def) => new GenericBackend(def));
511
+ } catch {
512
+ return [];
513
+ }
514
+ }
515
+ var BACKENDS = [...BUILTIN, ...loadUserBackends()];
332
516
  function pickBackend(model2) {
333
517
  const override = process.env.BRIDGERAPI_BACKEND?.toLowerCase();
334
518
  if (override) {
@@ -448,7 +632,11 @@ async function handleChat(req, res) {
448
632
  res.write(chunk(id, ts, model2, { content: raw.toString("utf8") }));
449
633
  }
450
634
  } catch (err) {
451
- console.error(` stream error: ${err.message}`);
635
+ const msg = err.message ?? String(err);
636
+ console.error(` stream error [${backend2.name}]: ${msg}`);
637
+ res.write(chunk(id, ts, model2, { content: `
638
+
639
+ \u26A0\uFE0F ${backend2.name} error: ${msg}` }));
452
640
  }
453
641
  res.write(chunk(id, ts, model2, {}, "stop"));
454
642
  res.write("data: [DONE]\n\n");
@@ -491,18 +679,18 @@ function createBridgeServer(port2) {
491
679
 
492
680
  // src/service.ts
493
681
  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)();
682
+ var import_fs3 = require("fs");
683
+ var import_os3 = require("os");
684
+ var import_path3 = require("path");
685
+ var HOME2 = (0, import_os3.homedir)();
498
686
  var LABEL = "com.bridgerapi.server";
499
687
  function plistPath() {
500
- return (0, import_path.join)(HOME2, "Library/LaunchAgents", `${LABEL}.plist`);
688
+ return (0, import_path3.join)(HOME2, "Library/LaunchAgents", `${LABEL}.plist`);
501
689
  }
502
690
  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 });
691
+ const logDir = (0, import_path3.join)(HOME2, ".bridgerapi");
692
+ (0, import_fs3.mkdirSync)(logDir, { recursive: true });
693
+ (0, import_fs3.mkdirSync)((0, import_path3.join)(HOME2, "Library/LaunchAgents"), { recursive: true });
506
694
  const backendEntry = backend2 ? `
507
695
  <key>BRIDGERAPI_BACKEND</key>
508
696
  <string>${backend2}</string>` : "";
@@ -545,16 +733,16 @@ function writePlist(port2, scriptPath, nodePath, backend2) {
545
733
  <true/>
546
734
  </dict>
547
735
  </plist>`;
548
- (0, import_fs2.writeFileSync)(plistPath(), plist);
736
+ (0, import_fs3.writeFileSync)(plistPath(), plist);
549
737
  }
550
738
  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");
739
+ const configHome = process.env.XDG_CONFIG_HOME ?? (0, import_path3.join)(HOME2, ".config");
740
+ return (0, import_path3.join)(configHome, "systemd/user/bridgerapi.service");
553
741
  }
554
742
  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 });
743
+ const logDir = (0, import_path3.join)(HOME2, ".bridgerapi");
744
+ (0, import_fs3.mkdirSync)(logDir, { recursive: true });
745
+ (0, import_fs3.mkdirSync)((0, import_path3.join)(HOME2, ".config/systemd/user"), { recursive: true });
558
746
  const backendLine = backend2 ? `
559
747
  Environment=BRIDGERAPI_BACKEND=${backend2}` : "";
560
748
  const unit = `[Unit]
@@ -575,12 +763,12 @@ StandardError=append:${logDir}/server.log
575
763
  [Install]
576
764
  WantedBy=default.target
577
765
  `;
578
- (0, import_fs2.writeFileSync)(unitPath(), unit);
766
+ (0, import_fs3.writeFileSync)(unitPath(), unit);
579
767
  }
580
768
  function installService(port2, backend2) {
581
769
  const scriptPath = process.argv[1];
582
770
  const nodePath = process.execPath;
583
- const os = (0, import_os2.platform)();
771
+ const os = (0, import_os3.platform)();
584
772
  if (os === "darwin") {
585
773
  try {
586
774
  (0, import_child_process2.execSync)(`launchctl unload "${plistPath()}" 2>/dev/null`, { stdio: "ignore" });
@@ -602,15 +790,15 @@ function installService(port2, backend2) {
602
790
  }
603
791
  }
604
792
  function uninstallService() {
605
- const os = (0, import_os2.platform)();
793
+ const os = (0, import_os3.platform)();
606
794
  if (os === "darwin") {
607
795
  const p = plistPath();
608
- if ((0, import_fs2.existsSync)(p)) {
796
+ if ((0, import_fs3.existsSync)(p)) {
609
797
  try {
610
798
  (0, import_child_process2.execSync)(`launchctl unload "${p}"`);
611
799
  } catch {
612
800
  }
613
- (0, import_fs2.unlinkSync)(p);
801
+ (0, import_fs3.unlinkSync)(p);
614
802
  console.log("\u2713 LaunchAgent removed");
615
803
  } else {
616
804
  console.log(" bridgerapi service is not installed");
@@ -621,8 +809,8 @@ function uninstallService() {
621
809
  (0, import_child_process2.execSync)("systemctl --user disable --now bridgerapi");
622
810
  } catch {
623
811
  }
624
- if ((0, import_fs2.existsSync)(p)) {
625
- (0, import_fs2.unlinkSync)(p);
812
+ if ((0, import_fs3.existsSync)(p)) {
813
+ (0, import_fs3.unlinkSync)(p);
626
814
  try {
627
815
  (0, import_child_process2.execSync)("systemctl --user daemon-reload");
628
816
  } catch {
@@ -634,7 +822,7 @@ function uninstallService() {
634
822
  }
635
823
  }
636
824
  function serviceStatus() {
637
- const os = (0, import_os2.platform)();
825
+ const os = (0, import_os3.platform)();
638
826
  try {
639
827
  if (os === "darwin") {
640
828
  const out = (0, import_child_process2.execSync)(`launchctl list ${LABEL} 2>/dev/null`, { encoding: "utf8" });
@@ -649,33 +837,13 @@ function serviceStatus() {
649
837
  return { running: false };
650
838
  }
651
839
 
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
840
  // src/cli.ts
673
841
  var import_fs4 = require("fs");
674
842
  var import_os4 = require("os");
675
- var import_path3 = require("path");
843
+ var import_path4 = require("path");
676
844
  var import_readline = require("readline");
677
845
  var DEFAULT_PORT = parseInt(process.env.BRIDGERAPI_PORT ?? "8082");
678
- var LOG_DIR = (0, import_path3.join)((0, import_os4.homedir)(), ".bridgerapi");
846
+ var LOG_DIR = (0, import_path4.join)((0, import_os4.homedir)(), ".bridgerapi");
679
847
  var INSTALL_HINTS = {
680
848
  claude: "claude login (Claude Code \u2014 claude.ai/download)",
681
849
  gemini: "gemini auth (Gemini CLI \u2014 npm i -g @google/gemini-cli)",
@@ -891,7 +1059,7 @@ function cmdConfig(args) {
891
1059
  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
1060
  console.log(` backend : ${cfg.backend ?? "(auto \u2014 routed by model prefix)"}`);
893
1061
  console.log(` port : ${cfg.port ?? `${DEFAULT_PORT} (default)`}`);
894
- console.log(` file : ${(0, import_path3.join)((0, import_os4.homedir)(), ".bridgerapi/config.json")}`);
1062
+ console.log(` file : ${(0, import_path4.join)((0, import_os4.homedir)(), ".bridgerapi/config.json")}`);
895
1063
  console.log();
896
1064
  console.log(" To change:");
897
1065
  console.log(` bridgerapi config set backend=claude`);
@@ -899,6 +1067,70 @@ function cmdConfig(args) {
899
1067
  console.log(` bridgerapi config reset`);
900
1068
  console.log();
901
1069
  }
1070
+ async function cmdBackendAdd() {
1071
+ console.log();
1072
+ console.log(" Add a custom backend CLI");
1073
+ 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");
1074
+ console.log(" This lets bridgerapi drive any AI CLI tool.");
1075
+ console.log();
1076
+ const name = await ask(" Backend name (e.g. opencode): ");
1077
+ if (!name) {
1078
+ console.log(" Cancelled.");
1079
+ return;
1080
+ }
1081
+ const bin = await ask(` Binary name in PATH [${name}]: `) || name;
1082
+ const prefixRaw = await ask(` Model prefixes that route here, comma-separated [${name}]: `);
1083
+ const prefixes = prefixRaw ? prefixRaw.split(",").map((s) => s.trim()).filter(Boolean) : [name];
1084
+ console.log();
1085
+ console.log(" How is the prompt delivered to the CLI?");
1086
+ console.log(" 1 stdin (piped to process.stdin)");
1087
+ console.log(" 2 arg (appended as last argument)");
1088
+ const modeChoice = await ask(" Choose [1/2]: ");
1089
+ const promptMode = modeChoice === "2" ? "arg" : "stdin";
1090
+ console.log();
1091
+ console.log(` Command arguments template. Use {model} as placeholder for the model name.`);
1092
+ console.log(` Example: exec --output-format text --model {model} -`);
1093
+ const argsRaw = await ask(" Args: ");
1094
+ const args = argsRaw.trim().split(/\s+/).filter(Boolean);
1095
+ const modelsCmdRaw = await ask(" Args to list models (leave blank to skip): ");
1096
+ const modelsCmd = modelsCmdRaw.trim() ? modelsCmdRaw.trim().split(/\s+/) : void 0;
1097
+ const modelsRaw = await ask(" Fallback model list, comma-separated (leave blank to skip): ");
1098
+ const models = modelsRaw.trim() ? modelsRaw.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
1099
+ const def = { name, bin, prefixes, promptMode, args, modelsCmd, models };
1100
+ const cfg = loadConfig();
1101
+ const existing = cfg.customBackends ?? [];
1102
+ const idx = existing.findIndex((b) => b.name === name);
1103
+ if (idx >= 0) existing[idx] = def;
1104
+ else existing.push(def);
1105
+ saveConfig({ ...cfg, customBackends: existing });
1106
+ console.log();
1107
+ console.log(` \u2713 ${name} backend saved.`);
1108
+ console.log(` Restart bridgerapi for it to take effect.`);
1109
+ console.log();
1110
+ console.log(" Example JSON entry in ~/.bridgerapi/config.json:");
1111
+ console.log(` ${JSON.stringify(def, null, 2).split("\n").join("\n ")}`);
1112
+ console.log();
1113
+ }
1114
+ async function cmdBackendList() {
1115
+ console.log();
1116
+ console.log(" Backends:\n");
1117
+ for (const b of BACKENDS) {
1118
+ if (!b.available()) {
1119
+ const hint = INSTALL_HINTS[b.name] ?? "not installed";
1120
+ console.log(` \u2717 ${b.name}`);
1121
+ console.log(` \u2192 ${hint}
1122
+ `);
1123
+ continue;
1124
+ }
1125
+ process.stdout.write(` \u2713 ${b.name} (discovering models\u2026)\r`);
1126
+ const modelList = await b.models();
1127
+ const preview = modelList.slice(0, 6).join(" ");
1128
+ const extra = modelList.length > 6 ? ` +${modelList.length - 6} more` : "";
1129
+ console.log(` \u2713 ${b.name} `);
1130
+ console.log(` ${preview}${extra}
1131
+ `);
1132
+ }
1133
+ }
902
1134
  async function cmdChat(model2, backendFlag) {
903
1135
  const cfg = loadConfig();
904
1136
  const activeBackend = backendFlag ?? (model2 && BACKENDS.find((b) => b.name === model2?.toLowerCase())?.name) ?? cfg.backend;
@@ -967,8 +1199,11 @@ function showHelp() {
967
1199
  bridgerapi config set backend=<b> Set default backend (claude|gemini|codex|copilot|droid)
968
1200
  bridgerapi config set port=<n> Set default port
969
1201
  bridgerapi config reset Clear saved configuration
1202
+ bridgerapi backend List all backends (built-in + custom)
1203
+ bridgerapi backend add Add a custom CLI backend interactively
970
1204
 
971
- Backends: claude, gemini, codex, copilot, droid
1205
+ Built-in backends: claude, gemini, codex, copilot, droid
1206
+ Custom backends: add any AI CLI via "bridgerapi backend add"
972
1207
  `.trim());
973
1208
  }
974
1209
  function parseArgs() {
@@ -1016,6 +1251,10 @@ switch (cmd) {
1016
1251
  case "config":
1017
1252
  cmdConfig(rest);
1018
1253
  break;
1254
+ case "backend":
1255
+ if (rest[0] === "add") cmdBackendAdd();
1256
+ else cmdBackendList();
1257
+ break;
1019
1258
  case "help":
1020
1259
  case "--help":
1021
1260
  case "-h":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bridgerapi",
3
- "version": "1.7.0",
3
+ "version": "1.9.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",