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.
- package/dist/cli.js +328 -89
- 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
|
|
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
|
-
|
|
73
|
-
|
|
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 (
|
|
76
|
-
|
|
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,
|
|
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
|
-
|
|
111
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
166
|
-
|
|
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
|
|
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,
|
|
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
|
|
295
|
-
|
|
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)(
|
|
311
|
-
|
|
312
|
-
|
|
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
|
-
|
|
322
|
-
|
|
493
|
+
yield* spawnStream(
|
|
494
|
+
which("droid") || this.bin,
|
|
495
|
+
["exec", "--output-format", "text", "--model", model2, "-"],
|
|
496
|
+
prompt
|
|
497
|
+
);
|
|
323
498
|
}
|
|
324
499
|
};
|
|
325
|
-
var
|
|
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
|
-
|
|
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
|
|
495
|
-
var
|
|
496
|
-
var
|
|
497
|
-
var HOME2 = (0,
|
|
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,
|
|
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,
|
|
504
|
-
(0,
|
|
505
|
-
(0,
|
|
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,
|
|
736
|
+
(0, import_fs3.writeFileSync)(plistPath(), plist);
|
|
549
737
|
}
|
|
550
738
|
function unitPath() {
|
|
551
|
-
const configHome = process.env.XDG_CONFIG_HOME ?? (0,
|
|
552
|
-
return (0,
|
|
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,
|
|
556
|
-
(0,
|
|
557
|
-
(0,
|
|
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,
|
|
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,
|
|
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,
|
|
793
|
+
const os = (0, import_os3.platform)();
|
|
606
794
|
if (os === "darwin") {
|
|
607
795
|
const p = plistPath();
|
|
608
|
-
if ((0,
|
|
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,
|
|
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,
|
|
625
|
-
(0,
|
|
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,
|
|
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
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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":
|