falconsh 1.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 +181 -0
- package/dist/cli.js +2191 -0
- package/package.json +50 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,2191 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// src/cli.ts
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/agents/claude.ts
|
|
7
|
+
import * as fs2 from "fs";
|
|
8
|
+
import * as path3 from "path";
|
|
9
|
+
|
|
10
|
+
// src/agents/shared/bifrost.ts
|
|
11
|
+
import { spawn } from "child_process";
|
|
12
|
+
import fs from "fs";
|
|
13
|
+
import os from "os";
|
|
14
|
+
import path from "path";
|
|
15
|
+
|
|
16
|
+
// src/utils.ts
|
|
17
|
+
import net from "net";
|
|
18
|
+
function getFreePort() {
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
const server = net.createServer();
|
|
21
|
+
server.unref();
|
|
22
|
+
server.on("error", reject);
|
|
23
|
+
server.listen(0, "127.0.0.1", () => {
|
|
24
|
+
const address = server.address();
|
|
25
|
+
const port = typeof address === "string" ? 0 : address?.port || 0;
|
|
26
|
+
server.close(() => {
|
|
27
|
+
resolve(port);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
function waitForPort(port, timeoutMs = 5e3) {
|
|
33
|
+
const start = Date.now();
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
function check() {
|
|
36
|
+
const socket = new net.Socket();
|
|
37
|
+
socket.setTimeout(200);
|
|
38
|
+
socket.on("connect", () => {
|
|
39
|
+
socket.destroy();
|
|
40
|
+
resolve();
|
|
41
|
+
});
|
|
42
|
+
socket.on("error", () => {
|
|
43
|
+
socket.destroy();
|
|
44
|
+
if (Date.now() - start > timeoutMs) {
|
|
45
|
+
reject(new Error(`Timeout waiting for port ${port}`));
|
|
46
|
+
} else {
|
|
47
|
+
setTimeout(check, 100);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
socket.on("timeout", () => {
|
|
51
|
+
socket.destroy();
|
|
52
|
+
if (Date.now() - start > timeoutMs) {
|
|
53
|
+
reject(new Error(`Timeout waiting for port ${port}`));
|
|
54
|
+
} else {
|
|
55
|
+
setTimeout(check, 100);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
socket.connect(port, "127.0.0.1");
|
|
59
|
+
}
|
|
60
|
+
check();
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
function formatCtx(len) {
|
|
64
|
+
if (len >= 1e6) return `${(len / 1e6).toFixed(0)}M ctx`;
|
|
65
|
+
if (len >= 1e3) return `${(len / 1e3).toFixed(0)}k ctx`;
|
|
66
|
+
return `${len} ctx`;
|
|
67
|
+
}
|
|
68
|
+
function maskString(val) {
|
|
69
|
+
if (!val) return "";
|
|
70
|
+
return val.length > 16 ? val.substring(0, 8) + "..." + val.substring(val.length - 4) : val;
|
|
71
|
+
}
|
|
72
|
+
function formatPricePerM(perM) {
|
|
73
|
+
if (perM === 0) return "free";
|
|
74
|
+
if (perM < 0.01) return `$${perM.toFixed(4)}/1M`;
|
|
75
|
+
return `$${perM.toFixed(2)}/1M`;
|
|
76
|
+
}
|
|
77
|
+
function sortModels(a, b) {
|
|
78
|
+
const createdDiff = (b.created ?? 0) - (a.created ?? 0);
|
|
79
|
+
if (createdDiff !== 0) return createdDiff;
|
|
80
|
+
const aPrice = a.pricing?.promptPerM ?? 0;
|
|
81
|
+
const bPrice = b.pricing?.promptPerM ?? 0;
|
|
82
|
+
const priceDiff = aPrice - bPrice;
|
|
83
|
+
if (priceDiff !== 0) return priceDiff;
|
|
84
|
+
const ctxDiff = (b.contextLength ?? 0) - (a.contextLength ?? 0);
|
|
85
|
+
if (ctxDiff !== 0) return ctxDiff;
|
|
86
|
+
return 0;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// src/agents/shared/bifrost.ts
|
|
90
|
+
async function startBifrost(provider, apiKey) {
|
|
91
|
+
const bifrostPort = await getFreePort();
|
|
92
|
+
const appDir = fs.mkdtempSync(path.join(os.tmpdir(), "bifrost-app-"));
|
|
93
|
+
const envKey = provider === "openai" ? "OPENAI_API_KEY" : "ANTHROPIC_API_KEY";
|
|
94
|
+
const configContent = {
|
|
95
|
+
providers: {
|
|
96
|
+
[provider]: {
|
|
97
|
+
keys: [
|
|
98
|
+
{
|
|
99
|
+
name: "default",
|
|
100
|
+
value: `env.${envKey}`,
|
|
101
|
+
weight: 1,
|
|
102
|
+
models: ["*"]
|
|
103
|
+
}
|
|
104
|
+
]
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
fs.writeFileSync(path.join(appDir, "config.json"), JSON.stringify(configContent, null, 2));
|
|
109
|
+
const logFile = fs.openSync(path.join(appDir, "bifrost.log"), "a");
|
|
110
|
+
const proc = spawn(
|
|
111
|
+
"npx",
|
|
112
|
+
["--no-install", "bifrost", "-port", String(bifrostPort), "-app-dir", appDir],
|
|
113
|
+
{
|
|
114
|
+
env: { ...process.env, [envKey]: apiKey },
|
|
115
|
+
detached: true,
|
|
116
|
+
stdio: ["ignore", logFile, logFile]
|
|
117
|
+
}
|
|
118
|
+
);
|
|
119
|
+
proc.unref();
|
|
120
|
+
const cleanup = () => {
|
|
121
|
+
if (proc) {
|
|
122
|
+
try {
|
|
123
|
+
if (proc.pid) {
|
|
124
|
+
process.kill(-proc.pid, "SIGTERM");
|
|
125
|
+
} else {
|
|
126
|
+
proc.kill();
|
|
127
|
+
}
|
|
128
|
+
} catch (_) {
|
|
129
|
+
try {
|
|
130
|
+
proc.kill();
|
|
131
|
+
} catch (__) {
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
fs.rmSync(appDir, { recursive: true, force: true });
|
|
137
|
+
} catch (_) {
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
try {
|
|
141
|
+
await waitForPort(bifrostPort, 15e3);
|
|
142
|
+
} catch (err) {
|
|
143
|
+
if (fs.existsSync(path.join(appDir, "bifrost.log"))) {
|
|
144
|
+
try {
|
|
145
|
+
const logs = fs.readFileSync(path.join(appDir, "bifrost.log"), "utf8");
|
|
146
|
+
console.error(`\x1B[33mBifrost Startup Logs:
|
|
147
|
+
${logs}\x1B[0m`);
|
|
148
|
+
} catch (_) {
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
cleanup();
|
|
152
|
+
throw err;
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
port: bifrostPort,
|
|
156
|
+
appDir,
|
|
157
|
+
proc,
|
|
158
|
+
cleanup
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// src/constants.ts
|
|
163
|
+
import os2 from "os";
|
|
164
|
+
import path2 from "path";
|
|
165
|
+
var DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1";
|
|
166
|
+
var DEFAULT_ANTHROPIC_BASE_URL = "https://api.anthropic.com";
|
|
167
|
+
var DEFAULT_OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1";
|
|
168
|
+
var DEFAULT_OPENROUTER_ANTHROPIC_BASE_URL = "https://openrouter.ai/api";
|
|
169
|
+
var DEFAULT_CLOUDFLARE_BASE_URL = "https://gateway.ai.cloudflare.com/v1";
|
|
170
|
+
var PROVIDER_HOST_MAPPINGS = {
|
|
171
|
+
"api.openai.com": "OpenAI Official",
|
|
172
|
+
"api.anthropic.com": "Anthropic Official"
|
|
173
|
+
};
|
|
174
|
+
var ENV_FALCON_DIR = "FALCON_DIR";
|
|
175
|
+
var ENV_CLAUDE_CONFIG_DIR = "CLAUDE_CONFIG_DIR";
|
|
176
|
+
var ENV_CODEX_HOME = "CODEX_HOME";
|
|
177
|
+
var ENV_FALCON_CONFIG_FILE = "FALCON_CONFIG_FILE";
|
|
178
|
+
var DEFAULT_FALCON_DIR = path2.join(os2.homedir(), ".falcon");
|
|
179
|
+
|
|
180
|
+
// src/agents/claude.ts
|
|
181
|
+
var ClaudeLauncher = class {
|
|
182
|
+
name = "Claude Code";
|
|
183
|
+
slug = "claude";
|
|
184
|
+
async resolveConfig(gatewayConfig, gatewaySlug, apiKey, model, options) {
|
|
185
|
+
const env = { ...gatewayConfig.env };
|
|
186
|
+
let cleanup;
|
|
187
|
+
if (gatewaySlug === "openai") {
|
|
188
|
+
if (options?.dryRun) {
|
|
189
|
+
env["ANTHROPIC_BASE_URL"] = "http://localhost:<BIFROST_PORT>/anthropic";
|
|
190
|
+
env["ANTHROPIC_API_KEY"] = apiKey;
|
|
191
|
+
} else {
|
|
192
|
+
const bifrost = await startBifrost("openai", apiKey);
|
|
193
|
+
env["ANTHROPIC_BASE_URL"] = `http://localhost:${bifrost.port}/anthropic`;
|
|
194
|
+
env["ANTHROPIC_API_KEY"] = apiKey;
|
|
195
|
+
cleanup = bifrost.cleanup;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const baseUrl = env["ANTHROPIC_BASE_URL"];
|
|
199
|
+
const isOfficialAnthropic = baseUrl === DEFAULT_ANTHROPIC_BASE_URL || baseUrl === `${DEFAULT_ANTHROPIC_BASE_URL}/` || baseUrl === `${DEFAULT_ANTHROPIC_BASE_URL}/v1` || baseUrl === `${DEFAULT_ANTHROPIC_BASE_URL}/v1/`;
|
|
200
|
+
if (isOfficialAnthropic) {
|
|
201
|
+
delete env["ANTHROPIC_AUTH_TOKEN"];
|
|
202
|
+
} else {
|
|
203
|
+
env["ANTHROPIC_AUTH_TOKEN"] = env["ANTHROPIC_API_KEY"];
|
|
204
|
+
env["ANTHROPIC_API_KEY"] = "";
|
|
205
|
+
}
|
|
206
|
+
env["CLAUDE_CODE_ATTRIBUTION_HEADER"] = "0";
|
|
207
|
+
env["DISABLE_TELEMETRY"] = "1";
|
|
208
|
+
env["CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC"] = "1";
|
|
209
|
+
env["DISABLE_ERROR_REPORTING"] = "1";
|
|
210
|
+
env["CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY"] = "1";
|
|
211
|
+
if (model) {
|
|
212
|
+
env["ANTHROPIC_DEFAULT_OPUS_MODEL"] = model;
|
|
213
|
+
env["ANTHROPIC_DEFAULT_SONNET_MODEL"] = model;
|
|
214
|
+
env["ANTHROPIC_DEFAULT_HAIKU_MODEL"] = model;
|
|
215
|
+
env["CLAUDE_CODE_SUBAGENT_MODEL"] = model;
|
|
216
|
+
}
|
|
217
|
+
const falconDir = process.env[ENV_FALCON_DIR] || DEFAULT_FALCON_DIR;
|
|
218
|
+
const claudeDir = process.env[ENV_CLAUDE_CONFIG_DIR] || path3.join(falconDir, this.slug);
|
|
219
|
+
env[ENV_CLAUDE_CONFIG_DIR] = claudeDir;
|
|
220
|
+
if (!options?.dryRun) {
|
|
221
|
+
if (!fs2.existsSync(claudeDir)) {
|
|
222
|
+
fs2.mkdirSync(claudeDir, { recursive: true, mode: 448 });
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
env,
|
|
227
|
+
cleanup
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
buildSpawnConfig(resolvedConfig, model, extraArgs) {
|
|
231
|
+
const args = [];
|
|
232
|
+
if (model) {
|
|
233
|
+
args.push("--model", model);
|
|
234
|
+
}
|
|
235
|
+
args.push("--dangerously-skip-permissions");
|
|
236
|
+
args.push(...extraArgs);
|
|
237
|
+
return {
|
|
238
|
+
command: "claude",
|
|
239
|
+
args,
|
|
240
|
+
env: resolvedConfig.env,
|
|
241
|
+
cleanup: resolvedConfig.cleanup
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
// src/agents/codex.ts
|
|
247
|
+
import * as fs3 from "fs";
|
|
248
|
+
import * as path4 from "path";
|
|
249
|
+
var CodexLauncher = class {
|
|
250
|
+
name = "Codex";
|
|
251
|
+
slug = "codex";
|
|
252
|
+
async resolveConfig(gatewayConfig, gatewaySlug, apiKey, model, options) {
|
|
253
|
+
const env = { ...gatewayConfig.env };
|
|
254
|
+
let baseUrl = gatewayConfig.baseUrl;
|
|
255
|
+
let cleanup;
|
|
256
|
+
if (gatewaySlug === "anthropic") {
|
|
257
|
+
if (options?.dryRun) {
|
|
258
|
+
baseUrl = "http://localhost:<BIFROST_PORT>/openai";
|
|
259
|
+
env["OPENAI_BASE_URL"] = baseUrl;
|
|
260
|
+
env["OPENAI_API_KEY"] = apiKey;
|
|
261
|
+
} else {
|
|
262
|
+
const bifrost = await startBifrost("anthropic", apiKey);
|
|
263
|
+
baseUrl = `http://localhost:${bifrost.port}/openai`;
|
|
264
|
+
env["OPENAI_BASE_URL"] = baseUrl;
|
|
265
|
+
env["OPENAI_API_KEY"] = apiKey;
|
|
266
|
+
cleanup = bifrost.cleanup;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
const codexDir = this.getCodexDir();
|
|
270
|
+
env[ENV_CODEX_HOME] = codexDir;
|
|
271
|
+
if (model) {
|
|
272
|
+
try {
|
|
273
|
+
ensureCodexConfig(codexDir, model, baseUrl, env["OPENAI_BASE_URL"]);
|
|
274
|
+
} catch (err) {
|
|
275
|
+
console.error(
|
|
276
|
+
`Warning: Failed to configure Codex: ${err instanceof Error ? err.message : err}`
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return {
|
|
281
|
+
env,
|
|
282
|
+
baseUrl,
|
|
283
|
+
cleanup
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
buildSpawnConfig(resolvedConfig, model, extraArgs) {
|
|
287
|
+
const codexDir = this.getCodexDir();
|
|
288
|
+
const catalogPath = path4.join(codexDir, "model.json");
|
|
289
|
+
const args = ["--profile", "falcon"];
|
|
290
|
+
if (model) {
|
|
291
|
+
args.push("-c", `model_catalog_json=${catalogPath}`);
|
|
292
|
+
args.push("-m", model);
|
|
293
|
+
}
|
|
294
|
+
args.push(...extraArgs);
|
|
295
|
+
return {
|
|
296
|
+
command: "codex",
|
|
297
|
+
args,
|
|
298
|
+
env: resolvedConfig.env,
|
|
299
|
+
cleanup: resolvedConfig.cleanup
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
getCodexDir() {
|
|
303
|
+
const falconDir = process.env[ENV_FALCON_DIR] || DEFAULT_FALCON_DIR;
|
|
304
|
+
return process.env[ENV_CODEX_HOME] || path4.join(falconDir, this.slug);
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
function getContextWindow(modelName) {
|
|
308
|
+
const name = modelName.toLowerCase();
|
|
309
|
+
if (name.includes("claude-3")) {
|
|
310
|
+
return 2e5;
|
|
311
|
+
}
|
|
312
|
+
if (name.includes("gpt-4o") || name.includes("gpt-4-turbo") || name.includes("gpt-4")) {
|
|
313
|
+
return 128e3;
|
|
314
|
+
}
|
|
315
|
+
if (name.includes("gpt-3.5")) {
|
|
316
|
+
return 16385;
|
|
317
|
+
}
|
|
318
|
+
if (name.includes("gemini-1.5") || name.includes("gemini-2.0") || name.includes("gemini-2.5")) {
|
|
319
|
+
return 1e6;
|
|
320
|
+
}
|
|
321
|
+
return 128e3;
|
|
322
|
+
}
|
|
323
|
+
function getModalities(modelName) {
|
|
324
|
+
const name = modelName.toLowerCase();
|
|
325
|
+
const hasVision = name.includes("vision") || name.includes("gpt-4o") || name.includes("claude-3") || name.includes("gemini");
|
|
326
|
+
const modalities = ["text"];
|
|
327
|
+
if (hasVision) {
|
|
328
|
+
modalities.push("image");
|
|
329
|
+
}
|
|
330
|
+
return modalities;
|
|
331
|
+
}
|
|
332
|
+
function writeCodexModelCatalog(catalogPath, modelName) {
|
|
333
|
+
let catalog = { models: [] };
|
|
334
|
+
if (fs3.existsSync(catalogPath)) {
|
|
335
|
+
try {
|
|
336
|
+
const data = fs3.readFileSync(catalogPath, "utf8");
|
|
337
|
+
const parsed = JSON.parse(data);
|
|
338
|
+
if (parsed && Array.isArray(parsed.models)) {
|
|
339
|
+
catalog = parsed;
|
|
340
|
+
}
|
|
341
|
+
} catch (_e) {
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
const contextWindow = getContextWindow(modelName);
|
|
345
|
+
const modalities = getModalities(modelName);
|
|
346
|
+
const truncationMode = modelName.includes("/") ? "tokens" : "bytes";
|
|
347
|
+
const entry = {
|
|
348
|
+
slug: modelName,
|
|
349
|
+
display_name: modelName,
|
|
350
|
+
context_window: contextWindow,
|
|
351
|
+
shell_type: "default",
|
|
352
|
+
visibility: "list",
|
|
353
|
+
supported_in_api: true,
|
|
354
|
+
priority: 0,
|
|
355
|
+
truncation_policy: { mode: truncationMode, limit: 1e4 },
|
|
356
|
+
input_modalities: modalities,
|
|
357
|
+
base_instructions: "",
|
|
358
|
+
support_verbosity: true,
|
|
359
|
+
default_verbosity: "low",
|
|
360
|
+
supports_parallel_tool_calls: false,
|
|
361
|
+
supports_reasoning_summaries: false,
|
|
362
|
+
supported_reasoning_levels: [],
|
|
363
|
+
experimental_supported_tools: []
|
|
364
|
+
};
|
|
365
|
+
const existingIndex = catalog.models.findIndex((m) => m.slug === modelName);
|
|
366
|
+
if (existingIndex !== -1) {
|
|
367
|
+
catalog.models[existingIndex] = entry;
|
|
368
|
+
} else {
|
|
369
|
+
catalog.models.push(entry);
|
|
370
|
+
}
|
|
371
|
+
fs3.writeFileSync(catalogPath, JSON.stringify(catalog, null, 2), "utf8");
|
|
372
|
+
}
|
|
373
|
+
function upsertSection(text, header, lines) {
|
|
374
|
+
const fileLines = text.split(/\r?\n/);
|
|
375
|
+
const targetHeader = header.trim();
|
|
376
|
+
let startIndex = -1;
|
|
377
|
+
let endIndex = -1;
|
|
378
|
+
for (let i = 0; i < fileLines.length; i++) {
|
|
379
|
+
const trimmed = fileLines[i].trim();
|
|
380
|
+
if (trimmed === targetHeader) {
|
|
381
|
+
startIndex = i;
|
|
382
|
+
for (let j = i + 1; j < fileLines.length; j++) {
|
|
383
|
+
const nextTrimmed = fileLines[j].trim();
|
|
384
|
+
if (nextTrimmed.startsWith("[") && nextTrimmed.endsWith("]")) {
|
|
385
|
+
endIndex = j;
|
|
386
|
+
break;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
if (endIndex === -1) {
|
|
390
|
+
endIndex = fileLines.length;
|
|
391
|
+
}
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
const blockLines = [targetHeader, ...lines, ""];
|
|
396
|
+
if (startIndex !== -1) {
|
|
397
|
+
fileLines.splice(startIndex, endIndex - startIndex, ...blockLines);
|
|
398
|
+
} else {
|
|
399
|
+
if (fileLines.length > 0 && fileLines[fileLines.length - 1].trim() !== "") {
|
|
400
|
+
fileLines.push("");
|
|
401
|
+
}
|
|
402
|
+
fileLines.push(...blockLines);
|
|
403
|
+
}
|
|
404
|
+
return fileLines.join("\n");
|
|
405
|
+
}
|
|
406
|
+
function ensureCodexConfig(codexDir, modelName, resolvedBaseUrl, envBaseUrl) {
|
|
407
|
+
if (!fs3.existsSync(codexDir)) {
|
|
408
|
+
fs3.mkdirSync(codexDir, { recursive: true, mode: 448 });
|
|
409
|
+
}
|
|
410
|
+
const configPath = path4.join(codexDir, "config.toml");
|
|
411
|
+
const catalogPath = path4.join(codexDir, "model.json");
|
|
412
|
+
writeCodexModelCatalog(catalogPath, modelName);
|
|
413
|
+
let baseUrl = resolvedBaseUrl || envBaseUrl || process.env["OPENAI_BASE_URL"] || DEFAULT_OPENAI_BASE_URL;
|
|
414
|
+
if (!baseUrl.endsWith("/")) {
|
|
415
|
+
baseUrl += "/";
|
|
416
|
+
}
|
|
417
|
+
let text = "";
|
|
418
|
+
if (fs3.existsSync(configPath)) {
|
|
419
|
+
try {
|
|
420
|
+
text = fs3.readFileSync(configPath, "utf8");
|
|
421
|
+
} catch (_e) {
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
const profileName = "falcon";
|
|
425
|
+
let providerKey = "falcon";
|
|
426
|
+
if (resolvedBaseUrl) {
|
|
427
|
+
try {
|
|
428
|
+
const urlStr = resolvedBaseUrl.includes("://") ? resolvedBaseUrl : `http://${resolvedBaseUrl}`;
|
|
429
|
+
const sanitizedUrlStr = urlStr.replace("<BIFROST_PORT>", "9999");
|
|
430
|
+
const parsedUrl = new URL(sanitizedUrlStr);
|
|
431
|
+
if (parsedUrl.hostname) {
|
|
432
|
+
providerKey = parsedUrl.hostname.replaceAll(".", "-");
|
|
433
|
+
}
|
|
434
|
+
} catch (_e) {
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
const profileLines = [
|
|
438
|
+
`model = "${modelName}"`,
|
|
439
|
+
`model_provider = "${providerKey}"`,
|
|
440
|
+
`forced_login_method = "api"`,
|
|
441
|
+
`model_catalog_json = "${catalogPath}"`
|
|
442
|
+
];
|
|
443
|
+
const providerLines = [
|
|
444
|
+
`name = "${providerKey}"`,
|
|
445
|
+
`base_url = "${baseUrl}"`,
|
|
446
|
+
`wire_api = "responses"`
|
|
447
|
+
];
|
|
448
|
+
text = upsertSection(text, `[profiles.${profileName}]`, profileLines);
|
|
449
|
+
text = upsertSection(text, `[model_providers.${providerKey}]`, providerLines);
|
|
450
|
+
text = upsertSection(text, `[analytics]`, [`enabled = false`]);
|
|
451
|
+
text = upsertSection(text, `[feedback]`, [`enabled = false`]);
|
|
452
|
+
fs3.writeFileSync(configPath, text, "utf8");
|
|
453
|
+
return catalogPath;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// src/agents/index.ts
|
|
457
|
+
var ALL_AGENTS = [new CodexLauncher(), new ClaudeLauncher()];
|
|
458
|
+
function findAgent(name) {
|
|
459
|
+
return ALL_AGENTS.find(
|
|
460
|
+
(a) => a.slug === name.toLowerCase() || a.name.toLowerCase() === name.toLowerCase()
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// src/gateways/shared/modelEnricher.ts
|
|
465
|
+
function normalizeModelId(id) {
|
|
466
|
+
let normalized = id.toLowerCase();
|
|
467
|
+
const parts = normalized.split("/");
|
|
468
|
+
normalized = parts[parts.length - 1] || normalized;
|
|
469
|
+
normalized = normalized.split(":")[0] || normalized;
|
|
470
|
+
normalized = normalized.replace(/-latest$/, "");
|
|
471
|
+
normalized = normalized.replace(/-\d{4}-\d{2}-\d{2}$/, "");
|
|
472
|
+
normalized = normalized.replace(/-\d{8}$/, "");
|
|
473
|
+
normalized = normalized.replace(/-\d{4}$/, "");
|
|
474
|
+
normalized = normalized.replace(/-latest$/, "");
|
|
475
|
+
normalized = normalized.replace(/[._]/g, "-");
|
|
476
|
+
normalized = normalized.replace(/-+/g, "-");
|
|
477
|
+
return normalized.trim();
|
|
478
|
+
}
|
|
479
|
+
var metadataCache = null;
|
|
480
|
+
async function fetchModelMetadataCatalog() {
|
|
481
|
+
if (metadataCache) {
|
|
482
|
+
return metadataCache;
|
|
483
|
+
}
|
|
484
|
+
if (process.env.NODE_ENV === "test") {
|
|
485
|
+
return {};
|
|
486
|
+
}
|
|
487
|
+
const cache = {};
|
|
488
|
+
try {
|
|
489
|
+
const res = await fetch("https://openrouter.ai/api/v1/models");
|
|
490
|
+
if (!res.ok) return cache;
|
|
491
|
+
const data = await res.json();
|
|
492
|
+
for (const m of data.data) {
|
|
493
|
+
if (!m.id) continue;
|
|
494
|
+
const parts = m.id.split("/");
|
|
495
|
+
const strippedId = parts[1] || parts[0] || m.id;
|
|
496
|
+
const contextLength = m.context_length ?? 0;
|
|
497
|
+
const promptRaw = parseFloat(m.pricing?.prompt ?? "0");
|
|
498
|
+
const completionRaw = parseFloat(m.pricing?.completion ?? "0");
|
|
499
|
+
const promptPerM = promptRaw * 1e6;
|
|
500
|
+
const completionPerM = completionRaw * 1e6;
|
|
501
|
+
const metadata = {
|
|
502
|
+
contextLength,
|
|
503
|
+
pricing: {
|
|
504
|
+
prompt: formatPricePerM(promptPerM),
|
|
505
|
+
completion: formatPricePerM(completionPerM),
|
|
506
|
+
promptPerM
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
const fullId = m.id.toLowerCase();
|
|
510
|
+
cache[fullId] = metadata;
|
|
511
|
+
const key1 = strippedId.toLowerCase();
|
|
512
|
+
cache[key1] = metadata;
|
|
513
|
+
const normId = normalizeModelId(m.id);
|
|
514
|
+
cache[normId] = metadata;
|
|
515
|
+
const key2 = key1.replace(/\./g, "-");
|
|
516
|
+
if (key2 !== key1) {
|
|
517
|
+
cache[key2] = metadata;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
metadataCache = cache;
|
|
521
|
+
} catch {
|
|
522
|
+
}
|
|
523
|
+
return cache;
|
|
524
|
+
}
|
|
525
|
+
function enrichModelWithCatalog(model, catalog) {
|
|
526
|
+
if (model.contextLength && model.pricing) {
|
|
527
|
+
return model;
|
|
528
|
+
}
|
|
529
|
+
const id = model.id.toLowerCase();
|
|
530
|
+
const idNormalized = normalizeModelId(id);
|
|
531
|
+
const match = catalog[id] || catalog[idNormalized];
|
|
532
|
+
if (match) {
|
|
533
|
+
const updated = { ...model };
|
|
534
|
+
if (!updated.contextLength) {
|
|
535
|
+
updated.contextLength = match.contextLength;
|
|
536
|
+
}
|
|
537
|
+
if (!updated.pricing) {
|
|
538
|
+
updated.pricing = match.pricing;
|
|
539
|
+
}
|
|
540
|
+
return updated;
|
|
541
|
+
}
|
|
542
|
+
return model;
|
|
543
|
+
}
|
|
544
|
+
function enrichModelInfo(model) {
|
|
545
|
+
return enrichModelWithCatalog(model, metadataCache || {});
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// src/gateways/anthropic.ts
|
|
549
|
+
var AnthropicGateway = class {
|
|
550
|
+
name = "Anthropic";
|
|
551
|
+
slug = "anthropic";
|
|
552
|
+
detectKey() {
|
|
553
|
+
return process.env["ANTHROPIC_API_KEY"];
|
|
554
|
+
}
|
|
555
|
+
async listModels(apiKey) {
|
|
556
|
+
try {
|
|
557
|
+
await fetchModelMetadataCatalog();
|
|
558
|
+
} catch {
|
|
559
|
+
}
|
|
560
|
+
const baseUrl = process.env.ANTHROPIC_BASE_URL || DEFAULT_ANTHROPIC_BASE_URL;
|
|
561
|
+
let url = baseUrl;
|
|
562
|
+
if (!url.includes("/v1") && !url.includes("/v2")) {
|
|
563
|
+
url = url.endsWith("/") ? `${url}v1/models` : `${url}/v1/models`;
|
|
564
|
+
} else {
|
|
565
|
+
url = url.endsWith("/") ? `${url}models` : `${url}/models`;
|
|
566
|
+
}
|
|
567
|
+
const res = await fetch(url, {
|
|
568
|
+
headers: {
|
|
569
|
+
"x-api-key": apiKey,
|
|
570
|
+
"anthropic-version": "2023-06-01",
|
|
571
|
+
"Content-Type": "application/json"
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
if (!res.ok) {
|
|
575
|
+
throw new Error(`Anthropic API error: ${res.status} ${res.statusText}`);
|
|
576
|
+
}
|
|
577
|
+
const data = await res.json();
|
|
578
|
+
return data.data.map(
|
|
579
|
+
(m) => enrichModelInfo({
|
|
580
|
+
id: m.id,
|
|
581
|
+
name: m.display_name || m.id,
|
|
582
|
+
contextLength: m.context_window,
|
|
583
|
+
provider: "Anthropic"
|
|
584
|
+
})
|
|
585
|
+
).sort(sortModels);
|
|
586
|
+
}
|
|
587
|
+
getEnvConfig(apiKey, _model) {
|
|
588
|
+
return {
|
|
589
|
+
env: {
|
|
590
|
+
ANTHROPIC_API_KEY: apiKey,
|
|
591
|
+
ANTHROPIC_BASE_URL: process.env.ANTHROPIC_BASE_URL || DEFAULT_ANTHROPIC_BASE_URL
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
// src/gateways/cloudflare.ts
|
|
598
|
+
var CloudflareGateway = class {
|
|
599
|
+
name = "Cloudflare AI Gateway";
|
|
600
|
+
slug = "cloudflare";
|
|
601
|
+
detectKey() {
|
|
602
|
+
return process.env["CLOUDFLARE_API_KEY"] || process.env["CF_API_KEY"];
|
|
603
|
+
}
|
|
604
|
+
getAccountId() {
|
|
605
|
+
return process.env["CLOUDFLARE_ACCOUNT_ID"] || process.env["CF_ACCOUNT_ID"] || "";
|
|
606
|
+
}
|
|
607
|
+
getGatewayId() {
|
|
608
|
+
return process.env["CLOUDFLARE_GATEWAY_ID"] || process.env["CF_GATEWAY_ID"] || "default";
|
|
609
|
+
}
|
|
610
|
+
async listModels(_apiKey) {
|
|
611
|
+
return [
|
|
612
|
+
{ id: "gpt-4o", name: "GPT-4o (via CF Gateway)", provider: "Cloudflare" },
|
|
613
|
+
{ id: "gpt-4o-mini", name: "GPT-4o Mini (via CF Gateway)", provider: "Cloudflare" },
|
|
614
|
+
{
|
|
615
|
+
id: "claude-sonnet-4-20250514",
|
|
616
|
+
name: "Claude Sonnet 4 (via CF Gateway)",
|
|
617
|
+
provider: "Cloudflare"
|
|
618
|
+
},
|
|
619
|
+
{
|
|
620
|
+
id: "claude-3-5-haiku-20241022",
|
|
621
|
+
name: "Claude 3.5 Haiku (via CF Gateway)",
|
|
622
|
+
provider: "Cloudflare"
|
|
623
|
+
}
|
|
624
|
+
];
|
|
625
|
+
}
|
|
626
|
+
getEnvConfig(apiKey, _model) {
|
|
627
|
+
const accountId = this.getAccountId();
|
|
628
|
+
const gatewayId = this.getGatewayId();
|
|
629
|
+
const baseUrl = accountId ? `https://gateway.ai.cloudflare.com/v1/${accountId}/${gatewayId}/openai` : DEFAULT_CLOUDFLARE_BASE_URL;
|
|
630
|
+
return {
|
|
631
|
+
env: {
|
|
632
|
+
OPENAI_API_KEY: apiKey,
|
|
633
|
+
OPENAI_BASE_URL: baseUrl
|
|
634
|
+
},
|
|
635
|
+
baseUrl
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
// src/gateways/openai.ts
|
|
641
|
+
var OpenAIGateway = class {
|
|
642
|
+
name = "OpenAI";
|
|
643
|
+
slug = "openai";
|
|
644
|
+
detectKey() {
|
|
645
|
+
return process.env["OPENAI_API_KEY"];
|
|
646
|
+
}
|
|
647
|
+
async listModels(apiKey) {
|
|
648
|
+
try {
|
|
649
|
+
await fetchModelMetadataCatalog();
|
|
650
|
+
} catch {
|
|
651
|
+
}
|
|
652
|
+
const baseUrl = process.env.OPENAI_BASE_URL || DEFAULT_OPENAI_BASE_URL;
|
|
653
|
+
let url = baseUrl;
|
|
654
|
+
if (!url.includes("/v1") && !url.includes("/v2")) {
|
|
655
|
+
url = url.endsWith("/") ? `${url}v1/models` : `${url}/v1/models`;
|
|
656
|
+
} else {
|
|
657
|
+
url = url.endsWith("/") ? `${url}models` : `${url}/models`;
|
|
658
|
+
}
|
|
659
|
+
const res = await fetch(url, {
|
|
660
|
+
headers: {
|
|
661
|
+
Authorization: `Bearer ${apiKey}`,
|
|
662
|
+
"Content-Type": "application/json"
|
|
663
|
+
}
|
|
664
|
+
});
|
|
665
|
+
if (!res.ok) {
|
|
666
|
+
throw new Error(`OpenAI API error: ${res.status} ${res.statusText}`);
|
|
667
|
+
}
|
|
668
|
+
const data = await res.json();
|
|
669
|
+
const useful = data.data.filter((m) => {
|
|
670
|
+
const id = m.id.toLowerCase();
|
|
671
|
+
return id.includes("gpt") || id.includes("o1") || id.includes("o3") || id.includes("o4") || id.includes("codex") || id.startsWith("chatgpt");
|
|
672
|
+
}).map(
|
|
673
|
+
(m) => enrichModelInfo({
|
|
674
|
+
id: m.id,
|
|
675
|
+
name: m.id,
|
|
676
|
+
provider: "OpenAI"
|
|
677
|
+
})
|
|
678
|
+
).sort(sortModels);
|
|
679
|
+
return useful;
|
|
680
|
+
}
|
|
681
|
+
getEnvConfig(apiKey, _model) {
|
|
682
|
+
const config = {
|
|
683
|
+
env: {
|
|
684
|
+
OPENAI_API_KEY: apiKey
|
|
685
|
+
}
|
|
686
|
+
};
|
|
687
|
+
if (process.env.OPENAI_BASE_URL) {
|
|
688
|
+
config.env.OPENAI_BASE_URL = process.env.OPENAI_BASE_URL;
|
|
689
|
+
}
|
|
690
|
+
return config;
|
|
691
|
+
}
|
|
692
|
+
};
|
|
693
|
+
|
|
694
|
+
// src/gateways/openrouter.ts
|
|
695
|
+
var OpenRouterGateway = class {
|
|
696
|
+
name = "OpenRouter";
|
|
697
|
+
slug = "openrouter";
|
|
698
|
+
detectKey() {
|
|
699
|
+
return process.env["OPENROUTER_API_KEY"];
|
|
700
|
+
}
|
|
701
|
+
async listModels(apiKey) {
|
|
702
|
+
const res = await fetch(`${DEFAULT_OPENROUTER_BASE_URL}/models`, {
|
|
703
|
+
headers: {
|
|
704
|
+
Authorization: `Bearer ${apiKey}`,
|
|
705
|
+
"Content-Type": "application/json"
|
|
706
|
+
}
|
|
707
|
+
});
|
|
708
|
+
if (!res.ok) {
|
|
709
|
+
throw new Error(`OpenRouter API error: ${res.status} ${res.statusText}`);
|
|
710
|
+
}
|
|
711
|
+
const data = await res.json();
|
|
712
|
+
return data.data.filter((m) => m.id && m.name).map((m) => {
|
|
713
|
+
const promptRaw = parseFloat(m.pricing?.prompt ?? "0");
|
|
714
|
+
const completionRaw = parseFloat(m.pricing?.completion ?? "0");
|
|
715
|
+
const promptPerM = promptRaw * 1e6;
|
|
716
|
+
const completionPerM = completionRaw * 1e6;
|
|
717
|
+
return {
|
|
718
|
+
id: m.id,
|
|
719
|
+
name: m.name,
|
|
720
|
+
contextLength: m.context_length,
|
|
721
|
+
pricing: m.pricing ? {
|
|
722
|
+
prompt: formatPricePerM(promptPerM),
|
|
723
|
+
completion: formatPricePerM(completionPerM),
|
|
724
|
+
promptPerM
|
|
725
|
+
} : void 0,
|
|
726
|
+
provider: "OpenRouter",
|
|
727
|
+
created: m.created
|
|
728
|
+
};
|
|
729
|
+
}).sort(sortModels);
|
|
730
|
+
}
|
|
731
|
+
getEnvConfig(apiKey, _model) {
|
|
732
|
+
return {
|
|
733
|
+
env: {
|
|
734
|
+
OPENROUTER_API_KEY: apiKey,
|
|
735
|
+
OPENAI_API_KEY: apiKey,
|
|
736
|
+
OPENAI_BASE_URL: DEFAULT_OPENROUTER_BASE_URL,
|
|
737
|
+
ANTHROPIC_API_KEY: apiKey,
|
|
738
|
+
ANTHROPIC_BASE_URL: DEFAULT_OPENROUTER_ANTHROPIC_BASE_URL
|
|
739
|
+
},
|
|
740
|
+
baseUrl: DEFAULT_OPENROUTER_BASE_URL
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
|
|
745
|
+
// src/config.ts
|
|
746
|
+
import crypto from "crypto";
|
|
747
|
+
import fs4 from "fs";
|
|
748
|
+
import os3 from "os";
|
|
749
|
+
import path5 from "path";
|
|
750
|
+
var ALGORITHM = "aes-256-cbc";
|
|
751
|
+
function getFalconDir() {
|
|
752
|
+
return process.env[ENV_FALCON_DIR] || DEFAULT_FALCON_DIR;
|
|
753
|
+
}
|
|
754
|
+
function getConfigFile() {
|
|
755
|
+
return process.env[ENV_FALCON_CONFIG_FILE] || path5.join(getFalconDir(), "config.json");
|
|
756
|
+
}
|
|
757
|
+
function getEncryptionKey() {
|
|
758
|
+
let userIdentifier = "falcon-default-user";
|
|
759
|
+
try {
|
|
760
|
+
userIdentifier = os3.userInfo().username;
|
|
761
|
+
} catch {
|
|
762
|
+
}
|
|
763
|
+
const rawKeyMaterial = [
|
|
764
|
+
os3.platform(),
|
|
765
|
+
os3.hostname(),
|
|
766
|
+
os3.homedir(),
|
|
767
|
+
userIdentifier,
|
|
768
|
+
"falcon-secure-salt-2026"
|
|
769
|
+
].join(":");
|
|
770
|
+
return crypto.createHash("sha256").update(rawKeyMaterial).digest();
|
|
771
|
+
}
|
|
772
|
+
function encrypt(text) {
|
|
773
|
+
const iv = crypto.randomBytes(16);
|
|
774
|
+
const key = getEncryptionKey();
|
|
775
|
+
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
|
776
|
+
let encrypted = cipher.update(text, "utf8", "hex");
|
|
777
|
+
encrypted += cipher.final("hex");
|
|
778
|
+
return {
|
|
779
|
+
iv: iv.toString("hex"),
|
|
780
|
+
data: encrypted
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
function decrypt(encryptedData, ivHex) {
|
|
784
|
+
const iv = Buffer.from(ivHex, "hex");
|
|
785
|
+
const key = getEncryptionKey();
|
|
786
|
+
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
|
|
787
|
+
let decrypted = decipher.update(encryptedData, "hex", "utf8");
|
|
788
|
+
decrypted += decipher.final("utf8");
|
|
789
|
+
return decrypted;
|
|
790
|
+
}
|
|
791
|
+
function loadFalconConfigV2() {
|
|
792
|
+
const configFile = getConfigFile();
|
|
793
|
+
try {
|
|
794
|
+
if (fs4.existsSync(configFile)) {
|
|
795
|
+
const raw = fs4.readFileSync(configFile, "utf8").trim();
|
|
796
|
+
if (!raw) return { version: 2, gateways: [] };
|
|
797
|
+
const parsed = JSON.parse(raw);
|
|
798
|
+
if (parsed && typeof parsed === "object") {
|
|
799
|
+
let decryptedJson = null;
|
|
800
|
+
if (parsed.encrypted === true && typeof parsed.data === "string" && typeof parsed.iv === "string") {
|
|
801
|
+
try {
|
|
802
|
+
const decryptedText = decrypt(parsed.data, parsed.iv);
|
|
803
|
+
decryptedJson = JSON.parse(decryptedText);
|
|
804
|
+
} catch {
|
|
805
|
+
return { version: 2, gateways: [] };
|
|
806
|
+
}
|
|
807
|
+
} else {
|
|
808
|
+
decryptedJson = parsed;
|
|
809
|
+
}
|
|
810
|
+
if (decryptedJson && typeof decryptedJson === "object") {
|
|
811
|
+
const obj = decryptedJson;
|
|
812
|
+
if (obj.version === 2 && Array.isArray(obj.gateways)) {
|
|
813
|
+
return decryptedJson;
|
|
814
|
+
} else {
|
|
815
|
+
const gateways = [];
|
|
816
|
+
const flat = decryptedJson;
|
|
817
|
+
if (flat.OPENROUTER_API_KEY) {
|
|
818
|
+
gateways.push({
|
|
819
|
+
id: "migrated-openrouter",
|
|
820
|
+
gatewaySlug: "openrouter",
|
|
821
|
+
fields: { OPENROUTER_API_KEY: flat.OPENROUTER_API_KEY }
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
if (flat.OPENAI_API_KEY) {
|
|
825
|
+
gateways.push({
|
|
826
|
+
id: "migrated-openai",
|
|
827
|
+
gatewaySlug: "openai",
|
|
828
|
+
fields: {
|
|
829
|
+
OPENAI_API_KEY: flat.OPENAI_API_KEY,
|
|
830
|
+
OPENAI_BASE_URL: flat.OPENAI_BASE_URL || ""
|
|
831
|
+
}
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
if (flat.ANTHROPIC_API_KEY) {
|
|
835
|
+
gateways.push({
|
|
836
|
+
id: "migrated-anthropic",
|
|
837
|
+
gatewaySlug: "anthropic",
|
|
838
|
+
fields: {
|
|
839
|
+
ANTHROPIC_API_KEY: flat.ANTHROPIC_API_KEY,
|
|
840
|
+
ANTHROPIC_BASE_URL: flat.ANTHROPIC_BASE_URL || ""
|
|
841
|
+
}
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
const cfKey = flat.CLOUDFLARE_API_KEY || flat.CF_API_KEY;
|
|
845
|
+
if (cfKey) {
|
|
846
|
+
gateways.push({
|
|
847
|
+
id: "migrated-cloudflare",
|
|
848
|
+
gatewaySlug: "cloudflare",
|
|
849
|
+
fields: {
|
|
850
|
+
CLOUDFLARE_API_KEY: cfKey,
|
|
851
|
+
CLOUDFLARE_ACCOUNT_ID: flat.CLOUDFLARE_ACCOUNT_ID || flat.CF_ACCOUNT_ID || "",
|
|
852
|
+
CLOUDFLARE_GATEWAY_ID: flat.CLOUDFLARE_GATEWAY_ID || flat.CF_GATEWAY_ID || ""
|
|
853
|
+
}
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
const migratedConfig = { version: 2, gateways };
|
|
857
|
+
saveFalconConfigV2(migratedConfig);
|
|
858
|
+
return migratedConfig;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
} catch {
|
|
864
|
+
}
|
|
865
|
+
return { version: 2, gateways: [] };
|
|
866
|
+
}
|
|
867
|
+
function saveFalconConfigV2(config) {
|
|
868
|
+
const falconDir = getFalconDir();
|
|
869
|
+
const configFile = getConfigFile();
|
|
870
|
+
try {
|
|
871
|
+
if (!fs4.existsSync(falconDir)) {
|
|
872
|
+
fs4.mkdirSync(falconDir, { recursive: true });
|
|
873
|
+
}
|
|
874
|
+
const plainText = JSON.stringify(config);
|
|
875
|
+
const encrypted = encrypt(plainText);
|
|
876
|
+
const payload = {
|
|
877
|
+
version: 2,
|
|
878
|
+
encrypted: true,
|
|
879
|
+
iv: encrypted.iv,
|
|
880
|
+
data: encrypted.data
|
|
881
|
+
};
|
|
882
|
+
fs4.writeFileSync(configFile, JSON.stringify(payload, null, 2), "utf8");
|
|
883
|
+
} catch {
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// src/gateways/index.ts
|
|
888
|
+
var ALL_GATEWAYS = [
|
|
889
|
+
new OpenRouterGateway(),
|
|
890
|
+
new OpenAIGateway(),
|
|
891
|
+
new AnthropicGateway(),
|
|
892
|
+
new CloudflareGateway()
|
|
893
|
+
];
|
|
894
|
+
function getGatewayInstanceLabel(gatewaySlug, fields) {
|
|
895
|
+
if (gatewaySlug === "openai") {
|
|
896
|
+
const baseUrl = fields.OPENAI_BASE_URL || DEFAULT_OPENAI_BASE_URL;
|
|
897
|
+
try {
|
|
898
|
+
const url = baseUrl.includes("://") ? baseUrl : `http://${baseUrl}`;
|
|
899
|
+
const parsed = new URL(url);
|
|
900
|
+
if (PROVIDER_HOST_MAPPINGS[parsed.host]) {
|
|
901
|
+
return PROVIDER_HOST_MAPPINGS[parsed.host];
|
|
902
|
+
}
|
|
903
|
+
return `OpenAI@${parsed.host}`;
|
|
904
|
+
} catch {
|
|
905
|
+
return `OpenAI@${baseUrl}`;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
if (gatewaySlug === "anthropic") {
|
|
909
|
+
const baseUrl = fields.ANTHROPIC_BASE_URL || DEFAULT_ANTHROPIC_BASE_URL;
|
|
910
|
+
try {
|
|
911
|
+
const url = baseUrl.includes("://") ? baseUrl : `http://${baseUrl}`;
|
|
912
|
+
const parsed = new URL(url);
|
|
913
|
+
if (PROVIDER_HOST_MAPPINGS[parsed.host]) {
|
|
914
|
+
return PROVIDER_HOST_MAPPINGS[parsed.host];
|
|
915
|
+
}
|
|
916
|
+
return `Anthropic@${parsed.host}`;
|
|
917
|
+
} catch {
|
|
918
|
+
return `Anthropic@${baseUrl}`;
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
if (gatewaySlug === "openrouter") {
|
|
922
|
+
return "OpenRouter";
|
|
923
|
+
}
|
|
924
|
+
if (gatewaySlug === "cloudflare") {
|
|
925
|
+
const accountId = fields.CLOUDFLARE_ACCOUNT_ID || fields.CF_ACCOUNT_ID;
|
|
926
|
+
return accountId ? `Cloudflare@${accountId}` : "Cloudflare AI Gateway";
|
|
927
|
+
}
|
|
928
|
+
const gw = ALL_GATEWAYS.find((g) => g.slug === gatewaySlug);
|
|
929
|
+
return gw ? gw.name : gatewaySlug;
|
|
930
|
+
}
|
|
931
|
+
function detectGatewayInstances() {
|
|
932
|
+
const instances = [];
|
|
933
|
+
if (process.env.OPENROUTER_API_KEY) {
|
|
934
|
+
const gw = ALL_GATEWAYS.find((g) => g.slug === "openrouter");
|
|
935
|
+
instances.push({
|
|
936
|
+
id: "env-openrouter",
|
|
937
|
+
gateway: gw,
|
|
938
|
+
name: "OpenRouter",
|
|
939
|
+
apiKey: process.env.OPENROUTER_API_KEY,
|
|
940
|
+
fields: { OPENROUTER_API_KEY: process.env.OPENROUTER_API_KEY },
|
|
941
|
+
isEnv: true
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
if (process.env.OPENAI_API_KEY) {
|
|
945
|
+
const gw = ALL_GATEWAYS.find((g) => g.slug === "openai");
|
|
946
|
+
const fields = {
|
|
947
|
+
OPENAI_API_KEY: process.env.OPENAI_API_KEY
|
|
948
|
+
};
|
|
949
|
+
if (process.env.OPENAI_BASE_URL) {
|
|
950
|
+
fields.OPENAI_BASE_URL = process.env.OPENAI_BASE_URL;
|
|
951
|
+
}
|
|
952
|
+
instances.push({
|
|
953
|
+
id: "env-openai",
|
|
954
|
+
gateway: gw,
|
|
955
|
+
name: getGatewayInstanceLabel("openai", fields),
|
|
956
|
+
apiKey: process.env.OPENAI_API_KEY,
|
|
957
|
+
fields,
|
|
958
|
+
isEnv: true
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
if (process.env.ANTHROPIC_API_KEY) {
|
|
962
|
+
const gw = ALL_GATEWAYS.find((g) => g.slug === "anthropic");
|
|
963
|
+
const fields = {
|
|
964
|
+
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY
|
|
965
|
+
};
|
|
966
|
+
if (process.env.ANTHROPIC_BASE_URL) {
|
|
967
|
+
fields.ANTHROPIC_BASE_URL = process.env.ANTHROPIC_BASE_URL;
|
|
968
|
+
}
|
|
969
|
+
instances.push({
|
|
970
|
+
id: "env-anthropic",
|
|
971
|
+
gateway: gw,
|
|
972
|
+
name: getGatewayInstanceLabel("anthropic", fields),
|
|
973
|
+
apiKey: process.env.ANTHROPIC_API_KEY,
|
|
974
|
+
fields,
|
|
975
|
+
isEnv: true
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
const cfKey = process.env.CLOUDFLARE_API_KEY || process.env.CF_API_KEY;
|
|
979
|
+
if (cfKey) {
|
|
980
|
+
const gw = ALL_GATEWAYS.find((g) => g.slug === "cloudflare");
|
|
981
|
+
const fields = {
|
|
982
|
+
CLOUDFLARE_API_KEY: cfKey
|
|
983
|
+
};
|
|
984
|
+
const cfAcc = process.env.CLOUDFLARE_ACCOUNT_ID || process.env.CF_ACCOUNT_ID;
|
|
985
|
+
if (cfAcc) fields.CLOUDFLARE_ACCOUNT_ID = cfAcc;
|
|
986
|
+
const cfGw = process.env.CLOUDFLARE_GATEWAY_ID || process.env.CF_GATEWAY_ID;
|
|
987
|
+
if (cfGw) fields.CLOUDFLARE_GATEWAY_ID = cfGw;
|
|
988
|
+
instances.push({
|
|
989
|
+
id: "env-cloudflare",
|
|
990
|
+
gateway: gw,
|
|
991
|
+
name: getGatewayInstanceLabel("cloudflare", fields),
|
|
992
|
+
apiKey: cfKey,
|
|
993
|
+
fields,
|
|
994
|
+
isEnv: true
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
const config = loadFalconConfigV2();
|
|
998
|
+
for (const gwConfig of config.gateways) {
|
|
999
|
+
const gw = ALL_GATEWAYS.find((g) => g.slug === gwConfig.gatewaySlug);
|
|
1000
|
+
if (gw) {
|
|
1001
|
+
let apiKey = "";
|
|
1002
|
+
if (gwConfig.gatewaySlug === "openrouter") {
|
|
1003
|
+
apiKey = gwConfig.fields.OPENROUTER_API_KEY || "";
|
|
1004
|
+
} else if (gwConfig.gatewaySlug === "openai") {
|
|
1005
|
+
apiKey = gwConfig.fields.OPENAI_API_KEY || "";
|
|
1006
|
+
} else if (gwConfig.gatewaySlug === "anthropic") {
|
|
1007
|
+
apiKey = gwConfig.fields.ANTHROPIC_API_KEY || "";
|
|
1008
|
+
} else if (gwConfig.gatewaySlug === "cloudflare") {
|
|
1009
|
+
apiKey = gwConfig.fields.CLOUDFLARE_API_KEY || "";
|
|
1010
|
+
}
|
|
1011
|
+
instances.push({
|
|
1012
|
+
id: gwConfig.id,
|
|
1013
|
+
gateway: gw,
|
|
1014
|
+
name: getGatewayInstanceLabel(gwConfig.gatewaySlug, gwConfig.fields),
|
|
1015
|
+
apiKey,
|
|
1016
|
+
fields: gwConfig.fields
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
return instances;
|
|
1021
|
+
}
|
|
1022
|
+
function withGatewayEnv(instance, fn) {
|
|
1023
|
+
const keysToSave = [
|
|
1024
|
+
"OPENAI_API_KEY",
|
|
1025
|
+
"OPENAI_BASE_URL",
|
|
1026
|
+
"ANTHROPIC_API_KEY",
|
|
1027
|
+
"ANTHROPIC_BASE_URL",
|
|
1028
|
+
"CLOUDFLARE_API_KEY",
|
|
1029
|
+
"CF_API_KEY",
|
|
1030
|
+
"CLOUDFLARE_ACCOUNT_ID",
|
|
1031
|
+
"CF_ACCOUNT_ID",
|
|
1032
|
+
"CLOUDFLARE_GATEWAY_ID",
|
|
1033
|
+
"CF_GATEWAY_ID",
|
|
1034
|
+
"OPENROUTER_API_KEY"
|
|
1035
|
+
];
|
|
1036
|
+
const saved = {};
|
|
1037
|
+
for (const k of keysToSave) {
|
|
1038
|
+
saved[k] = process.env[k];
|
|
1039
|
+
delete process.env[k];
|
|
1040
|
+
}
|
|
1041
|
+
for (const [k, v] of Object.entries(instance.fields)) {
|
|
1042
|
+
if (v !== void 0) {
|
|
1043
|
+
process.env[k] = v;
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
try {
|
|
1047
|
+
return fn();
|
|
1048
|
+
} finally {
|
|
1049
|
+
for (const k of keysToSave) {
|
|
1050
|
+
if (saved[k] !== void 0) {
|
|
1051
|
+
process.env[k] = saved[k];
|
|
1052
|
+
} else {
|
|
1053
|
+
delete process.env[k];
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
async function withGatewayEnvAsync(instance, fn) {
|
|
1059
|
+
const keysToSave = [
|
|
1060
|
+
"OPENAI_API_KEY",
|
|
1061
|
+
"OPENAI_BASE_URL",
|
|
1062
|
+
"ANTHROPIC_API_KEY",
|
|
1063
|
+
"ANTHROPIC_BASE_URL",
|
|
1064
|
+
"CLOUDFLARE_API_KEY",
|
|
1065
|
+
"CF_API_KEY",
|
|
1066
|
+
"CLOUDFLARE_ACCOUNT_ID",
|
|
1067
|
+
"CF_ACCOUNT_ID",
|
|
1068
|
+
"CLOUDFLARE_GATEWAY_ID",
|
|
1069
|
+
"CF_GATEWAY_ID",
|
|
1070
|
+
"OPENROUTER_API_KEY"
|
|
1071
|
+
];
|
|
1072
|
+
const saved = {};
|
|
1073
|
+
for (const k of keysToSave) {
|
|
1074
|
+
saved[k] = process.env[k];
|
|
1075
|
+
delete process.env[k];
|
|
1076
|
+
}
|
|
1077
|
+
for (const [k, v] of Object.entries(instance.fields)) {
|
|
1078
|
+
if (v !== void 0) {
|
|
1079
|
+
process.env[k] = v;
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
try {
|
|
1083
|
+
return await fn();
|
|
1084
|
+
} finally {
|
|
1085
|
+
for (const k of keysToSave) {
|
|
1086
|
+
if (saved[k] !== void 0) {
|
|
1087
|
+
process.env[k] = saved[k];
|
|
1088
|
+
} else {
|
|
1089
|
+
delete process.env[k];
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
// src/ui/App.tsx
|
|
1096
|
+
import { spawn as spawn2 } from "child_process";
|
|
1097
|
+
import { Box as Box7, render, Text as Text7, useApp } from "ink";
|
|
1098
|
+
import React, { useCallback, useEffect, useState as useState5 } from "react";
|
|
1099
|
+
|
|
1100
|
+
// src/recents.ts
|
|
1101
|
+
import fs5 from "fs";
|
|
1102
|
+
import path6 from "path";
|
|
1103
|
+
function getFalconDir2() {
|
|
1104
|
+
return process.env[ENV_FALCON_DIR] || DEFAULT_FALCON_DIR;
|
|
1105
|
+
}
|
|
1106
|
+
function getRecentsFile() {
|
|
1107
|
+
return path6.join(getFalconDir2(), "recents.json");
|
|
1108
|
+
}
|
|
1109
|
+
var MAX_RECENTS = 5;
|
|
1110
|
+
function readRecents() {
|
|
1111
|
+
try {
|
|
1112
|
+
const raw = fs5.readFileSync(getRecentsFile(), "utf8");
|
|
1113
|
+
const parsed = JSON.parse(raw);
|
|
1114
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
1115
|
+
} catch {
|
|
1116
|
+
return [];
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
function writeRecents(entries) {
|
|
1120
|
+
const falconDir = getFalconDir2();
|
|
1121
|
+
try {
|
|
1122
|
+
if (!fs5.existsSync(falconDir)) {
|
|
1123
|
+
fs5.mkdirSync(falconDir, { recursive: true });
|
|
1124
|
+
}
|
|
1125
|
+
fs5.writeFileSync(getRecentsFile(), JSON.stringify(entries, null, 2), "utf8");
|
|
1126
|
+
} catch {
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
function recordRecentModel(model) {
|
|
1130
|
+
const entries = readRecents().filter((e) => e.id !== model.id);
|
|
1131
|
+
entries.unshift({
|
|
1132
|
+
id: model.id,
|
|
1133
|
+
name: model.name,
|
|
1134
|
+
contextLength: model.contextLength,
|
|
1135
|
+
pricing: model.pricing,
|
|
1136
|
+
provider: model.provider,
|
|
1137
|
+
lastUsed: (/* @__PURE__ */ new Date()).toISOString()
|
|
1138
|
+
});
|
|
1139
|
+
writeRecents(entries.slice(0, MAX_RECENTS));
|
|
1140
|
+
}
|
|
1141
|
+
function getRecentModels() {
|
|
1142
|
+
return readRecents().map((e) => ({
|
|
1143
|
+
id: e.id,
|
|
1144
|
+
name: e.name,
|
|
1145
|
+
contextLength: e.contextLength,
|
|
1146
|
+
pricing: e.pricing,
|
|
1147
|
+
provider: e.provider
|
|
1148
|
+
}));
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
// src/ui/GatewayPicker.tsx
|
|
1152
|
+
import { Box, Text, useInput } from "ink";
|
|
1153
|
+
import { useState } from "react";
|
|
1154
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
1155
|
+
function GatewayPicker({
|
|
1156
|
+
instances,
|
|
1157
|
+
onAdd,
|
|
1158
|
+
onDelete,
|
|
1159
|
+
onUpdate,
|
|
1160
|
+
onBack
|
|
1161
|
+
}) {
|
|
1162
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
1163
|
+
const [message, setMessage] = useState(null);
|
|
1164
|
+
useInput((input, key) => {
|
|
1165
|
+
if (input === "a" || input === "A") {
|
|
1166
|
+
onAdd();
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
1169
|
+
if (key.escape) {
|
|
1170
|
+
onBack();
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
if (instances.length === 0) {
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
const highlighted = instances[selectedIndex];
|
|
1177
|
+
if (key.upArrow) {
|
|
1178
|
+
setSelectedIndex((prev) => Math.max(0, prev - 1));
|
|
1179
|
+
setMessage(null);
|
|
1180
|
+
} else if (key.downArrow) {
|
|
1181
|
+
setSelectedIndex((prev) => Math.min(instances.length - 1, prev + 1));
|
|
1182
|
+
setMessage(null);
|
|
1183
|
+
} else if (key.return) {
|
|
1184
|
+
onBack();
|
|
1185
|
+
} else if (input === "d" || input === "D") {
|
|
1186
|
+
if (highlighted) {
|
|
1187
|
+
if (highlighted.isEnv) {
|
|
1188
|
+
setMessage("Cannot delete system environment gateways.");
|
|
1189
|
+
} else {
|
|
1190
|
+
onDelete(highlighted);
|
|
1191
|
+
setSelectedIndex((prev) => Math.max(0, prev - 1));
|
|
1192
|
+
setMessage(`Deleted gateway: ${highlighted.name}`);
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
} else if (input === "u" || input === "U") {
|
|
1196
|
+
if (highlighted) {
|
|
1197
|
+
if (highlighted.isEnv) {
|
|
1198
|
+
setMessage("Cannot update system environment gateways.");
|
|
1199
|
+
} else {
|
|
1200
|
+
onUpdate(highlighted);
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
});
|
|
1205
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
|
|
1206
|
+
/* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: "\u{1F985} Falcon Gateway Manager" }),
|
|
1207
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "Manage your API key configurations and providers." }),
|
|
1208
|
+
/* @__PURE__ */ jsx(Box, { flexDirection: "column", marginTop: 1, children: instances.length === 0 ? /* @__PURE__ */ jsx(Text, { color: "yellow", children: "No active gateways. Press 'a' to add one." }) : instances.map((inst, i) => {
|
|
1209
|
+
const isSelected = i === selectedIndex;
|
|
1210
|
+
const maskedKey = inst.apiKey ? inst.apiKey.substring(0, 8) + "..." + inst.apiKey.substring(inst.apiKey.length - 4) : "None";
|
|
1211
|
+
return /* @__PURE__ */ jsxs(Box, { children: [
|
|
1212
|
+
/* @__PURE__ */ jsx(Text, { color: isSelected ? "cyan" : "white", children: isSelected ? "\u276F " : " " }),
|
|
1213
|
+
/* @__PURE__ */ jsx(Text, { color: isSelected ? "cyan" : "white", bold: isSelected, children: inst.name }),
|
|
1214
|
+
/* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
1215
|
+
" (",
|
|
1216
|
+
maskedKey,
|
|
1217
|
+
")"
|
|
1218
|
+
] }),
|
|
1219
|
+
inst.isEnv && /* @__PURE__ */ jsxs(Text, { color: "magenta", bold: true, children: [
|
|
1220
|
+
" ",
|
|
1221
|
+
"[Env] [read-only]"
|
|
1222
|
+
] })
|
|
1223
|
+
] }, inst.id);
|
|
1224
|
+
}) }),
|
|
1225
|
+
message && /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { color: "yellow", bold: true, children: message }) }),
|
|
1226
|
+
/* @__PURE__ */ jsxs(Box, { marginTop: 1, flexDirection: "column", children: [
|
|
1227
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2191\u2193 navigate \u2022 a add new gateway \u2022 Esc/Enter back" }),
|
|
1228
|
+
instances.length > 0 && /* @__PURE__ */ jsx(Text, { dimColor: true, children: "d delete selected \u2022 u update selected" })
|
|
1229
|
+
] })
|
|
1230
|
+
] });
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// src/ui/Header.tsx
|
|
1234
|
+
import { Box as Box2, Text as Text2 } from "ink";
|
|
1235
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
1236
|
+
var FALCON_LOGO = `
|
|
1237
|
+
\u256D\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\u256E
|
|
1238
|
+
\u2502 \u{1F985} F A L C O N \u2502
|
|
1239
|
+
\u2502 Multi-Gateway Agent Runner \u2502
|
|
1240
|
+
\u2570\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\u256F`;
|
|
1241
|
+
function Header({ agent }) {
|
|
1242
|
+
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
|
|
1243
|
+
/* @__PURE__ */ jsx2(Text2, { color: "magenta", bold: true, children: FALCON_LOGO }),
|
|
1244
|
+
/* @__PURE__ */ jsx2(Box2, { marginTop: 1, marginLeft: 2, children: /* @__PURE__ */ jsxs2(Text2, { children: [
|
|
1245
|
+
"Agent:",
|
|
1246
|
+
" ",
|
|
1247
|
+
/* @__PURE__ */ jsx2(Text2, { bold: true, color: "green", children: agent.name }),
|
|
1248
|
+
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " \u2022 v0.1.0" })
|
|
1249
|
+
] }) })
|
|
1250
|
+
] });
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// src/ui/LaunchConfirm.tsx
|
|
1254
|
+
import { Box as Box3, Text as Text3, useInput as useInput2 } from "ink";
|
|
1255
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1256
|
+
function LaunchConfirm({ agent, gateway, model, onConfirm, onCancel }) {
|
|
1257
|
+
useInput2((_input, key) => {
|
|
1258
|
+
if (key.return) {
|
|
1259
|
+
onConfirm();
|
|
1260
|
+
} else if (key.escape) {
|
|
1261
|
+
onCancel();
|
|
1262
|
+
}
|
|
1263
|
+
});
|
|
1264
|
+
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", marginTop: 1, children: [
|
|
1265
|
+
/* @__PURE__ */ jsxs3(Text3, { children: [
|
|
1266
|
+
/* @__PURE__ */ jsx3(Text3, { color: "green", children: "\u2713" }),
|
|
1267
|
+
" Connected to",
|
|
1268
|
+
" ",
|
|
1269
|
+
/* @__PURE__ */ jsx3(Text3, { bold: true, color: "cyan", children: gateway.name })
|
|
1270
|
+
] }),
|
|
1271
|
+
/* @__PURE__ */ jsxs3(
|
|
1272
|
+
Box3,
|
|
1273
|
+
{
|
|
1274
|
+
marginTop: 1,
|
|
1275
|
+
flexDirection: "column",
|
|
1276
|
+
borderStyle: "round",
|
|
1277
|
+
borderColor: "magenta",
|
|
1278
|
+
paddingX: 2,
|
|
1279
|
+
paddingY: 1,
|
|
1280
|
+
children: [
|
|
1281
|
+
/* @__PURE__ */ jsx3(Text3, { bold: true, color: "white", children: "Launch Configuration" }),
|
|
1282
|
+
/* @__PURE__ */ jsxs3(Box3, { marginTop: 1, flexDirection: "column", children: [
|
|
1283
|
+
/* @__PURE__ */ jsxs3(Box3, { children: [
|
|
1284
|
+
/* @__PURE__ */ jsx3(Box3, { width: 14, children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Agent:" }) }),
|
|
1285
|
+
/* @__PURE__ */ jsx3(Text3, { bold: true, color: "green", children: agent.name })
|
|
1286
|
+
] }),
|
|
1287
|
+
/* @__PURE__ */ jsxs3(Box3, { children: [
|
|
1288
|
+
/* @__PURE__ */ jsx3(Box3, { width: 14, children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Gateway:" }) }),
|
|
1289
|
+
/* @__PURE__ */ jsx3(Text3, { bold: true, color: "cyan", children: gateway.name })
|
|
1290
|
+
] }),
|
|
1291
|
+
/* @__PURE__ */ jsxs3(Box3, { children: [
|
|
1292
|
+
/* @__PURE__ */ jsx3(Box3, { width: 14, children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Model:" }) }),
|
|
1293
|
+
/* @__PURE__ */ jsx3(Text3, { bold: true, color: "yellow", children: model.name })
|
|
1294
|
+
] }),
|
|
1295
|
+
/* @__PURE__ */ jsxs3(Box3, { children: [
|
|
1296
|
+
/* @__PURE__ */ jsx3(Box3, { width: 14, children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Model ID:" }) }),
|
|
1297
|
+
/* @__PURE__ */ jsx3(Text3, { color: "gray", children: model.id })
|
|
1298
|
+
] }),
|
|
1299
|
+
model.contextLength && /* @__PURE__ */ jsxs3(Box3, { children: [
|
|
1300
|
+
/* @__PURE__ */ jsx3(Box3, { width: 14, children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Context:" }) }),
|
|
1301
|
+
/* @__PURE__ */ jsx3(Text3, { children: formatContextLength(model.contextLength) })
|
|
1302
|
+
] }),
|
|
1303
|
+
model.pricing && /* @__PURE__ */ jsxs3(Box3, { children: [
|
|
1304
|
+
/* @__PURE__ */ jsx3(Box3, { width: 14, children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Pricing:" }) }),
|
|
1305
|
+
/* @__PURE__ */ jsxs3(Text3, { color: "green", children: [
|
|
1306
|
+
model.pricing.prompt,
|
|
1307
|
+
" in / ",
|
|
1308
|
+
model.pricing.completion,
|
|
1309
|
+
" out"
|
|
1310
|
+
] })
|
|
1311
|
+
] })
|
|
1312
|
+
] })
|
|
1313
|
+
]
|
|
1314
|
+
}
|
|
1315
|
+
),
|
|
1316
|
+
/* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsxs3(Text3, { children: [
|
|
1317
|
+
/* @__PURE__ */ jsx3(Text3, { color: "green", bold: true, children: "Enter" }),
|
|
1318
|
+
" ",
|
|
1319
|
+
"to launch \u2022",
|
|
1320
|
+
" ",
|
|
1321
|
+
/* @__PURE__ */ jsx3(Text3, { color: "yellow", bold: true, children: "Esc" }),
|
|
1322
|
+
" ",
|
|
1323
|
+
"to go back"
|
|
1324
|
+
] }) })
|
|
1325
|
+
] });
|
|
1326
|
+
}
|
|
1327
|
+
function formatContextLength(len) {
|
|
1328
|
+
if (len >= 1e6) return `${(len / 1e6).toFixed(1)}M tokens`;
|
|
1329
|
+
if (len >= 1e3) return `${(len / 1e3).toFixed(0)}k tokens`;
|
|
1330
|
+
return `${len} tokens`;
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
// src/ui/ModelPicker.tsx
|
|
1334
|
+
import { Box as Box4, Text as Text4, useInput as useInput3 } from "ink";
|
|
1335
|
+
import { useMemo, useState as useState2 } from "react";
|
|
1336
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1337
|
+
var PAGE_SIZE = 12;
|
|
1338
|
+
function levenshtein(a, b) {
|
|
1339
|
+
const m = a.length;
|
|
1340
|
+
const n = b.length;
|
|
1341
|
+
const dp = Array.from({ length: (m + 1) * (n + 1) }, () => 0);
|
|
1342
|
+
const idx = (i, j) => i * (n + 1) + j;
|
|
1343
|
+
for (let i = 0; i <= m; i++) dp[idx(i, 0)] = i;
|
|
1344
|
+
for (let j = 0; j <= n; j++) dp[idx(0, j)] = j;
|
|
1345
|
+
for (let i = 1; i <= m; i++) {
|
|
1346
|
+
for (let j = 1; j <= n; j++) {
|
|
1347
|
+
if (a[i - 1] === b[j - 1]) {
|
|
1348
|
+
dp[idx(i, j)] = dp[idx(i - 1, j - 1)];
|
|
1349
|
+
} else {
|
|
1350
|
+
dp[idx(i, j)] = 1 + Math.min(
|
|
1351
|
+
dp[idx(i - 1, j)],
|
|
1352
|
+
// deletion
|
|
1353
|
+
dp[idx(i, j - 1)],
|
|
1354
|
+
// insertion
|
|
1355
|
+
dp[idx(i - 1, j - 1)]
|
|
1356
|
+
// substitution
|
|
1357
|
+
);
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
return dp[idx(m, n)] ?? 0;
|
|
1362
|
+
}
|
|
1363
|
+
function fuzzyMatch(query, target) {
|
|
1364
|
+
const q = query.toLowerCase();
|
|
1365
|
+
const t = target.toLowerCase();
|
|
1366
|
+
if (!q) return true;
|
|
1367
|
+
if (t.includes(q)) return true;
|
|
1368
|
+
const threshold = Math.min(3, Math.floor(q.length / 4));
|
|
1369
|
+
if (threshold === 0) return false;
|
|
1370
|
+
const winLen = q.length;
|
|
1371
|
+
for (let start = 0; start <= t.length - winLen; start++) {
|
|
1372
|
+
const window = t.slice(start, start + winLen);
|
|
1373
|
+
if (levenshtein(q, window) <= threshold) return true;
|
|
1374
|
+
}
|
|
1375
|
+
return levenshtein(q, t) <= threshold;
|
|
1376
|
+
}
|
|
1377
|
+
function modelMatchesQuery(model, query) {
|
|
1378
|
+
if (!query) return true;
|
|
1379
|
+
return fuzzyMatch(query, model.name) || fuzzyMatch(query, model.id) || !!model.provider && fuzzyMatch(query, model.provider);
|
|
1380
|
+
}
|
|
1381
|
+
function ModelPicker({
|
|
1382
|
+
models,
|
|
1383
|
+
recentModels = [],
|
|
1384
|
+
onSelect,
|
|
1385
|
+
onCancel,
|
|
1386
|
+
onConfigure,
|
|
1387
|
+
showGatewayBadge
|
|
1388
|
+
}) {
|
|
1389
|
+
const [searchQuery, setSearchQuery] = useState2("");
|
|
1390
|
+
const [selectedIndex, setSelectedIndex] = useState2(0);
|
|
1391
|
+
const recentIds = useMemo(() => new Set(recentModels.map((m) => m.id)), [recentModels]);
|
|
1392
|
+
const filteredModels = useMemo(() => {
|
|
1393
|
+
if (!searchQuery) {
|
|
1394
|
+
const rest = models.filter((m) => !recentIds.has(m.id));
|
|
1395
|
+
return [...recentModels, ...rest];
|
|
1396
|
+
}
|
|
1397
|
+
return models.filter((m) => modelMatchesQuery(m, searchQuery));
|
|
1398
|
+
}, [models, recentModels, recentIds, searchQuery]);
|
|
1399
|
+
const safeIndex = Math.min(selectedIndex, Math.max(0, filteredModels.length - 1));
|
|
1400
|
+
const scrollOffset = Math.max(0, safeIndex - PAGE_SIZE + 3);
|
|
1401
|
+
const visibleModels = filteredModels.slice(scrollOffset, scrollOffset + PAGE_SIZE);
|
|
1402
|
+
useInput3((input, key) => {
|
|
1403
|
+
if (onConfigure && key.ctrl && input === "g") {
|
|
1404
|
+
onConfigure();
|
|
1405
|
+
return;
|
|
1406
|
+
}
|
|
1407
|
+
if (key.escape) {
|
|
1408
|
+
onCancel();
|
|
1409
|
+
return;
|
|
1410
|
+
}
|
|
1411
|
+
if (key.return) {
|
|
1412
|
+
if (filteredModels.length > 0) {
|
|
1413
|
+
const model = filteredModels[safeIndex];
|
|
1414
|
+
if (model) {
|
|
1415
|
+
onSelect(model);
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1420
|
+
if (key.upArrow) {
|
|
1421
|
+
setSelectedIndex((prev) => Math.max(0, prev - 1));
|
|
1422
|
+
return;
|
|
1423
|
+
}
|
|
1424
|
+
if (key.downArrow) {
|
|
1425
|
+
setSelectedIndex((prev) => Math.min(filteredModels.length - 1, prev + 1));
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1428
|
+
if (key.backspace || key.delete) {
|
|
1429
|
+
setSearchQuery((prev) => prev.slice(0, -1));
|
|
1430
|
+
setSelectedIndex(0);
|
|
1431
|
+
return;
|
|
1432
|
+
}
|
|
1433
|
+
if (input && !key.ctrl && !key.meta) {
|
|
1434
|
+
setSearchQuery((prev) => prev + input);
|
|
1435
|
+
setSelectedIndex(0);
|
|
1436
|
+
}
|
|
1437
|
+
});
|
|
1438
|
+
const hasRecents = !searchQuery && recentModels.length > 0;
|
|
1439
|
+
return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", marginTop: 1, children: [
|
|
1440
|
+
/* @__PURE__ */ jsxs4(Box4, { children: [
|
|
1441
|
+
/* @__PURE__ */ jsxs4(Text4, { color: "cyan", bold: true, children: [
|
|
1442
|
+
"\u276F",
|
|
1443
|
+
" "
|
|
1444
|
+
] }),
|
|
1445
|
+
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Search: " }),
|
|
1446
|
+
/* @__PURE__ */ jsx4(Text4, { color: "white", bold: true, children: searchQuery || "" }),
|
|
1447
|
+
/* @__PURE__ */ jsx4(Text4, { color: "gray", children: searchQuery ? "" : "(type to filter)" })
|
|
1448
|
+
] }),
|
|
1449
|
+
/* @__PURE__ */ jsx4(Box4, { marginTop: 1, flexDirection: "column", children: filteredModels.length === 0 ? /* @__PURE__ */ jsxs4(Text4, { color: "yellow", children: [
|
|
1450
|
+
'No models match "',
|
|
1451
|
+
searchQuery,
|
|
1452
|
+
'"'
|
|
1453
|
+
] }) : visibleModels.map((model, i) => {
|
|
1454
|
+
const actualIndex = scrollOffset + i;
|
|
1455
|
+
const isSelected = actualIndex === safeIndex;
|
|
1456
|
+
const isRecentItem = hasRecents && recentIds.has(model.id);
|
|
1457
|
+
const showRecentsHeader = hasRecents && actualIndex === 0;
|
|
1458
|
+
const showDivider = hasRecents && actualIndex === recentModels.length && filteredModels.length > recentModels.length;
|
|
1459
|
+
return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
|
|
1460
|
+
showRecentsHeader && /* @__PURE__ */ jsxs4(Text4, { color: "yellow", dimColor: true, children: [
|
|
1461
|
+
" ",
|
|
1462
|
+
"Recently used"
|
|
1463
|
+
] }),
|
|
1464
|
+
showDivider && /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "\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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }),
|
|
1465
|
+
/* @__PURE__ */ jsxs4(Box4, { children: [
|
|
1466
|
+
/* @__PURE__ */ jsx4(Text4, { color: isSelected ? "cyan" : "white", children: isSelected ? "\u276F " : " " }),
|
|
1467
|
+
/* @__PURE__ */ jsxs4(Box4, { width: 65, flexDirection: "row", children: [
|
|
1468
|
+
/* @__PURE__ */ jsx4(
|
|
1469
|
+
Text4,
|
|
1470
|
+
{
|
|
1471
|
+
color: isSelected ? "cyan" : isRecentItem ? "yellow" : "white",
|
|
1472
|
+
bold: isSelected,
|
|
1473
|
+
children: model.name.length > 40 ? model.name.substring(0, 37) + "..." : model.name
|
|
1474
|
+
}
|
|
1475
|
+
),
|
|
1476
|
+
showGatewayBadge && model.gatewayInstance && /* @__PURE__ */ jsxs4(Text4, { color: "gray", children: [
|
|
1477
|
+
" (",
|
|
1478
|
+
model.gatewayInstance.name,
|
|
1479
|
+
")"
|
|
1480
|
+
] })
|
|
1481
|
+
] }),
|
|
1482
|
+
model.contextLength && /* @__PURE__ */ jsx4(Box4, { width: 12, children: /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: formatContextLength2(model.contextLength) }) }),
|
|
1483
|
+
model.pricing && /* @__PURE__ */ jsx4(Box4, { children: /* @__PURE__ */ jsx4(Text4, { color: "green", dimColor: true, children: model.pricing.prompt }) })
|
|
1484
|
+
] })
|
|
1485
|
+
] }, model.id + "-" + actualIndex);
|
|
1486
|
+
}) }),
|
|
1487
|
+
/* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
|
|
1488
|
+
filteredModels.length,
|
|
1489
|
+
" model",
|
|
1490
|
+
filteredModels.length !== 1 ? "s" : "",
|
|
1491
|
+
" \u2022 \u2191\u2193 navigate \u2022 Enter select \u2022 Esc cancel",
|
|
1492
|
+
onConfigure ? " \u2022 Ctrl+G configure" : ""
|
|
1493
|
+
] }) }),
|
|
1494
|
+
filteredModels.length > PAGE_SIZE && /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
|
|
1495
|
+
"Showing ",
|
|
1496
|
+
scrollOffset + 1,
|
|
1497
|
+
"-",
|
|
1498
|
+
Math.min(scrollOffset + PAGE_SIZE, filteredModels.length),
|
|
1499
|
+
" of",
|
|
1500
|
+
" ",
|
|
1501
|
+
filteredModels.length
|
|
1502
|
+
] })
|
|
1503
|
+
] });
|
|
1504
|
+
}
|
|
1505
|
+
function formatContextLength2(len) {
|
|
1506
|
+
if (len >= 1e6) return `${(len / 1e6).toFixed(0)}M ctx`;
|
|
1507
|
+
if (len >= 1e3) return `${(len / 1e3).toFixed(0)}k ctx`;
|
|
1508
|
+
return `${len} ctx`;
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
// src/ui/ConfigureGatewayPicker.tsx
|
|
1512
|
+
import { Box as Box5, Text as Text5, useInput as useInput4 } from "ink";
|
|
1513
|
+
import { useState as useState3 } from "react";
|
|
1514
|
+
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1515
|
+
function ConfigureGatewayPicker({ onSelect, onCancel, title }) {
|
|
1516
|
+
const [selectedIndex, setSelectedIndex] = useState3(0);
|
|
1517
|
+
useInput4((_input, key) => {
|
|
1518
|
+
if (key.upArrow) {
|
|
1519
|
+
setSelectedIndex((prev) => Math.max(0, prev - 1));
|
|
1520
|
+
} else if (key.downArrow) {
|
|
1521
|
+
setSelectedIndex((prev) => Math.min(ALL_GATEWAYS.length - 1, prev + 1));
|
|
1522
|
+
} else if (key.return) {
|
|
1523
|
+
const selected = ALL_GATEWAYS[selectedIndex];
|
|
1524
|
+
if (selected) {
|
|
1525
|
+
onSelect(selected);
|
|
1526
|
+
}
|
|
1527
|
+
} else if (key.escape) {
|
|
1528
|
+
onCancel();
|
|
1529
|
+
}
|
|
1530
|
+
});
|
|
1531
|
+
return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", marginTop: 1, children: [
|
|
1532
|
+
/* @__PURE__ */ jsx5(Text5, { color: "yellow", bold: true, children: title || "No API keys detected. Select a provider to configure:" }),
|
|
1533
|
+
/* @__PURE__ */ jsx5(Box5, { flexDirection: "column", marginTop: 1, children: ALL_GATEWAYS.map((gw, i) => {
|
|
1534
|
+
const isSelected = i === selectedIndex;
|
|
1535
|
+
return /* @__PURE__ */ jsxs5(Box5, { children: [
|
|
1536
|
+
/* @__PURE__ */ jsx5(Text5, { color: isSelected ? "cyan" : "white", children: isSelected ? "\u276F " : " " }),
|
|
1537
|
+
/* @__PURE__ */ jsx5(Text5, { color: isSelected ? "cyan" : "white", bold: isSelected, children: gw.name })
|
|
1538
|
+
] }, gw.slug);
|
|
1539
|
+
}) }),
|
|
1540
|
+
/* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "\u2191\u2193 navigate \u2022 Enter select \u2022 Esc exit" }) })
|
|
1541
|
+
] });
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
// src/ui/ConfigureGatewayInput.tsx
|
|
1545
|
+
import { Box as Box6, Text as Text6, useInput as useInput5 } from "ink";
|
|
1546
|
+
import TextInput from "ink-text-input";
|
|
1547
|
+
import { useState as useState4 } from "react";
|
|
1548
|
+
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1549
|
+
var CONFIG_FIELDS = {
|
|
1550
|
+
openrouter: [{ label: "OpenRouter API Key", envVar: "OPENROUTER_API_KEY", mask: true }],
|
|
1551
|
+
openai: [
|
|
1552
|
+
{ label: "OpenAI API Key", envVar: "OPENAI_API_KEY", mask: true },
|
|
1553
|
+
{
|
|
1554
|
+
label: "OpenAI Base URL",
|
|
1555
|
+
envVar: "OPENAI_BASE_URL",
|
|
1556
|
+
defaultValue: DEFAULT_OPENAI_BASE_URL,
|
|
1557
|
+
optional: true
|
|
1558
|
+
}
|
|
1559
|
+
],
|
|
1560
|
+
anthropic: [
|
|
1561
|
+
{ label: "Anthropic API Key", envVar: "ANTHROPIC_API_KEY", mask: true },
|
|
1562
|
+
{
|
|
1563
|
+
label: "Anthropic Base URL",
|
|
1564
|
+
envVar: "ANTHROPIC_BASE_URL",
|
|
1565
|
+
defaultValue: DEFAULT_ANTHROPIC_BASE_URL,
|
|
1566
|
+
optional: true
|
|
1567
|
+
}
|
|
1568
|
+
],
|
|
1569
|
+
cloudflare: [
|
|
1570
|
+
{ label: "Cloudflare API Key", envVar: "CLOUDFLARE_API_KEY", mask: true },
|
|
1571
|
+
{ label: "Cloudflare Account ID", envVar: "CLOUDFLARE_ACCOUNT_ID" },
|
|
1572
|
+
{ label: "Cloudflare Gateway ID", envVar: "CLOUDFLARE_GATEWAY_ID", defaultValue: "default" }
|
|
1573
|
+
]
|
|
1574
|
+
};
|
|
1575
|
+
function ConfigureGatewayInput({
|
|
1576
|
+
gateway,
|
|
1577
|
+
editingInstance,
|
|
1578
|
+
onConfigured,
|
|
1579
|
+
onCancel
|
|
1580
|
+
}) {
|
|
1581
|
+
const fields = CONFIG_FIELDS[gateway.slug] || [
|
|
1582
|
+
{
|
|
1583
|
+
label: `${gateway.name} API Key`,
|
|
1584
|
+
envVar: `${gateway.slug.toUpperCase()}_API_KEY`,
|
|
1585
|
+
mask: true
|
|
1586
|
+
}
|
|
1587
|
+
];
|
|
1588
|
+
const [fieldIndex, setFieldIndex] = useState4(0);
|
|
1589
|
+
const [values, setValues] = useState4(() => {
|
|
1590
|
+
if (editingInstance) {
|
|
1591
|
+
return { ...editingInstance.fields };
|
|
1592
|
+
}
|
|
1593
|
+
return {};
|
|
1594
|
+
});
|
|
1595
|
+
const [currentVal, setCurrentVal] = useState4(() => {
|
|
1596
|
+
const currentField2 = fields[0];
|
|
1597
|
+
if (editingInstance && currentField2) {
|
|
1598
|
+
return editingInstance.fields[currentField2.envVar] || currentField2.defaultValue || "";
|
|
1599
|
+
}
|
|
1600
|
+
return currentField2?.defaultValue || "";
|
|
1601
|
+
});
|
|
1602
|
+
const [isValidating, setIsValidating] = useState4(false);
|
|
1603
|
+
const [validationError, setValidationError] = useState4(null);
|
|
1604
|
+
useInput5((_input, key) => {
|
|
1605
|
+
if (key.escape) {
|
|
1606
|
+
onCancel();
|
|
1607
|
+
}
|
|
1608
|
+
});
|
|
1609
|
+
const currentField = fields[fieldIndex];
|
|
1610
|
+
const handleSubmit = async (val) => {
|
|
1611
|
+
if (!currentField || isValidating) {
|
|
1612
|
+
return;
|
|
1613
|
+
}
|
|
1614
|
+
const trimmed = val.trim();
|
|
1615
|
+
if (!trimmed && !currentField.optional && !currentField.defaultValue) {
|
|
1616
|
+
return;
|
|
1617
|
+
}
|
|
1618
|
+
const finalVal = trimmed || "";
|
|
1619
|
+
const newValues = { ...values, [currentField.envVar]: finalVal };
|
|
1620
|
+
setValues(newValues);
|
|
1621
|
+
if (fieldIndex < fields.length - 1) {
|
|
1622
|
+
const nextIndex = fieldIndex + 1;
|
|
1623
|
+
setFieldIndex(nextIndex);
|
|
1624
|
+
const nextField = fields[nextIndex];
|
|
1625
|
+
const nextVal = (editingInstance ? newValues[nextField.envVar] : void 0) ?? nextField?.defaultValue ?? "";
|
|
1626
|
+
setCurrentVal(nextVal);
|
|
1627
|
+
} else {
|
|
1628
|
+
setIsValidating(true);
|
|
1629
|
+
setValidationError(null);
|
|
1630
|
+
let apiKey = "";
|
|
1631
|
+
if (gateway.slug === "openrouter") {
|
|
1632
|
+
apiKey = newValues.OPENROUTER_API_KEY || "";
|
|
1633
|
+
} else if (gateway.slug === "openai") {
|
|
1634
|
+
apiKey = newValues.OPENAI_API_KEY || "";
|
|
1635
|
+
} else if (gateway.slug === "anthropic") {
|
|
1636
|
+
apiKey = newValues.ANTHROPIC_API_KEY || "";
|
|
1637
|
+
} else if (gateway.slug === "cloudflare") {
|
|
1638
|
+
apiKey = newValues.CLOUDFLARE_API_KEY || "";
|
|
1639
|
+
}
|
|
1640
|
+
try {
|
|
1641
|
+
await withGatewayEnvAsync({ fields: newValues }, async () => {
|
|
1642
|
+
await gateway.listModels(apiKey);
|
|
1643
|
+
});
|
|
1644
|
+
setIsValidating(false);
|
|
1645
|
+
onConfigured(newValues);
|
|
1646
|
+
} catch (err) {
|
|
1647
|
+
setIsValidating(false);
|
|
1648
|
+
setValidationError(err instanceof Error ? err.message : String(err));
|
|
1649
|
+
setFieldIndex(0);
|
|
1650
|
+
const firstField = fields[0];
|
|
1651
|
+
setCurrentVal(newValues[firstField.envVar] || firstField.defaultValue || "");
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
};
|
|
1655
|
+
return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", marginTop: 1, children: [
|
|
1656
|
+
/* @__PURE__ */ jsx6(Text6, { bold: true, color: "cyan", children: editingInstance ? `Updating ${editingInstance.name}` : `Configuring ${gateway.name}` }),
|
|
1657
|
+
fields.slice(0, fieldIndex).map((f) => {
|
|
1658
|
+
const val = values[f.envVar] || "";
|
|
1659
|
+
const displayVal = val ? f.mask ? val.substring(0, 8) + "..." + val.substring(val.length - 4) : val : "<default>";
|
|
1660
|
+
return /* @__PURE__ */ jsxs6(Box6, { marginTop: 0.5, marginLeft: 2, children: [
|
|
1661
|
+
/* @__PURE__ */ jsx6(Text6, { color: "green", children: "\u2713 " }),
|
|
1662
|
+
/* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
|
|
1663
|
+
f.label,
|
|
1664
|
+
": "
|
|
1665
|
+
] }),
|
|
1666
|
+
/* @__PURE__ */ jsx6(Text6, { color: "white", children: displayVal })
|
|
1667
|
+
] }, f.envVar);
|
|
1668
|
+
}),
|
|
1669
|
+
!isValidating && currentField && /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", marginTop: 1, marginLeft: 2, children: [
|
|
1670
|
+
/* @__PURE__ */ jsxs6(Text6, { bold: true, children: [
|
|
1671
|
+
"Enter ",
|
|
1672
|
+
currentField.label,
|
|
1673
|
+
": ",
|
|
1674
|
+
currentField.optional ? "(optional)" : ""
|
|
1675
|
+
] }),
|
|
1676
|
+
currentField.defaultValue && /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
|
|
1677
|
+
"Default: ",
|
|
1678
|
+
currentField.defaultValue
|
|
1679
|
+
] }),
|
|
1680
|
+
/* @__PURE__ */ jsx6(Box6, { borderStyle: "single", borderColor: "cyan", paddingX: 1, marginTop: 0.5, width: 60, children: /* @__PURE__ */ jsx6(
|
|
1681
|
+
TextInput,
|
|
1682
|
+
{
|
|
1683
|
+
value: currentVal,
|
|
1684
|
+
onChange: setCurrentVal,
|
|
1685
|
+
onSubmit: handleSubmit,
|
|
1686
|
+
showCursor: true,
|
|
1687
|
+
placeholder: currentField.defaultValue,
|
|
1688
|
+
mask: currentField.mask ? "*" : void 0
|
|
1689
|
+
}
|
|
1690
|
+
) })
|
|
1691
|
+
] }),
|
|
1692
|
+
isValidating && /* @__PURE__ */ jsx6(Box6, { marginTop: 1, marginLeft: 2, flexDirection: "column", children: /* @__PURE__ */ jsx6(Text6, { color: "yellow", bold: true, children: "\u280B Validating connection with gateway API..." }) }),
|
|
1693
|
+
validationError && /* @__PURE__ */ jsxs6(Box6, { marginTop: 1, marginLeft: 2, flexDirection: "column", children: [
|
|
1694
|
+
/* @__PURE__ */ jsx6(Text6, { color: "red", bold: true, children: "\u2716 Validation Error:" }),
|
|
1695
|
+
/* @__PURE__ */ jsx6(Text6, { color: "red", children: validationError })
|
|
1696
|
+
] }),
|
|
1697
|
+
/* @__PURE__ */ jsx6(Box6, { marginTop: 1, marginLeft: 2, children: /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "Enter to submit \u2022 Esc to cancel" }) })
|
|
1698
|
+
] });
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
// src/ui/App.tsx
|
|
1702
|
+
import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
1703
|
+
var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
1704
|
+
function App({ agent, preselectedModel, preselectedGateway, extraArgs }) {
|
|
1705
|
+
const { exit } = useApp();
|
|
1706
|
+
const [state, setState] = useState5({ phase: "detect" });
|
|
1707
|
+
const [recentModels] = useState5(() => getRecentModels());
|
|
1708
|
+
const normalizedPreselectedGateway = preselectedGateway?.toLowerCase();
|
|
1709
|
+
const launchAgent = useCallback(
|
|
1710
|
+
async (gateway, model) => {
|
|
1711
|
+
recordRecentModel(model);
|
|
1712
|
+
setState({ phase: "launching", gateway, model });
|
|
1713
|
+
const config = gateway.gateway.getEnvConfig(gateway.apiKey, model.id);
|
|
1714
|
+
let resolved;
|
|
1715
|
+
try {
|
|
1716
|
+
resolved = await withGatewayEnvAsync({ fields: gateway.fields }, async () => {
|
|
1717
|
+
return await agent.resolveConfig(config, gateway.gateway.slug, gateway.apiKey, model.id);
|
|
1718
|
+
});
|
|
1719
|
+
} catch (err) {
|
|
1720
|
+
console.error(
|
|
1721
|
+
`\x1B[31mFailed to resolve config: ${err instanceof Error ? err.message : err}\x1B[0m`
|
|
1722
|
+
);
|
|
1723
|
+
process.exit(1);
|
|
1724
|
+
}
|
|
1725
|
+
const spawnConfig = withGatewayEnv({ fields: gateway.fields }, () => {
|
|
1726
|
+
return agent.buildSpawnConfig(resolved, model.id, extraArgs);
|
|
1727
|
+
});
|
|
1728
|
+
const cleanUp = () => {
|
|
1729
|
+
if (spawnConfig.cleanup) {
|
|
1730
|
+
try {
|
|
1731
|
+
spawnConfig.cleanup();
|
|
1732
|
+
} catch (_) {
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
};
|
|
1736
|
+
const handleSignalDuringLaunch = () => {
|
|
1737
|
+
cleanUp();
|
|
1738
|
+
process.exit(1);
|
|
1739
|
+
};
|
|
1740
|
+
process.on("SIGINT", handleSignalDuringLaunch);
|
|
1741
|
+
process.on("SIGTERM", handleSignalDuringLaunch);
|
|
1742
|
+
process.on("exit", cleanUp);
|
|
1743
|
+
setTimeout(() => {
|
|
1744
|
+
exit();
|
|
1745
|
+
setTimeout(() => {
|
|
1746
|
+
process.off("SIGINT", handleSignalDuringLaunch);
|
|
1747
|
+
process.off("SIGTERM", handleSignalDuringLaunch);
|
|
1748
|
+
const ignoreSignal = () => {
|
|
1749
|
+
};
|
|
1750
|
+
process.on("SIGINT", ignoreSignal);
|
|
1751
|
+
process.on("SIGTERM", ignoreSignal);
|
|
1752
|
+
const proc = spawn2(spawnConfig.command, spawnConfig.args, {
|
|
1753
|
+
stdio: "inherit",
|
|
1754
|
+
env: { ...process.env, ...gateway.fields, ...spawnConfig.env }
|
|
1755
|
+
});
|
|
1756
|
+
proc.on("error", (err) => {
|
|
1757
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1758
|
+
console.error(`\x1B[31mFailed to launch ${agent.name}: ${message}\x1B[0m`);
|
|
1759
|
+
process.off("SIGINT", ignoreSignal);
|
|
1760
|
+
process.off("SIGTERM", ignoreSignal);
|
|
1761
|
+
cleanUp();
|
|
1762
|
+
process.exit(1);
|
|
1763
|
+
});
|
|
1764
|
+
proc.on("exit", (code) => {
|
|
1765
|
+
process.off("SIGINT", ignoreSignal);
|
|
1766
|
+
process.off("SIGTERM", ignoreSignal);
|
|
1767
|
+
cleanUp();
|
|
1768
|
+
process.exit(code ?? 0);
|
|
1769
|
+
});
|
|
1770
|
+
}, 100);
|
|
1771
|
+
}, 800);
|
|
1772
|
+
},
|
|
1773
|
+
[agent, extraArgs, exit]
|
|
1774
|
+
);
|
|
1775
|
+
useEffect(() => {
|
|
1776
|
+
if (state.phase !== "detect") return;
|
|
1777
|
+
const instances = detectGatewayInstances();
|
|
1778
|
+
if (instances.length === 0) {
|
|
1779
|
+
if (normalizedPreselectedGateway) {
|
|
1780
|
+
const matchGateway = ALL_GATEWAYS.find((g) => g.slug === normalizedPreselectedGateway);
|
|
1781
|
+
if (matchGateway) {
|
|
1782
|
+
setState({ phase: "configure-gateway-input", gateway: matchGateway });
|
|
1783
|
+
return;
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
setState({ phase: "configure-gateway-list" });
|
|
1787
|
+
return;
|
|
1788
|
+
}
|
|
1789
|
+
if (normalizedPreselectedGateway) {
|
|
1790
|
+
const match = instances.find(
|
|
1791
|
+
(inst) => inst.gateway.slug === normalizedPreselectedGateway || inst.name.toLowerCase() === normalizedPreselectedGateway
|
|
1792
|
+
);
|
|
1793
|
+
if (match) {
|
|
1794
|
+
if (preselectedModel) {
|
|
1795
|
+
launchAgent(match, { id: preselectedModel, name: preselectedModel });
|
|
1796
|
+
return;
|
|
1797
|
+
}
|
|
1798
|
+
setState({ phase: "loading-models", instances: [match] });
|
|
1799
|
+
return;
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
if (preselectedModel && instances.length > 0) {
|
|
1803
|
+
const match = instances[0];
|
|
1804
|
+
if (match) {
|
|
1805
|
+
launchAgent(match, { id: preselectedModel, name: preselectedModel });
|
|
1806
|
+
return;
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
setState({ phase: "loading-models", instances });
|
|
1810
|
+
}, [launchAgent, normalizedPreselectedGateway, preselectedModel, state.phase]);
|
|
1811
|
+
useEffect(() => {
|
|
1812
|
+
if (state.phase !== "loading-models") return;
|
|
1813
|
+
const { instances } = state;
|
|
1814
|
+
const promises = instances.map(async (inst) => {
|
|
1815
|
+
try {
|
|
1816
|
+
const models = await withGatewayEnvAsync({ fields: inst.fields }, async () => {
|
|
1817
|
+
return await inst.gateway.listModels(inst.apiKey);
|
|
1818
|
+
});
|
|
1819
|
+
return models.map((m) => ({
|
|
1820
|
+
...m,
|
|
1821
|
+
gatewayInstance: inst
|
|
1822
|
+
}));
|
|
1823
|
+
} catch {
|
|
1824
|
+
return [];
|
|
1825
|
+
}
|
|
1826
|
+
});
|
|
1827
|
+
Promise.all(promises).then((results) => {
|
|
1828
|
+
const allModels = results.flat();
|
|
1829
|
+
if (allModels.length === 0) {
|
|
1830
|
+
setState({
|
|
1831
|
+
phase: "error",
|
|
1832
|
+
message: "Failed to load models from any configured gateways. Please check your configurations."
|
|
1833
|
+
});
|
|
1834
|
+
} else {
|
|
1835
|
+
setState({ phase: "pick-model", instances, models: allModels });
|
|
1836
|
+
}
|
|
1837
|
+
}).catch((err) => {
|
|
1838
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1839
|
+
setState({ phase: "error", message: `Failed to load models: ${message}` });
|
|
1840
|
+
});
|
|
1841
|
+
}, [state]);
|
|
1842
|
+
return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", paddingX: 1, children: [
|
|
1843
|
+
/* @__PURE__ */ jsx7(Header, { agent }),
|
|
1844
|
+
state.phase === "detect" && /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsxs7(Text7, { children: [
|
|
1845
|
+
/* @__PURE__ */ jsx7(Text7, { color: "cyan", children: "\u280B" }),
|
|
1846
|
+
" Detecting API keys..."
|
|
1847
|
+
] }) }),
|
|
1848
|
+
state.phase === "gateway-manager" && /* @__PURE__ */ jsx7(
|
|
1849
|
+
GatewayPicker,
|
|
1850
|
+
{
|
|
1851
|
+
instances: state.instances,
|
|
1852
|
+
onAdd: () => {
|
|
1853
|
+
setState({
|
|
1854
|
+
phase: "configure-gateway-list",
|
|
1855
|
+
title: "Select a provider to configure:"
|
|
1856
|
+
});
|
|
1857
|
+
},
|
|
1858
|
+
onDelete: (instance) => {
|
|
1859
|
+
const config = loadFalconConfigV2();
|
|
1860
|
+
config.gateways = config.gateways.filter((g) => g.id !== instance.id);
|
|
1861
|
+
saveFalconConfigV2(config);
|
|
1862
|
+
const nextInstances = detectGatewayInstances();
|
|
1863
|
+
setState({ phase: "gateway-manager", instances: nextInstances });
|
|
1864
|
+
},
|
|
1865
|
+
onUpdate: (instance) => {
|
|
1866
|
+
setState({
|
|
1867
|
+
phase: "configure-gateway-input",
|
|
1868
|
+
gateway: instance.gateway,
|
|
1869
|
+
editingInstance: instance
|
|
1870
|
+
});
|
|
1871
|
+
},
|
|
1872
|
+
onBack: () => {
|
|
1873
|
+
const current = detectGatewayInstances();
|
|
1874
|
+
if (current.length > 0) {
|
|
1875
|
+
setState({ phase: "detect" });
|
|
1876
|
+
} else {
|
|
1877
|
+
exit();
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
),
|
|
1882
|
+
state.phase === "configure-gateway-list" && /* @__PURE__ */ jsx7(
|
|
1883
|
+
ConfigureGatewayPicker,
|
|
1884
|
+
{
|
|
1885
|
+
title: state.title,
|
|
1886
|
+
onSelect: (gw) => {
|
|
1887
|
+
setState({
|
|
1888
|
+
phase: "configure-gateway-input",
|
|
1889
|
+
gateway: gw
|
|
1890
|
+
});
|
|
1891
|
+
},
|
|
1892
|
+
onCancel: () => {
|
|
1893
|
+
const detected = detectGatewayInstances();
|
|
1894
|
+
if (detected.length > 0) {
|
|
1895
|
+
setState({ phase: "gateway-manager", instances: detected });
|
|
1896
|
+
} else {
|
|
1897
|
+
exit();
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
),
|
|
1902
|
+
state.phase === "configure-gateway-input" && /* @__PURE__ */ jsx7(
|
|
1903
|
+
ConfigureGatewayInput,
|
|
1904
|
+
{
|
|
1905
|
+
gateway: state.gateway,
|
|
1906
|
+
editingInstance: state.editingInstance,
|
|
1907
|
+
onConfigured: (newValues) => {
|
|
1908
|
+
const config = loadFalconConfigV2();
|
|
1909
|
+
const inst = state.editingInstance;
|
|
1910
|
+
if (inst) {
|
|
1911
|
+
const existing = config.gateways.find((g) => g.id === inst.id);
|
|
1912
|
+
if (existing) {
|
|
1913
|
+
existing.fields = newValues;
|
|
1914
|
+
}
|
|
1915
|
+
} else {
|
|
1916
|
+
const newInstanceId = `gw-${state.gateway.slug}-${Date.now()}`;
|
|
1917
|
+
config.gateways.push({
|
|
1918
|
+
id: newInstanceId,
|
|
1919
|
+
gatewaySlug: state.gateway.slug,
|
|
1920
|
+
fields: newValues
|
|
1921
|
+
});
|
|
1922
|
+
}
|
|
1923
|
+
saveFalconConfigV2(config);
|
|
1924
|
+
setState({ phase: "detect" });
|
|
1925
|
+
},
|
|
1926
|
+
onCancel: () => {
|
|
1927
|
+
const detected = detectGatewayInstances();
|
|
1928
|
+
if (detected.length > 0) {
|
|
1929
|
+
setState({ phase: "gateway-manager", instances: detected });
|
|
1930
|
+
} else {
|
|
1931
|
+
setState({ phase: "configure-gateway-list" });
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
),
|
|
1936
|
+
state.phase === "loading-models" && /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", marginTop: 1, children: [
|
|
1937
|
+
/* @__PURE__ */ jsxs7(Text7, { children: [
|
|
1938
|
+
/* @__PURE__ */ jsx7(Text7, { color: "green", children: "\u2713" }),
|
|
1939
|
+
" Connected to",
|
|
1940
|
+
" ",
|
|
1941
|
+
/* @__PURE__ */ jsx7(Text7, { bold: true, color: "cyan", children: state.instances.map((inst) => inst.name).join(", ") })
|
|
1942
|
+
] }),
|
|
1943
|
+
/* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx7(LoadingSpinner, { text: "Fetching available models..." }) })
|
|
1944
|
+
] }),
|
|
1945
|
+
state.phase === "pick-model" && /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", marginTop: 1, children: [
|
|
1946
|
+
/* @__PURE__ */ jsxs7(Text7, { children: [
|
|
1947
|
+
/* @__PURE__ */ jsx7(Text7, { color: "green", children: "\u2713" }),
|
|
1948
|
+
" Connected to",
|
|
1949
|
+
" ",
|
|
1950
|
+
/* @__PURE__ */ jsx7(Text7, { bold: true, color: "cyan", children: state.instances.map((inst) => inst.name).join(", ") }),
|
|
1951
|
+
/* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
|
|
1952
|
+
" \u2022 ",
|
|
1953
|
+
state.models.length,
|
|
1954
|
+
" models available"
|
|
1955
|
+
] })
|
|
1956
|
+
] }),
|
|
1957
|
+
/* @__PURE__ */ jsx7(
|
|
1958
|
+
ModelPicker,
|
|
1959
|
+
{
|
|
1960
|
+
models: state.models,
|
|
1961
|
+
recentModels,
|
|
1962
|
+
showGatewayBadge: state.instances.length > 1,
|
|
1963
|
+
onSelect: (model) => {
|
|
1964
|
+
const targetInstance = model.gatewayInstance || state.instances[0];
|
|
1965
|
+
if (targetInstance) {
|
|
1966
|
+
setState({
|
|
1967
|
+
phase: "confirm",
|
|
1968
|
+
gateway: targetInstance,
|
|
1969
|
+
model,
|
|
1970
|
+
instances: state.instances,
|
|
1971
|
+
models: state.models
|
|
1972
|
+
});
|
|
1973
|
+
}
|
|
1974
|
+
},
|
|
1975
|
+
onCancel: () => exit(),
|
|
1976
|
+
onConfigure: () => {
|
|
1977
|
+
setState({ phase: "gateway-manager", instances: state.instances });
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
)
|
|
1981
|
+
] }),
|
|
1982
|
+
state.phase === "confirm" && /* @__PURE__ */ jsx7(
|
|
1983
|
+
LaunchConfirm,
|
|
1984
|
+
{
|
|
1985
|
+
agent,
|
|
1986
|
+
gateway: { name: state.gateway.name, slug: state.gateway.gateway.slug },
|
|
1987
|
+
model: state.model,
|
|
1988
|
+
onConfirm: () => launchAgent(state.gateway, state.model),
|
|
1989
|
+
onCancel: () => setState({
|
|
1990
|
+
phase: "pick-model",
|
|
1991
|
+
instances: state.instances,
|
|
1992
|
+
models: state.models
|
|
1993
|
+
})
|
|
1994
|
+
}
|
|
1995
|
+
),
|
|
1996
|
+
state.phase === "launching" && /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", marginTop: 1, children: [
|
|
1997
|
+
/* @__PURE__ */ jsxs7(Text7, { children: [
|
|
1998
|
+
/* @__PURE__ */ jsx7(Text7, { color: "green", children: "\u2713" }),
|
|
1999
|
+
" Connected to",
|
|
2000
|
+
" ",
|
|
2001
|
+
/* @__PURE__ */ jsx7(Text7, { bold: true, color: "cyan", children: state.gateway.name })
|
|
2002
|
+
] }),
|
|
2003
|
+
/* @__PURE__ */ jsxs7(Text7, { children: [
|
|
2004
|
+
/* @__PURE__ */ jsx7(Text7, { color: "green", children: "\u2713" }),
|
|
2005
|
+
" Model:",
|
|
2006
|
+
" ",
|
|
2007
|
+
/* @__PURE__ */ jsx7(Text7, { bold: true, color: "yellow", children: state.model.name })
|
|
2008
|
+
] }),
|
|
2009
|
+
/* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsxs7(Text7, { color: "magenta", bold: true, children: [
|
|
2010
|
+
"\u{1F680} Launching ",
|
|
2011
|
+
agent.name,
|
|
2012
|
+
"..."
|
|
2013
|
+
] }) })
|
|
2014
|
+
] }),
|
|
2015
|
+
state.phase === "error" && /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", marginTop: 1, children: [
|
|
2016
|
+
/* @__PURE__ */ jsx7(Text7, { color: "red", bold: true, children: "\u2716 Error" }),
|
|
2017
|
+
/* @__PURE__ */ jsx7(Text7, { color: "red", children: state.message }),
|
|
2018
|
+
/* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "Press Ctrl+C to exit" }) })
|
|
2019
|
+
] })
|
|
2020
|
+
] });
|
|
2021
|
+
}
|
|
2022
|
+
function LoadingSpinner({ text }) {
|
|
2023
|
+
const [frame, setFrame] = useState5(0);
|
|
2024
|
+
useEffect(() => {
|
|
2025
|
+
const timer = setInterval(() => {
|
|
2026
|
+
setFrame((prev) => (prev + 1) % SPINNER_FRAMES.length);
|
|
2027
|
+
}, 80);
|
|
2028
|
+
return () => clearInterval(timer);
|
|
2029
|
+
}, []);
|
|
2030
|
+
return /* @__PURE__ */ jsxs7(Text7, { children: [
|
|
2031
|
+
/* @__PURE__ */ jsx7(Text7, { color: "cyan", children: SPINNER_FRAMES[frame] }),
|
|
2032
|
+
" ",
|
|
2033
|
+
text
|
|
2034
|
+
] });
|
|
2035
|
+
}
|
|
2036
|
+
function renderApp(props) {
|
|
2037
|
+
render(React.createElement(App, props));
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
// src/cli.ts
|
|
2041
|
+
var VERSION = "0.1.0";
|
|
2042
|
+
var program = new Command().name("falcon").version(VERSION).description(
|
|
2043
|
+
chalk.magenta("\u{1F985} Falcon") + " \u2014 Launch coding agents with multi-gateway API support"
|
|
2044
|
+
);
|
|
2045
|
+
async function handleLaunch(agentName, agentArgs, options) {
|
|
2046
|
+
const extraArgs = agentArgs || [];
|
|
2047
|
+
if (!agentName) {
|
|
2048
|
+
console.log(chalk.magenta.bold("\n \u{1F985} Falcon \u2014 Available Agents\n"));
|
|
2049
|
+
for (const agent2 of ALL_AGENTS) {
|
|
2050
|
+
console.log(` ${chalk.cyan("\u2022")} ${chalk.bold(agent2.name)} ${chalk.dim(`(${agent2.slug})`)}`);
|
|
2051
|
+
}
|
|
2052
|
+
console.log();
|
|
2053
|
+
console.log(
|
|
2054
|
+
chalk.dim(" Usage: falcon launch <agent> [--model <model>] [--gateway <gateway>]\n")
|
|
2055
|
+
);
|
|
2056
|
+
return;
|
|
2057
|
+
}
|
|
2058
|
+
const agent = findAgent(agentName);
|
|
2059
|
+
if (!agent) {
|
|
2060
|
+
console.error(
|
|
2061
|
+
chalk.red(
|
|
2062
|
+
`Unknown agent: "${agentName}". Available agents: ${ALL_AGENTS.map((a) => a.slug).join(", ")}`
|
|
2063
|
+
)
|
|
2064
|
+
);
|
|
2065
|
+
process.exit(1);
|
|
2066
|
+
}
|
|
2067
|
+
if (options.dryRun) {
|
|
2068
|
+
const detected = detectGatewayInstances();
|
|
2069
|
+
const targetGateway = options.gateway;
|
|
2070
|
+
const gw = targetGateway ? detected.find(
|
|
2071
|
+
(d) => d.gateway.slug === targetGateway || d.name.toLowerCase() === targetGateway.toLowerCase()
|
|
2072
|
+
) : detected[0];
|
|
2073
|
+
if (!gw) {
|
|
2074
|
+
console.error(chalk.red("No API gateway available for dry run."));
|
|
2075
|
+
process.exit(1);
|
|
2076
|
+
}
|
|
2077
|
+
const model = options.model || "interactive-selection";
|
|
2078
|
+
const config = gw.gateway.getEnvConfig(gw.apiKey, model);
|
|
2079
|
+
const resolved = await withGatewayEnvAsync({ fields: gw.fields }, async () => {
|
|
2080
|
+
return await agent.resolveConfig(config, gw.gateway.slug, gw.apiKey, model, {
|
|
2081
|
+
dryRun: true
|
|
2082
|
+
});
|
|
2083
|
+
});
|
|
2084
|
+
const spawnConfig = withGatewayEnv({ fields: gw.fields }, () => {
|
|
2085
|
+
return agent.buildSpawnConfig(resolved, model, extraArgs);
|
|
2086
|
+
});
|
|
2087
|
+
console.log(chalk.bold("\nDry Run Configuration:\n"));
|
|
2088
|
+
console.log(` Agent: ${chalk.green(agent.name)}`);
|
|
2089
|
+
console.log(` Gateway: ${chalk.cyan(gw.name)}`);
|
|
2090
|
+
console.log(` Model: ${chalk.yellow(model)}`);
|
|
2091
|
+
console.log(` Command: ${chalk.dim(`${spawnConfig.command} ${spawnConfig.args.join(" ")}`)}`);
|
|
2092
|
+
console.log(chalk.bold("\n Environment:"));
|
|
2093
|
+
for (const [key, value] of Object.entries(spawnConfig.env)) {
|
|
2094
|
+
const masked = maskString(value);
|
|
2095
|
+
console.log(` ${chalk.dim(key)}=${chalk.dim(masked)}`);
|
|
2096
|
+
}
|
|
2097
|
+
console.log();
|
|
2098
|
+
return;
|
|
2099
|
+
}
|
|
2100
|
+
renderApp({
|
|
2101
|
+
agent,
|
|
2102
|
+
preselectedModel: options.model,
|
|
2103
|
+
preselectedGateway: options.gateway,
|
|
2104
|
+
extraArgs
|
|
2105
|
+
});
|
|
2106
|
+
}
|
|
2107
|
+
program.command("launch").argument("[agent]", "Agent to launch (codex, claude)").argument("[agentArgs...]", "Arguments to pass to the agent").option("-m, --model <model>", "Model to use").option(
|
|
2108
|
+
"-g, --gateway <gateway>",
|
|
2109
|
+
"API gateway to use (openrouter, openai, anthropic, cloudflare)"
|
|
2110
|
+
).option("--dry-run", "Show what would be launched without actually launching").option("--list-gateways", "List detected API gateways and exit").allowUnknownOption(true).description("Launch a coding agent with a specific model via an API gateway").action(
|
|
2111
|
+
async (agentName, agentArgs, options) => {
|
|
2112
|
+
if (options.listGateways) {
|
|
2113
|
+
const detected = detectGatewayInstances();
|
|
2114
|
+
if (detected.length === 0) {
|
|
2115
|
+
console.log(chalk.red("No API keys detected."));
|
|
2116
|
+
console.log(
|
|
2117
|
+
chalk.dim(
|
|
2118
|
+
"Set one of: OPENROUTER_API_KEY, OPENAI_API_KEY, ANTHROPIC_API_KEY, CLOUDFLARE_API_KEY"
|
|
2119
|
+
)
|
|
2120
|
+
);
|
|
2121
|
+
process.exit(1);
|
|
2122
|
+
}
|
|
2123
|
+
console.log(chalk.bold("\nDetected API Gateways:\n"));
|
|
2124
|
+
for (const { name, apiKey, isEnv } of detected) {
|
|
2125
|
+
const masked = maskString(apiKey);
|
|
2126
|
+
const typeStr = isEnv ? chalk.magenta(" [Env] [read-only]") : "";
|
|
2127
|
+
console.log(
|
|
2128
|
+
` ${chalk.green("\u2713")} ${chalk.bold(name)} ${chalk.dim(`(${masked})`)}${typeStr}`
|
|
2129
|
+
);
|
|
2130
|
+
}
|
|
2131
|
+
console.log();
|
|
2132
|
+
return;
|
|
2133
|
+
}
|
|
2134
|
+
await handleLaunch(agentName, agentArgs, options);
|
|
2135
|
+
}
|
|
2136
|
+
);
|
|
2137
|
+
for (const agent of ALL_AGENTS) {
|
|
2138
|
+
program.command(agent.slug).argument("[agentArgs...]", "Arguments to pass to the agent").option("-m, --model <model>", "Model to use").option(
|
|
2139
|
+
"-g, --gateway <gateway>",
|
|
2140
|
+
"API gateway to use (openrouter, openai, anthropic, cloudflare)"
|
|
2141
|
+
).option("--dry-run", "Show what would be launched without actually launching").allowUnknownOption(true).description(`Launch ${agent.name} with a specific model via an API gateway`).action(
|
|
2142
|
+
async (agentArgs, options) => {
|
|
2143
|
+
await handleLaunch(agent.slug, agentArgs, options);
|
|
2144
|
+
}
|
|
2145
|
+
);
|
|
2146
|
+
}
|
|
2147
|
+
program.command("models").option("-g, --gateway <gateway>", "API gateway to query").description("List available models from detected API gateways").action(async (options) => {
|
|
2148
|
+
const detected = detectGatewayInstances();
|
|
2149
|
+
if (detected.length === 0) {
|
|
2150
|
+
console.error(chalk.red("No API keys detected."));
|
|
2151
|
+
process.exit(1);
|
|
2152
|
+
}
|
|
2153
|
+
const targetGateway = options.gateway;
|
|
2154
|
+
const target = targetGateway ? detected.find(
|
|
2155
|
+
(d) => d.gateway.slug === targetGateway || d.name.toLowerCase() === targetGateway.toLowerCase()
|
|
2156
|
+
) : detected[0];
|
|
2157
|
+
if (!target) {
|
|
2158
|
+
console.error(chalk.red(`Gateway "${options.gateway}" not found or no key set.`));
|
|
2159
|
+
process.exit(1);
|
|
2160
|
+
}
|
|
2161
|
+
console.log(chalk.dim(`
|
|
2162
|
+
Fetching models from ${target.name}...
|
|
2163
|
+
`));
|
|
2164
|
+
try {
|
|
2165
|
+
const models = await withGatewayEnvAsync({ fields: target.fields }, async () => {
|
|
2166
|
+
return await target.gateway.listModels(target.apiKey);
|
|
2167
|
+
});
|
|
2168
|
+
console.log(chalk.bold(`${target.name} \u2014 ${models.length} models:
|
|
2169
|
+
`));
|
|
2170
|
+
for (const model of models.slice(0, 50)) {
|
|
2171
|
+
const ctx = model.contextLength ? chalk.dim(` (${formatCtx(model.contextLength)})`) : "";
|
|
2172
|
+
const price = model.pricing ? chalk.green(` ${model.pricing.prompt}`) : "";
|
|
2173
|
+
console.log(` ${chalk.cyan(model.id)}${ctx}${price}`);
|
|
2174
|
+
}
|
|
2175
|
+
if (models.length > 50) {
|
|
2176
|
+
console.log(
|
|
2177
|
+
chalk.dim(
|
|
2178
|
+
`
|
|
2179
|
+
... and ${models.length - 50} more. Use 'falcon launch' for interactive search.
|
|
2180
|
+
`
|
|
2181
|
+
)
|
|
2182
|
+
);
|
|
2183
|
+
}
|
|
2184
|
+
console.log();
|
|
2185
|
+
} catch (err) {
|
|
2186
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2187
|
+
console.error(chalk.red(`Failed to fetch models: ${message}`));
|
|
2188
|
+
process.exit(1);
|
|
2189
|
+
}
|
|
2190
|
+
});
|
|
2191
|
+
program.parse();
|