apispoof 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +155 -0
- package/dist/cli.js +2034 -0
- package/package.json +38 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,2034 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
25
|
+
|
|
26
|
+
// src/server.ts
|
|
27
|
+
var import_http = require("http");
|
|
28
|
+
var import_crypto2 = require("crypto");
|
|
29
|
+
|
|
30
|
+
// src/config.ts
|
|
31
|
+
var import_fs = require("fs");
|
|
32
|
+
var import_crypto = require("crypto");
|
|
33
|
+
var import_os = require("os");
|
|
34
|
+
var import_path = require("path");
|
|
35
|
+
var CONFIG_DIR = (0, import_path.join)((0, import_os.homedir)(), ".apispoof");
|
|
36
|
+
var CONFIG_FILE = (0, import_path.join)(CONFIG_DIR, "config.json");
|
|
37
|
+
function configDir() {
|
|
38
|
+
return CONFIG_DIR;
|
|
39
|
+
}
|
|
40
|
+
function loadConfig() {
|
|
41
|
+
try {
|
|
42
|
+
if ((0, import_fs.existsSync)(CONFIG_FILE)) {
|
|
43
|
+
return JSON.parse((0, import_fs.readFileSync)(CONFIG_FILE, "utf8"));
|
|
44
|
+
}
|
|
45
|
+
} catch {
|
|
46
|
+
}
|
|
47
|
+
return {};
|
|
48
|
+
}
|
|
49
|
+
function saveConfig(cfg) {
|
|
50
|
+
(0, import_fs.mkdirSync)(CONFIG_DIR, { recursive: true });
|
|
51
|
+
(0, import_fs.writeFileSync)(CONFIG_FILE, JSON.stringify(cfg, null, 2) + "\n", { mode: 384 });
|
|
52
|
+
}
|
|
53
|
+
function getOrCreateApiKey() {
|
|
54
|
+
const cfg = loadConfig();
|
|
55
|
+
if (cfg.apiKey) return cfg.apiKey;
|
|
56
|
+
const key = `sk-spoof-${(0, import_crypto.randomBytes)(24).toString("hex")}`;
|
|
57
|
+
saveConfig({ ...cfg, apiKey: key });
|
|
58
|
+
return key;
|
|
59
|
+
}
|
|
60
|
+
function getAccount(provider) {
|
|
61
|
+
const cfg = loadConfig();
|
|
62
|
+
return (cfg.accounts ?? []).find((a) => a.provider === provider && a.enabled);
|
|
63
|
+
}
|
|
64
|
+
function saveAccount(account) {
|
|
65
|
+
const cfg = loadConfig();
|
|
66
|
+
const accounts = (cfg.accounts ?? []).filter((a) => a.provider !== account.provider);
|
|
67
|
+
accounts.push(account);
|
|
68
|
+
saveConfig({ ...cfg, accounts });
|
|
69
|
+
}
|
|
70
|
+
function removeAccount(provider) {
|
|
71
|
+
const cfg = loadConfig();
|
|
72
|
+
const before = (cfg.accounts ?? []).length;
|
|
73
|
+
const accounts = (cfg.accounts ?? []).filter((a) => a.provider !== provider);
|
|
74
|
+
if (accounts.length === before) return false;
|
|
75
|
+
saveConfig({ ...cfg, accounts });
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
function listAccounts() {
|
|
79
|
+
const cfg = loadConfig();
|
|
80
|
+
return cfg.accounts ?? [];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// src/routing.ts
|
|
84
|
+
var registeredProviders = [];
|
|
85
|
+
function registerProviders(providers) {
|
|
86
|
+
registeredProviders = providers;
|
|
87
|
+
}
|
|
88
|
+
function availableProviders() {
|
|
89
|
+
return registeredProviders.filter((p) => p.available());
|
|
90
|
+
}
|
|
91
|
+
function pickProvider(model) {
|
|
92
|
+
const cfg = loadConfig();
|
|
93
|
+
const m = model.toLowerCase();
|
|
94
|
+
const apiProviders = registeredProviders.filter((p) => p.type !== "cli" && p.available());
|
|
95
|
+
for (const p of apiProviders) {
|
|
96
|
+
if (p.prefixes.some((pfx) => m.startsWith(pfx))) return p;
|
|
97
|
+
}
|
|
98
|
+
const cliProviders = registeredProviders.filter((p) => p.type === "cli" && p.available());
|
|
99
|
+
const override = process.env.APISPOOF_BACKEND?.toLowerCase() ?? cfg.backend?.toLowerCase();
|
|
100
|
+
if (override) {
|
|
101
|
+
const forced = cliProviders.find((p) => p.name === override);
|
|
102
|
+
if (forced) return forced;
|
|
103
|
+
}
|
|
104
|
+
for (const p of cliProviders) {
|
|
105
|
+
if (p.prefixes.some((pfx) => m.startsWith(pfx))) return p;
|
|
106
|
+
}
|
|
107
|
+
const anyAvailable = registeredProviders.find((p) => p.available());
|
|
108
|
+
if (anyAvailable) return anyAvailable;
|
|
109
|
+
throw new Error("No providers available. Run: apispoof connect <provider>");
|
|
110
|
+
}
|
|
111
|
+
var MODEL_CACHE_TTL = 5 * 60 * 1e3;
|
|
112
|
+
var modelCache = null;
|
|
113
|
+
async function allModels() {
|
|
114
|
+
if (modelCache && Date.now() < modelCache.expiresAt) return modelCache.models;
|
|
115
|
+
const available = registeredProviders.filter((p) => p.available());
|
|
116
|
+
const results = [];
|
|
117
|
+
for (const p of available) {
|
|
118
|
+
const ids = await p.models();
|
|
119
|
+
for (const id of ids) results.push({ id, provider: p.displayName });
|
|
120
|
+
}
|
|
121
|
+
modelCache = { models: results, expiresAt: Date.now() + MODEL_CACHE_TTL };
|
|
122
|
+
return results;
|
|
123
|
+
}
|
|
124
|
+
function providerStatus() {
|
|
125
|
+
return registeredProviders.map((p) => ({
|
|
126
|
+
name: p.name,
|
|
127
|
+
type: p.type,
|
|
128
|
+
available: p.available(),
|
|
129
|
+
displayName: p.displayName
|
|
130
|
+
}));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// src/server.ts
|
|
134
|
+
function sse(data) {
|
|
135
|
+
return `data: ${JSON.stringify(data)}
|
|
136
|
+
|
|
137
|
+
`;
|
|
138
|
+
}
|
|
139
|
+
function chunk(id, ts, model, delta, finish) {
|
|
140
|
+
return sse({
|
|
141
|
+
id,
|
|
142
|
+
object: "chat.completion.chunk",
|
|
143
|
+
created: ts,
|
|
144
|
+
model,
|
|
145
|
+
choices: [{ index: 0, delta, finish_reason: finish ?? null }]
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
function completionResponse(id, ts, model, text, usage) {
|
|
149
|
+
const pt = usage ? (usage.input_tokens ?? 0) + (usage.cache_creation_input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0) + (usage.promptTokenCount ?? 0) + (usage.prompt_tokens ?? 0) : 0;
|
|
150
|
+
const ct = usage ? (usage.output_tokens ?? 0) + (usage.candidatesTokenCount ?? 0) + (usage.completion_tokens ?? 0) : 0;
|
|
151
|
+
return {
|
|
152
|
+
id,
|
|
153
|
+
object: "chat.completion",
|
|
154
|
+
created: ts,
|
|
155
|
+
model,
|
|
156
|
+
choices: [{ index: 0, message: { role: "assistant", content: text }, finish_reason: "stop" }],
|
|
157
|
+
usage: { prompt_tokens: pt, completion_tokens: ct, total_tokens: pt + ct }
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function setCors(res) {
|
|
161
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
162
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
163
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
164
|
+
}
|
|
165
|
+
function sendJson(res, statusCode, body) {
|
|
166
|
+
const payload = Buffer.from(JSON.stringify(body));
|
|
167
|
+
setCors(res);
|
|
168
|
+
res.writeHead(statusCode, {
|
|
169
|
+
"Content-Type": "application/json",
|
|
170
|
+
"Content-Length": payload.byteLength
|
|
171
|
+
});
|
|
172
|
+
res.end(payload);
|
|
173
|
+
}
|
|
174
|
+
async function readBody(req) {
|
|
175
|
+
return new Promise((resolve, reject) => {
|
|
176
|
+
const chunks = [];
|
|
177
|
+
req.on("data", (c) => chunks.push(c));
|
|
178
|
+
req.on("end", () => resolve(Buffer.concat(chunks).toString()));
|
|
179
|
+
req.on("error", reject);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
function validateApiKey(req) {
|
|
183
|
+
const expected = getOrCreateApiKey();
|
|
184
|
+
const auth = req.headers.authorization ?? "";
|
|
185
|
+
if (auth.startsWith("Bearer ")) {
|
|
186
|
+
return auth.slice(7).trim() === expected;
|
|
187
|
+
}
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
async function handleModels(res) {
|
|
191
|
+
const ts = Math.floor(Date.now() / 1e3);
|
|
192
|
+
const models = await allModels();
|
|
193
|
+
sendJson(res, 200, {
|
|
194
|
+
object: "list",
|
|
195
|
+
data: models.map((m) => ({
|
|
196
|
+
id: m.id,
|
|
197
|
+
object: "model",
|
|
198
|
+
created: ts,
|
|
199
|
+
owned_by: m.provider
|
|
200
|
+
}))
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
function handleHealth(res, port) {
|
|
204
|
+
const status = providerStatus();
|
|
205
|
+
const providers = {};
|
|
206
|
+
for (const s of status) providers[s.displayName] = { type: s.type, available: s.available };
|
|
207
|
+
sendJson(res, 200, {
|
|
208
|
+
status: "ok",
|
|
209
|
+
version: "3.0.0",
|
|
210
|
+
port,
|
|
211
|
+
providers
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
async function handleChat(req, res) {
|
|
215
|
+
let body;
|
|
216
|
+
try {
|
|
217
|
+
body = JSON.parse(await readBody(req));
|
|
218
|
+
} catch {
|
|
219
|
+
sendJson(res, 400, { error: { message: "invalid JSON", type: "invalid_request_error" } });
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const messages = body?.messages;
|
|
223
|
+
if (!Array.isArray(messages) || messages.length === 0) {
|
|
224
|
+
sendJson(res, 400, { error: { message: "messages required", type: "invalid_request_error" } });
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
const model = body.model ?? "claude-sonnet-4-6";
|
|
228
|
+
const streaming = Boolean(body.stream);
|
|
229
|
+
let provider;
|
|
230
|
+
try {
|
|
231
|
+
provider = pickProvider(model);
|
|
232
|
+
} catch (err) {
|
|
233
|
+
sendJson(res, 503, { error: { message: err.message, type: "server_error" } });
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
const id = `chatcmpl-${(0, import_crypto2.randomUUID)().replace(/-/g, "").slice(0, 20)}`;
|
|
237
|
+
const ts = Math.floor(Date.now() / 1e3);
|
|
238
|
+
console.log(` ${provider.displayName} model=${model} stream=${streaming} turns=${messages.length}`);
|
|
239
|
+
if (streaming) {
|
|
240
|
+
setCors(res);
|
|
241
|
+
res.writeHead(200, {
|
|
242
|
+
"Content-Type": "text/event-stream",
|
|
243
|
+
"Cache-Control": "no-cache",
|
|
244
|
+
"X-Accel-Buffering": "no"
|
|
245
|
+
});
|
|
246
|
+
res.flushHeaders();
|
|
247
|
+
res.write(chunk(id, ts, model, { role: "assistant" }));
|
|
248
|
+
try {
|
|
249
|
+
for await (const raw of provider.stream(messages, model)) {
|
|
250
|
+
res.write(chunk(id, ts, model, { content: raw.toString("utf8") }));
|
|
251
|
+
}
|
|
252
|
+
} catch (err) {
|
|
253
|
+
const msg = err.message ?? String(err);
|
|
254
|
+
console.error(` stream error [${provider.name}]: ${msg}`);
|
|
255
|
+
res.write(chunk(id, ts, model, { content: `
|
|
256
|
+
|
|
257
|
+
[apispoof] ${provider.name} error: ${msg}` }));
|
|
258
|
+
}
|
|
259
|
+
res.write(chunk(id, ts, model, {}, "stop"));
|
|
260
|
+
res.write("data: [DONE]\n\n");
|
|
261
|
+
res.end();
|
|
262
|
+
} else {
|
|
263
|
+
try {
|
|
264
|
+
const [text, usage] = await provider.runBlocking(messages, model);
|
|
265
|
+
sendJson(res, 200, completionResponse(id, ts, model, text, usage));
|
|
266
|
+
} catch (err) {
|
|
267
|
+
console.error(` error [${provider.name}]: ${err.message}`);
|
|
268
|
+
sendJson(res, 500, { error: { message: err.message, type: "server_error" } });
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
function createSpoofServer(port) {
|
|
273
|
+
return (0, import_http.createServer)(async (req, res) => {
|
|
274
|
+
try {
|
|
275
|
+
const path = (req.url ?? "/").split("?")[0];
|
|
276
|
+
const method = req.method ?? "GET";
|
|
277
|
+
if (method === "OPTIONS") {
|
|
278
|
+
setCors(res);
|
|
279
|
+
res.writeHead(200);
|
|
280
|
+
res.end();
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (method === "GET" && path === "/health") {
|
|
284
|
+
handleHealth(res, port);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (!validateApiKey(req)) {
|
|
288
|
+
sendJson(res, 401, { error: { message: "Invalid API key", type: "authentication_error" } });
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (method === "GET" && (path === "/v1/models" || path === "/models")) {
|
|
292
|
+
await handleModels(res);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
if (method === "POST" && (path === "/v1/chat/completions" || path === "/chat/completions")) {
|
|
296
|
+
await handleChat(req, res);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
sendJson(res, 404, { error: { message: "not found", type: "not_found_error" } });
|
|
300
|
+
} catch (err) {
|
|
301
|
+
console.error(" unhandled server error:", err.message);
|
|
302
|
+
if (!res.headersSent) {
|
|
303
|
+
sendJson(res, 500, { error: { message: "internal server error", type: "server_error" } });
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// src/service.ts
|
|
310
|
+
var import_child_process = require("child_process");
|
|
311
|
+
var import_fs2 = require("fs");
|
|
312
|
+
var import_os2 = require("os");
|
|
313
|
+
var import_path2 = require("path");
|
|
314
|
+
var HOME = (0, import_os2.homedir)();
|
|
315
|
+
var LABEL = "com.apispoof.server";
|
|
316
|
+
function plistPath() {
|
|
317
|
+
return (0, import_path2.join)(HOME, "Library/LaunchAgents", `${LABEL}.plist`);
|
|
318
|
+
}
|
|
319
|
+
function writePlist(port, scriptPath, nodePath) {
|
|
320
|
+
const logDir = (0, import_path2.join)(HOME, ".apispoof");
|
|
321
|
+
(0, import_fs2.mkdirSync)(logDir, { recursive: true });
|
|
322
|
+
(0, import_fs2.mkdirSync)((0, import_path2.join)(HOME, "Library/LaunchAgents"), { recursive: true });
|
|
323
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
324
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
|
325
|
+
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
326
|
+
<plist version="1.0">
|
|
327
|
+
<dict>
|
|
328
|
+
<key>Label</key>
|
|
329
|
+
<string>${LABEL}</string>
|
|
330
|
+
|
|
331
|
+
<key>ProgramArguments</key>
|
|
332
|
+
<array>
|
|
333
|
+
<string>${nodePath}</string>
|
|
334
|
+
<string>${scriptPath}</string>
|
|
335
|
+
<string>start</string>
|
|
336
|
+
</array>
|
|
337
|
+
|
|
338
|
+
<key>EnvironmentVariables</key>
|
|
339
|
+
<dict>
|
|
340
|
+
<key>APISPOOF_PORT</key>
|
|
341
|
+
<string>${port}</string>
|
|
342
|
+
<key>PATH</key>
|
|
343
|
+
<string>${HOME}/.local/bin:/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin</string>
|
|
344
|
+
<key>HOME</key>
|
|
345
|
+
<string>${HOME}</string>
|
|
346
|
+
</dict>
|
|
347
|
+
|
|
348
|
+
<key>StandardOutPath</key>
|
|
349
|
+
<string>${logDir}/server.log</string>
|
|
350
|
+
<key>StandardErrorPath</key>
|
|
351
|
+
<string>${logDir}/server.log</string>
|
|
352
|
+
<key>WorkingDirectory</key>
|
|
353
|
+
<string>${HOME}</string>
|
|
354
|
+
<key>RunAtLoad</key>
|
|
355
|
+
<true/>
|
|
356
|
+
<key>KeepAlive</key>
|
|
357
|
+
<true/>
|
|
358
|
+
</dict>
|
|
359
|
+
</plist>`;
|
|
360
|
+
(0, import_fs2.writeFileSync)(plistPath(), plist);
|
|
361
|
+
}
|
|
362
|
+
function unitPath() {
|
|
363
|
+
const configHome = process.env.XDG_CONFIG_HOME ?? (0, import_path2.join)(HOME, ".config");
|
|
364
|
+
return (0, import_path2.join)(configHome, "systemd/user/apispoof.service");
|
|
365
|
+
}
|
|
366
|
+
function writeUnit(port, scriptPath, nodePath) {
|
|
367
|
+
const logDir = (0, import_path2.join)(HOME, ".apispoof");
|
|
368
|
+
(0, import_fs2.mkdirSync)(logDir, { recursive: true });
|
|
369
|
+
(0, import_fs2.mkdirSync)((0, import_path2.join)(HOME, ".config/systemd/user"), { recursive: true });
|
|
370
|
+
const unit = `[Unit]
|
|
371
|
+
Description=apispoof \u2014 AI API Bridge
|
|
372
|
+
After=network.target
|
|
373
|
+
|
|
374
|
+
[Service]
|
|
375
|
+
Type=simple
|
|
376
|
+
ExecStart=${nodePath} ${scriptPath} start
|
|
377
|
+
Environment=APISPOOF_PORT=${port}
|
|
378
|
+
Environment=HOME=${HOME}
|
|
379
|
+
Environment=PATH=${HOME}/.local/bin:/usr/local/bin:/usr/bin:/bin
|
|
380
|
+
Restart=always
|
|
381
|
+
StandardOutput=append:${logDir}/server.log
|
|
382
|
+
StandardError=append:${logDir}/server.log
|
|
383
|
+
|
|
384
|
+
[Install]
|
|
385
|
+
WantedBy=default.target
|
|
386
|
+
`;
|
|
387
|
+
(0, import_fs2.writeFileSync)(unitPath(), unit);
|
|
388
|
+
}
|
|
389
|
+
function installService(port) {
|
|
390
|
+
const scriptPath = process.argv[1];
|
|
391
|
+
const nodePath = process.execPath;
|
|
392
|
+
const os = (0, import_os2.platform)();
|
|
393
|
+
if (os === "darwin") {
|
|
394
|
+
try {
|
|
395
|
+
(0, import_child_process.execSync)(`launchctl unload "${plistPath()}" 2>/dev/null`, { stdio: "ignore" });
|
|
396
|
+
} catch {
|
|
397
|
+
}
|
|
398
|
+
writePlist(port, scriptPath, nodePath);
|
|
399
|
+
(0, import_child_process.execSync)(`launchctl load -w "${plistPath()}"`);
|
|
400
|
+
console.log(` LaunchAgent installed -> ${plistPath()}`);
|
|
401
|
+
} else if (os === "linux") {
|
|
402
|
+
writeUnit(port, scriptPath, nodePath);
|
|
403
|
+
try {
|
|
404
|
+
(0, import_child_process.execSync)("systemctl --user daemon-reload");
|
|
405
|
+
} catch {
|
|
406
|
+
}
|
|
407
|
+
(0, import_child_process.execSync)("systemctl --user enable --now apispoof");
|
|
408
|
+
console.log(" systemd user service installed");
|
|
409
|
+
} else {
|
|
410
|
+
throw new Error(`Auto-install not supported on ${os}. Run 'apispoof start' manually.`);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
function uninstallService() {
|
|
414
|
+
const os = (0, import_os2.platform)();
|
|
415
|
+
if (os === "darwin") {
|
|
416
|
+
const p = plistPath();
|
|
417
|
+
if ((0, import_fs2.existsSync)(p)) {
|
|
418
|
+
try {
|
|
419
|
+
(0, import_child_process.execSync)(`launchctl unload "${p}"`);
|
|
420
|
+
} catch {
|
|
421
|
+
}
|
|
422
|
+
(0, import_fs2.unlinkSync)(p);
|
|
423
|
+
console.log(" LaunchAgent removed");
|
|
424
|
+
} else {
|
|
425
|
+
console.log(" apispoof service is not installed");
|
|
426
|
+
}
|
|
427
|
+
} else if (os === "linux") {
|
|
428
|
+
const p = unitPath();
|
|
429
|
+
try {
|
|
430
|
+
(0, import_child_process.execSync)("systemctl --user disable --now apispoof");
|
|
431
|
+
} catch {
|
|
432
|
+
}
|
|
433
|
+
if ((0, import_fs2.existsSync)(p)) {
|
|
434
|
+
(0, import_fs2.unlinkSync)(p);
|
|
435
|
+
try {
|
|
436
|
+
(0, import_child_process.execSync)("systemctl --user daemon-reload");
|
|
437
|
+
} catch {
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
console.log(" systemd service removed");
|
|
441
|
+
} else {
|
|
442
|
+
throw new Error(`Not supported on ${os}`);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
function serviceStatus() {
|
|
446
|
+
const os = (0, import_os2.platform)();
|
|
447
|
+
try {
|
|
448
|
+
if (os === "darwin") {
|
|
449
|
+
const out = (0, import_child_process.execSync)(`launchctl list ${LABEL} 2>/dev/null`, { encoding: "utf8" });
|
|
450
|
+
const pid = parseInt(out.match(/"PID"\s*=\s*(\d+)/)?.[1] ?? "0");
|
|
451
|
+
return { running: pid > 0, pid: pid || void 0 };
|
|
452
|
+
} else if (os === "linux") {
|
|
453
|
+
(0, import_child_process.execSync)("systemctl --user is-active apispoof", { stdio: "ignore" });
|
|
454
|
+
return { running: true };
|
|
455
|
+
}
|
|
456
|
+
} catch {
|
|
457
|
+
}
|
|
458
|
+
return { running: false };
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// src/auth.ts
|
|
462
|
+
var import_http2 = require("http");
|
|
463
|
+
var import_crypto3 = require("crypto");
|
|
464
|
+
var import_child_process2 = require("child_process");
|
|
465
|
+
var import_os3 = require("os");
|
|
466
|
+
function base64url(buf) {
|
|
467
|
+
return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
468
|
+
}
|
|
469
|
+
function generatePKCE() {
|
|
470
|
+
const verifier = base64url((0, import_crypto3.randomBytes)(32));
|
|
471
|
+
const challenge = base64url((0, import_crypto3.createHash)("sha256").update(verifier).digest());
|
|
472
|
+
return { verifier, challenge };
|
|
473
|
+
}
|
|
474
|
+
var ANTHROPIC_OAUTH = {
|
|
475
|
+
authUrl: "https://claude.ai/oauth/authorize",
|
|
476
|
+
tokenUrl: "https://api.anthropic.com/v1/oauth/token",
|
|
477
|
+
clientId: "9d1c250a-e61b-44d9-88ed-5944d1962f5e",
|
|
478
|
+
scopes: "user:inference user:profile"
|
|
479
|
+
};
|
|
480
|
+
function openBrowser(url) {
|
|
481
|
+
try {
|
|
482
|
+
const cmd = (0, import_os3.platform)() === "darwin" ? "open" : (0, import_os3.platform)() === "win32" ? "start" : "xdg-open";
|
|
483
|
+
(0, import_child_process2.execSync)(`${cmd} "${url}"`, { stdio: "ignore" });
|
|
484
|
+
return true;
|
|
485
|
+
} catch {
|
|
486
|
+
return false;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
function waitForCallback(port, state) {
|
|
490
|
+
return new Promise((resolve, reject) => {
|
|
491
|
+
const timeout = setTimeout(() => {
|
|
492
|
+
server.close();
|
|
493
|
+
reject(new Error("OAuth callback timed out after 5 minutes"));
|
|
494
|
+
}, 3e5);
|
|
495
|
+
const server = (0, import_http2.createServer)((req, res) => {
|
|
496
|
+
const url = new URL(req.url ?? "/", `http://127.0.0.1:${port}`);
|
|
497
|
+
if (!url.pathname.endsWith("/callback")) {
|
|
498
|
+
res.writeHead(404);
|
|
499
|
+
res.end("Not found");
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
const code = url.searchParams.get("code");
|
|
503
|
+
const returnedState = url.searchParams.get("state");
|
|
504
|
+
const error = url.searchParams.get("error");
|
|
505
|
+
if (error) {
|
|
506
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
507
|
+
res.end(callbackPage("Authentication Failed", `Error: ${error}. You can close this tab.`));
|
|
508
|
+
clearTimeout(timeout);
|
|
509
|
+
server.close();
|
|
510
|
+
reject(new Error(`OAuth error: ${error}`));
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
if (!code || returnedState !== state) {
|
|
514
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
515
|
+
res.end(callbackPage("Error", "Invalid callback. Please try again."));
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
519
|
+
res.end(callbackPage("Connected!", "Authentication successful. You can close this tab."));
|
|
520
|
+
clearTimeout(timeout);
|
|
521
|
+
server.close();
|
|
522
|
+
resolve(code);
|
|
523
|
+
});
|
|
524
|
+
server.listen(port, "127.0.0.1");
|
|
525
|
+
server.on("error", (err) => {
|
|
526
|
+
clearTimeout(timeout);
|
|
527
|
+
reject(err);
|
|
528
|
+
});
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
function callbackPage(title, message) {
|
|
532
|
+
return `<!DOCTYPE html><html><head><title>apispoof - ${title}</title>
|
|
533
|
+
<style>body{font-family:system-ui;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:#0a0a0a;color:#fafafa}
|
|
534
|
+
.card{text-align:center;padding:3rem;border-radius:12px;background:#1a1a1a;border:1px solid #333}
|
|
535
|
+
h1{margin:0 0 1rem;font-size:1.5rem}p{color:#888;margin:0}</style></head>
|
|
536
|
+
<body><div class="card"><h1>${title}</h1><p>${message}</p></div></body></html>`;
|
|
537
|
+
}
|
|
538
|
+
async function exchangeCode(config, code, verifier, redirectUri) {
|
|
539
|
+
const res = await fetch(config.tokenUrl, {
|
|
540
|
+
method: "POST",
|
|
541
|
+
headers: { "Content-Type": "application/json" },
|
|
542
|
+
body: JSON.stringify({
|
|
543
|
+
grant_type: "authorization_code",
|
|
544
|
+
code,
|
|
545
|
+
client_id: config.clientId,
|
|
546
|
+
code_verifier: verifier,
|
|
547
|
+
redirect_uri: redirectUri
|
|
548
|
+
})
|
|
549
|
+
});
|
|
550
|
+
if (!res.ok) {
|
|
551
|
+
const text = await res.text();
|
|
552
|
+
throw new Error(`Token exchange failed (${res.status}): ${text}`);
|
|
553
|
+
}
|
|
554
|
+
const data = await res.json();
|
|
555
|
+
return {
|
|
556
|
+
accessToken: data.access_token,
|
|
557
|
+
refreshToken: data.refresh_token,
|
|
558
|
+
expiresIn: data.expires_in,
|
|
559
|
+
email: data.account?.email_address,
|
|
560
|
+
orgId: data.organization?.uuid
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
async function refreshAnthropicToken(account) {
|
|
564
|
+
if (!account.refreshToken) throw new Error("No refresh token available");
|
|
565
|
+
const res = await fetch(ANTHROPIC_OAUTH.tokenUrl, {
|
|
566
|
+
method: "POST",
|
|
567
|
+
headers: { "Content-Type": "application/json" },
|
|
568
|
+
body: JSON.stringify({
|
|
569
|
+
grant_type: "refresh_token",
|
|
570
|
+
refresh_token: account.refreshToken,
|
|
571
|
+
client_id: ANTHROPIC_OAUTH.clientId
|
|
572
|
+
})
|
|
573
|
+
});
|
|
574
|
+
if (!res.ok) {
|
|
575
|
+
const text = await res.text();
|
|
576
|
+
throw new Error(`Token refresh failed (${res.status}): ${text}`);
|
|
577
|
+
}
|
|
578
|
+
const data = await res.json();
|
|
579
|
+
const updated = {
|
|
580
|
+
...account,
|
|
581
|
+
accessToken: data.access_token,
|
|
582
|
+
refreshToken: data.refresh_token ?? account.refreshToken,
|
|
583
|
+
expiresAt: Date.now() + (data.expires_in ?? 28800) * 1e3
|
|
584
|
+
};
|
|
585
|
+
saveAccount(updated);
|
|
586
|
+
return updated;
|
|
587
|
+
}
|
|
588
|
+
async function getValidAnthropicToken() {
|
|
589
|
+
const account = getAccount("anthropic");
|
|
590
|
+
if (!account) return null;
|
|
591
|
+
const bufferMs = 5 * 60 * 1e3;
|
|
592
|
+
if (account.expiresAt && Date.now() > account.expiresAt - bufferMs) {
|
|
593
|
+
try {
|
|
594
|
+
const refreshed = await refreshAnthropicToken(account);
|
|
595
|
+
return refreshed.accessToken;
|
|
596
|
+
} catch {
|
|
597
|
+
return null;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
return account.accessToken;
|
|
601
|
+
}
|
|
602
|
+
async function connectAnthropic() {
|
|
603
|
+
const callbackPort = 18920 + Math.floor(Math.random() * 100);
|
|
604
|
+
const redirectUri = `http://127.0.0.1:${callbackPort}/callback`;
|
|
605
|
+
const { verifier, challenge } = generatePKCE();
|
|
606
|
+
const state = (0, import_crypto3.randomBytes)(16).toString("hex");
|
|
607
|
+
const authUrl = new URL(ANTHROPIC_OAUTH.authUrl);
|
|
608
|
+
authUrl.searchParams.set("response_type", "code");
|
|
609
|
+
authUrl.searchParams.set("client_id", ANTHROPIC_OAUTH.clientId);
|
|
610
|
+
authUrl.searchParams.set("redirect_uri", redirectUri);
|
|
611
|
+
authUrl.searchParams.set("scope", ANTHROPIC_OAUTH.scopes);
|
|
612
|
+
authUrl.searchParams.set("code_challenge", challenge);
|
|
613
|
+
authUrl.searchParams.set("code_challenge_method", "S256");
|
|
614
|
+
authUrl.searchParams.set("state", state);
|
|
615
|
+
console.log("\n Opening browser for Anthropic authentication...");
|
|
616
|
+
const opened = openBrowser(authUrl.toString());
|
|
617
|
+
if (!opened) {
|
|
618
|
+
console.log("\n Could not open browser. Visit this URL manually:");
|
|
619
|
+
console.log(` ${authUrl.toString()}
|
|
620
|
+
`);
|
|
621
|
+
}
|
|
622
|
+
console.log(" Waiting for authorization...");
|
|
623
|
+
const code = await waitForCallback(callbackPort, state);
|
|
624
|
+
const tokens = await exchangeCode(ANTHROPIC_OAUTH, code, verifier, redirectUri);
|
|
625
|
+
const account = {
|
|
626
|
+
provider: "anthropic",
|
|
627
|
+
accessToken: tokens.accessToken,
|
|
628
|
+
refreshToken: tokens.refreshToken,
|
|
629
|
+
expiresAt: tokens.expiresIn ? Date.now() + tokens.expiresIn * 1e3 : void 0,
|
|
630
|
+
email: tokens.email,
|
|
631
|
+
orgId: tokens.orgId,
|
|
632
|
+
enabled: true,
|
|
633
|
+
connectedAt: Date.now()
|
|
634
|
+
};
|
|
635
|
+
saveAccount(account);
|
|
636
|
+
return account;
|
|
637
|
+
}
|
|
638
|
+
async function connectOpenAI(apiKey) {
|
|
639
|
+
const res = await fetch("https://api.openai.com/v1/models", {
|
|
640
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
641
|
+
});
|
|
642
|
+
if (!res.ok) {
|
|
643
|
+
const text = await res.text();
|
|
644
|
+
throw new Error(`OpenAI API key invalid (${res.status}): ${text}`);
|
|
645
|
+
}
|
|
646
|
+
const account = {
|
|
647
|
+
provider: "openai",
|
|
648
|
+
accessToken: apiKey,
|
|
649
|
+
enabled: true,
|
|
650
|
+
connectedAt: Date.now()
|
|
651
|
+
};
|
|
652
|
+
saveAccount(account);
|
|
653
|
+
return account;
|
|
654
|
+
}
|
|
655
|
+
async function connectGoogle(apiKey) {
|
|
656
|
+
const res = await fetch(
|
|
657
|
+
`https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`
|
|
658
|
+
);
|
|
659
|
+
if (!res.ok) {
|
|
660
|
+
const text = await res.text();
|
|
661
|
+
throw new Error(`Google API key invalid (${res.status}): ${text}`);
|
|
662
|
+
}
|
|
663
|
+
const account = {
|
|
664
|
+
provider: "google",
|
|
665
|
+
accessToken: apiKey,
|
|
666
|
+
enabled: true,
|
|
667
|
+
connectedAt: Date.now()
|
|
668
|
+
};
|
|
669
|
+
saveAccount(account);
|
|
670
|
+
return account;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// src/providers-api.ts
|
|
674
|
+
function extractText(content) {
|
|
675
|
+
if (typeof content === "string") return content;
|
|
676
|
+
if (Array.isArray(content))
|
|
677
|
+
return content.filter((p) => p.type === "text").map((p) => p.text ?? "").join(" ");
|
|
678
|
+
return String(content ?? "");
|
|
679
|
+
}
|
|
680
|
+
function toAnthropicPayload(messages, model, stream) {
|
|
681
|
+
const system = [];
|
|
682
|
+
const anthropicMsgs = [];
|
|
683
|
+
for (const m of messages) {
|
|
684
|
+
const text = extractText(m.content).trim();
|
|
685
|
+
if (!text) continue;
|
|
686
|
+
if (m.role === "system") {
|
|
687
|
+
system.push(text);
|
|
688
|
+
continue;
|
|
689
|
+
}
|
|
690
|
+
const role = m.role === "assistant" ? "assistant" : "user";
|
|
691
|
+
const last = anthropicMsgs[anthropicMsgs.length - 1];
|
|
692
|
+
if (last && last.role === role) {
|
|
693
|
+
anthropicMsgs[anthropicMsgs.length - 1] = { ...last, content: last.content + "\n" + text };
|
|
694
|
+
} else {
|
|
695
|
+
anthropicMsgs.push({ role, content: text });
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
if (anthropicMsgs.length === 0 || anthropicMsgs[0].role !== "user") {
|
|
699
|
+
anthropicMsgs.unshift({ role: "user", content: "Hello" });
|
|
700
|
+
}
|
|
701
|
+
return {
|
|
702
|
+
model,
|
|
703
|
+
max_tokens: 8192,
|
|
704
|
+
stream,
|
|
705
|
+
...system.length > 0 ? { system: system.join("\n") } : {},
|
|
706
|
+
messages: anthropicMsgs
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
function toGooglePayload(messages, stream) {
|
|
710
|
+
const systemParts = [];
|
|
711
|
+
const contents = [];
|
|
712
|
+
for (const m of messages) {
|
|
713
|
+
const text = extractText(m.content).trim();
|
|
714
|
+
if (!text) continue;
|
|
715
|
+
if (m.role === "system") {
|
|
716
|
+
systemParts.push(text);
|
|
717
|
+
continue;
|
|
718
|
+
}
|
|
719
|
+
const role = m.role === "assistant" ? "model" : "user";
|
|
720
|
+
contents.push({ role, parts: [{ text }] });
|
|
721
|
+
}
|
|
722
|
+
return {
|
|
723
|
+
contents,
|
|
724
|
+
...systemParts.length > 0 ? { systemInstruction: { parts: [{ text: systemParts.join("\n") }] } } : {},
|
|
725
|
+
generationConfig: { maxOutputTokens: 8192 }
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
async function* parseSSELines(response) {
|
|
729
|
+
const reader = response.body?.getReader();
|
|
730
|
+
if (!reader) return;
|
|
731
|
+
const decoder = new TextDecoder();
|
|
732
|
+
let buffer = "";
|
|
733
|
+
try {
|
|
734
|
+
while (true) {
|
|
735
|
+
const { done, value } = await reader.read();
|
|
736
|
+
if (done) break;
|
|
737
|
+
buffer += decoder.decode(value, { stream: true });
|
|
738
|
+
const lines = buffer.split("\n");
|
|
739
|
+
buffer = lines.pop() ?? "";
|
|
740
|
+
for (const line of lines) {
|
|
741
|
+
const trimmed = line.trim();
|
|
742
|
+
if (trimmed.startsWith("data: ")) {
|
|
743
|
+
yield trimmed.slice(6);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
if (buffer.trim().startsWith("data: ")) {
|
|
748
|
+
yield buffer.trim().slice(6);
|
|
749
|
+
}
|
|
750
|
+
} finally {
|
|
751
|
+
reader.releaseLock();
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
var AnthropicProvider = class {
|
|
755
|
+
constructor() {
|
|
756
|
+
this.name = "anthropic";
|
|
757
|
+
this.displayName = "Anthropic (OAuth)";
|
|
758
|
+
this.type = "oauth";
|
|
759
|
+
this.prefixes = ["claude"];
|
|
760
|
+
}
|
|
761
|
+
available() {
|
|
762
|
+
return Boolean(getAccount("anthropic"));
|
|
763
|
+
}
|
|
764
|
+
async models() {
|
|
765
|
+
return [
|
|
766
|
+
"claude-opus-4-6",
|
|
767
|
+
"claude-sonnet-4-6",
|
|
768
|
+
"claude-haiku-4-5-20251001"
|
|
769
|
+
];
|
|
770
|
+
}
|
|
771
|
+
async headers() {
|
|
772
|
+
const token = await getValidAnthropicToken();
|
|
773
|
+
if (!token) throw new Error("Anthropic not connected or token expired. Run: apispoof connect anthropic");
|
|
774
|
+
return {
|
|
775
|
+
"Authorization": `Bearer ${token}`,
|
|
776
|
+
"Content-Type": "application/json",
|
|
777
|
+
"anthropic-version": "2023-06-01",
|
|
778
|
+
"anthropic-beta": "oauth-2025-04-20",
|
|
779
|
+
"user-agent": "apispoof/3.0.0"
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
async runBlocking(messages, model) {
|
|
783
|
+
const headers = await this.headers();
|
|
784
|
+
const body = toAnthropicPayload(messages, model, false);
|
|
785
|
+
const res = await fetch("https://api.anthropic.com/v1/messages", {
|
|
786
|
+
method: "POST",
|
|
787
|
+
headers,
|
|
788
|
+
body: JSON.stringify(body)
|
|
789
|
+
});
|
|
790
|
+
if (!res.ok) {
|
|
791
|
+
const text2 = await res.text();
|
|
792
|
+
throw new Error(`Anthropic API error (${res.status}): ${text2}`);
|
|
793
|
+
}
|
|
794
|
+
const data = await res.json();
|
|
795
|
+
const content = data.content;
|
|
796
|
+
const text = content?.filter((b) => b.type === "text").map((b) => b.text ?? "").join("") ?? "";
|
|
797
|
+
const usage = data.usage;
|
|
798
|
+
return [text, usage ?? null];
|
|
799
|
+
}
|
|
800
|
+
async *stream(messages, model) {
|
|
801
|
+
const headers = await this.headers();
|
|
802
|
+
const body = toAnthropicPayload(messages, model, true);
|
|
803
|
+
const res = await fetch("https://api.anthropic.com/v1/messages", {
|
|
804
|
+
method: "POST",
|
|
805
|
+
headers,
|
|
806
|
+
body: JSON.stringify(body)
|
|
807
|
+
});
|
|
808
|
+
if (!res.ok) {
|
|
809
|
+
const text = await res.text();
|
|
810
|
+
throw new Error(`Anthropic API error (${res.status}): ${text}`);
|
|
811
|
+
}
|
|
812
|
+
for await (const line of parseSSELines(res)) {
|
|
813
|
+
if (line === "[DONE]") break;
|
|
814
|
+
try {
|
|
815
|
+
const event = JSON.parse(line);
|
|
816
|
+
if (event.type === "content_block_delta") {
|
|
817
|
+
const delta = event.delta;
|
|
818
|
+
if (delta?.text) yield Buffer.from(delta.text);
|
|
819
|
+
}
|
|
820
|
+
} catch {
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
};
|
|
825
|
+
var OpenAIProvider = class {
|
|
826
|
+
constructor() {
|
|
827
|
+
this.name = "openai";
|
|
828
|
+
this.displayName = "OpenAI (API Key)";
|
|
829
|
+
this.type = "apikey";
|
|
830
|
+
this.prefixes = ["gpt", "o1", "o3", "o4", "chatgpt"];
|
|
831
|
+
}
|
|
832
|
+
available() {
|
|
833
|
+
return Boolean(getAccount("openai"));
|
|
834
|
+
}
|
|
835
|
+
async models() {
|
|
836
|
+
const account = getAccount("openai");
|
|
837
|
+
if (!account) return [];
|
|
838
|
+
try {
|
|
839
|
+
const res = await fetch("https://api.openai.com/v1/models", {
|
|
840
|
+
headers: { Authorization: `Bearer ${account.accessToken}` }
|
|
841
|
+
});
|
|
842
|
+
if (!res.ok) return this.fallbackModels();
|
|
843
|
+
const data = await res.json();
|
|
844
|
+
return data.data.map((m) => m.id).filter((id) => /^(gpt|o[134]|chatgpt)/.test(id)).sort();
|
|
845
|
+
} catch {
|
|
846
|
+
return this.fallbackModels();
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
fallbackModels() {
|
|
850
|
+
return ["gpt-4o", "gpt-4o-mini", "o3", "o4-mini"];
|
|
851
|
+
}
|
|
852
|
+
headers() {
|
|
853
|
+
const account = getAccount("openai");
|
|
854
|
+
if (!account) throw new Error("OpenAI not connected. Run: apispoof connect openai");
|
|
855
|
+
return {
|
|
856
|
+
"Authorization": `Bearer ${account.accessToken}`,
|
|
857
|
+
"Content-Type": "application/json"
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
async runBlocking(messages, model) {
|
|
861
|
+
const res = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
862
|
+
method: "POST",
|
|
863
|
+
headers: this.headers(),
|
|
864
|
+
body: JSON.stringify({ model, messages, stream: false })
|
|
865
|
+
});
|
|
866
|
+
if (!res.ok) {
|
|
867
|
+
const text2 = await res.text();
|
|
868
|
+
throw new Error(`OpenAI API error (${res.status}): ${text2}`);
|
|
869
|
+
}
|
|
870
|
+
const data = await res.json();
|
|
871
|
+
const choices = data.choices;
|
|
872
|
+
const text = choices?.[0]?.message?.content ?? "";
|
|
873
|
+
const usage = data.usage;
|
|
874
|
+
return [text, usage ?? null];
|
|
875
|
+
}
|
|
876
|
+
async *stream(messages, model) {
|
|
877
|
+
const res = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
878
|
+
method: "POST",
|
|
879
|
+
headers: this.headers(),
|
|
880
|
+
body: JSON.stringify({ model, messages, stream: true })
|
|
881
|
+
});
|
|
882
|
+
if (!res.ok) {
|
|
883
|
+
const text = await res.text();
|
|
884
|
+
throw new Error(`OpenAI API error (${res.status}): ${text}`);
|
|
885
|
+
}
|
|
886
|
+
for await (const line of parseSSELines(res)) {
|
|
887
|
+
if (line === "[DONE]") break;
|
|
888
|
+
try {
|
|
889
|
+
const chunk2 = JSON.parse(line);
|
|
890
|
+
const choices = chunk2.choices;
|
|
891
|
+
const content = choices?.[0]?.delta?.content;
|
|
892
|
+
if (content) yield Buffer.from(content);
|
|
893
|
+
} catch {
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
};
|
|
898
|
+
var GoogleProvider = class {
|
|
899
|
+
constructor() {
|
|
900
|
+
this.name = "google";
|
|
901
|
+
this.displayName = "Google AI (API Key)";
|
|
902
|
+
this.type = "apikey";
|
|
903
|
+
this.prefixes = ["gemini"];
|
|
904
|
+
}
|
|
905
|
+
available() {
|
|
906
|
+
return Boolean(getAccount("google"));
|
|
907
|
+
}
|
|
908
|
+
async models() {
|
|
909
|
+
const account = getAccount("google");
|
|
910
|
+
if (!account) return [];
|
|
911
|
+
try {
|
|
912
|
+
const res = await fetch(
|
|
913
|
+
`https://generativelanguage.googleapis.com/v1beta/models?key=${account.accessToken}`
|
|
914
|
+
);
|
|
915
|
+
if (!res.ok) return this.fallbackModels();
|
|
916
|
+
const data = await res.json();
|
|
917
|
+
return data.models.filter((m) => m.supportedGenerationMethods?.includes("generateContent")).map((m) => m.name.replace("models/", "")).filter((id) => id.startsWith("gemini")).sort();
|
|
918
|
+
} catch {
|
|
919
|
+
return this.fallbackModels();
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
fallbackModels() {
|
|
923
|
+
return ["gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.0-flash"];
|
|
924
|
+
}
|
|
925
|
+
apiKey() {
|
|
926
|
+
const account = getAccount("google");
|
|
927
|
+
if (!account) throw new Error("Google AI not connected. Run: apispoof connect google");
|
|
928
|
+
return account.accessToken;
|
|
929
|
+
}
|
|
930
|
+
async runBlocking(messages, model) {
|
|
931
|
+
const body = toGooglePayload(messages, false);
|
|
932
|
+
const res = await fetch(
|
|
933
|
+
`https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${this.apiKey()}`,
|
|
934
|
+
{ method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }
|
|
935
|
+
);
|
|
936
|
+
if (!res.ok) {
|
|
937
|
+
const text2 = await res.text();
|
|
938
|
+
throw new Error(`Google AI error (${res.status}): ${text2}`);
|
|
939
|
+
}
|
|
940
|
+
const data = await res.json();
|
|
941
|
+
const candidates = data.candidates;
|
|
942
|
+
const text = candidates?.[0]?.content?.parts?.map((p) => p.text).join("") ?? "";
|
|
943
|
+
const usage = data.usageMetadata;
|
|
944
|
+
return [text, usage ?? null];
|
|
945
|
+
}
|
|
946
|
+
async *stream(messages, model) {
|
|
947
|
+
const body = toGooglePayload(messages, true);
|
|
948
|
+
const res = await fetch(
|
|
949
|
+
`https://generativelanguage.googleapis.com/v1beta/models/${model}:streamGenerateContent?alt=sse&key=${this.apiKey()}`,
|
|
950
|
+
{ method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }
|
|
951
|
+
);
|
|
952
|
+
if (!res.ok) {
|
|
953
|
+
const text = await res.text();
|
|
954
|
+
throw new Error(`Google AI error (${res.status}): ${text}`);
|
|
955
|
+
}
|
|
956
|
+
for await (const line of parseSSELines(res)) {
|
|
957
|
+
if (line === "[DONE]") break;
|
|
958
|
+
try {
|
|
959
|
+
const data = JSON.parse(line);
|
|
960
|
+
const candidates = data.candidates;
|
|
961
|
+
const text = candidates?.[0]?.content?.parts?.map((p) => p.text).join("") ?? "";
|
|
962
|
+
if (text) yield Buffer.from(text);
|
|
963
|
+
} catch {
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
};
|
|
968
|
+
|
|
969
|
+
// src/providers-cli.ts
|
|
970
|
+
var import_child_process3 = require("child_process");
|
|
971
|
+
var import_fs3 = require("fs");
|
|
972
|
+
var import_os4 = require("os");
|
|
973
|
+
var import_path3 = require("path");
|
|
974
|
+
|
|
975
|
+
// src/messages.ts
|
|
976
|
+
function extractText2(content) {
|
|
977
|
+
if (typeof content === "string") return content;
|
|
978
|
+
if (Array.isArray(content)) {
|
|
979
|
+
return content.filter((p) => p.type === "text").map((p) => p.text ?? "").join(" ");
|
|
980
|
+
}
|
|
981
|
+
return String(content ?? "");
|
|
982
|
+
}
|
|
983
|
+
function messagesToPrompt(messages) {
|
|
984
|
+
const system = [];
|
|
985
|
+
const turns = [];
|
|
986
|
+
for (const m of messages) {
|
|
987
|
+
const text = extractText2(m.content).trim();
|
|
988
|
+
if (!text) continue;
|
|
989
|
+
switch (m.role) {
|
|
990
|
+
case "system":
|
|
991
|
+
system.push(text);
|
|
992
|
+
break;
|
|
993
|
+
case "assistant":
|
|
994
|
+
turns.push(`Assistant: ${text}`);
|
|
995
|
+
break;
|
|
996
|
+
case "tool":
|
|
997
|
+
turns.push(`Tool result: ${text}`);
|
|
998
|
+
break;
|
|
999
|
+
default:
|
|
1000
|
+
turns.push(`User: ${text}`);
|
|
1001
|
+
break;
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
const parts = [];
|
|
1005
|
+
if (system.length) parts.push("System instructions:\n" + system.join("\n"));
|
|
1006
|
+
if (turns.length) parts.push(turns.join("\n\n"));
|
|
1007
|
+
return parts.join("\n\n");
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
// src/providers-cli.ts
|
|
1011
|
+
var HOME2 = (0, import_os4.homedir)();
|
|
1012
|
+
function which(cmd) {
|
|
1013
|
+
try {
|
|
1014
|
+
return (0, import_child_process3.execFileSync)("which", [cmd], { encoding: "utf8" }).trim();
|
|
1015
|
+
} catch {
|
|
1016
|
+
return "";
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
var SKIP_TOKENS = /* @__PURE__ */ new Set([
|
|
1020
|
+
"output",
|
|
1021
|
+
"format",
|
|
1022
|
+
"text",
|
|
1023
|
+
"json",
|
|
1024
|
+
"model",
|
|
1025
|
+
"usage",
|
|
1026
|
+
"help",
|
|
1027
|
+
"exec",
|
|
1028
|
+
"run",
|
|
1029
|
+
"list",
|
|
1030
|
+
"command",
|
|
1031
|
+
"option",
|
|
1032
|
+
"default",
|
|
1033
|
+
"true",
|
|
1034
|
+
"false",
|
|
1035
|
+
"stdin",
|
|
1036
|
+
"stdout",
|
|
1037
|
+
"stderr",
|
|
1038
|
+
"path",
|
|
1039
|
+
"file",
|
|
1040
|
+
"config",
|
|
1041
|
+
"error",
|
|
1042
|
+
"warn",
|
|
1043
|
+
"approval",
|
|
1044
|
+
"mode",
|
|
1045
|
+
"yolo",
|
|
1046
|
+
"version",
|
|
1047
|
+
"verbose",
|
|
1048
|
+
"debug",
|
|
1049
|
+
"quiet",
|
|
1050
|
+
"output-format",
|
|
1051
|
+
"approval-mode",
|
|
1052
|
+
"list-models",
|
|
1053
|
+
"api-key",
|
|
1054
|
+
"base-url"
|
|
1055
|
+
]);
|
|
1056
|
+
function extractModelIds(text) {
|
|
1057
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1058
|
+
const re = /\b([a-z][a-z0-9]*(?:[.\-][a-z0-9]+)+)\b/g;
|
|
1059
|
+
for (const [, id] of text.matchAll(re)) {
|
|
1060
|
+
if (id.length >= 5 && /\d/.test(id) && !SKIP_TOKENS.has(id)) seen.add(id);
|
|
1061
|
+
}
|
|
1062
|
+
return [...seen];
|
|
1063
|
+
}
|
|
1064
|
+
async function tryDiscover(bin, strategies) {
|
|
1065
|
+
for (const args of strategies) {
|
|
1066
|
+
try {
|
|
1067
|
+
const out = (0, import_child_process3.execFileSync)(bin, args, {
|
|
1068
|
+
encoding: "utf8",
|
|
1069
|
+
timeout: 5e3,
|
|
1070
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1071
|
+
});
|
|
1072
|
+
const found = extractModelIds(out);
|
|
1073
|
+
if (found.length > 0) return found;
|
|
1074
|
+
} catch {
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
return [];
|
|
1078
|
+
}
|
|
1079
|
+
async function* spawnStream(cmd, args, stdin) {
|
|
1080
|
+
const proc = (0, import_child_process3.spawn)(cmd, args, { env: process.env, stdio: ["pipe", "pipe", "pipe"] });
|
|
1081
|
+
if (stdin !== void 0) proc.stdin.end(stdin);
|
|
1082
|
+
const stderrBufs = [];
|
|
1083
|
+
proc.stderr.on("data", (c) => stderrBufs.push(c));
|
|
1084
|
+
let hasOutput = false;
|
|
1085
|
+
try {
|
|
1086
|
+
for await (const chunk2 of proc.stdout) {
|
|
1087
|
+
hasOutput = true;
|
|
1088
|
+
yield chunk2;
|
|
1089
|
+
}
|
|
1090
|
+
} finally {
|
|
1091
|
+
if (proc.exitCode === null) proc.kill();
|
|
1092
|
+
}
|
|
1093
|
+
const exitCode = await new Promise((resolve) => {
|
|
1094
|
+
proc.exitCode !== null ? resolve(proc.exitCode) : proc.on("close", (c) => resolve(c ?? 0));
|
|
1095
|
+
});
|
|
1096
|
+
if (!hasOutput && exitCode !== 0) {
|
|
1097
|
+
const stderr = Buffer.concat(stderrBufs).toString().trim();
|
|
1098
|
+
throw new Error(stderr || `${(0, import_path3.basename)(cmd)} exited with code ${exitCode}`);
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
var GenericCLIBackend = class {
|
|
1102
|
+
constructor(def) {
|
|
1103
|
+
this.type = "cli";
|
|
1104
|
+
this.name = def.name;
|
|
1105
|
+
this.displayName = `${def.name} (CLI)`;
|
|
1106
|
+
this.prefixes = def.prefixes;
|
|
1107
|
+
this.def = def;
|
|
1108
|
+
}
|
|
1109
|
+
get bin() {
|
|
1110
|
+
return which(this.def.bin) || `${HOME2}/.local/bin/${this.def.bin}`;
|
|
1111
|
+
}
|
|
1112
|
+
available() {
|
|
1113
|
+
return Boolean(which(this.def.bin)) || (0, import_fs3.existsSync)(`${HOME2}/.local/bin/${this.def.bin}`);
|
|
1114
|
+
}
|
|
1115
|
+
async models() {
|
|
1116
|
+
if (this.def.modelsCmd) {
|
|
1117
|
+
try {
|
|
1118
|
+
const out = (0, import_child_process3.execFileSync)(this.bin, this.def.modelsCmd, {
|
|
1119
|
+
encoding: "utf8",
|
|
1120
|
+
timeout: 5e3,
|
|
1121
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1122
|
+
});
|
|
1123
|
+
const found = extractModelIds(out);
|
|
1124
|
+
if (found.length > 0) return found;
|
|
1125
|
+
} catch {
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
return this.def.models ?? [];
|
|
1129
|
+
}
|
|
1130
|
+
buildArgs(model) {
|
|
1131
|
+
return this.def.args.map((a) => a === "{model}" ? model : a);
|
|
1132
|
+
}
|
|
1133
|
+
async runBlocking(messages, model) {
|
|
1134
|
+
const prompt = messagesToPrompt(messages);
|
|
1135
|
+
const args = this.buildArgs(model);
|
|
1136
|
+
let out;
|
|
1137
|
+
try {
|
|
1138
|
+
if (this.def.promptMode === "stdin") {
|
|
1139
|
+
out = (0, import_child_process3.execFileSync)(this.bin, args, { input: prompt, encoding: "utf8", timeout: 3e5 });
|
|
1140
|
+
} else {
|
|
1141
|
+
out = (0, import_child_process3.execFileSync)(this.bin, [...args, prompt], { encoding: "utf8", timeout: 3e5 });
|
|
1142
|
+
}
|
|
1143
|
+
} catch (e) {
|
|
1144
|
+
throw new Error(e.stderr?.trim() || `${this.def.bin} exited non-zero`);
|
|
1145
|
+
}
|
|
1146
|
+
return [out.trim(), null];
|
|
1147
|
+
}
|
|
1148
|
+
async *stream(messages, model) {
|
|
1149
|
+
const prompt = messagesToPrompt(messages);
|
|
1150
|
+
const args = this.buildArgs(model);
|
|
1151
|
+
if (this.def.promptMode === "stdin") {
|
|
1152
|
+
yield* spawnStream(this.bin, args, prompt);
|
|
1153
|
+
} else {
|
|
1154
|
+
yield* spawnStream(this.bin, [...args, prompt]);
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
};
|
|
1158
|
+
var ClaudeCLIBackend = class {
|
|
1159
|
+
constructor() {
|
|
1160
|
+
this.name = "claude-cli";
|
|
1161
|
+
this.displayName = "Claude Code (CLI)";
|
|
1162
|
+
this.type = "cli";
|
|
1163
|
+
this.prefixes = ["claude"];
|
|
1164
|
+
}
|
|
1165
|
+
get bin() {
|
|
1166
|
+
return process.env.CLAUDE_BIN ?? which("claude") ?? `${HOME2}/.local/bin/claude`;
|
|
1167
|
+
}
|
|
1168
|
+
available() {
|
|
1169
|
+
return Boolean(which("claude")) || (0, import_fs3.existsSync)(`${HOME2}/.local/bin/claude`);
|
|
1170
|
+
}
|
|
1171
|
+
async models() {
|
|
1172
|
+
const bin = which("claude") || this.bin;
|
|
1173
|
+
const found = await tryDiscover(bin, [["models"], ["models", "list"], ["model", "list"], ["--list-models"]]);
|
|
1174
|
+
if (found.length > 0) return found;
|
|
1175
|
+
return ["claude-opus-4-6", "claude-sonnet-4-6", "claude-haiku-4-5-20251001"];
|
|
1176
|
+
}
|
|
1177
|
+
async runBlocking(messages, model) {
|
|
1178
|
+
const prompt = messagesToPrompt(messages);
|
|
1179
|
+
let out;
|
|
1180
|
+
try {
|
|
1181
|
+
out = (0, import_child_process3.execFileSync)(this.bin, ["-p", "--output-format", "json", "--model", model], {
|
|
1182
|
+
input: prompt,
|
|
1183
|
+
encoding: "utf8",
|
|
1184
|
+
timeout: 3e5
|
|
1185
|
+
});
|
|
1186
|
+
} catch (e) {
|
|
1187
|
+
throw new Error(e.stderr?.trim() || "claude exited non-zero");
|
|
1188
|
+
}
|
|
1189
|
+
try {
|
|
1190
|
+
const data = JSON.parse(out.trim() || "{}");
|
|
1191
|
+
return [data.result ?? "", data.usage ?? null];
|
|
1192
|
+
} catch {
|
|
1193
|
+
return [out.trim(), null];
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
async *stream(messages, model) {
|
|
1197
|
+
const prompt = messagesToPrompt(messages);
|
|
1198
|
+
yield* spawnStream(this.bin, ["-p", "--output-format", "text", "--model", model], prompt);
|
|
1199
|
+
}
|
|
1200
|
+
};
|
|
1201
|
+
var GeminiCLIBackend = class {
|
|
1202
|
+
constructor() {
|
|
1203
|
+
this.name = "gemini-cli";
|
|
1204
|
+
this.displayName = "Gemini (CLI)";
|
|
1205
|
+
this.type = "cli";
|
|
1206
|
+
this.prefixes = ["gemini"];
|
|
1207
|
+
}
|
|
1208
|
+
get bin() {
|
|
1209
|
+
return process.env.GEMINI_BIN ?? which("gemini") ?? "/opt/homebrew/bin/gemini";
|
|
1210
|
+
}
|
|
1211
|
+
available() {
|
|
1212
|
+
return Boolean(which("gemini")) || (0, import_fs3.existsSync)(this.bin);
|
|
1213
|
+
}
|
|
1214
|
+
async models() {
|
|
1215
|
+
const bin = which("gemini") || this.bin;
|
|
1216
|
+
const found = await tryDiscover(bin, [["models"], ["models", "list"], ["--list-models"], ["list-models"]]);
|
|
1217
|
+
if (found.length > 0) return found;
|
|
1218
|
+
return ["gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.0-flash"];
|
|
1219
|
+
}
|
|
1220
|
+
async runBlocking(messages, model) {
|
|
1221
|
+
const prompt = messagesToPrompt(messages);
|
|
1222
|
+
let out;
|
|
1223
|
+
try {
|
|
1224
|
+
out = (0, import_child_process3.execFileSync)(this.bin, ["--output-format", "json", "--model", model, "--approval-mode", "yolo"], {
|
|
1225
|
+
input: prompt,
|
|
1226
|
+
encoding: "utf8",
|
|
1227
|
+
timeout: 3e5,
|
|
1228
|
+
env: process.env
|
|
1229
|
+
});
|
|
1230
|
+
} catch (e) {
|
|
1231
|
+
const err = e.stderr?.trim() ?? "";
|
|
1232
|
+
if (/auth|login|sign.?in/i.test(err))
|
|
1233
|
+
throw new Error("Gemini not authenticated. Run: gemini auth OR export GEMINI_API_KEY=<key>");
|
|
1234
|
+
throw new Error(err || "gemini exited non-zero");
|
|
1235
|
+
}
|
|
1236
|
+
const raw = out.trim();
|
|
1237
|
+
try {
|
|
1238
|
+
const data = JSON.parse(raw);
|
|
1239
|
+
return [String(data.response ?? data.result ?? data.text ?? raw), data.tokenCount ?? data.usage ?? null];
|
|
1240
|
+
} catch {
|
|
1241
|
+
return [raw, null];
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
async *stream(messages, model) {
|
|
1245
|
+
const prompt = messagesToPrompt(messages);
|
|
1246
|
+
yield* spawnStream(this.bin, ["--output-format", "text", "--model", model, "--approval-mode", "yolo"], prompt);
|
|
1247
|
+
}
|
|
1248
|
+
};
|
|
1249
|
+
var CodexCLIBackend = class {
|
|
1250
|
+
constructor() {
|
|
1251
|
+
this.name = "codex-cli";
|
|
1252
|
+
this.displayName = "Codex (CLI)";
|
|
1253
|
+
this.type = "cli";
|
|
1254
|
+
this.prefixes = ["gpt", "o3", "o4"];
|
|
1255
|
+
}
|
|
1256
|
+
get bin() {
|
|
1257
|
+
return process.env.CODEX_BIN ?? which("codex") ?? "codex";
|
|
1258
|
+
}
|
|
1259
|
+
available() {
|
|
1260
|
+
return Boolean(which("codex"));
|
|
1261
|
+
}
|
|
1262
|
+
async models() {
|
|
1263
|
+
const found = await tryDiscover(this.bin, [["models"], ["models", "list"], ["--list-models"], ["list-models"]]);
|
|
1264
|
+
if (found.length > 0) return found;
|
|
1265
|
+
return ["gpt-4o", "gpt-4o-mini", "o3", "o4-mini"];
|
|
1266
|
+
}
|
|
1267
|
+
async runBlocking(messages, model) {
|
|
1268
|
+
const prompt = messagesToPrompt(messages);
|
|
1269
|
+
let out;
|
|
1270
|
+
try {
|
|
1271
|
+
out = (0, import_child_process3.execFileSync)(this.bin, ["-q", "--model", model, prompt], { encoding: "utf8", timeout: 3e5 });
|
|
1272
|
+
} catch (e) {
|
|
1273
|
+
throw new Error(e.stderr?.trim() || "codex exited non-zero");
|
|
1274
|
+
}
|
|
1275
|
+
return [out.trim(), null];
|
|
1276
|
+
}
|
|
1277
|
+
async *stream(messages, model) {
|
|
1278
|
+
const prompt = messagesToPrompt(messages);
|
|
1279
|
+
yield* spawnStream(this.bin, ["-q", "--model", model, prompt]);
|
|
1280
|
+
}
|
|
1281
|
+
};
|
|
1282
|
+
var CopilotCLIBackend = class {
|
|
1283
|
+
constructor() {
|
|
1284
|
+
this.name = "copilot-cli";
|
|
1285
|
+
this.displayName = "GitHub Copilot (CLI)";
|
|
1286
|
+
this.type = "cli";
|
|
1287
|
+
this.prefixes = ["copilot"];
|
|
1288
|
+
}
|
|
1289
|
+
get bin() {
|
|
1290
|
+
return process.env.GH_BIN ?? which("gh") ?? "gh";
|
|
1291
|
+
}
|
|
1292
|
+
available() {
|
|
1293
|
+
if (!which("gh")) return false;
|
|
1294
|
+
try {
|
|
1295
|
+
(0, import_child_process3.execFileSync)("gh", ["copilot", "--version"], { encoding: "utf8", stdio: "pipe" });
|
|
1296
|
+
return true;
|
|
1297
|
+
} catch {
|
|
1298
|
+
return false;
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
async models() {
|
|
1302
|
+
const found = await tryDiscover(this.bin, [["copilot", "models"], ["copilot", "models", "list"]]);
|
|
1303
|
+
if (found.length > 0) return found;
|
|
1304
|
+
return ["copilot"];
|
|
1305
|
+
}
|
|
1306
|
+
async runBlocking(messages, model) {
|
|
1307
|
+
const prompt = messagesToPrompt(messages);
|
|
1308
|
+
let out;
|
|
1309
|
+
try {
|
|
1310
|
+
out = (0, import_child_process3.execFileSync)(this.bin, ["copilot", "explain", prompt], { encoding: "utf8", timeout: 12e4 });
|
|
1311
|
+
} catch (e) {
|
|
1312
|
+
throw new Error(e.stderr?.trim() || "gh copilot exited non-zero");
|
|
1313
|
+
}
|
|
1314
|
+
return [out.trim(), null];
|
|
1315
|
+
}
|
|
1316
|
+
async *stream(messages, model) {
|
|
1317
|
+
const prompt = messagesToPrompt(messages);
|
|
1318
|
+
yield* spawnStream(this.bin, ["copilot", "explain", prompt]);
|
|
1319
|
+
}
|
|
1320
|
+
};
|
|
1321
|
+
var DroidCLIBackend = class _DroidCLIBackend {
|
|
1322
|
+
constructor() {
|
|
1323
|
+
this.name = "droid-cli";
|
|
1324
|
+
this.displayName = "Droid (CLI)";
|
|
1325
|
+
this.type = "cli";
|
|
1326
|
+
this.prefixes = ["droid", "factory", "glm", "kimi", "minimax"];
|
|
1327
|
+
}
|
|
1328
|
+
static {
|
|
1329
|
+
this.KNOWN_MODELS = [
|
|
1330
|
+
"claude-opus-4-6",
|
|
1331
|
+
"claude-sonnet-4-6",
|
|
1332
|
+
"claude-haiku-4-5-20251001",
|
|
1333
|
+
"gpt-4o",
|
|
1334
|
+
"gpt-4o-mini",
|
|
1335
|
+
"glm-4.7",
|
|
1336
|
+
"glm-5",
|
|
1337
|
+
"kimi-k2.5",
|
|
1338
|
+
"minimax-m2.5"
|
|
1339
|
+
];
|
|
1340
|
+
}
|
|
1341
|
+
get bin() {
|
|
1342
|
+
return process.env.DROID_BIN ?? which("droid") ?? `${HOME2}/.local/bin/droid`;
|
|
1343
|
+
}
|
|
1344
|
+
available() {
|
|
1345
|
+
return Boolean(which("droid")) || (0, import_fs3.existsSync)(`${HOME2}/.local/bin/droid`);
|
|
1346
|
+
}
|
|
1347
|
+
async models() {
|
|
1348
|
+
try {
|
|
1349
|
+
const help = (0, import_child_process3.execFileSync)(which("droid") || this.bin, ["exec", "--help"], {
|
|
1350
|
+
encoding: "utf8",
|
|
1351
|
+
timeout: 5e3,
|
|
1352
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1353
|
+
});
|
|
1354
|
+
const found = extractModelIds(help);
|
|
1355
|
+
if (found.length > 0) return found;
|
|
1356
|
+
} catch {
|
|
1357
|
+
}
|
|
1358
|
+
return _DroidCLIBackend.KNOWN_MODELS;
|
|
1359
|
+
}
|
|
1360
|
+
async runBlocking(messages, model) {
|
|
1361
|
+
const prompt = messagesToPrompt(messages);
|
|
1362
|
+
let out;
|
|
1363
|
+
try {
|
|
1364
|
+
out = (0, import_child_process3.execFileSync)(
|
|
1365
|
+
which("droid") || this.bin,
|
|
1366
|
+
["exec", "--output-format", "text", "--model", model],
|
|
1367
|
+
{ input: prompt, encoding: "utf8", timeout: 3e5 }
|
|
1368
|
+
);
|
|
1369
|
+
} catch (e) {
|
|
1370
|
+
throw new Error(e.stderr?.trim() || "droid exited non-zero");
|
|
1371
|
+
}
|
|
1372
|
+
return [out.trim(), null];
|
|
1373
|
+
}
|
|
1374
|
+
async *stream(messages, model) {
|
|
1375
|
+
const prompt = messagesToPrompt(messages);
|
|
1376
|
+
yield* spawnStream(
|
|
1377
|
+
which("droid") || this.bin,
|
|
1378
|
+
["exec", "--output-format", "text", "--model", model],
|
|
1379
|
+
prompt
|
|
1380
|
+
);
|
|
1381
|
+
}
|
|
1382
|
+
};
|
|
1383
|
+
function buildCLIBackends() {
|
|
1384
|
+
const builtins = [
|
|
1385
|
+
new ClaudeCLIBackend(),
|
|
1386
|
+
new GeminiCLIBackend(),
|
|
1387
|
+
new CodexCLIBackend(),
|
|
1388
|
+
new CopilotCLIBackend(),
|
|
1389
|
+
new DroidCLIBackend()
|
|
1390
|
+
];
|
|
1391
|
+
try {
|
|
1392
|
+
const cfg = loadConfig();
|
|
1393
|
+
const custom = (cfg.customBackends ?? []).map((def) => new GenericCLIBackend(def));
|
|
1394
|
+
return [...builtins, ...custom];
|
|
1395
|
+
} catch {
|
|
1396
|
+
return builtins;
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
// src/cli.ts
|
|
1401
|
+
var import_fs4 = require("fs");
|
|
1402
|
+
var import_os5 = require("os");
|
|
1403
|
+
var import_path4 = require("path");
|
|
1404
|
+
var import_readline = require("readline");
|
|
1405
|
+
var DEFAULT_PORT = parseInt(process.env.APISPOOF_PORT ?? "8082");
|
|
1406
|
+
var LOG_DIR = configDir();
|
|
1407
|
+
function initProviders() {
|
|
1408
|
+
const api = [new AnthropicProvider(), new OpenAIProvider(), new GoogleProvider()];
|
|
1409
|
+
const cli = buildCLIBackends();
|
|
1410
|
+
registerProviders([...api, ...cli]);
|
|
1411
|
+
}
|
|
1412
|
+
function ask(question) {
|
|
1413
|
+
const rl = (0, import_readline.createInterface)({ input: process.stdin, output: process.stdout });
|
|
1414
|
+
return new Promise((resolve) => {
|
|
1415
|
+
rl.question(question, (answer) => {
|
|
1416
|
+
rl.close();
|
|
1417
|
+
resolve(answer.trim());
|
|
1418
|
+
});
|
|
1419
|
+
});
|
|
1420
|
+
}
|
|
1421
|
+
function askHidden(question) {
|
|
1422
|
+
return new Promise((resolve) => {
|
|
1423
|
+
const rl = (0, import_readline.createInterface)({ input: process.stdin, output: process.stdout });
|
|
1424
|
+
process.stdout.write(question);
|
|
1425
|
+
const stdin = process.stdin;
|
|
1426
|
+
if (stdin.setRawMode) stdin.setRawMode(true);
|
|
1427
|
+
let input = "";
|
|
1428
|
+
const onData = (c) => {
|
|
1429
|
+
const ch = c.toString();
|
|
1430
|
+
if (ch === "\n" || ch === "\r") {
|
|
1431
|
+
if (stdin.setRawMode) stdin.setRawMode(false);
|
|
1432
|
+
process.stdin.removeListener("data", onData);
|
|
1433
|
+
process.stdout.write("\n");
|
|
1434
|
+
rl.close();
|
|
1435
|
+
resolve(input);
|
|
1436
|
+
} else if (ch === "") {
|
|
1437
|
+
process.exit(1);
|
|
1438
|
+
} else if (ch === "\x7F") {
|
|
1439
|
+
input = input.slice(0, -1);
|
|
1440
|
+
} else {
|
|
1441
|
+
input += ch;
|
|
1442
|
+
process.stdout.write("*");
|
|
1443
|
+
}
|
|
1444
|
+
};
|
|
1445
|
+
process.stdin.on("data", onData);
|
|
1446
|
+
});
|
|
1447
|
+
}
|
|
1448
|
+
async function printBanner(port) {
|
|
1449
|
+
const apiKey = getOrCreateApiKey();
|
|
1450
|
+
const providers = availableProviders();
|
|
1451
|
+
const models = await allModels();
|
|
1452
|
+
console.log();
|
|
1453
|
+
console.log(" apispoof is running");
|
|
1454
|
+
console.log();
|
|
1455
|
+
console.log(` Base URL : http://127.0.0.1:${port}/v1`);
|
|
1456
|
+
console.log(` API Key : ${apiKey}`);
|
|
1457
|
+
console.log(` Providers : ${providers.map((p) => p.displayName).join(", ") || "none"}`);
|
|
1458
|
+
console.log(` Models : ${models.length} available`);
|
|
1459
|
+
console.log(` Logs : ${LOG_DIR}/server.log`);
|
|
1460
|
+
console.log();
|
|
1461
|
+
console.log(" Use in your tools:");
|
|
1462
|
+
console.log(` OPENAI_API_BASE=http://127.0.0.1:${port}/v1`);
|
|
1463
|
+
console.log(` OPENAI_API_KEY=${apiKey}`);
|
|
1464
|
+
console.log();
|
|
1465
|
+
}
|
|
1466
|
+
async function cmdConnect(providerArg) {
|
|
1467
|
+
const providers = [
|
|
1468
|
+
{ name: "anthropic", label: "Anthropic (Claude)", method: "OAuth \u2014 connect with your Claude account" },
|
|
1469
|
+
{ name: "openai", label: "OpenAI", method: "API Key \u2014 from platform.openai.com/api-keys" },
|
|
1470
|
+
{ name: "google", label: "Google AI (Gemini)", method: "API Key \u2014 from aistudio.google.com/apikey" }
|
|
1471
|
+
];
|
|
1472
|
+
let provider;
|
|
1473
|
+
if (providerArg) {
|
|
1474
|
+
const match = providers.find((p) => p.name === providerArg.toLowerCase() || p.label.toLowerCase().includes(providerArg.toLowerCase()));
|
|
1475
|
+
if (!match) {
|
|
1476
|
+
console.error(` Unknown provider: ${providerArg}`);
|
|
1477
|
+
console.error(` Available: ${providers.map((p) => p.name).join(", ")}`);
|
|
1478
|
+
process.exit(1);
|
|
1479
|
+
}
|
|
1480
|
+
provider = match;
|
|
1481
|
+
} else {
|
|
1482
|
+
console.log();
|
|
1483
|
+
console.log(" Connect an AI provider");
|
|
1484
|
+
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");
|
|
1485
|
+
console.log();
|
|
1486
|
+
providers.forEach((p, i) => {
|
|
1487
|
+
console.log(` ${i + 1}. ${p.label}`);
|
|
1488
|
+
console.log(` ${p.method}`);
|
|
1489
|
+
});
|
|
1490
|
+
console.log();
|
|
1491
|
+
const choice = await ask(" Choose [1/2/3]: ");
|
|
1492
|
+
const idx = parseInt(choice) - 1;
|
|
1493
|
+
if (idx < 0 || idx >= providers.length) {
|
|
1494
|
+
console.log(" Cancelled.");
|
|
1495
|
+
return;
|
|
1496
|
+
}
|
|
1497
|
+
provider = providers[idx];
|
|
1498
|
+
}
|
|
1499
|
+
console.log();
|
|
1500
|
+
console.log(` Connecting to ${provider.label}...`);
|
|
1501
|
+
try {
|
|
1502
|
+
if (provider.name === "anthropic") {
|
|
1503
|
+
const account = await connectAnthropic();
|
|
1504
|
+
console.log();
|
|
1505
|
+
console.log(` Connected as ${account.email ?? "unknown user"}`);
|
|
1506
|
+
const ap = new AnthropicProvider();
|
|
1507
|
+
const models = await ap.models();
|
|
1508
|
+
console.log(` Available models: ${models.join(", ")}`);
|
|
1509
|
+
} else if (provider.name === "openai") {
|
|
1510
|
+
console.log();
|
|
1511
|
+
const key = await askHidden(" Enter your OpenAI API key: ");
|
|
1512
|
+
if (!key) {
|
|
1513
|
+
console.log(" Cancelled.");
|
|
1514
|
+
return;
|
|
1515
|
+
}
|
|
1516
|
+
await connectOpenAI(key);
|
|
1517
|
+
console.log();
|
|
1518
|
+
const op = new OpenAIProvider();
|
|
1519
|
+
const models = await op.models();
|
|
1520
|
+
console.log(` Connected (${models.length} models available)`);
|
|
1521
|
+
const popular = models.filter((m) => /^(gpt-[45]|o[134])/.test(m)).slice(0, 8);
|
|
1522
|
+
if (popular.length) console.log(` Popular: ${popular.join(", ")}`);
|
|
1523
|
+
} else if (provider.name === "google") {
|
|
1524
|
+
console.log();
|
|
1525
|
+
const key = await askHidden(" Enter your Gemini API key: ");
|
|
1526
|
+
if (!key) {
|
|
1527
|
+
console.log(" Cancelled.");
|
|
1528
|
+
return;
|
|
1529
|
+
}
|
|
1530
|
+
await connectGoogle(key);
|
|
1531
|
+
console.log();
|
|
1532
|
+
const gp = new GoogleProvider();
|
|
1533
|
+
const models = await gp.models();
|
|
1534
|
+
console.log(` Connected (${models.length} models available)`);
|
|
1535
|
+
const gemini = models.filter((m) => m.startsWith("gemini")).slice(0, 6);
|
|
1536
|
+
if (gemini.length) console.log(` Models: ${gemini.join(", ")}`);
|
|
1537
|
+
}
|
|
1538
|
+
console.log();
|
|
1539
|
+
} catch (err) {
|
|
1540
|
+
console.error(`
|
|
1541
|
+
Connection failed: ${err.message}`);
|
|
1542
|
+
process.exit(1);
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
async function cmdDisconnect(providerArg) {
|
|
1546
|
+
if (!providerArg) {
|
|
1547
|
+
console.error(" Usage: apispoof disconnect <provider>");
|
|
1548
|
+
console.error(" Providers: anthropic, openai, google");
|
|
1549
|
+
process.exit(1);
|
|
1550
|
+
}
|
|
1551
|
+
const name = providerArg.toLowerCase();
|
|
1552
|
+
if (!["anthropic", "openai", "google"].includes(name)) {
|
|
1553
|
+
console.error(` Unknown provider: ${providerArg}`);
|
|
1554
|
+
process.exit(1);
|
|
1555
|
+
}
|
|
1556
|
+
const removed = removeAccount(name);
|
|
1557
|
+
if (removed) {
|
|
1558
|
+
console.log(` Disconnected from ${name}`);
|
|
1559
|
+
} else {
|
|
1560
|
+
console.log(` ${name} was not connected`);
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
function cmdAccounts() {
|
|
1564
|
+
const accounts = listAccounts();
|
|
1565
|
+
console.log();
|
|
1566
|
+
console.log(" Connected accounts");
|
|
1567
|
+
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");
|
|
1568
|
+
console.log();
|
|
1569
|
+
if (accounts.length === 0) {
|
|
1570
|
+
console.log(" No accounts connected.");
|
|
1571
|
+
console.log(" Run: apispoof connect");
|
|
1572
|
+
console.log();
|
|
1573
|
+
return;
|
|
1574
|
+
}
|
|
1575
|
+
for (const a of accounts) {
|
|
1576
|
+
const label = a.provider === "anthropic" ? "Anthropic (Claude)" : a.provider === "openai" ? "OpenAI" : "Google AI (Gemini)";
|
|
1577
|
+
const method = a.provider === "anthropic" ? "OAuth" : "API Key";
|
|
1578
|
+
const since = new Date(a.connectedAt).toLocaleDateString();
|
|
1579
|
+
const status = a.enabled ? "active" : "disabled";
|
|
1580
|
+
console.log(` ${a.enabled ? "+" : "-"} ${label}`);
|
|
1581
|
+
console.log(` Method: ${method} | Since: ${since} | Status: ${status}`);
|
|
1582
|
+
if (a.email) console.log(` Email: ${a.email}`);
|
|
1583
|
+
if (a.expiresAt) {
|
|
1584
|
+
const remaining = Math.max(0, Math.round((a.expiresAt - Date.now()) / 6e4));
|
|
1585
|
+
console.log(` Token: ${remaining > 0 ? `${remaining}min remaining` : "expired (will auto-refresh)"}`);
|
|
1586
|
+
}
|
|
1587
|
+
console.log();
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
async function cmdModels() {
|
|
1591
|
+
console.log();
|
|
1592
|
+
console.log(" Available models");
|
|
1593
|
+
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");
|
|
1594
|
+
console.log();
|
|
1595
|
+
const models = await allModels();
|
|
1596
|
+
if (models.length === 0) {
|
|
1597
|
+
console.log(" No models available.");
|
|
1598
|
+
console.log(" Run: apispoof connect to add a provider");
|
|
1599
|
+
console.log();
|
|
1600
|
+
return;
|
|
1601
|
+
}
|
|
1602
|
+
const byProvider = /* @__PURE__ */ new Map();
|
|
1603
|
+
for (const m of models) {
|
|
1604
|
+
const list = byProvider.get(m.provider) ?? [];
|
|
1605
|
+
list.push(m.id);
|
|
1606
|
+
byProvider.set(m.provider, list);
|
|
1607
|
+
}
|
|
1608
|
+
for (const [provider, ids] of byProvider) {
|
|
1609
|
+
console.log(` ${provider}:`);
|
|
1610
|
+
for (const id of ids.slice(0, 12)) {
|
|
1611
|
+
console.log(` ${id}`);
|
|
1612
|
+
}
|
|
1613
|
+
if (ids.length > 12) console.log(` ... +${ids.length - 12} more`);
|
|
1614
|
+
console.log();
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
async function cmdSetup() {
|
|
1618
|
+
console.log();
|
|
1619
|
+
console.log(" apispoof \u2014 AI API Bridge");
|
|
1620
|
+
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");
|
|
1621
|
+
console.log(" Connect your AI accounts to create an OpenAI-compatible API.");
|
|
1622
|
+
console.log(" Use with Goose, Cursor, Aider, Continue, or any tool.");
|
|
1623
|
+
console.log();
|
|
1624
|
+
const accounts = listAccounts();
|
|
1625
|
+
const cliAvailable = buildCLIBackends().filter((b) => b.available());
|
|
1626
|
+
if (accounts.length > 0) {
|
|
1627
|
+
console.log(" Connected accounts:");
|
|
1628
|
+
for (const a of accounts) {
|
|
1629
|
+
const label = a.provider === "anthropic" ? "Anthropic" : a.provider === "openai" ? "OpenAI" : "Google";
|
|
1630
|
+
console.log(` + ${label}${a.email ? ` (${a.email})` : ""}`);
|
|
1631
|
+
}
|
|
1632
|
+
console.log();
|
|
1633
|
+
}
|
|
1634
|
+
if (cliAvailable.length > 0) {
|
|
1635
|
+
console.log(" CLI backends detected:");
|
|
1636
|
+
for (const b of cliAvailable) console.log(` + ${b.displayName}`);
|
|
1637
|
+
console.log();
|
|
1638
|
+
}
|
|
1639
|
+
const wantsConnect = await ask(" Connect a new account? [Y/n]: ");
|
|
1640
|
+
if (wantsConnect.toLowerCase() !== "n") {
|
|
1641
|
+
await cmdConnect();
|
|
1642
|
+
}
|
|
1643
|
+
let more = true;
|
|
1644
|
+
while (more) {
|
|
1645
|
+
const again = await ask(" Connect another provider? [y/N]: ");
|
|
1646
|
+
if (again.toLowerCase() === "y") {
|
|
1647
|
+
await cmdConnect();
|
|
1648
|
+
} else {
|
|
1649
|
+
more = false;
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
const cfg = loadConfig();
|
|
1653
|
+
const defaultPort = cfg.port ?? DEFAULT_PORT;
|
|
1654
|
+
const portAnswer = await ask(` Port [${defaultPort}]: `);
|
|
1655
|
+
const port = portAnswer ? parseInt(portAnswer) || defaultPort : defaultPort;
|
|
1656
|
+
saveConfig({ ...cfg, port });
|
|
1657
|
+
console.log();
|
|
1658
|
+
console.log(" How to run apispoof?");
|
|
1659
|
+
console.log(" 1 Foreground (stops when terminal closes)");
|
|
1660
|
+
console.log(" 2 Background service (auto-starts on login)");
|
|
1661
|
+
console.log();
|
|
1662
|
+
const choice = await ask(" Choose [1/2]: ");
|
|
1663
|
+
console.log();
|
|
1664
|
+
if (choice === "2") {
|
|
1665
|
+
cmdInstall(port);
|
|
1666
|
+
} else {
|
|
1667
|
+
cmdStart(port);
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
function cmdStart(port) {
|
|
1671
|
+
(0, import_fs4.mkdirSync)(LOG_DIR, { recursive: true });
|
|
1672
|
+
const cfg = loadConfig();
|
|
1673
|
+
if (cfg.backend) process.env.APISPOOF_BACKEND = cfg.backend;
|
|
1674
|
+
const available = availableProviders();
|
|
1675
|
+
if (available.length === 0) {
|
|
1676
|
+
console.error(" No providers available.");
|
|
1677
|
+
console.error(" Run: apispoof connect to add an account");
|
|
1678
|
+
console.error(" Or install a CLI: claude, gemini, codex, etc.");
|
|
1679
|
+
process.exit(1);
|
|
1680
|
+
}
|
|
1681
|
+
const server = createSpoofServer(port);
|
|
1682
|
+
server.listen(port, "127.0.0.1", async () => {
|
|
1683
|
+
allModels().catch(() => {
|
|
1684
|
+
});
|
|
1685
|
+
await printBanner(port);
|
|
1686
|
+
console.log(" Ctrl+C to stop.");
|
|
1687
|
+
});
|
|
1688
|
+
server.on("error", (err) => {
|
|
1689
|
+
if (err.code === "EADDRINUSE") {
|
|
1690
|
+
console.error(` Port ${port} already in use. Try: apispoof start --port 9000`);
|
|
1691
|
+
} else {
|
|
1692
|
+
console.error(" Server error:", err.message);
|
|
1693
|
+
}
|
|
1694
|
+
process.exit(1);
|
|
1695
|
+
});
|
|
1696
|
+
}
|
|
1697
|
+
function cmdInstall(port) {
|
|
1698
|
+
try {
|
|
1699
|
+
installService(port);
|
|
1700
|
+
console.log(" Waiting for server to start...");
|
|
1701
|
+
let attempts = 0;
|
|
1702
|
+
const poll = setInterval(async () => {
|
|
1703
|
+
attempts++;
|
|
1704
|
+
try {
|
|
1705
|
+
const http = await import("http");
|
|
1706
|
+
http.get(`http://127.0.0.1:${port}/health`, (res) => {
|
|
1707
|
+
if (res.statusCode === 200) {
|
|
1708
|
+
clearInterval(poll);
|
|
1709
|
+
printBanner(port).then(() => {
|
|
1710
|
+
console.log(" Stop: apispoof uninstall");
|
|
1711
|
+
process.exit(0);
|
|
1712
|
+
});
|
|
1713
|
+
}
|
|
1714
|
+
}).on("error", () => {
|
|
1715
|
+
});
|
|
1716
|
+
} catch {
|
|
1717
|
+
}
|
|
1718
|
+
if (attempts >= 15) {
|
|
1719
|
+
clearInterval(poll);
|
|
1720
|
+
console.log(`
|
|
1721
|
+
Server did not respond. Check: tail -f ${LOG_DIR}/server.log`);
|
|
1722
|
+
process.exit(1);
|
|
1723
|
+
}
|
|
1724
|
+
}, 600);
|
|
1725
|
+
} catch (err) {
|
|
1726
|
+
console.error(" Install failed:", err.message);
|
|
1727
|
+
process.exit(1);
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
function cmdUninstall() {
|
|
1731
|
+
try {
|
|
1732
|
+
uninstallService();
|
|
1733
|
+
} catch (err) {
|
|
1734
|
+
console.error(" Uninstall failed:", err.message);
|
|
1735
|
+
process.exit(1);
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
async function cmdStatus(port) {
|
|
1739
|
+
const { running, pid } = serviceStatus();
|
|
1740
|
+
if (running) {
|
|
1741
|
+
await printBanner(port);
|
|
1742
|
+
if (pid) console.log(` PID: ${pid}`);
|
|
1743
|
+
} else {
|
|
1744
|
+
console.log(" apispoof is not running.");
|
|
1745
|
+
console.log();
|
|
1746
|
+
console.log(" Run: apispoof -> setup wizard");
|
|
1747
|
+
console.log(" Run: apispoof start -> start in foreground");
|
|
1748
|
+
console.log(" Run: apispoof install -> install background service");
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
function cmdKey(args) {
|
|
1752
|
+
if (args[0] === "reset") {
|
|
1753
|
+
const cfg2 = loadConfig();
|
|
1754
|
+
const { apiKey: _, ...rest } = cfg2;
|
|
1755
|
+
saveConfig(rest);
|
|
1756
|
+
const newKey = getOrCreateApiKey();
|
|
1757
|
+
console.log(` API key regenerated: ${newKey}`);
|
|
1758
|
+
return;
|
|
1759
|
+
}
|
|
1760
|
+
const apiKey = getOrCreateApiKey();
|
|
1761
|
+
const cfg = loadConfig();
|
|
1762
|
+
const port = cfg.port ?? DEFAULT_PORT;
|
|
1763
|
+
console.log();
|
|
1764
|
+
console.log(" apispoof API key (OpenAI-compatible)");
|
|
1765
|
+
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");
|
|
1766
|
+
console.log();
|
|
1767
|
+
console.log(` ${apiKey}`);
|
|
1768
|
+
console.log();
|
|
1769
|
+
console.log(" Quick setup:");
|
|
1770
|
+
console.log(` export OPENAI_API_BASE=http://127.0.0.1:${port}/v1`);
|
|
1771
|
+
console.log(` export OPENAI_API_KEY=${apiKey}`);
|
|
1772
|
+
console.log();
|
|
1773
|
+
console.log(" Reset: apispoof key reset");
|
|
1774
|
+
console.log();
|
|
1775
|
+
}
|
|
1776
|
+
function cmdConfig(args) {
|
|
1777
|
+
const cfg = loadConfig();
|
|
1778
|
+
if (args[0] === "set") {
|
|
1779
|
+
for (const pair of args.slice(1)) {
|
|
1780
|
+
const eqIdx = pair.indexOf("=");
|
|
1781
|
+
if (eqIdx === -1) {
|
|
1782
|
+
console.error(` Invalid format: ${pair} (use key=value)`);
|
|
1783
|
+
process.exit(1);
|
|
1784
|
+
}
|
|
1785
|
+
const key = pair.slice(0, eqIdx);
|
|
1786
|
+
const val = pair.slice(eqIdx + 1);
|
|
1787
|
+
if (key === "port") {
|
|
1788
|
+
const p = parseInt(val);
|
|
1789
|
+
if (isNaN(p) || p < 1 || p > 65535) {
|
|
1790
|
+
console.error(" Invalid port");
|
|
1791
|
+
process.exit(1);
|
|
1792
|
+
}
|
|
1793
|
+
saveConfig({ ...cfg, port: p });
|
|
1794
|
+
console.log(` port -> ${p}`);
|
|
1795
|
+
} else {
|
|
1796
|
+
console.error(` Unknown key: ${key} (valid: port)`);
|
|
1797
|
+
process.exit(1);
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
return;
|
|
1801
|
+
}
|
|
1802
|
+
if (args[0] === "reset") {
|
|
1803
|
+
saveConfig({});
|
|
1804
|
+
console.log(" Config reset to defaults.");
|
|
1805
|
+
return;
|
|
1806
|
+
}
|
|
1807
|
+
console.log();
|
|
1808
|
+
console.log(" apispoof config");
|
|
1809
|
+
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");
|
|
1810
|
+
console.log(` port : ${cfg.port ?? `${DEFAULT_PORT} (default)`}`);
|
|
1811
|
+
console.log(` apiKey : ${cfg.apiKey ?? "(not generated yet)"}`);
|
|
1812
|
+
console.log(` accounts : ${(cfg.accounts ?? []).length} connected`);
|
|
1813
|
+
console.log(` file : ${(0, import_path4.join)((0, import_os5.homedir)(), ".apispoof/config.json")}`);
|
|
1814
|
+
console.log();
|
|
1815
|
+
}
|
|
1816
|
+
async function cmdBackendAdd() {
|
|
1817
|
+
console.log();
|
|
1818
|
+
console.log(" Add a custom backend CLI");
|
|
1819
|
+
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");
|
|
1820
|
+
console.log();
|
|
1821
|
+
const name = await ask(" Backend name (e.g. opencode): ");
|
|
1822
|
+
if (!name) {
|
|
1823
|
+
console.log(" Cancelled.");
|
|
1824
|
+
return;
|
|
1825
|
+
}
|
|
1826
|
+
const bin = await ask(` Binary name in PATH [${name}]: `) || name;
|
|
1827
|
+
const prefixRaw = await ask(` Model prefixes, comma-separated [${name}]: `);
|
|
1828
|
+
const prefixes = prefixRaw ? prefixRaw.split(",").map((s) => s.trim()).filter(Boolean) : [name];
|
|
1829
|
+
console.log(" How is the prompt delivered?");
|
|
1830
|
+
console.log(" 1 stdin (piped to process.stdin)");
|
|
1831
|
+
console.log(" 2 arg (appended as last argument)");
|
|
1832
|
+
const modeChoice = await ask(" Choose [1/2]: ");
|
|
1833
|
+
const promptMode = modeChoice === "2" ? "arg" : "stdin";
|
|
1834
|
+
console.log(" Command args template. Use {model} for the model name.");
|
|
1835
|
+
const argsRaw = await ask(" Args: ");
|
|
1836
|
+
const args = argsRaw.trim().split(/\s+/).filter(Boolean);
|
|
1837
|
+
const modelsCmdRaw = await ask(" Args to list models (blank to skip): ");
|
|
1838
|
+
const modelsCmd = modelsCmdRaw.trim() ? modelsCmdRaw.trim().split(/\s+/) : void 0;
|
|
1839
|
+
const modelsRaw = await ask(" Fallback model list, comma-separated (blank to skip): ");
|
|
1840
|
+
const models = modelsRaw.trim() ? modelsRaw.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
|
|
1841
|
+
const def = { name, bin, prefixes, promptMode, args, modelsCmd, models };
|
|
1842
|
+
const cfg = loadConfig();
|
|
1843
|
+
const existing = cfg.customBackends ?? [];
|
|
1844
|
+
const idx = existing.findIndex((b) => b.name === name);
|
|
1845
|
+
if (idx >= 0) {
|
|
1846
|
+
existing[idx] = def;
|
|
1847
|
+
} else {
|
|
1848
|
+
existing.push(def);
|
|
1849
|
+
}
|
|
1850
|
+
saveConfig({ ...cfg, customBackends: existing });
|
|
1851
|
+
console.log(`
|
|
1852
|
+
${name} backend saved. Restart apispoof for it to take effect.`);
|
|
1853
|
+
}
|
|
1854
|
+
async function cmdBackendList() {
|
|
1855
|
+
console.log();
|
|
1856
|
+
console.log(" CLI Backends:");
|
|
1857
|
+
console.log();
|
|
1858
|
+
const backends = buildCLIBackends();
|
|
1859
|
+
for (const b of backends) {
|
|
1860
|
+
if (!b.available()) {
|
|
1861
|
+
console.log(` - ${b.displayName} (not installed)`);
|
|
1862
|
+
continue;
|
|
1863
|
+
}
|
|
1864
|
+
process.stdout.write(` + ${b.displayName} (discovering models...)\r`);
|
|
1865
|
+
const models = await b.models();
|
|
1866
|
+
const preview = models.slice(0, 6).join(" ");
|
|
1867
|
+
const extra = models.length > 6 ? ` +${models.length - 6} more` : "";
|
|
1868
|
+
console.log(` + ${b.displayName} `);
|
|
1869
|
+
console.log(` ${preview}${extra}`);
|
|
1870
|
+
console.log();
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
async function cmdChat(model) {
|
|
1874
|
+
const models = await allModels();
|
|
1875
|
+
const fallbackModel = models[0]?.id ?? "claude-sonnet-4-6";
|
|
1876
|
+
const chatModel = model ?? fallbackModel;
|
|
1877
|
+
let provider;
|
|
1878
|
+
try {
|
|
1879
|
+
provider = pickProvider(chatModel);
|
|
1880
|
+
} catch (err) {
|
|
1881
|
+
console.error(` ${err.message}`);
|
|
1882
|
+
process.exit(1);
|
|
1883
|
+
}
|
|
1884
|
+
console.log();
|
|
1885
|
+
console.log(` apispoof chat \u2014 ${provider.displayName}`);
|
|
1886
|
+
console.log(` Model: ${chatModel}`);
|
|
1887
|
+
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");
|
|
1888
|
+
console.log(" Type a message and press Enter. Ctrl+C to exit.");
|
|
1889
|
+
console.log();
|
|
1890
|
+
const history = [];
|
|
1891
|
+
const rl = (0, import_readline.createInterface)({ input: process.stdin, output: process.stdout });
|
|
1892
|
+
rl.on("close", () => {
|
|
1893
|
+
console.log("\n Goodbye.");
|
|
1894
|
+
process.exit(0);
|
|
1895
|
+
});
|
|
1896
|
+
const prompt = () => {
|
|
1897
|
+
rl.question("You: ", async (input) => {
|
|
1898
|
+
const text = input.trim();
|
|
1899
|
+
if (!text) {
|
|
1900
|
+
prompt();
|
|
1901
|
+
return;
|
|
1902
|
+
}
|
|
1903
|
+
history.push({ role: "user", content: text });
|
|
1904
|
+
process.stdout.write("\n");
|
|
1905
|
+
let reply = "";
|
|
1906
|
+
try {
|
|
1907
|
+
process.stdout.write(`${provider.displayName}: `);
|
|
1908
|
+
for await (const chunk2 of provider.stream(history, chatModel)) {
|
|
1909
|
+
const piece = chunk2.toString("utf8");
|
|
1910
|
+
process.stdout.write(piece);
|
|
1911
|
+
reply += piece;
|
|
1912
|
+
}
|
|
1913
|
+
} catch (err) {
|
|
1914
|
+
process.stdout.write(`
|
|
1915
|
+
Error: ${err.message}`);
|
|
1916
|
+
}
|
|
1917
|
+
process.stdout.write("\n\n");
|
|
1918
|
+
if (reply) history.push({ role: "assistant", content: reply });
|
|
1919
|
+
prompt();
|
|
1920
|
+
});
|
|
1921
|
+
};
|
|
1922
|
+
prompt();
|
|
1923
|
+
}
|
|
1924
|
+
function showHelp() {
|
|
1925
|
+
console.log(`
|
|
1926
|
+
apispoof \u2014 AI API Bridge
|
|
1927
|
+
Turn your AI subscriptions into an OpenAI-compatible API.
|
|
1928
|
+
|
|
1929
|
+
Account management:
|
|
1930
|
+
apispoof Interactive setup wizard
|
|
1931
|
+
apispoof connect [provider] Connect an account (anthropic, openai, google)
|
|
1932
|
+
apispoof disconnect <provider> Disconnect an account
|
|
1933
|
+
apispoof accounts Show connected accounts
|
|
1934
|
+
apispoof models List all available models
|
|
1935
|
+
|
|
1936
|
+
Server:
|
|
1937
|
+
apispoof start [--port n] Start API server in the foreground
|
|
1938
|
+
apispoof install [--port n] Install as a background service
|
|
1939
|
+
apispoof uninstall Remove background service
|
|
1940
|
+
apispoof status Show service status
|
|
1941
|
+
|
|
1942
|
+
Usage:
|
|
1943
|
+
apispoof key Show your OpenAI-compatible API key
|
|
1944
|
+
apispoof key reset Regenerate the API key
|
|
1945
|
+
apispoof chat [--model m] Chat in the terminal
|
|
1946
|
+
|
|
1947
|
+
Configuration:
|
|
1948
|
+
apispoof config Show saved configuration
|
|
1949
|
+
apispoof config set port=<n> Set default port
|
|
1950
|
+
apispoof config reset Clear saved configuration
|
|
1951
|
+
apispoof backend List all CLI backends
|
|
1952
|
+
apispoof backend add Add a custom CLI backend
|
|
1953
|
+
|
|
1954
|
+
Providers: anthropic (OAuth), openai (API key), google (API key)
|
|
1955
|
+
CLI backends: claude, gemini, codex, copilot, droid
|
|
1956
|
+
`.trim());
|
|
1957
|
+
}
|
|
1958
|
+
function parseArgs() {
|
|
1959
|
+
const cfg = loadConfig();
|
|
1960
|
+
const args = process.argv.slice(2);
|
|
1961
|
+
const cmd = args[0] ?? "";
|
|
1962
|
+
let port = cfg.port ?? DEFAULT_PORT;
|
|
1963
|
+
let model;
|
|
1964
|
+
const rest = [];
|
|
1965
|
+
for (let i = 1; i < args.length; i++) {
|
|
1966
|
+
if ((args[i] === "--port" || args[i] === "-p") && args[i + 1]) {
|
|
1967
|
+
port = parseInt(args[++i]);
|
|
1968
|
+
} else if ((args[i] === "--model" || args[i] === "-m") && args[i + 1]) {
|
|
1969
|
+
model = args[++i];
|
|
1970
|
+
} else {
|
|
1971
|
+
rest.push(args[i]);
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
return { cmd, port, model, rest };
|
|
1975
|
+
}
|
|
1976
|
+
async function main() {
|
|
1977
|
+
initProviders();
|
|
1978
|
+
const { cmd, port, model, rest } = parseArgs();
|
|
1979
|
+
switch (cmd) {
|
|
1980
|
+
case "":
|
|
1981
|
+
case "setup":
|
|
1982
|
+
await cmdSetup();
|
|
1983
|
+
break;
|
|
1984
|
+
case "connect":
|
|
1985
|
+
await cmdConnect(rest[0]);
|
|
1986
|
+
break;
|
|
1987
|
+
case "disconnect":
|
|
1988
|
+
await cmdDisconnect(rest[0]);
|
|
1989
|
+
break;
|
|
1990
|
+
case "accounts":
|
|
1991
|
+
cmdAccounts();
|
|
1992
|
+
break;
|
|
1993
|
+
case "models":
|
|
1994
|
+
await cmdModels();
|
|
1995
|
+
break;
|
|
1996
|
+
case "chat":
|
|
1997
|
+
await cmdChat(model);
|
|
1998
|
+
break;
|
|
1999
|
+
case "start":
|
|
2000
|
+
cmdStart(port);
|
|
2001
|
+
break;
|
|
2002
|
+
case "install":
|
|
2003
|
+
cmdInstall(port);
|
|
2004
|
+
break;
|
|
2005
|
+
case "uninstall":
|
|
2006
|
+
cmdUninstall();
|
|
2007
|
+
break;
|
|
2008
|
+
case "status":
|
|
2009
|
+
await cmdStatus(port);
|
|
2010
|
+
break;
|
|
2011
|
+
case "key":
|
|
2012
|
+
cmdKey(rest);
|
|
2013
|
+
break;
|
|
2014
|
+
case "config":
|
|
2015
|
+
cmdConfig(rest);
|
|
2016
|
+
break;
|
|
2017
|
+
case "backend":
|
|
2018
|
+
if (rest[0] === "add") await cmdBackendAdd();
|
|
2019
|
+
else await cmdBackendList();
|
|
2020
|
+
break;
|
|
2021
|
+
case "help":
|
|
2022
|
+
case "--help":
|
|
2023
|
+
case "-h":
|
|
2024
|
+
showHelp();
|
|
2025
|
+
break;
|
|
2026
|
+
default:
|
|
2027
|
+
showHelp();
|
|
2028
|
+
process.exit(1);
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
main().catch((err) => {
|
|
2032
|
+
console.error(" Fatal:", err.message);
|
|
2033
|
+
process.exit(1);
|
|
2034
|
+
});
|