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.
Files changed (3) hide show
  1. package/README.md +181 -0
  2. package/dist/cli.js +2191 -0
  3. 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();